- KVSplit es un proyecto que busca ejecutar contextos más largos y modelos más pesados con el mismo presupuesto de memoria en Apple Silicon, aplicando distinta precisión de cuantización para key y value en el KV cache de attention de los LLM
- El resultado clave es la configuración K8V4, que reduce el uso de memoria de 176.00MB en FP16 a 71.50MB con 8K tokens, aumenta la velocidad de procesamiento de 54,360 tokens/sec a 57,438 tokens/sec y reporta un cambio de perplexity de +0.86%
- Con base en el hallazgo de que las key son más sensibles a la cuantización que las value, se resume que K4V8, aun usando el mismo total de bits que K8V4, sufre una degradación de calidad aproximadamente 7 veces mayor
- Las funciones incluidas abarcan aplicación de parche para llama.cpp, compilación con soporte Metal, benchmarks de memoria, velocidad y perplexity, guardado de resultados en CSV/JSON, herramientas de visualización y capturas de ahorro de memoria basadas en Activity Monitor
- La configuración recomendada es K8V4 por su equilibrio entre calidad y ahorro de memoria; si se necesita el máximo ahorro, K4V4 ofrece una reducción del 72% a cambio de una pérdida de calidad de alrededor del 6%
El problema que KVSplit busca resolver
- KVSplit es un proyecto para reducir la memoria del KV cache durante la inferencia de LLM en Macs con Apple Silicon
- Aplica distinta precisión de cuantización a key y value dentro del KV cache del mecanismo de attention
- Sus objetivos son los siguientes
- Reducir el uso de memoria hasta en 72%
- Ejecutar contextos 2-3 veces más largos con el mismo presupuesto de memoria
- Mantener o mejorar la velocidad de inferencia frente a FP16
- Ofrecer soporte Metal ajustado a Apple Silicon
Resultados clave de benchmark
- Los resultados por configuración con 8K tokens son los siguientes
- FP16: 176.00MB, 54,360 tokens/sec
- K8V8: 93.50MB, 51,503 tokens/sec, perplexity +0.03%
- K8V4: 71.50MB, 57,438 tokens/sec, perplexity +0.86%
- K4V8: 71.50MB, 58,690 tokens/sec, perplexity +6.06%
- K4V4: 49.50MB, 55,193 tokens/sec, perplexity +6.15%
- En la tabla de ahorro de memoria, K8V4 aparece con una reducción del 59% en 8K tokens, mientras que K4V4 alcanza 72%
- En la tabla de rendimiento, K8V4 muestra una mejora de +5.7% frente a FP16, K4V8 de +8.0% y K4V4 de +1.5%
- K8V8 reduce memoria frente a FP16, pero su velocidad cae -5.3%
Uso de memoria según la longitud de secuencia
- A medida que crece la longitud del contexto, aumenta el efecto de ahorro de memoria en el KV cache
- El uso de memoria con 8192 tokens es el siguiente
- FP16: 176.00MB
- K8V8: 93.50MB
- K8V4: 71.50MB
- K4V8: 71.50MB
- K4V4: 49.50MB
- Incluso con 4096 tokens, frente a los 88.00MB de FP16, K8V4/K4V8 usan 35.75MB y K4V4 usa 24.75MB
- Con 128 tokens, se reportan 5.50MB para FP16, 2.23MB para K8V4/K4V8 y 1.55MB para K4V4
Asimetría entre key y value
- La memoria del KV cache está dominada por el almacenamiento de los vectores key y value de cada token
- La observación central del proyecto es que las key son mucho más sensibles a la cuantización que las value
- K8V4 usa key de 8 bits y value de 4 bits, ofreciendo este punto de equilibrio
- Deterioro de perplexity de 0.86% frente a FP16
- 59% de ahorro de memoria
- Velocidad de inferencia superior a FP16
- K4V8 usa el mismo total de bits que K8V4, pero se resume que su degradación de calidad es aproximadamente 7 veces mayor que la de K8V4
- Gracias a esta asimetría, se explica que es posible ejecutar contextos más largos y modelos más grandes en hardware de consumo
Instalación e integración
- La instalación consiste en clonar el repositorio y luego ejecutar
scripts/install_kvsplit.sh
git clone https://github.com/dipampaul17/KVSplit.git
cd kvsplit
chmod +x scripts/install_kvsplit.sh
./scripts/install_kvsplit.sh
- El script de instalación permite elegir cómo configurar el entorno de Python
- Virtual Environment: crea un entorno de Python aislado dentro de la carpeta del proyecto
- System Python: usa la instalación existente de Python
- Skip Python Setup: el usuario administra el entorno de Python manualmente
- También se puede elegir el modo de integración con llama.cpp
- Modo estándar: clona llama.cpp y aplica el parche de KV split
- Modo Git submodule: agrega llama.cpp como submodule para desarrolladores o usuarios avanzados
- El proceso de instalación incluye configurar llama.cpp con soporte Metal para Apple Silicon, habilitar differentiated KV cache quantization, descarga opcional de modelos de prueba y configuración de herramientas de visualización
Ejemplos de uso y opciones de CLI
- Una comparación rápida puede ejecutarse con cualquier modelo GGUF que tenga el usuario
python scripts/quick_compare.py --model models/your-model.gguf
- Las configuraciones comparadas son FP16, K8V8, K8V4, K4V8 y K4V4, mostrando en conjunto memoria, velocidad y métricas de calidad
- El ejemplo de ejecución del README usa
llama-cli junto con --flash-attn y opciones de cuantización KV
./llama.cpp/build/bin/llama-cli -m models/your-model.gguf -p "Your prompt" \
-t 8 --flash-attn --kvq 8
- El ejemplo de K4V8 especifica por separado los bits de key y value
./llama.cpp/build/bin/llama-cli -m models/your-model.gguf -p "Your prompt" \
-t 8 --flash-attn --kvq-key 4 --kvq-val 8
- El ejemplo de contexto de 32K indica que en FP16 se requieren aproximadamente 1.4GB, mientras que en K8V4 se necesitan aproximadamente 400MB
./llama.cpp/build/bin/llama-cli -m models/your-model.gguf \
-c 32768 -n 4096 -t 8 --flash-attn --kvq 8 \
-f your-long-document.txt
- Las principales flags de CLI son las siguientes
-t 8: número de hilos; se recomiendan 8 en la mayoría de los chips Apple Silicon
--flash-attn: activa attention optimizada; recomendado en Apple Silicon
--kvq N: configura los bits de key y value
--kvq-key N: configura solo los bits de key
--kvq-val N: configura solo los bits de value
-c N: tamaño del contexto
-n N: número de tokens a generar
-f FILE: archivo de entrada
-m MODEL: ruta del archivo del modelo .gguf
Herramientas de benchmark y visualización
- El benchmark completo se ejecuta con
scripts/benchmark_kvsplit.py
python scripts/benchmark_kvsplit.py
python scripts/benchmark_kvsplit.py --config K8V4 --seq-len 4096
- La visualización se genera con
scripts/visualize_results.py
python scripts/visualize_results.py
- El benchmark mide los siguientes puntos
- Memory Usage: VRAM y memoria del KV cache
- Performance: tokens/sec según la longitud de secuencia
- Quality: perplexity usando
llama-perplexity
- Scaling: cambios de memoria y rendimiento según la longitud de secuencia
- Los resultados se guardan en formato CSV/JSON y generan estadísticas de resumen y gráficos de forma automática
capture_memory.sh es una herramienta para capturar el ahorro de memoria desde Activity Monitor
Optimización para Apple Silicon y limitaciones
- KVSplit está optimizado para el framework Metal de Apple
- Pone énfasis en la eficiencia de memoria en dispositivos con restricciones de memoria, como Apple Silicon de la serie M
- El README aclara que, debido al alineamiento de páginas de 256B de llama.cpp, el ahorro de memoria real puede diferir ligeramente del cálculo teórico
- Entre los dispositivos compatibles se incluyen chips M1, M2, M3 y M4
Configuraciones recomendadas y hoja de ruta
- La configuración recomendada es K8V4
- key de 8 bits, value de 4 bits
- 59% de ahorro de memoria
- 0.86% de pérdida de calidad
- +5.7% de velocidad de inferencia frente a FP16
- La opción de máximo ahorro de memoria es K4V4
- key de 4 bits y value de 4 bits
- 72% de ahorro de memoria
- cerca de 6% de pérdida de calidad
- se presenta como adecuada para aplicaciones menos sensibles
- Para contextos muy largos se recomiendan K8V4 o K4V4, y cuanto más largo es el contexto, más se acumula el ahorro de memoria
- Los planes a futuro son los siguientes
- Adaptive Precision basada en la importancia del token
- Layer-Specific Quantization con distinta precisión por capa
- Optimizaciones específicas por modelo para Mistral, Phi-3 y otros
- Demo web
- Soporte para iOS y iPadOS
- La licencia es MIT y se aceptan contribuciones vía issue o pull request
1 comentarios
Opiniones de Hacker News
Interesante. Me pregunto si hay alguna intuición sobre por qué se obtienen estos resultados. También me pregunto si lo descubrieron a partir de esa intuición o si lo encontraron mediante experimentos aleatorios.
Parece que en el paso "apply patch" del script de instalación todavía queda un placeholder. En vez de hacer
git cloney luego aplicar un parche, sería más amigable para el usuario hacer un fork de llama.cpp e incluirlo como submódulo de Git.Además, como cada persona tiene una configuración local de Python distinta, estaría bueno poder separar la parte relacionada con llama.cpp de la parte relacionada con Python, en lugar de fijar una dependencia de Homebrew Python.
Las claves determinan a qué tokens prestar atención y crean el patrón real de atención mediante el cálculo de similitud. Los valores solo almacenan la información que se transmitirá después de que se haya decidido la atención.
Si cuantizas los vectores de clave de forma demasiado agresiva, se distorsionan los cálculos de similitud de todas las interacciones entre tokens. Un pequeño error en una clave puede hacer que la atención se desvíe a un token completamente equivocado.
Los valores son mucho más tolerantes. El error de cuantización de un vector de valor solo afecta el contenido de información de ese token individual después de que el patrón de atención ya quedó definido.
Es parecido a la diferencia entre el sistema de catálogo de una biblioteca y los libros en sí. Si se estropea el número de catálogo (clave), terminas mirando un estante totalmente equivocado; pero si algunas palabras de un libro (valor) se emborronan, sigues leyendo el libro correcto y solo aparece ruido de vez en cuando.
Matemáticamente, las claves entran en el cálculo de softmax, donde pequeños errores se amplifican exponencialmente durante la normalización. Los valores solo pasan por un promedio ponderado lineal, por lo que los errores tienden a cancelarse.
Al principio me encontré con esta asimetría en papers como "More for Keys, Less for Values" y "KV-AdaQuant", y quería cuantificar exactamente cuánto impacto tenía en la inferencia sobre Apple Silicon. Me impresionó que, con la misma memoria, hubiera una diferencia de calidad de 7 veces entre K8V4 y K4V8.
Gracias también por el feedback sobre la instalación; voy a corregir el placeholder y hacer más flexibles las dependencias de Python.
arg.cpphace 8 meses.Aun así, no importa, porque las opciones para configurar la cuantización de K y V ya se agregaron a llama.cpp en 2023.
No entiendo por qué existe este parche. No veo otra razón que intentar hacer que algo ya existente parezca nuevo cambiándolo por otros argumentos de línea de comandos.
Recomiendo enfáticamente que nadie ejecute el archivo
install.shde este repositorio nuevo. Sobre todo cuando es innecesario para algo tan simple como aplicar un solo archivo de parche.¿Esto es distinto de usar
--cache-type-ky--cache-type-v?Dejé algunas otras rarezas del repositorio en otro comentario.
Así que con el método anterior de
type_k/v, y en GPU de Apple, creo que lo mínimo a lo que se podía bajar era 16 bits f16/bf16. Dicho eso, no soy experto en los internos de llama.cpp, así que podría estar equivocado.Me pregunto si este parche también se podría hacer en MLX. Como en MLX se obtiene mejor velocidad, si se combinara con este enfoque, los usuarios de Mac podrían tener conversaciones largas a una velocidad utilizable.
Personalmente, lo que más me entusiasma, aunque parezca difícil de creer, son los bindings de Haskell. Hace unos días alguien señaló que la evaluación perezosa de Haskell encaja bastante bien con este paradigma, y que un enfoque casi puramente funcional para los grafos de compilación también ayuda. Hacer aprendizaje automático en Haskell suena divertido.
Me pregunto si la cuantización KV diferencial (por ejemplo, K8V4) se puede aplicar a modelos ya convertidos al formato
.gguf. ¿O hay que reconstruir el modelo con soporte especial?Si es compatible con cualquier archivo
.gguf, también me pregunto si hay limitaciones por tipo de modelo (Mistral, Phi-3, etc.) o por configuración del tokenizador..ggufexistentes tal cual, sin reconstruirlos ni hacer una conversión especial. La cuantización ocurre en la caché KV en tiempo de ejecución, no durante la carga ni la conversión del modelo.Esto es posible porque la caché KV se genera durante la inferencia a medida que se procesan los tokens, y es completamente independiente de los pesos del modelo. Las flags
--kvq-keyy--kvq-valsolo le indican a llama.cpp cómo almacenar esos tensores intermedios en memoria.Se probó con éxito en Llama-3, Mistral, Phi-2/Phi-3, TinyLlama y variantes de Qwen.
La única limitación es que se necesita el backend Metal de llama.cpp y, como la implementación actual de Flash Attention de llama.cpp omite el formato personalizado de caché KV, hay que desactivar Flash Attention con
-fa 0. La técnica en sí debería funcionar con cualquier arquitectura Transformer que use el mecanismo estándar de atención.Tuve tiempo de leer el código. Si entendí bien este PR, el parche es innecesario porque esta función ya estaba en llama.cpp desde 2023: https://github.com/ggml-org/llama.cpp/pull/4312
En vez de ofrecer un fork de llama.cpp con los cambios aplicados como commits, el repositorio hace que se ejecute el script
install.sh. Ese script hace checkout de la rama master de llama.cpp sin fijar una revisión y luego aplica un parche corto. Eso por sí solo ya es una señal de alerta de que algo anda raroEn el repositorio hay 4 archivos de parche distintos, y dentro del script de instalación hay una versión adicional del parche incrustada como Heredoc. El script también contiene dos versiones del código que clona el repositorio e intenta aplicar el parche
install.shsobrescribe un archivo de parche con otro mediante la líneacp patch/split_kv_quant.diff patch/fixed_kv_patch.diff. Así que elfixed_kv_patch.diffincluido en el repositorio se sobrescribe antes de aplicarseA mi parecer, originalmente querían usar este parche: https://github.com/dipampaul17/KVSplit/blob/main/patch/split... (edición: por el comentario del final, parece que en realidad es este: https://github.com/dipampaul17/KVSplit/blob/main/patch/fixed... )
Lo único que agrega este parche es el argumento
--kvq, que supuestamente configura la cuantización de K y V al mismo tiempo, pero justo arriba ya hay argumentos integrados para configurar la cuantización de K y V por separado. ¿De verdad el autor no se dio cuenta de que la función ya existía mientras movía estos parches de un lado a otro?Recomiendo firmemente no ejecutar scripts de shell de repositorios nuevos como este. Especialmente si son scripts tan complejos
El post en HN recibió más de 200 votos y el repositorio de GitHub ya tiene más de 200 estrellas y sigue subiendo, pero el contenido parece engañoso. El comentario que señaló el problema en este hilo y recibió un montón de flags en realidad tenía razón. También preocupa que el autor siga respondiendo en este hilo, pero evite la pregunta de si la función ya existía
Edición: leí mal el script de shell. Parece que en realidad aplica este parche: https://github.com/dipampaul17/KVSplit/blob/main/patch/fixed... Después de aplicar el parche, de forma extraña sobrescribe
fixed_kv_patch.diffconsplit_kv_quant.diff, pero después no hace nada más. No sé si esto es resultado de vibe coding o simplemente de una edición descuidada del código, pero quiero reiterar que no deberían ejecutar scripts de shell así de repositorios desconocidosEdición 2: es aún más confuso. El script
install.shhace referencia a la URL antigua del repositorio de llama.cpp (https://github.com/ggerganov/llama.cpp), que cambió hace tiempo y ahora redirige. Los parches intentan modificar el parseo de argumentos encommon.cpp, pero ese código se movió aarg.cpphace 8 meses (https://github.com/ggml-org/llama.cpp/commit/bfe76d4a17228bf...). Entonces este script de instalación y el repositorio parecen basarse en código de alrededor de 2024, usando una opción que se agregó a llama.cpp alrededor de 2023. ¿Qué demonios está pasando?Hay muchas señales de alerta. Siendo generosos, parece alguien que intenta inflar su perfil de GitHub con código generado por LLM. Basta con mirar la actividad de ese perfil del 12 de mayo
Pero toda la actividad de GitHub del autor original es sospechosa. El 12 de mayo mandó PRs basura de LLM a varios proyectos populares, y solo el de JAX fue rechazado. Aun así, gracias a eso pudo fijar proyectos populares en su perfil como si fuera colaborador
Es difícil expresar con palabras lo repulsivo que es esto. Cualquiera que trabaje en IA está siendo cómplice de la contaminación de la información, y ni siquiera podemos predecir sus consecuencias todavía. La internet muerta y la avalancha de basura de IA son apenas el comienzo
En Apple Silicon con 64 GB o 128 GB, ¿esto es significativamente más rápido o mejor que con 36 GB o 48 GB?
He leído que los contextos grandes y los modelos grandes son dolorosamente lentos incluso en el Apple Silicon más rápido y grande que se puede comprar con dinero.
Por eso me pregunto si esto permite aprovechar mejor la memoria más grande, o si en la práctica la respuesta en Apple Silicon siguen siendo modelos relativamente pequeños.
Dicho eso, KVSplit no cambia de forma fundamental la velocidad de cómputo; solo cambia la eficiencia de memoria. En los benchmarks, K8V4 mejoró el rendimiento en un 14.5%, pero eso se debió a una mejor localidad de memoria, no a una reducción del cómputo.
La razón principal por la que los modelos grandes se sienten “dolorosamente lentos” en Apple Silicon no son las restricciones de memoria, sino los límites de rendimiento de cómputo. Un modelo de 70B parámetros correrá a velocidades de generación de tokens parecidas sin importar la RAM disponible o la optimización de la caché KV.
KVSplit permite usar mejor la memoria disponible. Es especialmente valioso cuando el cuello de botella es la longitud del contexto, más que el tamaño del modelo.
Para un uso práctico en Apple Silicon, el punto óptimo sigue siendo combinar modelos más pequeños (7B~13B) con ventanas de contexto ampliadas. Así se puede procesar mucho más texto manteniendo una velocidad de generación razonable.
Si tu flujo de trabajo requiere tanto contextos enormes como modelos grandes, todavía conviene considerar GPU de nivel servidor, pero KVSplit empuja un poco más los límites de lo posible en hardware de Apple.
Es un gran trabajo y se ve muy interesante, pero para entenderlo necesito una explicación un poco más de alto nivel.
Por ejemplo, ¿permite ejecutar un modelo con ventana de contexto de 2048 tokens como si tuviera una ventana de 4~6K? ¿O permite ejecutar un modelo de 128K como gemma3 con una ventana de contexto de 256K o más?
¿Cuál sería el caso de uso ideal para modelos locales?
En términos prácticos, eso significa poder procesar un libro completo de una vez en una MacBook, analizar una base de código grande sin dividir archivos, o mantener historiales de conversación largos en aplicaciones de chat.
El ahorro de memoria escala linealmente con la longitud del contexto. Cuanto más larga sea la ventana de contexto, mayor será la memoria ahorrada en términos absolutos. En mi MacBook M4, con contexto 8K, la caché KV bajó de 176 MB a 72 MB. Con un contexto de 128K, el mismo porcentaje de ahorro liberaría memoria en el orden de gigabytes.
Esta optimización es más valiosa cuando te topas con el límite de la ventana de contexto, más que con el límite de parámetros del modelo. Si te quedas sin memoria por entradas largas y no por pesos de modelos grandes, KVSplit ataca directamente ese cuello de botella.
Aumentar la ventana de contexto después del entrenamiento no es sencillo, así que si no sabes exactamente lo que estás haciendo, es mejor buscar un modelo entrenado con una ventana de contexto más grande.
Los modelos locales sirven para muchas cosas, como trabajo offline, privacidad/seguridad, etc. Aunque la mayoría suele usarlos para experimentar ajustando modelos.
Está pasando algo raro, así que recomendaría no instalar esto ni ejecutar ese script.
Ya marqué la publicación.
Es una gran idea y un buen intento. ¿Esto también aplica a GPU? Y parece que también sería compatible con otros métodos de cuantización; ¿hay que asumir que probablemente cada uno necesitaría un parche aparte?
El backend CUDA de llama.cpp ya permite configurar tipos de caché separados con los flags
--cache-type-ky--cache-type-v. Este parche específico se enfoca en una optimización para Metal, pero la técnica central se traslada tal cual.También es compatible con otros métodos de cuantización. Esta optimización de caché KV es complementaria a la cuantización de pesos del modelo (Q4_K_M, GPTQ, AWQ, etc.). Se puede usar precisión asimétrica de caché KV junto con cualquier formato de pesos del modelo.
La cuantización de la caché KV ocurre en tiempo de ejecución durante el procesamiento de tokens y es independiente de los pesos del modelo, por lo que no entra en conflicto con la forma en que el modelo en sí fue cuantizado. Operan en partes distintas del pipeline de inferencia.
Donde se necesita trabajo adicional es en la integración con motores de inferencia especializados que tienen manejo personalizado de caché KV, como vLLM o TensorRT-LLM. Cada uno tendría que implementar por separado la precisión KV asimétrica.
En GPU, el beneficio más inmediato probablemente vendría de integrar esta idea directamente en implementaciones de FlashAttention. En hardware CUDA, el ahorro de ancho de banda de memoria podría traducirse en una mejora de velocidad mayor.
Con tamaños de contexto pequeños, una perplejidad de +0.86% parece bastante alta, ¿no? ¿Qué pasa con tamaños de contexto más realistas, como 64~128K?
O también se puede usar la memoria libre para otras cosas, como un IDE.