Por qué dos tarjetas RTX 3090 alcanzan 55 tok/s en lugar de 220 con SGLang y DFlash2.
Una explicación práctica sobre cómo ejecutar Qwen3.8-27B en dos tarjetas 3090 con SGLang, y cómo los enlaces PCIe x4, la falta de P2P, una caché KV limitada y picos de energía redujeron la velocidad anunciada.
Un video corto se ha difundido entre los aficionados al auto-hosting. Muestra a Qwen3.8-27B escribiendo código en un par de tarjetas RTX 3090, con una velocidad de generación de 220 tokens por segundo. La configuración utiliza SGLang junto con DFlash2, un sistema de redacción especulativa. Ese modelo pequeño propone siete tokens futuros al mismo tiempo, y el modelo grande los acepta o rechaza en una sola pasada, por lo que las predicciones correctas no tienen costo alguno.
El atractivo va más allá de una simple demostración. A esa velocidad, un sistema podría utilizar un modelo alojado localmente, ejecutando varios agentes de programación uno al lado del otro en una sola máquina, sin claves API, sin facturación por uso y sin que los datos salgan del edificio. Un servidor doméstico con dos GPUs de clase 3090 parece ideal: el doble de ancho de banda de memoria, lo que seguramente permite alcanzar los 220 tokens por segundo.
No funciona de esa manera. Las razones son más instructivas que el propio video.
Instalación de SGLang
Cualquiera acostumbrado a llama.cpp, donde un binario y un archivo de modelo permiten tener una conversación en diez minutos, encontrará que SGLang es más exigente. Aquí está el procedimiento en un servidor Ubuntu.
Paso 1: el entorno de Python. SGLang tiene un gran árbol de dependencias, por lo que se crea su propio entorno virtual en Python 3.12 después de clonar el repositorio.
git clone https://github.com/sgl-project/sglang.git
cd sglang
python3.12 -m venv .venv
source .venv/bin/activate
Paso 2: una instalación complicada. El primer intento real de instalación duró aproximadamente 39 minutos, llenando el caché de paquetes con la mayor parte de los contenidos de PyPI, y luego se interrumpió debido a un error.
RuntimeError: cargo is required to discover the Rust extension modules
Un compilador de Rust para un servidor de inferencia en GPU. Las versiones más recientes de SGLang incluyen extensiones opcionales en Rust, y las herramientas de compilación se niegan a continuar sin cargo, aunque el motor nunca las utiliza. Configurar una variable de entorno permite omitirlas.
SGLANG_BUILD_RUST_EXTS=none pip install -e "./python[all]"
Esa segunda ejecución necesitó unos 30 minutos más y finalizó sin problemas. El resultado fue sglang 0.5.20, torch compilado para CUDA 13 y triton fijado automáticamente en 3.7.1, que resulta ser lo necesario para las GPUs Ampere para evitar un bug conocido en los kernels.
Paso 3: los modelos. Siguiendo la guía de la comunidad, se realizan dos descargas:
cyankiwi/Qwen3.8-27B-AWQ-INT4es el modelo principal. La cuantización INT4 lo reduce a aproximadamente 20 GB en 5 fragmentos, lo suficientemente pequeño para dos GPUs de 24 GB.z-lab/Qwen3.8-27B-DFlash2es la versión preliminar, de unos 3.9 GB.
Paso 4: el inicio de ejecución. Se hace con un solo comando, donde TP=2 indica paralelismo de tensores en ambas GPUs 3090.
CUDA_VISIBLE_DEVICES=1,2 python -m sglang.launch_server \
--model-path /mnt/models/Qwen3.8-27B-AWQ-INT4 \
--tp 2 \
--speculative-algorithm DFLASH \
--speculative-draft-model-path /mnt/models/Qwen3.8-27B-DFlash2 \
--speculative-num-draft-tokens 8 \
--mamba-radix-cache-strategy extra_buffer \
--disable-prefill-cuda-graph \
--cuda-graph-max-bs-decode 16 \
--mem-fraction-static 0.85 \
--context-length 65536 \
--reasoning-parser qwen3 \
--host 0.0.0.0 --port 30000
Tenga cuidado con dos trampas. Una bandera llamada --cuda-graph-max-bs impide el inicio, ya que las versiones recientes la renombraron para separar las variantes de decodificación y prellenado. Además, si se omite --reasoning-parser qwen3, la API devuelve el razonamiento en bruto del modelo, con etiquetas think anidadas, dentro del campo de contenido normal. Al agregar el analizador, la salida se divide en los campos ordenados reasoning_content y content, el mismo formato que utilizan otros servidores compatibles con OpenAI, lo cual será de gran utilidad para quienes desarrollen clientes.
Paso 5: esperar a que termine. El arranque en la primera ejecución duró unos seis minutos hasta que apareció el mensaje de listo. Nada falló: SGLang compila los kernels de la GPU justo a tiempo a través de Triton y registra los gráficos CUDA en cada tarjeta. Los arranques posteriores reutilizan la caché y terminan en aproximadamente 35 segundos. La barra de progreso que parece estar atascada durante ese primer arranque es simplemente el proceso de compilación en curso.
Lo que mostraron las mediciones
El servidor funcionó. Respondió a las solicitudes, transmitió reasoning_content y aceptó conexiones de agentes. Estos son los números medidos en la propia máquina:
| Metric | Min | Max | Average |
|-------------------------|----------|-----------|-----------|
| Decode speed | 47 t/s | ~100 t/s | ~55 t/s |
| Prompt prefill | 342 t/s | 466 t/s | ~428 t/s |
| Accept length (DFlash) | 3.2 | 6.8 | ~4 |
Y esta es la imagen de la capacidad:
| Capability | Advertised limit | Reality |
|-------------------------|---------------------------|-----------------------|
| Max context per request | 65,536 (I set it) | fits, one at a time |
| Total KV pool | ~131K tokens max | 103K at default flags |
| Parallel slots | 48 concurrent requests | 3–6 with real prompts |
Eso equivale a unos 55 tokens por segundo en promedio, y no 220. Lo problemático es que una sola RTX 3090 Ti con llama.cpp alcanza alrededor de 75 tokens por segundo con la misma familia de modelos. Dos tarjetas que utilizan paralelismo tensorial y un mecanismo de redacción avanzado resultaron inferiores a una sola tarjeta que emplea el enfoque más antiguo.
Las tasas de aceptación explican parte de esta diferencia. Cuando DFlash acierta en sus predicciones, con 6.8 de cada 8 tokens aceptados, los lotes alcanzan brevemente unos 100 t/s. Cuando falla, la velocidad desciende a 47. Incluso el mejor lote quedó muy por debajo de los 220, por lo que el mecanismo de redacción no era la causa principal. Había algo más que reducía la velocidad.
La pista en el registro
La clave estuvo todo el tiempo en el registro de inicio:
Setup Custom allreduce failed with CUDART error: peer access is not
supported between these two devices.
Bajo el paralelismo tensorial, cada matriz de pesos se divide entre las dos GPUs, y los resultados parciales deben fusionarse después de cada bloque de atención y de cada bloque MLP. En un modelo de 27 mil millones, esto supone aproximadamente 100 idas y venidas por cada token generado. Fusionar a esa velocidad requiere una conexión rápida entre las tarjetas, y esta pareja no la tenía. Dos fallos separados se agravaron mutuamente.
Sin comunicación punto a punto. Las tarjetas GeForce ya no ofrecen oficialmente soporte para GPU-direct P2P, por lo que cada transferencia debe realizarse a través de la RAM del sistema: desde la GPU 1, hacia la memoria, hasta la GPU 2 y de vuelta. Eso duplica las copias y triplica la latencia.
Una conexión limitada, el verdadero problema. Revisar el estado de la conexión física lo explica todo:
/sys/bus/pci/devices/05:00.0/current_link_width → 4 (max 16)
/sys/bus/pci/devices/09:00.0/current_link_width → 4 (max 16)
Ambas GPUs negociaban una conexión PCIe x4, no x16. Con cuatro canales cada una, dirigidos a través del puente del sistema, solo se logran 1 a 2 GB/s de transferencia entre las tarjetas; sin embargo, la carga de trabajo utiliza esa conexión unas 100 veces por cada operación. Las GPUs permanecían inactivas mientras esperaban las copias. Esto explica por qué la carga previa avanza a solo 428 t/s, cuando se esperaba 1,200 o más, y por qué la decodificación se estanca cerca de los 55.
Las GPUs son rápidas. El cable que las conecta es lento. Bajo el paralelismo tensorial, el cable se convierte efectivamente en el modelo.
Cómo alguien logra 220
Nadie muestra su placa base en una demostración. Un par x16 bien conectado con P2P funcional ofrece aproximadamente diez veces más ancho de banda entre tarjetas que esta configuración. La cifra indicada probablemente corresponde a un breve período de ejecución de código predecible, con cachés calientes y condiciones ideales para la impresión, y no a una tasa sostenida. El aspecto software de esta solución es sólido, pero asume silenciosamente la existencia de hardware que pocas computadoras domésticas poseen.
Otras dos conclusiones prácticas
La sala de contexto es más pequeña de lo esperado. Dos tarjetas de 24 GB parecen suficientes. Pero hay que restar los pesos INT4 (unos 10 GB en cada tarjeta), el generador, los gráficos CUDA y, dado que Qwen3.8 utiliza atención híbrida, la memoria de estado recurrente. Lo que quedó para el caché KV fue aproximadamente 103K tokens en total, o 131K con configuraciones más arriesgadas. Una solicitud de 65K tokens cabe sin problemas. Los 48 slots concurrentes anunciados no son viables, ya que cada conversación en tiempo real también ocupa parte del pool de estado de Mamba, y en la práctica solo caben de tres a seis sesiones. En contraste, llama.cpp asigna casi toda la VRAM a un único modelo y maneja contextos grandes sin problemas.
Un reinicio relacionado con la alimentación eléctrica. Al activar ambas tarjetas 3090 al mismo tiempo, ejecutando tareas de prellenado y generación bajo carga total en modo paralelo tensorial, el consumo eléctrico aumenta drásticamente desde estado de inactividad hasta aproximadamente 700 vatios en cuestión de milisegundos. Las fuentes de alimentación no son capaces de manejar tal carga. Uno de esos picos provocó la activación de la protección y apagó el servidor a mitad de proceso. Se reinició por sí solo, pero la lección queda clara: usar dos tarjetas con esta carga de trabajo requiere una fuente de alimentación de calidad que cuente con margen real ante picos transitorios, y la potencia indicada en el empaque no es suficiente.
¿Debería probarse en casa?
SGLang plus DFlash2 representa una ingeniería impresionante siempre y cuando se cumplan sus suposiciones, y esas suposiciones equivalen a una lista de verificación de hardware:
- Ambas GPUs en ranuras x16 reales. Las placas de gaming para consumidores suelen ofrecer una, quizás con una segunda conectada vía chipset a velocidad x4. Lea el manual antes de comprar cualquier cosa, ya que este detalle determina si la velocidad será de 220 o 55.
- Comunicación efectiva entre GPUs. Eso implica la función Resizable BAR y ajustes en ACS en el BIOS, además de versiones modificadas de controladores para GeForce o NVLink fuera del ámbito de los productos para consumidores. En la práctica, esto corresponde al dominio de las placas base para servidores.
- Fuente de alimentación de alta calidad con margen suficiente. Los picos de consumo generados por dos tarjetas de 350 W acabarán superando la capacidad de una fuente débil.
- Cuatro RTX 3090 para Qwen3.8-27B con contexto completo. Con ese presupuesto, la caché KV deja de ser un obstáculo, a diferencia de cuando se utiliza un conjunto de 103K con limitaciones.
El software de este tipo no perdona un hardware barato; funciona más lento en aspectos en los que nadie pensó en verificarlo.
Aunque carece de las dos partes necesarias, es decir, del software y del hardware que debería utilizar, llama.cpp sigue siendo la opción más razonable. Implica usar una sola tarjeta, un único binario, una velocidad de aproximadamente 75 tokens por segundo en comparación con las conexiones PCIe normales, sin necesidad de herramientas Rust ni reinicios imprevistos. La pareja de GPUs puede volver a funcionar de forma independiente a una velocidad x4.
La cifra de 220 era real. Simplemente no se aplica a esta tarjeta, y probablemente tampoco a muchas otras hasta que se revise el cableado entre las tarjetas.