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.
Le calcul hybride sur la puce Apple silicon orchestre une tâche entre une intelligence de pointe dans le cloud et un modèle local sur le Mac. Les modèles cloud gèrent la recherche et le raisonnement, tandis qu'un modèle local travaille avec des fichiers et des applications privés sur le Mac.
Pour que cette division du travail semble 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 consignes et de maintenir un taux élevé de génération de jetons.
Lily, notre moteur d'inférence locale léger, est conçu spécifiquement pour la puce 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 ouvert.
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 la puce Apple silicon. Sa bibliothèque complémentaire, 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 fournissent une pile prête à l'emploi et polyvalente pour l'inférence de LLM locaux.
Qwen3.6-35B-A3B est un modèle hybride creux : il utilise le routage par mélange d'experts (MoE) et associe des états récurrents de 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 jetons sont acheminés vers différents poids d'experts et les états récurrents sont de 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, le mouvement des données et la planification autour de la structure fixe du modèle.
Lily implémente cette spécialisation de bout en bout dans un processus unique. Un runtime Rust charge le point de contrôle du modèle et gère l'état de la session ainsi que la boucle de génération, une API de complats de chat compatible avec OpenAI accepte les requêtes et transmet les jetons en flux continus, et des noyaux Metal personnalisés exécutent des opérations spécifiques à Qwen. Ni PyTorch ni MLX ne se trouvent sur le chemin d'exécution.

Nous mesurons séparément les performances de pré-remplissage et de décodage. Le débit de pré-remplissage mesure la vitesse à laquelle le moteur traite la consigne ; le débit de décodage mesure la vitesse à laquelle il génère des jetons de sortie.
Nous avons évalué Qwen3.6-35B-A3B sur un seul MacBook Pro doté d'une puce M5 Max avec un processeur graphique (GPU) à 40 cœurs et 128 Go de mémoire unifiée. Sur dix longueurs de consignes pour le pré-remplissage et dix longueurs de contexte pour le décodage, allant de 256 à 128 Ko de jetons (Ko = 1 024), le moteur atteint en moyenne 1,23× le débit de pré-remplissage de MLX-LM et 1,35× son débit de décodage. Pour une consigne de 4 Ko de jetons et un contexte de décodage de 4 Ko de jetons, le moteur personnalisé atteint 5 749,9 jetons de pré-remplissage par seconde et 186,6 jetons de décodage par seconde, contre 4 737,5 et 140,9 pour MLX-LM. Au cours d'une session à tours multiples, ces gains de temps s'accumulent à chaque appel de modèle supplémentaire.

Nous expliquons ensuite comment l'architecture de Qwen crée des opportunités d'optimisation spécifiques au modèle sur la puce 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 la puce 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 jeton. Un routeur attribue des scores à 256 sous-réseaux d'experts et en sélectionne huit, ainsi qu'un expert partagé qui traite chaque jeton. Cette conception MoE creuse réduit les calculs mais produit un travail inégal : les experts reçoivent un nombre variable de jetons et chaque jeton nécessite des poids provenant d'une combinaison différente d'experts.
Qwen associe également 10 couches à attention complète à 30 couches Gated DeltaNet. Ces deux types de couches retiennent 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 de 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 jeton, de sorte que chaque étape de décodage lit davantage de données à mesure que le contexte grandit.
Gated DeltaNet compresse plutôt 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 incorpore les informations du jeton actuel. Le modèle définit ces mises à jour de manière récurrente, de sorte que chaque jeton dépend de l'état produit par le jeton précédent. Lors du pré-remplissage, cependant, un moteur peut évaluer le même calcul de deux manières. Il peut analyser les jetons directement tout en transportant l'état vers l'avant, ou réorganiser les mises à jour en blocs qui exposent davantage d'opérations matricielles et de parallélisme au niveau des jetons. 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 schémas de calcul : des groupes d'experts inégaux, l'attention sur un cache en expansion et une récurrence à taille fixe qui peut être évaluée directement ou par blocs.
La puce Apple silicon fournit différents chemins pour différentes charges de travail
Le pré-remplissage traite un grand nombre de lignes d'activation de jetons de consigne en même temps. La charge de travail locale examiné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 ne le peut généralement pas, car chaque nouveau jeton nécessite un nouveau passage à travers les poids.
La puce Apple silicon place le processeur et le GPU derrière la mémoire unifiée, un espace de mémoire physique unique accessible aux deux. Cela permet au modèle de rester résident sans maintenir de copie GPU distincte, mais cela 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 les 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 matrice-matrice (GEMM), appliquant une matrice de poids à de nombreuses lignes à la fois. Les opérations GEMM compatibles peuvent utiliser l'accélérateur neuronal (Neural Accelerator) de chaque cœur de GPU par l'intermédiaire des opérations de tenseur Metal 4. Le décodage de lot 1 utilise plutôt la multiplication générale matrice-vecteur (GEMV), appliquant les mêmes poids à une seule ligne. Avec peu de réutilisation des poids, l'opération GEMV est limitée principalement par la bande passante de la mémoire et est mieux adaptée 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 exclusifs à 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 tenant compte de GQA. Ces fonctionnalités constituent le point de départ partagé pour une inférence Qwen efficace sur la puce Apple silicon.
Stratégie d'optimisation
La portée plus restreinte 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 le mouvement des données, et sélectionne les noyaux et les dispositions en fonction de la forme mesurée de la charge de travail. La stratégie comporte trois volets :
- Faire correspondre 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 un grand nombre de lignes, et une exécution orientée vecteur lorsque le décodage de lot 1 traite une ligne à la fois.
- Mapper la structure de Qwen sur le GPU tout en minimisant le mouvement des données. Conserver les poids compressés jusqu'à ce qu'ils soient utilisés, organiser le travail des experts acheminés sans retourner au processeur, conserver l'état de Gated DeltaNet sur la puce tout au long de son analyse récurrente et réutiliser les données KV partagées par l'attention à requêtes groupées.
- Adapter les noyaux à la forme de la charge de travail. Au sein de chaque phase, sélectionnez la taille des vignettes, les dispositions d'exécution et les chemins d'attention parmi le nombre de lignes disponibles, la distribution des lignes entre les experts, les dimensions de l'opération et la longueur actuelle du contexte.
Les sections suivantes expliquent ces choix. Pour les optimisations évaluées dans le cadre d'ablations appariées sur une puce M5 Max, nous estimons leurs effets en comparant des configurations de moteurs 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 conserver le routage sur le GPU
Le pré-remplissage expose de nombreuses lignes de jetons à 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 d'experts creux autour des lignes acheminées, conserver l'analyse Gated DeltaNet sur la puce et diviser les consignes longues en blocs délimités.

Optimiser le calcul d'experts creux
Désquantifier les poids pendant la multiplication matricielle
Le point de contrôle Qwen3.6-35B-A3B utilise une quantification affine par groupes de 4 bits. Chaque poids est stocké sous la forme d'un code entier de 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, qui passe d'environ 70 Go de poids bfloat16 à un point de contrôle de 19,4 Go, ce qui permet de conserver facilement le modèle résident sur le Mac.
L'opération de tenseur Metal 4 utilisée pour la multiplication matricielle consomme des opérandes bfloat16 plutôt que la représentation empaquetée de 4 bits. Avant la multiplication, le GPU doit reconstruire les poids en bfloat16. Le GEMM groupé optimisé de Lily effectue cette conversion une petite vignette de poids à la fois et conserve le résultat dans la mémoire de threadgroup sur puce juste assez longtemps pour le multiplier par les lignes d'activation acheminées. L'accumulation utilise la virgule flottante sur 32 bits et la sortie est écrite en bfloat16. Le tableau complet des poids expansés n'est jamais créé en mémoire unifiée.
Dans l'ablation, la désquantification s'exécute comme une opération distincte : elle étend les poids de 4 bits en un tableau bfloat16 dans la mémoire unifiée, après quoi le noyau matriciel relit ce tableau. Pour une consigne de 512 jetons, l'intégration de la désquantification dans le GEMM groupé a amélioré le débit de pré-remplissage de bout en bout de 77,4 % en éliminant cette écriture et cette lecture intermédiaires.
Conserver le routage des experts sur le GPU
Le GEMM groupé nécessite que les lignes d'activation assignées à chaque expert soient stockées ensemble. Après avoir sélectionné huit experts par jeton, un histogramme compte le nombre d'attributions reçues par chaque expert. Une analyse 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 un mappage de blocs répertorie les blocs matriciels de taille fixe que le GEMM groupé doit traiter.
Le chemin optimisé conserve l'intégralité de cette séquence dans un seul tampon de commandes (command buffer), un lot ordonné d'opérations GPU, pour chaque bloc de consigne. Une ablation fait plutôt une pause pour permettre au processeur d'inspecter les intermédiaires de routage et de soumettre l'opération suivante. Le maintien de l'histogramme et de l'analyse de préfixe sur le GPU ajoute deux noyaux mais supprime la synchronisation processeur-GPU au sein de chaque couche MoE.
Pour une consigne de 512 jetons, l'activation du routage résident sur le GPU a augmenté le pré-remplissage de bout en bout de 89 %. Cela montre également pourquoi le nombre de noyaux à lui seul peut être trompeur : la route la plus rapide lance un plus grand nombre de noyaux mais n'attend jamais le processeur à l'intérieur de la couche.
Faire correspondre la taille de la vignette à la charge de l'expert
Pour une consigne de 2 Ko de jetons, l'acheminement de chaque jeton vers huit des 256 experts produit 16 384 attributions jeton-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 vignettes, qui sont de petits blocs rectangulaires de la sortie d'une multiplication matricielle. Chaque vignette est assignée à un threadgroup de GPU. Sur les GPU de la puce Apple silicon, un threadgroup contient un ou plusieurs simdgroups, chacun étant constitué de 32 threads qui exécutent des instructions en pas cadencé.
Des vignettes 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 vignette reste inactive lorsqu'un expert ne reçoit que quelques lignes. La taille des vignettes et le nombre de simdgroups sont par conséquent indissociables.
Une ablation fixe la vignette à 16 lignes. Par rapport à ce témoin, l'activation de la vignette de 32 lignes avec quatre simdgroups a amélioré le pré-remplissage de bout en bout de 13,2 % pour 2 Ko de jetons.
Conserver l'état récurrent sur la puce
Pendant le pré-remplissage, chaque couche Gated DeltaNet analyse la consigne dans l'ordre tout en transportant son état récurrent vers l'avant. La résidence dans les registres étant désactivée, l'ablation utilise une analyse par blocs. Pour une consigne de 2 Ko de jetons, ce chemin déplace 256 Mio (mébioctets) d'état par couche et arrête à plusieurs reprises les threads coopératifs au niveau de barrières, des points de synchronisation où tous les threads participants doivent s'attendre mutuellement.
L'état récurrent est une matrice. Le noyau optimisé assigne chaque colonne à un simdgroup. Le simdgroup divise la colonne entre ses threads, charge la colonne dans leurs registres une seule fois et propage l'état à travers l'analyse entière. Les threads échangent des résultats intermédiaires au moyen d'opérations de simdgroup plutôt que de mémoire de threadgroup, un espace de stockage sur puce partagé entre les threadgroups. L'état achevé n'est réécrit qu'après l'analyse.
L'état et sa porte utilisent un format à virgule flottante sur 32 bits, car de petites erreurs d'arrondi se cumulent lors des mises à jour séquentielles. Les activations de requête et de clé restent en bfloat16.
Pour une consigne de 2 Ko de jetons, l'activation de l'analyse résidente dans les registres a amélioré le pré-remplissage de bout en bout de 5,6 %. Les opérations GEMM d'experts ont représenté environ 90 % du temps de pré-remplissage. L'analyse séquentielle n'expose pas suffisamment de travail matriciel réutilisable pour tirer parti des accélérateurs neuronaux (Neural Accelerators).
Délimiter la mémoire temporaire avec le découpage des consignes en blocs
Le runtime traite une consigne longue sous la forme d'une séquence de blocs délimités plutôt que de conserver en mémoire des données temporaires pour chaque jeton de consigne 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 bloc à l'autre. Aucun contexte antérieur n'est rejeté.
Sans découpage en blocs, les tableaux d'activation temporaires augmentent avec la consigne complète 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 en blocs ne maintient en vie que les valeurs temporaires d'un seul segment à la fois, puis libère ou réutilise cet espace de stockage avant de traiter le segment suivant. Cela limite le pic de mémoire de travail et permet au moteur de traiter des consignes plus longues sans modifier la sortie du modèle.
Le pré-remplissage par blocs est populaire dans de nombreux moteurs et s'avère crucial pour traiter de longues trajectoires à tours multiples dans ces environnements à mémoire limitée. Le temps de pré-remplissage total pour les couches d'attention reste quadratique par rapport à la longueur de la consigne, avec un surcroît de travail provenant des charges KV répétées des blocs précédents.
Décodage : minimiser les octets déplacés par jeton
Le décodage de lot 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 jeton. Les modifications de décodage se divisent en quatre groupes : optimiser le chemin de poids à une ligne, conserver chaque étape sur le GPU, réduire le trafic intermédiaire et d'état, et lire efficacement le cache d'attention.

Optimiser le chemin de poids à une ligne
MLX répartit déjà le travail d'une seule ligne sur des noyaux matrice-vecteur spécialisés. Étant donné que Lily n'utilise pas MLX, le runtime personnalisé doit fournir la même stratégie de base. Notre opération GEMV parallèle aux lignes est conçue pour une seule ligne d'activation. Un simdgroup coopère sur la sortie tout en lisant différentes parties de la matrice de poids en parallèle.
Conserver chaque étape de décodage sur le GPU
Conserver le transfert des jetons sur le GPU
Chaque étape de décodage se termine par la sélection du jeton suivant ; l'étape suivante commence avec ce jeton comme entrée. L'envoi de la sélection au processeur, puis son renvoi au GPU, ajoute un point de synchronisation à chaque jeton. Notre runtime alterne plutôt entre deux tampons de commandes et deux emplacements de jetons résidents sur le GPU. Le GPU sélectionne le jeton ayant le score le plus élevé et écrit son identifiant de jeton directement dans l'emplacement d'entrée pour l'étape de décodage suivante, tandis que le processeur prépare le travail ultérieur.
Chevaucher le travail indépendant du GPU
Au cours d'une étape de décodage de lot 1 enregistrée, la génération d'un jeton a lancé 795 noyaux GPU. Leurs dépendances ont formé 555 étapes séquentielles, ce qui a permis à certains noyaux de s'exécuter simultanément. Le mode d'exécution en série de Metal a néanmoins exécuté chaque noyau dans l'ordre.
Le chemin de décodage optimisé enregistre les dépendances de données réelles lors d'une passe Metal concurrente. Des 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 des 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 d'experts avec leur activation régulée ; la projection de sortie d'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 l'intermédiaire de 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
Coalescer les lectures du cache d'attention
L'attention lit les clés et les valeurs du cache KV pendant chaque étape de décodage. Dans l'ablation, les threads GPU voisins ne demandent pas toujours des octets voisins, ce qui force le système de mémoire à traiter un plus grand nombre de transactions distinctes. L'activation des chargements coalescents amène les threads adjacents à demander des octets adjacents afin que le matériel puisse combiner leurs lectures.
Sur la configuration bfloat16, la coalescence a augmenté la bande passante des clés de 33,8 à 47,9 Go/s, a augmenté la bande passante 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 jetons.
Regrouper 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 têtes 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 dans quatre calculs d'attention. Un deuxième threadgroup gère les quatre têtes restantes.
Cette technique, communément appelée regroupement 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 32 Ko de jetons.
Basculer les dispositions d'attention pour les contextes longs
Chaque étape de décodage dans une couche à attention complète analyse le cache KV existant. Une disposition à blocs fixes divise ce cache en parties égales que le GPU peut traiter en parallèle. Sa planification supplémentaire ne vaut pas la peine lorsque le cache est de petite taille, mais la disposition à blocs fixes répartit le travail de manière plus uniforme à mesure que le contexte grandit.
Pour ce modèle, le runtime maintient le chemin d'attention général en dessous de 32 Ko de jetons et utilise le chemin à blocs fixes à partir de 32 Ko. Le basculement s'applique lorsque chaque tête possède 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 la route à blocs fixes a amélioré le décodage de bout en bout de 7,7 % à 32 Ko, de 27,4 % à 64 Ko et de 40,2 % à 128 Ko.
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 jetons que le modèle complet doit vérifier, a rendu le décodage de lot 1 plus lent de 18 %. 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é des experts différents, augmentant ainsi la quantité de données de poids d'experts lues. La réduction du vocabulaire de sortie du générateur a amélioré son débit de 4,7 à 5,1 %, mais n'a pas accéléré la boucle spéculative complète. Ce résultat est spécifique à la charge de travail : notre déploiement groupé de Qwen 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 des vignettes de pré-remplissage plus grandes, à appliquer une fusion plus large, à accélérer le routeur et à combiner la projection de sortie avec la sélection de jetons. Aucune n'a amélioré la boucle d'inférence complète.
Les mesures des limites matérielles ont également montré qu'il restait peu de marge de manœuvre dans les principales opérations de pré-remplissage et de décodage. Les opérations GEMM et GEMV MoE ont atteint respectivement 97,9 % et 90,3 % des vitesses de lecture de poids soutenues les plus rapides pour leurs schémas d'accès. La suppression des opérations arithmétiques de la GEMV creuse n'a modifié le débit que de 0,2 %, ce qui confirme que les lectures de poids constituaient la ressource limitante plutôt que les calculs. 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 % à l'intérieur des modèles testés.
Performance de bout en bout
La comparaison de bout en bout charge des octets de point de contrôle de 4 bits identiques dans les deux moteurs et exécute une requête à la fois sur une puce 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 d'arrière-plan 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 frais généraux de service.
Le balayage couvre dix longueurs de consignes pour le pré-remplissage et dix longueurs de contexte pour le décodage, de 256 à 128 Ko de jetons. 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 jetons. Le pré-remplissage atteint un pic autour d'une consigne de 4 Ko de jetons, puis chute parce que les dix couches à attention complète effectuent davantage de travail à mesure que la consigne 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 significative. Le moteur personnalisé est plus rapide pour 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 (teacher-forced), les deux moteurs ont prédit le jeton suivant à partir du même préfixe de référence à chacune des 192 positions, 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 le moteur a sélectionné le même jeton le mieux classé à 96,35 % des positions testées.

Conçu pour la plateforme locale
La puce 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 unités logiques arithmétiques (ALU) vectorielles gèrent le reste, limité par la bande passante et à faible taux de réutilisation.
Qwen offre d'autres opportunités de spécialisation : conserver le routage des experts et l'état récurrent sur le GPU, éliminer les intermédiaires inutiles, chevaucher 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 général 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 et le matériel ouverts de pointe évolueront, 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.