Optimiser l’inférence sur appareil pour Apple Silicon

Un moteur local personnalisé qui améliore le débit de pré-remplissage et de décodage.

AuteursPerplexity Engineering

Le calcul hybride sur Apple Silicon orchestre une tâche entre l’intelligence de pointe dans le cloud et un modèle local sur le Mac. Les modèles dans le cloud gèrent la recherche et le raisonnement, tandis qu’un modèle local traite les fichiers et applications privés sur le Mac.

Pour que cette division du travail paraisse fluide, l’inférence locale doit suivre le rythme du reste de la tâche. Cela nécessite un moteur capable de traiter rapidement les prompts et de maintenir un taux de génération de tokens élevé.

Lily, notre moteur d’inférence local léger, est conçu spécifiquement pour Apple Silicon et Qwen3.6-35B-A3B, avec des optimisations distinctes pour le pré-remplissage et le décodage. Le code source du moteur sera bientôt publié en open source.

Introduction

Une méthode courante pour exécuter des LLM sur un Mac consiste à utiliser MLX, le framework d’apprentissage automatique open source d’Apple pour Apple Silicon. Sa bibliothèque compagnon, MLX-LM, ajoute les composants nécessaires pour charger et générer du texte avec un large éventail de modèles de langage. Ensemble, MLX et MLX-LM constituent une pile prête à l’emploi et polyvalente pour l’inférence de LLM locaux.

Qwen3.6-35B-A3B est un modèle hybride et creux : il utilise un routage par mélange d’experts (MoE) et associe des états récurrents à taille fixe à une attention complète. Ces choix architecturaux réduisent le volume de calcul nécessaire, mais génèrent également des charges de travail irrégulières. Les tokens sont acheminés vers différents poids d’experts, et les états récurrents ont une nature séquentielle.

MLX-LM sélectionne déjà des noyaux optimisés pour les phases d’inférence et les formes de charges de travail courantes, mais ses opérations réutilisables doivent prendre en charge de nombreuses architectures de modèles. Un moteur dédié à Qwen peut se spécialiser au niveau du modèle et du runtime, en coordonnant les noyaux, les transferts de données et l’ordonnancement autour de la structure fixe du modèle.

Lily implémente cette spécialisation de bout en bout dans un seul processus. Un runtime Rust charge le point de contrôle du modèle et gère l’état de la Session et la boucle de génération, une API de complétion de chat compatible OpenAI accepte les requêtes et diffuse les tokens en continu, et des noyaux Metal personnalisés exécutent les opérations spécifiques à Qwen. Ni PyTorch ni MLX ne se trouvent sur le chemin d’exécution.

Où se situent la généricité et la spécialisation dans les deux piles d'inférence. MLX-LM décrit le modèle sous forme d'opérations de tableaux MLX composables, que MLX ordonnance par le biais de noyaux réutilisables. Lily place plutôt la structure du modèle, les plans d'exécution spécifiques à la phase et la sélection des noyaux dans un runtime Rust unique conçu autour de Qwen et d'Apple Silicon.
Où se situent la généricité et la spécialisation dans les deux piles d’inférence. MLX-LM décrit le modèle comme des opérations de tableaux MLX composables, que MLX ordonnance par le biais de noyaux réutilisables. Lily place au contraire la structure du modèle, les plans d’exécution spécifiques à chaque phase et la sélection des noyaux dans un runtime Rust unique conçu autour de Qwen et d’Apple Silicon.

Nous mesurons séparément les performances de pré-remplissage et de décodage. Le débit de pré-remplissage indique la rapidité avec laquelle le moteur traite le prompt ; le débit de décodage indique la rapidité avec laquelle il génère des tokens de sortie.

Nous évaluons Qwen3.6-35B-A3B sur un unique MacBook Pro équipé d’une puce M5 Max avec un GPU à 40 cœurs et 128 Go de mémoire unifiée. Sur dix longueurs de prompt pour le pré-remplissage et dix longueurs de contexte pour le décodage, de 256 à 128K tokens (K = 1 024), le moteur atteint en moyenne un débit de pré-remplissage de 1,23× supérieur à celui de MLX-LM et un débit de décodage de 1,35× supérieur. Avec un prompt de 4K tokens et un contexte de décodage de 4K tokens, le moteur personnalisé atteint 5 749,9 tokens de pré-remplissage par seconde et 186,6 tokens de décodage par seconde, contre 4 737,5 et 140,9 pour MLX-LM. Au cours d’une Session multi-tour, ces gains de temps s’accumulent à chaque appel de modèle supplémentaire.

Débit moyen arithmétique sur dix longueurs équipondérées de 256 à 128 Ko de jetons. Lily atteint en moyenne 4 156 jetons de pré-remplissage/s contre 3 388 pour MLX-LM (1,23×), et 170,0 jetons de décodage/s contre 126,4 (1,35×). Le pré-remplissage varie selon la longueur de l'invite ; le décodage varie selon la longueur du contexte.
Débit moyen arithmétique sur dix longueurs équipondérées de 256 à 128K tokens. Lily atteint en moyenne 4 156 tokens de pré-remplissage/s contre 3 388 pour MLX-LM (1,23×), et 170,0 tokens de décodage/s contre 126,4 (1,35×). Le pré-remplissage fait varier la longueur du prompt ; le décodage fait varier la longueur du contexte.

Ensuite, nous expliquons comment l’architecture de Qwen crée des opportunités d’optimisation spécifiques au modèle sur Apple Silicon. Nous passons ensuite en revue les modifications de pré-remplissage et de décodage qui en résultent. Nous abordons également le moment où l’optimisation supplémentaire cesse d’être rentable, avant de conclure par une comparaison de bout en bout avec MLX-LM.

Opportunités d’optimisation spécifiques à Qwen sur Apple Silicon

Qwen crée trois formes de charges de travail distinctes

Qwen3.6-35B-A3B contient 35 milliards de paramètres mais n’en active qu’environ 3 milliards pour chaque token. Un routeur attribue des scores à 256 sous-réseaux d’experts et en sélectionne huit, en plus d’un expert partagé qui traite chaque token. Cette conception MoE creuse réduit les calculs mais produit un travail inégal : les experts reçoivent un nombre variable de tokens, et chaque token nécessite des poids provenant d’une combinaison différente d’experts.

Qwen associe également 10 couches d’attention complète à 30 couches Gated DeltaNet. Ces deux types de couches conservent les informations antérieures de différentes manières.

Les couches d’attention utilisent l’attention à requêtes groupées (GQA). Qwen possède 16 têtes de requête et deux têtes clé-valeur (KV), huit têtes de requête partageant chaque tête KV. Le partage rend le cache KV plus petit et permet de réutiliser les données mises en cache entre les têtes de requête. Le cache stocke toujours de nouvelles clés et valeurs pour chaque token, de sorte que chaque étape de décodage lit davantage de données à mesure que le contexte s’étend.

Gated DeltaNet compresse à la place les informations antérieures dans un état récurrent de taille fixe. Une porte apprise contrôle la quantité d’état existant à conserver, tandis qu’une mise à jour delta intègre les informations du token actuel. Le modèle définit ces mises à jour de manière récurrente, de sorte que chaque token dépend de l’état produit par le token précédent. Lors du pré-remplissage, cependant, un moteur peut évaluer le même calcul de deux manières. Il peut parcourir directement les tokens tout en faisant avancer l’état, ou réorganiser les mises à jour en blocs qui exposent davantage d’opérations matricielles et de parallélisme au niveau des tokens. L’approche la plus rapide dépend des dimensions du modèle, de la charge de travail et du matériel.

Ensemble, ces structures créent trois motifs de calcul : des groupes d’experts inégaux, une attention sur un cache en expansion et une récurrence à taille fixe qui peut être évaluée directement ou par blocs.

Apple Silicon offre différents chemins pour différentes charges de travail

Le pré-remplissage traite simultanément de nombreuses lignes d’activation de tokens de prompt. La charge de travail locale considérée ici décode généralement une requête à la fois (lot 1) et traite une nouvelle ligne par étape. Cette différence modifie la façon dont les mêmes poids de modèle sont utilisés. Le pré-remplissage peut réutiliser chaque bloc de poids sur des centaines ou des milliers de lignes. Le décodage le peut en grande partie pas, dans la mesure où chaque nouveau token nécessite un nouveau passage à travers les poids.

Apple Silicon place le CPU et le GPU derrière une mémoire unifiée, un pool de mémoire physique unique accessible aux deux. Cela permet au modèle de rester résident sans maintenir de copie GPU séparée, mais ne rend pas le mouvement des données gratuit. La lecture des poids et des valeurs intermédiaires consomme toujours de la bande passante mémoire, tandis que les registres et autres stockages sur puce sont plus rapides mais beaucoup plus petits.

Le GPU M5 fournit également différents chemins de calcul. Les couches linéaires du pré-remplissage utilisent la multiplication générale de matrices (GEMM), appliquant une matrice de poids à de nombreuses lignes à la fois. Les GEMM compatibles peuvent utiliser l’accélérateur neuronal de chaque cœur GPU via les opérations tensorielles Metal 4. Le décodage par lots de 1 utilise à la place la multiplication générale matrice-vecteur (GEMV), appliquant les mêmes poids à une seule ligne. Avec peu de réutilisation des poids, le GEMV est principalement limité par la bande passante de la mémoire et convient mieux aux unités logiques arithmétiques (ALU) vectorielles du GPU qu’aux accélérateurs neuronaux conçus pour des opérations matricielles présentant une plus grande réutilisation des données.

Ces chemins d’exécution ne sont pas propres à Lily. MLX fonctionne sur la même mémoire unifiée et sélectionne des noyaux matriciels et vectoriels optimisés en fonction de la forme de la charge de travail. L’implémentation de Qwen de MLX-LM regroupe déjà le travail des experts, évalue Gated DeltaNet avec un noyau Metal récurrent fusionné et utilise l’attention consciente du GQA. Ces capacités constituent le point de départ partagé pour une inférence Qwen efficace sur Apple Silicon.

Stratégie d’optimisation

La portée plus étroite de Lily lui permet de coordonner ces chemins d’exécution partagés autour de l’architecture et des dimensions exactes de Qwen. Il utilise des chemins GPU spécifiques à chaque phase, associe les charges de travail d’experts, récurrentes et d’attention de Qwen pour minimiser les transferts de données, et sélectionne les noyaux et dispositions en fonction de la forme de la charge de travail mesurée. La stratégie comporte trois volets :

  1. Adapter le chemin du GPU à la phase d’inférence. Utiliser une exécution orientée matrice lorsque le pré-remplissage peut réutiliser les poids sur de nombreuses lignes, et une exécution orientée vecteur lorsque le décodage par lots de 1 traite une ligne à la fois.
  2. Mettre en correspondance la structure de Qwen sur le GPU tout en minimisant les transferts de données. Conserver les poids compressés jusqu’à leur utilisation, organiser le travail des experts acheminés sans retourner au CPU, conserver l’état de Gated DeltaNet sur la puce lors de son balayage récurrent et réutiliser les données KV partagées par l’attention à requêtes groupées.
  3. Adapter les noyaux à la forme de la charge de travail. Au sein de chaque phase, sélectionner la taille des tuiles, les dispositions d’exécution et les chemins d’attention en fonction du nombre de lignes disponibles, de la distribution des lignes entre les experts, des dimensions de l’opération et de la longueur du contexte actuel.

Les sections suivantes expliquent ces choix. Pour les optimisations évaluées dans le cadre d’ablations comparables sur un M5 Max, nous estimons leurs effets en comparant des configurations de moteur par ailleurs identiques qui ne diffèrent que par l’optimisation étudiée. Étant donné que ces expériences comparent des versions de notre moteur entre elles, elles expliquent les mécanismes plutôt que de décomposer les résultats finaux par rapport à MLX-LM.

Pré-remplissage : réutiliser les poids et maintenir le routage sur le GPU

Le pré-remplissage expose de nombreuses lignes de tokens à la fois, mais Qwen achemine ces lignes de manière inégale entre les experts et met à jour l’état récurrent tout au long de la séquence. Ses optimisations se divisent en trois groupes : organiser le travail des experts creux autour des lignes acheminées, maintenir le balayage Gated DeltaNet sur la puce et diviser les longs prompts en fragments délimités.

Exécution et résidence des données pour un segment de pré-remplissage délimité à travers une couche Qwen. L'attention étend le cache KV, tandis que Gated DeltaNet transporte son état récurrent de travail dans les registres. Les métadonnées de routage des experts restent sur le GPU, les poids Q4 restent groupés jusqu'à ce qu'ils soient déquantifiés à l'intérieur du GEMM groupé, et les activations temporaires sont limitées au segment actuel.
Exécution et résidence des données pour un fragment de pré-remplissage délimité à travers une couche Qwen. L’attention étend le cache KV, tandis que Gated DeltaNet conserve son état récurrent de travail dans les registres. Les métadonnées de routage des experts restent sur le GPU, les poids Q4 restent empaquetés jusqu’à ce qu’ils soient déquantifiés au sein du GEMM groupé, et les activations temporaires sont limitées au fragment actuel.

Optimiser le calcul des experts creux

Déquantifier les poids lors de la multiplication matricielle

Le point de contrôle Qwen3.6-35B-A3B utilise une quantification affine par groupe sur 4 bits. Chaque poids est stocké sous la forme d’un code entier sur 4 bits, tandis que chaque groupe de 64 poids partage une échelle et un biais bfloat16 utilisés pour reconstruire ses valeurs. Cela réduit le modèle de 35 milliards de paramètres d’environ 70 Go de poids bfloat16 à un point de contrôle de 19,4 Go, ce qui permet de maintenir facilement le modèle résident sur le Mac.

L’opération tensorielle Metal 4 utilisée pour la multiplication matricielle consomme des opérandes bfloat16 plutôt que la représentation 4 bits empaquetée. Avant la multiplication, le GPU doit reconstruire les poids en bfloat16. Le GEMM groupé optimisé dans Lily effectue cette conversion une petite tuile de poids à la fois et conserve le résultat dans la mémoire de threadgroup sur la puce juste le temps de le multiplier par les lignes d’activation acheminées. L’accumulation utilise des virgules flottantes sur 32 bits, et la sortie est écrite en bfloat16. Le tableau complet des poids expansés n’est jamais créé dans la mémoire unifiée.

Dans l’ablation, la déquantification s’exécute comme une opération distincte : elle développe les poids 4 bits en un tableau bfloat16 dans la mémoire unifiée, après quoi le noyau matriciel relit ce tableau. Pour un prompt de 512 tokens, l’intégration de la déquantification dans le GEMM groupé a augmenté le débit de pré-remplissage de bout en bout de 77,4 % en éliminant cette écriture et cette lecture intermédiaires.

Maintenir le routage des experts sur le GPU

Le GEMM groupé nécessite que les lignes d’activation attribuées à chaque expert soient stockées ensemble. Après avoir sélectionné huit experts par token, un histogramme compte le nombre d’attributions reçues par chaque expert. Un balayage de préfixe transforme ces comptes en décalages de départ, une étape de dispersion place les lignes dans leurs groupes d’experts, et une table de blocs répertorie les blocs matriciels de taille fixe que le GEMM groupé doit traiter.

Le chemin optimisé conserve toute cette séquence dans un seul tampon de commande, un lot ordonné d’opérations GPU, pour chaque fragment de prompt. Une ablation fait en revanche une pause afin que le CPU puisse inspecter les intermédiaires de routage et soumettre l’opération suivante. Le maintien de l’histogramme et du balayage de préfixe sur le GPU ajoute deux noyaux mais supprime la synchronisation CPU-GPU au sein de chaque couche MoE.

Pour un prompt de 512 tokens, l’activation du routage résidant sur le GPU a augmenté le pré-remplissage de bout en bout de 89 %. Cela montre également pourquoi le seul nombre de noyaux peut être trompeur : le chemin le plus rapide lance un plus grand nombre de noyaux mais n’attend jamais le CPU à l’intérieur de la couche.

Adapter la taille des tuiles à la charge des experts

Pour un prompt de 2K tokens, acheminer chaque token vers huit des 256 experts produit 16 384 attributions token-expert, soit une moyenne de 64 lignes d’activation par expert. La distribution réelle est inégale : certains experts reçoivent de nombreuses lignes, tandis que d’autres en reçoivent peu.

Le GEMM groupé divise la sortie de chaque expert en tuiles, qui sont de petits blocs rectangulaires de la sortie d’une multiplication matricielle. Chaque tuile est affectée à un threadgroup de GPU. Sur les GPU d’Apple Silicon, un threadgroup contient un ou plusieurs simdsgroups, chacun étant constitué de 32 fils d’exécution qui exécutent des instructions en synchronisme strict.

Des tuiles plus grandes répartissent le coût de configuration sur un plus grand nombre de lignes et exposent davantage de travail en parallèle, mais une partie d’une grande tuile reste inactive lorsqu’un expert ne reçoit que quelques lignes. La taille des tuiles et le nombre de simdgroups sont par conséquent liés.

Une ablation fixe la tuile à 16 lignes. Par rapport à ce témoin, l’activation de la tuile de 32 lignes avec quatre simdsgroups a amélioré le pré-remplissage de bout en bout de 13,2 % à 2K tokens.

Maintenir l’état récurrent sur la puce

Pendant le pré-remplissage, chaque couche Gated DeltaNet balaie le prompt dans l’ordre tout en portant son état récurrent vers l’avant. Lorsque la résidence dans les registres est désactivée, l’ablation utilise un balayage par blocs. Pour un prompt de 2K tokens, ce chemin déplace 256 Mio (mébioctets) d’état par couche et arrête de manière répétée les fils d’exécution coopératifs au niveau de barrières, c’est-à-dire des points de synchronisation où tous les fils d’exécution participants doivent s’attendre mutuellement.

L’état récurrent est une matrice. Le noyau optimisé attribue chaque colonne à un simdgroup. Le simdgroup divise la colonne entre ses fils d’exécution, charge la colonne dans leurs registres une seule fois et transporte l’état tout au long du balayage. Les fils d’exécution échangent les résultats intermédiaires par le biais d’opérations de simdgroup plutôt que via la mémoire de threadgroup, un stockage sur puce partagé entre les threadgroups. L’état final n’est réécrit qu’à l’issue du balayage.

L’état et sa porte utilisent un format à virgule flottante sur 32 bits, car les petites erreurs d’arrondi s’accumulent au fil des mises à jour séquentielles. Les activations de requête et de clé restent en bfloat16.

Pour un prompt de 2K tokens, l’activation du balayage résidant dans les registres a amélioré le pré-remplissage de bout en bout de 5,6 %. Les GEMM d’experts ont représenté environ 90 % du temps de pré-remplissage. Le balayage séquentiel n’expose pas suffisamment de travail matriciel réutilisable pour tirer parti des accélérateurs neuronaux.

Limiter la mémoire temporaire grâce au découpage des prompts

Le runtime traite un long prompt comme une séquence de fragments délimités plutôt que de conserver en mémoire des données temporaires pour chaque token du prompt en même temps. Les poids du modèle restent résidents dans la mémoire unifiée, tandis que l’état récurrent et le cache KV transportent le contexte d’un fragment au suivant. Aucun contexte antérieur n’est rejeté.

Sans découpage, les tableaux d’activation temporaires grandissent avec l’ensemble du prompt et entrent en concurrence avec les poids du modèle, l’état récurrent et le cache KV pour la mémoire unifiée. Le découpage ne maintient en vie les valeurs temporaires que d’un seul segment à la fois, puis libère ou réutilise ce stockage avant de traiter le segment suivant. Cela plafonne la mémoire de travail de pointe et permet au moteur de traiter des prompts plus longs sans modifier la sortie du modèle.

Le pré-remplissage par fragments est populaire dans de nombreux moteurs et s’avère crucial pour servir de longues trajectoires multi-tours dans ces environnements contraints en mémoire. Le temps total de pré-remplissage pour les couches d’attention reste quadratique par rapport à la longueur du prompt, avec une surcharge supplémentaire provenant des chargements KV répétés des fragments précédents.

Décodage : minimiser les octets déplacés par token

Le décodage par lots de 1 traite une nouvelle ligne à la fois. Avec peu de réutilisation des poids, son débit dépend principalement du nombre d’octets que le moteur déplace pour chaque token. Les modifications apportées au décodage se répartissent en quatre groupes : optimiser le chemin des poids à ligne unique, maintenir chaque étape sur le GPU, réduire le trafic intermédiaire et d’état, et lire efficacement le cache d’attention.

Flux de données pour une étape de décodage par lot de 1 et deux mécanismes qui réduisent le temps d'inactivité et le trafic de cache. (A) Le GPU transmet en continu les poids Q4 et l'état du modèle via l'attention, Gated DeltaNet, le routage et les noyaux d'experts fusionnés, puis écrit le jeton sélectionné directement dans l'emplacement d'entrée de l'étape suivante tout en envoyant une copie au processeur. (B) L'ordonnancement tenant compte des dépendances permet aux noyaux indépendants de se chevaucher. (C) Le regroupement GQA permet à quatre têtes de requête de partager chaque chargement de ligne KV, réduisant huit requêtes indépendantes à deux chargements partagés.
Flux de données pour une étape de décodage par lots de 1 et deux mécanismes qui réduisent le temps d’inactivité et le trafic du cache. (A) Le GPU diffuse les poids Q4 et l’état du modèle via l’attention, Gated DeltaNet, le routage et les noyaux d’experts fusionnés, puis écrit le token sélectionné directement dans l’emplacement d’entrée de l’étape suivante tout en envoyant une copie au CPU. (B) L’ordonnancement tenant compte des dépendances permet aux noyaux indépendants de se chevaucher. (C) Le regroupement GQA permet à quatre têtes de requête de partager chaque chargement de ligne KV, réduisant huit requêtes indépendantes à deux chargements partagés.

Optimiser le chemin des poids à ligne unique

MLX répartit déjà le travail sur une seule ligne vers des noyaux matrice-vecteur spécialisés. Étant donné que Lily n’utilse pas MLX, le runtime personnalisé doit fournir la même stratégie de base. Notre GEMV parallèle par ligne est conçu pour une ligne d’activation. Un simdgroup coopère sur la sortie tout en lisant différentes parties de la matrice de poids en parallèle.

Maintenir chaque étape de décodage sur le GPU

Maintenir le transfert des tokens sur le GPU

Chaque étape de décodage se termine par la sélection du token suivant ; l’étape suivante commence avec ce token comme entrée. L’envoi de la sélection au CPU puis de nouveau au GPU ajoute un point de synchronisation à chaque token. Notre runtime alterne à la place entre deux tampons de commande et deux emplacements de tokens résidents sur le GPU. Le GPU sélectionne le token ayant le score le plus élevé et écrit son ID de token directement dans l’emplacement d’entrée pour l’étape de décodage suivante, tandis que le CPU prépare le travail ultérieur.

Faire se chevaucher le travail GPU indépendant

Au cours d’une étape de décodage par lots de 1 enregistrée, la génération d’un token a lancé 795 noyaux GPU. Leurs dépendances ont formé 555 étapes séquentielles, laissant certains noyaux libres de s’exécuter simultanément. Le mode d’exécution sérielle de Metal a néanmoins exécuté chaque noyau dans l’ordre.

Le chemin de décodage optimisé enregistre les véritables dépendances de données dans une passe Metal concurrente. Les lancements de noyaux indépendants peuvent s’exécuter en même temps lorsque les ressources du GPU le permettent. Une barrière n’est insérée que lorsque les travaux ultérieurs nécessitent un résultat antérieur.

Réduire le trafic intermédiaire et d’état

Des noyaux distincts matérialisent souvent un intermédiaire : un noyau écrit un résultat temporaire en mémoire, et le suivant relit le résultat. Le chemin de décodage optimisé fusionne quatre chaînes : les deux projections d’entrée des experts avec leur activation déclenchée ; la projection de sortie de l’expert avec son score de routage et le résultat de l’expert partagé ; la préparation des requêtes et des clés avant l’attention ; et la mise à jour récurrente avec sa normalisation. Chaque noyau fusionné conserve les valeurs temporaires dans des registres au lieu de les envoyer par la mémoire.

La fusion raccourcit également le graphe de dépendances : lorsqu’une écriture intermédiaire disparaît, il en va de même pour la barrière qui protégeait son consommateur.

Lire efficacement le cache d’attention

Fusionner les lectures du cache d’attention

L’attention lit les clés et les valeurs à partir du cache KV lors de chaque étape de décodage. Dans l’ablation, les fils d’exécution voisins du GPU ne demandent pas toujours des octets voisins, ce qui oblige le système de mémoire à traiter un plus grand nombre de transactions distinctes. L’activation des chargements fusionnés amène les fils d’exécution adjacents à demander des octets adjacents afin que le matériel puisse combiner leurs lectures.

Sur la configuration bfloat16, la fusion a fait passer la bande passante des clés de 33,8 à 47,9 Go/s, celle des valeurs de 42,0 à 61,8 Go/s, et a amélioré le décodage de bout en bout de 2,1 % pour un contexte de 3 840 tokens.

Empaqueter les têtes de requête pour réutiliser les lignes KV

L’attention à requêtes groupées permet à huit têtes de requête de partager une tête KV. Lors de l’ablation, chaque tête de requête s’exécute dans un simdgroup distinct, de sorte que les huit demandent indépendamment la même ligne KV mise en cache. Le noyau optimisé regroupe quatre têtes de requête dans un threadgroup, qui charge chaque ligne KV une seule fois et la réutilise pour quatre calculs d’attention. Un second threadgroup gère les quatre têtes restantes.

Cette technique, communément appelée empilement GQA, effectue la même arithmétique et produit des octets de sortie identiques tout en réduisant huit requêtes KV indépendantes à deux chargements partagés. Par rapport à l’ablation non empaquetée, elle a amélioré le débit de décodage de bout en bout de 23,8 % pour un contexte de 32K tokens.

Changer de disposition d’attention pour les contextes longs

Chaque étape de décodage dans une couche à attention complète balaie le cache KV existant. Une disposition en blocs fixes divise ce cache en parties égales que le GPU peut traiter en parallèle. Son ordonnancement supplémentaire n’est pas intéressant lorsque le cache est petit, mais la disposition en blocs fixes répartit le travail de manière plus homogène à mesure que le contexte s’étend.

Pour ce modèle, le runtime maintient le chemin d’attention général en dessous de 32K tokens et utilise le chemin à blocs fixes à partir de 32K tokens. Le basculement s’applique lorsque chaque tête comporte 256 valeurs et que huit têtes de requête partagent une tête KV ; les autres formes restent sur le chemin général. Une ablation désactive ce basculement et utilise toujours le chemin général. L’activation de l’acheminement par blocs fixes a amélioré le décodage de bout en bout de 7,7 % à 32K, de 27,4 % à 64K et de 40,2 % à 128K.

Limites d’une optimisation ultérieure

Certaines modifications ont amélioré une opération isolée mais n’ont pas amélioré l’inférence de bout en bout.

Le décodage spéculatif, qui utilise un modèle plus petit pour proposer des tokens que le modèle complet doit vérifier, a rendu le décodage par lots de 1 18 % plus lent. La vérification a traité des groupes de deux à cinq lignes, une forme inefficace pour ce matériel, et les lignes ont souvent sélectionné différents experts, augmentant la quantité de données de poids d’experts lues. La réduction du vocabulaire de sortie du générateur a amélioré le débit du générateur de 4,7–5,1 %, mais n’a pas rendu la boucle spéculative complète plus rapide. Ce résultat dépend de la charge de travail : notre déploiement de Qwen par lots sur Blackwell utilise le décodage spéculatif dans des conditions différentes.

D’autres expériences ont consisté à réduire les lancements de GPU, à chevaucher des phases entières, à utiliser de plus grandes tuiles de pré-remplissage, à appliquer une fusion plus large, à accélérer le routeur et à combiner la projection de sortie avec la sélection de tokens. Aucune n’a amélioré la boucle d’inférence complète.

Les mesures des limites matérielles ont également montré peu de marge restante dans les principales opérations de pré-remplissage et de décodage. Les GEMM et GEMV MoE ont atteint 97,9 % et 90,3 % des vitesses de lecture de poids soutenues les plus rapides pour leurs schémas d’accès. La suppression de l’arithmétique du GEMV creux n’a modifié le débit que de 0,2 %, confirmant que les lectures de poids, plutôt que les calculs, constituaient la ressource limitante. La multiplication matricielle du pré-remplissage a de même atteint 93 % de la limite matricielle théorique de manière isolée et 80–86 % au sein des modèles testés.

Performances de bout en bout

La comparaison de bout en bout charge des octets de point de contrôle 4 bits identiques dans les deux moteurs et exécute une requête à la fois sur un M5 Max à 40 cœurs et 128 Go. Au sein de chaque cycle, les deux moteurs s’exécutent dans un ordre alterné pour réduire les biais liés à la charge de fond et aux variations de température de la puce. Nous effectuons la comparaison par rapport au chemin de génération directe le plus rapide de MLX-LM, et non par rapport à son serveur, de sorte que la mesure se concentre sur l’exécution du modèle plutôt que sur les surcharges de service.

Le balayage couvre dix longueurs de prompt pour le pré-remplissage et dix longueurs de contexte pour le décodage, de 256 à 128K tokens. Le débit de pré-remplissage augmente d’abord à mesure que le moteur répartit les coûts de configuration fixes sur un plus grand nombre de tokens. Le pré-remplissage atteint un pic autour d’un prompt de 4K tokens, puis chute parce que les dix couches d’attention complète effectuent davantage de travail à mesure que le prompt s’allonge. Le décodage reste presque plat pour les contextes courts et diminue dès lors que la lecture du cache KV en expansion devient importante. Le moteur personnalisé est plus rapide à chaque longueur enregistrée.

Étant donné que l’exécution spécialisée peut modifier l’ordre des opérations en virgule flottante, nous avons également vérifié la cohérence numérique par rapport à MLX-LM. Dans une comparaison guidée par un enseignant, les deux moteurs ont prédit le token suivant à partir du même préfixe de référence à chacun des 192 emplacements, empêchant les différences antérieures d’affecter les entrées ultérieures. La perplexité de Lily n’était que de 0,04 % supérieure, et il a sélectionné le même token le mieux classé à 96,35 % des emplacements testés.

Débit de pré-remplissage par longueur d'invite et débit de décodage par longueur de contexte pour Qwen3.6-35B-A3B Q4, lot 1, sur un M5 Max à 40 cœurs et 128 Go. Sur dix longueurs de 256 à 128 Ko de jetons, Lily est plus rapide à chaque point enregistré : 1,12–1,42× le débit de pré-remplissage de MLX-LM et 1,31–1,37× son débit de décodage. La comparaison utilise le chemin de génération directe le plus rapide de MLX-LM. Les deux axes horizontaux utilisent des échelles logarithmiques ; aucun axe vertical ne commence à zéro.
Débit de pré-remplissage par longueur de prompt et débit de décodage par longueur de contexte pour Qwen3.6-35B-A3B Q4, lot 1, sur un M5 Max à 40 cœurs et 128 Go. Sur dix longueurs de 256 à 128K tokens, Lily est plus rapide à chaque point enregistré : 1,12–1,42× le débit de pré-remplissage de MLX-LM et 1,31–1,37× son débit de décodage. La comparaison utilise le chemin de génération directe le plus rapide de MLX-LM. Les deux axes horizontaux utilisent des échelles logarithmiques ; aucun axe vertical ne commence à zéro.

Conçu pour la plateforme locale

Apple Silicon n’est pas un GPU de centre de données réduit. Il s’agit d’une plateforme d’inférence locale complète dotée de ses propres caractéristiques matérielles et logicielles. La mémoire unifiée offre à un nœud unique un plafond très élevé quant à la quantité de modèles et d’états qu’il peut contenir. Les accélérateurs neuronaux M5 absorbent le travail matriciel dense lors du pré-remplissage. Les ALU vectorielles gèrent le reste, limité par la bande passante et à faible réutilisation, lors du décodage.

Qwen offre de nouvelles opportunités de spécialisation : maintenir le routage des experts et l’état récurrent sur le GPU, éliminer les intermédiaires inutiles, superposer le travail indépendant, réutiliser les données KV partagées et adapter les noyaux à la forme de la charge de travail.

Grâce à une optimisation spécifique au modèle et à la plateforme, un seul Mac peut exécuter efficacement un grand modèle creux. Les travaux futurs élargiront la couverture à l’ensemble des modèles, des puces et des charges de travail de service, et transformeront les mécanismes validés ici sur une seule configuration en une politique de runtime plus générale.

Le principe plus large consiste à adapter le moteur à la fois à l’architecture du modèle et aux chemins spécifiques de calcul et de mémoire du matériel. À mesure que les modèles ouverts de pointe et le matériel évoluent, l’inférence locale haute performance dépendra de plus en plus de moteurs adaptés aux deux plutôt que de moteurs qui font abstraction de leurs différences.