Agentes desatendidos en oMLX y gentle-shell
Los logos identifican los proyectos de los que trata este informe. Es una contribución independiente.
Introducción
Este artículo aborda una pregunta concreta: ¿es posible completar trabajo real de principio a fin con modelos locales, hardware de consumo y solamente tecnología open source?
Para investigarlo, desarrollamos un piloto de documentación: convertir manuales PDF y capturas antiguas en documentación en formato wiki, con imágenes de la nueva versión de ADempiere y un asistente que responde preguntas. La implementación combinó el servidor de inferencia oMLX, el harness de agentes Gentle Shell y modelos de pesos abiertos de la familia Qwen, ejecutados en una estación de trabajo personal.
Soy el mantenedor principal de ADempiere, un proyecto open source de ERP, CRM y SCM. Su repositorio oficial ofrece acceso público al código fuente y al historial de desarrollo, dando contexto al proyecto detrás de este piloto. También dirijo una empresa especializada en el desarrollo, soporte y mantenimiento profesional de soluciones basadas en esta tecnología. Esa experiencia nos ayudó a elegir una tarea concreta, útil y con resultados verificables.
El propósito no era comparar esta configuración con modelos de frontera ni con herramientas como Claude Code o Codex, sino establecer si podía completar el trabajo propuesto, identificar los obstáculos y documentar las condiciones necesarias para hacerlo posible.
A continuación comparto la experiencia, los problemas encontrados, las soluciones desarrolladas y los resultados obtenidos, junto con los límites de la evidencia.
Resumen ejecutivo
Probamos si modelos locales, hardware de consumo y herramientas open source podían realizar documentación sin intervención constante. El piloto utilizó modelos Qwen servidos por oMLX en un Apple M5 Max de 128 GB, con pi y gentle-shell coordinando agentes sobre una wiki de ADempiere.
- Para oMLX: las correcciones de decode TurboQuant por lotes y prefill con prefijos restaurados mejoraron el rendimiento medido. La propuesta de pausa y reanudación conservó solicitudes bajo presión forzada.
- Para gentle-shell: la reescritura del prompt de sistema por todo causaba pérdidas de caché. La corrección probada mueve las tareas a mensajes de conversación para mantener estable ese prompt.
Con las correcciones propuestas de oMLX y gentle-shell aplicadas durante el piloto, el proyecto alcanzó su objetivo: completar de principio a fin la documentación en formato wiki, con imágenes de la nueva versión de ADempiere y un asistente que responde preguntas, utilizando modelos locales, hardware de consumo y herramientas open source. Los benchmarks documentan mejoras de rendimiento, memoria y estabilidad; no auditan independientemente el contenido de la wiki. Los resultados históricos, las comparaciones recientes y la ejecución no pareada responden preguntas distintas.
Los resultados corresponden a una estación de trabajo de consumo de Apple preparada para IA local y una familia de modelos. Upstream integró PR2 como #4442 y la corrección de PR1 como #4453 el 2026-10-12; las demás correcciones son propuestas, no afirmaciones de integración en upstream, y ninguna es una garantía para equipos menores.
Detalle de contribuciones, beneficios y agradecimientos
Panorama de contribuciones
Qué aportamos
A oMLX
- PR1: decode por lotes con TurboQuant. Cada fila con relleno a la izquierda atiende solo a su propia porción, en el decode y en la verificación MTP. Una corrección de bug. Upstream la cerró en favor de #4453, que construye la misma corrección sobre el kernel de #4408 y nos incluye como coautores (integrado el 2026-10-12). Detalles
- PR2: prefill tras un prefijo TurboQuant restaurado. Un sufijo largo se procesa sobre una copia densa del prefijo restaurado, que luego se vuelve a cuantizar para el decode. Una corrección de bug, integrada en upstream como #4442 (2026-10-12). Detalles
- Pausar y reanudar en lugar de abortar. Bajo presión de memoria, una solicitud conserva el progreso de su prefill y se estaciona, las filas en decode se pausan, y ambas se reanudan cuando se libera memoria; una solicitud que nunca podrá caber recibe un error estructurado, y los prompts con imágenes pasan a poder estacionarse. Desactivado por defecto o condicionado, abierto el 2026-10-12 como un stack de 12 pull requests, de #4478 a #4489. Detalles
- Correcciones de caché de prefijo A1 y A2. Los mensajes de sistema a mitad de conversación ya no reescriben la cabeza del prompt (A1), y los mensajes sintéticos de un cliente ya no eliminan el razonamiento en medio de un ciclo del agente (A2). Se proponen por separado, después de las correcciones de bugs. Detalles
- Observabilidad y un límite de memoria. Logs de resumen de expulsión, resultado de caché por solicitud y desglose del TTFT, y
memory.mlx_cache_limit_gbpara acotar la caché de buffers de Metal, como propuestas posteriores separadas. Detalles
A gentle-shell
- La corrección de todo (#1367). El estado de las tareas sale del prompt de sistema y pasa a mensajes ocultos de la conversación, enviados solo cuando la lista cambia, y el recordatorio de lista desactualizada se envía una sola vez, de modo que el prompt de sistema se mantiene idéntico byte a byte dentro de una sesión. Un commit, listo en
fix/todo-prompt-cache-1367. Detalles - Un test de invariante de estabilidad del prompt. Verifica que cada solicitud extienda la anterior, y se puede generalizar a cada extensión que toca el prompt de sistema. Detalles
- Un chequeo de caché de extremo a extremo y un grabador de payloads (opcionales). Detectan reescrituras del prompt contra un servidor local que informa los tokens cacheados, y hacen reproducibles los reportes de "se puso lento". Detalles
- Cuatro hallazgos más del harness, con evidencia. La directiva YOLO y el anexo de memoria que se alternan en el prompt de sistema (planteados en #1367), el rol worker sin
agent_browser, y los workers de squad que ignoran el modelo fijado. Detalles
Por qué vale la pena integrarlo. Los benchmarks pareados compararon el fork con la versión de upstream indicada en cada sección, una build a la vez. El tráfico registrado y la ejecución en vivo aportan evidencia observacional, no comparaciones pareadas con upstream. Cada resultado indica su número de ejecuciones y sus límites. Cada cambio de comportamiento se escribió con pruebas primero; cerca de dos tercios de las líneas cambiadas del ciclo de pausa y reanudación son pruebas. El trabajo está dividido en pull requests pequeños y revisables, con un orden de merge indicado. Más allá de las dos correcciones de bugs, cada slice del stack de pausa y reanudación está desactivado por defecto o condicionado a un techo de memoria explícito, de modo que con todas las opciones desactivadas se comporta como main en estas rutas. El orden y la forma son propuestas; los adaptaremos a sus preferencias.
Beneficios para oMLX
- Decode por lotes más rápido y con menos memoria (PR1). Decode TurboQuant por lotes 5–12× más rápido por fila (desde unos 1.5 tok/s por fila en
main25aebb3e); fase B=3 345 → 66 s con MTP activado y 780 → 65 s con MTP desactivado; pico 11–15 GB menor. El #4453 de upstream, construido a partir de este diagnóstico, informa 62–65 tok/s por fila con tres solicitudes concurrentes (medición del mantenedor, Qwen3.6-35B-A3B con TQ4). Evidencia - Los prefijos restaurados ya no ralentizan el prefill (PR2). Prefill del sufijo tras un prefijo TurboQuant restaurado: 2.41× → 0.92× de un prefill en frío. Con el commit de progreso del fork, un reenvío tras una cancelación tarda 80–82 s en lugar de 192–193 s. Evidencia
- Pausar en lugar de abortar (stack opt-in). Bajo presión forzada, 9 de 9 solicitudes se completan con 0 abortos, frente a 0 de 9; los tokens son idénticos tras 12 pausas con MTP desactivado; una solicitud que nunca podrá caber recibe un 400 estructurado en 0.1 s. Evidencia
- Sin trabajo perdido. Un prefill cancelado conserva 73,728 tokens confirmados en lugar de 0. Evidencia
- Mejor reutilización de caché para cualquier cliente (A1, A2). A2 elimina una causa que equivalía a unos 0.94M tokens con re-prefill en 1.5 días; los turnos con imágenes alcanzan 97.4–99.7% de hits. A1 evita que los mensajes de sistema a mitad de la conversación reescriban la cabeza del prompt. Evidencia
- Memoria acotada y observabilidad.
mlx_cache_limit_gbacota la caché de buffers de Metal (evidencia: el pico de P2); el resumen de desalojos, el resultado de caché por solicitud y el desglose de TTFT respaldan cada medición en vivo de este informe. Evidencia
Beneficios para gentle-shell
- Un prompt de sistema estable byte a byte. Reescrituras del prompt de sistema 21 de 61 → 0 de 90 solicitudes; 31 de 31 turnos consecutivos de solo agregado; re-prefills de más de 20k tokens de 1–2 por hora a 0. Evidencia
- Tiempo y cómputo recuperados. Antes de la corrección se hizo re-prefill de ~1.05M tokens en ~1.5 días (≈80–85 min de tiempo de GPU), y cada turno afectado esperaba ≈2–6 min más. Evidencia
- Relevante más allá de oMLX. Las reescrituras del prompt también pueden reducir la reutilización en otros servidores locales y API alojadas con caché de prefijo. El ahorro real depende de las reglas de caché y los precios de cada backend; no se midió aquí. Evidencia
- Pequeña y verificada. Un commit, 388 líneas cambiadas (296 de ellas pruebas); 63 pruebas de todo pasan; una revisión independiente no encontró problemas bloqueantes; se conserva el recordatorio de #1924. Evidencia
- Regresiones detectadas a tiempo. La corrección incluye una prueba de invariante de estabilidad del prompt para la herramienta de todo, que puede generalizarse a cada extensión que toca el prompt de sistema. Propuesta
status:approved en #1367, para que la corrección pueda entrar como pull request, o incorporarse a otro cambio si lo prefieren. Detalles: Contribución propuesta.Ambos proyectos son abiertos, y eso es lo que nos permitió leer el código, medirlo y corregir lo que encontramos. Con ese mismo espíritu queremos devolver estas mejoras en lugar de mantener un fork privado. Gracias a jundot por oMLX y a Alan-TheGentleman por gentle-shell, y por el gran trabajo que cada uno comparte en su proyecto.
Acerca de este informe
Un informe de campo para los mantenedores de dos proyectos: oMLX, el servidor de inferencia, y gentle-shell, el harness de agentes. Sigue a una estación de trabajo de un solo desarrollador que ejecuta agentes de programación desatendidos: cómo comenzó el trabajo, cómo se midió cada problema, qué se cambió en un fork, cómo se validó y qué partes nos gustaría contribuir upstream. Ambos proyectos se mejoraron con el mismo método: medir sobre tráfico real, atribuir cada pérdida a una causa, corregir con pruebas primero y validar contra el otro lado.
Alcance. El trabajo en oMLX comenzó en v0.7.0 y ahora se basa en v0.7.1.dev1, upstream main 013bc3f3. Es un informe de campo sobre versiones específicas, no una validación general del fork actual ni del upstream main de hoy. Las ejecuciones C7 y la ejecución en producción real usaron upstream oMLX main en b7c7929e, la rama del fork en los commits indicados en cada sección y gentle-shell main en aa2c0389. Dos revalidaciones son más recientes: la de la corrección de TurboQuant (PR1) sobre upstream main 25aebb3e, y la del fork completo contra main c0b1056b, que además sacó a la luz un segundo defecto de TurboQuant en upstream (PR2). Las ramas propuestas a upstream están rebasadas sobre main 013bc3f3.
Resultados y límites de un vistazo
Cada resultado debe leerse junto con su línea base. Varias solicitudes dentro de una ejecución no son repeticiones independientes de un benchmark. Los enlaces de evidencia llevan a la configuración, los logs y los commits exactos.
| Pregunta | Línea base y runtime | Evidencia | Límite |
|---|---|---|---|
| ¿Se completó el piloto de documentación de principio a fin? | Resultado del proyecto informado por el autor | El autor confirma que, con las correcciones propuestas del servidor y del harness aplicadas durante el piloto, el proyecto completó la documentación en formato wiki, con imágenes de la nueva versión de ADempiere y un asistente que responde preguntas, utilizando modelos locales, hardware de consumo y herramientas open source. | Finalización informada con las correcciones propuestas aplicadas, no con la configuración original ni con una versión de upstream que las incluya; los benchmarks no son una auditoría independiente de la calidad del contenido. |
| ¿Los lotes terminan sin intervención? | Comparación histórica con v0.7.0 | P5: 3/3 lotes del fork, 0/3 de upstream. Evidencia | Mide la finalización de lotes por separado del resultado final del proyecto; no se repitió frente a un main más reciente. |
| ¿PR1 mejora el decode por lotes? | main 25aebb3e; mismo venv con versiones fijadas | B=3: 5–12× más rápido por fila; pico 11–15 GB menor. Una ejecución por build y configuración MTP. Evidencia | No se reprodujo degeneración sobre esta línea base; las salidas no son idénticas token a token a B=1. |
| ¿PR2 mejora el prefill con un prefijo restaurado? | main c0b1056b; venv con versiones fijadas | Relación entre sufijo restaurado y frío: 2.41× → 0.92×. Una ejecución por configuración. Evidencia | Un modelo y una forma de prompt; la memoria adicional de caché densa debe caber. |
| ¿Las solicitudes sobreviven a la presión tras la corrección de prefill? | Fork 4aa66150 frente a main 013bc3f3; mismo venv | El decode de texto e imágenes termina 3/3 en el fork con los techos comparados; main termina 0/3. Una ejecución por escenario. Evidencia | Presión forzada, no carga típica; algunos otros escenarios de upstream no se repitieron. |
| ¿El servidor sostiene tráfico real de agentes? | Ejecución no pareada de fork.5; runtime de la app v0.7.0 | 333 solicitudes; 94.8% de los tokens de prompt desde caché; 0 abortos por memoria. Evidencia | Ventana del log 02:00–04:10; sin presión de memoria, por lo que no se ejercitó pausa/reanudación. |
| ¿La corrección de todo estabiliza los prompts? | Tráfico registrado antes/después de la corrección de gentle-shell | Mutaciones del prompt de sistema: 21/61 → 0/90 solicitudes; 31/31 pares de solo agregado. Evidencia | Muestras observacionales con cargas distintas; no es un benchmark controlado de latencia ni de costos de API alojadas. |
De un vistazo
main (d42264b4, 2026-10-11). Hay una corrección probada lista en fix/todo-prompt-cache-1367, registrada en #1367. Ver la sección de gentle-shell.- Sobre un
mainmás reciente (25aebb3e, con sus dependencias fijadas), PR1 es principalmente una corrección de rendimiento. Un lote B=3 de prompts de 20k/45k/135k se completa en ambos árboles sin degeneración, y PR1 hace el decode por lotes 5–12× más rápido por fila, reduce la fase B=3 de 345 a 66 s (MTP activado) y de 780 a 65 s (MTP desactivado), y baja el pico en 11–15 GB. La señal de corrección es más débil: con MTP desactivado, las filas por lotes demainse apartan de la salida determinista de B=1 alrededor del token 1–2, y las de PR1 alrededor del token 8–10. Ver la revalidación. - Histórico (
mainb7c7929e sobre el runtime de la app v0.7.0): el mismo lote degeneró en una ejecución (4-gram 0.33/0.29/0.18, 121 GB) y abortó en otra (6 abortos por memoria, 120 GB). Esto no se reproduce sobre 25aebb3e.
El caso de uso
La configuración es un Apple M5 Max (128 GB) que sirve un modelo local a varias sesiones del agente de programación pi. pi ejecuta encima el harness gentle-shell (paquete gentle-pi), que agrega el flujo de trabajo del agente: orquestación, una herramienta de tareas pendientes (todo), integración de revisión y delegación a subagentes. Los subagentes se ejecutan como procesos pi separados, cada uno con el mismo harness. Las sesiones leen archivos fuente completos y toman capturas de pantalla. Los prompts son grandes: en 1,260 solicitudes registradas la mediana fue de 107k tokens y el p90 de 168k. Cada turno reenvía la conversación creciente, por lo que la caché de prefijo decide si un turno cuesta 1 segundo o varios minutos.
El objetivo era simple de enunciar: dejar la máquina trabajando hasta que terminen todos los trabajos. Eso implica usar la memoria libre para concurrencia, no descartar nunca trabajo completado y pausar en lugar de abortar cuando la memoria escasea.
Ilustración de las grabaciones posteriores de fork.5, no un benchmark de la configuración original.
Qué fallaba en v0.7.0 estándar
- Varios prompts largos concurrentes alcanzaban el umbral duro (hard watermark) de la guardia de memoria (memory guard). Las solicitudes se abortaban a mitad del prefill, se perdía todo el progreso de prefill y el agente se detenía a esperar a una persona.
- Aumentar la concurrencia dejaba las cosas peor. En un benchmark mixto de 3 solicitudes, la ejecución concurrente terminó en 538 s frente a 246 s en serie, con 6 abortos.
- Cuando el decode por lotes sí se ejecutaba, las filas cortas de un lote TurboQuant con relleno a la izquierda producían salida degenerada.
- La reutilización de la caché de prefijo era alta en promedio, pero colapsaba en ráfagas: pérdidas de 70k–170k tokens en un solo turno.
Cómo se desarrolló
La pregunta
Tras la llegada de los niveles de la guardia de memoria en 0.7.0, la pregunta era si una solicitud podía pausarse cuando se agota la memoria y reanudarse cuando se libera, en lugar de abortarse. Analizamos dónde corresponde eso, en el cliente o en el servidor, y decidimos que en ambos. La extensión de pi pausa y continúa automáticamente. El servidor conserva el progreso de prefill y estaciona (park) la solicitud en lugar de abortarla.
Premisas antes que código
Escribimos seis premisas falsables (P1–P6), cada una ligada a un benchmark que se ejecuta contra upstream v0.7.0 en la misma máquina, con el mismo modelo y los mismos prompts. Nada se consideró terminado hasta que su premisa se cumplió frente a upstream.
Implementación en slices pequeños y revisados
El trabajo se hizo con pruebas primero, con un commit revisable por comportamiento y una revisión independiente de cada slice (cuatro lentes de revisión en los de alto riesgo). Se integró en una rama del fork derivada de v0.7.0 y se empaquetó como una app paralela para uso diario.
La sorpresa del procesamiento por lotes
El uso concurrente real dejó expuesta la corrupción de la atención con relleno y un decode TurboQuant por lotes muy lento, de alrededor de 1.5 tok/s por fila. Corregir el bug de corrección y agregar un kernel ragged de un solo lanzamiento llevó el decode por fila a alrededor de 13 tok/s.
La investigación de la reutilización de caché
Una vez que nada abortaba, el costo restante era el re-prefill. Registramos el payload de cada solicitud con un pequeño grabador del lado del cliente y atribuimos cada ruptura de caché a una causa. Se volvieron a hacer prefill de unos 2.7M tokens en aproximadamente 1.5 días. La causa principal estaba en el harness gentle-shell y dos estaban en el servidor; las tres se corrigieron.
Harness y servidor, probados juntos
Probar el harness gentle-shell contra el servidor oMLX en ejecución, con monitoreo del lado del servidor, expuso una invalidación de caché que ninguno de los dos lados mostraba por sí solo: mientras había tareas pendientes abiertas, el harness reescribía su prompt de sistema en cada prompt del usuario. La corrección se envió a gentle-shell, y el método pasó a formar parte de este informe (detalles).
Validación en vivo
El tráfico registrado y el uso diario ayudaron a validar los cambios ejercitados por esas cargas. Los logs estructurados (resultado de caché, desglose de TTFT, resumen de desalojos) respaldaron las mediciones; las funcionalidades probadas solo mediante pruebas unitarias o de presión forzada se identifican por separado.
Pausa y reanudación, de extremo a extremo
Un benchmark de forzado mostró que el park de prefill funciona pero no puede salvar un lote, porque las filas en decode y los prompts con imágenes todavía se abortaban. Construimos el ciclo completo: pausa en decode, reanudación consciente de la memoria, un error para lo que nunca cabe y prompts con imágenes que se pueden estacionar. Su primera ejecución de validación falló de varias maneras. Cada falla se rastreó en los logs y se corrigió con pruebas primero, hasta que todas las solicitudes terminaron bajo presión forzada con 0 abortos.
Ejecución en producción real
El fork empaquetado (0.7.0-fork.5) atendió luego trabajo real de agentes desatendidos durante 1 h 39 min, con el límite de concurrencia elevado en vivo de 3 a 5. Registró 0 abortos por memoria y sirvió el 94.8% de los tokens de prompt desde la caché de prefijo. La memoria se mantuvo lejos de la línea de presión, por lo que la pausa y reanudación nunca se activó.
Premisas y resultados
Las revalidaciones más recientes aparecen más abajo. Las comparaciones originales se conservan como evidencia histórica; no son mediciones de la versión actual de upstream.
Comparaciones históricas: v0.7.0 y main b7c7929e
Todas las ejecuciones usaron un servidor aislado en un puerto separado con un directorio de caché nuevo, el mismo modelo y los mismos prompts. Compararon upstream v0.7.0 con el fork.
| Premisa | Upstream v0.7.0 | Fork | Resultado |
|---|---|---|---|
| P1 · Sin trabajo perdido (cancelar a mitad del prefill, reenviar) | 0 tokens en caché | 73,728 tokens en caché | PASS |
| P2 · 2×~130k concurrentes, sin abortos | 4 abortos · pico 116 GB | 0 abortos · pico 44 GB | PASS |
| P3 · Corrección del lote greedy (B=3 vs B=1) | filas cortas degeneradas | sin degeneración; divergencias solo en casi empates (near-ties) de bf16 | PASS |
| P4 · La concurrencia rinde (3 solicitudes mixtas) | 538 s vs 246 s en serie · 6 abortos | 241 s vs 247 s en serie · 34.6 vs 29.7 tok/s agregados | PASS |
| P5 · Lote pi desatendido | 0/3 completados | 3/3 completados | PASS |
| P6 · Una sola solicitud de 110k, sin regresión | línea base | prefill +1.6% · decode +8.5% | PASS |
El criterio de tiempo total de P4 está cerca del ruido entre ejecuciones: una de cinco ejecuciones del fork no lo alcanzó por 0.2 s. El criterio de decode agregado se cumple con margen.
Nueva ejecución contra main b7c7929e (runtime de la app v0.7.0)
Rebasamos el fork sobre upstream main (b7c7929e) y volvimos a ejecutar las premisas. Cada build se ejecutó sola en la máquina, con el mismo runtime (el Python, las dependencias y los kernels compilados de la app v0.7.0, mlx-vlm 0.7.1), de modo que ambas difieren solo en el código fuente.
| Premisa | main | Fork rebasado sobre main | Lectura |
|---|---|---|---|
| P1 · Reenvío tras cancelar a mitad del prefill | 0 en caché · 110,031 con re-prefill | 73,728 en caché · 36,303 con re-prefill | PASS |
| P2 · 2×~130k concurrentes | 0 abortos · pico 116 GB · 1,427 s | 0 abortos · pico 44 GB · 461 s | PASS main sobrevive, pero cerca del techo de memoria y unas 3× más lento |
| P3 · Corrección del lote greedy (B=3 vs B=1) | el lote B=3 abortó (6 abortos, pico de 120 GB); no se reproduce sobre 25aebb3e | ninguna fila degenerada; proporciones de 4-gram 1.00 / 1.00 / 0.99 | fork correcto divergencias en casi empates de bf16 |
| P4 · La concurrencia rinde (3 solicitudes mixtas) | 248 s concurrente · 251 s en serie · 0 abortos | 227 s concurrente · 224 s en serie · 0 abortos | dentro del ruido no se reclama ganancia frente a main |
| P6 · Una sola solicitud de 110k | prefill 755 tok/s · decode 21.1 tok/s | 744 tok/s (−1.4%) · 20.6 tok/s (−2.4%) | dentro del límite de −5% |
Hay tres puntos a destacar. Primero, main ya no aborta en P2 ni en P4 como lo hacía v0.7.0. Segundo, la ganancia de concurrencia del fork en P4 está dentro del ruido frente a main. Tercero, P5 requiere el cliente de agentes en vivo, por lo que no se volvió a ejecutar. Las ventajas claras frente a main b7c7929e en este runtime son no perder trabajo (P1), un pico de memoria mucho menor y una finalización más rápida bajo concurrencia (P2), y la corrección de los lotes (P3). P3 se volvió a ejecutar sobre un main más reciente para PR1, donde el panorama cambia (más abajo).
Revalidación sobre un upstream main más reciente
Ilustración de decode concurrente de las grabaciones de fork.5. Las mediciones pareadas B=3 de abajo son evidencia separada; este video B=5 no es una grabación de esas pruebas.
La evidencia anterior se midió sobre b7c7929e con el runtime de la app v0.7.0. Se volvió a comprobar sobre commits más recientes de upstream main en dos rondas: la premisa de PR1 sobre 25aebb3e (esta subsección) y luego el fork completo contra main c0b1056b (siguiente subsección). Cada rama propuesta para upstream está ahora rebasada sobre main 013bc3f3 (2026-10-10), con sus pruebas específicas y el comando de CI ejecutados de nuevo allí (Contribución propuesta). Los commits de upstream entre 25aebb3e y 013bc3f3 no tocan los archivos de TurboQuant que cambia PR1.
PR1 sobre 25aebb3e. PR1 estaba rebasado sobre 25aebb3e en ese momento, por lo que su premisa, P3, se volvió a ejecutar sobre ese árbol. main 25aebb3e y PR1 (61bd8a06, dos commits sobre 25aebb3e) se ejecutaron desde un mismo venv construido con las versiones fijadas de main: mlx 0.32.3, mlx-vlm 0.7.7, mlx-lm 94cdcae1, transformers 5.17.0. Entre ellos solo cambió el árbol de código fuente. Los prompts tienen unos 20k / 45k / 135k tokens, idénticos byte a byte a los de las ejecuciones anteriores, con 512 tokens nuevos y muestreo greedy. Tres filas B=3 se restauran desde la caché de prefijo, hacen decode juntas y se comparan con los mismos prompts restaurados con B=1. Una ejecución por build, un servidor a la vez, con la app en vivo cerrada.
| Build | MTP | Abortos | Pico | 4-gram distintos, B=3 (20k / 45k / 135k) | Decode por lotes tok/s por fila | Fase B=3 | Tiempo total de ejecución |
|---|---|---|---|---|---|---|---|
main | activado | 0 | 117 GB | 1.00 / 1.00 / 0.99 | 1.54 / 1.53 / 1.49 | 345 s | 1015 s |
| PR1 | activado | 0 | 102 GB | 1.00 / 1.00 / 0.98 | 8.35 / 8.83 / 7.84 | 66 s | 682 s |
main | desactivado | 0 | 112 GB | 1.00 / 1.00 / 0.98 | 0.66 / 0.66 / 0.66 | 780 s | 1492 s |
| PR1 | desactivado | 0 | 101 GB | 1.00 / 0.99 / 0.98 | 8.17 / 8.15 / 8.05 | 65 s | 782 s |
- Rendimiento. Ninguno de los dos árboles aborta ni degenera. PR1 hace el decode por lotes 5.4× más rápido por fila con MTP activado y 12× más rápido con MTP desactivado, reduce la fase B=3 de 345 a 66 s y de 780 a 65 s, y baja el pico del proceso en 11–15 GB. Los 0.66 tok/s de
maincon MTP desactivado son lo que predice el microbenchmark: unos 86 ms por capa TurboQuant en la ruta con relleno, multiplicados por 15 capas. El decode con B=1 es de unos 40 / 29 / 17 tok/s con MTP activado, igual en ambos árboles, por lo que las filas por lotes siguen siendo más lentas que las filas individuales. - Identidad frente a B=1, MTP desactivado. Aquí la referencia B=1 es determinista: una segunda ejecución B=1 es idéntica en ambos árboles. Las filas por lotes de
mainse apartan de ella alrededor del token 1–2 en las filas de 20k y 45k; las de PR1 alrededor del token 8–10, y alrededor del token 45 en la fila de 135k. Ninguna es idéntica token a token. Upstream no tiene volcado de logprobs, por lo que estas divergencias no se pueden clasificar como ruido de casi empates ni como un defecto. La separación más temprana enmaines consistente con la atención de filas con relleno, pero no la demuestra. - Identidad, MTP activado. Dos ejecuciones B=1 ya difieren entre sí, por lo que las divergencias de B=3 no dicen nada sobre el procesamiento por lotes.
- Qué cambió desde la evidencia anterior. Sobre b7c7929e con el runtime de la app v0.7.0 (mlx-vlm 0.7.1),
maindegeneró (4-gram de alrededor de 0.33 / 0.29 / 0.18 con 121 GB) en una ejecución y abortó (6 abortos por memoria con 120 GB) en otra. Ninguno de los dos casos se reproduce sobre 25aebb3e. Hay dos diferencias: los 52 commits de upstream intermedios (incluidos el rollback exacto de #3127 y cambios en la guardia de memoria y en la equidad del decode) y el runtime. Con una ejecución por build, no se sabe cuál de los dos lo eliminó.
El fork completo contra main c0b1056b
El fork se rebasó de nuevo, esta vez sobre upstream main c0b1056b (el commit etiquetado como v0.7.1.dev1). Ambos árboles se ejecutaron desde un mismo venv construido con las versiones fijadas de main (mlx 0.32.3, mlx-vlm 0.7.7), un servidor a la vez, con la app en vivo cerrada. El fork (6fbb3274) se ejecutó con la configuración de la app del fork; main se ejecutó con una configuración sin cambios. Los escenarios de presión forzada son los de C7 (más abajo): tres prompts greedy de unos 130k / 115k / 100k tokens iniciados con 0.5 s de diferencia, hard_threshold 0.8, el mismo techo personalizado para ambos árboles. La recuperación de buffers agrupados está ahora siempre activa, porque upstream eliminó la opción que la desactivaba. Una ejecución por escenario.
| Escenario | Techo | Fork | main c0b1056b |
|---|---|---|---|
| Decode de texto | 44 GB | 3/3 0 abortos; 2 parks de prefill, ambos reanudados | 0/3 abort-all a mitad del prefill tras 25 s |
| Decode con imágenes | 42 GB | 3/3 0 abortos; 2 pausas de decode, ambas reanudadas (también 3/3 con 44 GB) | 0/3 dos abort-all y un rechazo de la guardia de prefill |
| Prefill con imágenes | 44 GB | 3/3 0 abortos; 2 parks de prefill, ambos reanudados | 3/3 0 abortos |
Nunca cabe (prompt de 60k, max_tokens 2,000,000) | 42 GB | 400 context_exceeds_capacity en 0.11 s, los 7 campos; endpoint de capacidad 200; encabezado 288,739 | 200; endpoint 404; sin encabezado |
- Identidad tras una pausa, MTP desactivado. Cuatro pausas de decode forzadas por solicitud (12 en total, en los offsets de salida 100, 323, 1044 y 1706) dan una salida idéntica token a token en 3/3 solicitudes.
- Park durante un prefill empaquetado. El prefill empaquetado de upstream hace ahora el prefill de prompts concurrentes en un solo forward. Una fila estacionada mientras estaba empaquetada se reanudó, y las 3 solicitudes terminaron con 0 abortos. Las salidas difieren de las referencias en solitario solo en casi empates (un empate exacto y un margen de 0.125 nat), y un control con 56 GB sin ningún park diverge en los mismos tokens. La fila que nunca se estacionó es idéntica token a token con y sin el park de su compañera.
- Un defecto del fork encontrado y corregido durante esta ronda. Con 42 GB, la primera ejecución de decode con imágenes rechazó una solicitud solitaria que cabe por sí sola, basándose en una lectura de memoria que todavía incluía los buffers de una solicitud recién terminada. La corrección vuelve a medir antes de decidir y renueva el presupuesto de desalojo de la solicitud cuando salen las filas que esperaba (TDD estricto); la nueva ejecución pasó con 42 y 44 GB.
Premisas, fork contra main c0b1056b, mismo venv:
| Premisa | Fork | main c0b1056b | Lectura |
|---|---|---|---|
| P1 · Reenvío tras cancelar a mitad del prefill | 73,728 en caché · TTFT 198.9 s | 0 en caché · TTFT 192.2 s | caché PASS, sin ganancia de TTFT en este commit; con la corrección de prefill el reenvío tarda 80–82 s frente a 192–193 s en main (revalidación), atribuido a un defecto de upstream, corregido más abajo |
| P2 · 2×~130k concurrentes | 2/2 · 0 abortos · pico 51 GB · 564 s | 2/2 · 0 abortos · pico 116 GB · 870 s | PASS pico menor, más rápido |
| P3 · Lote B=3, MTP activado | 13.0 / 12.2 / 10.9 tok/s por fila · pico 58 GB | 1.55 / 1.54 / 1.50 tok/s por fila · pico 117 GB | sin regresión sin costo visible del rollback de #3127; el fork incluye PR1 y el kernel ragged |
| P4 · Concurrencia (3 solicitudes mixtas, 2 ejecuciones) | 292.4 s concurrente · 287.7 s en serie · 12/12 | 289.3 s concurrente · 285.9 s en serie · 12/12 | paridad ambos limitados por el prefill sin kernels compilados |
| P6 · Una sola solicitud de 110k, decode | 22.77 tok/s | 22.61 tok/s | sin regresión +0.7% con 110k, +4.6% con 20k |
La primera medición de P6 (3 ejecuciones, 256 tokens de decode cada una, sin intercalar) mostró el decode del fork un 7.1% más lento, lo mismo con todas las opciones del fork desactivadas. No se reprodujo. Una nueva medición con 1,024 tokens de decode y tres sesiones intercaladas por árbol, MTP activado, da las medianas de la tabla, y un costo por ciclo de verificación de +0.9% (error estándar 1.4%) con 110k tras controlar por los tokens propuestos por el borrador. Con MTP desactivado, ambos árboles hacen decode a 16.8–17.0 tok/s con 110k y a 24.9–25.4 con 20k. La primera diferencia se debió a la aceptación de MTP en una muestra corta (1.90 frente a 2.01 tokens por ciclo) más la deriva entre ejecuciones no intercaladas, por lo que se cerró como ruido sin hacer bisect.
Revalidación con la corrección de prefill: fork 4aa66150 contra main 013bc3f3
Las ejecuciones anteriores son previas a la corrección del prefill tras un prefijo restaurado, que cambia cómo continúa un park reanudado o una solicitud reenviada sobre bloques TurboQuant restaurados. Por eso, los escenarios de presión forzada y P1/P2 se ejecutaron de nuevo en el fork con la corrección (4aa66150), contra main en 013bc3f3, con el mismo venv, techos, prompts y configuración. En cada reanudación se verificó en el log la línea dequantized … restored TurboQuant layers de la corrección, o su alternativa Kept … quantized, y la reserva que le sigue. Una ejecución por escenario.
| Escenario | Techo | Fork + corrección | main 013bc3f3 | Corrección en el log |
|---|---|---|---|---|
| Decode de texto | 44 GB | 3/3 0 abortos; 2 parks de prefill, ambos reanudados | 0/3 abort-all a mitad del prefill tras 22 s | no se alcanzó: los parks se reanudaron desde 0 tokens confirmados |
| Decode con imágenes | 44 / 42 GB | 3/3 0 abortos en ambos; 1 y 2 pausas de decode, todas reanudadas | 0/3 dos abort-all, un rechazo de admisión (44 GB) | no se alcanzó: una pausa de decode reanuda su caché exacta, sin prefill de sufijo |
| Prefill con imágenes | 44 / 42 GB | 3/3 0 abortos en ambos; con 42 GB un park de prefill, reanudado en solitario; tiempo total 640 s (2,132 s antes de la corrección) | no se volvió a ejecutar (sin cambios en el scheduler ni en TurboQuant desde c0b1056b) | 7 líneas de descuantización para el bloque de imagen de 2,048 tokens restaurado por cada prompt, incluido el park reanudado; reservas sobre 16 cachés |
| Nunca cabe | 42 GB | 400 en 0.11 s, los 7 campos; endpoint de capacidad 200; encabezado 288,900 | no se volvió a ejecutar | - |
| Prefill empaquetado, MTP desactivado | 44 / 42 GB | 3/3 0 abortos; empaquetado, pero sin park en ninguno de los techos | - | no se alcanzó (sin restauración) |
- El prefill con imágenes es donde se nota la corrección. Cada prompt con imagen restaura un bloque de 2,048 tokens desde la caché paginada. Antes de la corrección, ese pequeño prefijo cuantizado mantenía todo el prefill de 100k–130k en la ruta cuantizada: TTFT de 1,797 / 1,267 / 2,130 s con dos parks. Con la corrección: 249 / 460 / 637 s. Las capas densas forman parte ahora de la reserva de prefill, por lo que la admisión ejecuta los prompts uno tras otro. Con 42 GB, un prompt admitido junto a otro, ambos densos, se estacionó bajo presión dura 4.6 s después, se reanudó solo, se descuantizó de nuevo y se completó, con un pico de 37 GB. Eso es un park y una reanudación a través del paso denso en hardware real, sin abortos.
- Reenvío tras una cancelación (P1, 2 ejecuciones por árbol). El fork conserva 73,728 tokens y hace el reenvío en 80.0 y 82.0 s.
mainno conserva nada y hace el reenvío en 192.0 y 193.0 s, lo mismo que su prefill en frío. Antes de la corrección, el mismo código del fork tardaba 198.9 s. El log muestradequantized 15 restored TurboQuant layers (73728 tokens) for a 36302-token suffix prefill in 0.42sy luego una reserva sobre 16 cachés (+2.22 GB). - Dos prompts concurrentes de 130k (P2). 2/2, 0 abortos, pico de 53 GB, tiempo total de 574 s, frente a 51 GB y 564 s antes de la corrección. Sin regresión; no interviene ninguna restauración.
- Identidad tras una pausa, MTP desactivado. 12 pausas forzadas por ejecución, todas reanudadas de forma exacta. Dos solicitudes son idénticas token a token; la tercera se aparta en un casi empate de 0.125 nat. Su deriva empieza en el token 4, antes de su primera pausa, tras un throttle de prefill puntual. El commit anterior a la corrección da los mismos tokens en la misma sesión (idénticos en logprobs, 2,000/2,000), por lo que ni la corrección ni la ruta de pausa lo causan.
- Prefill empaquetado. La salida con presión es igual al control sin presión (la ejecución con 42 GB difiere de él en un empate exacto), y ambas se apartan de las referencias en solitario en los mismos tokens, como antes. En esta sesión nunca se estacionó una fila empaquetada, por lo que esa ruta no se volvió a ejercitar.
Defecto de upstream: prefill de sufijo lento tras un prefijo TurboQuant restaurado
Ilustración de prefill concurrente de las grabaciones anteriores de fork.5, no una grabación de la corrección de prefijo restaurado ni de su benchmark pareado de abajo.
Rastrear la falta de ganancia de TTFT de P1 sobre c0b1056b llevó a un segundo defecto de TurboQuant en upstream. Tras un hit de prefijo en la caché SSD paginada, el sufijo no cacheado de un prompt hacía prefill unas 2.4× más lento por token que un prefill en frío de las mismas posiciones. Se reproduce sobre main, por lo que no es un bug del fork, y con TurboQuant desactivado ambos tardan lo mismo.
Mecanismo
- Un hit en la caché SSD paginada restaura las capas TurboQuant como objetos
TurboQuantKVCachecuantizados (#1842), y el scheduler las mantiene cuantizadas mientras hace el prefill del sufijo. - Cada chunk de cada capa TurboQuant atiende entonces a todo el prefijo cuantizado a través de la ruta de atención cuantizada por mosaicos, nunca SDPA denso, por lo que el costo crece con la longitud del prefijo.
- La reserva de capacidad de prefill informa
across 1 cachestras una restauración de ese tipo en lugar de 16, porque solo la última capa, no cuantizada, es unKVCachesimple que puede planificar. Esa línea es un síntoma, no la causa.
Corrección
Antes de un sufijo que necesita más de un chunk de prefill, el scheduler copia una sola vez las capas TurboQuant restauradas en capas KVCache densas, de modo que los chunks del sufijo siguen la misma ruta densa que un prefill en frío. Al final del prefill solo se cuantiza el sufijo y se agrega a la caché TurboQuant original, por lo que los códigos del prefijo restaurado se reutilizan bit a bit en lugar de cuantizarse de nuevo. El paso se ejecuta solo cuando las capas densas, dimensionadas para todo el prompt, caben bajo el objetivo de pico de prefill, la misma condición que la reserva de prefill; de lo contrario, registra en el log que conservó el prefijo cuantizado. Un sufijo de un solo chunk, como un turno de chat, mantiene la ruta actual. Dos commits con 16 pruebas, sobre main 013bc3f3.
Resultados
Qwen3.8-27B, KV TurboQuant de 4 bits en 15 de 16 capas KV, un servidor a la vez desde el venv con versiones fijadas. A = 73,787 tokens, almacenado; B = A + 36,358 tokens con un hit de prefijo de 73,728 tokens; las posiciones [73,728, 110,086) de B se comparan con un prompt en frío de la misma sesión. Una ejecución por configuración.
| Ejecución | Sufijo restaurado | En frío, mismas posiciones | Proporción | Reserva tras la restauración |
|---|---|---|---|---|
main c0b1056b, MTP desactivado | 197.9 s | 82.2 s | 2.41× | across 1 caches (+0.14GB) |
main + corrección, MTP desactivado | 87.3 s | 94.7 s | 0.92× | across 16 caches (+2.23GB) |
| Fork + corrección, configuración del fork, MTP activado | 84.8 s | 85.0 s | 1.00× | across 16 caches (+2.23GB) |
- TTFT de B: 198.8 s en
main, 87.9 s con la corrección. La copia densa puntual de 15 capas × 73,728 tokens tarda 0.43 s. - Calidad (MTP desactivado, greedy, 512 tokens, logprobs por token desde un hook de depuración local): en frío frente a en frío es idéntico. La salida restaurada, con o sin la corrección, se aparta de la de en frío solo en casi empates (un margen de 0.125 nat, un paso de logprob de bf16), en los mismos tokens, porque el propio prefijo cuantizado desplaza la distribución. Con y sin la corrección, las salidas restauradas coinciden durante los primeros 239 y 97 tokens, y luego se separan en empates exactos.
- Memoria: la reserva tras una restauración planifica ahora las capas densas (16 cachés, +2.23 GB en lugar de 1 caché, +0.14 GB), y las guardias de memoria las ven como uso actual. El pico de prefill es de 26.29 GB frente a 25.22 GB para un prompt en frío de la misma longitud; la diferencia es el prefijo cuantizado, de alrededor de 1 GB, que se conserva hasta el final del prefill.
- Prueba rápida en vivo con fork.7 (kernels compilados): un prompt de 41,323 tokens con 24,576 tokens restaurados registró
dequantized 15 restored TurboQuant layers (24576 tokens) for a 16746-token suffix prefill in 0.16sy una reserva sobre 16 cachés. El TTFT fue de 20.6 s, frente a 53.9 s para el mismo prompt en frío.
main. La alternativa cuantizada nunca se activó, y las reanudaciones de pausas de decode nunca llegan a ese paso, porque restauran su caché exacta. La corrección se propone como un pull request pequeño e independiente después de PR1, separado del stack de pausa y reanudación.La estrategia: pausar, no abortar
La idea más valiosa del proyecto fue cambiar la pregunta. Cuando la memoria alcanza el límite duro, el servidor estándar aborta las solicitudes en curso, lo que descarta todos los tokens a los que ya se les hizo prefill y todos los generados. Nos preguntamos si el servidor podía pausar una solicitud en su lugar. La caché SSD paginada ya existía y hace que una pausa sea barata.
La primera versión, el park, cubría solo el prefill por chunks. Un benchmark de forzado (P7) mostró que funciona, y también dónde se detiene. El siguiente pico afectó a una solicitud en decode, que no se podía pausar, y las tres solicitudes se abortaron. Eso llevó al ciclo completo. Pausa y reanuda solicitudes en prefill y en decode, incluidos los prompts con imágenes, y rechaza con un error estructurado una solicitud que nunca podrá caber. A continuación se describe qué hace el código, verificado contra la implementación (omlx/scheduler.py, omlx/process_memory_enforcer.py, omlx/settings.py).
La primera versión: estacionar un prefill
- Guardar el progreso durante el prefill (D1,
prefill_progress_commit_blocks). Cada N bloques completados (4 bloques de 2048 tokens en la app del fork), los bloques con prefill se almacenan en la caché SSD paginada bajo la tabla de bloques propia de la solicitud. Esto requiere la caché SSD. Sin ella se omite el commit y un park se reanuda en frío. - Estacionar en lugar de abortar (T7,
memory_pressure_park). Con presión dura (no de emergencia, y dentro de un presupuesto de 30 sondeos de aplazamiento), el enforcer de memoria pide al scheduler que estacione a una víctima antes de considerar un aborto. El scheduler descarta la caché KV en vivo de la víctima, lo que libera la memoria. Conserva el stream del cliente, el prompt, los parámetros de muestreo y la hora de llegada. Fija los bloques confirmados contra el desalojo, tanto en el pool de bloques en memoria como en el índice del SSD. La solicitud sigue registrada y en espera, y el cliente no recibe ningún error. - Reanudar desde la caché fría. En la primera versión, una solicitud estacionada se readmitía tras
park_resume_ok_polls(3 por defecto) sondeosokconsecutivos, cuando el monitor quedaba en silencio, o cuando no se podía admitir nada más. La readmisión hace una búsqueda de prefijo normal, que restaura los bloques confirmados, y el prefill continúa desde el último límite confirmado. El ciclo completo reemplazó esta regla de reanudación por una consciente de la memoria (más abajo).
tests/test_scheduler_park.py más pruebas del enforcer. Con la configuración normal no se activó en nuestra carga de trabajo:
- En P2 el fork terminó con 0 abortos y 0 parks.
- Ningún log en vivo contiene un evento
Parked request. - Los cuatro intentos de park en vivo (2026-10-02) encontraron solo solicitudes en decode y recurrieron al aborto.
Forzarlo: el benchmark de park P7
La configuración consistía en tres prompts largos concurrentes de unos 130k, 115k y 100k tokens, iniciados con 0.5 s de diferencia, en un servidor aislado. Usaba un techo de memoria personalizado bajo (42 GB, umbral duro al 80%), y la recuperación de buffers agrupados estaba desactivada para que la presión llegara a la decisión de park. Lo ejecutamos una vez con prompts solo de texto y otra con una imagen al principio de cada prompt, en el fork completo y en upstream main, un servidor a la vez.
07:04:13 Hard memory pressure: parking a prefill victim on 1 scheduler(s) instead of aborting (poll 1/30) 07:04:13 Parked request e2f2… at 129024 tokens committed (park 1/3), reason: hard memory pressure 07:04:14 Resumed parked request e2f2…: cached=129024 07:04:23 Park skipped for e2f2…: decoding, not in chunked prefill (memory pressure falls back to abort)
| Variante | Parks | Reanudación | Resultado | Reenvío tras un aborto |
|---|---|---|---|---|
| Fork, texto (2 ejecuciones) | 1 por ejecución | 0.5 s, los 129,024 tokens confirmados restaurados | El siguiente pico afectó a la solicitud reanudada mientras estaba en decode. No se pudo estacionar, y las 3 se abortaron. | En caliente. Se registraron restauraciones de 129,024 tokens, 126,976 de 130,029 en una ejecución más larga. |
| Fork, imagen (2 ejecuciones) | 0 | n/a | Se omitió cada vez ("multimodal prompt … prefills in the external loop") y luego se abortó. | En caliente. Se registraron restauraciones de 129,024 y 98,304 tokens (alrededor del 98% del prompt). |
main, texto | 0 | n/a (sin ruta de park) | 2 solicitudes rechazadas por la verificación previa de prefill (HTTP 400) y luego reenviadas en frío. | En frío (0 en caché). |
main, imagen | 0 | n/a | 3 de 3 abortadas, y los reenvíos en frío también se abortaron. | Ninguno. |
P7 demostró que la ruta funciona: una solicitud en prefill se pausó, se liberó su memoria y se reanudó desde la caché SSD en alrededor de medio segundo con todos los tokens confirmados restaurados. No salvó un lote, porque las filas en decode y los prompts con imágenes seguían recurriendo al aborto.
El ciclo completo
- Víctima única (C1). Cuando no hay nada que se pueda pausar, abortar una sola víctima (la de mayor KV en vivo, la más reciente en caso de empate) en lugar de todas. El enforcer espera a que esa memoria se libere antes de elegir otra. La emergencia y la descarga del modelo mantienen el abort-all. Opcional (opt-in):
OMLX_MEMORY_ABORT_SINGLE_VICTIM. - Pausa y reanudación del decode (C2). Primero se emite el token muestreado pendiente. Se extrae la KV de la fila y se almacena como una cadena SSD exacta, asociada a la solicitud y fijada, y la fila sale del lote mientras su stream sigue abierto. Se guardan el sampler, los procesadores de logits, la posición de la secuencia de parada, el estado del parser y el
max_tokensrestante. La reanudación restaura la cadena y vuelve a insertar la fila sin prefill, y el RNG no se vuelve a sembrar. Una fila singleton de Lightning MTP se transfiere primero al decode estándar. Se excluyen las filas de MTP por lotes, DFlash, VLM-MTP y SpecPrefill. - Reanudación consciente de la memoria (C3). Una solicitud pausada se reanuda cuando la memoria libre estimada cubre su necesidad: la KV de su longitud final más un margen transitorio. Nunca se reanuda bajo presión dura. FIFO entre las solicitudes pausadas, y la retención de cabeza de línea reserva el espacio.
- Nunca cabe frente a cabe más tarde (C4). Si pesos + KV(prompt +
max_tokens) + transitorio superan la capacidad, la solicitud recibe HTTP 400context_exceeds_capacitycon siete campos numéricos yretryable: false. Una solicitud que cabe por sí sola espera en su lugar.GET /v1/omlx/capacityy el encabezadoX-oMLX-Max-Context-Tokensexponen las cifras. - Compactación en el harness (C5, lado del cliente). La extensión de pi compacta y reintenta ante
context_exceeds_capacity, y fija su umbral de compactación a partir del endpoint de capacidad. Tiene pruebas unitarias, pero todavía no tiene validación en vivo. - Prompts con imágenes (C6). Un sufijo solo de texto tras una última imagen en caché pasa por el prefill en la ruta por chunks, de modo que se puede pausar. Esta ruta se toma solo tras comprobar que las posiciones mRoPE del sufijo son
arange + deltaen cada plano. Opcional (opt-in):OMLX_MEDIA_TEXT_SUFFIX_CHUNKED.
La pausa y reanudación están detrás de memory_pressure_park (desactivado por defecto). La condición de nunca cabe, la espera de cabe más tarde y un reloj de bloqueo de admisión basado en el progreso están detrás de memory_scheduling (auto | on | off, auto por defecto). auto los activa solo cuando la guardia de memoria tiene un techo explícito (el nivel custom); de lo contrario, la admisión se mantiene como en main. Todas las ejecuciones C7 fijaron un techo personalizado, por lo que su evidencia es válida con el valor por defecto.
C7: el benchmark extendido, y qué falló primero
C7 extiende P7 a la presión durante la fase de decode, a los prompts con imágenes, a una solicitud que nunca cabe y a la identidad greedy tras una pausa. El objetivo es 0 abortos, con un servidor a la vez y la app en vivo cerrada. La primera ejecución (rama e80eb1f7, techo de 42 GB, hard 0.8, una ejecución por par) no lo cumplió:
| Escenario | Rama (primera ejecución) | main |
|---|---|---|
| Decode de texto | 0/3. Un park de prefill y 2 pausas de decode, todos reanudados (260,112 tokens restaurados), luego 1 aborto de víctima única y 2 rechazos de admisión | 0/3, abort-all |
| Decode con imágenes | 0/3, todas abortadas a la vez (abort-all) | 0/3, abort-all |
| Prefill con imágenes | 1/3; 2 solicitudes fallaron tras 60 s de admission_paused | 0/3, abort-all |
| Nunca cabe | HTTP 200 (condición inactiva), capacidad available: false | HTTP 200 |
| Sondeos de techo, decode de texto | 48 GB: 9 pausas y reanudaciones, luego 3 abortos. 52 GB: 2 pausas, 3/3 terminadas. 56 GB: sin pausas, 3/3 terminadas. | n/a |
Cada pausa registró restored N tokens exactly (bloqueo de 1.6–3.5 s), por lo que la mecánica funcionaba. Las brechas estaban en la política y en el cableado. Cada una se rastreó hasta una causa raíz en los logs del servidor y se corrigió con pruebas primero, un commit por cada una:
| Brecha | Causa raíz | Corrección |
|---|---|---|
| D1 · las pausas entran en thrashing y terminan en abortos | La salida de "nada admitido" de la retención del park liberaba una solicitud pausada en el paso siguiente sin comprobar si cabía, por lo que volvía a pausarse hasta agotar el presupuesto. | Línea de reanudación min(línea de admisión − 2%, umbral duro − 5%), regla anti-thrashing, ejecución en solitario sin nada admitido, presupuesto de decode 8 (5a881160) |
| D2 · condición de nunca cabe inoperante en el motor VLM | nn.Module.parameters() omite el atributo con guion bajo del adaptador, _vlm_model, por lo que los pesos sumaban 0 y no existía ninguna estimación. | Dimensionar los pesos a través del adaptador (e5353d70) |
| D3 · abort-all durante el prefill con imágenes | La selección de víctima única no encontraba ninguna solicitud en vivo, porque el prefill del bucle externo no está en running, y recurría a un abort-all que no liberaba nada. | El prefill del bucle externo se ejecuta en solitario, sin víctima en vivo no se aborta nada por debajo de emergencia, una pausa de decode antes del abort-all de emergencia (19aa28ef, 31cab33d) |
| D4 · identidad imposible de juzgar | Las propias referencias se abortaban con 42 GB. | Referencias en solitario con 56 GB, con margen (solo en el benchmark) |
| D5 · la tercera solicitud falla tras 60–90 s | El reloj de bloqueo de admisión seguía corriendo mientras otras solicitudes avanzaban (un decode en solitario, un prefill chunk a chunk). | Contar solo el tiempo sin progreso; límite de 600 s para retenciones deliberadas (65899460) |
| D6 · efecto de la pausa no aislado | Las divergencias eran de la numérica de lotes B=3 frente a B=1, algunas antes de la primera pausa. | OMLX_DEBUG_FORCE_DECODE_PAUSE_AT, solo para pruebas, para pausas forzadas con B=1 (f3c11356) |
| D7 · prefill con imágenes de 46 GB rechazado | Sin nada admitido, la verificación previa del uso actual contaba memoria que no pertenecía a ninguna solicitud admitida: embeddings de visión en cola más alrededor de 1.9 GB sin atribuir. | Admitir una cabeza solitaria que cabe sola bajo el techo (47c0c70b); esperar a que termine una escritura pendiente en el almacén (3b72cde7, defensiva) |
| D8 · divergencias con MTP activado tras las pausas | Probablemente el reagrupamiento de la verificación de MTP tras una pausa desplaza casi empates de bf16 (hipótesis; no aislada con MTP activado). | Nueva ejecución de identidad con MTP desactivado: tokens idénticos (benchmark 95dd47e8) |
Dos ejecuciones intermedias muestran la progresión:
- Tras D1–D3 (31cab33d, 48 GB): nunca cabe pasó, el decode con imágenes terminó 3/3 con 0 abortos y no se activó ningún abort-all. Pero el decode de texto (dos veces) y el prefill con imágenes terminaron 2/3: la tercera solicitud falló por el timeout de bloqueo de admisión, lo que llevó a D5.
- Tras D5–D6 (f3c11356): el decode de texto y el de imágenes terminaron 3/3. El prefill con imágenes con 46 GB todavía terminó 2/3, lo que llevó a D7.
Resultados finales
Mismo modelo, máquina y escenarios, una ejecución por par. main (b7c7929e) ejecutó los mismos escenarios con 48 GB. La ejecución de prefill con imágenes de la rama usó 46 GB, lo que es más duro para la rama.
| Escenario | Ejecución de la rama | Completadas | Abortos | Pausas / reanudaciones de decode | Tokens restaurados | Pico | Bloqueo máx. | main |
|---|---|---|---|---|---|---|---|---|
| Decode de texto | f3c11356 · 48 GB | 3/3 | 0 | 6 / 6 | 691,293 | 41 GB | 1.75 s | 0/3 abort-all a mitad del prefill tras 15 s |
| Decode con imágenes | f3c11356 · 48 GB | 3/3 | 0 | 5 / 5 | 561,494 | 43 GB | 1.73 s | 0/3 abort-all, bloqueo de prefill de 168 s |
| Prefill con imágenes | 47c0c70b · 46 GB | 3/3 | 0 | 1 / 1 | 132,102 | 42 GB | 1.61 s | 0/3 abort-all, bloqueo de prefill de 417 s |
Nunca cabe (42 GB, 31cab33d), un prompt de 60k tokens con max_tokens 2,000,000:
| Rama | main | |
|---|---|---|
| Respuesta | HTTP 400 context_exceeds_capacity en 0.1 s, los 7 campos, retryable: false, sin pausa ni park | HTTP 200 (stream cerrado en 0.1 s) |
GET /v1/omlx/capacity | 200, available: true | 404 |
X-oMLX-Max-Context-Tokens | 288273 | ausente |
Identidad greedy tras una pausa. B=1 con 56 GB, de modo que no hay pausa por memoria, y pausas de decode forzadas en los offsets de salida 100, 323, 1044 y 1706. Cada ejecución de prueba se compara con una ejecución sin pausas.
| Ejecución | Pausas | Resultado |
|---|---|---|
| MTP desactivado (47c0c70b; referencias en 3b72cde7) | 12 | idéntico Tokens idénticos en 3/3 solicitudes de 2,000 tokens; deriva de logprobs exactamente 0.0. El texto transmitido de una solicitud difiere en el carácter 1496. Sus tokens son iguales, y la referencia sin pausas muestra la misma peculiaridad alrededor de un token <think> suelto a mitad del razonamiento: un problema preexistente de ensamblado de texto, no un bug de la pausa. |
| MTP activado (f3c11356) | 12 | casi empates Primeras divergencias en los tokens 142, 145 y 179: 42–79 tokens después de la primera pausa y antes de la segunda, con márgenes de 0.125–1.0 nat. Con MTP desactivado los mismos prompts son idénticos. Eso apunta a que el reagrupamiento de la verificación de MTP desplaza casi empates, pero es una hipótesis: la identidad con MTP desactivado no descarta por sí misma un efecto del estado de pausa que solo aparezca con MTP activado. |
Premisas frente a main, medidas en e80eb1f7, antes de D1–D8:
| Premisa | main | Rama | Lectura |
|---|---|---|---|
| P1 · Reenvío tras cancelar a mitad del prefill | 0 en caché · 110,031 con re-prefill | 73,728 en caché · 36,303 con re-prefill | PASS |
| P2 · 2×~130k concurrentes | 2/2 · 0 abortos · 116 GB · 939 s | 2/2 · 0 abortos · 44 GB · 501 s | PASS |
| P3 · Lote greedy (B=3 vs B=1) | 3/3 degeneradas (4-gram 0.33 / 0.29 / 0.18), 121 GB, sobre b7c7929e con el runtime de la app v0.7.0; no se reproduce sobre 25aebb3e (revalidación) | sin degeneración (1.00 / 1.00 / 0.99), 55 GB | sin regresión |
| P4 · Concurrencia (2 ejecuciones) | 0.96× · 12/12 ok | 0.98× · 12/12 ok | igual |
| P6 · Una sola de 110k (3 ejecuciones, mediana) | 740.7 tok/s · decode 20.42 | 729.5 (−1.5%) · 20.75 (+1.6%) | dentro del ruido |
- Bajo presión forzada, la rama completa todas las solicitudes de los tres escenarios con 0 abortos. Las solicitudes se pausan y se reanudan en decode, incluidos los prompts con imágenes, mientras que
mainaborta las tres en cada uno. - Una pausa es exacta: con MTP desactivado, los tokens y logprobs tras 12 pausas son iguales a los de una ejecución sin interrupciones.
- Una solicitud que nunca podrá caber recibe en 0.1 s una respuesta estructurada sobre la que un harness puede actuar, en lugar de terminar en un aborto.
- Límites:
- una Mac, una familia de modelos y una ejecución por par;
- el decode y el decode con imágenes se midieron en f3c11356 y nunca cabe en 31cab33d, por lo que esos escenarios no se volvieron a ejecutar tras las últimas correcciones de admisión;
- las ejecuciones de la rama también tenían activados el límite de caché de Metal y la condición de decode por lotes del fork;
- una pausa bloquea todas las filas durante 1.6–3.5 s mientras se escribe la cadena exacta, porque el almacenamiento es síncrono;
- las premisas deben volver a ejecutarse sobre la cabeza final, porque D1–D8 cambiaron la admisión después de e80eb1f7.
Comandos de reproducción
El harness es benchmarks/unattended/bench_premises.py en la rama del fork bench/p7-park (95dd47e8). Superpone el omlx/ de un checkout sobre el runtime de la app. Ejecute un solo servidor oMLX a la vez, con todas las demás instancias cerradas. La variante fork fija OMLX_MEMORY_PRESSURE_PARK=true y OMLX_PREFILL_PROGRESS_COMMIT_BLOCKS=4.
# branch build: export once, so every command below gets the same env export OMLX_BENCH_FORK_REPO=<pause-resume checkout> export OMLX_BENCH_EXTRA_ENV=OMLX_DISABLE_PRESSURE_RECLAIM=1,OMLX_MEMORY_ABORT_SINGLE_VICTIM=true,OMLX_MEDIA_TEXT_SUFFIX_CHUNKED=true # references for one scenario: alone, 56 GB python benchmarks/unattended/bench_premises.py --variant fork --premise C7 \ --c7-scenario decode --runs 1 --p7-ceiling-gb 56 --c7-refs-only --out refs/ref-decode # pressure run: decode | image-decode at 48 GB, image-prefill at 46 GB python benchmarks/unattended/bench_premises.py --variant fork --premise C7 \ --c7-scenario decode --runs 1 --p7-ceiling-gb 48 --c7-refs-from refs/ref-decode --out out/branch-decode # never fits python benchmarks/unattended/bench_premises.py --variant fork --premise C7 \ --c7-scenario never-fits --runs 1 --p7-ceiling-gb 42 --out out/branch-never-fits # identity with MTP off: references, then forced pauses python benchmarks/unattended/bench_premises.py --variant fork-nomtp --premise C7 \ --c7-scenario identity --runs 1 --p7-ceiling-gb 56 --c7-refs-only --out out/idrefnm python benchmarks/unattended/bench_premises.py --variant fork-nomtp --premise C7 \ --c7-scenario identity --runs 1 --p7-ceiling-gb 56 --c7-refs-from out/idrefnm \ --c7-force-pause-at auto --out out/idforcenm # main build: re-export, then run the same scenario commands (with main's own --out paths) export OMLX_BENCH_FORK_REPO=<main checkout> export OMLX_BENCH_EXTRA_ENV=OMLX_DISABLE_PRESSURE_RECLAIM=1 # unit and rig tests, then upstream's CI selection (needs pytest-xdist, as CI installs it) pytest tests/test_scheduler_decode_pause.py tests/test_memory_resume_thrash.py \ tests/test_memory_external_prefill.py tests/test_scheduler_capacity_vlm.py \ tests/test_scheduler_forced_decode_pause.py tests/test_scheduler_solo_admission.py pytest tests/ -m "not slow and not integration" -n 3 --dist loadgroup
Nuestras ejecuciones también pasaron OMLX_BATCHED_DECODE_HOLD_MAX_S=120, OMLX_MID_SYSTEM_POLICY=inline_note y OMLX_THINKING_RETENTION_GUARD_TOKENS=20000 a ambas builds. Pertenecen a otras funcionalidades del fork, y main las ignora. Prompts de los escenarios: unos 130k, 115k y 100k tokens, iniciados con 0.5 s de diferencia, greedy, max_tokens 2000 (32 para el prefill con imágenes).
Ejecución en producción real (fork.5)
Los resultados de C7 provienen de presión forzada en un servidor aislado. Después ejecutamos el fork empaquetado como servidor diario para trabajo real de agentes de programación desatendidos: pi con el harness gentle-shell, una sesión orquestadora y cuatro subagentes de gentle-shell haciendo captura de documentación guiada por navegador para un proyecto de documentación, más una segunda sesión de agente no relacionada. Cada solicitud fue al modelo local a través de la fijación del perfil.
| Configuración | Valor |
|---|---|
| Build | oMLX Fork 0.7.0-fork.5: feat/pause-resume-cycle sobre el commit b7c7929e de upstream main (#4148, confirmado el 2026-10-05 16:29:00 +0900), ejecutándose sobre el runtime oficial de la app 0.7.0 (mlx-vlm 0.7.1; upstream main fija 0.7.4). La opción --allow-pin-mismatch del script de empaquetado (5dd56ca5) lo hace explícito: construye sobre el runtime anterior, emite una advertencia y registra la discrepancia en el resumen de la build. |
| Modelo y máquina | Qwen3.8-27B-oQ4e-mtp (KV TurboQuant de 4 bits, MTP), M5 Max 128 GB |
| Valores por defecto del fork | Se distribuyen como un archivo de valores por defecto incluido, leído como una capa por debajo de settings.json, de modo que settings.json o la interfaz de administración los sobrescriben sin recompilar (c31820cb, 78291feb). Antes estaban integrados en el entorno de la app. El paquete activa el commit de progreso de prefill, el park y la pausa, el aborto de víctima única, el sufijo de texto con medios en caché y memory_scheduling: on. |
| Versiones del harness | gentle-shell (paquete gentle-pi 4.0.0) en el commit aa2c0389 de upstream main, "Merge pull request #1835" (feat/background-jobs), confirmado el 2026-10-07 01:30:59 +0200, ejecutado desde un checkout local con gentle-shell.mjs --link. Este main no contiene la corrección de la caché de prompt de todo de #1367, que todavía estaba abierta. Otra sesión ejecutó la rama de la corrección fix/todo-prompt-cache-1367 (955523df, 2026-10-05); sus sesiones principales usaron un proveedor remoto, por lo que solo su subagente llegó a oMLX. |
| Condición de decode por lotes | Eliminada de la app del fork (159673d5): las pausas de decode resuelven ahora la presión de memoria sin perder trabajo, por lo que el límite de 200k de contexto combinado solo frenaba las solicitudes largas mientras había memoria libre |
| Empaquetado exclusivo del fork | El paquete de valores por defecto, la eliminación de la condición y --allow-pin-mismatch pertenecen solo al empaquetado de la app del fork. No forman parte de ningún pull request a upstream. |
| Guardia de memoria | Nivel aggressive: techo de 101.7 GB, soft al 95% y hard al 98% (unos 96.6 y 99.7 GB). memory_pressure_park activado. |
| Planificación de memoria | Activada durante toda la ventana, a partir de los valores por defecto del fork (verificado en vivo: endpoint de capacidad available: true y el encabezado X-oMLX-Max-Context-Tokens presente) |
| Ventana | 2026-10-07 02:31–04:10 hora local, alrededor de 1 h 39 min, 393 muestras cada 15 s. max_concurrent_requests se elevó en vivo a través de la API de administración, sin reiniciar: 3, luego 4 a las 03:02:00, luego 5 a las 03:08:15. |
| Límite | Muestras | Solicitudes abiertas (prom. / máx.) | En espera (prom.) | Mín. libre del sistema | Eficiencia de caché |
|---|---|---|---|---|---|
| 3 (02:31–03:02) | 121 | 5.2 / 6 | 2.2 | 42% | 94.5% |
| 4 (03:02–03:08) | 25 | 4.9 / 5 | 0.9 | 30% | 94.8% |
| 5 (03:08–04:10) | 247 | 4.0 / 6 | 0.1 | 20% | 95.4% |
Las solicitudes abiertas son las que tienen un stream abierto, en ejecución o en cola, y las que están en espera son las encoladas (ambas desde /api/status). La memoria libre del sistema proviene de memory_pressure de macOS e incluye los navegadores de los agentes y otras apps. La eficiencia de caché es la cifra acumulada del panel al final de cada nivel.
| Log del servidor, 02:00–04:10 (incluye los 31 min previos al inicio del muestreo a las 02:31; la tabla por nivel anterior cubre 02:31–04:10) | Cantidad |
|---|---|
| Solicitudes · tokens de prompt · servidos desde la caché de prefijo | 333 · 14.5M · 94.8% |
| Transiciones de presión de memoria (soft, hard, emergencia) | 0 |
| Pausas de decode, reanudaciones y parks | 0 |
| Admisiones en solitario, esperas de admisión, 400 de nunca cabe | 0 |
| Abortos por memoria | 0 |
| Otros abortos | 3 a las 03:09:01. El orquestador canceló 3 tareas de subagentes, y el servidor registró abortos de stream sin ninguna acción del enforcer. |
| Decode TurboQuant por lotes | Ruta ragged de un solo lanzamiento con B=4 (03:04) y B=5 (03:08). Ninguna respuesta alcanzó el límite de tokens: 320 terminaron en llamadas a herramientas y 6 en stop. |
| Oleada de subagentes | Sesiones | Solicitudes | Prompts de sistema por sesión | Turnos de solo agregado | Caché por sesión |
|---|---|---|---|---|---|
| 02:28–03:09 | 4 | 92 | 1 | 88 de 88 | 95.5–98.3% |
| Desde 03:32 (todavía en ejecución) | 4 | 162 a las 04:10 | 1 | 158 de 158 | 95.6–98.5% (hasta 04:14) |
- Concurrencia elevada en vivo. Con un límite de 5 la cola casi se vació (promedio en espera 2.2 → 0.1), mientras que la eficiencia de caché se mantuvo en 94.5–95.4%.
- Decode por lotes con B=4 y B=5. Sobre upstream
mainb7c7929e con el mismo runtime de la app v0.7.0, P3 con B=3 degeneró o abortó. Sobre unmainmás reciente 25aebb3e se completa, pero el decode por lotes con relleno se ejecuta a alrededor de 1.5 tok/s por fila (revalidación de P3). - Arranques en frío cuando una oleada llega de golpe. Las primeras solicitudes de cada oleada (unos 18k tokens, en su mayoría el prompt de sistema compartido) llegaron juntas. De 8, 5 hicieron prefill en frío en paralelo (58–129 s) y 3 reutilizaron 16,384 tokens compartidos (87–91%). Después de eso, cada turno de subagente fue de solo agregado.
- Throttle de prefill de upstream. El throttle adaptativo de prefill de upstream pausó prefills por chunks 21 veces en 14 solicitudes para redimensionar los chunks. La memoria de proceso más alta que registró fue de 46.6 GB. No se trata de la pausa y reanudación del fork.
- La planificación de memoria se mantuvo activada todo el tiempo. El log muestra líneas "Memory scheduling: off" en grupos de cuatro u ocho (00:27, 03:02, 03:08, 04:19, incluidos momentos sin cambios de configuración). Provienen de la página de configuración de administración, que construye cuatro enforcers desechables (uno por nivel de memoria, modo por defecto
auto) para previsualizar los techos de cada nivel; el enforcer en vivo se mantuvo activado, como confirman el endpoint de capacidad y el encabezado de contexto. Las líneas engañosas están corregidas en el fork (04bb7899, propuesto a upstream con el slice G): solo el enforcer en vivo registra su estado, y la API de configuración de administración informa el estado efectivo comomemory_scheduling_effective. Por lo tanto, los conteos en cero de admisiones en solitario, esperas de admisión y nunca cabe reflejan una carga de trabajo que nunca se acercó a los límites de memoria, no funcionalidades desactivadas.
aggressive, la presión empieza al 95% de un techo de 101.7 GB, y esta carga de trabajo se mantuvo muy por debajo. La evidencia del ciclo siguen siendo los resultados de presión forzada de C7 de más arriba. El siguiente paso es una ejecución en vivo con un techo explícito más bajo, por ejemplo 45 GB en el nivel custom.Hallazgos del harness en las mismas sesiones
Las mismas sesiones también sacaron a la luz cambios de prompt del lado del harness. Se enumeran en la sección de gentle-shell.
Material grabado en campo: carga sostenida
Grabaciones de pantalla de la misma noche (alrededor de las 04:28–04:43, poco después de la ventana medida), aceleradas. El panel de oMLX se muestra en su configuración regional en español. El contenido de la terminal y del IDE está pixelado por privacidad; las cifras del panel son las grabadas. Era una carga normal, por lo que no aparece ninguna pausa ni reanudación en pantalla; la evidencia de pausa y reanudación son las ejecuciones de presión forzada de C7 de más arriba. Los controles permiten pausar, volver a reproducir e inspeccionar las grabaciones. Los otros videos acompañan las secciones del caso de uso, decode por lotes y prefill.
Qué se construyó
| Cambio | Problema que resuelve | Evidencia |
|---|---|---|
| Commit de progreso de prefill (D1) | Una cancelación o un aborto a mitad del prefill descartaba todo. Ahora el progreso se confirma en la caché SSD paginada cada N bloques, para prompts de texto y de medios. | P1 |
| Estacionar en lugar de abortar (T7) | Bajo presión de memoria, un prefill por chunks se estaciona (con el progreso ya en la caché SSD) y se reanuda en orden de llegada en lugar de abortarse. Ver la estrategia. | Pruebas unitarias; P7 (estacionado en 129,024 tokens, reanudado en 0.5 s) |
| Ciclo de pausa y reanudación (C1–C6, D1–D8) | Las filas en decode y los prompts con imágenes no se podían estacionar, por lo que la presión seguía abortando todo. Ahora una solicitud se pausa en prefill o en decode, se guarda como una cadena SSD exacta y se reanuda en el mismo stream cuando su necesidad de memoria cabe. Las solicitudes que nunca caben reciben un 400 estructurado y un endpoint de capacidad. | C7: 9/9 completadas, 0 abortos (main 0/9); tokens idénticos tras 12 pausas con MTP desactivado |
mlx_cache_limit_gb (T5a) | La caché de buffers de Metal crecía sin límite y empujaba la memoria del proceso por encima del umbral duro. | Pico de memoria de P2 |
| Condición de contexto del decode por lotes + backfill/envejecimiento (T5b, T12b) | Dos filas muy largas en el mismo lote no ganan nada. Una solicitud larga retenida ya no bloquea a las cortas, y el envejecimiento acota la espera a 120 s. | P4; un barrido de límites no mostró ganancia por encima de 200k combinados |
| Corrección de la atención con relleno de TurboQuant + kernel ragged (T5d, T5e, T12a, T12c) | Las filas cortas con relleno a la izquierda atendían sobre el relleno (corrección), y el decode por lotes era de alrededor de 1.5 tok/s por fila (rendimiento). | Proporción de normas 0.32 → 1.0; por capa con B=3 39.8 → 1.7 ms; P3, P4 |
Política de sistema intermedio inline_note (A1) | Una actualización de sistema a mitad de la conversación tras el resultado de una herramienta recurría a strict y se fusionaba con el mensaje de sistema inicial, reescribiendo la cabeza del prompt. | Prueba RED: primera divergencia al final de la cabeza del sistema |
| Retención determinista del razonamiento (A2) | El mensaje de usuario sintético de "imagen adjunta" de un cliente movía el last_query_index de la plantilla y eliminaba el razonamiento a mitad del bucle, rompiendo el prefijo en cada turno con imagen. | 10/10 turnos con imagen con 97.4–99.7% de hit |
| Observabilidad (A5) | El desalojo era silencioso en INFO, no había un motivo del resultado de caché por solicitud y no había desglose de TTFT. | Usada en cada medición en vivo de este informe |
La investigación de la reutilización de caché
Sin abortos, el costo pasó al re-prefill. Atribuimos a una causa cada ruptura grande de caché en el tráfico registrado. Las rupturas provenían de cambios del prompt en el medio, lo que invalida todo lo que viene después.
| Causa | Tokens con re-prefill (~1.5 días) | Dónde se corrige |
|---|---|---|
El harness reescribía el prompt de sistema (bloque de todo con un contador de obsolescencia por prompt en las sesiones padre afectadas; los cambios registrados en la sección de herramientas también incluían tráfico de subagentes), y la alternativa strict del servidor plegaba las actualizaciones de sistema en la cabeza del sistema | ~1.05M | Corrección del harness (gentle-shell #1367) + A1 (servidor) |
Mensaje de imagen sintético del cliente que mueve last_query_index | ~0.94M | A2 (servidor) |
| Compactaciones del cliente | ~0.7M | Esperado |
| Mensajes reales del usuario (la plantilla elimina el razonamiento anterior) | ~0.1M | Se mantuvo por calidad en el modelo de 8 bits; preserve_thinking: true lo elimina en el modelo actual (medido) |
La causa más grande estaba en el harness. Cómo se encontró, qué costó, la corrección y su validación están en la sección de gentle-shell.
El cambio A1 del lado del servidor es complementario. pi también envía los cambios del prompt de sistema como mensajes de sistema a mitad de la conversación, y la alternativa strict de oMLX los volvía a fusionar en la cabeza. Cualquier cliente que haga lo mismo se beneficia de A1.
preserve_thinking: true, medido de nuevo
El resultado anterior. En el modelo de 8 bits con decodificación especulativa DFlash, conservar todo el razonamiento anterior daba una reutilización casi perfecta, pero alrededor de los 53k tokens de contexto el modelo empezaba a emitir </think> literales y XML de llamadas a herramientas sin procesar como texto, su aceptación del borrador caía del 65% al 54% y la sesión no se recuperaba. A2 se diseñó en torno a ese resultado. Conserva el razonamiento solo dentro del bucle de agente actual y lo elimina en los mensajes reales del usuario, exactamente como hace la plantilla con preserve_thinking: false. Una guardia determinista lo elimina una vez cuando el razonamiento retenido del bucle supera N tokens, y luego se mantiene de solo agregado. La guardia está fijada en 30k.
La pregunta. Con false, cada mensaje con rol de usuario elimina el razonamiento del segmento anterior, por lo que el prompt se reduce y el prefill se reinicia en el primer mensaje del asistente de ese segmento. pi envía los avisos del harness (actualizaciones de todo, resultados y despertares de subagentes, avisos de trabajos en segundo plano) como mensajes con rol de usuario, por lo que provocan la misma eliminación. Esa era la última pérdida de caché que quedaba en la sesión principal tras la corrección de gentle-shell. true nunca se había probado durante mucho tiempo en el modelo actual (oQ4e de 4 bits con Lightning MTP), así que lo medimos el 2026-10-11.
Método. La app instalada fork.7 era el único servidor en ejecución. La configuración se cambió en tiempo de ejecución a través de la API de administración (PUT /admin/api/models/<id>/settings, que se aplica sin recargar y persiste), tras un respaldo de la configuración del modelo. El cliente fue el gentle-shell instalado en modo RPC de pi. Cada solicitud de pi se asoció a su línea en el log del servidor mediante sus conteos de tokens de prompt y en caché.
- P1, escenario del harness. Una sesión principal con una lista de tareas pendientes, un subagente en primer plano, un subagente en segundo plano y su despertar, un trabajo en segundo plano y su aviso, y dos prompts de seguimiento.
truese comparó con una ejecuciónfalsedel mismo driver en el mismo servidor. - P2, sesión larga y realista. Un guion fijo de 27 prompts de programación reales sobre un clon temporal del fork de oMLX: leer y explicar código, encontrar puntos de llamada, escribir y ejecutar pruebas unitarias, corregirlas, escribir un parser de logs con pruebas, usar git. Hizo 99 llamadas a herramientas en cada ejecución. La ejecución
trueagotó su presupuesto de 110 minutos tras 27 prompts. El controlfalseejecutó después los mismos 27 prompts. Contamos marcadores de degeneración en el texto del asistente, en los argumentos de las herramientas y en el razonamiento:<think>/</think>literales,<function=,<parameter=,<tool_call>, bucles de repetición en el texto visible, paradas por longitud y errores de parseo de llamadas a herramientas. Las señales del detector de repetición en el razonamiento se presentan por separado a continuación.
Configuración fija en todas las ejecuciones: razonamiento forzado activado (presupuesto de 3,000 tokens), la guardia de retención de 30k activada, MTP activado, KV TurboQuant de 4 bits, temperatura 1.0.
| P2: 27 prompts, mismo guion | true | false |
|---|---|---|
| Contexto en el prompt 1 → prompt 27 | 41.8k → 200.0k | 41.8k → 135.4k |
| Límites de turno de usuario que reutilizaron todo el prefijo en caché | 26 / 26 | 3 / 26 |
| Tokens no cacheados por límite, media / máx. | 893 / 1,870 | 4,048 / 15,056 |
| Tiempo hasta el primer token en un límite, media / máx. | 6.8 / 15.9 s | 8.5 / 31.3 s |
| Tasa de hit de la caché de prefijo, todas las solicitudes | 99.1% | 98.1% |
| Tiempo de prefill sumado | 690 s | 660 s |
| Tokens de salida / caracteres de razonamiento conservados en el contexto | 106k / 278k | 66k / 115k |
| Aceptación de MTP (estable en los tramos de 40k–200k) | 83.9% | 84.1% |
| Decode tok/s, del tramo de contexto más bajo → al más alto | 20.8 → 13.8 | 23.1 → 15.1 |
| Marcadores literales en texto, argumentos y razonamiento; bucles en texto visible; paradas por longitud; errores de parseo | 0 | 0 |
| Señales del detector de repetición en razonamiento (sin clasificar) | 3 | 0 |
| Errores de herramientas | 2 | 7 |
| Tiempo total para los 27 prompts | 6,670 s | 3,773 s |
Auditoría de los registros primarios. El recuento de tool_errors en preserve-thinking/p2-false-turns.jsonl da 7 y corrige los 9 reportados antes. El archivo correspondiente p2-true-turns.jsonl contiene 3 señales del detector thinking_loops; el resumen original solo contaba loops en texto visible. El detector señala líneas de al menos 30 caracteres repetidas al menos cuatro veces dentro de un mensaje. Estas señales siguen sin clasificar y pueden incluir falsos positivos; no son degeneración confirmada. Los registros locales se auditaron sin volver a ejecutar el experimento y actualmente no se enlazan como un archivo público.
En P1, true hizo que las 11 solicitudes de la sesión principal fueran hits de prefijo completo, frente a 4 divergencias alineadas a bloques en 13 con false. Los tokens no cacheados bajaron de 6,208 a 1,803 y el prefill sumado de 17.2 a 8.1 s. Las sesiones de subagentes y de trabajos se comportaron igual con ambas configuraciones.
Hallazgos.
- Sin degeneración visible confirmada con
trueen este modelo hasta 200k de contexto; las señales de repetición en razonamiento siguen sin clasificar, alrededor de 4× el punto en que falló la configuración de 8 bits con DFlash. La aceptación de MTP se mantuvo estable, y las comprobaciones puntuales a 90k, 115k, 121k y 196k citaron correctamente código y referencias archivo:línea del contexto. El único desliz fue a 125k: el modelo ejecutó un comando en una ruta de documentación citada en el prompt de sistema del harness y se corrigió en la llamada siguiente. Se trata de una ejecución por configuración con temperatura 1.0, por lo que es evidencia, no prueba. - La ganancia de caché es real, pero pequeña con estos tamaños de segmento.
trueeliminó todas las divergencias en los límites, pero aquí cada turno agregaba solo 1–15k tokens, por lo que una divergencia costaba unos pocos segundos. El re-prefill de varios minutos observado confalseen el uso diario (unos 560 s con 135k) requiere un turno que lea decenas de miles de tokens de archivos antes del siguiente mensaje del usuario. Este guion no produjo ninguno. truetiene un costo: el contexto se llena alrededor de 1.5× más rápido, porque todo el razonamiento permanece en él. El modelo además razonó alrededor de 2.4× más en esa ejecución, y el decode se ralentiza a medida que crece el contexto. Los mismos 27 prompts tardaron 1.8× más, y el prefill sumado fue aproximadamente igual, porquetruehizo prefill de un contexto mucho más largo. Parte del razonamiento adicional puede deberse a la varianza del muestreo.- Factor de confusión: la guardia de retención de 30k se mantuvo activada en ambas ejecuciones. Con
truenunca se activó; el prompt nunca se redujo.
Decisión. No se observaron marcadores literales de degeneración con true en esta única ejecución sobre este modelo; las señales del detector de repetición en razonamiento siguen sin clasificar y estas observaciones no demuestran la ausencia de riesgo de calidad. El 2026-10-11 se activó en el uso diario para seguir observándolo en cargas de trabajo donde algunos turnos leen archivos grandes, aceptando los costos medidos en crecimiento del contexto y tiempo total. Lo que hay que vigilar son los marcadores de degeneración, el TTFT en los límites, la frecuencia con que las sesiones llegan a la compactación y el tiempo total por tarea. La guardia de A2 se mantiene en 30k como respaldo.
Resultado en vivo
- Turnos que agregan una imagen nueva: 10 de 10 tuvieron un hit de 97.4–99.7%, por lo que solo se hizo prefill del nuevo resultado de herramienta y de la imagen.
- Turnos consecutivos tras la corrección del cliente: 31 de 31 estrictamente de solo agregado.
- Eficiencia de caché a nivel de sesión en el panel: 95.2–95.3%.
gentle-shell: hacer el harness compatible con la caché
Esta sección está dirigida a los mantenedores de gentle-shell. La mayor pérdida individual de caché de este informe estaba en el harness, no en oMLX. Mientras había tareas pendientes abiertas, la extensión de todo de gentle-shell reescribía el prompt de sistema en cada prompt del usuario. En alrededor de 1.5 días de uso real causó unos 1.05M tokens con re-prefill, aproximadamente 80–85 minutos de tiempo de GPU, y turnos que tardaban 2–6 minutos en lugar de segundos. Mejoramos el harness con el mismo método que el servidor, y la corrección está lista en fix/todo-prompt-cache-1367, registrada en #1367.
Las pruebas existentes del harness no expusieron el costo de caché: la herramienta de todo se comportaba correctamente y ninguna solicitud fallaba, pero algunos turnos del agente tardaban minutos en lugar de segundos. Identificamos la causa al ejercitar el harness contra un servidor de inferencia local cuyo comportamiento de caché podíamos observar. Las pruebas de estabilidad del prompt pueden detectar la reescritura en sí; las mediciones del servidor establecen su costo operativo.
El proceso
El síntoma
El panel de oMLX mostraba una y otra vez prefills largos, de 40k–110k tokens, en turnos del agente que deberían haber sido casi gratuitos, con solicitudes concurrentes congeladas detrás. Nada fallaba y todas las pruebas del harness pasaban.
Evidencia del lado del servidor
El log de la caché de prefijo de oMLX indica dónde diverge cada solicitud de la secuencia almacenada más cercana. Una sesión divergía en la misma posición, alrededor del token 37.7k, cada pocos turnos. Una divergencia en una posición fija apunta a algo que reescribe el prompt en ese punto, no a un desalojo.
Evidencia del lado del cliente
Una pequeña extensión de pi registró el payload exacto de cada solicitud. Comparar solicitudes consecutivas permitió encontrar el primer byte modificado dentro del prompt de sistema, en el contador de obsolescencia del apéndice de todo, en la misma posición de token que informaba el servidor.
Atribución
Hacer lo mismo en todas las sesiones registradas atribuyó unos 945k tokens con re-prefill en sesiones principales y 107k en subagentes a cambios del prompt de sistema o de la sección de herramientas, con el apéndice de todo como causa principal.
Corrección, con pruebas primero
El estado de todo salió del prompt de sistema y pasó a mensajes de conversación ocultos, enviados solo cuando la lista cambia. El recordatorio de obsolescencia se activa una sola vez, sin contador, y el estado se vuelve a inyectar una vez tras una compactación. Las pruebas ejercitan la extensión a través de sus hooks públicos y comprueban que el prompt de sistema se mantiene estable byte a byte.
Validación en ambos lados
En el cliente, las solicitudes que reescribían el prompt de sistema pasaron de 21 de 61 a 0 de 90, y 31 de 31 turnos consecutivos fueron de solo agregado. En el servidor, los re-prefills por encima de 20k tokens pasaron de 1–2 por hora a 0.
Nuevas pruebas sobre upstream main
Cada nueva prueba ejercitó el harness contra el servidor en ejecución. Una ventana sobre aa2c0389 no mostró ningún problema porque su disparador nunca se produjo, y una prueba posterior sobre 43541a0d lo reprodujo. Nuestro primer comentario se corrigió públicamente. La corrección se rebasó, sus 63 pruebas de todo pasan y superó una revisión independiente.
Subagentes y trabajos en segundo plano
Un escenario completo del harness con la corrección (lista de todo, subagentes en primer plano y en segundo plano, un trabajo en segundo plano) mantuvo un solo prompt de sistema por sesión en el padre y en cada hijo.
La última pérdida, compartida con el servidor
Las pérdidas de caché restantes en el padre provenían de los avisos del harness. pi los envía como mensajes con rol de usuario, y la plantilla de chat elimina el razonamiento anterior en cada mensaje con rol de usuario cuando preserve_thinking está desactivado. Con la opción activada, cada solicitud del padre reutilizó su prefijo completo.
Lo que costó en el servidor
Esto corresponde a alrededor de 1.5 días de uso real, todo en una sola máquina. Las cifras marcadas con ≈ se derivan de tasas medidas.
| Costo | Sin la corrección del harness | Cómo se deriva |
|---|---|---|
| Tokens con re-prefill | ~1.05M | Divergencias atribuidas a cambios en el prompt de sistema o en la sección de herramientas (sesiones principales más subagentes) |
| Tiempo de GPU dedicado al re-prefill | ≈ 80–85 min | 1.05M tokens a la tasa media de prefill observada de 205–211 tok/s. Los contextos más profundos son más lentos, por lo que esto es una cota inferior. |
| Espera adicional por turno afectado | ≈ 2–6 min | 40k–110k tokens con re-prefill por evento a 300–430 tok/s. La tasa cae a unos 110 tok/s cerca de 170k de contexto. |
| Otras solicitudes congeladas | hasta 155–158 s por evento | Los prompts con imágenes hacen prefill de forma síncrona, por lo que el decode concurrente se detiene durante todo el re-prefill. |
| Escrituras en la caché SSD | ≈ 90 GB | Los bloques con re-prefill se vuelven a almacenar: unos 513 bloques × 0.1787 GB por bloque de 2048 tokens (KV más el sidecar GDN). Eso agita una caché de 150 GB y desaloja antes historial útil. |
| Concurrencia perdida | las solicitudes retenidas esperan más | Los re-prefills largos mantienen ocupado el carril de decode, por lo que la condición de decode por lotes retiene más tiempo a otras sesiones. |
En una API alojada, un prefijo modificado puede reducir la entrada cacheada y aumentar la latencia o el costo, según las reglas de caché y los precios del proveedor. Aquí no se hizo ningún experimento de caché o facturación con proveedores alojados; los efectos medidos corresponden a la configuración local probada.
Cómo se identificó la invalidación
- El síntoma en el panel. Durante el uso normal, el panel de oMLX mostraba una y otra vez prefills largos (la barra de progreso azul) en turnos que deberían haber sido casi gratuitos. Un ejemplo: 43.2k tokens a unos 300 tok/s en un prompt de 80k. El decode de una solicitud concurrente quedaba congelado detrás ("last token 46 s ago").
- El diagnóstico del propio servidor. El log de la caché de prefijo para esa solicitud dice:
prefix cache restore: cached=36864 suffix=43171 re-prefills 43171 of 80035 tokens; closest stored sequence shares the first 37735
La misma sesión volvió a divergir más tarde en 37,739 tokens. Una divergencia en el mismo lugar cada pocos turnos apunta a algo que reescribe el prompt en una posición fija, no a un desalojo. - Descartar la caché del servidor. Se contrastó el desalojo del SSD con las pérdidas: la caché guarda unas 8 horas de historial, y ninguna de las pérdidas grandes coincidía con desalojos. Las recargas del modelo ocurrieron con horas de diferencia. La cuadrícula de bloques explica como máximo 2,047 tokens por evento.
- La diferencia del lado del cliente. Comparar los payloads registrados de dos solicitudes consecutivas de esa sesión mostró la primera diferencia dentro del mensaje de sistema, en el carácter 70,384 de unos 104,700. Eso equivale aproximadamente al token 37.7k, lo que coincide con el punto de divergencia del servidor:
…pending: install dependencies and verify loading. + (stale: 2 turns without an update — bring the list up to date now) </addendum>
- Atribución entre sesiones. Hacer lo mismo en todas las sesiones registradas atribuyó unos 945k tokens con re-prefill en las sesiones principales y unos 107k en los subagentes a cambios en el prompt de sistema o en la sección de herramientas. El apéndice de todo fue el factor principal.
- Confirmación. Tras la corrección, el prompt de sistema de la misma sesión dejó de llevar el estado de todo a las 00:26:37. Cada turno posterior fue de solo agregado, salvo una ruptura de caché esperada en el cambio.
La corrección: mantener estable el prompt de sistema
El harness gentle-shell agregaba un bloque de todo al prompt de sistema en cada prompt del usuario. Cuando la lista quedaba desactualizada, agregaba un contador, (stale: N turns without an update …), que cambiaba en cada prompt. El bloque estaba alrededor del token 30–35k de un prompt de sistema de ~41k, seguido de otras secciones. En las sesiones padre afectadas, cada incremento o edición de todo rompía la reutilización del resto del prompt de sistema y de la conversación posterior. Esto no se aplica a todos los subagentes: en las versiones de upstream examinadas en las nuevas pruebas, los hijos y trabajos en segundo plano con GENTLE_PI_AGENTS_CHILD=1 no cargan el hook de todo. Por lo tanto, no todos los cambios del prompt de sistema o de la sección de herramientas registrados en tráfico de hijos deben atribuirse a este hook. Algunos turnos individuales perdieron así 70k–110k tokens de caché.
Lo corregimos en el harness. El prompt de sistema es ahora estable byte a byte dentro de una sesión. El estado de todo viaja como un mensaje de conversación persistido, enviado solo cuando la lista cambia realmente. El recordatorio de obsolescencia se activa una sola vez, sin contador. Tras una compactación, el estado se vuelve a inyectar una vez. La corrección está probada (45/45 pruebas de todo, con las fallas no relacionadas reproduciéndose sobre una base limpia), se usó a diario y está propuesta a upstream en gentle-shell #1367. La validación siguiente se ejecutó sobre la rama de la corrección fix/todo-prompt-cache-1367 en 955523df (2026-10-05, gentle-shell 4.0.0), no sobre upstream main.
- Solicitudes registradas que llevaban una mutación del prompt de sistema: 21 de 61 antes, 0 de 90 después.
- Turnos consecutivos que fueron estrictamente de solo agregado tras la corrección: 31 de 31.
- Re-prefills del lado del servidor por encima de 20k tokens: pasaron de 1–2 por hora a 0.
Nuevas pruebas sobre upstream gentle-shell main
Volvimos a comprobar upstream gentle-shell main en aa2c0389 durante la ejecución en producción real y publicamos los resultados en #1367 el 2026-10-07. Ocho subagentes enviaron 344 solicitudes con un prompt de sistema por sesión; 336 de 336 pares consecutivos fueron de solo agregado, y los hits de la caché de prefijo por sesión fueron de 95.5–98.4%. El bloque de todo no apareció en ningún prompt de sistema, y el problema no se repitió. Para mayor precisión: aa2c0389 todavía agrega el bloque de todo en before_agent_start, y su disparador (tareas pendientes abiertas más un prompt de usuario nuevo) no se produjo en esta ejecución. El issue sigue abierto para que su autor y los mantenedores decidan.
Nueva prueba el 2026-10-10: el problema sigue presente en upstream main, y hubo que aplicar la corrección. Volvimos a probar gentle-shell main en 43541a0d con una prueba que ejercita los hooks públicos de la extensión de todo y con una sesión de pi en vivo contra oMLX. Upstream main todavía agrega ## Todo list y su contador de obsolescencia al prompt de sistema en before_agent_start (extensions/gentle-todo.ts). En una sesión con tareas pendientes abiertas, cada prompt de usuario nuevo cambiaba el prompt de sistema: aparecía el bloque y luego el contador pasaba de stale: 2 a 3 y 4. En vivo, la sesión produjo 4 mensajes de sistema, 3 de ellos actualizaciones "Updated system prompt section" que llevaban el bloque de todo. Reanudar una sesión también agregó un bloque de todo (+515 caracteres) que el prompt de sistema inicial nunca tuvo. El cambio reciente del recordatorio (#1924) agrega un mensaje de recordatorio local a la solicitud, que es compatible con la caché, pero el bloque en el prompt de sistema permanece.
El disparador es la sesión principal (padre). Los subagentes y los trabajos en segundo plano de gentle-shell no cargan el hook de todo, por lo que la ventana anterior, solo con subagentes, no mostró cambios. Cada actualización se envía como un mensaje de sistema, y la plantilla de chat de Qwen acepta un mensaje de sistema solo al principio, por lo que se pliega en la cabeza del sistema justo después del prompt base (unos 4,050 tokens). Todo lo que viene después de ese punto debe volver a pasar por prefill. El log de oMLX de la ejecución en vivo lo muestra:
| Solicitud | Upstream main 43541a0d: tokens en caché / de prompt | Con la corrección |
|---|---|---|
| 1 (en frío) | 0 / 4,090 | 2,048 / 4,091 |
| 2 (resultado de herramienta) | 4,085 / 4,258 | 4,086 / 4,255 |
| 3 (aparece el bloque de todo) | 2,048 / 4,351 | 4,086 / 4,342 |
4 (stale: 2) | 4,096 / 4,394 | 4,337 / 4,507 |
5 (stale: 3) | 4,096 / 4,418 | 4,502 / 4,531 |
La sesión era corta, por lo que aquí cada ruptura costó solo unos cientos de tokens. En una sesión de 100k tokens, la misma ruptura vuelve a hacer prefill de casi toda la conversación en cada prompt del usuario mientras haya tareas pendientes abiertas.
La lista de tareas pendientes tiene que seguir cambiando; la cuestión es dónde va el cambio. La corrección no congela el estado de todo. Agrega las actualizaciones de todo al final de la conversación, como mensajes ocultos enviados solo cuando la lista cambia realmente, en lugar de reescribir el prompt de sistema, de modo que se preserva el prefijo en caché. El recordatorio de obsolescencia se envía una sola vez, sin contador. Rebasamos la corrección sobre 43541a0d (conserva el recordatorio de #1924). Con ella, cada solicitud reutilizó su prefijo completo salvo el re-renderizado esperado de la respuesta anterior, y las 63 pruebas de todo pasan. También superó una revisión independiente sin hallazgos bloqueantes. La nueva prueba, la tabla de caché y la corrección rebasada (rama fix/todo-prompt-cache-1367) se publicaron en #1367 el 2026-10-10, corrigiendo el comentario del 2026-10-07 mencionado arriba.
Estado al 2026-10-11. Upstream main en d42264b4 todavía agrega el bloque de todo al prompt de sistema en before_agent_start (appendSystemPromptOnce en extensions/gentle-todo.ts). La rama de la corrección es un solo commit sobre 43541a0d. El mismo parche se aplica sin cambios sobre 563fede6, donde se ejecuta en el uso diario. Se rebasará sobre el main actual antes de abrir un pull request.
Subagentes y trabajos en segundo plano
En upstream main, el disparador es la sesión padre: los subagentes y los trabajos en segundo plano se ejecutan con GENTLE_PI_AGENTS_CHILD=1 y no cargan el hook de todo. Para comprobar todo el harness con la corrección, ejecutamos una build de gentle-shell que lleva el mismo parche (git patch-id idéntico al de 4b929257, aplicado sobre upstream 563fede6) en modo RPC de pi contra el servidor en ejecución. Una sola sesión padre ejecutó una lista de todo, un subagente en primer plano, un subagente en segundo plano y su despertar, un trabajo en segundo plano y su aviso, y dos prompts de seguimiento.
- El padre conservó un solo mensaje de sistema, el del inicio de la sesión. Las instantáneas de todo (4) y los avisos de subagentes y de trabajos viajaron como mensajes de conversación personalizados.
- Cada sesión hija conservó un solo mensaje de sistema.
- De 18 solicitudes, 11 reutilizaron el prefijo en caché completo y 2 fueron los arranques en frío del padre y del primer hijo. El segundo hijo arrancó desde un límite de bloque de 2048 tokens de la caché del primer hijo, como se esperaba. Las otras 4 fueron solicitudes del padre que divergieron en un límite de bloque; ninguna fue causada por un cambio del prompt de sistema.
Avisos del harness y conservación del razonamiento
Esas 4 divergencias del padre ocurrieron justo después de avisos del harness: una actualización de todo, un resultado o un despertar de subagente, un aviso de trabajo. pi los envía como mensajes con rol de usuario, por lo que la plantilla de Qwen trata cada uno como una nueva consulta del usuario. Con preserve_thinking: false, la plantilla elimina entonces el razonamiento del segmento anterior, el prompt se acorta y el prefill se reinicia en el primer mensaje del asistente de ese segmento. Del lado del servidor, preserve_thinking: true elimina la pérdida: en el mismo escenario, 11 de 11 solicitudes del padre reutilizaron su prefijo completo, y los tokens sin caché bajaron de 6,208 a 1,803 (medición). Esto es útil para los autores de harness. Un aviso que llega como mensaje con rol de usuario es un límite de caché para las plantillas que eliminan el razonamiento en la última consulta del usuario, por lo que importa con qué frecuencia se envían los avisos.
Otros hallazgos del harness
Ejecutar el harness y el servidor juntos durante la ejecución en producción real (fork.5) sacó a la luz más cambios de prompt que ninguno de los dos lados muestra por sí solo. Pertenecen a gentle-shell, no a oMLX. Los dos primeros ya están planteados en #1367: la directiva YOLO como pregunta de seguimiento (relacionada con #1029, el estado volátil de RDD en el prompt de sistema), y la alternancia del anexo de memoria en el comentario del 2026-10-07. Los dos últimos todavía no están reportados. En aa2c0389 el bloque de todo todavía se inyecta en el prompt de sistema en before_agent_start; no se activó en esta ventana porque no llegó ningún prompt de usuario nuevo mientras había tareas pendientes abiertas.
- Activar el modo YOLO de gentle-shell inyecta su directiva en el prompt de sistema. En la sesión orquestadora (2026-10-06 22:53, antes de esta ventana) eso costó un re-prefill de 34,379 tokens: 111.9 s de prefill, frente a alrededor de 1.6 s del siguiente turno con hit de caché.
- Un apéndice de memoria de Engram se agregó y se quitó del prompt de sistema de la segunda sesión dos veces, y se alternó una vez en el del orquestador. Cada alternancia invalida la caché desde ese punto en adelante.
- El rol
gentle-ai-workercarecía de la herramienta nativaagent_browser, mientras que la guardia del harness la exige para el trabajo con navegador. - Los workers de squad lanzados por una extensión de terceros no usaron el modelo local fijado en el perfil. Arrancaron con un modelo en la nube y fallaron por falta de credenciales.
Por qué importa para proyectos de harness como gentle-shell
- La caché de prefijo y de KV permite que tanto los servidores locales (oMLX, llama.cpp, vLLM, MLX) como las API alojadas mantengan asequibles las sesiones largas de agentes. Un harness que reescribe un prefijo anterior puede aumentar la entrada no cacheada y la latencia. El impacto depende de las reglas de caché, los precios y la carga de cada backend.
- Las pruebas habituales del comportamiento de todo no detectaron esta interacción. Una invariante de estabilidad del prompt puede detectar la reescritura sin servidor; las mediciones de extremo a extremo son necesarias para cuantificar su impacto en caché y latencia.
- El mismo ciclo separó las causas con claridad: una estaba en el harness (corregida en una rama de gentle-shell; todavía presente en upstream
mainel 2026-10-10 y el 2026-10-11), dos estaban en el servidor (A1, A2) y el resto eran esperadas (compactación, turnos reales del usuario).
Una propuesta para gentle-shell
- Una prueba de invariante de estabilidad del prompt. Ejecutar N turnos a través del harness con fakes y comprobar que cada solicitud renderizada es una extensión estricta, por prefijo, de la anterior, salvo en los límites declarados (un mensaje real del usuario, una compactación). La corrección de #1367 ya incluye la versión acotada de esto para la herramienta de todo. Se podría generalizar a cada extensión que toca el prompt de sistema (estado de RDD, directiva YOLO, memoria).
- Una comprobación opcional de extremo a extremo contra un servidor local. Ejecutar una sesión guionada contra un servidor local compatible con OpenAI que informe los tokens en caché (oMLX lo hace, mediante
usage.prompt_tokens_details.cached_tokens). Señalar cualquier turno cuyo conteo de tokens en caché caiga por debajo de la longitud del prompt anterior. - Un modo grabador. Un grabador de payloads opcional hace reproducible este tipo de diagnóstico para los usuarios que informan "se puso lento".
Hallazgos abiertos
La equidad del decode no cubre los prompts multimodales
Cualquier solicitud con una imagen en cualquier parte de su historial lleva vlm_inputs_embeds y hace prefill en el bucle externo síncrono. Eso ocurre incluso cuando todas las imágenes ya están dentro del prefijo en caché restaurado y el sufijo no cacheado es texto puro. Ambas ramas por chunks requieren vlm_embeds is None, por lo que el decode concurrente se congela durante todo el prefill: se observaron 25–46 s, y 155 s en prefills en frío de 43k. La corrección ya está construida como parte del ciclo de pausa y reanudación (C6, opt-in OMLX_MEDIA_TEXT_SUFFIX_CHUNKED). Envía un sufijo solo de texto que sigue a una última imagen en caché por la ruta por chunks, tras comprobar que las posiciones del sufijo son t + rope_delta en cada plano mRoPE. Las pruebas con un modelo diminuto muestran una salida idéntica con la ruta activada y desactivada. Su efecto sobre los congelamientos del decode todavía no se ha medido en hardware real.
Atención del decode TurboQuant en profundidad
Una regresión sobre 975 solicitudes de una sola fila da unos 250–295 ns por token de contexto por ciclo de MTP, frente a aproximadamente 32 ns para una lectura de KV pura con el tamaño de KV de este modelo. La intersección es de 70–75 ms frente a un roofline de pesos cercano a 30 ms. Son estimaciones a partir de logs. Todavía no hemos hecho perfilado a nivel de kernel.
La verificación de MTP de profundidad 1 usa el qmm estándar
La verificación de profundidad 1 (M=2) se ejecuta en el 97.5% de los ciclos, y ninguna ruta de verify-qmm acepta M=2: las rutas cubren 3–6, 4–8 y 7–24. A partir de esto estimamos 10–20 ms por ciclo. MTP sigue siendo positivo en neto en todos los tramos de contexto (1.3–1.45×).
Cómo se validó
Estado de la evidencia. Las cifras de benchmarks, los conteos de tráfico, los resultados de las suites de pruebas y la finalización del piloto incluidos en este informe son reportados por el autor, no una reproducción independiente. Una auditoría de referencias comprobó los destinos de los enlaces publicados, las anclas internas, el código y las pruebas disponibles, la aritmética y la relación entre los commits medidos y publicados. Que un enlace esté disponible o exista el código de una prueba no demuestra que una ejecución haya pasado. Esta página no enlaza actualmente un archivo inmutable con los logs primarios, los resultados y la configuración exacta de cada medición, ni una auditoría independiente del contenido de la wiki. Las salidas no idénticas de PR1 y el fallback de PR2 no ejercitado en hardware siguen siendo limitaciones; los enlaces a ramas pueden cambiar, por lo que deben usarse los hashes de commits como evidencia fija.
- Pruebas primero. Cada comportamiento vino con una prueba de regresión que fallaba antes del cambio y pasaba después. La suite completa del fork tiene 17,063 aprobadas y 439 omitidas, más tres fallas solo del entorno (un modelo de recurso local sin plantilla de chat).
- Revisiones independientes. Cada slice se revisó por separado, con cuatro lentes de revisión en los slices de alto riesgo. Los hallazgos se incorporaron como pequeños commits de seguimiento, por ejemplo la degradación controlada cuando un tokenizer VLM no tiene
encode. - Benchmarks contra upstream. El harness de premisas ejecutó servidores de upstream y del fork de forma aislada, y comprobó que la app en vivo estuviera inactiva. Señaló las ejecuciones contaminadas, las descartó y las volvió a ejecutar.
- Reproducción de tráfico registrado. Un grabador del lado del cliente captura los payloads exactos del proveedor, y un reproductor los reenvía en orden a un servidor aislado para medir la reutilización por build.
Código fuente y referencias a commits
Cada commit y rama que se nombra en este informe enlaza al fork en GitHub, e-Evolution/omlx (las referencias de gentle-shell enlazan a e-Evolution/gentle-shell). Los commits de upstream se pueden ver desde el fork con los mismos hashes. Las ramas de los PR son fix/turboquant-padded-batched-decode (PR1) y fix/turboquant-restored-prefix-prefill (PR2).
Las ramas del fork se publicaron el 2026-10-11. Antes de publicarlas se reemplazaron las rutas locales de la máquina en los valores por defecto de los benchmarks, en un fixture de test y en un docstring (directorio de salida, nombre del volumen de caché). La comparación de los 17 pares de árboles Git medidos y publicados no encontró diferencias bajo omlx/. Las diferencias se limitan a rutas por defecto de los benchmarks, documentación y ejemplos de apoyo, el fixture de rutas y la referencia del docstring de C7. Por lo tanto, los árboles completos no son idénticos byte a byte; la reproducción requiere configurar explícitamente las rutas locales. Esto corrobora la continuidad del código fuente, no las mediciones históricas. Los hashes de los commits cambiaron. El informe muestra los hashes publicados; la tabla relaciona cada uno con el hash que se midió, que es el que aparece en los logs. Al pasar el cursor sobre un hash publicado se ve la misma relación. El commit anterior de PR1, 61bd8a06, reemplazado por la rama rebasada, se conserva con el tag report/61bd8a06.
| Commit publicado | Medido como |
|---|---|
65899460 | 082a870c |
5dd56ca5 | 0b932a39 |
19aa28ef | 25685e36 |
159673d5 | 301bde14 |
4aa66150 | 4e81e443 |
f3c11356 | 85c1f902 |
95dd47e8 | 8aa178c5 |
04bb7899 | aa06f32e |
c31820cb | b000a28f |
5a881160 | c36b92fb |
78291feb | c9694977 |
3b72cde7 | d34413ec |
6fbb3274 | e24198f5 |
47c0c70b | e80a1842 |
e5353d70 | f6c601b0 |
31cab33d | f7dd2bc8 |
e80eb1f7 | f92fbd7c |
main (b7c7929e) con un único conflicto en la documentación. La selección de CI de upstream (pytest tests/ -m "not slow and not integration", Python 3.11) da 16,941 aprobadas en main y 17,635 aprobadas en el fork rebasado, con las mismas 3 fallas solo del entorno en ambos. Los benchmarks ejecutaron cada build sola con el mismo runtime, que fija las dependencias de la app v0.7.0 en lugar de las de main. La excepción es la revalidación de P3 de PR1 sobre 25aebb3e, que ejecutó ambos árboles desde un mismo venv con las versiones fijadas de main (mlx 0.32.3, mlx-vlm 0.7.7) y sin kernels personalizados compilados. La rama de pausa y reanudación (47c0c70b) da 17,908 aprobadas con las mismas 3 fallas solo del entorno. Con xdist, como lo ejecuta CI, el fork rebasado sobre c0b1056b (4aa66150) da 18,072 aprobadas frente a 16,921 de main, con las mismas 3 fallas solo del entorno más una prueba de clúster sensible a la carga que pasa por sí sola; la revalidación sobre c0b1056b ejecutó ambos árboles desde un mismo venv con las versiones fijadas de main. Las ramas sobre 013bc3f3 se enumeran en Contribución propuesta.Contribución propuesta
Para oMLX
Nos gustaría enviarlos a upstream como pull requests pequeños, cada uno con sus propias pruebas de regresión y cifras de antes y después en el mismo hardware, siguiendo docs/CONTRIBUTING.md. Upstream dirige las propuestas de funcionalidades a GitHub Discussions > Ideas, y los issues en blanco están deshabilitados, por lo que la propuesta se publicó como la discusión de Ideas #4440 (2026-10-11). Las dos correcciones de TurboQuant, PR1 y PR2, son correcciones de bugs de menos de 1,000 líneas modificadas, por lo que se abrieron directamente el mismo día: PR1 como #4441 y PR2 como #4442. El 2026-10-12 el mantenedor integró PR2 como #4442, con un ajuste posterior del dtype denso cuando ninguna capa KV es simple y el embedding está cuantizado, y cerró PR1 en favor de #4453, que extiende el kernel de #4408 para tomar el relleno a la izquierda de cada fila y nos incluye como coautores. El ciclo de pausa y reanudación supera las 1,000 líneas modificadas y agrega un subsistema, por lo que primero se presentó para recibir comentarios en #4440; el 2026-10-12 su stack se abrió como doce pull requests, de #4478 (listo para revisión) a #4489 (borradores). Cada slice indica su dependencia y su orden de merge. El orden es una propuesta y lo adaptaremos a sus preferencias.
Cada rama de abajo está rebasada sobre upstream main 013bc3f3 (2026-10-10, MLX 0.32.3, mlx-vlm 0.7.7). Sobre esa base, el comando de CI (-m "not slow and not integration" -n 3 --dist loadgroup, Python 3.11, un venv con las versiones fijadas de main) da 16,928 aprobadas en main, 16,956 con PR1 (+28 pruebas) y 16,944 con PR2 (+16), con las mismas 3 fallas solo del entorno (test_gemma4_rendering.py). Antes de abrirlos, PR1 y PR2 también se comprobaron contra el main más reciente e02ae514 (2026-10-11): ambos se integran sin conflictos, y el comando de CI da 16,938 aprobadas en main, 16,966 con PR1 y 16,954 con PR2, con las mismas 3 fallas. Todo el stack se reconstruyó sobre la misma base a partir del fork, cada slice de P-a a S9, con la condición memory_scheduling (G) antes de S4; cada slice se aplica sobre el anterior, y sus pruebas específicas y el comando de CI pasan en su punta (salvo dos pruebas sensibles a la carga que pasan por sí solas). Para los pull requests, el stack se rebasó sobre main f3ad34d2 (2026-10-12) y, siguiendo #4396 de upstream, sus dos interruptores solo por variable de entorno pasaron a ser configuraciones (memory.abort_single_victim, scheduler.media_text_suffix_chunked) y se quitó el control de prueba para pausas forzadas. El comando de CI da 17,119 aprobadas en ese main y de 17,175 (P-a) a 17,580 (S9) en las puntas de los slices, con las mismas 3 fallas; una prueba de cluster sensible a la carga también falló en S2 y S4 y pasa por sí sola 3/3 en ambos.
Alrededor de dos tercios de las líneas modificadas del ciclo son pruebas. Para S1–S9 y G, los conteos por slice de la tabla siguiente suman 8,632 líneas de prueba de 12,961 (67%); con los prerrequisitos P-a y P-b, 11,658 de 17,652 (66%). El scheduler es concurrente (el hilo de MLX, el enforcer de memoria y el escritor del SSD se intercalan), por lo que cada caso límite encontrado con el modelo real tiene una prueba de regresión sobre el scheduler y el enforcer reales en lugar de sobre fakes. Por eso se divide en pull requests encadenados, cada uno con sus propias pruebas.
| # | Pull request | Líneas (pruebas) | Por defecto | Estado |
|---|---|---|---|---|
| PR1 | fix(turboquant): atender el slice propio de cada fila con relleno a la izquierda en el decode por lotes y en la verificación de MTP. Sobre un main más reciente es principalmente una corrección de rendimiento para el decode por lotes con relleno, con una señal de corrección. Dos commits: atención por fila para el decode, y la ruta por fila para la verificación con relleno (con la máscara propia conservada a través del slice por capa de mlx-vlm). Un tercer commit, un rollback exacto de la cola de verificación, se descartó: upstream #3127 corrigió el mismo rollback con pérdidas en main. | 816 (544): +815 / −1, 28 pruebas | siempre (corrección de bug) | Cerrado en favor de #4453 (integrado el 2026-10-12, en coautoría). Rama fix/turboquant-padded-batched-decode sobre main 013bc3f3: 16,956 aprobadas frente a 16,928 de main, las mismas 3 fallas solo del entorno. Modelo real, P3 sobre 25aebb3e con las dependencias fijadas de main (una ejecución por build; los archivos de TurboQuant no han cambiado en upstream desde entonces): sin abortos ni degeneración en ninguno de los dos árboles; decode por lotes 5–12× más rápido por fila (1.5 → 8.3 tok/s con MTP activado, 0.66 → 8.1 con MTP desactivado), fase B=3 345 → 66 s y 780 → 65 s, pico 11–15 GB menor; con MTP desactivado, las filas por lotes se mantienen más tiempo en la salida de B=1 (alrededor del token 8–10 frente a 1–2). La atención de decode por capa se ejecuta 9–28× más rápido (microbenchmark). Histórico, antes del rebase (main b7c7929e sobre el runtime de la app v0.7.0, PR1 con el commit de rollback descartado): el lote B=3 abortó en main (6 abortos, pico de 120 GB), y PR1 dio 4-gram 1.00 / 0.99 / 0.99 y 0 abortos. |
| PR2 | fix(scheduler): hacer prefill de sufijos largos sobre una copia densa de un prefijo TurboQuant restaurado (detalles). Dos commits: el paso denso, y la alternativa del dtype de embeddings cuando todas las capas KV son TurboQuant. | 547 (360), 16 pruebas | siempre (corrección de bug) | Integrado como #4442 (2026-10-12), independiente de PR1 y del stack. Rama fix/turboquant-restored-prefix-prefill sobre main 013bc3f3: 16,944 aprobadas, las mismas 3 fallas solo del entorno. Sufijo restaurado 2.41× → 0.92× del de en frío en main; diferencias de calidad solo en casi empates, como sin la corrección; reserva +2.23 GB. |
| P-a | Commit de progreso de prefill, incluido el ejecutor de commits en su propio módulo (prerrequisito) | 2,133 (1,293) | desactivado | Abierto desde el 2026-10-12, rebasado sobre main f3ad34d2: P-a #4478, P-b #4479, S1 #4480, S2 #4481, S3 #4482, S5 #4483, G #4484, S4 #4485, S6 #4486, S7 #4487, S8 #4488, S9 #4489. P-a está listo para revisión; el resto son borradores en orden de merge. |
| P-b | Park de prefill en lugar de aborto, más el commit de progreso de medios y el watchdog de apagado (prerrequisito) | 2,558 (1,733) | desactivado | |
| S1 | Alternativa de víctima única y primitivas de caché de cadena exacta | 1,284 (687) | desactivado | |
| S2 | Pausa y reanudación del decode (primitiva de pausa, ruta rápida de reanudación, continuidad de la parada y del parser, cableado del enforcer, transferencia de MTP) | 2,735 (1,912) | desactivado | |
| S3 | Prioridad de reanudación consciente de la memoria (reanudar cuando la necesidad cabe; retención FIFO como reserva), adaptada a #3482 | 532 (348) | desactivado | |
| S5 | Sufijo de texto con medios en caché por la ruta por chunks (hace estacionables los prompts con imágenes), con la prueba de posiciones también en la ruta de prefill empaquetado | 997 (758) | desactivado | |
| G | Primero la condición de planificación de memoria: la configuración memory_scheduling (desactivada sin un techo explícito), su resolución y propagación por el enforcer, la configuración de administración y el estado en vivo; todavía nada la lee | 696 (464) | agrega la configuración | |
| S4 | Condición de nunca cabe, espera de cabe más tarde y API de capacidad (endpoint, encabezado, esquema del 400; dimensionamiento de los pesos VLM), adaptada a #3482 y #4252; los bloques confirmados propios de una solicitud estacionada se informan como prefill reanudado (#4383) | 2,550 (1,612) | planificación de memoria | |
| S6 | Reanudación bajo el umbral duro sin thrashing, ejecución en solitario, presupuesto de pausas de decode | 922 (537) | desactivado | |
| S7 | Enforcer: respetar los prefills del bucle externo, sin abort-all sin una víctima en vivo, pausa primero en emergencia | 1,012 (736) | desactivado / planificación de memoria | |
| S8 | Reloj de progreso de admisión, admisión en solitario, una cabeza solitaria que cabe sola vuelve a medirse ante una lectura obsoleta (el control de prueba para pausas forzadas de esta versión se quitó del pull request abierto) | 1,743 (1,238) | planificación de memoria / desactivado | |
| S9 | Seguimientos: la caché caliente eleva la línea de nunca cabe, y el reloj de bloqueo a través de las retenciones del park | 490 (340) | planificación de memoria / park |
Los conteos de líneas son adiciones más eliminaciones de cada slice frente al slice anterior, sobre main 013bc3f3. "Planificación de memoria" significa desactivada a menos que se fije un techo de memoria explícito o memory_scheduling=on. G agrega esa condición antes de que cualquier ruta la lea, y S4, S7 y S8 la consultan desde su primer commit, por lo que, con todas las opciones desactivadas, cada prefijo del stack se comporta como main en estas rutas; cada slice condicionado tiene pruebas que lo ejercitan con la condición desactivada y esperan el comportamiento de main. La evidencia de C7 sobre c0b1056b (revalidación) cubre P-b, S1–S3, G y S6–S9, la ejecución de identidad con MTP desactivado cubre S2, la ejecución de nunca cabe cubre S4, y S5 solo tiene pruebas unitarias y de identidad con un modelo diminuto. La reconstrucción hizo cherry-pick de los commits del fork en el orden de los slices con resoluciones mecánicas; la única prueba que usaba el archivo de valores por defecto del fork (la prueba de estado de administración de G) ahora fija el modo en settings.json, y una corrección de fixture de prueba para upstream #4312 pasó a S6, cuyas pruebas la necesitan. Estos commits del fork no están en ningún pull request: los commits de empaquetado (paquete de valores por defecto, eliminación de la condición, --allow-pin-mismatch), la capa del archivo de valores por defecto y el lector compartido de variables de entorno on/off que la acompaña, el harness benchmarks/unattended (enlazado solo como reproducción) y la prueba del lado del fork que combina PR2 con el park de prefill (llegará cuando ambos estén en main).
Propuestas posteriores y separadas para oMLX
| Pull request | Tamaño | Notas |
|---|---|---|
| perf(turboquant): kernel por lotes ragged de un solo lanzamiento | — | Seguimiento de PR1. Tras PR1, el decode por lotes se ejecuta a unos 8 tok/s por fila sobre un main más reciente (unos 7 en el runtime v0.7.0), frente a 17–40 tok/s con B=1; el kernel ragged del fork es el siguiente paso de velocidad. El #4453 de upstream ahora ejecuta un lote con relleno en un solo lanzamiento de kernel por capa (62–65 tok/s por fila con tres solicitudes concurrentes según el mantenedor, Qwen3.6-35B-A3B con TQ4), por lo que esta propuesta primero se volvería a medir contra él. |
feat(memory): memory.mlx_cache_limit_gb | ~170 | Acota la caché de buffers de Metal. Incluye entradas de i18n. |
| feat(cache): resumen de desalojos y resultado de caché por solicitud; log de desglose de TTFT | 2 PR | Solo observabilidad. |
feat(api): inline_note como tercera política de sistema intermedio | ~700 | Opcional (opt-in), junto a strict y user_note_safe. |
| feat(api): retención determinista del razonamiento con una lista configurable de marcadores que no son consultas | ~1.6k | Primero la generalizaremos más allá del texto de marcador de un solo cliente y revisaremos las plantillas una por una. |
| feat(scheduler): condición de contexto del decode por lotes con backfill y envejecimiento | — | En espera. La app del fork la eliminó en fork.5 (159673d5) porque las pausas de decode ahora resuelven el caso de memoria. |
Reportes de bugs de oMLX que presentaríamos (o comentaríamos)
- El decode por lotes de TurboQuant con relleno a la izquierda atiende sobre columnas de relleno (proporción de normas de la fila corta de 0.32 en pruebas sintéticas) y es lento (alrededor de 1.5 tok/s por fila con B=3 sobre 25aebb3e), con un caso de reproducción; PR1 es la corrección. La salida degenerada y los abortos se observaron solo sobre b7c7929e con el runtime de la app v0.7.0. Relacionados: #4110, #3767, #2784.
- Prefill de sufijo lento tras una restauración desde la caché SSD paginada de un prefijo TurboQuant (2.4× un prefill en frío de las mismas posiciones sobre c0b1056b); PR2 es la corrección. Relacionados: #1842, #3933.
- Equidad del decode omitida para prompts multimodales. Relacionado: #3209.
- La salida de una sola solicitud con Lightning MTP no es reproducible bit a bit entre ejecuciones. Divergió en 1 de 3 ejecuciones en upstream.
- Contexto de A1 y A2 agregado a los hilos existentes (#3608, #1966, #3378, #900, #1003) en lugar de abrir issues nuevos.
Antes de cada PR de oMLX vamos a
- rebasar sobre el
mainactual (013bc3f3 en el último rebase, 2026-10-10: MLX 0.32.3, mlx-vlm 0.7.7); - ejecutar el comando de CI:
python -m pytest tests/ -m "not slow and not integration" -n 3 --dist loadgroupcon Python 3.11, más Black y Ruff a 88 columnas; - volver a ejecutar el benchmark correspondiente contra
mainindicando el número de ejecuciones.
Para gentle-shell
gentle-shell exige un issue con status:approved antes de un pull request. #1367 está abierto con status:needs-review, y pedimos su aprobación para que la corrección pueda entrar como pull request, o integrarse en otro cambio si los mantenedores lo prefieren.
| Cambio | Líneas (pruebas) | Estado |
|---|---|---|
| fix(todo): entregar el estado de todo como mensajes para mantener el prompt estable para la caché. El prompt de sistema se mantiene estable byte a byte dentro de una sesión; las actualizaciones de todo son mensajes de conversación ocultos enviados solo cuando la lista cambia; el recordatorio de obsolescencia se activa una sola vez; el estado se vuelve a inyectar una vez tras la compactación. Conserva el recordatorio de #1924 y los estados blocked/dropped. | 388 (296) | Lista. Rama fix/todo-prompt-cache-1367, un solo commit (4b929257) sobre 43541a0d: pasan 63 pruebas de todo, el typecheck, check:runtime-modules y verify-package-files; las fallas restantes de pnpm test también fallan sobre una base limpia. Espera status:approved; antes se rebasará sobre el main actual. |
| test: un invariante de estabilidad del prompt para cada extensión que toca el prompt de sistema (estado de RDD, directiva YOLO, memoria) | — | Propuesta (arriba) |
| Una comprobación opcional de extremo a extremo contra un servidor local que informe los tokens en caché, y un grabador de payloads opcional | — | Propuesta |
Los otros hallazgos del harness: las alternancias de la directiva YOLO y del apéndice de memoria en el prompt de sistema, el rol worker sin agent_browser, los workers de squad que ignoran el modelo fijado | — | Los dos primeros, planteados en #1367 (relacionado: #1029); los otros dos todavía no están reportados, un issue por cada uno |

