Documentação

Executar uma sessão de investigação sem supervisão

Este é o programa operativo de uma sessão de investigação que corre sem ninguém a vigiar: uma sessão noturna, ou uma longa passagem de testemunho durante o dia. Substitui os prompts por noite (os programas da ronda 6 e da noite 3, mantidos na tag git research-archive-2026-09-24 em docs/prompts/) e incorpora o que correu mal nessas noites. As regras de investigação que aplica estão no PLAN.md, “Standing rules for every round”; os comandos estão no AGENTS.md.

Uma sessão faz hill-climbing e confirma segundo regras registadas e deixa um registo sobre o qual o Jared pode agir. Não publica.

1. Arrancar

Passa os primeiros 30 minutos a ler, não a lançar.

  1. AGENTS.md, todo (comandos, suites congeladas, localizações canónicas, definições do Modal).
  2. PLAN.md: onde estamos, o que aprendemos, as regras permanentes, a política de dados e Next. Para um resultado sobre o qual queres construir, lê a sua prova no arquivo: git show research-archive-2026-09-24:PLAN.md.
  3. As skills em .agents/skills/: kev-modal-study (lançar, vigiar e recolher trabalho de GPU; lê os seus Gotchas), kev-verify (provar que uma alteração de código não tem regressões), kev-pr-description (antes de qualquer PR), thermonuclear-code-review.
  4. kev/rounds.py (a sua docstring é o esquema da especificação), a especificação passada mais próxima em experiments/rounds/, e kev/autoresearch.py (session).
  5. A própria passagem de testemunho: autorização (dólares do Modal, dólares do AI Gateway), o que está no âmbito, o que precisa do Jared.

Depois prepara:

  • Trabalha numa worktree num ramo de investigação (git worktree add -b research/<session> /tmp/kev-<session> origin/main). Faz push após cada commit para que nada se perca se a máquina dormir. O código destinado ao main passa pelo seu próprio PR revisto.
  • Lê uv run modal billing summary --json e registra metered_cost como a linha de base no ficheiro de estado (secção 6).
  • Se uma sessão anterior deixou um ficheiro de estado, lê-o primeiro e retoma a partir dele; os trabalhos destacados do Modal continuam a correr sem ti.

2. Orçamentos e a regra de despesa

  • A autorização é o total da sessão, contando tudo o que ainda está a correr. Antes de cada lançamento, lê de novo o custo medido e não lances se (metered_now - baseline) + sum(admission bounds of everything still running) >= authorization.
  • O limite de admissão de um estudo é impresso no lançamento e guardado em runs/<study>.spawn.json; o limite de uma chamada de benchmark é compute_bound(gpu, timeout, trials) (kev/budget.py). O budget do estudo de uma especificação tem de ser pelo menos o seu limite (o kev.rounds validate verifica-o; o modal_app.admit_study recusa um estudo acima do seu orçamento antes de qualquer coisa correr, e um estudo tem um teto de $250 e 28,800 s).
  • Mantém uma reserva (cerca de 10 % da autorização) que nenhuma fase inclui no plano: as leituras de faturação atrasam-se e são revistas, e os limites de admissão sobrestimam muito as leituras (um lote de leitura carrega o timeout do seu trabalho mais lento).
  • Regista cada leitura com a sua hora UTC no ficheiro de estado. A despesa do AI Gateway (leituras de referência do Jev, juízes de etiquetas) tem o seu próprio teto, imposto pelo script que a gasta, e é registada em runs/<name>/usage.json.
  • O limite de despesa do espaço de trabalho do Modal só pode ser aumentado a partir do painel; atingi-lo mata os contentores em execução a meio do treino.

3. Registar uma ronda

Uma ronda é uma secção do PLAN.md mais uma especificação, commitadas em conjunto antes de qualquer treino ou leitura.

  1. Escreve a secção do PLAN.md: porquê (a lacuna medida e a sua prova), os dados (congelados primeiro, com manifestos), os braços, a regra (primária, guardas com limiares dimensionados a cada suite, classificação), as etapas de confirmação e o orçamento. Usa as regras permanentes; não inventes uma estatística nova para uma ronda.
  2. Escreve experiments/rounds/r<N>.json copiando a especificação passada mais próxima (r15 para um delta conjunto, r17 para um 27B, r10 para uma ronda de skills, r20 para braços post-hoc sem treino: um pool de temperaturas, checkpoints interpolados; r23 para misturas para outro checkpoint, cujos braços nomeiam trained_on para o treino de ambos os extremos). Omita "archive": essa chave marca as rondas registadas 5-18. Cada ficheiro de plano que a especificação nomeie, e cada leitura pai de que a sua regra precisa, têm de existir neste checkout; se faltar uma leitura a um pai, launch-reads <spec> --parents fá-la. Elimina cada leitura de uma suite removida (kev.suite.REMOVED_SUITES, com o motivo): evals/external/scienthoon-v1 foi removida em 2026-09-27, por isso a partir da ronda 23 a leitura, o painel e a guarda do scienthoon saem; evals/external/wanli-v2 e typesafe-v1 foram removidas em 2026-09-30, por isso a partir da ronda 27 as suas leituras também saem, e o SemIf é a única leitura externa que resta (apenas para reporte). Os externos agrupados não são uma guarda: a regra auditada da ronda 24, que as rondas 23-26 seguem, reportava o SemIf, o WANLI-v2 e o TypeSafe como painéis opcionais. O validate e o launch recusam uma ronda que ainda nomeie uma suite depois da sua última ronda.
  3. Dados novos são um novo diretório em evals/ com um manifest.json (sha256 por ficheiro, hashes das entradas). Sob a política de dados de SFT (PLAN.md), os corpora privados mantêm apenas o manifesto no git, com uma entrada "mirror" a apontar para o conjunto de dados privado.
  4. OBRIGATÓRIO: cada temperatura servida ou lançada vem de um pool de conjuntos de dados reservados, nunca de uma partição do corpus de treino. Uma ronda que lê calibração (ECE, Brier, erros confiantes, cobertura) registra um pool temperature para os seus braços (copia o r20: as oito fontes públicas reservadas da partição de calibração do transfer-r3 + o MMLU-Pro do transfer-v9), e um lançamento distribui a temperatura que o scripts/calibrate_checkpoint.py ajusta no mesmo pool. Os itens reservados das fontes de treino (as partições calibration / development de uma suite de treino) estão em distribuição: a ronda 19 serviu os seus braços de SFT a T 0.955 ajustado em linhas de desenvolvimento do sft-v1 e falhou todos os critérios de calibração (breadth-v1 ECE 0.059); o pool de conjuntos de dados reservados da ronda 20 deu 0.0085 no mesmo checkpoint. O que o impõe:
    • A partir da ronda 21, o kev.rounds validate e o launch recusam uma ronda cuja regra ou confirmação tenha um critério que a temperatura move (ECE, Brier, NLL, erros confiantes, cobertura; tudo menos precisão) e nenhum pool temperature. As rondas <= 20 apenas imprimem um !!! warning por braço treinado num corpus de treino, por isso as suas especificações registadas ainda validam.
    • O kev.rounds validate recusa uma leitura de pool que (a) seja a suite de treino de um braço, um componente dela (os inputs.components do sft-v1) ou a suite data do seu plano, (b) agrupe uma fonte em que qualquer braço treinou, ou (c) leia a partição calibration ou development de qualquer corpus de treino; também recusa um pool que não consegue verificar (um braço cujo treino é desconhecido, uma suite sem manifesto ou fontes listadas). Os braços de checkpoint sem ensaio podem nomear trained_on.
    • A leitura registra o temperature_source de cada braço; a tabela imprime !!! para um braço servido nas linhas de desenvolvimento de um corpus de treino do seu ensaio (as rondas 5-19 foram todas; a partir de agora uma tal temperatura é apenas de triagem).
    • O scripts/calibrate_checkpoint.py recusa os mesmos conjuntos de ajuste (verificados contra a suite de treino do head.pt); o --allow-in-distribution serve apenas para reproduzir um ajuste antigo, e é registado em head.pt["temperature_fit"].
    • A temperatura dentro do ensaio (o calibration_fit do result.json) diz role: in-trial screening ... not a served or shipped temperature.
    • Os pais são servidos à temperatura ajustada nas linhas de desenvolvimento do seu ensaio (para o Kev-27B é a sua 1.38 de distribuição, ajustada nas mesmas linhas); a leitura registra isso e a sua T de distribuição no head.pt (parent_temperature_source), e o validate avisa quando as duas diferem em mais de 0.05 nas linhas de um corpus de treino.
    • A verificação de disjunção é por NOME de fonte (nominal, não semântica): duas suites que carreguem o mesmo conjunto de dados com nomes diferentes passam-na. Por isso um pool tem de usar fontes que são apenas de avaliação no Kev por construção, como as oito fontes públicas reservadas do transfer-r3 e o MMLU-Pro do transfer-v9. A allowlist sources de uma leitura de pool tem de nomear fontes que a sua suite lista (um erro ortográfico é um problema), e um treino que o verificador não consegue listar (um ficheiro data fora de evals/, um manifesto sem fontes) é um problema para uma ronda nova.
    • calibrate_checkpoint.py --temperature T (um valor manual, nada ajustado) precisa de --reason, registado em head.pt["temperature_fit"] (por exemplo, “copied from the pool fit of runs/r20-readout”).
  5. uv run python -m kev.rounds validate experiments/rounds/r<N>.json (acrescenta --partitions para verificar as partições) até imprimir ok. Commita a secção do PLAN e a especificação num único commit, faz push. Essa hora de commit é a hora de registo.

4. Correr de ponta a ponta

KEV_GPU=H200 uv run modal deploy modal_app.py                     # after any change to kev/*.py or any new file under evals/
uv run python -m kev.rounds launch experiments/rounds/r<N>.json   # one ::study per study, 60 s apart, logs in runs/<study>.log
caffeinate -i nohup uv run python -m kev.rounds watch experiments/rounds/r<N>.json > runs/r<N>.watch.log 2>&1 &
  • Nos primeiros cinco minutos de cada estudo, conta os passos do otimizador por minuto em modal container logs <id> e projeta o tempo real contra o timeout (ep0 step N/M: M é sobre todas as épocas). Um contentor que esgota o tempo não guarda nada; cancela (FunctionCall.from_id(cid).cancel()) e relança com um novo nome de estudo, com menos registos ou um timeout maior.
  • O watch consulta os ensaios lançados, recolhe cada estudo terminado (uma recolha por estudo de cada vez), lança as leituras desse braço uma vez (uma chamada ::benchmarks em lote por braço, com 60 s de intervalo), espera por elas e escreve runs/r<N>-readout/round<N>.json e uma tabela. É reiniciável: o estado está em runs/<study>.watch.json e a intenção de lançamento em runs/r<N>-reads-<arm>.json. À mão: launch-reads <spec> [--arms a,b] [--parents] [--dry-run], readout <spec>.
  • Escreve a leitura na secção do PLAN: cada braço, cada critério com o seu intervalo, o veredicto e o que falhou.
  • A confirmação é deliberada, nunca automática. Para o candidato que a leitura nomeia, escreve a escolha no PLAN.md e commita-a, depois por etapa: launch-reads <spec> --stage <stage> --arm <arm>, depois confirm <spec> --stage <stage> --arm <arm> (→ runs/r<N>-verdict/<size>-<stage>.json). Testa os painéis antes da leitura bloqueada. Uma leitura cada, sem exceções.
  • Várias rondas registadas em sequência sob um teto: uv run python -m kev.autoresearch session experiments/rounds/r19.json [...] --spend-start <baseline> --spend-cap <authorization>. Valida, lança e vigia cada ronda até à sua leitura, para antes de uma ronda cujos orçamentos passariam o teto, acrescenta a runs/autoresearch-sessions.jsonl e imprime os comandos de confirmação; nunca os corre. O kev.autoresearch leaderboard atualiza runs/leaderboard.{jsonl,md} (não commitado), o compare emparelha ensaios contra uma referência na precisão de transferência, o release-check --study <name> tria cada configuração desse estudo (cada configuração passa só se todas as suas sementes passarem as suas guardas).

5. O que uma sessão pode e não pode tocar

Pode: escrever especificações, planos e secções do PLAN.md; construir dados congelados novos em diretórios novos; lançar estudos e leituras através do modal_app.py; alterar scripts e constantes de infraestrutura do modal_app.py; abrir PRs para código que pertence ao main.

Não pode, sem o OK explícito do Jared:

  • publicar ou alterar seja o que for no Hub (kev.publish, hf upload, hf repos tag, scripts/publish_space.sh, um head.pt lançado), tornar público um repositório privado, ou implementar um endpoint público;
  • fazer commit no main, force-push, ou fundir um PR (o código chega ao main através de PRs revistos e fundidos por squash com CI verde);
  • editar seja o que for que exista em evals/ (congelado), ou o avaliador: kev/experiment.py: EVALUATOR_FILES, as guardas, kev/metrics.py, a leitura emparelhada do kev/rounds.py. Uma alteração necessária ao avaliador é o seu próprio PR, verificado com o kev-verify e o tests/test_rounds.py, antes de qualquer ronda depender dela;
  • passar --allow-test ou correr o locked_test fora de uma etapa de confirmação registada;
  • colocar qualquer saída do Jev, ou qualquer geração de modelo fechado, nos dados de treino;
  • treinar localmente (um Mac de 32 GB não comporta estes modelos) ou correr dois processos de treino numa máquina;
  • apagar um checkpoint ou um snapshot do volume de execuções (modal volume rm, shutil.rmtree num contentor), ou desativar os snapshots de um ensaio de pesos completos ("snapshot_fractions": "none") numa especificação registada. Os ensaios de pesos completos mantêm snapshots a 0.25, 0.5 e 0.75 dos seus passos (kev.experiment.SNAPSHOT_FRACTIONS) para que uma leitura possa encontrar o melhor ponto de uma execução depois de ela terminar: a ronda 19 não conseguiu, porque o único estado a meio da execução era um ponto de retoma, apagado quando a execução terminou, e o melhor checkpoint do AutoJev estava a 0.7 de época. Os snapshots de um 27B são ~154 GB de volume por ensaio; o espaço é decisão do Jared, não da sessão. Os snapshots vivem no volume de execuções (primário); um espelho privado no Hub (snapshot_hub_repo num plano, ou modal_app.py::mirror_snapshots) é armazenamento de longo prazo para um checkpoint que vale a pena guardar, não um substituto: espelhar checkpoints de 27B (~51 GB cada, num repositório privado como jaredpalmer/kev-snapshots) também é decisão do Jared, e nunca para um repositório público.

Se um braço estiver bloqueado (autenticação, um limite de despesa, uma implementação que não funcionará em 30 minutos), escreve o que aconteceu e passa ao braço seguinte. Não esperes por um humano.

6. Resiliência

  • Ficheiro de estado runs/<session>-state.json (runs/ está no gitignore; faz git add -f dele no ramo de investigação): linha de base e autorização, leituras de despesa com horas UTC, cada estudo com os seus ids de spawn, limite e estado, leituras lançadas e recolhidas, candidatos, PRs, decisões pendentes. Atualiza-o após cada lançamento, recolha e leitura, e commita-o com a secção do PLAN.
  • Trabalhos destacados. Os estudos são lançados na aplicação implementada e sobrevivem ao cliente local; um erro local após o study pode ainda ter lançado ensaios, por isso corre modal container list antes de relançar, e nunca relances com o mesmo nome de estudo. As sondas e os benchmarks correm com --detach.
  • Os watchers são processos locais e morrem com a máquina ou a rede. Corre-os sob nohup e caffeinate; reinicia o watch após qualquer interrupção (retoma a partir do seu estado). Reintenta erros de DNS e de ligação por si próprio; uma exceção do próprio ensaio é uma falha e é reportada.
  • Os ensaios de pesos completos que esgotam o tempo são continuados pelo watcher, não pelo Modal. Os ensaios são lançados com as retentativas do Modal desligadas; quando a chamada de um ensaio de pesos completos termina pelo seu timeout, o watch corre modal_app.py::resume --trial <label>, que lança a tentativa seguinte (continua a partir do último ponto de retoma commitado) com a GPU e o timeout para que o estudo foi admitido e registra-o em runs/<study>.spawn.json (attempts, no máximo 1 + kev.budget.FULL_FT_RETRIES por ensaio, a contagem com que o limite de admissão foi calculado; um ensaio cuja chamada atual ainda está a correr nunca é continuado). Enquanto o watcher está em baixo nada é continuado: reinicia-o e ele retoma o timeout. Porquê: o Modal cobrava cada tentativa que esgotava o tempo duas vezes (o timeout, e depois a tarefa que matava 30 s mais tarde), por isso Retries(2) deu ao ensaio da ronda 22 duas das suas três tentativas, e a retentativa após o kill pode arrancar ao lado de uma tentativa em execução (scripts/modal_retry_probe.py). Um estudo lançado antes do livro-razão não tem contagem: resume --trial <label> --beyond-bound continua-o à mão, fora de qualquer limite, e di-lo. Duas tentativas nunca partilham um ensaio: cada uma é registada como pendente antes do seu spawn, e cada uma detém um lease no volume kev-leases (heartbeat a cada minuto); uma nova tentativa recusa enquanto o lease de outra está fresco, e uma continuação espera até kev.budget.LEASE_STALE (15 min) após o último heartbeat de uma tentativa morta antes de arrancar.
  • Quedas de rede matam os clientes locais, não o trabalho remoto: uma leitura cujo cliente morreu normalmente já terminou no Modal; recolhe o seu diretório do volume (modal volume get kev-runs /<name> runs/<name>) em vez de a relançar.
  • Um benchmark ou sonda falhado deixa o seu diretório no volume; tenta de novo com um nome novo.

7. Reporte

No fim da sessão (e no ficheiro de estado à medida que avança):

  • A secção do PLAN.md de cada ronda transporta o seu registo, a tabela de leitura, os resultados de confirmação e o veredicto, negativo ou não, com os caminhos dos relatórios.
  • Atualiza o “Where we stand” do PLAN.md (candidatos lançados e confirmados, trabalhos em execução, despesa) e o “What we have learned” se um resultado mudou; acrescenta cada ronda à tabela Record.
  • Um resumo da sessão no PLAN.md: despesa (linha de base, leitura final, limites em execução), o que está pendente no Modal com os comandos exatos para o terminar, incidentes, e no máximo três passos seguintes com a sua prova.
  • Cada número transporta checkpoint, suite e partição, n e caminho do relatório; commita as leituras e os veredictos de onde vêm os números (o .gitignore guarda relatórios, não despejos de previsões; acrescenta uma regra para novos diretórios de leituras).
  • Marcações temporais: as horas de registo e de resultado são horas de commit. Não escrevas uma hora num cabeçalho antes de ela acontecer; o scratchpad da noite 3 fê-lo, e as suas marcações não puderam ser usadas.

8. Armadilhas conhecidas

  • Limite de criação de aplicações do Modal. Mais de cerca de três modal run destacados num minuto falham com “App create rate limit exceeded” e nada corre. O kev.rounds espaça os lançamentos 60 s e agrupa as leituras de um braço numa chamada; faz o mesmo à mão.
  • repo@sha em trabalhos de benchmark costumava deslocar cada campo de run@suite@name@flags; o modal_app.parse_jobs agora analisa a partir da direita, por isso as revisões do Hub fixadas são seguras. As suites e os nomes não podem conter @ ou ,.
  • Uma recolha por estudo. Recolhas simultâneas do mesmo estudo apagavam os diretórios de ensaio umas das outras; o pull_study agora detém um bloqueio por estudo. Uma recolha enquanto os ensaios ainda correm é segura e atualiza apenas os ensaios inacabados.
  • As recolhas deixam os pesos completos no volume. O ::pull (e o watch) ignoram os shards de pesos completos (model*.safetensors, ~51 GB por checkpoint ou snapshot de 27B) e os pontos de retoma; todo o resto é descarregado (resultados, linhas, head.pt, configurações). Lê um checkpoint ou um snapshot no volume: ::benchmarks --jobs "/runs/<study>/<trial>/snapshots/step-<N>/checkpoint@<suite>@<name>". O ::pull --weights copia os shards quando algo local realmente precisa deles.
  • Implementa depois dos dados. A imagem copia evals/; o lançador só verifica os hashes de kev/*.py, por isso um ensaio cujo ficheiro de dados foi acrescentado depois da implementação falha dentro do contentor. --gpu H200 no study precisa de uma aplicação implementada com KEV_GPU=H200.
  • 27B. Só H200 (backbone bf16, 55 GB residentes); timeouts de estudo até 28,800 s (um delta de skills de 1 época a lr 2e-5 correu cerca de 8.8 s por passo do otimizador); as leituras em fp32 cerca de três vezes as de um 9B (especificação read_timeout: {"27b": 14400}); a leitura bloqueada precisa de --timeout 14400 --memory-mb 131072 (especificação locked_args) numa H200 (a GPU vem do gpu da especificação / da aplicação implementada, ou --gpu H200 à mão). Cada ensaio de pesos bf16 falha a guarda isolation_and_packing dentro do ensaio (uma verificação em fp32); lê os resultados a partir das linhas e mede o isolamento servido em bf16 separadamente.
  • Nomenclatura do locked_test. Quando uma guarda de triagem dentro do ensaio falhou, a ferramenta exige o sufixo -ungated (kev-4b-r8-ungated); o veredicto segue na mesma a regra registada.
  • Timeouts de leitura por suite. O modal_app.READ_TIMEOUTS define painéis de estados longos 7,200 s, documentos 5,400 s, transfer-v9 3,600 s, senão 1,800 s. Um --timeout global infla o limite de admissão de cada trabalho do lote.
  • Admissão de orçamento. Um lançamento acima do seu --budget sai antes de qualquer coisa correr; relança com um orçamento pelo menos igual ao limite impresso.
  • Os servidores externos são de voo único. O servidor do AutoJev respondia a um pedido de cada vez (HTTP 529 enquanto ocupado); sonda um endpoint estrangeiro antes de um longo kev.benchmark --remote e define --remote-concurrency para o que ele aguenta. Conta os pedidos que ele rejeita (por exemplo um 422 além do seu contexto) como cobertura, nunca os descartes silenciosamente.
  • Temperatura: de distribuição vs dentro do ensaio. O result.json de um ensaio e o seu resumo bloqueado são avaliados no ajuste dentro do ensaio; um lançamento distribui a T que o scripts/calibrate_checkpoint.py escreveu em head.pt. O primeiro confronto direto com o AutoJev serviu o Kev-27B a 1.19 dentro do ensaio em vez da 1.38 de distribuição e teve de ser corrigido. Diz que T cada número usa, e onde foi ajustada (secção 3, regra 4: conjuntos de dados reservados, nunca as próprias partições do corpus de treino).
  • Calibração de contexto longo. Um painel com "by_length": true reporta precisão, ECE, Brier e erros confiantes por intervalo de tokens de estado (abaixo de 8k até 64k+, e as caudas 8k+/16k+/32k+), e um critério pode fazer de guarda a um (long.ece_16k_plus.candidate <= 0.05). Os tokens são contados a partir dos registos da suite das leituras, por isso ambos os lados partilham os intervalos.
  • Correções de suite sem uma nova versão de suite. Um painel pode excluir fontes, tarefas ou uma lista (privada, registada por hash) de ids ou fontes de ambos os lados (exclude_sources, exclude_tasks, exclude_file), e um painel apenas de reporte é marcado com "optional": true para que uma leitura de reporte em falta nunca torne um candidato incompleto. Escolhe as exclusões pela validade das etiquetas antes de qualquer leitura sob elas (a ronda 24 tirou-as da auditoria de 2026-09-27), e nunca commites uma lista privada.
  • Capacidade do espaço de trabalho. O espaço de trabalho correu no máximo cerca de dez contentores de GPU de uma vez; os contentores pendentes são capacidade, não um bug, por isso não os relances.
  • Suites pequenas. Uma guarda em 89 ou 144 perguntas não consegue resolver um piso de 2-3 pp; faz-lhes guarda através de um painel agrupado.
  • As leituras do Jev falham a meio da execução em 503s do gateway; corre de novo toda a leitura com um nome novo em vez de remendar linhas parciais.
  • Dados de alvo suave. Ao escrever um construtor, verifica alguns registos a olho: o target soma 1 e a massa da etiqueta é pelo menos 0.5 a menos que o registo seja incognoscível (o kev.data.none_pair treinou uma vez massa zero em alvos suaves, corrigido no #60).
  • Redireciona a saída do modal run ...::study para um ficheiro de log; um filtro pode esconder o SystemExit que explica porque nada foi lançado.