Les poids de Qwen3.8-27B sont arrivés le matin, et la même journée j'ai commencé une aventure avec Kimi K3 en mode haute réflexion. J'ai lancé tout cela avec une seule question : ce 27B dense frais — facilement le modèle ouvert le plus puissant de sa classe de taille — pourrait-il servir mes agents de codage à pleine vitesse sur un Mac Studio avec un M1 Ultra, 20 cœurs CPU, 128 GB de mémoire unifiée, et 800 GB/s de bande passante mémoire? La connaissance courante dit que les modèles denses décodent lentement sur Apple Silicon parce qu'ils sont limités par la bande passante mémoire, et je voulais savoir si cette quantité de RAM et de bande passante pouvait rompre le schéma. Les poids MLX de 4 bits sont arrivés à 15 GB, le cadre était à jour, rien d'autre ne tournait sur la machine, et le premier chiffre était 12.1 tokens par seconde. Cela semblait faux.
J'avais déjà envisagé ce type d'expérience depuis un moment. En mars, travaillant avec une autre IA dans mon laboratoire d'affinement autonome, nous avons tracé un mur de <20 tok/s pour les modèles 30B sur un M4 Max à cause de la famine de bande passante due au paging niveau OS — et cette enquête a laissé un playbook : powermetrics d'abord, échantillonnage par cluster, pinning mémoire avant de blâmer. Cette exécution a commencé par pure curiosité : j'avais entendu parler des optimisations de Qwen3.8, et je voulais savoir si elle fonctionnerait rapidement hors de la boîte sur cette machine avec autant de RAM. Ce n'est qu'à mi-experiment que la réalité limitée par la bande passante s'est fait sentir. Ce qui a suivi a été une série d'expériences contrôlées que nous avons conçues ensemble — Kimi a rédigé les hypothèses et lu les compteurs matériels à mes côtés tandis que je gardais mes mains sur le métal. Chaque expérience était construite pour acquitter ou condamner une seule couche de la pile. Les gains provenaient de 2 endroits que le folklore de l'inférence locale sous-évalue : indices du planificateur CPU et décodage spéculatif. Les alternatives 2 à la mode — une construction GGUF servie par llama.cpp, et un modèle mixture-of-experts qui semblait imbattable sur papier — ont toutes deux perdu sur la mesure. Cet article est le parcours complet de preuves que nous avons construit ensemble, car la méthode s'est avérée plus précieuse qu'un seul chiffre.

Le relais réseau et le serveur étaient les premiers suspects, et les deux étaient innocents

Le chemin de service avait 3 sauts : ma station de travail, un relais d'exécution basé sur SSH, et le serveur de modèle sur le loopback du Studio. Blâmer le relais aurait été facile, alors nous l'avons mesuré d'abord. Les propres temps du serveur indiquaient 12.1 tok/s décodage ; un benchmark de génération en processus brut, sans serveur et sans relais du tout, a produit 11.5 tok/s. Le transport coûtait environ 25% par surcharge de connexion, et le modèle lui-même était la couche lente.
Un bug de transport valait la peine d'être corrigé de toute façon, et c'est le genre de détail qui coûte des heures si vous ne l'avez jamais vu : un relais Python TCP qui utilise un read(65536) tamponné se bloque contre un protocole local bavard, parce que le lecteur tamponné attend un tampon plein ou EOF avant de retourner. Passer à read1(), qui retourne après une seule lecture sous-jacente, a fait fonctionner le relais. Le symptôme ressemblait à un blocage réseau ; la cause était la sémantique stdio.
Avec le transport acquitté, nous avons travaillé à travers la couche sysctl habituelle. Élever la limite de mémoire filaire GPU (iogpu.wired_limit_mb) n'a rien changé avec 128 GB de RAM libre, alors nous l'avons révoqué. Le processus Python était natif arm64, MLX rapportait le GPU comme son appareil par défaut, et powermetrics montrait le GPU à 54-62% de résidence active consommant 20-23 W pendant le décodage. Chaque suspect de cette ronde est parti libre — ce qui était lui-même le résultat utile, car il a dirigé l'investigation vers le CPU.

Le planificateur avait mis l’inférence sur les cœurs d’efficacité

Lire powermetrics par cluster CPU au lieu de par machine a révélé la première constatation réelle. Pendant le décodage, les clusters d’efficacité fonctionnaient à 75-93% occupés tandis que les clusters de performance étaient inactifs à 1-12%. Tout lancé sur SSH hérite d’une classe de qualité de service que macOS lit comme travail à faible priorité, et le planificateur honorait ce conseil en gardant une charge de travail critique en latence sur les cœurs lents.
La correction était 1 ligne de ctypes, exécutée avant le chargement des poids du modèle :
python
import ctypes
ctypes.CDLL("libSystem.B.dylib").pthread_set_qos_class_self_np(0x21, 0) # QOS_CLASS_USER_INTERACTIVE
Ce pin, plus le chargement de la tour de texte du modèle via mlx-lm au lieu de la pile vision complète, a déplacé le décodage brut de 11,5 à 15,1 tok/s — un gain de 31 % grâce au placement du planificateur et à un chargeur plus léger, sans changement du modèle. Nous avons ensuite appris que la portée du pin a une limite : il déplace le thread appelant, et les threads travailleurs de MLX gardent leur propre classe de planification. Pour ce modèle, le thread principal portait suffisamment de travail pour que cela compte.

Le décodage spéculatif a fourni le saut que aucun sysctl ne pouvait

Le plus grand gain unique provenait d’une fonctionnalité que le modèle de base possédait déjà. Qwen3.8 embarque une tête de prédiction multi‑jeton — un petit module auxiliaire qui rédige plusieurs jetons à l’avance — et le convertisseur MLX supprime silencieusement ces tenseurs 15 pendant la conversion. Un paquet communautaire réhéberge la tête comme un rédacteur autonome 253 MB, ce qui m’a permis de la réassocier avec le modèle avec lequel elle a été entraînée.
Le schéma de service est rédiger‑puis‑vérifier : le rédacteur propose quelques jetons, le modèle complet les vérifie en une passe 1, et les jetons acceptés comptent tous. Avec --draft-model et --draft-kind mtp sur le serveur, l’acceptation du brouillon mesurait 94 % sur des invites de codage et de chat réalistes, et le décodage passait de 15,1 à 20,3 tok/s côté serveur — 13,4 tok/s de bout en bout via le relais, contre 9,0. La GPU a confirmé l’histoire : 77% résidence active, tout à la pleine 1296 MHz, consommant 48 W, avec le cluster de performance 97% occupé. Le préremplissage s’est amélioré dans la même mise à niveau, de 14.7 tok/s à 18-55 selon la forme de l’invite.
Decode speed by configuration (tok/s, M1 Ultra, 27B-4bit)
Chart data
tokens per second
MLX stackllama.cpp Q4_K_M
raw stack11.59.2
+ text tower13.6
+ QoS pin15.1
+ MTP drafter20.312.2

Le challenger GGUF a perdu par 40%

Les publications communautaires placent llama.cpp avec un brouillon spéculatif à 25‑32 tok/s pour cette classe de modèle sur M1 Ultra, confortablement devant nos chiffres MLX, alors nous avons donné au challenger un cercle équitable : le Q4_K_M GGUF officiel, un modèle de brouillon MTP uniquement correspondant, et une version actuelle de llama.cpp avec l’accélération Metal confirmée active. La voie GGUF a mesuré 9.2 tok/s de base et 12.2 avec le brouillon — à environ 40% derrière la pile MLX qu’elle était censée destituer.
llama.cpp reste une excellente ingénierie ; l’écart se trouve probablement dans la façon dont les noyaux Metal de chaque runtime gèrent ce point de contrôle sur ce puce. La leçon durable est plus simple : un nombre que quelqu’un d’autre a mesuré sur sa machine, sa version, et son point de contrôle est une hypothèse sur le vôtre. Les 18 GB de poids du challenger ont quitté la machine le même après‑midi, et le binaire llama.cpp de 50 MB est resté pour les futurs affrontements. J’ai déjà écrit sur cette règle auparavant comme benchmark-driven development — elle a porté SEOReport from heuristics to a product — et ici elle a sauvé une migration.

Un MoE à 3B d'activité aurait dû gagner, et le compteur GPU a expliqué la perte

Le dernier challenger avait la théorie la plus forte — la même connaissance commune que nous avions l'intention de tester au départ. Les modèles denses décodent lentement sur Apple Silicon parce que chaque token généré doit lire presque tous les poids ; un modèle mixture-of-experts avec 35B de paramètres totaux active seulement environ 3B par token, donc sur une machine limitée par la bande passante son décodage devrait être plusieurs fois plus rapide qu'un dense 27B — chaque token lit environ 11% des poids. Nous avons téléchargé le MoE 4-bit et son brouilleur MTP correspondant, réchauffé le serveur, et mesuré 14,7 tok/s brut : la même vitesse que le modèle dense qu'il était censé humilié. Avec le décodage spéculatif, il a atteint 19 tok/s sur des invites réalistes et 26.3 sur un texte très prévisible — un match contre le 20.3 du modèle dense, sans argument de qualité pour rompre l'égalité.
powermetrics a identifié le régime en 1 échantillon. Pendant le décodage MoE le GPU restait à 0-3% de résidence active tandis que les clusters d'efficacité se déplaçaient à 60-90%. Le modèle consacre son budget par token au travail côté CPU, et le timing niveau opération a montré pourquoi : chaque opération dispatchée coûte 50-100 microsecondes de surcharge, et cette architecture hybride — GatedDeltaNet attention linéaire plus 256 experts derrière un petit état caché de 2048 large — émet environ 78 opérations par couche sur 40 couches. À la taille de lot 1, le GPU termine chaque petit noyau avant que le CPU ne puisse mettre en file d'attente le suivant. La machine était limitée par le dispatch, et les charges de travail limitées par le dispatch ne gagnent rien en lisant moins de poids par token.
Diagram source
graph TB
    A[Décodage lent  
à la taille de lot 1] --> B{GPU occupé  
pendant le décodage?}
    B -->|oui| C[Limité par la bande passante  
moins d'octets gagnent]
    B -->|non| D[Limité par le dispatch  
moins d'opérations gagnent]
    C --> E[MoE aide  
une quantification plus petite aide]
    D --> F[Le décodage spéculatif  
aide le plus]

    style E fill:transparent,stroke:#10B981,stroke-width:2px
    style F fill:transparent,stroke:#3B82F6,stroke-width:2px
Pour boucler, nous avons prouvé que le chemin GPU lui-même était sain avec un test de charge synthétique : une boucle de multiplication de matrice 8192³ a atteint 10,8 TFLOPS en bfloat16 avec le GPU à 97-100% de résidence et 68 W. Les preuves externes ont confirmé le diagnostic local — des estimations indépendantes placent cette classe MoE près de 21,7 tok/s sur un M2 Ultra, et un problème OpenVINO documente le même modèle perdant contre un dense 8B sur des backends avec des chemins de dispatch plus faibles. Le dense 27B avec son rédacteur MTP a conservé le créneau de production, et 38,5 GB de MoE poids ont été supprimés le même soir.
GPU active residency during decode (powermetrics, %)
Chart data
GPU active residency (%)
dense 27B + MTP77
MoE 35B-A3B2
matmul hog test100

Ce que les expériences contrôlées 5 ont laissé derrière

La machine sert désormais Qwen 3.8-27B à 20.3 tok/s côté serveur, une amélioration de 68% par rapport au début de la journée, et le rendement plus durable est une liste de contrôle que nous réutiliserons sur chaque boîte future :
  • Le décodage à une taille de lot 1 a 2 régimes, limité par la bande passante et limité par la distribution, et un échantillon de 2 secondes powermetrics identifie le vôtre avant que vous ne dépensiez un téléchargement sur la mauvaise correction.
  • Le décodage spéculatif est le bouton de réglage le plus puissant pour une boîte à utilisateur unique. Il a ajouté 34% ici et s’accumule avec chaque autre optimisation, car les lots de vérification font travailler le GPU tandis que le rédacteur absorbe les lacunes d’inactivité.
  • Le planificateur QoS est un véritable paramètre d’inférence sur macOS. Tout ce qui est lancé sur SSH doit fixer ses threads délibérément, et l’effet du verrouillage est vérifiable par cluster plutôt que par vibes.
  • Les revendications de débit communautaire voyagent mal à travers les checkpoints, les builds et les puces. Un benchmark local est moins cher qu’une migration.
  • La suppression fait partie du flux de travail. Chaque challenger qui a perdu a quitté le disque dans la journée, ce qui rend l’expérience suivante honnête et la machine légère.
La partie satisfaisante de cette aventure est que les réponses étaient toutes dans les compteurs matériels, attendant que quelqu’un pose des questions précises — et cette fois nous les avons posées ensemble. Un modèle local que vous avez mesuré vaut plus qu’un modèle plus grand que vous avez supposé — le même rendement composé que j’obtiens en traitant local AI gains as infrastructure plutôt que des trivia. J’ai lancé tout cela le matin où les poids sont arrivés, et Kimi K3 est resté dans le combat pour chaque expérience, chaque lecture de compteur, et cette rédaction issue des notes de laboratoire de la même soirée. La prochaine expérience est déjà en file d’attente : les graphes de décodage compilés promettent de réduire le coût de distribution par opération qui a décidé le verdict MoE, et lorsqu’une version MLX est publiée, le revanche prendra un après‑midi et 1 téléchargement.