Inteligencia Artificial 6 min de lectura 1246 palabras

Cuando la caché de un LLM mezcla clientes: el incidente de Cloudflare con GLM-5.3

EN
Cuando la caché de un LLM mezcla clientes: el incidente de Cloudflare con GLM-5.3

Cloudflare ha publicado el informe de un incidente que me parece de lectura obligada para cualquiera que sirva modelos de lenguaje: entre el 27 de agosto y el 9 de septiembre, el modelo GLM-5.3 de Workers AI devolvió a algunos clientes respuestas que contenían datos de peticiones de otros clientes. Fueron 155 respuestas, todas a prompts de más de 681.280 tokens.

Lo detectó el investigador Gadi Evron, de Knostic, que lo reportó por HackerOne con un script para reproducirlo. Cloudflare confirmó el fallo a la mañana siguiente, retiró el modelo y lo volvió a publicar corregido ese mismo día. Según su análisis, no hay indicios de que nadie lo explotara a propósito: las peticiones afectadas encajan con un uso normal de contextos largos. El informe sale a la vez que otro de SGLang, el motor de inferencia open source que usan, como divulgación coordinada.

Un poco de contexto: prefill, decode y la caché KV

Cuando un LLM procesa un prompt, calcula para cada token unos tensores de keys y values que luego necesita para generar la respuesta. Eso es la caché KV, y vive en la memoria de la GPU dividida en páginas de tamaño fijo que se asignan a cada petición y se liberan al terminar. Por rendimiento, las páginas no se ponen a cero entre peticiones: la siguiente que la recibe simplemente la sobrescribe. Es lo habitual en los motores de inferencia, y obliga al motor a llevar muy bien la cuenta de qué página es de quién.

Cloudflare sirve GLM-5.3 en modo desagregado: unas GPU hacen el prefill (procesan el prompt y generan su caché KV) y se la pasan por RDMA a otras GPU que hacen el decode (generan la respuesta token a token).

flowchart LR
    A["Cliente · prompt"] --> B["Router"]
    B --> C["Prefill · procesa el prompt"]
    B --> D["Decode · reserva páginas KV"]
    C -->|"caché KV por RDMA"| D
    D --> E["Respuesta"]

Dos fallos que se juntaron

El primero fue de configuración. Desplegaron el modelo con una ventana de contexto de 1.048.576 tokens, pero en los nodos de prefill la caché KV sólo tenía sitio para 681.280. SGLang no fija ese tamaño en la configuración: lo calcula al arrancar con la memoria que queda libre después de cargar los pesos del modelo, y nada obliga a que coincida con el contexto anunciado. Lo sabían antes del lanzamiento, pero en sus pruebas con contextos largos todo devolvía HTTP 200, así que supusieron que el motor gestionaba la diferencia de alguna forma. Ellos mismos lo reconocen: comprobaron los códigos de respuesta y el rendimiento, pero no el contenido de las respuestas.

El segundo era un fallo de SGLang. Cuando llegaba un prompt que no cabía, el planificador lo rechazaba y, para que la petición saliera por el circuito normal sin reservar memoria, cambiaba el prompt por uno de un solo token. En un único nodo funcionaba: el cliente acababa recibiendo un error. Pero en modo desagregado las colas de admisión comprobaban si la petición había terminado, no si había sido rechazada, y una petición rechazada seguía pareciendo viva. Pasaba esto:

PasoQué ocurría
ArranqueEl router le pasa al decode la longitud real del prompt y este reserva, por ejemplo, 8 páginas
PrefillAdmite la petición rechazada y procesa sólo el token de relleno
TransferenciaEnvía una única página y avisa de que la transferencia está completa
DecodeAcepta el traspaso y genera leyendo las 8 páginas reservadas
RespuestaHTTP 200 con una respuesta coherente, construida en parte sobre las 7 páginas que aún tenían datos de peticiones anteriores

El resultado es especialmente traicionero: no había ningún error. El cliente recibía un 200, el recuento de tokens del prompt era el correcto y la respuesta tenía sentido. La única pista era que llegaba más rápido de lo que debería tardar un prefill de ese tamaño. Además, el fallo ya estaba reportado en SGLang desde julio por otro usuario, sin relación con Cloudflare.

La corrección

Corrigieron las dos cosas, y lo explican bien: cualquiera de ellas por separado habría bastado para cerrar este caso concreto, pero no para estar tranquilos.

  • Configuración: ajustaron el despliegue para que la caché KV del prefill tenga sitio para unos 2,04 millones de tokens, el doble del contexto. Así ninguna petición válida llega a la ruta de rechazo.
  • Motor: parchearon SGLang para que una petición rechazada se descarte, con su error, antes de reservar memoria o transferir nada. Depender sólo del tamaño de la caché es frágil, porque cualquier cambio de cuantización, de paralelismo o de versión del modelo lo altera sin que sea evidente.

Después vino un trabajo más amplio con los mantenedores de SGLang para atacar toda una clase de fallos parecidos: páginas que se liberan o reutilizan mientras otra parte del sistema todavía las usa, ya sea dentro de un nodo o entre el prefill y el decode. Entre otras cosas, SGLang ahora puede verificar una suma de comprobación de la caché transferida (--disaggregation-enable-kv-checksum) y retrasa la liberación de páginas hasta confirmar que no quedan escrituras pendientes. Todo está en la versión v0.5.22, y recomiendan actualizar a cualquiera que use SGLang en modo desagregado.

Sus recomendaciones para otros operadores

El informe termina con una lista práctica que resumo:

  1. Medir el tamaño de la caché KV y compararlo con el contexto en cada nodo, después de cualquier cambio de pesos, cuantización o paralelismo. Si no cabe, bajar el contexto. “No dar por hecho que el motor rechazará lo que no cabe”.
  2. Actualizar SGLang a la v0.5.22 o posterior, como red de seguridad para el día en que el punto anterior falle.
  3. Activar la liberación diferida de páginas en prefill y decode.
  4. Activar la detección de corrupción donde el modelo lo permita, y mirar los logs para confirmar que las peticiones se están verificando.
  5. Evitar que el balanceador aborte peticiones a mitad de transferencia por desconexiones del cliente.

A más largo plazo, Cloudflare quiere que cada página lleve una etiqueta que cambie al reasignarse, para que el decode pueda comprobar en cada lectura que la página es de esa petición. También quiere aplicar verificación formal al protocolo de transferencia de la caché por RDMA, que conecta con lo que contaba hace unos días sobre la verificación formal con LLM.

Lo que me llevo

Lo primero, la lección de siempre, y que no por repetida es menos útil: un 200 no significa que algo funcione. Sabían que la caché era más pequeña que el contexto, lo probaron y, como no vieron errores, lo dieron por bueno. Es una suposición muy fácil de hacer con cualquier librería: “si no cabe, ya fallará”. Aquí no falló, y lo que salió fue peor que un error.

Lo segundo, que este tipo de fallos es nuevo en cómo se manifiesta, pero no en su naturaleza. Es un problema de toda la vida de gestión de memoria y de estado compartido entre dos máquinas que no se ponen de acuerdo, sólo que ahora lo que se filtra son los prompts de otros usuarios. A medida que los modelos se sirven con más optimizaciones (cachés compartidas, desagregación, reutilización de memoria sin limpiar) el aislamiento entre clientes deja de ser algo que se da por supuesto.

Y lo tercero, el informe en sí. Es detallado, reconoce el error propio sin rodeos, enlaza cada corrección en SGLang y da recomendaciones que sirven a otros. Así es como deberían ser todos.

Fuente: Cross-request KV cache exposure in the GLM-5.3 Workers AI model, and our remediation with SGLang, Cloudflare.