Aplicado a tus proyectos · Pipeline-cloud
Tu pipeline NVFP4: meter 62 GB de modelo en 24 GB de laptop
El Capítulo 1 te dio la teoría de arquitecturas e inferencia en abstracto. Esto es esa misma teoría, más el pedazo que el curso oficial no cubre — cuantización — mirando directamente lo que ya corre en tu RTX 5090 y en "La Bestia".
Antes de leerHF cap. 1 §5, inferencia y KV cache · HF cap. 2 §5, vLLM · LLM 2026 · 1 §5, la geometría de Gemma 4
🎯 Al terminar vas a poder
- Hacer la cuenta de memoria de cualquier modelo (parámetros × bytes) y estimar cuánto ahorra pasar de BF16 a 4 bits con escalas por bloque.
- Explicar con tus palabras qué es NVFP4 (E2M1 + bloques de 16 + escala FP8) y por qué la elección de escala (absmax vs MSE) decide si el checkpoint sirve.
- Distinguir cuantización de pesos y de activaciones, PTQ y QAT, y decir por qué la calibración necesita datos reales.
- Explicar por qué el decoding especulativo no cambia la distribución de salida, y de qué depende el speedup.
- Verificar un checkpoint cuantizado leyendo el header de sus
safetensors, en vez de confiar en el log.
0. Qué construiste
main_nvfp4.py orquesta un pipeline de 5 etapas (parser LLM → generación de imagen → video →
narración → edición final), todo corriendo con pesos en NVFP4 para caber en hardware Blackwell. Pero la
pieza que más vale la pena entender a fondo — porque la reusás en otro proyecto tuyo,
interpretante-lacaniano — es cómo se produce el
checkpoint cuantizado de Gemma-4 en sí. Esa parte vive en
gemma4_quant_strategy.md y los scripts bootstrap_gemma4_qat.sh /
bootstrap_gemma4_finetuning.sh.
Tu pipeline produce tres checkpoints, cada uno con su propio origen:
| Checkpoint | Viene de |
|---|---|
gemma-4-31B-it-qat-NVFP4-Blackwell | Modelo base de Google, sin fine-tune |
gemma-4-31B-it-qat-assistant-NVFP4-Blackwell | El draft MTP del modelo base |
gemma-4-31B-analyst-NVFP4-Blackwell | Tu fine-tune propio (LoRA con Unsloth, mergeado a FP16 antes de cuantizar) |
🔧 En tu proyecto
El mismo pipeline de cuantización es compartido entre Pipeline-cloud y
interpretante-lacaniano — así se produjo también gemma-4-31B-lacan-NVFP4-Blackwell, el
"cuerpo" del sistema lacaniano. No son proyectos separados a nivel de infraestructura de modelos: es
una sola fábrica de checkpoints con distintos consumidores.
1. La arquitectura: Gemma-4 + su asistente MTP
Del Capítulo 1 ya sabés que Gemma es decoder-only. Tu documentación agrega el detalle real: Gemma-4 es
densa (no MoE, no Mamba — a diferencia de lo que asumía un borrador viejo de tu propia
estrategia, corregido en el commit 0b8b647), con atención local/global estilo Gemma-3.
Lo que el Capítulo 1 no menciona porque es demasiado reciente: al lado del modelo grande vive un segundo
modelo, de apenas 0.5B de parámetros, dedicado exclusivamente a predecir por
adelantado los próximos tokens — Gemma4AssistantForCausalLM. No es un modelo separado
que "adivina" por su cuenta: consume
directamente los shared_kv_states (los mismos estados de atención full_attention /
sliding_attention) que ya calculó el modelo grande. Es, literalmente, una segunda cabeza de
predicción pegada al mismo backbone — por eso se llama MTP, Multi-Token Prediction.
🔧 En tu proyecto
serve_lacan_vllm.sh carga los dos a la vez: MODEL_DIR (el cuerpo, 31B) y
DRAFT_DIR (el asistente MTP). Verlos como "un modelo" es el error más fácil de cometer acá
— son dos checkpoints con reglas de cuantización distintas, como vas a ver en la sección 3.
2. Qué es realmente "NVFP4"
El Capítulo 1 nunca explica cuantización porque el curso original de HF asume que el modelo entra en memoria tal cual. Tu laptop te obligó a resolver este problema de verdad: Gemma-4 31B en BF16 pesa ~62 GB, y tu RTX 5090 tiene 24 GB de VRAM.
La cuenta de memoria, en general
Antes de cualquier detalle de formato, la aritmética que gobierna todo esto es una multiplicación: memoria de pesos = cantidad de parámetros × bytes por parámetro. Con eso ya podés estimar qué entra y qué no en cualquier GPU, para cualquier modelo:
| Formato | Bits por peso | 31B parámetros | Qué se pierde |
|---|---|---|---|
| FP32 | 32 | ~124 GB | Nada — es como se entrena normalmente (o BF16 con acumulación FP32) |
| BF16 | 16 | ~62 GB | Mantisa corta, pero mismo rango que FP32; el default de inferencia |
| FP8 (E4M3) | 8 | ~31 GB | Rango y precisión reducidos; suele pedir escalas por tensor o por canal |
| 4 bits (E2M1 / int4) | 4 | ~15,5 GB | Solo 16 valores representables por peso — sin escalas por bloque sería inusable |
| NVFP4 real | 4 + escalas | ~17,5 GB | Una escala E4M3 (8 bits) cada 16 pesos agrega 0,5 bit por peso: 4,5 bits efectivos |
La última fila es la que importa: 31 × 10⁹ × 4,5 / 8 ≈ 17,4 GB. Esa es la cuenta de los pesos cuantizados solos — a eso hay que sumarle las capas que se dejan en BF16 (§3), el draft en BF16 (§4) y el KV cache, que crece con la longitud de contexto y con la cantidad de requests simultáneas. Por eso "entra en 24 GB" nunca es una propiedad del checkpoint: es una propiedad del checkpoint más la configuración de serving.
La idea de fondo de cualquier cuantización es simple: en vez de guardar cada peso del modelo con 16 bits de precisión (BF16), lo guardás con muchos menos bits, aceptando algo de error a cambio de un modelo drásticamente más chico. NVFP4 es el formato de 4 bits que NVIDIA diseñó específicamente para que las GPU Blackwell (tu RTX 5090, y la RTX 6000 de "La Bestia") lo puedan multiplicar en hardware sin pasar por un paso de conversión lento.
E2M1
Cada peso individual se guarda en 4 bits: 1 de signo, 2 de exponente, 1 de mantisa. Muy pocos valores representables — por sí solo sería demasiado impreciso.
Bloques de 16
Los pesos no se cuantizan uno por uno: se agrupan de a 16, y ese bloque comparte una única escala.
Escala E4M3 por bloque
Esa escala compartida sí se guarda con más precisión (FP8, 4 bits de exponente + 3 de mantisa) — es lo que le devuelve rango dinámico al bloque entero.
📐 Fundamento matemático: bitfield E2M1 y cuantización jerárquica
Un valor en punto flotante empaquetado en 4 bits bajo el estándar E2M1 se descompone en un bit de signo , dos bits de exponente sesgado con sesgo , y un bit de mantisa . La evaluación numérica del valor decodificado responde a la formulación IEEE por partes:
Evaluando las combinaciones posibles para : ; ; . El reticulado discreto resultante es exactamente:
Para cuantizar un peso continuo en un bloque de 16 elementos con escala de bloque y escala global del tensor , el mapeo al reticulado y la reconstrucción aproximada son:
Un detalle más del formato, que explica por qué una escala de 8 bits alcanza: NVFP4 usa dos niveles de escala. Cada bloque de 16 tiene su escala E4M3, y además todo el tensor tiene una única escala global en FP32. La escala de bloque no tiene que cubrir el rango completo de la matriz — solo el rango relativo de su bloque respecto del tensor. Los 16 valores de E2M1 son exactamente estos: 0, 0,5, 1, 1,5, 2, 3, 4 y 6, con sus negativos. Cada peso real se guarda como "uno de esos 16 × la escala de su bloque × la escala del tensor".
Tres preguntas que cualquier esquema de cuantización tiene que responder
1. ¿Qué se cuantiza: pesos, activaciones, o ambos? La notación habitual es WxAy: bits de pesos y bits de activaciones. W4A16 guarda los pesos en 4 bits pero los desempaqueta a 16 bits para multiplicar — ahorra memoria y ancho de banda, pero el matmul sigue siendo el de siempre. W4A4 cuantiza también la activación (la entrada de cada capa) a 4 bits, en runtime, con sus propias escalas por bloque, para que el tensor core multiplique 4 bits × 4 bits directamente. NVFP4 en Blackwell es de este segundo tipo: por eso el hardware importa tanto — el kernel necesita un tensor core que sepa hacer esa multiplicación de forma nativa. Y por eso cuantizar activaciones es más delicado que cuantizar pesos: los pesos son fijos y se pueden estudiar con calma; las activaciones cambian con cada input y tienen outliers que dependen del texto.
2. ¿Cuándo se cuantiza: después de entrenar o durante?
PTQ (Post-Training Quantization) toma un modelo ya entrenado y le busca las escalas
con un pequeño conjunto de datos, sin volver a entrenar — es lo que hace hf_ptq.py, como
dice su nombre. QAT (Quantization-Aware Training) es otra cosa: durante el entrenamiento
(o un fine-tuning corto al final) el forward pass simula la cuantización — redondea pesos y
activaciones a los niveles que va a tener el formato final, y deja que el gradiente ajuste el modelo
para que tolere ese ruido. El resultado sigue siendo un checkpoint en alta precisión, pero cuyos pesos
ya "saben" que van a ser redondeados. El sufijo -qat del checkpoint base refiere a esa
técnica: un modelo preparado para cuantizarse con menos pérdida que uno entrenado sin pensar en eso. Lo
que tu pipeline hace después es PTQ sobre ese punto de partida — las dos cosas no compiten, se apilan.
3. ¿De dónde salen las escalas? Para los pesos, en principio de los pesos mismos — podés mirar el bloque y elegir. Pero las escalas de activación (W4A4) y cualquier criterio que mire el error de salida de una capa necesitan ver activaciones reales. De ahí la calibración: se pasan por el modelo unos cientos de muestras de texto y se registran los rangos y distribuciones de cada capa. Con pocos datos, o con datos que no se parecen a los de producción, las escalas quedan mal ajustadas — un modelo calibrado solo con inglés puede degradar en español, por ejemplo. No es entrenamiento: no hay gradiente, no cambian los pesos; solo se observan estadísticas para decidir cómo redondear.
📐 Derivación del objetivo de optimización MSE vs Absmax
Mientras que Absmax fija la escala del bloque forzando que el elemento de mayor magnitud absoluta coincida con el extremo del reticulado (), el algoritmo MSE formula la selección de escala como un problema variacional de minimización de error cuadrático:
El error cuadrático esperado se descompone analíticamente en dos fuentes contrapuestas:
Bajo distribuciones de pesos leptocúrticas (Gaussianas o Student-t), Absmax anula el error de truncamiento fijando sobre el outlier extremo, pero multiplica el error de granularidad en los 15 pesos restantes que residen cerca de cero. La optimización MSE contrae la escala a : acepta un pequeño truncamiento en el outlier y comprime la distancia entre escalones, reduciendo la varianza del ruido de cuantización en más de un 25%.
La parte interesante — y la que tu documentación registra con más cuidado que cualquier paper — es cómo se elige esa escala por bloque. No es un detalle menor: es la diferencia entre un modelo que sirve y uno que no.
| Algoritmo | Cómo elige la escala | En tu pipeline |
|---|---|---|
| Max / absmax | El valor más grande del bloque define la escala — un solo outlier aplasta el resto del bloque hacia cero | Lo que usabas antes |
MSE (nvfp4_mse) | Prueba distintas escalas y elige la que minimiza el error de reconstrucción del bloque | Lo que usás ahora — adoptado de la receta de NVIDIA para Nemotron 3 Super |
| Four-over-six | Cada bloque elige entre dos valores máximos posibles (4 o 6), el que minimice error | Evaluado, no adoptado — necesita ModelOpt ≥0.46 (aún en dev) |
🔧 En tu proyecto
El cambio de --qformat nvfp4 a --qformat nvfp4_mse en
hf_ptq.py fue la única línea que tocaste para este upgrade — nada de código propio, solo
un flag de ModelOpt. Costo real medido: calibración de ~3 min (absmax) a ~7 min (MSE) para el 31B en la
RTX 6000, porque MSE además barre escalas candidatas de activación en FP8. Diferencia despreciable
frente al tiempo de descarga/subida del checkpoint.
nvfp4 y nvfp4_mse en un bloque de 16 valores, calculada. Absmax
fija la escala para que el valor más grande quede exacto y deja a los demás con pocos niveles; la
búsqueda acepta recortar un poco al grande a cambio de resolver bien a los otros quince. Fue tu único
cambio de este ciclo, y es un flag.
3. Por qué no se cuantiza todo
Si cuantizaras literalmente cada matriz del modelo, algunas partes se rompen mucho más fácil que otras. Tu pipeline excluye tres tipos de capas, cada una por una razón distinta:
- Embeddings (
embed_tokens) ylm_head: excluidas por default en ModelOpt en cualquier preset NVFP4 — son las capas que traducen entre tokens discretos y espacio vectorial, y son especialmente sensibles a la pérdida de precisión. pre_projection/post_projectiondel asistente MTP: el "empalme" entre el backbone grande y el draft chico. Necesitaste un monkeypatch propio (patch_modelopt.py) para que la calibración corra conshared_kv_statessintéticos, e inyectar estos dos nombres enhf_ptq.pypara que nunca se cuanticen — sin importar qué modelo se esté procesando.- El propio draft model, completo: ver sección 4 — no es que se excluya una capa, es que el modelo entero se sirve en BF16.
📐 Análisis de sensibilidad de Hessian en capas críticas
La degradación de la función de pérdida del modelo ante una perturbación de pesos se modela formalmente mediante su expansión de Taylor de segundo orden:
En el mínimo local de convergencia de un modelo preentrenado, el vector gradiente , por lo que el impacto depende estrictamente de la curvatura de la matriz Hessiana .
En la cabeza final de predicción de vocabulario (, con ), cada fila genera un logit . El error inducido por cuantización altera las diferencias relativas entre los candidatos más probables. Si , el operador invierte la predicción del token óptimo, induciendo una catástrofe de perplejidad. Por esta razón, preservar e en 16 bits es un imperativo físico-matemático de estabilidad.
🔧 Bug real que ya pisaste
El 2026-07-10 un re-quant corrió contra un hf_ptq.py sin parchear porque los
marcadores .done del clone/patch eran de junio (previos a la exclusión MTP). Resultado:
pre_projection/post_projection quedaron cuantizadas a FP4 con las dimensiones
a la mitad — el mismo bug de shape-mismatch reportado públicamente por AQLabs. Se detectó inspeccionando
los headers de los safetensors antes de publicar. La regla que quedó: para re-cuantizar,
borrar todos los marcadores (rm -f data/.gemma4_qat_bootstrap/*.done), no
solo los de cuantización — los pasos de patch no tienen output verificable, así que un marcador viejo
se salta en silencio.
4. Servir rápido: KV cache + decoding especulativo
El Capítulo 1 explicó que generar texto es autoregresivo: un token depende de todos los anteriores, y el
KV cache evita recalcular la atención sobre toda la conversación en cada paso. Tu configuración de
serve_lacan_vllm.sh usa --kv-cache-dtype fp8 — el cache también se comprime,
con escalas dinámicas calculadas al vuelo durante el serving.
🔧 Un hallazgo tuyo que no está en ningún paper
Probaste exportar el checkpoint con escalas FP8 calibradas y guardadas para el KV cache
(--kv_cache_qformat fp8). Resultado: salida degenerada, byte-idéntica entre
el checkpoint absmax y el MSE — la prueba de que la falla estaba en las escalas de KV, no en los pesos.
Hipótesis más probable: las dimensiones heterogéneas de cabeza de Gemma-4
(head_dim=256, global_head_dim=512) fuerzan el backend TRITON_ATTN de vLLM,
que aplica mal esas escalas por capa. Conclusión operativa: --kv_cache_qformat none en el
checkpoint, y dejar que vLLM calcule las escalas dinámicamente en runtime. Sin pérdida real de ahorro
de memoria.
La parte que el Capítulo 1 ni siquiera menciona porque no la necesita para explicar transformers en general: decoding especulativo. La idea es contra-intuitiva — para generar más rápido, primero generás de más.
- El modelo chico (el draft MTP, 0.5B) propone varios tokens seguidos de una, rápido y barato — justamente su único trabajo es predecir tokens por adelantado, nada más.
- El modelo grande verifica esos tokens propuestos en paralelo, en una sola pasada — verificar es mucho más barato que generar token a token.
- Los tokens que el modelo grande acepta se guardan; en el primer rechazo, el grande genera él mismo ese token y el ciclo vuelve a empezar.
Por qué no cambia lo que el modelo diría
Lo primero que uno sospecha es que esto degrada la calidad: "si el chico propone, ¿no termino sirviendo el modelo chico?". No — y la razón es una regla precisa de aceptación (Leviathan et al., 2023; Chen et al., 2023). Llamemos q(x) a la probabilidad que el draft le dio al token propuesto x, y p(x) a la que le da el modelo grande al verificarlo:
- Si p(x) ≥ q(x), el grande lo acepta siempre: el chico propuso algo que el grande consideraba al menos igual de probable.
- Si p(x) < q(x), lo acepta con probabilidad p(x) / q(x) — el chico estaba "demasiado seguro", y se descuenta exactamente ese exceso.
- Si lo rechaza, el grande muestrea un token de reemplazo de la distribución máx(0, p − q), normalizada: justo la parte de p que el draft subestimó.
Sumando los tres casos, la probabilidad de que salga cada token es exactamente p(x) — la del modelo grande, token por token. Es la misma lógica que el muestreo por rechazo en estadística: la propuesta puede ser mala, y lo único que cambia es cuántas veces hay que reintentar, nunca qué distribución se obtiene. Con greedy (temperatura 0) el criterio se simplifica a "¿el token del draft coincide con el argmax del grande?", y la salida es idéntica a la del grande solo.
📐 Demostración formal de invarianza distribucional (Leviathan et al., 2023)
Sea la probabilidad asignada por el modelo borrador (draft) y la del modelo destino (target). La probabilidad de aceptación condicional es . La probabilidad total de que el draft sea aceptado en esa posición es:
En caso de rechazo (evento con probabilidad ), se muestrea de la distribución residual normalizada:
Calculando la probabilidad marginal de emitir el token por ley de probabilidad total:
Queda matemáticamente demostrado que la distribución final generada es estrictamente idéntica a la del modelo grande, sin aproximación ni sesgo estocástico.
De qué depende el speedup
Si el draft propone γ tokens por ronda y cada uno se acepta con probabilidad α (independiente, para simplificar), la cantidad esperada de tokens que salen por cada pasada del modelo grande es (1 − α^(γ+1)) / (1 − α) — el token rechazado (o el extra si acepta todos) también cuenta. El speedup real es ese número dividido por el costo relativo de la ronda: una pasada del grande más γ pasadas del draft. Dos consecuencias prácticas:
- α manda. Con α = 0,5 y γ = 3 salen ≈1,9 tokens por pasada; con α = 0,8, ≈3,0. Un draft que se parece mucho al grande (mismo tokenizer, entrenado sobre sus salidas, o — como acá — leyendo sus propios estados internos) vale más que uno más chico pero peor.
- El draft tiene que ser casi gratis. Si costara la mitad que el grande, γ = 3 ya consumiría 1,5 pasadas equivalentes y el speedup se evaporaría. Por eso el asistente MTP de 0,5B pegado al backbone es una configuración tan buena: no recalcula nada que el grande ya calculó.
Hay un segundo argumento, menos visible: decodificar un token a la vez es una operación limitada por ancho de banda de memoria (hay que leer los 17 GB de pesos enteros para producir un solo token), no por cómputo. Verificar γ+1 tokens en una pasada lee los pesos una sola vez para varios tokens — el hardware que estaba ocioso esperando memoria pasa a trabajar. Eso es lo que hace que "verificar varios" cueste casi lo mismo que "generar uno".
Medido por vos en La Bestia (vLLM 0.22.1, 300 tokens, temperatura 0.2, una sola request):
| Configuración | tok/s |
|---|---|
| Sin decoding especulativo | 23.0 |
| Con draft MTP, 3 tokens especulativos | 52.1 (2.26×) |
Con una tasa de aceptación real de 210/387 tokens (54%, 78% en la primera posición del lote propuesto).
Vale la pena poner esos dos números en la fórmula: con α ≈ 0,54 y γ = 3 salen ≈2 tokens por pasada del grande, del mismo orden que el 2,26× medido. La fórmula asume α constante en cada posición, y tu propia medición muestra que no lo es (78% en la primera, mucho menos después): los primeros tokens propuestos son casi siempre fáciles y los últimos casi siempre difíciles. Esa es la razón por la que subir γ tiene rendimientos decrecientes — cada token extra que propone el draft tiene menos chances de sobrevivir que el anterior.
🔧 La trampa que ya te costó un error en runtime
El draft tiene que quedarse sin cuantizar (0.5B parámetros, BF16, ~927 MB — la
cuenta cierra: 0.5×10⁹ parámetros × 2 bytes ≈ 1 GB). Si le pasás la versión
NVFP4 como draft, vLLM la carga sin aplicar la config de ModelOpt y explota con
RuntimeError: start (0) + length (8192) exceeds dimension size (4096) — está leyendo un
peso empaquetado en 4 bits como si fuera BF16. El ahorro de VRAM del draft nunca fue el punto: es tan
chico que no importa: lo que importa es que decodifique rápido y sin bugs de dtype.
5. La lección de ingeniería: verificá el artefacto, no el log
Tu propia documentación deja una regla que vale para cualquier pipeline de ML, no solo este: los logs de
hf_ptq.py imprimen texto degenerado en sus "example outputs" incluso para checkpoints que
después sirven perfecto — porque esa prueba interna no aplica el chat template. Un log que se ve mal no
significa un checkpoint roto, y viceversa: el bug del §3 pasó una corrida "exitosa" según el log.
Lo único que de verdad confirma que la cuantización salió bien es inspeccionar el artefacto: que
hf_quant_config.json liste pre_projection/post_projection en
exclude_modules, y que el header de safetensors muestre esas capas en BF16 con
sus dimensiones completas — un dtype U8 o dimensiones a la mitad es la señal inequívoca de que el patch
no corrió.
6. Lo que generaliza
Sacando los nombres propios, lo que esta página deja para cualquier proyecto que cuantice o sirva un modelo:
| Lección | Dónde la viste acá | Cómo se aplica en otro proyecto |
|---|---|---|
| La memoria se calcula, no se adivina | 62 GB en BF16 → ~17,5 GB en NVFP4 más lo que queda en BF16 y el KV cache | Antes de elegir GPU o formato, hacé la cuenta parámetros × bytes y sumá el cache a la longitud de contexto que vas a servir. |
| Nunca se cuantiza todo | Embeddings, lm_head, proyecciones del draft, y el draft entero | Identificá las capas que traducen entre espacios (entrada/salida, empalmes entre modelos) y dejalas en alta precisión por default. |
| La elección de escala es el algoritmo | absmax vs MSE, un solo flag | Cuando un checkpoint cuantizado "sirve mal", el primer sospechoso son las escalas, no el formato de 4 bits en sí. |
| Un modelo de dos piezas tiene dos reglas | Cuerpo NVFP4, draft BF16 | Cada componente que se carga por separado necesita su propia decisión de precisión y su propia verificación. |
| Los pasos sin output verificable se saltan en silencio | Marcadores .done viejos que dejaron pasar un patch | Los sistemas de "hacer una sola vez" necesitan invalidarse cuando cambia lo que hacen — o inspeccioná el resultado final en vez del marcador. |
| Verificá el artefacto, no el log | Headers de safetensors vs "example outputs" | Definí qué propiedad del archivo final prueba que el paso salió bien, y chequeala en código antes de publicar. |
7. Mapa de tu propio pipeline
Uniendo todo lo de arriba con la teoría del Capítulo 1:
- Gemma-4 es un decoder-only denso — la arquitectura dominante del Capítulo 1, sin sorpresas ahí.
- Un segundo modelo (MTP) vive pegado al primero para acelerar generación — una pieza que el curso oficial no cubre porque es demasiado nueva.
- NVFP4 comprime cada peso a 4 bits en bloques de 16 con una escala FP8 compartida; cómo se elige esa escala (MSE > absmax) fue tu único cambio real este ciclo.
- Embeddings,
lm_head, las proyecciones MTP y el draft completo se salvan de la cuantización — cada uno por una razón distinta y verificable. - El decoding especulativo te dio 2.26× de velocidad real, a costa de mantener el draft en BF16 y las escalas de KV en modo dinámico, no calibrado.
Este es exactamente el mismo cuerpo de conocimiento que sostiene a interpretante-lacaniano — con el añadido de RAG, fine-tuning generacional, y un motor de razonamiento simbólico encima.
8. Ponete a prueba
Respondé antes de abrir cada respuesta. Si fallás una, volvé a la sección indicada.
1. Un modelo de 8B parámetros en BF16, ¿cuánto pesa? ¿Y en NVFP4 con escalas de bloque de 16?
8 × 10⁹ × 2 bytes = 16 GB en BF16. En NVFP4 son 4 bits por peso más 8 bits de escala cada 16 pesos: 4,5 bits efectivos → 8 × 10⁹ × 4,5 / 8 ≈ 4,5 GB, sin contar las capas que se dejan en BF16 ni el KV cache. (§2)
2. ¿Por qué un bloque de 16 pesos con un solo outlier grande sale mal con absmax, y qué hace distinto MSE?
Con absmax la escala se fija para que el outlier caiga en el valor máximo representable (6); los otros 15 pesos, mucho más chicos, quedan aplastados en los niveles cercanos a cero y pierden casi toda su información. MSE prueba varias escalas y elige la que minimiza el error total del bloque: acepta recortar el outlier a cambio de resolver bien los 15 restantes. (§2)
3. ¿Qué diferencia hay entre W4A16 y W4A4, y por qué NVFP4 necesita Blackwell?
W4A16 guarda pesos en 4 bits pero multiplica en 16: ahorra memoria, no cómputo. W4A4 cuantiza también la activación en runtime y multiplica 4×4 en el tensor core. NVFP4 es W4A4: necesita un tensor core que haga esa multiplicación de forma nativa, que es lo que Blackwell agrega. (§2)
4. ¿Qué significa que el checkpoint base sea "-qat" y qué relación tiene con lo que hace hf_ptq.py?
QAT: durante el entrenamiento se simula la cuantización para que los pesos aprendan a tolerar el redondeo; el checkpoint sigue en alta precisión. hf_ptq.py hace PTQ: cuantiza ese checkpoint ya entrenado con datos de calibración, sin gradiente. Se apilan: QAT prepara, PTQ ejecuta. (§2)
5. Si el draft acepta el 30% de los tokens en vez del 54%, ¿qué pasa con el speedup? ¿Y con la calidad del texto?
El speedup baja: con α = 0,3 y γ = 3 salen ≈1,4 tokens por pasada del grande en vez de ≈2, y con el costo del draft puede llegar a ser más lento que sin especulación. La calidad no cambia en absoluto: la regla de aceptación garantiza que la distribución de salida es la del modelo grande, sea cual sea el draft. (§4)
6. Un log de cuantización imprime texto degenerado en sus ejemplos. ¿Qué concluís?
Nada todavía. Esa prueba interna no aplica el chat template, así que un log feo es compatible con un checkpoint sano — y un log lindo fue compatible con el bug de proyecciones cuantizadas. Lo que prueba algo es el artefacto: exclude_modules en hf_quant_config.json y los dtypes/shapes en el header de los safetensors. (§5)
9. Para practicar
✏️ Ejercicio 1 — Inspeccioná el artefacto: dtype y shape de cada tensor
Tomá cualquier checkpoint en formato safetensors que tengas a mano (uno cuantizado si
tenés, uno chico de Hugging Face si no) y listá cada tensor con su dtype y su forma. El header se lee
sin cargar los pesos a memoria, así que corre en CPU en segundos aunque el modelo pese decenas de
GB. Buscá: capas en U8 (empaquetadas en 4 bits), tensores de escala (nombres con scale),
y cualquier capa que debería estar en BF16 y no lo está.
# pip install safetensors
import glob, collections
from safetensors import safe_open
carpeta = "ruta/al/checkpoint"
por_dtype = collections.Counter()
for archivo in sorted(glob.glob(f"{carpeta}/*.safetensors")):
with safe_open(archivo, framework="pt") as f:
for nombre in f.keys():
t = f.get_slice(nombre) # solo metadata, no carga el tensor
dtype, shape = t.get_dtype(), tuple(t.get_shape())
por_dtype[dtype] += 1
if "proj" in nombre or "embed" in nombre or "lm_head" in nombre:
print(f"{dtype:<8} {str(shape):<22} {nombre}")
print(por_dtype)
Pista / qué esperar
En un checkpoint NVFP4 típico vas a ver las matrices grandes en U8 con la última dimensión a la mitad de la del modelo (dos valores por byte), sus escalas en F8_E4M3 con la última dimensión dividida por 16, y las capas excluidas en BF16 con dimensiones completas. Si una capa que tenía que estar excluida aparece en U8 con la dimensión a la mitad, ese es exactamente el bug del §3 — detectado sin cargar nada en la GPU. El Counter final te da la foto global: cuántos tensores quedaron en cada dtype.
✏️ Ejercicio 2 — La cuenta del KV cache a partir de un config
El KV cache guarda, por cada token del contexto y por cada capa, un vector K y uno V de tamaño
num_kv_heads × head_dim. Este script lee esa geometría de un config.json
cualquiera (o usa una de ejemplo estilo Llama-3-8B) y calcula cuánto ocupa el cache para una
longitud de contexto dada, en BF16 y en FP8. Corre en CPU, sin dependencias.
import json, sys
def kv_bytes_por_token(cfg, bytes_por_valor=2):
L = cfg["num_hidden_layers"]
kv_heads = cfg.get("num_key_value_heads", cfg["num_attention_heads"])
head_dim = cfg.get("head_dim") or cfg["hidden_size"] // cfg["num_attention_heads"]
return 2 * L * kv_heads * head_dim * bytes_por_valor # 2 = K y V
# cfg = json.load(open(sys.argv[1])) # el config.json de cualquier checkpoint
cfg = {"num_hidden_layers": 32, "num_attention_heads": 32,
"num_key_value_heads": 8, "hidden_size": 4096} # geometría estilo Llama-3-8B
for dtype, b in [("bf16", 2), ("fp8", 1)]:
por_token = kv_bytes_por_token(cfg, b)
print(f"{dtype}: {por_token/1024:.0f} KiB/token -> {por_token*26000/2**30:.2f} GiB para 26k tokens")
bf16: 128 KiB/token -> 3.17 GiB para 26k tokens · fp8: 64 KiB/token -> 1.59 GiB para 26k tokensPista / qué esperar
Dos cosas para probar después: (a) multiplicá por la cantidad de requests simultáneas que querés servir — el cache es por secuencia; (b) para un modelo con geometría heterogénea por capa, como Gemma-4 (head_dim distinto en capas de ventana y globales), la función tiene que sumar capa por capa en vez de multiplicar por L: ese es justo el tipo de detalle que un cálculo "global" se come, y el tema de la página de vLLM.
10. Fuentes y para seguir
- Introducing NVFP4 for Efficient and Accurate Low-Precision Inference (NVIDIA Developer Blog) — la descripción de NVIDIA del formato: E2M1, bloques de 16, escala E4M3 por bloque y escala FP32 por tensor. Leelo con el §2 abierto.
- NVIDIA TensorRT Model Optimizer — documentación — la herramienta detrás de
hf_ptq.py: formatos, calibración, y qué capas se excluyen por default. - Leviathan, Kalman & Matias — Fast Inference from Transformers via Speculative Decoding (arXiv:2211.17192) — el paper original: la regla de aceptación del §4 y la fórmula del speedup, con las demostraciones.
- Chen et al. — Accelerating Large Language Model Decoding with Speculative Sampling (arXiv:2302.01318) — la misma idea, desarrollada en paralelo por DeepMind; más corto y muy claro sobre por qué la distribución no cambia.
- DeepSeek-AI — DeepSeek-V3 Technical Report (arXiv:2412.19437) — la sección de Multi-Token Prediction: la idea de una cabeza extra que predice tokens por adelantado reusando el backbone, el antecedente del asistente MTP del §1.
- vLLM — documentación — buscá las secciones "Speculative Decoding" y "Quantization": los flags que usa
serve_lacan_vllm.sh, explicados por quienes los escribieron. - Gemma — documentación en ai.google.dev — las fichas de modelo y las variantes publicadas, para confirmar arquitectura y checkpoints disponibles sin depender de un borrador viejo.