- KVSplit vise à exécuter des contextes plus longs et des modèles plus lourds avec le même budget mémoire sur Apple Silicon, en appliquant des précisions de quantification différentes aux clés et aux valeurs du KV cache d’attention des LLM
- Le résultat clé est la configuration K8V4 : pour 8K tokens, elle passe de 176,00 Mo en FP16 à 71,50 Mo, augmente le débit de traitement de 54 360 tokens/s à 57 438 tokens/s, avec une variation de perplexity annoncée à +0,86 %
- En s’appuyant sur le constat que les clés sont plus sensibles à la quantification que les valeurs, le projet résume que K4V8, qui utilise le même nombre total de bits que K8V4, dégrade la qualité environ 7 fois plus que K8V4
- Les fonctionnalités fournies incluent l’application d’un patch llama.cpp, une build avec prise en charge de Metal, des benchmarks mémoire/vitesse/perplexity, l’enregistrement des résultats en CSV/JSON, des outils de visualisation et des captures de réduction mémoire basées sur Activity Monitor
- La configuration recommandée est K8V4 pour équilibrer qualité et réduction mémoire ; si une réduction maximale de la mémoire est nécessaire, K4V4 permet d’obtenir 72 % d’économie au prix d’environ 6 % de perte de qualité
Le problème que KVSplit cherche à résoudre
- KVSplit est un projet destiné à réduire la mémoire du KV cache lors de l’inférence de LLM sur les Mac Apple Silicon
- Il applique des précisions de quantification différentes aux clés et aux valeurs dans le KV cache du mécanisme d’attention
- Ses objectifs sont les suivants
- Réduire l’utilisation mémoire jusqu’à 72 %
- Exécuter des contextes 2 à 3 fois plus longs avec le même budget mémoire
- Maintenir ou améliorer la vitesse d’inférence par rapport au FP16
- Fournir une prise en charge de Metal adaptée à Apple Silicon
Principaux résultats de benchmark
- Pour 8K tokens, les résultats par configuration sont les suivants
- FP16 : 176,00 Mo, 54 360 tokens/s
- K8V8 : 93,50 Mo, 51 503 tokens/s, perplexity +0,03 %
- K8V4 : 71,50 Mo, 57 438 tokens/s, perplexity +0,86 %
- K4V8 : 71,50 Mo, 58 690 tokens/s, perplexity +6,06 %
- K4V4 : 49,50 Mo, 55 193 tokens/s, perplexity +6,15 %
- Dans le tableau de réduction mémoire, K8V4 est présenté avec 59 % d’économie à 8K tokens, et K4V4 avec 72 % d’économie
- Dans le tableau de performances, K8V4 affiche une amélioration de vitesse de +5,7 % par rapport au FP16, K4V8 de +8,0 % et K4V4 de +1,5 %
- K8V8 réduit la mémoire par rapport au FP16, mais sa vitesse baisse de -5,3 %
Utilisation mémoire selon la longueur de séquence
- Plus la longueur de contexte augmente, plus l’effet de réduction mémoire du KV cache devient important
- Pour 8192 tokens, l’utilisation mémoire est la suivante
- FP16 : 176,00 Mo
- K8V8 : 93,50 Mo
- K8V4 : 71,50 Mo
- K4V8 : 71,50 Mo
- K4V4 : 49,50 Mo
- Même à 4096 tokens, contre 88,00 Mo en FP16, K8V4/K4V8 utilisent 35,75 Mo et K4V4 24,75 Mo
- À 128 tokens, les chiffres annoncés sont 5,50 Mo en FP16, 2,23 Mo pour K8V4/K4V8 et 1,55 Mo pour K4V4
Asymétrie entre clés et valeurs
- La mémoire du KV cache est dominée par le stockage du vecteur de clé et du vecteur de valeur de chaque token
- L’observation centrale du projet est que les clés sont beaucoup plus sensibles à la quantification que les valeurs
- K8V4 utilise des clés 8 bits et des valeurs 4 bits pour offrir le point d’équilibre suivant
- Dégradation de perplexity de 0,86 % par rapport au FP16
- Réduction mémoire de 59 %
- Vitesse d’inférence supérieure au FP16
- K4V8 utilise le même nombre total de bits que K8V4, mais sa dégradation de qualité est résumée comme environ 7 fois plus importante que celle de K8V4
- Cette asymétrie permet, selon le projet, d’exécuter des contextes plus longs et des modèles plus grands sur du matériel grand public
Installation et mode d’intégration
- L’installation consiste à cloner le dépôt puis à exécuter
scripts/install_kvsplit.sh
git clone https://github.com/dipampaul17/KVSplit.git
cd kvsplit
chmod +x scripts/install_kvsplit.sh
./scripts/install_kvsplit.sh
- Le script d’installation permet de choisir le mode de configuration de l’environnement Python
- Virtual Environment : créer un environnement Python indépendant dans le dossier du projet
- System Python : utiliser l’installation Python existante
- Skip Python Setup : laisser l’utilisateur gérer lui-même l’environnement Python
- Le mode d’intégration avec llama.cpp est également au choix
- Mode standard : cloner llama.cpp et appliquer le patch KV split
- Mode Git submodule : ajouter llama.cpp comme submodule pour les développeurs ou utilisateurs avancés
- Le processus d’installation inclut la configuration de llama.cpp avec prise en charge de Metal pour Apple Silicon, l’activation de la quantification différenciée du KV cache, le téléchargement optionnel d’un modèle de test et la configuration des outils de visualisation
Exemples d’utilisation et options CLI
- Une comparaison rapide peut être lancée avec un modèle GGUF possédé par l’utilisateur
python scripts/quick_compare.py --model models/your-model.gguf
- Les configurations comparées sont FP16, K8V8, K8V4, K4V8 et K4V4, avec affichage simultané des métriques de mémoire, de vitesse et de qualité
- L’exemple d’exécution du README utilise
--flash-attn avec les options de quantification KV dans llama-cli
./llama.cpp/build/bin/llama-cli -m models/your-model.gguf -p "Your prompt" \
-t 8 --flash-attn --kvq 8
- L’exemple K4V8 définit séparément les bits des clés et des valeurs
./llama.cpp/build/bin/llama-cli -m models/your-model.gguf -p "Your prompt" \
-t 8 --flash-attn --kvq-key 4 --kvq-val 8
- Pour un contexte 32K, l’exemple indique qu’il faut environ 1,4 Go en FP16 et environ 400 Mo avec K8V4
./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
- Les principaux flags CLI sont les suivants
-t 8 : nombre de threads, 8 recommandé sur la plupart des puces Apple Silicon
--flash-attn : active l’attention optimisée, recommandé sur Apple Silicon
--kvq N : définit les bits pour les clés et les valeurs
--kvq-key N : définit uniquement les bits des clés
--kvq-val N : définit uniquement les bits des valeurs
-c N : taille du contexte
-n N : nombre de tokens à générer
-f FILE : fichier d’entrée
-m MODEL : chemin vers le fichier de modèle .gguf
Benchmarks et outils de visualisation
- Le benchmark complet s’exécute avec
scripts/benchmark_kvsplit.py
python scripts/benchmark_kvsplit.py
python scripts/benchmark_kvsplit.py --config K8V4 --seq-len 4096
- Les visualisations se génèrent avec
scripts/visualize_results.py
python scripts/visualize_results.py
- Le benchmark mesure les éléments suivants
- Memory Usage : mémoire VRAM et mémoire du KV cache
- Performance : tokens/s selon la longueur de séquence
- Quality : perplexity avec
llama-perplexity
- Scaling : évolution de la mémoire et des performances selon la longueur de séquence
- Les résultats sont enregistrés aux formats CSV/JSON, avec génération automatique de statistiques récapitulatives et de graphiques de visualisation
capture_memory.sh est un outil permettant de capturer les réductions mémoire dans Activity Monitor
Optimisation pour Apple Silicon et contraintes
- KVSplit est optimisé pour le framework Metal d’Apple
- Le projet met l’accent sur l’efficacité mémoire pour les appareils soumis à des contraintes de mémoire, comme les puces Apple Silicon série M
- Le README précise qu’en raison du 256B page alignment de llama.cpp, les économies mémoire réelles peuvent différer légèrement du calcul théorique
- Les puces M1, M2, M3 et M4 font partie des cibles prises en charge
Configuration recommandée et feuille de route
- La configuration recommandée est K8V4
- Clés 8 bits, valeurs 4 bits
- 59 % de réduction mémoire
- 0,86 % de perte de qualité
- +5,7 % de vitesse d’inférence par rapport au FP16
- La réduction mémoire maximale est obtenue avec K4V4
- Clés 4 bits et valeurs 4 bits
- 72 % de réduction mémoire
- Environ 6 % de perte de qualité
- Présentée comme adaptée aux applications moins sensibles
- Pour les contextes très longs, K8V4 ou K4V4 sont recommandés, et plus la longueur de contexte augmente, plus les économies mémoire s’accumulent
- Les plans futurs sont les suivants
- Adaptive Precision basée sur l’importance des tokens
- Layer-Specific Quantization utilisant des précisions différentes selon les couches
- Optimisations spécifiques aux modèles pour Mistral, Phi-3, etc.
- Démo web
- Prise en charge d’iOS et d’iPadOS
- La licence est MIT, et les contributions sont acceptées via issue ou pull request
1 commentaires
Avis sur Hacker News
Intéressant. Je me demande s’il existe une intuition expliquant pourquoi on obtient ce résultat. Je me demande aussi si cela a été découvert à partir de cette intuition, ou bien trouvé par des expériences aléatoires.
Dans le script d’installation, l’étape « apply patch » semble encore contenir un placeholder. Plutôt que de demander d’appliquer un patch après
git clone, il serait sans doute plus convivial de forker llama.cpp et de l’inclure comme sous-module Git.Et comme la configuration Python locale varie beaucoup d’une personne à l’autre, ce serait bien de pouvoir séparer la partie liée à llama.cpp de la partie liée à Python, au lieu de figer une dépendance à Homebrew Python.
Les clés déterminent à quels tokens prêter attention et produisent les vrais motifs d’attention via le calcul de similarité. Les valeurs ne font que stocker l’information qui sera transmise une fois l’attention décidée.
Si l’on quantifie les vecteurs de clés de façon trop agressive, les calculs de similarité de toutes les interactions entre tokens sont faussés. Une petite erreur sur une clé peut rediriger complètement l’attention vers le mauvais token.
Les valeurs sont beaucoup plus tolérantes. L’erreur de quantification d’un vecteur de valeurs n’affecte que le contenu informatif de ce token précis, une fois que le motif d’attention est déjà fixé.
C’est un peu comme la différence entre le catalogue d’une bibliothèque et les livres eux-mêmes. Si le numéro de catalogue (clé) est corrompu, on se retrouve devant une étagère totalement incorrecte ; si quelques mots du livre (valeurs) sont flous, on lit toujours le bon livre, avec seulement un peu de bruit de temps en temps.
Mathématiquement, les clés entrent dans le calcul du softmax, où de petites erreurs sont amplifiées exponentiellement pendant la normalisation. Les valeurs ne passent que par une moyenne pondérée linéaire, donc les erreurs ont tendance à se compenser.
J’ai d’abord rencontré cette asymétrie dans des articles comme « More for Keys, Less for Values » et « KV-AdaQuant », puis j’ai voulu quantifier exactement son impact sur l’inférence Apple Silicon. Ce qui m’a frappé, c’est que, à mémoire identique, l’écart de qualité entre K8V4 et K4V8 était d’un facteur 7.
Merci aussi pour les retours sur l’installation ; je vais corriger le placeholder et rendre les dépendances Python plus flexibles.
arg.cppil y a 8 mois.Mais cela n’a pas d’importance, car les options permettant de configurer la quantification de K et V ont déjà été ajoutées à llama.cpp en 2023.
Je ne comprends pas pourquoi ce patch existe. À part remplacer une configuration déjà existante par d’autres arguments en ligne de commande pour donner l’impression que c’est nouveau, je ne vois pas de raison.
Je recommande fortement de ne pas exécuter le fichier
install.shde ce genre de nouveau dépôt. Surtout quand il est inutile pour quelque chose d’aussi simple que l’application d’un fichier de patch.Est-ce différent de l’utilisation de
--cache-type-ket--cache-type-v?J’ai listé une partie des autres bizarreries du dépôt dans un autre commentaire.
Donc avec l’ancienne approche
type_k/v, le minimum atteignable sur GPU Apple devait être du f16/bf16 16 bits. Mais je ne suis pas expert des internals de llama.cpp, je peux donc me tromper.Je me demande si ce patch peut aussi être fait dans MLX. MLX donne de meilleures vitesses, donc combiné à cette approche, les utilisateurs de Mac pourraient avoir de longues conversations à une vitesse acceptable.
Personnellement, ce qui m’enthousiasme le plus, aussi incroyable que cela puisse paraître, ce sont les bindings Haskell. Il y a quelques jours, quelqu’un a souligné que l’évaluation paresseuse de Haskell s’adapte plutôt bien à ce paradigme, et qu’une approche presque purement fonctionnelle du graphe de compilation aide aussi. Faire du machine learning en Haskell pourrait être amusant.
Je me demande si la quantification KV différenciée, par exemple K8V4, peut être appliquée à des modèles déjà convertis au format
.gguf. Ou faut-il reconstruire le modèle avec une prise en charge spéciale ?Si c’est compatible avec n’importe quel fichier
.gguf, je me demande aussi s’il existe des limitations liées au type de modèle (Mistral, Phi-3, etc.) ou aux paramètres du tokenizer..ggufexistants, sans les reconstruire ni effectuer de conversion spéciale. La quantification se fait dans le cache KV au moment de l’exécution, pas pendant le chargement ou la conversion du modèle.C’est possible parce que le cache KV est généré pendant l’inférence, au fil du traitement des tokens, et qu’il est totalement distinct des poids du modèle. Les flags
--kvq-keyet--kvq-valindiquent simplement à llama.cpp comment stocker ces tenseurs intermédiaires en mémoire.Je l’ai testé avec succès sur Llama-3, Mistral, Phi-2/Phi-3, TinyLlama et des variantes de Qwen.
La seule limitation est qu’il faut le backend Metal de llama.cpp et que l’implémentation actuelle de Flash Attention dans llama.cpp contourne les formats personnalisés de cache KV ; il faut donc désactiver Flash Attention avec
-fa 0. La technique elle-même devrait fonctionner avec n’importe quelle architecture Transformer utilisant un mécanisme d’attention standard.J’ai eu le temps de lire le code. Si j’ai bien compris cette PR, cette fonctionnalité existe déjà dans llama.cpp depuis 2023, donc le patch est inutile : https://github.com/ggml-org/llama.cpp/pull/4312
Au lieu de fournir un fork de llama.cpp avec les changements appliqués en commits, le dépôt fait exécuter un script
install.sh. Ce script checkout la branche master de llama.cpp sans préciser de révision, puis applique un petit patch. Rien que ça, c’est un signal d’alerteLe dépôt contient 4 fichiers de patch différents, et il y a encore une version supplémentaire du patch intégrée dans le script d’installation via un Heredoc. Le script contient aussi deux versions du code qui clone le dépôt et tente d’appliquer le patch
install.shécrase un fichier de patch par un autre avec la lignecp patch/split_kv_quant.diff patch/fixed_kv_patch.diff. Lefixed_kv_patch.diffcommité dans le dépôt est donc écrasé avant d’être appliquéÀ mon avis, c’est ce patch qui était censé être utilisé : https://github.com/dipampaul17/KVSplit/blob/main/patch/split... (correction : d’après le commentaire à la fin, il semble qu’en réalité ce soit celui-ci : https://github.com/dipampaul17/KVSplit/blob/main/patch/fixed... )
Tout ce que ce patch ajoute, c’est un argument
--kvqcensé définir simultanément la quantification de K et de V, alors que juste au-dessus il existe déjà des arguments intégrés pour définir séparément la quantification de K et celle de V. Est-il possible que l’auteur n’ait pas remarqué, en déplaçant ces patchs dans tous les sens, que la fonctionnalité existait déjà ?Je recommande fortement de ne pas exécuter les scripts shell de nouveaux dépôts de ce genre. Surtout quand le script est aussi complexe
Le post HN a reçu plus de 200 upvotes et le dépôt GitHub a aussi dépassé les 200 étoiles, avec une progression continue, mais le contenu semble trompeur. Dans ce fil, un commentaire qui pointait le problème et a été massivement flaggé avait en réalité raison. Il est aussi préoccupant que l’auteur continue de répondre dans ce fil tout en évitant la question de savoir si la fonctionnalité existe déjà
Correction : j’avais mal lu le script shell. En réalité, il semble appliquer ce patch : https://github.com/dipampaul17/KVSplit/blob/main/patch/fixed... Après avoir appliqué le patch, il écrase bizarrement
fixed_kv_patch.diffparsplit_kv_quant.diff, mais ne fait plus rien ensuite. Je ne sais pas si c’est le résultat de vibe coding ou simplement d’une édition de code négligente, mais je tiens à répéter qu’il ne faut pas exécuter ce genre de script shell provenant d’un dépôt inconnuCorrection 2 : c’est encore plus confus. Le script
install.shréférence l’ancienne URL du dépôt llama.cpp (https://github.com/ggerganov/llama.cpp), qui a changé il y a un moment et redirige désormais. Les patchs tentent de modifier le parsing des arguments danscommon.cpp, mais ce code a été déplacé versarg.cppil y a 8 mois (https://github.com/ggml-org/llama.cpp/commit/bfe76d4a17228bf...). Donc ce script d’installation et ce dépôt semblent basés sur du code datant d’environ 2024, tout en utilisant une option ajoutée à llama.cpp vers 2023. Que se passe-t-il au juste ?Il y a beaucoup de signaux d’alerte. Au mieux, ça ressemble à quelqu’un qui gonfle son profil GitHub avec du code généré par LLM. Il suffit de regarder l’activité du 12 mai sur son profil
Mais l’ensemble de l’activité GitHub de l’auteur du post d’origine est suspecte. Le 12 mai, il a envoyé des PR de bouillie LLM à plusieurs projets populaires, et seule celle côté JAX a été refusée. Pourtant, cela lui a permis d’épingler des projets populaires sur son profil comme s’il en était contributeur
Difficile de dire à quel point c’est détestable. Quiconque travaille dans l’IA contribue à la pollution de l’information, et nous ne pouvons même pas encore en prévoir les conséquences. L’Internet mort et le déluge de bouillie IA ne sont qu’un début
Sur Apple Silicon avec 64 Go ou 128 Go, est-ce que ces éléments sont significativement plus rapides ou meilleurs qu’avec 36 Go ou 48 Go ?
J’ai lu que les grands contextes et les grands modèles restent douloureusement lents, même sur les Apple Silicon les plus rapides et les plus gros que l’argent puisse acheter.
Je me demande donc si cela permet de mieux exploiter une mémoire plus importante, ou si, en pratique, la bonne réponse sur Apple Silicon reste quand même des modèles relativement petits.
Cela dit, KVSplit ne change pas fondamentalement la vitesse de calcul : il ne modifie que l’efficacité mémoire. Dans les benchmarks, K8V4 améliore le débit de 14,5 %, mais c’est dû à une meilleure localité mémoire, pas à une réduction du volume de calcul.
La principale raison pour laquelle les grands modèles sont « douloureusement lents » sur Apple Silicon n’est pas la contrainte mémoire, mais la limite de performance de calcul. Un modèle à 70B paramètres tournera avec une vitesse de génération de tokens similaire, indépendamment de la RAM disponible ou de l’optimisation du cache KV.
KVSplit permet de mieux utiliser la mémoire disponible. Il est particulièrement utile quand le goulot d’étranglement est la longueur du contexte plutôt que la taille du modèle.
Pour un usage pratique sur Apple Silicon, le bon compromis reste d’associer des modèles plus petits (7B à 13B) à une fenêtre de contexte étendue. On peut ainsi traiter beaucoup plus de texte tout en conservant une vitesse de génération raisonnable.
Si votre workflow exige à la fois un contexte énorme et un grand modèle, il faut toujours envisager des GPU de classe serveur, mais KVSplit repousse un peu les limites de ce qui est possible sur le matériel Apple.
Excellent travail, et ça a l’air très intéressant, mais il me faudrait une explication un peu plus haut niveau pour comprendre.
Par exemple, est-ce que cela permet de faire tourner un modèle prévu pour une fenêtre de contexte de 2048 tokens avec une fenêtre de 4 à 6K ? Ou bien de faire tourner un modèle 128K comme gemma3 avec une fenêtre de contexte de 256K ou plus ?
Quel est le cas d’usage idéal pour les modèles locaux ?
Concrètement, cela veut dire qu’on peut traiter un livre entier d’un coup sur un MacBook, analyser une grosse base de code sans découper les fichiers, ou conserver un long historique de conversation dans une application de chat.
Les économies de mémoire sont linéaires avec la longueur du contexte. Plus la fenêtre de contexte est longue, plus la quantité absolue de mémoire économisée est élevée. Sur mon MacBook M4, avec un contexte 8K, le cache KV est passé de 176 Mo à 72 Mo. Avec un contexte 128K, la même proportion d’économies libérerait plusieurs gigaoctets de mémoire.
Cette optimisation est la plus utile lorsque l’on atteint la limite de fenêtre de contexte plutôt que la limite liée aux paramètres du modèle. Si vous obtenez des erreurs de mémoire insuffisante à cause de longues entrées, et non à cause de gros poids de modèle, KVSplit s’attaque directement au goulot d’étranglement.
Étendre la fenêtre de contexte après l’entraînement n’est pas trivial ; si vous ne savez pas exactement ce que vous faites, mieux vaut chercher un modèle entraîné avec une fenêtre de contexte plus grande.
Les modèles locaux servent à beaucoup de choses : travail hors ligne, confidentialité/sécurité, etc. Mais la plupart des usages consistent surtout à expérimenter en ajustant les modèles.
Il se passe quelque chose de bizarre, donc mieux vaut ne pas installer ça ni exécuter ce script.
J’ai signalé la soumission.
Excellente idée et tentative. Est-ce que cela s’applique aussi aux GPU ? Et j’imagine que ce serait compatible avec d’autres techniques de quantification, mais qu’il faudrait probablement un patch séparé pour chacune ?
Le backend CUDA de llama.cpp prend déjà en charge des types de cache distincts via les options
--cache-type-ket--cache-type-v. Ce patch précis se concentre sur une optimisation propre à Metal, mais la technique centrale se transpose telle quelle.Il existe aussi une compatibilité avec d’autres méthodes de quantification. Cette optimisation du cache KV est complémentaire de la quantification des poids du modèle (Q4_K_M, GPTQ, AWQ, etc.). On peut utiliser une précision asymétrique du cache KV avec n’importe quel format de poids de modèle.
La quantification du cache KV se produit au moment de l’exécution, pendant le traitement des tokens, et reste distincte des poids du modèle ; elle n’entre donc pas en conflit avec la façon dont le modèle lui-même a été quantifié. Elle agit sur une autre partie du pipeline d’inférence.
Le travail supplémentaire concerne l’intégration avec des moteurs d’inférence spécialisés qui ont leur propre gestion du cache KV, comme vLLM ou TensorRT-LLM. Chacun devra implémenter séparément la précision KV asymétrique.
Sur GPU, le gain le plus immédiat viendra probablement de l’intégration directe de cette idée dans les implémentations de FlashAttention. Sur matériel CUDA, la réduction de la bande passante mémoire pourrait se traduire par une accélération plus importante.
Une perplexité à +0,86 % avec une petite taille de contexte, ce n’est pas déjà assez élevé ? Qu’en est-il avec des tailles de contexte plus réalistes, comme 64 à 128K ?
Ou bien d’utiliser la mémoire disponible pour autre chose, comme un IDE.