Sirviendo Gemma 4 con Rust en vLLM: Mi experimento de fin de semana que salió bien 🦀
El sábado pasado me encontré mirando una factura de AWS que me dejó el café frío. Tres instancias GPU corriendo endpoints de PyTorch para un side project — 47 dólares en dos días. Tenía que haber una forma mejor.
Llegaron el frontend de Rust de vLLM (`vllm-rs`), Gemma 4 y una instancia Graviton2 con una T4G. Lo que empezó como un "a ver si esto compila" por la tarde terminó en un stack de serving listo para producción que cuesta cuatro duros y maneja peticiones concurrentes como un campeón.
Aquí tienes el desglose completo — lo bueno, lo malo y el flag de configuración que me ahorró seis horas de debugging.
¿Por qué este stack? (¿Y por qué ahora?)
Si has servido LLMs en producción, conoces los dolores de cabeza: el GIL de Python mata el throughput bajo carga, la fragmentación de memoria CUDA se come la VRAM, y cada framework quiere su propia imagen docker especial.
vLLM resolvió lo de la memoria con PagedAttention. Pero el frontend de Python sigue siendo un cuello de botella a escala.
Rust cambia las reglas:
- **Sin GIL** — manejo real de peticiones en paralelo
- **Latencia predecible** — sin pausas de GC en medio de la generación
- **Despliegue en un solo binario** — copias, ejecutas, listo
- **Graviton2 + T4G** — compute ARM64 + GPU NVIDIA por 0,75$/hora en spot
Gemma 4 (la variante de 9B) entra cómoda en 16GB de VRAM con espacio para KV cache. Perfecta para una T4G.
La realidad del hardware
Antes de levantar instancias, saber en qué te metes:
| Componente | Spec | Realidad |
|------------|------|----------|
| Instancia | `g5g.xlarge` | 4 vCPU, 16GB RAM, 1× T4G (16GB VRAM) |
| SO | Ubuntu 22.04 ARM64 | Funciona out of the box |
| Almacenamiento | 100GB gp3 | Modelo + cache + logs |
| Red | Hasta 10 Gbps | De sobra |
**Coste**: ~0,75$/hora spot, ~2,03$/hora on-demand. Mi carga de prueba (200 req/min, 512 tokens promedio) salió a 12$/día vs 47$ en el stack viejo.
Paso 1: El toolchain de Rust — No te lo saltes
```bash
Instalar rustup (ARM64 nativo)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"
Crítico: usar el target correcto
rustup target add aarch64-unknown-linux-gnu
Verificar
rustc --version
rustc 1.82.0 (f6e511eec 2024-10-15) — bien
cargo --version
cargo 1.82.0 — bien
```
**Truco**: Si estás en una máquina x86_64 compilando para ARM, necesitas cross-compilación. Compila *en* la instancia Graviton2. Es más rápido que pelearte con `cross` y errores de linker.
Paso 2: Frontend Rust de vLLM — Compilando `vllm-rs`
El repo está en `github.com/vllm-project/vllm-rs`. Clonas y compilas:
```bash
git clone https://github.com/vllm-project/vllm-rs.git
cd vllm-rs
Esto trae vLLM core como submódulo — tarda 5-10 min
git submodule update --init --recursive
Compilar en release (ve por café, ~15 min en g5g.xlarge)
cargo build --release --features cuda
```
**El feature flag que me mordió**: `--features cuda` es obligatorio. Sin él, obtienes un build solo-CPU que *parece* funcionar pero cae a velocidades de `llama.cpp`. Verifica tu binario:
```bash
./target/release/vllm --help | grep -i cuda
Debe mostrar CUDA support: true
```
Paso 3: Gemma 4 — Consiguiendo los pesos como es debido
Gemma 4 no está en Hugging Face Hub como un solo archivo `safetensors` (a la hora de escribir esto). Tienes dos caminos:
Opción A: Convertir desde HF (Recomendado)
```bash
Instalar herramientas de conversión
pip install huggingface_hub safetensors torch --index-url https://download.pytorch.org/whl/cu121
Descargar y convertir
python -c "
from huggingface_hub import snapshot_download
from safetensors.torch import save_file
import torch
path = snapshot_download('google/gemma-4-9b', allow_patterns='*.safetensors')
Lógica de conversión aquí — ver docs de vLLM para script exacto
"
```
Opción B: Usar el AWQ pre-cuantizado (Lo que hice yo)
```bash
AWQ 4-bit entra en ~6GB VRAM, deja 10GB para KV cache
wget https://huggingface.co/TheBloke/gemma-4-9b-AWQ/resolve/main/gemma-4-9b-awq.safetensors
```
**Mi opinión**: AWQ 4-bit es indistinguible de FP16 para la mayoría de casos de chat. La ganancia de throughput (2.3x tokens/seg) es real. Si necesitas logits exactos para destilación o eval, conviértelo tú a FP16.
Paso 4: La config que de verdad funciona
Aquí mi `config.toml` — copia, ajusta, despliega:
```toml
config.toml
[model]
path = "/models/gemma-4-9b-awq.safetensors"
dtype = "float16" # AWQ carga como FP16 internamente
max_model_len = 8192
[server]
host = "0.0.0.0"
port = 8000
workers = 4 # Igual al número de vCPU
max_concurrent_requests = 128
[engine]
gpu_memory_utilization = 0.90
swap_space = 4 # GiB, buffer de offload a CPU
block_size = 16
max_num_batched_tokens = 4096
max_num_seqs = 256
[logging]
level = "info"
access_log = true
```
**Flags clave explicados**:
- `gpu_memory_utilization = 0.90` — Deja 10% de margen para contexto CUDA + fragmentación. Sube a 0,95 y te OOMearás bajo carga burst.
- `swap_space = 4` — Crítico para contextos largos. Offloadea bloques KV menos usados a RAM de CPU. Me salvó cuando un usuario pegó un documento de 12k tokens.
- `max_num_batched_tokens` — Ajusta esto. Muy bajo = GPU infrautilizada. Muy alto = OOM. 4096 es el sweet spot para T4G + Gemma 4 9B.
Paso 5: Systemd — Que sobreviva a reinicios
```ini
/etc/systemd/system/vllm-gemma.service
[Unit]
Description=vLLM Gemma 4 Rust Server
After=network.target nvidia-persistenced.service
Requires=nvidia-persistenced.service
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/opt/vllm-rs
ExecStart=/opt/vllm-rs/target/release/vllm --config /opt/vllm-rs/config.toml
Restart=on-failure
RestartSec=10
Environment=RUST_LOG=info
Environment=CUDA_VISIBLE_DEVICES=0
Límites de recursos
LimitNOFILE=65536
LimitNPROC=32768
[Install]
WantedBy=multi-user.target
```
Activa y arranca:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now vllm-gemma
journalctl -u vllm-gemma -f # Ver logs
```
Test real: Tres escenarios
Escenario 1: Chat API para un bot de docs (Mi caso real)
**Carga**: 50 usuarios concurrentes, 200 req/min, 512 tokens salida promedio
**Resultado**:
- Latencia P50: 340ms (primer token)
- Latencia P99: 1,2s
- Throughput: 1.850 tokens/seg sostenidos
- VRAM: 11,2GB / 16GB
- Cero OOMs en 72 horas
Escenario 2: Racha de code completion
**Carga**: 20 peticiones paralelas, prompts de 2k tokens, 512 completion
**Resultado**:
- Primer token: 410ms (prompt más largo de procesar)
- Throughput: 2.100 tokens/seg
- `swap_space` entró en acción a 14GB VRAM — sin despeinarse
Escenario 3: Stress test (Locust, 500 usuarios)
**Carga**: Rampa a 500 concurrentes en 5 min
**Resultado**:
- Tiempo de cola dominó a 300+ concurrentes
- Tasa de error: 0,3% (timeouts, no crashes)
- Recuperación: Instantánea al bajar la carga
**Conclusión**: El frontend Rust maneja backpressure con elegancia. Python vLLM habría matado los procesos worker.
Monitorización: No vueles a ciegas
Añade métricas Prometheus (vienen en `vllm-rs`):
```bash
Exponer endpoint de métricas
Añadir a config.toml:
[metrics]
enabled = true
port = 9090
```
Dashboards clave a vigilar:
- `vllm_requests_active` — concurrencia actual
- `vllm_gpu_memory_usage_bytes` — presión VRAM
- `vllm_iteration_tokens_total` — salud del throughput
- `vllm_request_duration_seconds` — distribución de latencia
JSON de dashboard Grafana: `github.com/vllm-project/vllm-rs/tree/main/monitoring/grafana`
Los momentos de "¿Por qué no hice esto antes?"
1. **Despliegue en un solo binario** — `scp` el binario, `systemctl start`, listo. Ni docker, ni drift de entornos Python.
2. **Cold start** — 2,3 segundos desde `systemctl start` a servir peticiones. Python vLLM: 18-25s (carga modelo + spawn workers).
3. **Estabilidad de memoria** — 72 horas, ni un reinicio. Workers de Python necesitaban reinicio diario por memory leaks.
4. **ARM64 nativo** — Sin tax de emulación. Graviton2 corre esto *mejor* que x86_64 en price/performance.
Lo que sigue molestándome
- **Churn de formatos de modelo** — Gemma 4 AWQ no es oficial. La próxima release puede romper mi script de conversión.
- **Parámetros de sampling limitados** — `vllm-rs` expone temperature, top_p, top_k, pero nada de `min_p` ni `typical_p` todavía. PR bienvenido.
- **Sin multi-GPU** — Solo single GPU por ahora. Roadmap dice tensor parallelism para Q1 2025.
- **Verbosity de logs** — `RUST_LOG=debug` escupe 50MB/min. Usa `info` en prod.
FAQ
P: ¿Puedo correr esto en una instancia x86_64 con A10G en vez de T4G?
**R**: Totalmente. Cambia el tipo de instancia a `g5.xlarge` (A10G, 24GB VRAM), compila en x86_64 y sube `gpu_memory_utilization` a 0,93. Sacarás ~3.200 tokens/seg en Gemma 4 9B. El coste sube a ~1,00$/hora spot.
P: ¿Cómo se compara con TGI (Text Generation Inference)?
**R**: TGI es más completo en features (cuantización, sharding, streaming fine-grained). `vllm-rs` gana en throughput bruto e integración nativa Rust. Si necesitas LoRA adapters o speculative decoding *hoy* — TGI. Si quieres máximos tokens/dólar en single GPU — `vllm-rs`.
P: ¿Y Gemma 4 27B?
**R**: No entra en T4G (16GB VRAM). Necesitarías `g5g.2xlarge` (2× T4G, 32GB combinados) con tensor parallelism — no soportado aún en `vllm-rs`. Para 27B, quédate con TGI o vLLM Python en A100/H100.
P: ¿La API compatible con OpenAI está totalmente implementada?
**R**: `/v1/chat/completions` y `/v1/completions` funcionan. `/v1/embeddings` devuelve 501 (no implementado). Streaming (`stream: true`) funciona vía SSE. Function calling — todavía no.
---
TL;DR
**Stack**: Gemma 4 9B AWQ + vLLM Rust (`vllm-rs`) + Graviton2 + T4G
**Coste**: ~12$/día para 200 req/min sostenidos
**Rendimiento**: 1.800-2.100 tokens/seg, P99 < 1,2s
**Ops**: Un solo binario, systemd, 2,3s cold start, cero reinicios en 72h
**Repo**: `github.com/vllm-project/vllm-rs`
**Modelo**: `huggingface.co/TheBloke/gemma-4-9b-AWQ`
**Instancia**: AWS `g5g.xlarge` spot
¿Correría esto en producción para un producto de pago? **Sí** — con la salvedad de que estás en el bleeding edge de `vllm-rs`. Fija tu commit hash, vigila el repo y ten un fallback Python vLLM a mano.
Pero para herramientas internas, side projects y cargas sensibles a coste? Este stack *es la onda*. El frontend Rust transforma vLLM de "gran motor, serving doloroso" a "gran motor, gran serving".
Ahora perdonadme, que tengo una factura de AWS que encoger. ☕
Tecnología
Comments (0)
No comments yet. Be the first to comment!
Leave a Comment