Cuando una GPU no basta: multi-GPU y P/D disaggregation
Tu server mono-GPU va bien hasta que el p99 de inter-token latency se dispara. El culpable suele ser un prompt de 30K tokens que entra mientras diez requests generan: todo se detiene hasta que el prefill termina. Un solo vllm serve hace tres trabajos que se estorban entre sí: prefill (limitado por compute, define tu TTFT), decode (limitado por memoria, define tu ITL) y un montón de CPU work — tokenizar, parsear tool calls, des-tokenizar — corriendo en la caja que alquilaste por sus GPUs. Este capítulo cierra la guía separando esos trabajos cuando ya no comparten GPU con dignidad.
Qué compra y qué no
Lo que compras es goodput: la tasa de requests que sostienes cumpliendo SLOs de TTFT e ITL a la vez. Si no tienes objetivo de latencia, partir las mismas GPUs en pools de prefill y decode no necesariamente mueve más tokens por segundo. Sin que un prefill de 30K tokens ponga en pausa todos los streams de decode, el p99 deja de ser una lotería.
Los números del post oficial de vLLM, medidos en una caja con 2×L40S y prompts de ~8K tokens: colocado, el p99 de ITL salta de 23 ms a 169 ms a 0.4 req/s; con P/D separados se queda en 29 ms y nunca pasa de 52 ms. En un nodo AMD de 8×MI300X, separar prefill y decode subió el goodput de 30 a 73 de cada 100 requests cumpliendo ambos SLOs: 2.4x en el mismo hardware. Fuente: Taking vLLM Apart, 29 de septiembre de 2026.
Antes de emocionarte, la cuenta que decide si te aplica: la KV cache viaja del prefiller al decoder. Llama-3.1-70B en BF16 almacena 320 KiB por token — un prompt de 10K son ~3 GB cruzando la red antes del primer token. Si el transfer es lento, cada primer token paga el peaje. Primero nvidia-smi topo -p2p r: si dice NS entre tus GPUs, olvídalo en una sola caja. Segundo, mira la línea KV Transfer metrics en el log del decoder: si Avg xfer time está en cientos de milisegundos, arregla eso antes de medir cualquier otra cosa.
El experimento mínimo: dos GPUs y un proxy
# Prefiller en GPU 0
CUDA_VISIBLE_DEVICES=0 vllm serve Qwen/Qwen2.5-7B-Instruct --port 8100 \
--kv-transfer-config '{"kv_connector":"NixlConnector","kv_role":"kv_producer"}'
# Decoder en GPU 1
CUDA_VISIBLE_DEVICES=1 vllm serve Qwen/Qwen2.5-7B-Instruct --port 8200 \
--kv-transfer-config '{"kv_connector":"NixlConnector","kv_role":"kv_consumer"}'
# Proxy que encadena: prefill primero, decode después
python tests/v1/kv_connector/nixl_integration/toy_proxy_server.py --port 8192 \
--prefiller-hosts localhost --prefiller-ports 8100 \
--decoder-hosts localhost --decoder-ports 8200
El truco del proxy: manda el request al prefiller con max_tokens=1 y do_remote_decode: true. El prefiller computa la KV cache, la retiene y devuelve unos kv_transfer_params que apuntan a esos bloques; el decoder los jala por NIXL y genera. Para producción ese proxy de ejemplo no sirve — los orquestadores reales son llm-d y Dynamo.
Y la medición honesta, con el mismo bench que usas desde el capítulo 6 de la guía anterior, ahora con goodput:
vllm bench serve --model Qwen/Qwen2.5-7B-Instruct --port 8192 \
--dataset-name random --random-input-len 8192 --random-output-len 256 --ignore-eos \
--request-rate 0.4 --num-prompts 100 \
--percentile-metrics ttft,tpot,itl --metric-percentiles 50,99 \
--goodput ttft:2000 tpot:30
Sube --request-rate hasta que el p99 del server colocado colapse. Ese punto es tu decisión de arquitectura.
Multi-GPU sin separar nada
Antes de partir el pipeline, las opciones en una sola instancia: tensor parallelism (--tensor-parallel-size) parte el modelo en capas entre GPUs; expert parallelism reparte los expertos de un MoE; pipeline parallelism apila etapas en nodos distintos. Para modelos que no caben, TP es el default. Para MoE gigantes en clusters, EP. Y si tu tráfico de MoE varía, Elastic EP escala expertos arriba y abajo sin reiniciar el server — near-zero downtime, publicado en octubre de 2026.
Recuerda del capítulo 3: PP no compone con speculative decoding. Y para agentes multi-turn, P/D con bidirectional_kv_xfer reutiliza la KV que el decoder ya tiene del turno anterior en vez de recomputarla — TTFT hasta 3.3x mejor en turnos largos. Eso sí: revisa que tu chat template no descarte los thinking traces entre turnos, porque entonces los bloques de decode ya no coinciden con el prompt y obtienes salida incorrecta, no solo lenta.
La tabla de decisión
| Tu situación | Recomendación |
|---|---|
| p99 de ITL rompe tu SLO en producción | Disagrega. Es EL caso de uso |
| Prompts largos a alta concurrencia | Disagrega, si tu transfer es rápido |
| Chat o agentes con contexto creciente | Disagrega con transfer bidireccional + KV offloading del capítulo 4 |
| TTFT es tu restricción | Quédate colocado, o mide primero: el transfer cae en cada primer token |
| Tráfico bajo, en ráfagas, sin SLO estricto | Quédate colocado. Tres servicios por uno no es gratis |
Checkpoint y el error típico
Checkpoint: tienes el número de tu p99 ITL a la carga que te duele, y sabes si tu fabric puede mover KV rápido. Con eso, la decisión de separar o no es aritmética, no fe.
El error típico: benchmarking P/D con un modelo de 0.6B. Un modelo tan chico prefiella tan rápido que apenas estorba al decode — no hay nada que separar. Usa un 7B o más, con prompts de tu workload real, y mide el turno 5 de una conversación, no el turno 1.
Cierre de la guía
Cinco capítulos y tu servidor ya: habla JSON con schema garantizado (cap. 1), cabe en la GPU que pagas (cap. 2), responde más rápido con especulación (cap. 3), sobrevive a tus agentes con la escalera de memoria (cap. 4) y separa el trabajo cuando la concurrencia lo pide (cap. 5). El siguiente paso es de orquestación: llm-d o Dynamo sobre Kubernetes, que es otro viaje entero.
¿Qué vas a servir primero con todo esto?