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
| Estado | Significado |
|---|---|
pending | Aceito e aguardando dispatch/capacidade. Se ainda não tem tentativa ativa, um stop é encerrado localmente sem contactar o provider. |
running | Uma tentativa de fine-tune executa, o pós-processamento na GPU está em andamento ou a finalização central está em andamento. |
cancel_requested | O proprietário pediu cancelamento cooperativo durante o trabalho. |
succeeded | O artefato final foi verificado e promovido. |
failed | A geração atual terminou sem artefato promovido. |
cancelled | O 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.
| Estado | Significado |
|---|---|
leased | O plano de controle reservou um slot de worker/provider. |
accepted | Um worker de fine-tune aceitou a execução e retornou sua identidade. |
started | O worker iniciou a operação real. |
finalizing | Um fine-tune entregou o manifesto imutável para verificação/promoção central. |
succeeded | A tentativa terminou com sucesso. |
failed | A tentativa terminou com código/categoria de falha. |
cancelled | O cancelamento cooperativo terminou. Se a execução havia treinado, o artefato ainda é verificado e promovido. |
expired | O lease ou heartbeat do worker expirou; a reconciliação bloqueia a tentativa. |
submission_uncertain | O 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ódigo | Significado |
|---|---|
provider_failed | O provedor de compute informou uma execução falha. |
provider_execution_timeout | Uma execução ativa excedeu seu limite. |
provider_ttl_expired | O TTL de fila/execução do provider expirou. |
provider_callback_missing | O trabalho ativo não foi confirmado por callback assinado. |
provider_terminal_callback_missing | O provedor de compute concluiu, mas não chegou manifesto assinado. |
provider_result_unavailable | O resultado sumiu duas vezes ou após retenção/TTL. |
provider_status_unavailable | Consultas repetidas não conseguiram confirmar o estado. |
provider_cancelled | O 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.