Pular para o conteúdo principal

Jobs e tentativas de treinamento

Treinamento é trabalho assíncrono em GPU. A API persiste um job, envia um snapshot imutável de entrada e registra uma ou mais tentativas duráveis. O status do job é o estado do produto; o status da tentativa explica o que o worker ou o plano de controle está fazendo.

Endpoints de jobs

GET /api/projects/{owner_slug}/{project_slug}/training-jobs
POST /api/projects/{owner_slug}/{project_slug}/training-jobs
GET /api/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}
POST /api/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}/cancel
POST /api/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}/retry
GET /api/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}/attempts
GET /api/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}/attempts/{attempt_id}/logs

A lista mantém a resposta antiga em array para clientes que não optarem pelo novo modo. As telas novas devem usar cursores:

GET /api/projects/{owner_slug}/{project_slug}/training-jobs?pagination=cursor&limit=25
GET /api/projects/{owner_slug}/{project_slug}/training-jobs?pagination=cursor&limit=25&cursor=<next_cursor>

As páginas retornam {data, next_cursor, total_count}. O limite padrão é 25 e o máximo é 100; a autorização é aplicada antes da contagem exata do projeto e da página por busca. Os cursores são opacos e devem ser enviados de volta sem alteração. Um cursor ou limite sem pagination=cursor retorna 400, assim como um cursor malformado.

Crie um job com tags de dataset e o modelo do catálogo:

{
"dataset_tags": ["support-clean"],
"model_name": "unsloth/Qwen3.5-9B",
"max_seq_length": 2048,
"num_train_epochs": 3
}

num_train_epochs define a duração do treino: quantas passagens completas ele faz sobre os exemplos selecionados. O servidor deriva quantos passos de otimização isso custa, a partir do tamanho da seleção e do formato do lote, e congela esse número no job como planned_steps. Toda configuração omitida é expandida a partir do perfil de fine-tune do modelo base e congelada junto, de modo que um job lido depois informa exatamente com o que rodou.

No app, a estimativa explicita essa conversão como passos planejados, passos por época e épocas. Épocas não são outro nome para passos: uma época é uma passagem completa pelos exemplos de treino, enquanto um passo do otimizador processa um lote efetivo.

A conversão exata é:

effective_batch_size = per_device_train_batch_size × gradient_accumulation_steps
steps_per_epoch = ceil(train_examples / effective_batch_size)
planned_steps = steps_per_epoch × num_train_epochs

Por exemplo, 152 exemplos de treino com o lote efetivo padrão de 8 custam ceil(152 / 8) = 19 passos do otimizador por época, ou 57 passos planejados em 3 épocas. Um lote parcial no fim ainda conta como um passo do otimizador.

Outras configurações que você pode enviar: system_prompt, learning_rate, lora_rank, lora_alpha, per_device_train_batch_size, gradient_accumulation_steps, loss_mask (assistant_only ou full_sequence), base_load_precision (4bit ou 8bit), warmup_steps e early_stopping_patience. Um valor recusado retorna 400 com um code e o field correspondente.

system_prompt é uma instrução única para o job inteiro, armazenada uma só vez e adicionada como a primeira mensagem de sistema na formatação de cada exemplo. Ela nunca afeta a inferência — um modelo hospedado continua respondendo exatamente às messages que você enviar. Se algum exemplo selecionado já tiver a própria mensagem de sistema, criar o job com system_prompt retorna 422 e informa quantos conflitam, em vez de mesclar dois conjuntos de instruções.

max_steps continua funcionando e continua significando o mesmo: um teto fixo de passos de otimização, entre 1 e 100. Envie ele ou num_train_epochs, nunca os dois — uma requisição com ambos é recusada.

Use GET /api/base-models para consultar modelos, os limites padrão e os profiles por modelo com seus intervalos.

Avaliação e parada antecipada

Se a seleção for grande o suficiente, uma pequena parte vira exemplos de avaliação e nunca é usada no treino. A execução avalia contra esses exemplos na mesma cadência em que grava checkpoints, e o modelo entregue é o checkpoint com o melhor resultado nessa avaliação, não o do último passo.

Quais exemplos são usados para avaliação é uma propriedade do seu dataset, não do job: todo job executado sobre um dataset inalterado usa exatamente os mesmos exemplos de avaliação. É isso que torna duas execuções comparáveis — uma perda de avaliação menor depois de aumentar o lora_rank significa que a mudança ajudou, e não que o segundo job fez uma prova mais fácil. Exemplos adicionados depois entram no lado que a identidade deles determinar; nada já atribuído muda de lado.

A política atual tenta destinar 5% à avaliação, exige pelo menos 8 exemplos de avaliação e limita esse conjunto a 512:

candidate_eval = floor(selected_examples × 0.05)
eval_examples = 0 if candidate_eval < 8 else min(candidate_eval, 512)
train_examples = selected_examples - eval_examples

É por isso que a avaliação começa com exatamente 160 exemplos selecionados: 5% de 159 arredonda para 7, enquanto 5% de 160 é 8. Uma seleção menor treina com todos os exemplos e simplesmente não informa perda de avaliação. Os limites 5% / 8 / 512 são uma política conservadora do produto, não um ótimo estatístico comprovado; a tarefa restante de benchmark dos perfis precisa medi-los antes de tratá-los como padrões calibrados.

A parada antecipada fica desligada, a menos que você peça. Envie early_stopping_patience (1 ou mais) para que a execução pare quando a perda de avaliação deixar de melhorar por essa quantidade de avaliações. O padrão é desligado porque parar antes entrega uma execução mais curta do que a que você pediu, e num conjunto de avaliação pequeno essa perda é ruidosa o bastante para que a decisão seja sua. Um job que para antecipadamente termina com status: "succeeded", antes de planned_steps — isso é concluído, não falho.

Um job concluído que avaliou informa um bloco evaluation: a melhor perda e o passo em que ocorreu, a última perda e o último passo, quantas avaliações rodaram e qual checkpoint está sendo servido. Quando o checkpoint servido não é o da avaliação mais recente, promoted_before_last_eval vem true — então um job de 74 passos que serve o checkpoint do passo 50 diz isso. Um job que nunca avaliou informa evaluation: null, e não zeros.

O detalhe do job mostra esses valores e um gráfico da perda de avaliação. Com menos de 160 exemplos selecionados não existe gráfico de avaliação, pois nenhum exemplo foi destinado à avaliação.

Por que uma execução terminou

A resposta da API de um job concluído pode trazer um bloco outcome com dois campos.

stop_reason é um entre completed, max_steps, early_stopping ou cancelled. É o que distingue uma execução que parou sozinha de uma que você parou — as duas terminam antes de planned_steps, e a contagem de passos sozinha não separa uma da outra.

downgrades é o registro operacional do worker para ajustes que o plano de execução não conseguiu usar, como tokens ajuste:ação:causa — por exemplo early_stopping:disabled:no_eval_set, quando você pediu parada antecipada sobre uma seleção pequena demais para produzir exemplos de avaliação. Ele também pode conter a adaptação de um ajuste do perfil em um job antigo. Por isso, o card de Desfecho no app é uma superfície de exceção: mostra parada antecipada, cancelamento, mudanças em campos realmente enviados por você e tokens desconhecidos; não repete uma conclusão rotineira por max_steps nem apresenta uma adaptação interna do perfil como uma mudança no seu pedido. O registro bruto continua disponível na API para auditoria. outcome: null significa que a execução não informou nada — todo job concluído antes deste campo existir e qualquer job que nunca chegou ao laço de treino.

Com o que um job treinou

O detalhe de um job traz os primeiros 50 exemplos com que ele treinou, com chat_count e message_count dando os totais reais e chats_truncated informando quando a lista é apenas uma prévia. O resto é paginado:

GET /api/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}/dataset?offset=0&limit=25
GET /api/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}/dataset?offset=0&limit=25&search=fatura&split=eval

Retorna items (id do chat, nome, contagem de mensagens e participação congelada em train/eval), total, offset e limit, além de has_more, next_offset, search_applied e split, até 200 por página. search procura no título congelado ou no texto das mensagens; split aceita all, train ou eval e usa a divisão congelada para aquela execução. Uma busca retorna total: null para o servidor não percorrer todos os shards imutáveis duas vezes apenas para desenhar uma contagem de páginas. As mensagens de um exemplo, exatamente como o job as usou, vêm de:

GET /api/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}/dataset/{chat_id}

Ambos respondem a partir do registro imutável do qual o job treinou, e não das suas conversas atuais. Se você editou ou apagou alguma delas depois, estes endpoints continuam mostrando o que a execução realmente usou, que é justamente o motivo de congelar isso.

Preparação

Um job criado não começa a treinar imediatamente. Os exemplos selecionados são primeiro copiados, exatamente como estão naquele momento, para um registro imutável que a execução lê — assim, editar ou excluir esses chats depois não muda no que o job treinou, e repetir um job reexecuta os mesmos dados em vez do que os dados se tornaram desde então.

O job informa isso em preparation, que passa por queued, snapshotting e então um de verified, failed ou cancelled. O treino começa somente depois de verified.

Falhas de preparação são falhas de entrada: nenhuma GPU foi alocada, então o job termina com failure_category: "input" e não é cobrado. O mesmo vale para cancelar enquanto o job ainda está em preparação — nada rodou, nada é cobrado.

Um job criado antes deste recurso informa preparation: null, o que significa que a etapa não se aplicava a ele, e não que ela falhou.

Números de jobs e IDs da API

Cada resposta de job tem dois identificadores. number é o número cronológico iniciado em um dentro do projeto e é o valor exibido pela interface do ReOpenly. id é o identificador operacional global do banco, mantido para mutações da API, artefatos e recursos relacionados. Para buscar um job pelo número exibido, use:

GET /api/projects/{owner_slug}/{project_slug}/training-jobs/{number}?identifier=number

Jobs de treinamento são registros imutáveis e não podem ser apagados, portanto os números locais do projeto não mudam. Respostas de modelos hospedados e de cobrança expõem o mesmo valor como training_job_number ao referenciar um job.

Admissão de duração e preços

A admissão de fine-tune usa a projeção operacional training_workload_seconds + 1.800 segundos de tolerância para pós-processamento na GPU. O ReOpenly rejeita uma projeção acima do teto de execução da plataforma antes de criar o job ou enviar trabalho ao provider. O teto é de 21.600 segundos (seis horas) por padrão e é uma configuração da plataforma, não uma constante fixa — leia-o em duration_components.execution_limit_seconds na resposta de POST /api/pricing/estimate em vez de fixá-lo no código. Esse é o limite do produto ReOpenly, independente do limite do provedor de infraestrutura. Com o teto padrão, 100 steps projetam 5.250 segundos na sequência 512 e 14.700 na 2.048; a sequência 4.096 aceita 76 steps (21.540) e rejeita 77 (21.780).

A criação retorna 422 e o retry explícito retorna 409 com o código tipado training_duration_exceeds_limit. A resposta pública de POST /api/pricing/estimate informa expected usando os segundos ativos projetados, authorization_ceiling usando separadamente a margem da autorização de créditos e duration_components para workload, tolerância de pós-processamento, segundos ativos projetados e limite de execução. Fila, cold-start e finalização central ficam fora da cobrança de GPU do cliente.

Admissão do dataset

dataset_tags deve conter nomes exatos de tags que já existem. Valores vazios, não string ou desconhecidos retornam o erro tipado training_dataset_selection_invalid. Nomes duplicados são removidos mantendo a primeira ocorrência, e várias tags selecionam a união dos chats. Uma lista de tags vazia seleciona todos os chats do projeto.

O snapshot imutável mantém todas as mensagens selecionadas, mas o treinador mantém apenas mensagens cujo texto não é vazio. A criação rejeita uma seleção vazia com training_dataset_empty, ou uma seleção sem texto treinável com training_dataset_unusable, antes de criar o job ou cobrar créditos. Essas respostas são 422 na criação e 409 na retomada; a retomada não altera o job nem cria uma tentativa. O endpoint de estimativa informa selected_chats, selected_messages, trainable_chats e nonempty_text_messages, retornando is_trainable: false para seleções vazias ou inutilizáveis.

As respostas de lista e detalhe incluem cost_credits, o gasto real positivo agregado das linhas de treinamento no ledger de créditos. Um job pendente, falho ou legado sem linha de cobrança retorna 0; a API nunca estima gasto histórico usando a taxa de hoje.

A resposta de detalhe também inclui uma projeção timing quando o job tem timestamps suficientes do ciclo de vida:

{
"total_seconds": 540,
"queued_seconds": 18,
"preparing_seconds": 211,
"training_seconds": 154.2,
"finalizing_seconds": 156.8,
"attempt_count": 1
}

total_seconds é o tempo de parede entre a criação e a conclusão terminal. Os valores respaldados por timestamp cobrem fila, preparação, treinamento e finalização central. O mapeamento de fases ao vivo da UI também inclui post_processing, projetado por dispatch_status enquanto o worker salva e envia o adaptador; este contrato não persiste uma coluna de fase separada para ele. Para um retry, o tempo na fila começa na criação do dispatch dessa tentativa (ou no pedido de retry quando o timestamp da tentativa não está disponível). O intervalo entre uma tentativa terminal e um retry explícito continua em total_seconds, mas não é rotulado incorretamente como fila. training_seconds é o loop informado pelo treinador. Uma fase é omitida ou null quando os timestamps não permitem uma medição honesta, como em tentativas antigas anteriores à adição de training_started_at. Esses tempos não são o intervalo cobrado: a cobrança usa o intervalo de GPU ativa started_at até finished_at de cada tentativa.

Estados do job

EstadoSignificado
pendingAceito e aguardando dispatch/capacidade. Se ainda não tem tentativa ativa, um stop é encerrado localmente sem contactar o provider.
runningUma tentativa de fine-tune executa, o pós-processamento na GPU está em andamento ou a finalização central está em andamento.
cancel_requestedO proprietário pediu cancelamento cooperativo durante o trabalho.
succeededO artefato final foi verificado e promovido.
failedA geração atual terminou sem artefato promovido.
cancelledO proprietário parou a execução, ou não havia mais trabalho para a geração. Uma execução que já havia treinado mantém o adaptador que produziu; veja Cancelamento.

Não existe uma etapa pública de evaluation, deployment ou timer. Um artefato está pronto para download ou hospedagem quando model_available é verdadeiro, o que cobre um job bem-sucedido e um job parado que já havia treinado. O status do job não é o sinal: model_output_path é atribuído na criação, então todo job carrega um. Um job parado pode, portanto, ser baixado e hospedado quando model_available é verdadeiro, mesmo com status terminal cancelled.

Estados da tentativa

O endpoint de tentativas do projeto retorna id, attempt_number, status, timestamps, código de erro/falha e metadados limitados do log. Identificadores de infraestrutura e diagnósticos ficam na superfície de operador.

EstadoSignificado
leasedO plano de controle reservou um slot de worker/provider.
acceptedUm worker de fine-tune aceitou a execução e retornou sua identidade.
startedO worker iniciou a operação real.
finalizingUm fine-tune entregou o manifesto imutável para verificação/promoção central.
succeededA tentativa terminou com sucesso.
failedA tentativa terminou com código/categoria de falha.
cancelledO cancelamento cooperativo terminou. Se a execução havia treinado, o artefato ainda é verificado e promovido.
expiredO lease ou heartbeat do worker expirou; a reconciliação bloqueia a tentativa.
submission_uncertainO plano de controle não conseguiu provar se o envio remoto foi aceito; ele não cria um duplicado às cegas.

Tentativas de inferência normalmente seguem leased → started → succeeded ou failed. Tentativas de fine-tune podem seguir leased → accepted → started → finalizing → succeeded; enquanto a tentativa permanece started, sua projeção de dispatch pode ser brevemente post_processing antes da entrega do manifesto. A perda do worker segue um caminho terminal sem promover artefato. Um cancelamento que produziu um adaptador chega a finalizing como qualquer outra execução e promove o que treinou. Um cancelamento antes da produção do adaptador termina sem artefato.

Cancelamento

POST .../cancel é cooperativo e é aceito desde o momento em que uma tentativa é reservada até ela entrar em finalização: o job está pending ou running, a tentativa de fine-tune está em leased, accepted ou started e o progresso é menor que 100. Isso cobre toda a preparação — download do modelo, obtenção do dataset, tokenização —, que num worker frio é a parte mais longa da execução e antes não podia ser cancelada. Um job pending sem nenhuma tentativa é encerrado localmente de forma idempotente (não há trabalho do provider para parar). post_processing e finalizing retornam 409 (training_job_not_in_training; finalizing mantém o erro mais específico training_job_finalizing), porque nesse ponto a GPU já foi liberada e o adaptador já foi enviado. Uma vez aceito, o job vira cancel_requested, o worker recebe o pedido e o plano de controle registra a intenção de cancelamento no provider depois do commit. Cancelar não cria uma nova tentativa.

Depois que o worker emite post_processing, ele conclui checkpoint, serialização do adaptador, envio e entrega do manifesto mesmo que chegue um pedido de cancelamento atrasado. Um cancelamento solicitado durante o treinamento ainda pode produzir o adaptador descrito abaixo; o pós-processamento nunca pode ser cancelado.

Parar mantém o que a execução aprendeu, e isso é cobrado. O treinador conclui o passo atual, salva e envia um adaptador completo, que é verificado e promovido como qualquer outro. O job continua cancelled — o status registra que você o parou, não que nada saiu dele — e o tempo de GPU usado é cobrado na tarifa normal. Uma execução parada antes de treinar qualquer coisa não produz artefato e não é cobrada.

O mesmo contrato model_available controla a inferência direta de training-job#<id> e o serving hospedado. Um job parado é válido quando sua tentativa de fine-tune está terminalmente cancelled e seu adaptador foi promovido; cancelled sem artefato promovido não pode ser servido.

Na finalizing central, a GPU já foi liberada e o artefato já foi enviado, então não há mais nada para parar; a verificação e a promoção continuam.

Reconciliação do provider e motivos de falha

Em um fine-tune gerenciado baseado em fila, o plano de controle confiável — não o navegador nem o worker isolado — consulta /status/{id} e, depois de um pedido de stop durante Training, /cancel/{id}. A reconciliação roda no ciclo de dispatch de 30 segundos, usa timeout de 10 segundos, no máximo três chamadas concorrentes e retries limitados para transporte, respostas 429 e 5xx. Aceita somente IN_QUEUE, IN_PROGRESS, RUNNING, COMPLETED, FAILED, CANCELLED e TIMED_OUT. A saída do provider nunca é artefato nem erro exposto ao cliente; apenas um callback assinado do ReOpenly com manifesto verificado entra na finalização. Os resultados do provider ficam retidos por até 30 minutos para a reconciliação assíncrona.

Uma observação ativa sem callback assinado atual recebe uma única janela de 120 segundos. O primeiro 404 ou erro transitório de status/autenticação recebe uma janela limitada de 60 segundos; uma segunda falha ou TTL expirado encerra a tentativa. A reconciliação nunca cria retry automático ou tentativa substituta. O cliente pode iniciar um novo retry manual somente quando o job terminal for elegível.

Os códigos públicos de falha são estáveis e não contêm traceback ou saída bruta do provider:

CódigoSignificado
provider_failedO provedor de compute informou uma execução falha.
provider_execution_timeoutUma execução ativa excedeu seu limite.
provider_ttl_expiredO TTL de fila/execução do provider expirou.
provider_callback_missingO trabalho ativo não foi confirmado por callback assinado.
provider_terminal_callback_missingO provedor de compute concluiu, mas não chegou manifesto assinado.
provider_result_unavailableO resultado sumiu duas vezes ou após retenção/TTL.
provider_status_unavailableConsultas repetidas não conseguiram confirmar o estado.
provider_cancelledO provider cancelou sem pedido de stop do ReOpenly.

Retry explícito

Um job failed ou cancelled pode ser repetido, sempre por iniciativa do proprietário. Envie um Idempotency-Key único:

curl -X POST "${REOPENLY_API_ROOT}/training-jobs/42/retry" \
-H "Authorization: Bearer ${REOPENLY_API_KEY}" \
-H "Idempotency-Key: 8f8d8ac4-8de5-4c63-a8f4-f0a39db1af4d" \
-H "Content-Type: application/json" \
-d '{"resume_from_checkpoint": false}'

O retry reutiliza os argumentos imutáveis, o snapshot do dataset e a raiz canônica de saída. Com resume_from_checkpoint: true, o plano de controle exige um checkpoint verificado e compatível. Retries automáticos de fine-tune não fazem parte do contrato.