<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Cascade-Attention on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/cascade-attention/</link><description>Recent content in Cascade-Attention on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Mon, 08 Jun 2026 05:40:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/cascade-attention/index.xml" rel="self" type="application/rss+xml"/><item><title>El especialista del plato estrella: el backend de atención de vLLM (FlashAttention, FlashInfer y la asimetría prefill/decode)</title><link>https://blog.lo0.es/posts/backend-atencion-vllm-flashinfer/</link><pubDate>Mon, 08 Jun 2026 05:40:00 +0200</pubDate><guid>https://blog.lo0.es/posts/backend-atencion-vllm-flashinfer/</guid><description>&lt;blockquote>
&lt;p>Sigue la serie &lt;em>por debajo del motor&lt;/em>. El &lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">post de PagedAttention&lt;/a> explicó &lt;strong>dónde&lt;/strong> vive el KV (en bloques paginados). Este explica &lt;strong>quién lo lee y cómo&lt;/strong>: el kernel de atención. Y conecta con &lt;a href="https://blog.lo0.es/posts/flashattention-fundamentos/">FlashAttention v1-v4&lt;/a>, que desmontó &lt;em>cómo&lt;/em> es ese kernel por dentro; aquí miramos el nivel de arriba —cómo vLLM &lt;strong>elige&lt;/strong> entre varios kernels y por qué necesita más de uno—.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Un forward de un LLM es, en su mayor parte, multiplicaciones de matrices estándar que cualquier librería hace bien. La excepción que decide el rendimiento es la &lt;strong>atención&lt;/strong>, y no basta con tener un kernel bueno: hacen falta &lt;strong>dos&lt;/strong>, porque las dos fases de la inferencia son problemas &lt;strong>físicamente opuestos&lt;/strong>. El &lt;strong>prefill&lt;/strong> procesa el prompt entero: muchas queries contra muchas keys, denso y &lt;strong>compute-bound&lt;/strong> —el terreno del tiling IO-aware de &lt;a href="https://blog.lo0.es/posts/flashattention-fundamentos/">FlashAttention&lt;/a>—. El &lt;strong>decode&lt;/strong> genera un token: una sola query contra &lt;strong>todo el KV acumulado&lt;/strong>, flaco y &lt;strong>memory-bound&lt;/strong> —aquí lo único que importa es saturar el ancho de banda de la HBM leyendo el KV paginado—. Por eso vLLM no tiene &amp;ldquo;el kernel de atención&amp;rdquo; sino un &lt;strong>backend conmutable&lt;/strong> (FLASH_ATTN, FLASHINFER, TRITON_ATTN…) y una lógica que &lt;strong>elige según la GPU&lt;/strong>: por defecto FA4 en Blackwell (SM100), FA3 en Hopper (SM90), FA2 en lo demás, con &lt;strong>FlashInfer&lt;/strong> como alternativa que compila kernels a medida (JIT) y sabe hacer &lt;strong>cascade attention&lt;/strong> para prefijos compartidos. Este post explica por qué prefill y decode son opuestos (con la intensidad aritmética), cómo el backend lee KV paginado, cómo el motor elige, qué aporta FlashInfer, los 10 knobs y la trampa de fijar un backend a ciegas. Sobre el cluster genérico 4×H100 SXM.&lt;/p>
&lt;h2 id="dónde-estás-el-especialista-no-el-pinche">Dónde estás: el especialista, no el pinche&lt;/h2>
&lt;p>En la cocina, casi todo el trabajo es picar y saltear: operaciones estándar que cualquier pinche competente ejecuta —son las multiplicaciones de matrices de las capas &lt;em>feed-forward&lt;/em> y las proyecciones—. Hay &lt;strong>un solo plato&lt;/strong> que no se delega: el plato estrella, el que define al restaurante. Ese plato es la &lt;strong>atención&lt;/strong>, y tiene una particularidad: se cocina &lt;strong>de dos maneras radicalmente distintas&lt;/strong> según el momento del servicio.&lt;/p>
&lt;p>Durante el &lt;em>prefill&lt;/em> —cuando llega una comanda nueva con su prompt entero— hay que cocinar a lo grande: mucha materia prima de golpe, mucho fuego, una operación intensa que llena los fogones. Durante el &lt;em>decode&lt;/em> —cuando una mesa pide &amp;ldquo;un plato más&amp;rdquo;— hay que cocinar a la carta: un solo plato, pero hay que ir a la despensa y &lt;strong>traer todos los ingredientes que esa mesa ha acumulado&lt;/strong> durante toda su comida. Uno es un problema de &lt;strong>potencia de fuego&lt;/strong>; el otro, de &lt;strong>velocidad de la despensa&lt;/strong>. No los hace bien el mismo especialista. Por eso vLLM tiene varios, y un jefe que decide cuál entra según la GPU y la fase. Eso es el &lt;strong>backend de atención&lt;/strong>.&lt;/p>
&lt;h2 id="por-qué-prefill-y-decode-son-problemas-opuestos">Por qué prefill y decode son problemas opuestos&lt;/h2>
&lt;p>Esta es la idea central, y se demuestra con una sola cuenta: la &lt;strong>intensidad aritmética&lt;/strong> (FLOPs por byte leído). Una operación con intensidad alta está limitada por el cómputo; una con intensidad baja, por la memoria.&lt;/p>
&lt;p>&lt;strong>Prefill.&lt;/strong> Atendemos $N$ queries (todo el prompt) contra $N$ keys. La operación $QK^\top$ y la $\text{softmax}\cdot V$ hacen del orden de $N^2 d$ FLOPs y leen del orden de $N d$ datos. La intensidad crece con $N$:&lt;/p>
&lt;p>$$I_\text{prefill} \sim \frac{N^2 d}{N d} = N$$&lt;/p>
&lt;p>Con $N$ grande (un prompt de miles de tokens), la intensidad es alta: &lt;strong>compute-bound&lt;/strong>. Es donde el tiling de FlashAttention exprime los tensor cores y donde se acerca a los TFLOPS de pico de la GPU.&lt;/p>
&lt;p>&lt;strong>Decode.&lt;/strong> Atendemos &lt;strong>una&lt;/strong> query (el token nuevo) contra $L$ keys (todo el KV acumulado). FLOPs del orden de $L d$; bytes leídos del orden de $L d s$ (hay que &lt;strong>leer el KV entero&lt;/strong> de la HBM). La intensidad es:&lt;/p>
&lt;p>$$I_\text{decode} \sim \frac{L d}{L d s} = \frac{1}{s} \quad (\approx 0,5 \text{ FLOP/byte en FP16})$$&lt;/p>
&lt;p>Constante y diminuta: &lt;strong>memory-bound&lt;/strong>. El kernel de decode no está limitado por cuánto puede calcular la GPU sino por &lt;strong>cuán rápido lee el KV de la HBM&lt;/strong>. Da igual que la H100 tenga 132 SMs ociosos (&lt;a href="https://blog.lo0.es/posts/sm-cuda-streams-cuda-graphs-inferencia/">ver el post de SMs&lt;/a>): el cuello es el ancho de banda de 3,35 TB/s, y el kernel de decode existe para no desperdiciar ni uno de esos bytes/s.&lt;/p>
&lt;svg viewBox="0 0 720 220" xmlns="http://www.w3.org/2000/svg" font-family="sans-serif" font-size="12" role="img" aria-label="Prefill compute-bound vs decode memory-bound">
&lt;line x1="60" y1="180" x2="700" y2="180" stroke="currentColor" stroke-width="1.5"/>
&lt;line x1="60" y1="180" x2="60" y2="30" stroke="currentColor" stroke-width="1.5"/>
&lt;text x="40" y="34" fill="currentColor" font-size="11" transform="rotate(-90 40 34)">rendimiento&lt;/text>
&lt;text x="380" y="205" text-anchor="middle" fill="currentColor" font-size="11">intensidad aritmética (FLOP/byte)&lt;/text>
&lt;path d="M60 180 L300 60" stroke="#16a34a" stroke-width="2" fill="none"/>
&lt;line x1="300" y1="60" x2="700" y2="60" stroke="#16a34a" stroke-width="2"/>
&lt;text x="500" y="52" fill="#16a34a" font-size="11">techo de cómputo (TFLOPS pico)&lt;/text>
&lt;circle cx="95" cy="160" r="5" fill="#2563eb"/>&lt;text x="105" y="158" fill="#2563eb">decode (I≈0,5): memory-bound — lo limita el BW de HBM&lt;/text>
&lt;circle cx="430" cy="60" r="5" fill="#7c3aed"/>&lt;text x="330" y="48" fill="#7c3aed">prefill (I≈N): compute-bound — lo limita el cómputo&lt;/text>
&lt;text x="70" y="120" fill="currentColor" font-size="10">la rampa = región&lt;/text>
&lt;text x="70" y="134" fill="currentColor" font-size="10">memory-bound&lt;/text>
&lt;/svg>
&lt;p>La consecuencia de diseño: un kernel optimizado para prefill (tiling denso, máxima ocupación de tensor cores) &lt;strong>no es&lt;/strong> el óptimo para decode (lecturas coalescidas del KV paginado, latencia mínima). Los servidores serios tienen kernels distintos —o un kernel con dos caminos—. En los modelos con &lt;strong>MLA&lt;/strong> (atención latente multi-cabeza), vLLM llega a usar &lt;strong>backends separados&lt;/strong> para prefill y decode, seleccionables de forma independiente (&lt;a href="https://docs.vllm.ai/en/latest/design/attention_backends/">attention backends, vLLM&lt;/a>).&lt;/p>
&lt;h2 id="el-truco-del-scheduler-prefill-y-decode-en-el-mismo-forward">El truco del scheduler: prefill y decode en el mismo forward&lt;/h2>
&lt;p>Aquí cierra el círculo con el &lt;a href="https://blog.lo0.es/posts/scheduler-step-vllm/">post del scheduler&lt;/a>. Como vLLM V1 mezcla en cada step peticiones en prefill y en decode, &lt;strong>un mismo forward&lt;/strong> tiene que atender las dos cosas. El backend recibe metadatos que le dicen, para cada secuencia del batch, cuántas queries trae y cuánto KV tiene que leer, y aplica el camino que toca a cada una. Por eso el backend de atención y el scheduler están acoplados: el primero tiene que digerir el batch heterogéneo que el segundo arma.&lt;/p>
&lt;h2 id="cómo-lee-el-backend-el-kv-paginado">Cómo lee el backend el KV paginado&lt;/h2>
&lt;p>El kernel no recibe un tensor de KV contiguo: recibe la &lt;strong>block table&lt;/strong> del &lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">block manager&lt;/a> y hace un &lt;strong>gather&lt;/strong> sobre los bloques físicos. Esto impone una restricción real al backend: tiene que soportar el &lt;em>layout&lt;/em> paginado y el &lt;code>block_size&lt;/code> de vLLM. No todos los kernels del mundo lo hacen; los que vLLM integra (FlashAttention, FlashInfer, Triton) están adaptados a leer KV en bloques de tamaño fijo dispersos por la HBM. Es la razón de que no puedas enchufar cualquier kernel de atención de un paper: tiene que hablar el idioma de la despensa por casilleros.&lt;/p>
&lt;h2 id="los-backends-y-cómo-elige-el-motor">Los backends y cómo elige el motor&lt;/h2>
&lt;p>vLLM expone una &lt;strong>abstracción de backend&lt;/strong> con varias implementaciones (&lt;a href="https://deepwiki.com/vllm-project/vllm/8.2-flashattention-and-flashinfer">deepwiki vLLM&lt;/a>):&lt;/p>
&lt;ul>
&lt;li>&lt;strong>FLASH_ATTN&lt;/strong> — la familia &lt;a href="https://blog.lo0.es/posts/flashattention-fundamentos/">FlashAttention&lt;/a>. Por defecto se elige la versión según la arquitectura: &lt;strong>FA4 en SM100 (Blackwell), FA3 en SM90 (Hopper), FA2 en el resto&lt;/strong>, configurable con &lt;code>flash_attn_version&lt;/code>.&lt;/li>
&lt;li>&lt;strong>FLASHINFER&lt;/strong> — motor de atención con compilación &lt;strong>JIT&lt;/strong> y &lt;em>kernels&lt;/em> especializables; fuerte en KV heterogéneo y prefijos compartidos.&lt;/li>
&lt;li>&lt;strong>TRITON_ATTN&lt;/strong> — escrito en Triton, portable y sin depender de binarios CUDA precompilados (&lt;a href="https://vllm.ai/blog/2026-03-04-vllm-triton-backend-deep-dive">Triton backend deep dive, vLLM, mar-2026&lt;/a>).&lt;/li>
&lt;li>Backends específicos para &lt;strong>MLA&lt;/strong> y para hardware no-NVIDIA.&lt;/li>
&lt;/ul>
&lt;p>La selección es &lt;strong>automática&lt;/strong> salvo que la fuerces con &lt;code>VLLM_ATTENTION_BACKEND&lt;/code>. La heurística prueba FlashAttention primero; en Blackwell (SM100) el orden de respaldo para MLA es TRT-LLM Ragged → FlashInfer → otros; en otras GPUs solo se considera FlashAttention para el camino principal (&lt;a href="https://docs.vllm.ai/en/latest/design/attention_backends/">attention backends, vLLM&lt;/a>). La decisión depende de: &lt;strong>arquitectura&lt;/strong> (SM), &lt;strong>dtype&lt;/strong> (FP16/BF16/FP8), &lt;strong>dimensión de cabeza&lt;/strong>, y si la carga necesita una &lt;strong>feature&lt;/strong> que solo un backend tiene (cascade attention, ciertos &lt;em>soft caps&lt;/em>, FP8 en KV).&lt;/p>
&lt;svg viewBox="0 0 720 220" xmlns="http://www.w3.org/2000/svg" font-family="sans-serif" font-size="12" role="img" aria-label="Selección de backend de atención">
&lt;rect x="270" y="20" width="180" height="36" rx="6" fill="none" stroke="currentColor" stroke-width="1.5"/>
&lt;text x="360" y="43" text-anchor="middle" fill="currentColor">¿qué arquitectura (SM)?&lt;/text>
&lt;rect x="40" y="100" width="160" height="36" rx="6" fill="none" stroke="#7c3aed" stroke-width="1.5"/>
&lt;text x="120" y="123" text-anchor="middle" fill="#7c3aed">SM100 Blackwell → FA4&lt;/text>
&lt;rect x="280" y="100" width="160" height="36" rx="6" fill="none" stroke="#2563eb" stroke-width="1.5"/>
&lt;text x="360" y="123" text-anchor="middle" fill="#2563eb">SM90 Hopper → FA3&lt;/text>
&lt;rect x="520" y="100" width="160" height="36" rx="6" fill="none" stroke="#16a34a" stroke-width="1.5"/>
&lt;text x="600" y="123" text-anchor="middle" fill="#16a34a">resto → FA2&lt;/text>
&lt;path d="M320 56 L120 100" stroke="currentColor" stroke-width="1.2" marker-end="url(#c)"/>
&lt;path d="M360 56 V100" stroke="currentColor" stroke-width="1.2" marker-end="url(#c)"/>
&lt;path d="M400 56 L600 100" stroke="currentColor" stroke-width="1.2" marker-end="url(#c)"/>
&lt;rect x="200" y="170" width="320" height="36" rx="6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-dasharray="4 3"/>
&lt;text x="360" y="193" text-anchor="middle" fill="currentColor">¿feature especial? (cascade, FP8 KV, MLA) → FlashInfer / específico&lt;/text>
&lt;path d="M360 136 V170" stroke="currentColor" stroke-width="1.2" marker-end="url(#c)"/>
&lt;defs>&lt;marker id="c" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">&lt;path d="M0 0 L8 4 L0 8 z" fill="currentColor"/>&lt;/marker>&lt;/defs>
&lt;/svg>
&lt;h2 id="qué-aporta-flashinfer-jit-y-cascade-attention">Qué aporta FlashInfer: JIT y cascade attention&lt;/h2>
&lt;p>FlashInfer no compite con FlashAttention en &amp;ldquo;ser un poco más rápido&amp;rdquo;; ataca un problema distinto: la &lt;strong>heterogeneidad&lt;/strong> del KV en servicio real (&lt;a href="https://arxiv.org/abs/2501.01005">FlashInfer, arXiv 2501.01005&lt;/a>). Dos ideas:&lt;/p>
&lt;p>&lt;strong>Compilación JIT.&lt;/strong> En lugar de un kernel monolítico, FlashInfer genera kernels &lt;strong>a medida&lt;/strong> para la variante de atención, la forma del problema y el layout del KV que tengas, inyectando &lt;em>functors&lt;/em> (transformaciones de query/key/logits, máscaras). Especializa en vez de generalizar.&lt;/p>
&lt;p>&lt;strong>Cascade attention.&lt;/strong> Aquí está la joya para servicio con prefijos compartidos. Si $R$ peticiones comparten un prefijo de $P$ tokens (un system prompt común), la atención ingenua leería ese prefijo $R$ veces. La cascade attention lo &lt;strong>calcula una vez&lt;/strong> contra el prefijo compartido y luego combina con el sufijo propio de cada petición:&lt;/p>
&lt;p>$$\text{lecturas: } \underbrace{R \cdot (P + s_i)}&lt;em>{\text{ingenua}} ;\longrightarrow; \underbrace{P + \textstyle\sum_i s_i}&lt;/em>{\text{cascade}}$$&lt;/p>
&lt;p>Con $R=50$ peticiones y un prefijo $P=1000$, eso es leer 50.000 tokens de prefijo frente a 1.000. Es el &lt;strong>complemento natural&lt;/strong> del &lt;a href="https://blog.lo0.es/posts/prefix-cache-hit-rate-engineering/">prefix caching&lt;/a>: el block manager comparte la &lt;em>memoria&lt;/em> del prefijo, y la cascade attention comparte el &lt;em>cómputo&lt;/em> de atender sobre él.&lt;/p>
&lt;h2 id="las-matemáticas-que-importan-cuándo-cambiar-de-backend-te-da-algo">Las matemáticas que importan: cuándo cambiar de backend te da algo&lt;/h2>
&lt;p>El backend solo mueve la aguja &lt;strong>donde la atención es el cuello&lt;/strong>. En decode memory-bound, un kernel que aprovecha mejor el ancho de banda de HBM da una mejora real; en prefill compute-bound con secuencias largas, FA3/FA4 acercándose al pico de tensor cores da una mejora real. Pero si tu cuello está en otra capa —el &lt;a href="https://blog.lo0.es/posts/sm-cuda-streams-cuda-graphs-inferencia/">launch overhead&lt;/a>, el &lt;a href="https://blog.lo0.es/posts/scheduler-step-vllm/">scheduler&lt;/a> mal dimensionado, el &lt;a href="https://blog.lo0.es/posts/del-disco-a-la-hbm-cold-start-carga-modelo/">cold start&lt;/a>— cambiar de backend &lt;strong>no toca esa parte&lt;/strong>. La regla, otra vez: medir el régimen antes de optimizar.&lt;/p>
&lt;h2 id="los-10-knobs">Los 10 knobs&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>#&lt;/th>
&lt;th>Knob&lt;/th>
&lt;th>Qué controla&lt;/th>
&lt;th>Coste / riesgo&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>&lt;code>VLLM_ATTENTION_BACKEND&lt;/code>&lt;/td>
&lt;td>forzar backend&lt;/td>
&lt;td>mismatch con hardware/feature&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>&lt;code>flash_attn_version&lt;/code> (2/3/4)&lt;/td>
&lt;td>versión de FA&lt;/td>
&lt;td>versión no soportada en tu SM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>habilitar FlashInfer&lt;/td>
&lt;td>JIT + cascade&lt;/td>
&lt;td>tiempo de compilación JIT inicial&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>cascade attention&lt;/td>
&lt;td>reuso de cómputo de prefijo&lt;/td>
&lt;td>solo ayuda con prefijo muy compartido&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>&lt;code>kv_cache_dtype&lt;/code> (FP8)&lt;/td>
&lt;td>soporte FP8 en el kernel&lt;/td>
&lt;td>no todos los backends/SM lo soportan&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6&lt;/td>
&lt;td>&lt;code>block_size&lt;/code>&lt;/td>
&lt;td>layout que el kernel debe leer&lt;/td>
&lt;td>coherencia con PagedAttention&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>backend de prefill MLA&lt;/td>
&lt;td>kernel de la fase densa&lt;/td>
&lt;td>solo modelos MLA&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>backend de decode MLA&lt;/td>
&lt;td>kernel de la fase flaca&lt;/td>
&lt;td>solo modelos MLA&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>9&lt;/td>
&lt;td>soft cap / sliding window&lt;/td>
&lt;td>features que limitan backends&lt;/td>
&lt;td>menos opciones de kernel&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>10&lt;/td>
&lt;td>head_dim / variante&lt;/td>
&lt;td>qué kernels son elegibles&lt;/td>
&lt;td>modelos exóticos sin soporte&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="cómo-se-conecta-con-el-resto-del-stack">Cómo se conecta con el resto del stack&lt;/h2>
&lt;p>&lt;strong>Con FlashAttention.&lt;/strong> El &lt;a href="https://blog.lo0.es/posts/flashattention-fundamentos/">post de FA&lt;/a> explica el kernel por dentro (tiling, online softmax, FA1-4); este es el nivel de arriba —cómo vLLM elige entre kernels y por qué necesita más de uno—.&lt;/p>
&lt;p>&lt;strong>Con PagedAttention.&lt;/strong> El backend &lt;strong>lee&lt;/strong> el KV que el &lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">block manager&lt;/a> coloca en bloques; tiene que hablar el idioma del block table.&lt;/p>
&lt;p>&lt;strong>Con el scheduler.&lt;/strong> El &lt;a href="https://blog.lo0.es/posts/scheduler-step-vllm/">scheduler&lt;/a> arma batches mixtos prefill+decode; el backend tiene que atender los dos regímenes en un solo forward.&lt;/p>
&lt;p>&lt;strong>Con los CUDA graphs.&lt;/strong> Los kernels de atención se capturan en los &lt;a href="https://blog.lo0.es/posts/sm-cuda-streams-cuda-graphs-inferencia/">CUDA graphs&lt;/a>; un backend que lanza muchos kernels pequeños se beneficia más de la captura.&lt;/p>
&lt;p>&lt;strong>Con el prefix caching.&lt;/strong> La cascade attention es el lado &lt;em>cómputo&lt;/em> de lo que el &lt;a href="https://blog.lo0.es/posts/prefix-cache-hit-rate-engineering/">prefix caching&lt;/a> hace en &lt;em>memoria&lt;/em>.&lt;/p>
&lt;p>&lt;strong>Con FP8.&lt;/strong> Atender sobre KV en &lt;a href="https://blog.lo0.es/posts/fp8-end-to-end-pesos-kv-calidad/">FP8&lt;/a> requiere que el backend tenga el camino FP8; no todos lo tienen en toda arquitectura.&lt;/p>
&lt;h2 id="trampas-y-cosas-que-no-son-lo-que-parecen">Trampas y cosas que no son lo que parecen&lt;/h2>
&lt;p>&lt;strong>&amp;ldquo;FlashInfer siempre es más rápido que FlashAttention.&amp;rdquo;&lt;/strong> No. FlashInfer gana cuando su especialización (cascade, KV heterogéneo, una variante de atención concreta) aplica a tu carga; en prefill denso clásico, FA3/FA4 suele ir igual o mejor. Depende del régimen, no hay un ganador universal.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;Un buen kernel de atención sirve para todo.&amp;rdquo;&lt;/strong> El error de fondo de este post. Prefill y decode son compute-bound y memory-bound respectivamente; un kernel ajustado a uno desperdicia en el otro. Por eso existen caminos separados (y backends separados en MLA).&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;El decode es compute-bound porque la GPU está al 100%.&amp;rdquo;&lt;/strong> El &lt;code>nvidia-smi&lt;/code> al 100% engaña (&lt;a href="https://blog.lo0.es/posts/sm-cuda-streams-cuda-graphs-inferencia/">ver el post de SMs&lt;/a>): el decode es &lt;strong>memory-bound&lt;/strong>, la GPU está moviendo KV, no calculando. Optimizar el cómputo del decode es pulir lo que no es el cuello.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;Fijo &lt;code>VLLM_ATTENTION_BACKEND&lt;/code> y me olvido.&amp;rdquo;&lt;/strong> Fijar un backend a mano puede dejarte en uno subóptimo cuando cambias de GPU o de versión, o forzar un fallback lento si tu hardware no soporta lo que pediste. La autoselección suele acertar; fíjalo solo con una medida que lo justifique.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;La cascade attention siempre ayuda.&amp;rdquo;&lt;/strong> Solo con prefijo &lt;strong>muy compartido&lt;/strong> entre muchas peticiones concurrentes. Si cada petición tiene su propio contexto, no hay nada que compartir y el overhead de organizar la cascada no se amortiza.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;El backend de atención es el cuello, por eso voy lento.&amp;rdquo;&lt;/strong> Casi siempre el cuello está más arriba (lanzamiento, scheduling, memoria) o más abajo (ancho de banda). El backend importa donde la atención domina; mídelo con &lt;code>nsys&lt;/code>/DCGM antes de cambiarlo.&lt;/p>
&lt;h2 id="conclusión">Conclusión&lt;/h2>
&lt;p>De todo lo que hace un LLM al generar texto, casi todo son multiplicaciones de matrices que cualquier librería resuelve. El rendimiento se juega en un solo kernel —la atención— y la sorpresa es que ni siquiera es &lt;em>un&lt;/em> kernel: son dos problemas opuestos disfrazados del mismo nombre. El prefill quiere fuego —cómputo denso sobre miles de tokens— y el decode quiere despensa rápida —leer todo el KV de un token con el mínimo desperdicio de ancho de banda—. Por eso vLLM no eligió un kernel ganador sino una abstracción que conmuta: FlashAttention afinado a cada arquitectura para el caso general, FlashInfer compilando a medida cuando hay heterogeneidad o prefijos que compartir, Triton para portabilidad. El jefe de cocina no cocina el plato estrella de una sola manera: mira quién pide y en qué momento del servicio, y manda al especialista que toca. La lección para quien tunea es la de siempre en esta serie: antes de cambiar de especialista, asegúrate de que el plato estrella es de verdad lo que te está frenando.&lt;/p>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/flashattention-fundamentos/">FlashAttention v1/v2/v3/v4&lt;/a> — el kernel por dentro; este post es el nivel de arriba (cómo se elige entre kernels).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">PagedAttention y el block manager&lt;/a> — el KV paginado que el backend lee vía block table.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/scheduler-step-vllm/">El pase: el scheduler step de vLLM&lt;/a> — el batch mixto prefill+decode que el backend digiere en un forward.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/sm-cuda-streams-cuda-graphs-inferencia/">SM, CUDA streams y CUDA graphs&lt;/a> — por qué el &lt;code>nvidia-smi&lt;/code> al 100% no significa compute-bound, y dónde se capturan los kernels de atención.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/prefix-cache-hit-rate-engineering/">Prefix cache hit rate engineering&lt;/a> — el lado memoria de lo que la cascade attention hace en cómputo.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/fp8-end-to-end-pesos-kv-calidad/">FP8 end-to-end: pesos y KV&lt;/a> — el camino FP8 que el backend necesita soportar.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/continuous-batching-fundamentos/">Continuous batching&lt;/a> — por qué un forward tiene que atender prefill y decode a la vez.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache: la memoria de trabajo&lt;/a> — el dato que el kernel de decode lee entero en cada paso.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>vLLM, &lt;em>Attention Backends&lt;/em> (selección, FA2/3/4 por arquitectura, MLA): &lt;a href="https://docs.vllm.ai/en/latest/design/attention_backends/">https://docs.vllm.ai/en/latest/design/attention_backends/&lt;/a>.&lt;/li>
&lt;li>vLLM / DeepWiki, &lt;em>FlashAttention and FlashInfer&lt;/em>: &lt;a href="https://deepwiki.com/vllm-project/vllm/8.2-flashattention-and-flashinfer">https://deepwiki.com/vllm-project/vllm/8.2-flashattention-and-flashinfer&lt;/a>.&lt;/li>
&lt;li>vLLM, &lt;em>Triton Attention Backend Deep Dive&lt;/em> (mar-2026): &lt;a href="https://vllm.ai/blog/2026-03-04-vllm-triton-backend-deep-dive">https://vllm.ai/blog/2026-03-04-vllm-triton-backend-deep-dive&lt;/a>.&lt;/li>
&lt;li>Z. Ye et al., &lt;em>FlashInfer: Efficient and Customizable Attention Engine for LLM Inference Serving&lt;/em> (arXiv 2501.01005): &lt;a href="https://arxiv.org/abs/2501.01005">https://arxiv.org/abs/2501.01005&lt;/a>.&lt;/li>
&lt;li>T. Dao, &lt;em>FlashAttention-2&lt;/em> / &lt;em>FlashAttention-3&lt;/em> (kernel IO-aware, async Hopper): &lt;a href="https://github.com/Dao-AILab/flash-attention">https://github.com/Dao-AILab/flash-attention&lt;/a>.&lt;/li>
&lt;/ul></description></item></channel></rss>