M5 — Caderno de medições do Spark 02 (2026-09)
Caderno de evidências do M5 (
M5-model-fabric.md). A decisão A/B/C foi reaberta com dados: este arquivo registra, cru e antes de qualquer análise, todo output de medição (comando + saída + data + host). A análise/matriz de decisão vive na Fase 3; a formalização, no task-pack e no architecture-target.Protocolo: (1) nenhuma medição entra aqui sem o comando exato que a produziu; (2) nada é resumido antes de o output bruto estar registrado; (3) cada medição lista runner —
agente-ssh(read-only, sem privilégio) ourunbook-humano(exigedoasinterativa no NeoTek, o agente nunca automatiza senha).
Contexto da decisão (2026-09-03)
A política de fato no catálogo (~/dev/small-router/.ai-factory/models/*.yaml) já escolheu B (segundo modelo local: deepseek-v4-flash-vllm warm no spark-02) e mantém gpt-oss-120b/gpt-oss-20b como experimental no mesmo nó. O M5 reabre A/B/C com números:
- A — réplica do Qwen Coder no spark-02 (concorrência/HA);
- B — segundo modelo local (política de fato; a validar);
- C — serving distribuído (qualificado no papel + interconnect medido; sem deploy
multi-node, conforme decisão do dono).
Contra os 6 critérios do task-pack: latência, confiabilidade, custo, privacidade, utilidade para agentic coding, facilidade operacional.
Riscos pré-registrados
- Serving own-fleet degradado (2026-08-21):
deepseek-v4-flash-vllmdava 404 na:8080;
sbatch sem contato com o controller. A Fase 1 existe para detectar isso antes de medir.
- Standalone spark02 crasha (ADR-0010): jobs diretos no spark02 fora do papel de worker
coordenado falhavam com CUDA error: unspecified launch failure. Se T1.1 mostrar isso, registrar e parar (bloqueio explícito do plano).
- Rota de acesso: workstation →
neotek.wg(WG autenticaid_pinhao; a LAN rejeita).
Do NeoTek ao enverge, a identidade zero é alcançada via doas -u zero (runbook) — exceto o que passa pelo router :8080 (loopback, pcms lê direto).
- Fallback silencioso do router: um warm indisponível cai em fallback serverless
(fallback=true) e queima custo sem erro — conferir logs do router ao interpretar qualquer latência "local".
Comandos aprovados
Fase 1 — Diagnóstico (read-only)
| # | Runner | Comando | O que estabelece |
|---|---|---|---|
| F1.1 | agente-ssh | ssh -J neotek.wg user@enverge.dev "hostname && uptime" | workstation → spark01 alcançável |
| F1.2 | agente-ssh | ssh -J neotek.wg user@enverge.dev sinfo && squeue | Slurm vivo; estado dos nós |
| F1.3 | agente-ssh | ssh -J neotek.wg user@enverge.dev "ssh spark02 hostname && ssh spark02 squeue" (quando aplicável) | spark02 alcançável do spark01 |
| F1.4 | agente-ssh | ssh neotek.wg "curl -s http://127.0.0.1:8080/v1/models" | catálogo real do router NeoTek; deepseek-v4-flash-vllm presente? |
| F1.5 | agente-ssh | ssh neotek.wg "curl -s -X POST http://127.0.0.1:8080/v1/chat/completions -H 'Content-Type: application/json' -H 'Authorization: Bearer <key do router se exigido>' -d '{\"model\":\"deepseek-v4-flash-vllm\",\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}],\"max_tokens\":8}'" | warm spark02 responde? (pré-condição do M5) |
| F1.6 | agente-ssh | ssh -J neotek.wg user@enverge.dev "nvidia-smi --query-gpu=name,memory.total,memory.used,temperature.gpu,power.draw --format=csv" e o mesmo em ssh spark02 | VRAM total/livre por nó (T1.2) |
| F1.7 | runbook-humano | scripts/operator/enverge-gpu-diagnose.sh (repo dev-machine) | só se F1.6 mostrar anomalia; doas no NeoTek |
Fase 2 — Medições núcleo
| # | Runner | Comando | Produz |
|---|---|---|---|
| F2.1 (T2.A) | agente-ssh | ssh neotek.wg "BENCH_STREAMS='1 4 8 16 32' sh -s" < scripts/operator/enverge-model-bench.sh deepseek-v4-flash-vllm (repo dev-machine) | TTFT + tok/s × concorrência do warm spark02 (128 tokens, ignore_eos, comparável ao histórico vllm-dgx-spark-mxfp4.md) |
| F2.2 (T2.A) | agente-ssh | durante F2.1: ssh -J neotek.wg user@enverge.dev "nvidia-smi --query-gpu=memory.used --format=csv -l 5" (spark02) | VRAM sob carga (antes/durante/depois) |
| F2.3 (T2.B) | agente-ssh | ssh neotek.wg "BENCH_STREAMS='1 4 8 16 32' sh -s" < scripts/operator/enverge-model-bench.sh qwen3-coder-30b-a3b-instruct | baseline spark01, comparável linha a linha com F2.1 |
| F2.4 (T2.C-custo) | agente-local | pricing dos YAMLs (~/dev/small-router/.ai-factory/models/) + ssh neotek.wg "curl -s http://127.0.0.1:8081/v1/sessions" (e /admin/metrics do small-pine) | USD/Mtok real por modelo serverless + custo por sessão |
| F2.5 (T2.C-interconnect) | agente-ssh | ssh -J neotek.wg user@enverge.dev "ping -c 20 192.168.100.11"; iperf3 spark01↔spark02 se instalado (sem instalar nada) | latência/bandwidth do rail RoCE |
| F2.6 (T2.C-interconnect, fallback) | runbook-humano | scripts/operator/enverge-fabric-smoke.sh | NCCL 2-rank coordenado spark01+spark02 — prova de viabilidade do C (doas) |
| F2.7 (opcional) | runbook-humano | scripts/operator/enverge-node-concurrency-smoke.sh both | 2 modelos simultâneos (um por nó) — discrimina envelope A vs B (doas) |
Regras da Fase 2: T2.A e T2.B em paralelo só se ambas passarem pelo NeoTek sem contenção de GPU (cada nó tem 1 GPU; jobs do bench são por nó) — caso contrário sequencial, T2.A primeiro (é a decisão em teste). T2.C não toca GPU: sempre paralela.
RESULTADOS — F2.1/F2.3 executados (2026-09-05 05:59Z e 06:13Z) — Fase 2 completa
Bench executado sequencial (T2.A → T2.B) via router NeoTek :8080, 128 tokens ignore_eos, formato comparável ao histórico vllm-dgx-spark-mxfp4.md. Logs completos: /tmp/t2a-bench-spark02.log, /tmp/t2b-bench-spark01.log. Jobs Slurm 30 (tp=2, spark01+02) e 31 (tp=1, spark01). Cold start: deepseek ~370 s, qwen ~390 s (ambos ≤8 min, F1.5 folgado). F2.2 (VRAM sob carga) e F2.4+ (custo/interconnect) não executados — o dono pediu o bench direto; restam como complemento opcional.
| streams | deepseek-v4-flash tp=2 (spark01+02) agg | tok/s/stream | qwen3-coder-30b tp=1 (spark01) agg | tok/s/stream |
|---|---|---|---|---|
| 1 | 15.40 | 15.40 | 21.96 | 21.96 |
| 4 | 29.67 | 10.88 | 83.98 | 21.03 |
| 8 | 27.14 | 7.53 | 170.12 | 21.66 |
| 16 | 31.23 | 5.15 | 320.19 | 20.41 |
| 32 | 30.10 | 3.12 | 426.43 | 13.60 |
TTFT: 0.44 s cravado em todas as linhas, nos dois modelos (prefill não degrada com concorrência nesta faixa). 32/32 requisições ok no pior caso, zero perdas.
Catálogo do dashboard (throughput.tsv) — 2026-09-05: o --record do qwen está gravado e confirmado ao vivo (5 linhas, grade 1/4/8/16/32: 25.09 → 507.26 tok/s agg, TTFT 0.42 s). O do deepseek foi medido às 18:53Z (2ª rodada do dia, job Slurm 39, tp=2) — mesmíssimos números da madrugada, e o único dado novo do dia:
| streams | deepseek tp=2 agg (18:53Z) | madrugada | TTFT | ok |
|---|---|---|---|---|
| 1 | 15.02 | 15.40 | 0.43 | 1/1 |
| 4 | 30.26 | 29.67 | 0.43 | 4/4 |
| 8 | 29.97 | 27.14 | 0.43 | 8/8 |
| 16 | 31.17 | 31.23 | 0.43 | 16/16 |
| 32 | 29.03 | 30.10 | 0.43 | 25/32 |
Reprodutibilidade total (ceiling ~30 tok/s agg em ambas as rodadas, TTFT 0.43–0.44 s). Diferença: 7 requisições perdidas em 32 streams nesta rodada (25/32; a madrugada teve 32/32) — variação sob concorrência máxima, não muda a leitura. Tempo de subida (dono pediu registrar): o próprio bench mediu "ready after ~380s" (pedido do router → primeiro 200), consistente com o cold de ~370 s da madrugada; contado da submissão do job Slurm (SubmitTime → ready) são ~492 s, sendo ~25 s de fila de alocação. Linhas prontas para append no /var/db/small-pine/throughput.tsv (comando doas no checklist do dev-machine, operador-e10-record-deepseek-e-senhas-stale.md) — é o último passo do FINDING-4/E10.
Leituras:
- O gargalo do tp=2 é o all-reduce por camada, não o rail RoCE — o transporte medido
(44.5 Gb/s, 0.935 ms, F2.5 histórico) é saudável, mas o decode do tp=2 trava em ~30 tok/s agregados; o tp=1 escala linear (21 tok/s/stream cravados até 16 streams).
- Para serving, um nó inteiro (tp=1) serve 14× mais que o mesmo hardware dividido em
tensor-parallel — o par de nós só compensa para modelos que não cabem num nó.
- Uso interativo (1 dev): qwen tp=1 (22 tok/s) > deepseek tp=2 (15 tok/s), e libera a
spark02 para o segundo modelo.
- Job 30 provou que o perfil
:distributedsubmete e serve (resposta V7.2 da matriz
de validação do upgrade 0.28: SIM) — a inviabilidade do C é de desempenho/operacional, não de funcionalidade.
- Impacto na política de promoção: a leitura "warm teoricamente = spark01" confirma-se
e o critério de latência agora tem número: warm single-node vence serverless em TTFT (0.44 s vs. cold start de minutos) e não tem custo GPU. O TBD de custo mensal (serverless vs nó ocioso a $0) permanece — F2.4 não executado.
Fase 3 — Análise (sem cluster)
Matriz A/B/C pontuada nos 6 critérios + recomendação → gate humano (dono aprova).
Log de evidências
Fase 1 — Diagnóstico
F1.1 — rota workstation → spark01 (2026-09-03 20:50 UTC)
$ ssh -J neotek.wg user@enverge.dev "hostname && uptime"
spark01
20:50:03 up 24 days, 9:39, 27 users, load average: 0.43, 0.32, 0.77
OK: jump via neotek.wg autentica, spark01 up 24 dias, carga baixa.
Revalidação (2026-09-03 21:08 UTC): máquina ON (up 24d 9:57, load 0.46), spark02 alcançável do spark01 — o que está failed desde 2026-08-21 é o daemon slurmctld (exit 1, unit disabled), não a máquina. Distinção explícita para o relatório.
F1.4 — catálogo do router NeoTek :8080 (2026-09-03 20:50 UTC)
$ ssh neotek.wg "curl -s -m 10 http://127.0.0.1:8080/v1/models"
{"object":"list","data":[
{"id":"deepseek-v4-flash-vllm","object":"model","owned_by":"small-infer"},
{"id":"gpt-oss-120b",...},{"id":"gpt-oss-20b",...},
{"id":"multilingual-e5-small",...},{"id":"nemotron-3-super-120b-a12b-nvfp4",...},
{"id":"qwen3-14b-fp4",...},{"id":"qwen3-235b-a22b-fp4",...},{"id":"qwen3-8b-fp4",...},
{"id":"qwen3-coder-30b-a3b-instruct",...},{"id":"qwen3-coder-30b-a3b-instruct-b",...},
{"id":"qwen3-coder-next",...},{"id":"qwen3.5-122b-a10b-gptq-int4",...},
{"id":"qwen3.6-35b-a3b-nvfp4",...},{"id":"qwen3.8-27b-fp8",...}]}
OK: 14 ids; deepseek-v4-flash-vllm presente (listado ≠ servindo — ver F1.5).
F1.5 — chat probe deepseek-v4-flash-vllm (2026-09-03 20:51 UTC) — STATUS: WARMING
$ ssh neotek.wg "curl -s -m 60 -X POST http://127.0.0.1:8080/v1/chat/completions \
-d '{\"model\":\"deepseek-v4-flash-vllm\",\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}],\"max_tokens\":8}'"
{"error":{"type":"router_error","message":"deepseek-v4-flash-vllm is still starting (0s elapsed).
The Slurm job is submitted and loading; a cold start takes roughly 8 minutes on two nodes.
Retry then, or watch it with: enverge-model-cli.sh jobs",
"data":{"status":503,"small-pine.service.warm-host/warming-up":true,
"model":"deepseek-v4-flash-vllm","elapsed-ms":0}}}
Leitura: o warm-host submeteu o job e está carregando (cold start ~8 min em dois nós — o serving do deepseek-v4-flash-vllm aparenta ser multi-node; confirmar depois do warm). Contraste com 2026-08-21 (404/sem contato): hoje o ciclo de vida responde com 503+warming.
F1.2 — Slurm do login node (2026-09-03 20:51 UTC) — DEGRADADO (parcial)
$ ssh -J neotek.wg user@enverge.dev "sinfo; echo ---; squeue"
slurm_load_partitions: Unable to contact slurm controller (connect failure)
---
slurm_load_jobs error: Unable to contact slurm controller (connect failure)
Leitura: o cliente Slurm do login node não alcança o controller — mesmo sintoma de 2026-08-21 — mas o warm-host submeteu job (F1.5), ou seja, o caminho sbatch do router (por ssh, ADR-0003) funciona. Medir sinfo/squeue de outro ponto se necessário.
F1.6 — VRAM estático (2026-09-03 20:52 UTC)
$ nvidia-smi --query-gpu=name,memory.total,memory.used,temperature.gpu,power.draw --format=csv
spark01: NVIDIA GB10, [N/A], [N/A], 45, 11.98 W
spark02: NVIDIA GB10, [N/A], [N/A], 44, 12.23 W (via ssh spark02)
Leitura: GPUs ociosas (~12 W, ~45 °C). memory.total N/A é particularidade do GB10 (memória unificada) — total/livre a coletar via nvidia-smi -q | grep -A2 Memory (F1.6b).
F1.6b — VRAM GB10 via nvidia-smi -q (2026-09-03 20:56 UTC) — N/A também no -q
FB Memory Usage: Total/Reserved/Used/Free = N/A em ambos os nós — o driver do GB10 não expõe VRAM via nvidia-smi. Substituto: free -g (memória unificada, F1.6c).
F1.6c — memória unificada GB10 (2026-09-03 20:56 UTC)
$ free -g | head -2 (spark01 e spark02 via ssh)
spark01: Mem: 116 total, 3 used, 108 free, 4 buff/cache, 112 available
spark02: Mem: 116 total, 3 used, 111 free, 1 buff/cache, 112 available
Leitura: 116 GB de memória unificada por nó, ~112 livres nos dois, GPU ociosa. Checkpoint 120B em NVFP4 (~60-70 GB) cabe em 1 nó; 2×120B warm simultâneos (A ou B) também cabem — a restrição real é compute/throughput, não capacidade.
Fase 2 — Medições (linhas não-GPU executadas em paralelo ao warm)
F2.4 — custo serverless (2026-09-03 20:57 UTC)
:8081local responde{"error":"invalid or missing API key"}para /v1/sessions sem
credencial — custo por sessão real fica para leitura com key (não bloqueia: pricing declarado cobre o eixo custo).
- Pricing dos YAMLs (
~/dev/small-router/.ai-factory/models/, USD/Mtok in/out):
| modelo | input | cached | output |
|---|---|---|---|
| deepseek-v4-flash | 0.22 | 0.007 | 0.66 |
| deepseek-v4-pro | 1.32 | 0.044 | 3.96 |
| qwen3.8-max | 2.00 | 0.25 | 6.00 |
| glm-5p2 / glm-5p3 | 1.40 | 0.14/0.26 | 4.40 |
| glm-5p3-flash | 0.15 | 0.03 | 0.50 |
| kimi-k2p7-code | 0.95 | 0.19 | 4.00 |
| kimi-k3 | 3.00 | 0.30 | 15.00 |
| minimax-m3 | 0.30 | 0.06 | 1.20 |
| muse-glimmer-30b | 0.35 | 0.04 | 1.50 |
| nemotron-3-ultra-nvfp4 | 0.60 | 0.12 | 2.40 |
| nemotron-lightning-3p5-30b | 0.05 | 0.01 | 0.20 |
| own-fleet (warm/experimental) | 0 | 0 | 0 |
F2.5 — interconnect rail spark01→spark02 (ping; 2026-09-03 20:56 UTC)
$ ping -c 20 192.168.100.11 (de spark01)
20 transmitted, 20 received, 0% loss
rtt min/avg/max/mdev = 0.172/0.935/1.457/0.383 ms
iperf3 presente em ambos os nós → bandwidth test na sequência (F2.5b).
F2.5b — bandwidth rail spark01→spark02 (iperf3; 2026-09-03 21:00 UTC)
$ iperf3 -c 192.168.100.11 -t 5 (server no spark02)
[ 5] 0.00-5.00 sec 25.9 GBytes 44.5 Gbits/sec 0 sender
[ 5] 0.00-5.00 sec 25.9 GBytes 44.5 Gbits/sec receiver
Leitura: 44.5 Gbits/sec, 0 retransmissões — rail RoCE saudável. Dado para qualificar C (serving distribuído): o transporte inter-nó existe e é rápido; o gargalo de C não é rede.
BLOQUEIO — Fase 2 (GPU) suspensa: slurmctld morto desde 2026-08-21
Descoberta da Fase 1 (2026-09-03 21:02 UTC) que ativa o bloqueio explícito do plano ("não medir sobre infra quebrada"):
$ ssh -J neotek.wg user@enverge.dev "systemctl status slurmctld"
× slurmctld.service - Slurm controller daemon
Loaded: loaded (/usr/lib/systemd/system/slurmctld.service; disabled; preset: enabled)
Active: failed (Result: exit-code) since Fri 2026-08-21 21:02:40 UTC; 1 week 5 days ago
Duration: 18h 52min 28.427s
$ ps aux | grep -E 'slurmctld|slurmd' (spark01)
user 3978408 bash /var/db/slurm/slurmd/job00062/slurm_script (03:09 de hoje)
root 4133614 /usr/sbin/slurmd (Sep02)
Estado observado:
slurmctldfailed + disabled no head (spark01) desde 2026-08-21 21:02:40 UTC — a
mesma data do incidente de serving degradado registrado na memória do projeto.
- Processos vLLM órfãos presos nos dois nós, sem pesos carregados (EngineCore ~2.2-2.4 GB
RSS, GPU a 12 W = ociosa, nenhuma porta de inferência respondendo): - spark01: gpt-oss-120b (srun, job00062, desde 03:09 UTC de hoje); - spark02: qwen3-coder-30b-a3b-instruct (srun, desde 03:47 UTC de hoje).
- O router
:8080responde chat paradeepseek-v4-flash-vllmcom 503warming-upe
elapsed-ms: 0 em todo poll (nunca progride): o ciclo warm-host acredita que um job está carregando, mas sem controller não há como o job viver — o estado está wedgeado.
- Endpoints
/prewarme/backendsretornam "not found" no:8080— a superfície do
small-pine consolidado difere da do small-infer que o enverge-model-bench.sh documenta (risco para a Fase 2 quando ela for executada: adaptar o script ou medir por outro caminho).
- Hardware 100% saudável (2×116 GB livres, rail 44.5 Gb/s, GPUs frias) — o problema é
puramente o daemon do Slurm + processo órfãos.
Runbook humano (execução privilegiada no head — o agente não roda):
# 1. Por que o controller morreu em 2026-08-21?
ssh -J neotek.wg user@enverge.dev \
"journalctl -u slurmctld --since '2026-08-21 20:00' -n 50 --no-pager"
# 2. Reativar e subir o controller
ssh -J neotek.wg user@enverge.dev "sudo systemctl enable --now slurmctld"
# 3. Fila e nós saudáveis?
ssh -J neotek.wg user@enverge.dev "squeue; sinfo"
# 4. Cancelar os jobs presos (00062 e afins) — se sobrar processo órfão nos nós:
ssh -J neotek.wg user@enverge.dev \
"ssh spark02 'pkill -f vllm.entrypoints.openai.api_server'"
# (o mesmo pkill no spark01, se necessário)
# 5. Revalidar o warm: um chat request no :8080 dispara o ciclo warm-host.
Regra de retomada: com squeue respondendo e GPUs limpas, reexecutar F1.5 (chat probe deve passar de 503→200 em ≤8 min) e então liberar a Fase 2 (T2.A/T2.B). T2.C já está completa (custo F2.4 + interconnect F2.5/F2.5b).
Adendo ao bloqueio (2026-09-03 21:20 UTC) — wedge é geral e atinge serverless
- Chat probe de
qwen3-coder-30b-a3b-instructe dedeepseek-v4-flash(serverless
Fireworks) no :8080 → mesmo 503 warming elapsed-ms:0. O wedge intercepta todo o chat do router own-fleet, inclusive modelos que não deveriam tocar Slurm. Reiniciar apenas o slurmctld pode não limpar o estado do router — se após o fix o 503 persistir, reiniciar o serviço do small-pine no NeoTek.
- Superfície real do
:8080:/health200;/models+/v1/models+/admin/metrics200;
/backends timeout (HTTP 000); /queue 404; POST /prewarm/:model existe no código (gateada admin-token/:schedule-model) mas responde "not found" no deploy atual — o build no NeoTek é mais velho que o core.clj do small-pine (que monta /queue e prewarm-routes).
- Router local da workstation (
:8081): no ar, exige API key (401 com placeholder) — a
era allow_anonymous acabou (conforme auditoria MR-020B do small-router).
- Resolvido (2026-09-03 21:40 UTC): a contradição era a topologia de jails — o
:8080
respondendo no host loopback é a jail small-pine (vnet=new, 17 jails persist no NeoTek; o Uberjar roda dentro dela, invisível ao ps do host). O rc small_infer do host é o serviço legado do small-infer (de fato não roda). Reiniciar o router wedged = doas jexec small-pine service <svc> restart dentro da jail (runbook). Jails kafka/qdrant estão no jail_list do rc.conf mas não estão rodando.
Adendo ao bloqueio (2026-09-05) — bloqueio ENCERRADO: controller sempre viveu em host dedicado; diagnóstico de 09-03 errou o host
A validação completa de infra de 2026-09-05 (dev-machine/docs/patches/2026-09-05-infra-verification.md, commit bc89e8be) observa um estado incompatível com este bloqueio:
- Inferência own-fleet ponta a ponta OK:
POST /prewarm/qwen3-8b-fp4submeteu job
Slurm real, backend ready em <1 min, completion HTTP 200 (usage {14,16,30}, vllm-0.26.0) — exatamente a Fase de revalidação (F1.5) que este runbook exigia para liberar a Fase 2.
sinfo: nósinferenceidle, sem fila (squeuevazio) — sem jobs presos nem
processos órfãos relatados.
POST /prewarm/:modelrespondeu no:8080— o ponto 4 do estado observado
("not found", build mais velho que o core.clj) está superado: o deploy do NeoTek foi atualizado entre 09-03 e 09-05.
Interpretação (CORRIGIDA em 2026-09-05, confirmação formal): o diagnóstico de 09-03 errou o host do controller. Verificado ao vivo (2026-09-05 ~04:30 UTC):
enverge.devé o spark01 — nó de computação. O unitslurmctldfailed+disabled ali
é artefato (controller nunca precisou rodar num nó de computação; só o slurmd, que está ativo desde Sep 02).
scontrol ping→Slurmctld(primary) at slurm-trusted-controller is UP— o
controller primário vive num host dedicado (SlurmctldHost[0] = slurm-trusted-controller(172.31.110.6)), não é acessível por SSH direto do spark01 (DNS/IP sem rota), e o RPC dele responde.
sinfo(nósinferenceidle) esqueue(vazio) respondem; órfãos vLLM de 09-03 sumiram
do ps; BootTime do trusted-controller = 2026-09-03 12:11 UTC — o reboot do controller dedicado explica a recuperação entre 09-03 e 09-05.
Consequências:
- Este BLOQUEIO está ENCERRADO — control plane UP + nós idle + completion real 200
(validação 09-05). Fase 2 (T2.A/T2.B) liberada, respeitando a regra de contenção de GPU (sequencial se necessário).
- O runbook original ("sudo systemctl enable --now slurmctld" no spark01) estava errado
— subiria um controller duplicado no nó errado. Nunca executado (por sorte).
- O wedge real de 09-03 (
503 warming-up elapsed-ms:0na jail small-pine) era estado do
router + possivelmente o reboot pendente do controller dedicado — não o unit do spark01.
- HASH_VAL divergente (
Ours=0xf0ba47fa Slurmctld=0x790f590noscontrol show config)
é esperado entre nó de computação e controller (configs distintos por papel).
Pendência separada da mesma validação: senhas salvas na estação estão stale (rotação 09-03/04); não afeta as medições GPU (autenticação do bench não passa pelo OWUI/Keycloak).
Reprodutibilidade
Re-run de F2.1 com BENCH_STREAMS='1': tolerância a declarar após a primeira medição (preencher: valor observado vs re-run, desvio %).
RESULTADOS — Upgrade vLLM 0.26→0.28 executado (2026-09-05 23:24Z/23:29Z): TTFT -95%, throughput igual
O upgrade planejado em docs/vllm/vllm-0.28-upgrade-plan.md foi executado até o fim (jobs Slurm 44–51, stack final no side-by-side ~/venvs/vllm-028). Bench comparável (128 tokens ignore_eos, streams 1/4/8/16/32, job 51 tp=2 spark01+02, job ainda no ar), 2 rodadas completas, log /tmp/bench-v028.log:
| streams | 0.26 agg (baseline 09-05) | 0.28 agg r1 / r2 | TTFT 0.26 | TTFT 0.28 r1 / r2 | ok |
|---|---|---|---|---|---|
| 1 | 15.02–15.40 | 16.13 / 15.61 | 0.43–0.44 | 0.02 / 0.00 | 1/1 |
| 4 | 30.26 | 30.16 / 29.74 | 0.43–0.44 | 0.02 / 0.00 | 4/4 |
| 8 | 29.97 | 29.14 / 28.54 | 0.43–0.44 | 0.02 / 0.00 | 8/8 |
| 16 | 31.17 | 26.73 / 29.18 | 0.43–0.44 | 0.02 / 0.00 | 16/16 |
| 32 | 29.03 | 29.45 / 30.96 | 0.43–0.44 | 0.02 / 0.00 | 32/32 |
Leituras:
- TTFT caiu ~95% (0.43–0.44 s → 0.00–0.02 s) — o ganho real do 0.28 neste hardware
(scheduler novo + --enable-prefix-caching + flashinfer autotune). Todas as 77 requisições ok (zero perdas, incluindo 32/32 que no 0.26 chegou a 25/32).
- Throughput agregado equivalente (ceiling ~30 tok/s agg mantido) — confirma a leitura
de que o gargalo do tp=2 é o all-reduce por camada, não a versão do vLLM.
- Cadeia de compatibilidade GB10 (aarch64+cu130) resolvida em 3 elos — registrado para
futuros upgrades de pilha neste hardware: a. torch 2.13 crasha o import (malloc(): corrupted top size) — bug do malloc glibc no GB10; LD_PRELOAD=libjemalloc.so.2 resolve (jemalloc 5.3 via .deb arm64 extraído userland). b. torch 2.12.1 (pin alternativo) é ABI-incompatível com o shim vendored vllm/third_party/deep_gemm do 0.28 (Unsupported TypeMeta in ATen) — bridge de símbolos resolve o load, mas o drift de TypeMeta quebra em runtime. 0.27.1 tem o mesmo pin 2.13 (porta fechada). Conclusão: usar o pin nativo. c. DSv4 (config index_topk) exige DeepGEMM no 0.28 (Sparse Attention Indexer, caminhos fp8_fp4_* sem escape por env); o ramo SM100 exige ue8m0_cast (não desligar VLLM_USE_DEEP_GEMM_E8M0) e as scales do checkpoint DS4 já são F8_E8M0 — o caminho nativo passa limpo. Stack final validada: vllm 0.28.0 + torch 2.13.0+cu130 + LD_PRELOAD jemalloc + E8M0 default, idêntica nos 2 nós (~/venvs/vllm-028, $HOME é node-local).
- KV cache 0.28: 179.827 tokens (job 51),
max_model_len131072 mantido. - Rollout para produção (router
:8080): trocarPYTHON_BIN/RAY_BINdo venv no
sbatch do catálogo — rollback = reverter a var (side-by-side preservado). O warm deepseek-v4-flash-vllm da política B continua no 0.26 até o dono aprovar a troca.
DRAFT — política explícita + matriz A/B/C parcial (2026-09-03, pré-análise GPU)
Rascunho para revisão do dono. Células com TBD dependem de T2.A/T2.B (medições GPU, suspensas pelo slurmctld). A formalização na Fase 4 promove este draft a decisão.
Matriz A/B/C contra os 6 critérios do task-pack
| Critério | A — réplica Qwen (spark02) | B — segundo modelo local (deepseek warm, atual) | C — serving distribuído |
|---|---|---|---|
| Latência | T2.B medido: 22 tok/s stream × 1 dev, escala linear até 16 streams, TTFT 0.44 s | T2.A medido: 15 tok/s stream (tp=2), TTFT 0.44 s, ceiling ~30 tok/s agg | medido (T2.A é o C): all-reduce por camada trava o decode em ~30 tok/s agg — hop de rede por token confirma a leitura qualitativa; rail saudável não salva o decode |
| Confiabilidade | T2.B confirma: single-node = menos pontos de falha (1 job, 1 nó) | acoplamento ao Slurm segue real (incidente 08-21→09-05) | pior: depende de 2 nós + interconnect + torchrun coordenados (ADR-0010: spark02 standalone crashava) |
| Custo | $0 GPU (capacidade ociosa hoje) | $0 GPU | $0 GPU, mas overhead de serving distribuído reduz tokens/s por nó |
| Privacidade | privada | privada | privada |
| Utilidade agentic coding | duplica coding (HA de qwen-coder); não diversifica capabilities | diversifica: reasoning/architecture local (deepseek), alivia serverless pago | capacita modelos > 116 GB (não é necessidade atual) |
| Facilidade operacional | média: 1 perfil novo, perfil já existe em princípio | média: em vigor hoje | pior: multi-node vLLM via torchrun + rdzv (complexidade alta, pouco uso) |
Leitura parcial (pré-GPU): C perde em operacional/confiabilidade para uma necessidade que não existe (nenhum checkpoint > 116 GB no portfólio). A decisão real é A vs B, e depende de: (1) latência/throughput do deepseek warm vs. qwen warm (T2.A/T2.B); (2) se a dor atual é *diversidade de capability* (favorece B) ou *concorrência/HA do coding* (favorece A).
Leitura PÓS-bench (2026-09-05) — dependência (1) resolvida: a latência não é mais conjectura — qwen tp=1 single-node domina o deepseek tp=2 em tok/s/stream (22 vs 15) e em throughput escalável (linear até 16 vs ceiling ~30 agg). C segue eliminado por desempenho/operacional (e o T2.A provou que funciona, só que lento). A decisão A vs B restante é estratégica: HA/diversidade de coding (A: segunda instância do coder liberando spark02) vs diversidade de capability (B: deepseek reasoning local). O dado novo que favorece A: o tp=2 acapara os DOIS nós para 30 tok/s — em B, o deepseek tp=2 impede qualquer segundo modelo simultâneo; um deepseek tp=1 serverless pago na falta de warm local pode sair mais barato que acaparar 2 GPUs. Formalização final segue na Fase 4 com F2.4 (custo real por sessão) pendente.
Draft da política (a formalizar na Fase 4)
- Promoção
experimental → warm: modelo mede (bench comparável, 128 tokens ignore_eos)
sem regressão vs. categoria atual + caso de uso capability real (não é "porque está no catálogo") + hardware cabendo no orçamento de memória do nó sem derrubar warm existente.
- Desligamento de warm: backend idle além da janela de hot-window já é candidato a
eviction (mecanismo existente); desligamento permanente = status: on-demand no YAML quando a capability for coberta por alternativa de custo/latência equivalente.
- Limite de custo que justifica serverless: sem warm local para a capability OU quando o
custo mensal projetado do serverless < custo de oportunidade do nó (hoje $0) — TBD número após T2.A/T2.B; fallback silencioso (fallback=true) deve gerar audit visível, não queima invisível (MR-028 já atribui o burn).
- Fronteira do portfólio serverless: entrada de modelo novo só com pricing declarado no
YAML e capability não coberta; remoção quando ocioso no router por X dias (TBD).
Aderência catálogo ↔ draft (estado hoje)
- Política de fato = B:
deepseek-v4-flash-vllmstatus: warm@ spark-02 ✓ gpt-oss-120b/20bstatus: experimental@ spark-02 — devem medir antes de warm
(nenhuma medição registrada ainda; vLLM órfão de 03:09 sugere tentativa sem sucesso)
- 13 serverless com pricing declarado ✓ (custo mensal real: ler
/v1/sessionscom key)