Acelerar el cold start de modelos: de minutos a segundos

Esta es la tercera tanda de una serie operativa sobre exprimir un cluster LLM on-premise genérico de 4×H100 SXM 80 GB con NVLink. Las hermanas de esta tanda son Multimodal: servir un VLM on-premise con vLLM —añadir visión al mismo motor— y FinOps y multi-tenancy de GPU con LiteLLM —repartir y cobrar la GPU entre equipos—. Este post toma el problema que Del disco a la HBM dejó planteado en lo conceptual y lo convierte en runbook: cómo se baja de verdad el cold start, knob a knob, para que la elasticidad sea usable.

TL;DR

El post Del disco a la HBM dejó una idea incómoda: mover los pesos del disco a la HBM es solo una de las cinco partidas del cold start. Las otras cuatro —arrancar el proceso, crear el contexto CUDA, montar el allocator y, la más cara que casi nadie mira, capturar los CUDA graphs y compilar los kernels JIT— no las toca ningún loader rápido. Por eso un servidor que carga los pesos en 6 s puede seguir tardando 90 s en estar listo. Este runbook ataca las cinco. Para los pesos: safetensors (mmap, zero-copy, sin rebote por un buffer FP32 de host) frente al pickle de torch.load; Tensorizer (CoreWeave) que serializa el modelo en un fichero y lo streamea tensor a tensor directo a la GPU desde objetos/HTTP/S3; y el Run:ai Model Streamer (de NVIDIA), que lee en concurrencia y solapa la lectura con la copia H2D —los tres se activan con una bandera, --load-format, en vLLM—. Para lo no-pesos: caché de compilación de torch.compile/Inductor (que se persiste y se reusa entre arranques), acotar la captura de CUDA graphs a los batch sizes que de verdad usas (la captura por defecto puede comerse ~54 s), y el sleep mode como atajo que se salta las cinco partidas. Las matemáticas: $t = W/B$ para los pesos, el ahorro de solapar lectura y H2D frente a hacerlo en serie, y un cold start de 70B descompuesto partida a partida. Y la economía: cuándo scale-to-zero con pre-warm gana a mantener una réplica caliente. Sobre el cluster genérico 4×H100 SXM 80 GB.

La analogía: abrir la cocina por la mañana

Un restaurante no abre con tocar un interruptor. Quien haya trabajado en uno lo sabe: la hora de apertura la decide todo lo que pasa antes, y casi nada de ello es “traer la comida”.

El primer cocinero llega a un local frío y a oscuras. Tiene que: encender las luces y arrancar los sistemas (el init del proceso); encender los hornos y dejar que cojan temperatura (el contexto CUDA, que tarda lo suyo en estar operativo); sacar las tablas, ordenar la cámara, organizar el espacio de trabajo (el allocator de memoria, que reserva y estructura la HBM); bajar al almacén y subir el género —las cajas de verdura, la carne, el pescado— y colocarlo en su sitio (la carga de pesos del disco a la HBM, el tema entero de Del disco a la HBM); y, lo que más se olvida, afilar todos los cuchillos, montar el mise en place, hacer los fondos, precalentar las salsas madre —toda la preparación que no es un ingrediente pero sin la cual no sale un solo plato— (la captura de CUDA graphs y la compilación JIT de los kernels).

Aquí está la trampa que Del disco a la HBM ya anticipó: si solo optimizas el género, sigues abriendo tarde. Puedes contratar el mejor servicio de reparto del mundo, montacargas rapidísimo, cajas que se colocan solas. Pero si el cocinero todavía está afilando cuchillos y esperando a que los hornos cojan temperatura, la cocina no sirve el primer plato. El género llegó pronto y el restaurante sigue cerrado.

El runbook de hoy es, literalmente, la lista de todo lo que hay que hacer para que la cocina abra a su hora —y cuál de esas tareas se puede acelerar, cuál se puede dejar hecha de la noche anterior (cachear), y cuándo sale más a cuenta no apagar nunca la cocina (réplica caliente) que volver a encenderla cada mañana (scale-to-zero).

Las cinco partidas del cold start

Cuando un pod de inferencia nace y hasta que devuelve su primer token, el reloj corre por cinco sitios distintos. Conviene nombrarlos porque cada optimización ataca a uno o dos, nunca a todos:

  1. Init del proceso. Arrancar el intérprete de Python, importar el motor (vLLM, sus dependencias), parsear la configuración. Segundos, y crece con el tamaño del entorno y los import de CUDA/PyTorch.
  2. Contexto CUDA. El primer cudaSetDevice / cudaFree(0) inicializa el runtime de CUDA contra el driver: carga el contexto, mapea la GPU. No es instantáneo —del orden de uno a varios segundos por GPU según driver y número de dispositivos.
  3. Allocator. vLLM monta su gestor de memoria sobre la HBM (caching allocator de PyTorch más el reparto del KV-cache). Reservar y estructurar decenas de GB tiene su coste.
  4. Carga de pesos disco→HBM. El trayecto que Del disco a la HBM diseccionó: disco → page cache → buffer de host → PCIe → HBM. Lo que casi todo el mundo cree que es el cold start, y es solo una de las cinco.
  5. Captura de CUDA graphs + JIT de kernels. vLLM captura grafos de CUDA para las distintas formas de batch y compila kernels con torch.compile/Inductor (y backends como DeepGEMM o FlashInfer). Esta partida es la gran ignorada: la captura de graphs por defecto en vLLM tarda del orden de 54 s porque cubre un abanico amplio de batch sizes (vLLM, torch.compile integration; Red Hat, vLLM with torch.compile).
Las cinco partidas de un cold start (ejemplo 70B, loader por defecto)ancho proporcional al tiempo · escala 0–110 s0 s55 s110 s1 · init proceso — ~4 s2 · contexto CUDA — ~6 s3 · allocator — ~4 s4 · carga de pesos disco→HBM — ~40 s5 · CUDA graphs + JIT — ~50 stotal ≈ 104 s · optimizar solo la partida 4 deja la 5 intacta — y la 5 es la mayor aquí

La lección que el diagrama grita: en un servidor moderno con torch.compile y CUDA graphs, la partida 5 puede ser tan grande o mayor que la 4. Acelerar la carga de pesos de 40 s a 6 s es un avance enorme, pero si dejas la captura de graphs en 50 s, el cold start cae de 104 s a ~70 s, no a 12 s. Hay que atacar las dos mitades.

Acelerar la partida 4: la carga de los pesos

safetensors frente a pickle: el formato manda

El primer knob es el formato en disco. El viejo torch.load usa pickle, que tiene dos problemas. El de seguridad es conocido: deserializar un pickle ejecuta código Python arbitrario —el protocolo __reduce__ permite que el fichero invoque cualquier callable al cargar, así que cada descarga de un modelo era un vector de ejecución remota (HuggingFace, Safetensors). El de rendimiento es el que nos ocupa: cargar un pickle reconstruye objetos Python y suele pasar por un buffer de host intermedio antes de llegar a la GPU.

safetensors —ahora bajo la PyTorch Foundation (HuggingFace, Safetensors joins PyTorch Foundation)— resuelve ambos. El fichero es solo una cabecera JSON + bytes de tensor en crudo: cargarlo no puede hacer nada salvo poblar buffers de tensores. Y la propiedad clave para nosotros es física: la región de datos está alineada a frontera de página (la cabecera se rellena para que el primer tensor empiece en un múltiplo del tamaño de página del SO). Eso es lo que hace posible el zero-copy: un loader puede hacer mmap del fichero, cudaHostRegister sobre la región mapeada y DMA directo de la page cache a la VRAM, sin deserialización de torch.load ni buffer FP32 temporal de host (HuggingFace, Safetensors). Es el formato base; todo lo demás se construye encima.

Trampa del mmap (ya señalada en Del disco a la HBM): mmap no lee nada de inmediato, difiere el coste al primer acceso a cada página. Si no fuerzas la lectura, el cold start parece corto y el primer token paga los page faults. Y la “segunda carga rapidísima” es la page cache mintiéndote: en producción los pods nacen fríos, en nodos donde esos ficheros no están cacheados.

Tensorizer: streamear tensor a tensor directo a la GPU

Tensorizer, de CoreWeave, serializa los pesos del modelo y sus tensores en un único fichero y, en vez de cargar el modelo entero a RAM antes de moverlo a la GPU, streamea los datos tensor a tensor desde disco, un endpoint HTTP/HTTPS o un bucket S3, deserializándolos al vuelo directamente sobre la GPU (vLLM, Loading Models with CoreWeave’s Tensorizer). La ventaja operativa: carga casi instantánea y bajo uso de RAM de host durante la inicialización, lo que importa especialmente en escenarios serverless y de autoescalado —justo el caso de scale-to-zero—. Y desacopla los pesos de la imagen del contenedor: el modelo vive en almacenamiento de objetos, no inflando el image pull.

En vLLM se activa con --load-format tensorizer (o load_format="tensorizer" por API). Requiere serializar el modelo una vez al formato de Tensorizer; a partir de ahí, cualquier pod lo streamea.

Run:ai Model Streamer: leer y copiar a la vez

El Run:ai Model Streamer (de NVIDIA) ataca el cuello desde otro ángulo: lee los tensores en concurrencia —N hilos del SO leyendo del almacenamiento al buffer de CPU— mientras los streamea a la VRAM, de modo que la lectura del almacenamiento y la copia H2D se solapan en lugar de hacerse en serie (vLLM, Loading models with Run:ai Model Streamer; NVIDIA, Reducing Cold Start Latency). Lee safetensors directamente, sin reconversión.

Los números publicados por NVIDIA dan el orden de magnitud: el streamer alcanza 4,88 s leyendo desde S3 a concurrencia 32 y 7,53 s desde un SSD IO2 a concurrencia 8; integrado en vLLM, el tiempo total hasta ready baja a 23,18 s desde S3, 28,28 s desde SSD IO2 y 35,08 s desde GP3 (NVIDIA, Reducing Cold Start Latency). Fíjate en la brecha entre el tiempo de carga de pesos (~5–8 s) y el total hasta ready (~23–35 s): esos ~18–27 s de diferencia son las otras cuatro partidas, sobre todo la 5. El streamer arregló la partida 4 y el resto sigue ahí —exactamente lo que advierte este post—.

La concurrencia es el parámetro de ajuste: controla cuántos hilos del SO leen tensores al buffer de CPU (y, para S3, cuántas conexiones cliente abre el host) (vLLM, Run:ai Model Streamer). 16 suele bastar para NVMe local; 32 para almacenamiento de objetos de alto throughput. Un hilo no satura un NVMe Gen5; el solapamiento sí.

# safetensors por defecto (el más lento de los rápidos)
vllm serve <model> --load-format safetensors

# Run:ai Model Streamer (lectura concurrente, solapa lectura + H2D)
vllm serve <model> --load-format runai_streamer \
  --model-loader-extra-config '{"concurrency": 32}'

# Tensorizer (stream tensor a tensor directo a GPU desde objetos/S3)
vllm serve <model> --load-format tensorizer

Las matemáticas de los pesos: $t = W/B$ y el ahorro de solapar

El suelo de la partida 4 es simple: mover $W$ gigabytes a un ancho de banda $B$ tarda

$$t = \frac{W}{B}$$

Tomemos un modelo grande, un 70B. En BF16 ($b=2$ bytes/parám) son $W = 70 \times 10^9 \cdot 2 = 140$ GB; en FP8 ($b=1$), $W = 70$ GB. El ancho de banda depende del tier (ver tabla más abajo): NVMe Gen5 local lee del orden de ~14 GB/s por disco; el PCIe Gen5 x16 copia host→GPU a ~50 GB/s; el almacenamiento de red, ~1–3 GB/s. El suelo teórico de leer 140 GB de un NVMe es $140/14 = 10$ s; de la red a 2 GB/s, 70 s. El cuello manda.

El truco del solapamiento. El loader por defecto hace los dos pasos —leer del disco y copiar H2D— en serie: primero llena un buffer, luego copia, luego el siguiente. El tiempo es la suma:

$$t_{\text{serie}} = t_{\text{lectura}} + t_{\text{H2D}}$$

Tensorizer y el Run:ai Model Streamer los solapan: mientras un hilo copia un chunk a la VRAM, otro ya está leyendo el siguiente del disco. Con suficiente concurrencia, el tiempo total tiende al máximo de los dos, no a la suma:

$$t_{\text{solapado}} \approx \max(t_{\text{lectura}},, t_{\text{H2D}})$$

Con los 70 GB del 70B en FP8, desde NVMe a 14 GB/s y PCIe a 50 GB/s: $t_{\text{lectura}} = 70/14 = 5{,}0$ s, $t_{\text{H2D}} = 70/50 = 1{,}4$ s. En serie, $5{,}0 + 1{,}4 = 6{,}4$ s; solapado, $\max(5{,}0,,1{,}4) = 5{,}0$ s. El ahorro aquí es modesto (~22%) porque el disco domina con holgura. El solapamiento brilla cuando los dos pasos son comparables: desde almacenamiento de red a 5 GB/s, $t_{\text{lectura}} = 70/5 = 14$ s y $t_{\text{H2D}} = 1{,}4$ s, serie 15,4 s vs solapado 14 s; pero con muchos flujos concurrentes que saturen un disco rápido y un H2D igualado, pasar de la suma al máximo puede casi doblar el throughput efectivo. La otra cara: el solapamiento solo te da lo que el cuello físico permite. Si el disco solo da 14 GB/s, ningún streamer te baja de 5 s para 70 GB —para eso está la otra palanca, mover menos bytes (FP8 frente a BF16 parte $W$ por la mitad)—.

El tier de almacenamiento

TierAncho de banda típico140 GB (70B BF16)70 GB (70B FP8)Notas
NVMe Gen5 local~14 GB/s por disco~10 s~5 sel camino corto; un disco
NVMe Gen5 local (varios, RAID0)~28–50 GB/s~3–5 s~1,5–2,5 ssi el loader satura varios flujos
Almacenamiento de objetos (S3/RGW)~1–3 GB/s por flujo~47–140 s~23–70 sla concurrencia del streamer ayuda mucho
NFS / red compartida~1–2 GB/s~70–140 s~35–70 smete la red y su contención en el camino

La conclusión operativa es la misma que en Del disco a la HBM: los pesos que sirven el cold start viven en NVMe local del nodo, no en la red. El almacenamiento de red es el repositorio; el nodo de inferencia tiene una copia local caliente (pre-pull con un initContainer o un DaemonSet de cache por nodo). Tensorizer es la excepción interesante: streamea tan eficientemente desde objetos que a veces hace viable servir desde S3/RGW sin copia local, a cambio de depender del ancho de banda de la red de almacenamiento.

Acelerar la partida 5: todo lo que no son pesos

Aquí está el medio cold start que los loaders rápidos no tocan. Tres frentes.

Cachear la compilación de torch.compile/Inductor

torch.compile tiene un sistema de caché incorporado: los artefactos compilados se guardan tras el primer arranque y se reusan entre arranques —incluso entre máquinas si se configura bien—. En vLLM, el resultado de la compilación de Dynamo se almacena en ~/.cache/vllm/torch_compile_cache/ (vLLM Blog, Introduction to torch.compile; vLLM, torch.compile integration). La consecuencia para el runbook: si ese directorio es un volumen persistente compartido entre pods (un PVC, o un hostPath en NVMe del nodo poblado por el primer arranque), los pods siguientes se saltan la compilación y arrancan más rápido. El primer pod paga la compilación; los demás heredan la caché.

Es exactamente “afilar los cuchillos la noche anterior”: la preparación cara se hace una vez y se reutiliza. El matiz: la caché es sensible a la versión de vLLM/PyTorch, la GPU, la cuantización y la configuración —cambiar cualquiera invalida la caché y vuelves a pagar la compilación una vez—.

Acotar la captura de CUDA graphs

vLLM captura CUDA graphs para un abanico amplio de batch sizes por defecto, y eso es caro: la captura por defecto ronda los 54 s, y en configuraciones grandes se ha medido hasta 294 s. Limitando la captura a los batch sizes que tu carga de verdad usa (p. ej. 1, 2, 4, 8, 16, 24, 32, 64) se reduce el cold start más de un 70%, de 294 s a ~82 s en el caso medido (Red Hat, vLLM with torch.compile). El knob es la configuración de compilación/CUDA graphs del engine; el principio es no capturar grafos para tamaños de batch que nunca verás.

Disyuntiva real: menos batch sizes capturados = arranque más rápido pero menos cobertura. Un batch fuera del conjunto capturado cae al camino eager (más lento por petición). Acotas a los tamaños frecuentes de tu tráfico, no a cualquiera. Y siempre existe el extremo de desactivar CUDA graphs (enforce_eager): arranque casi instantáneo en la partida 5 a costa de throughput en estado caliente —válido para depurar o para servicios de muy bajo tráfico donde el cold start pesa más que el régimen permanente—.

Warm pools: no pagar la partida 5 en el camino crítico

El atajo definitivo para las cinco partidas es no ejecutarlas cuando el usuario espera. Un warm pool —una réplica ya arrancada, con pesos cargados, graphs capturados y kernels compilados, esperando tráfico— convierte el cold start en cero para esa petición. Es coste de GPU (parcial o totalmente ociosa) a cambio de latencia de arranque; lo analizamos en la economía, más abajo.

El sleep mode: el atajo que se salta las cinco partidas

Y existe un atajo a medio camino entre “arrancar de cero” y “tener una réplica entera caliente”: el sleep mode de vLLM, el tema de Servir varios modelos en una sola GPU. En vez de matar el proceso, lo duerme: aparca los pesos en RAM del host (nivel 1) o los descarta (nivel 2), pero mantiene el proceso vivo —y con él el contexto CUDA, el allocator, los CUDA graphs y los kernels JIT ya compilados—. El wake no paga las partidas 1, 2, 3 ni 5; solo vuelve a poner los pesos en VRAM (partida 4, y desde RAM, no desde disco). Por eso un wake es 18–200× más rápido que un cold start completo, e incluso el nivel 2 —que recarga los pesos del mismo disco— sigue siendo 23–45× más rápido, porque se salta las otras cuatro partidas (vLLM Blog, Sleep Mode). El sleep mode es la prueba viva de la tesis de este post: si preservar las partidas 1, 2, 3 y 5 da un 18–200×, es que mover bytes nunca fue el coste entero.

Un cold start de ejemplo, partida a partida

Pongamos números a un cold start de 70B en FP8 (70 GB) desde NVMe local y veamos qué optimización ataca cada partida:

#PartidaPor defectoOptimizaciónOptimizado
1Init proceso~4 s(poco margen; entorno mínimo)~3 s
2Contexto CUDA~6 s(driver; fijo)~6 s
3Allocator~4 s(fijo)~4 s
4Carga de pesos~25 sRun:ai streamer / Tensorizer (concurrencia + solapado)~5 s
5CUDA graphs + JIT~54 scaché de torch.compile + captura acotada~10 s
Total~93 s~28 s

Dos lecturas. Primera: optimizar solo la partida 4 (loader rápido y nada más) baja el total de 93 a ~73 s —una mejora real pero decepcionante, porque la 5 sigue intacta—. Optimizar solo la 5 (caché + captura acotada) baja a ~49 s. Atacar las dos baja a ~28 s, y es lo que convierte scale-to-zero teórico en usable. Segunda: las partidas 2 y 3 son prácticamente irreductibles —dependen del driver y del hardware—; constituyen el suelo del cold start, del orden de 10 s, que ninguna optimización de software cruza. Por debajo de ese suelo, la única respuesta es no arrancar de cero: sleep mode (wake sub-segundo a pocos segundos) o réplica caliente (cero).

La economía del scale-to-zero

La pregunta de capacidad que todo esto sirve es: ¿apagar o mantener caliente? Scale-to-zero ahorra GPU mientras no hay tráfico, pero paga el cold start cuando vuelve. La decisión es un balance entre coste de GPU ociosa y SLA de latencia.

El criterio básico: scale-to-zero solo es viable si el cold start cabe dentro del SLA de arranque que tus usuarios toleran, o si tienes un mecanismo de pre-warming que arranca la réplica antes de que llegue la petición (predicción de carga, señal adelantada de la cola). Sin pre-warm, un cold start de 90 s significa que el primer usuario tras un período ocioso espera 90 s —inaceptable para casi cualquier SLO interactivo—. Con el cold start bajado a ~28 s, sigue siendo mucho para una petición interactiva, pero ya es tolerable para cargas batch o para un pre-warm disparado por autoscaling con KEDA sobre la profundidad de cola.

La regla de decisión, en una frase: mantén una réplica caliente cuando el coste de la GPU ociosa durante los valles es menor que el coste de incumplir el SLA en cada arranque; haz scale-to-zero (con pre-warm) cuando los valles son largos y profundos y el cold start optimizado cabe en tu tolerancia. Para un servicio interactivo 24×7 con tráfico irregular pero continuo, el suelo de réplicas calientes casi siempre gana. Para un modelo grande que se invoca pocas veces al día (un 70B de tareas difíciles), scale-to-zero con cold start optimizado —o sleep mode si comparte GPU con otro modelo— es lo razonable. Es la misma frontera de capacity planning: el cold start es un parámetro del colchón de réplicas, no un detalle de arranque.

Aplicado al cluster genérico 4×H100

Bajemos el runbook a las 4 H100 SXM de 80 GB con NVLink. Las decisiones concretas:

Loader y formato. Pesos en safetensors en disco (nunca pickle). Para el camino corto desde NVMe local, el Run:ai Model Streamer (--load-format runai_streamer, concurrencia 16–32) es la opción por defecto: lee concurrente, solapa lectura y H2D, y solo cambias una bandera. Reserva Tensorizer para el caso en que sirvas desde almacenamiento de objetos (RGW/S3) y quieras desacoplar los pesos de la imagen sin copia local —su streaming tensor-a-tensor directo a GPU es lo que hace viable ese patrón—. En ambos casos, FP8 frente a BF16 parte por la mitad la partida 4 casi gratis (mide la calidad, no la asumas).

Tier de almacenamiento. Repositorio de modelos en la red (Ceph RGW), copia caliente en NVMe local del nodo poblada por pre-pull. El cold start que cuenta es el frío, en un nodo donde los ficheros no están en page cache; servir ese arranque desde la red mete su latencia y contención en el camino crítico.

Caché de la partida 5. El directorio torch_compile_cache en un volumen persistente o hostPath NVMe compartido entre los pods del mismo modelo: el primer pod compila, los demás heredan. Y acotar la captura de CUDA graphs a los batch sizes reales del tráfico de cada servicio.

Cuándo scale-to-zero con pre-warm vs réplica caliente. El LLM de agentes (servicio principal, tráfico continuo) nunca hace scale-to-zero: un suelo de réplicas calientes en una o dos GPUs. El 70B ocasional sí: scale-to-zero con cold start optimizado, o —si comparte GPU con un modelo mediano— sleep mode para que el wake sea sub-segundo en vez de un arranque de 28 s (la jugada exacta de Servir varios modelos en una sola GPU). El cold start optimizado de este post es lo que hace que el swap y el failover entre réplicas sean tolerables: una réplica que cae y se reemplaza en 28 s degrada menos que una que tarda 90 s. Y conecta directo con la multi-tenancy: un cold start corto permite repartir GPUs entre equipos con scale-to-zero por tenant sin que el primer usuario de cada tenant pague un arranque eterno.

El principio transversal: el cold start es el techo de la elasticidad. Las cinco partidas, atacadas las dos mitades —loader rápido para los pesos, caché y captura acotada para los graphs—, lo bajan de minutos a segundos. Y por debajo del suelo irreductible de ~10 s (contexto CUDA + allocator), la única salida es no arrancar de cero: sleep mode o réplica caliente.

Trampas y cosas que no son lo que parecen

“Cambié a un loader rápido y el arranque casi no mejoró.” Probablemente tu cuello no era la partida 4. Si la captura de CUDA graphs se come 54 s, bajar la carga de pesos de 25 a 5 s te quita 20 s de 93 —se nota poco—. Mide dónde se va el tiempo antes de optimizar: los logs de arranque de vLLM desglosan carga de pesos frente a captura de graphs.

“La segunda vez arrancó volando.” La page cache (para los pesos) y la caché de torch.compile (para los graphs) mintiendo a la vez. El arranque que importa es el frío, en un nodo limpio. Benchmarquear el segundo arranque mide una situación que casi nunca ocurre en el pico que dispara el autoescalado.

“Tensorizer/streamer me dará el 18–200× del sleep mode.” No. Esos loaders aceleran solo la partida 4. El 18–200× del sleep mode viene de preservar las otras cuatro manteniendo el proceso vivo. Son herramientas para problemas distintos: el loader rápido es para un cold start de verdad (proceso nuevo); el sleep mode es para alternar modelos sin matar el proceso.

"enforce_eager arregla el cold start." Arregla la partida 5 (sin captura de graphs, arranque casi instantáneo) pero degrada el throughput en estado caliente —los CUDA graphs existen porque aceleran el régimen permanente—. Es un intercambio: gana para servicios de bajísimo tráfico donde el arranque pesa más que el caliente; pierde para un servicio con carga sostenida.

“Acoto los CUDA graphs a un solo batch size y listo.” Acotas a los tamaños que tu tráfico usa, no a uno. Un batch fuera del conjunto capturado cae al camino eager y va más lento. Demasiado agresivo y penalizas el régimen permanente para ganar unos segundos de arranque.

“Mover los pesos a FP8 también acelera la partida 5.” No directamente. FP8 parte por la mitad los bytes (partida 4) y dobla el throughput de inferencia, pero la captura de graphs y la compilación de kernels no dependen del tamaño de los pesos sino de la forma del grafo y los batch sizes. La partida 5 se ataca con caché y captura acotada, no con cuantización.

Conclusión

Del disco a la HBM lo dejó dicho en lo conceptual: mover los pesos es una de las cinco partidas del cold start. Este runbook lo convierte en acción. Para los pesos —la partida 4— hay un menú claro: safetensors como base (mmap, zero-copy, sin pickle), Run:ai Model Streamer para el camino corto desde NVMe (lectura concurrente que solapa lectura y H2D), Tensorizer para servir streaming desde objetos sin copia local, y la palanca transversal de mover menos bytes con FP8. Pero la mitad olvidada del arranque es la partida 5 —captura de CUDA graphs y compilación JIT—, que en un servidor moderno puede ser tan grande como la carga de pesos: se ataca cacheando la compilación entre pods y acotando la captura de graphs a los batch sizes reales. Atacar solo una mitad deja el cold start a medias; atacar las dos lo baja de ~90 s a ~28 s. Y por debajo del suelo irreductible de ~10 s que imponen el contexto CUDA y el allocator, la única salida es no arrancar de cero: el sleep mode (wake sub-segundo) o la réplica caliente (cero). La cocina abre a su hora cuando alguien se ocupó del género y de afilar los cuchillos la noche anterior. Optimizar solo el reparto del almacén deja los hornos fríos y el restaurante cerrado.

Ver también

Referencias