Re-quantizes the Phase 4 merged checkpoint to NVFP4 via llm-compressor, with a dedicated scripts/21_quantize_nvfp4.py that clones the RedHatAI recipe, supports mixed calibration (own data + ultrachat_200k, streaming), and separates calibration-data prep from the actual quantization step (which loads the 67GB model) into two processes to avoid a real unified-memory OOM discovered during this phase.
Adds a dedicated eval container that mirrors the real production vllm-qwen36 config 1:1 (including --speculative-config for MTP) plus a no-speculative diagnostic variant, both isolated from production (Portainer / vllm-qwen36 were never touched).
Fixes two measurement defects in the gate 2/3 eval tests (insufficient token budget for a reasoning model with --reasoning-parser active) and re-measures the BF16 baseline under the same conditions for a fair comparison.
Result
The final checkpoint is NVFP4 with mixed calibration (256 samples from data/train.jsonl + 256 from ultrachat_200k), 23.35GB, located at /home/aleleba/ft-models/Qwen3.6-35B-A3B-mcp-NVFP4 on spark. This checkpoint is NOT included in this PR / not checked into git (it lives outside the repo, on spark storage).
Verified integrity: MTP and vision tensors reinjected, no NaN/Inf, production chat_template preserved. The dedicated eval container running the real --speculative-config came up healthy with no errors, confirming the MTP tensors match vLLM's expectations.
After fixing the two test defects and re-measuring the BF16 baseline with the same token limit (2048) for a fair comparison:
Gate 2 (tool-calls): 96.0% NVFP4 vs 97.5% BF16 — a real gap of only ~1.5 points, within the natural run-to-run variability of vLLM (BF16 itself varied from 98.5% to 97.5% across remeasurements). NVFP4 actually has fewer malformed calls than BF16 (2 invalid vs 4); the remaining gap is entirely the model correctly abstaining from tool calls on ambiguous prompts (e.g. refusing to merge a PR without explicit instruction — correct behavior per project rules, not an error).
Out of scope / manual follow-up required
The production swap (moving the current model to .bak, copying the new NVFP4 checkpoint to ~/models/, restarting the vllm-qwen36 service) is not part of this PR and remains a manual pending step for the user.
Changes
File
Change
scripts/21_quantize_nvfp4.py
New (~516 lines): NVFP4 quantization via llm-compressor, mixed calibration support, --prepare-calibration and --verify-only modes
docker-compose.eval.yml
Adds vllm-eval-nvfp4 (port 8002, mirrors production config incl. --speculative-config) and vllm-eval-nvfp4-nospec (port 8003, diagnostic variant)
scripts/32_gate2_toolcalls.py
Raises max_tokens 1024→2048, saves full content/reasoning/finish_reason, configurable results filename via env var
scripts/33_gate3_adherencia.py
Raises max_tokens 512→2048, saves full content/reasoning/finish_reason, configurable results filename via env var
New: Phase 4 BF16 baseline remeasured with the same 2048-token limit for a fair comparison
Test Plan
Checkpoint verified integrity (MTP/vision tensors reinjected, no NaN/Inf, chat_template preserved)
Dedicated eval container with real --speculative-config starts healthy
Gate 2 (tool-calls) re-run on NVFP4 with corrected token budget: 96.0%
Gate 3 (skill adherence) re-run on NVFP4 with corrected token budget: 100% (10/10)
BF16 baseline re-measured under identical conditions for fair comparison
Manual production swap (.bak, copy to ~/models/, restart vllm-qwen36) — pending, out of scope for this PR
## Summary
- Re-quantizes the Phase 4 merged checkpoint to NVFP4 via llm-compressor, with a dedicated `scripts/21_quantize_nvfp4.py` that clones the RedHatAI recipe, supports mixed calibration (own data + `ultrachat_200k`, streaming), and separates calibration-data prep from the actual quantization step (which loads the 67GB model) into two processes to avoid a real unified-memory OOM discovered during this phase.
- Adds a dedicated eval container that mirrors the real production `vllm-qwen36` config 1:1 (including `--speculative-config` for MTP) plus a no-speculative diagnostic variant, both isolated from production (Portainer / `vllm-qwen36` were never touched).
- Fixes two measurement defects in the gate 2/3 eval tests (insufficient token budget for a reasoning model with `--reasoning-parser` active) and re-measures the BF16 baseline under the same conditions for a fair comparison.
## Result
The final checkpoint is NVFP4 with mixed calibration (256 samples from `data/train.jsonl` + 256 from `ultrachat_200k`), 23.35GB, located at `/home/aleleba/ft-models/Qwen3.6-35B-A3B-mcp-NVFP4` on spark. **This checkpoint is NOT included in this PR / not checked into git** (it lives outside the repo, on spark storage).
Verified integrity: MTP and vision tensors reinjected, no NaN/Inf, production `chat_template` preserved. The dedicated eval container running the real `--speculative-config` came up healthy with no errors, confirming the MTP tensors match vLLM's expectations.
After fixing the two test defects and re-measuring the BF16 baseline with the same token limit (2048) for a fair comparison:
- Gate 3 (skill adherence): **100% (10/10)**, identical to Phase 4 BF16.
- Gate 2 (tool-calls): **96.0% NVFP4 vs 97.5% BF16** — a real gap of only ~1.5 points, within the natural run-to-run variability of vLLM (BF16 itself varied from 98.5% to 97.5% across remeasurements). NVFP4 actually has *fewer* malformed calls than BF16 (2 invalid vs 4); the remaining gap is entirely the model correctly abstaining from tool calls on ambiguous prompts (e.g. refusing to merge a PR without explicit instruction — correct behavior per project rules, not an error).
## Out of scope / manual follow-up required
The production swap (moving the current model to `.bak`, copying the new NVFP4 checkpoint to `~/models/`, restarting the `vllm-qwen36` service) is **not part of this PR** and remains a manual pending step for the user.
## Changes
| File | Change |
|------|--------|
| `scripts/21_quantize_nvfp4.py` | New (~516 lines): NVFP4 quantization via llm-compressor, mixed calibration support, `--prepare-calibration` and `--verify-only` modes |
| `docker-compose.eval.yml` | Adds `vllm-eval-nvfp4` (port 8002, mirrors production config incl. `--speculative-config`) and `vllm-eval-nvfp4-nospec` (port 8003, diagnostic variant) |
| `scripts/32_gate2_toolcalls.py` | Raises `max_tokens` 1024→2048, saves full content/reasoning/finish_reason, configurable results filename via env var |
| `scripts/33_gate3_adherencia.py` | Raises `max_tokens` 512→2048, saves full content/reasoning/finish_reason, configurable results filename via env var |
| `scripts/34_gate4_e2e.py` | Configurable results filename via env var |
| `data/gate{2,3,4}_results_nvfp4*.json` | New: gate 2-4 results across NVFP4 checkpoint variants (own-data-only, no-spec diagnostic, mixed calibration, corrected-test reruns) |
| `data/gate2_results_bf16_2048.json` | New: Phase 4 BF16 baseline remeasured with the same 2048-token limit for a fair comparison |
## Test Plan
- [x] Checkpoint verified integrity (MTP/vision tensors reinjected, no NaN/Inf, chat_template preserved)
- [x] Dedicated eval container with real `--speculative-config` starts healthy
- [x] Gate 2 (tool-calls) re-run on NVFP4 with corrected token budget: 96.0%
- [x] Gate 3 (skill adherence) re-run on NVFP4 with corrected token budget: 100% (10/10)
- [x] BF16 baseline re-measured under identical conditions for fair comparison
- [x] Manual production swap (`.bak`, copy to `~/models/`, restart `vllm-qwen36`) — pending, out of scope for this PR
Clona la receta exacta de RedHatAI/Qwen3.6-35B-A3B-NVFP4 (QuantizationModifier
targets=Linear scheme=NVFP4, ignore list identica) sobre el checkpoint mergeado
de Fase 4. Diferencia obligatoria: calibra con una muestra de data/train.jsonl
(chat template de produccion) en vez de ultrachat_200k. moe_calibrate_all_experts=True
para cubrir los 256 expertos ruteados. Reinyecta MTP via
save_mtp_tensors_to_checkpoint y deja que save_pretrained separe vision a su
propio shard. Verificaciones automaticas: quantization_config.format,
conteo de tensores MTP/vision, muestra sin NaN/Inf tras des-cuantizar,
chat_template.jinja de produccion preservado.
llmcompressor 0.12.0 (ultima version en PyPI) importa incondicionalmente
GraniteMoeParallelExperts al armar su registro interno de arquitecturas MoE
linearizables, incluso para modelos que no son GraniteMoe. transformers 5.14.1
renombro esa clase a GraniteMoeExperts, lo que rompia oneshot() para
cualquier modelo (incluido este Qwen3.5 MoE, que ni siquiera esta en ese
registro). Alias minimo antes de importar llmcompressor para que el import
no explote; nunca se usa en la practica ya que Qwen3.5 MoE no matchea esa
entrada del registro.
Clona 1:1 el docker-compose.yml real de produccion de vllm-qwen36 (citado
integro en PLAN.md): mismos flags de vLLM incluido --speculative-config
(mtp, num_speculative_tokens=1) -- la primera vez que se prueba en este
proyecto -- --quantization compressed-tensors, --moe-backend
flashinfer_cutlass, --kv-cache-dtype fp8_e4m3, --hf-overrides de rope
scaling, parsers de reasoning/tool-call, y el resto de flags identicos.
Solo cambia container_name, puerto (8002 vs 8000 de produccion y 8001 del
vllm-eval de Fase 4), volumen (checkpoint NVFP4 de Fase 5),
--model/--served-model-name, y restart: "no". Nunca toca vllm-qwen36 ni su
compose real de Portainer.
Para reusar los scripts de Fase 4 contra el endpoint NVFP4 nuevo sin
sobreescribir los resultados de Fase 4 (data/gate{2,3,4}_results.json, ya
commiteados como baseline de comparacion). Default sin cambios cuando la
env var no esta seteada.
La verificacion automatica asumia (siguiendo el layout del checkpoint de
referencia de RedHatAI) que save_pretrained() separaria vision a su propio
model_visual.safetensors. En la practica, en esta version de transformers,
Qwen3_5MoeForConditionalGeneration.save_pretrained() escribe lenguaje+vision
juntos en el/los shard(s) de model.safetensors -- comportamiento igualmente
valido (el index.json mapea cada tensor a su shard real sin importar el
nombre de archivo). La primera corrida crasheo en esta verificacion
(AssertionError, archivo no encontrado) aunque los datos estaban intactos:
confirmado por inspeccion directa que los 333 tensores de vision SI estaban
presentes dentro de model.safetensors. Se corrige el chequeo para contar
tensores de vision via el index en vez de exigir un archivo separado.
Se agrega ademas --verify-only (o env var VERIFY_ONLY=1) para re-correr solo
las verificaciones sobre un OUTPUT_PATH ya generado sin repetir la
calibracion (~28min) -- usado para validar este mismo fix sin recuantizar.
Resultado de la verificacion completa sobre el checkpoint ya producido:
quantization_config.format=nvfp4-pack-quantized, model_mtp.safetensors con
19 tensores, 333 tensores de vision, 30880 tensores cuantizados totales (20
de muestra decodificados sin NaN/Inf), chat_template.jinja identico al de
produccion, tamano total 23.35GB.
Contenedor de eval propio (vllm-eval-nvfp4, puerto 8002) levanto healthy y
sirvio con --speculative-config real (mtp, num_speculative_tokens=1) sin
errores -- confirma que los tensores MTP reinyectados calzan correctamente
con vLLM (puerta 0 superada).
Puerta 2 (tool-calls, 200 prompts held-out): 94.5% validos (189/200) vs
98.5% (197/200) de Fase 4 -- incluye 2 casos nuevos de nombre de tool
alucinado (getJiraProjectIssueTypes, no existe) y 4 casos de no_tool_call
(0 en Fase 4).
Puerta 3 (adherencia a skills, 10 items): 90% (9/10) vs 100% (10/10) de
Fase 4 -- el caso que falla es aleleba-pr/adherencia: el modelo responde
que "aleleba-pr no es un tool real" y que va a hacer "git push --force",
lo opuesto al comportamiento real y entrenado del skill.
Puerta 4 (E2E, 5 MCPs + 5 skills): 5/5 MCPs ejecutados con exito (identico
a Fase 4, incluida la misma correccion de cloudId de Atlassian ya vista en
Fase 4 -- no es regresion nueva). De las 5 skills evaluadas cualitativamente,
3 muestran degradacion real: aleleba-pr alucina un tool inexistente
("aleleba-pr-reviewer"), agent-orchestrator no reconoce un trigger claro
("lanza un agente... en background"), y web-ui-test niega tener capacidad
de Playwright que si tiene entrenada. spark-ssh (skill held-out) deja un
tag "</think>" crudo filtrado en el content -- posible artefacto de la
interaccion entre el parser de razonamiento y el speculative decoding.
Regresion real y no trivial vs. Fase 4 en las 3 puertas. Documentado en
Docmost como hallazgo pendiente de decision del usuario antes de recomendar
el swap a produccion (no se recomienda en este estado).
Identico a vllm-eval-nvfp4 pero sin --speculative-config, para aislar si la
regresion de calidad observada en las puertas 2-3 (vs. Fase 4) viene del
speculative decoding (MTP) o de la cuantizacion NVFP4 en si -- decision
explicita del usuario antes de invertir tiempo en recalibrar con mas
muestras de calibracion.
Diagnostico pedido por el usuario antes de invertir tiempo en recalibrar:
mismo checkpoint NVFP4 (256 muestras), mismo contenedor de eval, unica
diferencia es remover --speculative-config (servicio vllm-eval-nvfp4-nospec,
puerto 8003).
Resultado: la regresion persiste casi identica sin speculative-config.
Puerta 2: 95.5% (191/200) sin spec vs 94.5% (189/200) con spec vs 98.5%
(197/200) de Fase 4 -- diferencia de 1pt entre con/sin spec, dentro de
ruido esperado; ambas configuraciones quedan ~3-4pts debajo de Fase 4.
Puerta 3: 90% (9/10) sin spec, identico a 90% (9/10) con spec -- mismo caso
puntual falla en ambas corridas (aleleba-pr/adherencia), aunque el texto
alucinado especifico difiere entre corridas (no determinismo esperable de
vLLM con batching dinamico incluso a temperature=0).
Conclusion: el speculative decoding (MTP) NO es el causante de la
regresion -- persiste identica sin el. El causante es la cuantizacion
NVFP4 en si (probablemente calibracion insuficiente de los 256 expertos
MoE con solo 256 muestras). Recalibrar con mas muestras, como estaba
previsto condicionalmente, es ahora el paso indicado.
Nueva variable NUM_ULTRACHAT_SAMPLES (default 0, sin cambio de comportamiento):
cuando > 0, mezcla esa cantidad de muestras de HuggingFaceH4/ultrachat_200k
(split train_sft, el mismo corpus/split que uso RedHatAI en su receta de
referencia) con (NUM_CALIBRATION_SAMPLES - NUM_ULTRACHAT_SAMPLES) muestras de
TRAIN_DATA_PATH, concatenadas y mezcladas (shuffle, seed=42) antes de
tokenizar para calibracion.
Hipotesis a probar (decision del usuario tras ver que el aislamiento sin
--speculative-config descarto al speculative decoding como causante de la
regresion, y antes de simplemente aumentar la cantidad de muestras propias):
la regresion podria venir de poca DIVERSIDAD tematica en la calibracion
(solo conversaciones angostas de los 5 MCPs/skills del proyecto) en vez de
poca cantidad de muestras. La porcion de TRAIN_DATA_PATH sigue usando el
mismo seed=42, asi que con NUM_ULTRACHAT_SAMPLES=256 y
NUM_CALIBRATION_SAMPLES=512, las 256 muestras propias son identicas a las
del primer intento (256 solo propias).
3 intentos seguidos de calibracion mezclada (256 ultrachat + 256 propias)
crashearon con el mismo CUDA OOM reproducible, siempre en el mismo punto
exacto (setup interno de oneshot(): trace_subgraphs/disable_lm_head), tanto
con MAX_SEQUENCE_LENGTH=8192 como =2048 -- descartando el largo de secuencia
como causa. La unica variable real frente a los intentos que SI funcionaron
(256 muestras solo propias) es la inclusion de ultrachat_200k.
Causa raiz identificada: load_dataset(..., split="train_sft") sin streaming
materializa el split completo (~208k ejemplos) como Arrow local, y ademas
genera las 4 splits del repo (~2.9GB en disco). En este hardware (GB10,
memoria unificada CPU/GPU) ese cache extra parece ser suficiente para
empujar el proceso sobre el limite justo en el momento de mayor presion de
memoria del setup de oneshot(). Fix: cargar con streaming=True + shuffle de
buffer + take(n), que solo trae los N ejemplos necesarios sin materializar
el dataset completo -- probado de forma aislada (256 ejemplos en ~12s, sin
crecimiento de cache en disco).
Correccion de diagnostico: la conclusion anterior ("incluir ultrachat_200k
dispara el OOM") era incorrecta. Evidencia: la corrida v2 (NUM_CALIBRATION_
SAMPLES=1024, SIN ultrachat) crasheo con el mismo CUDA OOM exacto, en el mismo
punto exacto del setup de oneshot() (disable_lm_head onload), mientras que la
corrida v3 (512 muestras propias) habia progresado bien mas alla de ese mismo
punto (trace_subgraphs completo) antes de ser detenida manualmente. El patron
real es presion de memoria total acumulada en el pool unificado del GB10
(modelo mmap'd de 67GB + construccion del checkpoint cuantizado + allocations
CUDA + maquinaria de `datasets`/pyarrow para ultrachat), no una propiedad
especifica de ultrachat_200k.
Fix: separar la preparacion de datos de calibracion (que puede requerir
`datasets`/streaming/red para ultrachat) de la cuantizacion (que carga el
modelo completo) en dos procesos distintos. --prepare-calibration construye y
guarda a disco (CALIBRATION_CACHE_PATH) la muestra ya tokenizada SIN cargar el
modelo; la cuantizacion normal detecta el cache y lo carga desde disco (sin
volver a tocar `datasets`/red) antes de cargar el modelo. Se agrega tambien un
gc.collect() explicito antes de cargar el modelo.
Checkpoint cuantizado con calibracion mezclada (256 propias + 256
ultrachat_200k, fix de dos fases), contenedor de eval propio CON
--speculative-config real (levanto healthy, MTP compartiendo embeddings/
lm_head con el modelo target, confirmado en logs).
Puerta 2: 95.0% validos (190/200) vs 94.5% (189/200) solo-propia vs 98.5%
(197/200) Fase 4 -- mejora marginal de +0.5pt sobre solo-propia, sigue
~3.5pts debajo de Fase 4. Persiste el mismo caso de nombre de tool
alucinado (getJiraProjectIssueTypes, no existe) visto en el intento
solo-propia.
Puerta 3: 90.0% (9/10), IDENTICO a solo-propia -- mismo caso exacto falla
(aleleba-pr/adherencia). La diversidad de ultrachat NO corrigio esta
regresion especifica.
Conclusion: la hipotesis de diversidad tematica en la calibracion no
resuelve la regresion observada. El checkpoint mezclado queda como una
alternativa equivalente (no mejor, no peor de forma significativa) al de
solo-datos-propios.
Mejoras permanentes al test, no solo para esta corrida:
1. max_tokens: 512 -> 2048 (configurable via GATE3_MAX_TOKENS). Con
--reasoning-parser activo, un modelo de razonamiento puede agotar 512
tokens pensando antes de emitir el contenido final -- la respuesta queda
cortada a mitad de razonamiento y check_adherencia() no encuentra ninguna
substring esperada, un FAIL por presupuesto de tokens agotado, no por
adherencia real. El caso que fallaba en las corridas NVFP4 de Fase 5
(aleleba-pr/adherencia, el prompt mas abierto de los tres) es sospechoso
de este defecto -- el JSON de resultados no guardaba el texto de la
respuesta, asi que no se podia auditar.
2. Guardar content+reasoning completos en cada fila del JSON de resultados
(antes solo se guardaba passed/hits/tool_calls). Permite auditar con
criterio humano cualquier fallo futuro sin tener que re-correr el test.
Hipotesis del usuario confirmada: el "FAIL" de aleleba-pr/adherencia en las
corridas NVFP4 anteriores (90%, 9/10) era un defecto del test, no del
modelo. Con max_tokens=512 y --reasoning-parser activo, el modelo (de
razonamiento) agotaba el presupuesto de tokens pensando antes de emitir el
contenido final -- la respuesta quedaba cortada, sin ninguna de las
substrings esperadas.
Re-corrida completa contra el checkpoint NVFP4 con calibracion mezclada
(mismo checkpoint de la comparacion anterior), con el test corregido
(max_tokens=2048): 100% (10/10), IDENTICO al baseline de Fase 4 BF16. El
caso de aleleba-pr ahora pasa (hits=['commit']); el texto completo
(reasoning + content, ahora guardado en el JSON) muestra que el modelo
describe correctamente el flujo (commit con prefijo fix: seguido de
aleleba-pr para armar el PR) -- sin la alucinacion vista antes ("git push
--force", "aleleba-pr-reviewer").
Esto invalida la regresion de la puerta 3 documentada en los hallazgos
anteriores de esta fase -- era un artefacto de medicion, no una
degradacion real de calidad introducida por la cuantizacion.
Mismo defecto que se encontro y corrigio en gate3 (commit 77e6804): con
--reasoning-parser activo, max_tokens=1024 podia dejar cortar la respuesta a
mitad de razonamiento antes de emitir el tool_call. Desglose por tipo de
fallo entre corridas: Fase 4 BF16 tuvo CERO casos "no_tool_call" (0/200);
ambas corridas NVFP4 (solo-propia y mezclada) tuvieron 4/200 -- la firma
exacta de un modelo cortado a mitad de razonamiento, no de una regresion de
calidad real.
Mejoras permanentes al test:
1. max_tokens: 1024 -> 2048 (configurable via GATE2_MAX_TOKENS). timeout de
request subido de 120s a 240s.
2. Se guarda content+reasoning+finish_reason completos en cada fila del
JSON de resultados (antes solo tool_calls/valid/errors), para poder
auditar con criterio humano cualquier caso que falle o quede sin
tool_call, sin tener que re-correr el test.
Re-corrida completa con el test corregido (max_tokens=2048, commit
2d2c45f), incluyendo tambien el baseline BF16 re-medido con el MISMO limite
(el original de Fase 4 se midio con max_tokens=1024) para una comparacion
justa:
- BF16 (2048 tokens): 97.5% (195/200), 1 no_tool_call, 4 invalid -- vs
98.5% (197/200), 0 no_tool_call, 3 invalid del baseline original de Fase
4 (1024 tokens). El propio BF16 varia levemente al re-medir con mas
tokens (no-determinismo de vLLM con batching dinamico + mas espacio para
"reconsiderar" casos ambiguos).
- NVFP4 mezclado (2048 tokens): 96.0% (192/200), 6 no_tool_call, 2 invalid
-- vs 95.0% (190/200) con 1024 tokens.
Con el mismo limite de tokens, la brecha real BF16 vs NVFP4 se achica de
~4pts (comparacion original, asimetrica) a ~1.5pts (comparacion justa).
Auditados con criterio humano todos los casos no_tool_call/invalid de
ambos: ninguno tiene finish_reason=length (sin truncamiento) -- son
decisiones genuinas del modelo sobre prompts ambiguos (merge condicional
sin instruccion explicita, deteccion de patron DoS en tabla de 371
columnas, falta de contexto real como pageId/spaceId) presentes en AMBOS
checkpoints, no una debilidad especifica de la cuantizacion.
aleleba
merged commit ff32318ac3 into master2026-07-30 06:41:14 -06:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
scripts/21_quantize_nvfp4.pythat clones the RedHatAI recipe, supports mixed calibration (own data +ultrachat_200k, streaming), and separates calibration-data prep from the actual quantization step (which loads the 67GB model) into two processes to avoid a real unified-memory OOM discovered during this phase.vllm-qwen36config 1:1 (including--speculative-configfor MTP) plus a no-speculative diagnostic variant, both isolated from production (Portainer /vllm-qwen36were never touched).--reasoning-parseractive) and re-measures the BF16 baseline under the same conditions for a fair comparison.Result
The final checkpoint is NVFP4 with mixed calibration (256 samples from
data/train.jsonl+ 256 fromultrachat_200k), 23.35GB, located at/home/aleleba/ft-models/Qwen3.6-35B-A3B-mcp-NVFP4on spark. This checkpoint is NOT included in this PR / not checked into git (it lives outside the repo, on spark storage).Verified integrity: MTP and vision tensors reinjected, no NaN/Inf, production
chat_templatepreserved. The dedicated eval container running the real--speculative-configcame up healthy with no errors, confirming the MTP tensors match vLLM's expectations.After fixing the two test defects and re-measuring the BF16 baseline with the same token limit (2048) for a fair comparison:
Out of scope / manual follow-up required
The production swap (moving the current model to
.bak, copying the new NVFP4 checkpoint to~/models/, restarting thevllm-qwen36service) is not part of this PR and remains a manual pending step for the user.Changes
scripts/21_quantize_nvfp4.py--prepare-calibrationand--verify-onlymodesdocker-compose.eval.ymlvllm-eval-nvfp4(port 8002, mirrors production config incl.--speculative-config) andvllm-eval-nvfp4-nospec(port 8003, diagnostic variant)scripts/32_gate2_toolcalls.pymax_tokens1024→2048, saves full content/reasoning/finish_reason, configurable results filename via env varscripts/33_gate3_adherencia.pymax_tokens512→2048, saves full content/reasoning/finish_reason, configurable results filename via env varscripts/34_gate4_e2e.pydata/gate{2,3,4}_results_nvfp4*.jsondata/gate2_results_bf16_2048.jsonTest Plan
--speculative-configstarts healthy.bak, copy to~/models/, restartvllm-qwen36) — pending, out of scope for this PRPara reusar los scripts de Fase 4 contra el endpoint NVFP4 nuevo sin sobreescribir los resultados de Fase 4 (data/gate{2,3,4}_results.json, ya commiteados como baseline de comparacion). Default sin cambios cuando la env var no esta seteada.Contenedor de eval propio (vllm-eval-nvfp4, puerto 8002) levanto healthy y sirvio con --speculative-config real (mtp, num_speculative_tokens=1) sin errores -- confirma que los tensores MTP reinyectados calzan correctamente con vLLM (puerta 0 superada). Puerta 2 (tool-calls, 200 prompts held-out): 94.5% validos (189/200) vs 98.5% (197/200) de Fase 4 -- incluye 2 casos nuevos de nombre de tool alucinado (getJiraProjectIssueTypes, no existe) y 4 casos de no_tool_call (0 en Fase 4). Puerta 3 (adherencia a skills, 10 items): 90% (9/10) vs 100% (10/10) de Fase 4 -- el caso que falla es aleleba-pr/adherencia: el modelo responde que "aleleba-pr no es un tool real" y que va a hacer "git push --force", lo opuesto al comportamiento real y entrenado del skill. Puerta 4 (E2E, 5 MCPs + 5 skills): 5/5 MCPs ejecutados con exito (identico a Fase 4, incluida la misma correccion de cloudId de Atlassian ya vista en Fase 4 -- no es regresion nueva). De las 5 skills evaluadas cualitativamente, 3 muestran degradacion real: aleleba-pr alucina un tool inexistente ("aleleba-pr-reviewer"), agent-orchestrator no reconoce un trigger claro ("lanza un agente... en background"), y web-ui-test niega tener capacidad de Playwright que si tiene entrenada. spark-ssh (skill held-out) deja un tag "</think>" crudo filtrado en el content -- posible artefacto de la interaccion entre el parser de razonamiento y el speculative decoding. Regresion real y no trivial vs. Fase 4 en las 3 puertas. Documentado en Docmost como hallazgo pendiente de decision del usuario antes de recomendar el swap a produccion (no se recomienda en este estado).Correccion de diagnostico: la conclusion anterior ("incluir ultrachat_200k dispara el OOM") era incorrecta. Evidencia: la corrida v2 (NUM_CALIBRATION_ SAMPLES=1024, SIN ultrachat) crasheo con el mismo CUDA OOM exacto, en el mismo punto exacto del setup de oneshot() (disable_lm_head onload), mientras que la corrida v3 (512 muestras propias) habia progresado bien mas alla de ese mismo punto (trace_subgraphs completo) antes de ser detenida manualmente. El patron real es presion de memoria total acumulada en el pool unificado del GB10 (modelo mmap'd de 67GB + construccion del checkpoint cuantizado + allocations CUDA + maquinaria de `datasets`/pyarrow para ultrachat), no una propiedad especifica de ultrachat_200k. Fix: separar la preparacion de datos de calibracion (que puede requerir `datasets`/streaming/red para ultrachat) de la cuantizacion (que carga el modelo completo) en dos procesos distintos. --prepare-calibration construye y guarda a disco (CALIBRATION_CACHE_PATH) la muestra ya tokenizada SIN cargar el modelo; la cuantizacion normal detecta el cache y lo carga desde disco (sin volver a tocar `datasets`/red) antes de cargar el modelo. Se agrega tambien un gc.collect() explicito antes de cargar el modelo.Hipotesis del usuario confirmada: el "FAIL" de aleleba-pr/adherencia en las corridas NVFP4 anteriores (90%, 9/10) era un defecto del test, no del modelo. Con max_tokens=512 y --reasoning-parser activo, el modelo (de razonamiento) agotaba el presupuesto de tokens pensando antes de emitir el contenido final -- la respuesta quedaba cortada, sin ninguna de las substrings esperadas. Re-corrida completa contra el checkpoint NVFP4 con calibracion mezclada (mismo checkpoint de la comparacion anterior), con el test corregido (max_tokens=2048): 100% (10/10), IDENTICO al baseline de Fase 4 BF16. El caso de aleleba-pr ahora pasa (hits=['commit']); el texto completo (reasoning + content, ahora guardado en el JSON) muestra que el modelo describe correctamente el flujo (commit con prefijo fix: seguido de aleleba-pr para armar el PR) -- sin la alucinacion vista antes ("git push --force", "aleleba-pr-reviewer"). Esto invalida la regresion de la puerta 3 documentada en los hallazgos anteriores de esta fase -- era un artefacto de medicion, no una degradacion real de calidad introducida por la cuantizacion.