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.
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).
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).
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.
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.
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).
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.
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.
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.
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 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.
Puerta 2 (tool-calls, 200 prompts held-out, parser real qwen3_coder de vLLM): 197/200
validos (98.5%). Los 3 invalidos son casos donde el prompt referencia un recurso por
nombre (space/repo) sin ID real -- el modelo elige la tool correcta pero omite un campo
requerido (spaceId/repo) que no puede conocer en un turno unico sin una llamada previa de
lookup; no es un fallo de sintaxis del parser.
Puerta 3 (adherencia por skill + no-activacion, 10 items sobre las 5 skills reales):
100% de aprobacion. vllm-qwen36 no estaba corriendo -- baseline de produccion documentado
como pendiente, no bloqueante.
Puerta 4 (E2E real contra los 5 MCPs, ejecutado por el agente orquestador con sus propios
MCPs conectados): 5/5 exitosos. El unico caso que requirio una segunda llamada fue
atlassian (el modelo adivino un cloudId plausible que no era el real -- se corrigio con
getAccessibleAtlassianResources y la llamada tuvo exito, comportamiento esperado en un
flujo multi-turno).
El primer resultado (promedio simple por ejemplo) daba 0.5185 vs 0.275 de Fase 3, señal de
alarma segun el propio script. La causa era metodologica, no un bug de merge: el bucket
replay concentra 112927 de los ~128849 tokens assistant del split de eval (87%), mientras
que buckets dificiles como negativos/skills_adherencia/delegacion_subagentes tienen pocos
ejemplos pero loss alto -- un promedio por ejemplo les da el mismo peso que a replay,
inflando el global. transformers.Trainer pondera por token, no por ejemplo. Con el mismo
ponderado por token: 0.2560 vs 0.275 de Fase 3 (diff=0.019, dentro del margen esperado) --
confirma que el merge es correcto.
- scripts/30_eval_suite.py --gate 1: eval-loss sobre el checkpoint mergeado, agrupado por
meta.bucket (aislando replay), comparado contra eval_loss=0.275 de Fase 3.
- docker-compose.eval.yml: servicio vllm-eval propio (puerto 8001), sirviendo el
checkpoint mergeado en BF16, con tool-call-parser=qwen3_coder y reasoning-parser=qwen3.
No se pudo leer el compose real de produccion (/data/compose/43/docker-compose.yml no
existe en spark, probablemente vive en el host del servidor Portainer) -- flags basados
en la arquitectura conocida del modelo.
- scripts/31_build_holdout_prompts.py: genera data/holdout_prompts.jsonl (200 prompts,
40 por MCP, sin overlap verificado contra train.jsonl/eval.jsonl).
- scripts/32_gate2_toolcalls.py: valida tool-calls devueltas por vllm-eval (parser real
de vLLM, nunca una regex propia) contra los 200 prompts held-out.
- scripts/33_gate3_adherencia.py: checklists de adherencia por skill + no-activacion,
con baseline opcional contra vllm-qwen36 si esta corriendo.
- scripts/34_gate4_e2e.py: arma el plan de llamadas E2E contra los 5 MCPs y 5 skills via
el checkpoint mergeado, para que el agente orquestador las ejecute con sus MCPs reales.
Remapea nombres de modulo (model.layers.* del adapter -> model.language_model.layers.*
del checkpoint base multimodal), preserva tensores mtp.*/visual.* al copiarlos sin
modificar, y toma chat_template.jinja de MODEL_PATH (nunca del adapter, que tiene el
template de masking de training). Verificacion automatica en verde: 310/310 tensores
LoRA-target mergeados, 1045/1045 tensores totales preservados, sin NaN/Inf, muestra de
200 tensores no-target byte-identica al base.
Los checkpoints intermedios (checkpoint-100/150/166, con optimizer.pt de
resumen de training) quedan ignorados -- no aportan nada mas alla del
peso final ya versionado aqui, y triplicarian el tamano sin necesidad.
El chat_template.jinja de produccion no tiene tags {% generation %}, por lo que
return_assistant_tokens_mask salia vacio para el 100% de los ejemplos en el primer run.
Se genero data/chat_template_train.jinja (copia exacta del template real, con {%- generation -%}
envolviendo solo el contenido/tool_calls/im_end de cada turno assistant) para el fallback
de masking ya anticipado en la Decision de diseno #4 del plan principal -- el chat_template.jinja
original no se toca, sigue siendo el que sirve produccion.
data/raw/replay.jsonl: 80 prompts semilla (conversacion general/codigo/
razonamiento) x 9 pasadas variando temperatura, contra vLLM de produccion
(vllm-qwen36), duracion real ~5h07min. Revisado: 720/720 lineas son JSON
valido con messages de 2 turnos y reasoning_content no vacio; 23 ejemplos
quedaron con content vacio por agotar el presupuesto de max_tokens durante
el razonamiento (finish_reason=length), marcados en meta para que la Fase 2
decida como tratarlos.
.gitignore: excepcion para versionar replay.jsonl pese a vivir en data/raw/,
como especifica el plan (es texto revisable, no un binario de checkpoint).
El script debe correr directo en el host de spark (sin contenedor), donde /workspace no existe.
Corrige a la ruta real del repo en el NFS compartido: /mnt/docker-nas/projects/ai-projects/qwen3-6-lora/data/raw/replay.jsonl
data/schemas/*.json: dump fiel de las tool definitions reales de
Penpot (4), Gitea (53), GitHub-personal (43), Docmost (17) y
Atlassian (37), obtenidas directo de las definiciones ya cargadas
en la sesion de Claude Code (no se escribio un cliente MCP nuevo,
para no arriesgar desviarse del esquema real).
PENPOT_DEPLOYMENT_NOTES.md: investigacion del codigo fuente oficial
de @penpot/mcp confirma que import_image y export_shape.filePath
estan ausentes porque este deployment corre en modo remoto/multi-
usuario (isFileSystemAccessEnabled() = !isRemoteMode()) -- documentado
tambien en una subpagina nueva de Docmost.
scripts/03_build_replay.py: genera ~700 ejemplos de replay (anti-
forgetting) contra el vLLM de produccion, 80 prompts semilla
(conversacion general/codigo/razonamiento) x 9 pasadas variando
temperatura. Mapea el campo "reasoning" de la API de vLLM a
"reasoning_content" para la convencion de chat template.
00_verify_hardware.py: flash_attn, sdpa y flash-linear-attention
funcionan los tres en el GB10 (SM121) via Triton JIT -- mejor de lo
esperado, ya no hace falta el fallback lento de PyTorch para Gated
DeltaNet. attn_implementation recomendado: flash_attention_2.
01_inspect_modules.py: resuelve la discrepancia de nombres de las
proyecciones de Gated DeltaNet inspeccionando la arquitectura real
(device_map=meta, sin pesos). Resultado: parcialmente fusionado --
in_proj_qkv (q+k+v en un solo Linear) pero in_proj_z, in_proj_a e
in_proj_b por separado. Confirma tambien que mlp.experts.{gate_up,down}_proj
son nn.Parameter 3D (no LoRA-ables) y que shared_expert.{gate,up,down}_proj
son nn.Linear normales, como anticipaba el plan.
constraints.txt: fija la version exacta de torch de la imagen NGC
(2.10.0a0+b4e4ee81d3.nv25.12) para instalar transformers/peft/trl/
accelerate/bitsandbytes sin romper el build ARM64/Blackwell.
Scaffolding inicial del repo: carpetas scripts/, data/schemas/,
data/raw/, out/, .gitignore para binarios/checkpoints, y el
docker-compose.yml del contenedor de training (imagen NGC pytorch
25.12-py3, GPU reservada, bind mounts a ai-projects vía NFS y a
~/ft-models en disco local rápido de spark) sin tocar jupyter-pyt
ni el vllm de producción.