Lograr que los modelos de lenguaje de gran tamaño (LLM) se ejecuten en un teléfono ya no es un experimento de investigación. Es una realidad comercial. Sin embargo, en el momento en que pasas de una demo de juguete a un producto real, la latencia vuelve a imponerse. Has recortado el modelo, has cuantizado los pesos y, aun así, la fase de prefill se arrastra. Los tokens se detienen. La interfaz de usuario se congela. Rara vez se culpa a quien realmente debería.
En los dispositivos Android, el cómputo casi nunca es el cuello de botella durante el prefill de un LLM. Lo es el ancho de banda de la memoria. Los SoC de gama alta modernos vienen con potentes núcleos de GPU y NPU que pueden procesar operaciones aritméticas mucho más rápido de lo que el subsistema de memoria puede alimentarlos. Cuando analizas el rendimiento de una implementación de atención ingenua, las unidades de ejecución no están saturadas. Están esperando. Esperando a la DRAM.
Por qué tu modelo cuantizado sigue sintiéndose lento
La cuantización se ha convertido en el primer paso por defecto para la inferencia en el dispositivo. Reducir los pesos de FP16 a INT8 reduce a la mitad el tamaño del modelo y ahorra almacenamiento. Ayuda. Pero no soluciona la latencia de la capa de atención. La razón es sencilla: la cuantización reduce la cantidad de datos que almacenas, pero no reduce el número de transacciones de memoria que realiza el mecanismo de atención.
Una capa de atención multi-head estándar, implementada de la forma tradicional, realiza tres viajes completos de ida y vuelta a la DRAM por cada capa. Las matrices Query, Key y Value se leen de la memoria principal, se calculan las puntuaciones (scores) y los resultados intermedios se vuelven a escribir. La aritmética es trivial. El movimiento de datos es brutal. En Android, donde los presupuestos de energía y térmicos son ajustados, este patrón satura el bus de memoria. El procesador, efectivamente, está pagando el peaje tres veces para cruzar el mismo puente.
Si has estado lanzando modelos INT8 y te preguntas por qué el paso de prefill sigue escalando cuadráticamente con la longitud del prompt, esta es la respuesta. Los pesos son más pequeños, pero el tráfico de activaciones sigue siendo enorme.
El cuello de botella es la memoria, no las matemáticas
Para entender la solución, observa el roofline. Las GPU y NPU móviles en chips como el Snapdragon 8 Gen 3 y el Dimensity 9300 tienen un rendimiento de cómputo teórico que supera con creces lo que sus interfaces LPDDR5X pueden mantener. En un kernel de atención ingenuo, cada cabeza calcula el softmax sobre el producto de Q y K, y luego multiplica por V. Cada matriz de puntuación intermedia se materializa en la memoria global. Eso significa que tus lecturas pico de DRAM escalan con el cuadrado de la longitud de la secuencia, o O(n²). Para un prompt de 1024 tokens, el tráfico de memoria ya es lo suficientemente grande como para dominar el tiempo de ejecución.
Los núcleos están infrautilizados porque no pueden ocultar la latencia. Los procesadores modernos dependen de las cachés para mantener los pipelines llenos. Cuando un algoritmo falla constantemente en la caché y busca en la DRAM, las unidades de ejecución se quedan inactivas. Ninguna cantidad de cuantización soluciona este desajuste estructural entre la capacidad de cómputo y el suministro de memoria.
Cómo el Tiling recupera el ancho de banda
La solución es una estrategia de tiling que mantiene las puntuaciones intermedias en la SRAM integrada en el chip en lugar de enviarlas a la DRAM. Este es el mismo concepto que impulsa a Flash Attention, adaptado para el stack de cómputo de Android. En lugar de materializar una matriz de puntuación completa de n × n en la memoria, divides el cómputo en pequeños tiles que quepan dentro de la caché L1. Calculas estadísticas de softmax locales, acumulas los valores máximos móviles y las sumas de normalización, y solo escribes los resultados finales ponderados de vuelta en la memoria.
Esto cambia la complejidad del ancho de banda. Las lecturas pico de DRAM bajan de O(n²) a O(n), porque ya no necesitas transportar matrices de puntuación completas a través de la memoria principal. El trabajo pesado ocurre dentro de la SRAM, justo al lado de las unidades de ejecución.
Para un ejemplo concreto, considera un tamaño de tile de 64 y una dimensión de cabeza de 128. El tile de puntuación ocupa 16 KB. Esa huella cabe cómodamente dentro de la caché L1 de los SoC de gama alta actuales como el Snapdragon 8 Gen 3 y el Dimensity 9300. La aritmética se mantiene local. El bus de memoria respira.
Implementando esto en Android
El esquema algorítmico es sencillo, aunque acertar en los detalles es importante.
Divide tu Query, Key
