Contenido ↑
e-EvolutionBuilt with Gentle-AI
Informe de campo · servidor oMLX + harness gentle-shell · Apple M5 Max 128 GB

Agentes desatendidos en oMLX y gentle-shell

oMLX 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.

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_gb para 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 main 25aebb3e); 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_gb acota 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
Lo que pedimos a continuación. oMLX: PR2 se integró como #4442 y la corrección de PR1 entró como #4453 (ambos el 2026-10-12). A continuación, una revisión del stack de pausa y reanudación, abierto en orden de merge de #4478 (listo para revisión) a #4489 (borradores) y anunciado en la discusión de Ideas #4440. gentle-shell: 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.

Autor: victor.perez@e-evolution.comModelo: Qwen3.8-27B-oQ4e-mtp (4 bits, Lightning MTP, KV TurboQuant de 4 bits)Cliente: agente de programación pi + harness gentle-shell 4.0.0 (paquete gentle-pi; commits por prueba más abajo) + subagentesoMLX: v0.7.0 → v0.7.1.dev1 (upstream main 013bc3f3); app del fork 0.7.1.dev1-fork.7Período: 2026-09-30 → 2026-10-11Código fuente: github.com/e-Evolution/omlx (mapa de commits)

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.

PreguntaLínea base y runtimeEvidenciaLímite
¿Se completó el piloto de documentación de principio a fin?Resultado del proyecto informado por el autorEl 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.0P5: 3/3 lotes del fork, 0/3 de upstream. EvidenciaMide 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 fijadasB=3: 5–12× más rápido por fila; pico 11–15 GB menor. Una ejecución por build y configuración MTP. EvidenciaNo 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 fijadasRelación entre sufijo restaurado y frío: 2.41× → 0.92×. Una ejecución por configuración. EvidenciaUn 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 venvEl 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. EvidenciaPresió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.0333 solicitudes; 94.8% de los tokens de prompt desde caché; 0 abortos por memoria. EvidenciaVentana 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-shellMutaciones del prompt de sistema: 21/61 → 0/90 solicitudes; 31/31 pares de solo agregado. EvidenciaMuestras observacionales con cargas distintas; no es un benchmark controlado de latencia ni de costos de API alojadas.

De un vistazo

116 → 44 GBpico de memoria, 2×130k concurrentes (frente a upstream main en b7c7929e), 3× más rápido
3/3 vs 0/3lotes de agentes desatendidos terminados sin intervención humana (frente a v0.7.0)
97–99.7%de hit en la caché de prefijo en turnos que agregan una imagen nueva
~30 s/hde decode congelado por el prefill de medios con una sesión con imágenes tras las correcciones de caché (n=2). Antes de las correcciones se observaron ~1100 s/h con una carga más pesada y superpuesta, por lo que ambas cifras son indicativas, no una comparación controlada
9/9 vs 0/9solicitudes terminadas bajo presión de memoria forzada, 0 abortos con pausa y reanudación (frente a upstream main en b7c7929e)
5–12×más rápido el decode TurboQuant por lotes por fila con PR1 sobre upstream main 25aebb3e (B=3, prompts de 20k/45k/135k), pico 11–15 GB menor, una ejecución por build
2.41× → 0.92×prefill del sufijo tras un prefijo TurboQuant restaurado, frente a un prefill en frío de las mismas posiciones, sobre upstream main c0b1056b con PR2 (sufijo de 36k tokens tras un prefijo de 74k, una ejecución)
21/61 → 0/90solicitudes registradas que reescribían el prompt de sistema, antes y después de la corrección de todo de gentle-shell (gentle-shell)
~1.05Mtokens con re-prefill en ~1.5 días por cambios del prompt de sistema en el harness, ≈80–85 min de tiempo de GPU, antes de la corrección de gentle-shell
0 / 333abortos por memoria en 333 solicitudes reales de agentes con hasta 5 concurrentes, 94.8% de los tokens de prompt desde la caché (fork.5, log del servidor 02:00–04:10)
Bug del harness encontrado en el camino (gentle-shell). En las sesiones padre afectadas con tareas pendientes abiertas, la extensión de todo reescribe el prompt de sistema en cada prompt del usuario, con un contador de obsolescencia que cambia cada vez. En la configuración de oMLX probada, estas reescrituras rompen la reutilización del prefijo desde la región modificada en adelante. Otros backends también pueden perder reutilización según sus reglas de caché; aquí no se midieron los efectos en proveedores alojados. Sigue presente en upstream 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.
Bug de upstream encontrado en el camino (oMLX). En el decode por lotes con TurboQuant, las filas cortas con relleno a la izquierda atienden sobre columnas de relleno con ceros, y cada paso de decode con relleno pasa por una ruta lenta que descuantiza todo. En pruebas sintéticas, la norma de salida de la fila corta cae a 0.32× de la referencia. Con Lightning MTP, los forwards de verificación con relleno tampoco usan la ruta por fila, y el rollback de la cola de verificación volvía a cuantizar el lote completo en cada ciclo; upstream ya corrigió el rollback en #3127. Una corrección probada para los otros dos defectos está lista como nuestro primer pull request (PR1).
  • Sobre un main má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 de main se 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 (main b7c7929e 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.

Un servidor local oMLX Fork, dos clientes: los subagentes de gentle-shell y el asistente de IA de IntelliJ envían solicitudes al mismo tiempo, y oMLX atiende 3–4 solicitudes activas en paralelo con Qwen3.8-27B (95.5% de eficiencia de caché). Velocidad 10×.

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

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.

PremisaUpstream v0.7.0ForkResultado
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 abortos4 abortos · pico 116 GB0 abortos · pico 44 GBPASS
P3 · Corrección del lote greedy (B=3 vs B=1)filas cortas degeneradassin degeneración; divergencias solo en casi empates (near-ties) de bf16PASS
P4 · La concurrencia rinde (3 solicitudes mixtas)538 s vs 246 s en serie · 6 abortos241 s vs 247 s en serie · 34.6 vs 29.7 tok/s agregadosPASS
P5 · Lote pi desatendido0/3 completados3/3 completadosPASS
P6 · Una sola solicitud de 110k, sin regresiónlínea baseprefill +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.

PremisamainFork rebasado sobre mainLectura
P1 · Reenvío tras cancelar a mitad del prefill0 en caché · 110,031 con re-prefill73,728 en caché · 36,303 con re-prefillPASS
P2 · 2×~130k concurrentes0 abortos · pico 116 GB · 1,427 s0 abortos · pico 44 GB · 461 sPASS 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 25aebb3eninguna fila degenerada; proporciones de 4-gram 1.00 / 1.00 / 0.99fork correcto divergencias en casi empates de bf16
P4 · La concurrencia rinde (3 solicitudes mixtas)248 s concurrente · 251 s en serie · 0 abortos227 s concurrente · 224 s en serie · 0 abortosdentro del ruido no se reclama ganancia frente a main
P6 · Una sola solicitud de 110kprefill 755 tok/s · decode 21.1 tok/s744 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

Decode por lotes para cinco solicitudes concurrentes sobre un único modelo Qwen3.8-27B (KV TurboQuant), eficiencia de caché del 95.5%. Velocidad 10×.

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.

BuildMTPAbortosPico4-gram distintos, B=3 (20k / 45k / 135k)Decode por lotes tok/s por filaFase B=3Tiempo total de ejecución
mainactivado0117 GB1.00 / 1.00 / 0.991.54 / 1.53 / 1.49345 s1015 s
PR1activado0102 GB1.00 / 1.00 / 0.988.35 / 8.83 / 7.8466 s682 s
maindesactivado0112 GB1.00 / 1.00 / 0.980.66 / 0.66 / 0.66780 s1492 s
PR1desactivado0101 GB1.00 / 0.99 / 0.988.17 / 8.15 / 8.0565 s782 s
Salvedades. Una ejecución por build, por lo que los tiempos totales y los picos son muestras únicas. El venv no tiene kernels personalizados compilados (aquí no hay toolchain de Metal para construirlos), por lo que el prefill y los tiempos totales de ejecución son más lentos que en la app distribuida, por igual para ambos árboles; la atención de TurboQuant usa kernels Metal JIT y no se ve afectada, y la comparación de decode B=3 se ejecuta en el mismo runtime para ambos. La identidad se juzga solo por el texto. No se sabe si los abortos y la degeneración anteriores desaparecieron por commits de upstream o por el runtime.

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.

EscenarioTechoForkmain c0b1056b
Decode de texto44 GB3/3 0 abortos; 2 parks de prefill, ambos reanudados0/3 abort-all a mitad del prefill tras 25 s
Decode con imágenes42 GB3/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ágenes44 GB3/3 0 abortos; 2 parks de prefill, ambos reanudados3/3 0 abortos
Nunca cabe (prompt de 60k, max_tokens 2,000,000)42 GB400 context_exceeds_capacity en 0.11 s, los 7 campos; endpoint de capacidad 200; encabezado 288,739200; endpoint 404; sin encabezado

Premisas, fork contra main c0b1056b, mismo venv:

PremisaForkmain c0b1056bLectura
P1 · Reenvío tras cancelar a mitad del prefill73,728 en caché · TTFT 198.9 s0 en caché · TTFT 192.2 scaché 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 concurrentes2/2 · 0 abortos · pico 51 GB · 564 s2/2 · 0 abortos · pico 116 GB · 870 sPASS pico menor, más rápido
P3 · Lote B=3, MTP activado13.0 / 12.2 / 10.9 tok/s por fila · pico 58 GB1.55 / 1.54 / 1.50 tok/s por fila · pico 117 GBsin 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/12289.3 s concurrente · 285.9 s en serie · 12/12paridad ambos limitados por el prefill sin kernels compilados
P6 · Una sola solicitud de 110k, decode22.77 tok/s22.61 tok/ssin 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.

EscenarioTechoFork + correcciónmain 013bc3f3Corrección en el log
Decode de texto44 GB3/3 0 abortos; 2 parks de prefill, ambos reanudados0/3 abort-all a mitad del prefill tras 22 sno se alcanzó: los parks se reanudaron desde 0 tokens confirmados
Decode con imágenes44 / 42 GB3/3 0 abortos en ambos; 1 y 2 pausas de decode, todas reanudadas0/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ágenes44 / 42 GB3/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 cabe42 GB400 en 0.11 s, los 7 campos; endpoint de capacidad 200; encabezado 288,900no se volvió a ejecutar-
Prefill empaquetado, MTP desactivado44 / 42 GB3/3 0 abortos; empaquetado, pero sin park en ninguno de los techos-no se alcanzó (sin restauración)
Runtime. Estas ejecuciones usaron el venv, que no tiene kernels personalizados compilados, por lo que las cifras absolutas de prefill son menores que en la app, por igual para ambos árboles. El fork empaquetado se ejecuta ahora sobre el runtime oficial de la app 0.7.1.dev1 (oMLX Fork 0.7.1.dev1-fork.7, el código fuente del fork sobre la app oficial como donante, mlx 0.32.3 y mlx-vlm 0.7.7), con los cinco kernels personalizados compilados cargando (5/5).

Defecto de upstream: prefill de sufijo lento tras un prefijo TurboQuant restaurado

Tres prompts haciendo prefill en paralelo mientras una cuarta solicitud sigue generando tokens. Velocidad 12×.

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

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ónSufijo restauradoEn frío, mismas posicionesProporciónReserva tras la restauración
main c0b1056b, MTP desactivado197.9 s82.2 s2.41×across 1 caches (+0.14GB)
main + corrección, MTP desactivado87.3 s94.7 s0.92×across 16 caches (+2.23GB)
Fork + corrección, configuración del fork, MTP activado84.8 s85.0 s1.00×across 16 caches (+2.23GB)
En hardware bajo presión (fork con la corrección, ejecuciones de presión forzada, más arriba): los prompts con imágenes que restauran un bloque TurboQuant, dos prefills de sufijo densos admitidos juntos y un park con reanudación a través del paso denso se completaron con 0 abortos. Un reenvío tras una cancelación tardó 80–82 s frente a 192–193 s en 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

  1. 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.
  2. 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.
  3. 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) sondeos ok consecutivos, 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).
Qué mostraba la evidencia antes de forzarlo. El mecanismo de park está cubierto por 32 funciones de prueba en 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.
Para probarlo de forma determinista construimos un benchmark de forzado.

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)
VarianteParksReanudaciónResultadoReenvío tras un aborto
Fork, texto (2 ejecuciones)1 por ejecución0.5 s, los 129,024 tokens confirmados restauradosEl 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)0n/aSe 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, texto0n/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, imagen0n/a3 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

  1. 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.
  2. 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_tokens restante. 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.
  3. 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.
  4. Nunca cabe frente a cabe más tarde (C4). Si pesos + KV(prompt + max_tokens) + transitorio superan la capacidad, la solicitud recibe HTTP 400 context_exceeds_capacity con siete campos numéricos y retryable: false. Una solicitud que cabe por sí sola espera en su lugar. GET /v1/omlx/capacity y el encabezado X-oMLX-Max-Context-Tokens exponen las cifras.
  5. 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.
  6. 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 + delta en 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ó:

EscenarioRama (primera ejecución)main
Decode de texto0/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ón0/3, abort-all
Decode con imágenes0/3, todas abortadas a la vez (abort-all)0/3, abort-all
Prefill con imágenes1/3; 2 solicitudes fallaron tras 60 s de admission_paused0/3, abort-all
Nunca cabeHTTP 200 (condición inactiva), capacidad available: falseHTTP 200
Sondeos de techo, decode de texto48 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:

BrechaCausa raízCorrección
D1 · las pausas entran en thrashing y terminan en abortosLa 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 VLMnn.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ágenesLa 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 juzgarLas 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 sEl 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 aisladoLas 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 rechazadoSin 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 pausasProbablemente 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:

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.

EscenarioEjecución de la ramaCompletadasAbortosPausas / reanudaciones de decodeTokens restauradosPicoBloqueo máx.main
Decode de textof3c11356 · 48 GB3/306 / 6691,29341 GB1.75 s0/3 abort-all a mitad del prefill tras 15 s
Decode con imágenesf3c11356 · 48 GB3/305 / 5561,49443 GB1.73 s0/3 abort-all, bloqueo de prefill de 168 s
Prefill con imágenes47c0c70b · 46 GB3/301 / 1132,10242 GB1.61 s0/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:

Ramamain
RespuestaHTTP 400 context_exceeds_capacity en 0.1 s, los 7 campos, retryable: false, sin pausa ni parkHTTP 200 (stream cerrado en 0.1 s)
GET /v1/omlx/capacity200, available: true404
X-oMLX-Max-Context-Tokens288273ausente

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ónPausasResultado
MTP desactivado (47c0c70b; referencias en 3b72cde7)12idé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)12casi 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:

PremisamainRamaLectura
P1 · Reenvío tras cancelar a mitad del prefill0 en caché · 110,031 con re-prefill73,728 en caché · 36,303 con re-prefillPASS
P2 · 2×~130k concurrentes2/2 · 0 abortos · 116 GB · 939 s2/2 · 0 abortos · 44 GB · 501 sPASS
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 GBsin regresión
P4 · Concurrencia (2 ejecuciones)0.96× · 12/12 ok0.98× · 12/12 okigual
P6 · Una sola de 110k (3 ejecuciones, mediana)740.7 tok/s · decode 20.42729.5 (−1.5%) · 20.75 (+1.6%)dentro del ruido
Qué demuestra esto, y qué no.
  • 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 main aborta 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ónValor
BuildoMLX 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áquinaQwen3.8-27B-oQ4e-mtp (KV TurboQuant de 4 bits, MTP), M5 Max 128 GB
Valores por defecto del forkSe 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 harnessgentle-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 lotesEliminada 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 forkEl 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 memoriaNivel 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 memoriaActivada 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)
Ventana2026-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ímiteMuestrasSolicitudes abiertas (prom. / máx.)En espera (prom.)Mín. libre del sistemaEficiencia de caché
3 (02:31–03:02)1215.2 / 62.242%94.5%
4 (03:02–03:08)254.9 / 50.930%94.8%
5 (03:08–04:10)2474.0 / 60.120%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 prefijo333 · 14.5M · 94.8%
Transiciones de presión de memoria (soft, hard, emergencia)0
Pausas de decode, reanudaciones y parks0
Admisiones en solitario, esperas de admisión, 400 de nunca cabe0
Abortos por memoria0
Otros abortos3 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 lotesRuta 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 subagentesSesionesSolicitudesPrompts de sistema por sesiónTurnos de solo agregadoCaché por sesión
02:28–03:09492188 de 8895.5–98.3%
Desde 03:32 (todavía en ejecución)4162 a las 04:101158 de 15895.6–98.5% (hasta 04:14)
Qué no muestra esta ejecución. El ciclo de pausa y reanudación no se ejercitó. Con el nivel 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.

Durante más de 3 minutos de trabajo real (20×), oMLX Fork mantiene en curso 3–4 solicitudes de los agentes de gentle-shell y de IntelliJ. Las solicitudes terminan y empiezan otras nuevas, y no aparece ningún error en los fotogramas visibles.

Qué se construyó

CambioProblema que resuelveEvidencia
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.

CausaTokens 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.05MCorrección del harness (gentle-shell #1367) + A1 (servidor)
Mensaje de imagen sintético del cliente que mueve last_query_index~0.94MA2 (servidor)
Compactaciones del cliente~0.7MEsperado
Mensajes reales del usuario (la plantilla elimina el razonamiento anterior)~0.1MSe 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é.

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 guiontruefalse
Contexto en el prompt 1 → prompt 2741.8k → 200.0k41.8k → 135.4k
Límites de turno de usuario que reutilizaron todo el prefijo en caché26 / 263 / 26
Tokens no cacheados por límite, media / máx.893 / 1,8704,048 / 15,056
Tiempo hasta el primer token en un límite, media / máx.6.8 / 15.9 s8.5 / 31.3 s
Tasa de hit de la caché de prefijo, todas las solicitudes99.1%98.1%
Tiempo de prefill sumado690 s660 s
Tokens de salida / caracteres de razonamiento conservados en el contexto106k / 278k66k / 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 alto20.8 → 13.823.1 → 15.1
Marcadores literales en texto, argumentos y razonamiento; bucles en texto visible; paradas por longitud; errores de parseo00
Señales del detector de repetición en razonamiento (sin clasificar)30
Errores de herramientas27
Tiempo total para los 27 prompts6,670 s3,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.

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

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.

CostoSin la corrección del harnessCómo se deriva
Tokens con re-prefill~1.05MDivergencias 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 min1.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 min40k–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 congeladashasta 155–158 s por eventoLos 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 GBLos 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 perdidalas solicitudes retenidas esperan másLos 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

  1. 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").
  2. 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.
  3. 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.
  4. 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>
  5. 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.
  6. 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.

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:

SolicitudUpstream main 43541a0d: tokens en caché / de promptCon la corrección
1 (en frío)0 / 4,0902,048 / 4,091
2 (resultado de herramienta)4,085 / 4,2584,086 / 4,255
3 (aparece el bloque de todo)2,048 / 4,3514,086 / 4,342
4 (stale: 2)4,096 / 4,3944,337 / 4,507
5 (stale: 3)4,096 / 4,4184,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.

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.

  1. 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é.
  2. 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.
  3. El rol gentle-ai-worker carecía de la herramienta nativa agent_browser, mientras que la guardia del harness la exige para el trabajo con navegador.
  4. 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

Una propuesta para gentle-shell

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.

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 publicadoMedido como
65899460082a870c
5dd56ca50b932a39
19aa28ef25685e36
159673d5301bde14
4aa661504e81e443
f3c1135685c1f902
95dd47e88aa178c5
04bb7899aa06f32e
c31820cbb000a28f
5a881160c36b92fb
78291febc9694977
3b72cde7d34413ec
6fbb3274e24198f5
47c0c70be80a1842
e5353d70f6c601b0
31cab33df7dd2bc8
e80eb1f7f92fbd7c
Alcance de la evidencia: un M5 Max con 128 GB, una familia de modelos (Qwen3.8-27B, 4 bits, MTP, KV TurboQuant), Python 3.11, macOS. El fork se rebasó sobre upstream 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 requestLíneas (pruebas)Por defectoEstado
PR1fix(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 pruebassiempre (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.
PR2fix(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 pruebassiempre (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-aCommit de progreso de prefill, incluido el ejecutor de commits en su propio módulo (prerrequisito)2,133 (1,293)desactivadoAbierto 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-bPark 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
S1Alternativa de víctima única y primitivas de caché de cadena exacta1,284 (687)desactivado
S2Pausa 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
S3Prioridad de reanudación consciente de la memoria (reanudar cuando la necesidad cabe; retención FIFO como reserva), adaptada a #3482532 (348)desactivado
S5Sufijo 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 empaquetado997 (758)desactivado
GPrimero 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 lee696 (464)agrega la configuración
S4Condició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
S6Reanudación bajo el umbral duro sin thrashing, ejecución en solitario, presupuesto de pausas de decode922 (537)desactivado
S7Enforcer: respetar los prefills del bucle externo, sin abort-all sin una víctima en vivo, pausa primero en emergencia1,012 (736)desactivado / planificación de memoria
S8Reloj 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
S9Seguimientos: la caché caliente eleva la línea de nunca cabe, y el reloj de bloqueo a través de las retenciones del park490 (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 requestTamañoNotas
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~170Acota 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 TTFT2 PRSolo observabilidad.
feat(api): inline_note como tercera política de sistema intermedio~700Opcional (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.6kPrimero 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)

Antes de cada PR de oMLX vamos a

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.

CambioLí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