Documentation

Recherche sur la performance de Laya MLX

Date de recherche : 2026-09-19. Cible : Apple M3 Max, 40 cœurs GPU, 128 GiB de mémoire unifiée, MLX/MLX Metal 0.32.2. Il s’agit d’une revue statique du runtime natif, de l’implémentation MLX installée, de la documentation officielle et du JSON de benchmark existant. Aucun benchmark GPU ni inférence de modèle n’a été exécuté pour cette recherche. Aucune des optimisations proposées ci-dessous n’a de gain de vitesse mesuré dans ce rapport.

Les premières expériences devraient être la compilation du modèle complet et l’ordonnancement représentatif des lots, suivis d’une multiplication matricielle quantizée sélective. Elles traitent le travail répété dominant. Un kernel d’attention locale spécialisé est un projet crédible à plus long terme pour les entrées longues. L’élagage exact de la dernière couche de la tête de décision est faisable, mais son économie arithmétique sur le modèle complet n’est que de quelques pour cent. De grandes améliorations sans changer le checkpoint exigeront d’améliorer le backbone dense, d’éliminer des requêtes réellement redondantes, ou de trouver un goulot d’étranglement d’implémentation mesuré ; remplacer simplement une activation ou activer un autre drapeau d’attention ne suffira probablement pas.

Ce que les mesures existantes établissent

Ce qui suit sont des latences médianes de bout en bout existantes, incluant la préparation du prompt et le formatage du résultat, avec achèvement GPU synchronisé, cinq warmups et 50 itérations mesurées. Le chargement et les téléchargements du modèle sont exclus. Le benchmark autorise un lot de 64 questions ; la valeur par défaut du runtime public est 16, donc le résultat sur 50 questions n’est pas la configuration d’API par défaut.

Checkpoint / précision Courte 1 question Courte 10 questions Courte 50 questions Longue 1 question Longue 10 questions
Laya MLX FP16 13.421 ms 71.068 ms 336.030 ms 44.927 ms 420.987 ms
Laya MLX FP32 15.954 ms 98.820 ms 450.712 ms 61.331 ms 534.242 ms
Laya stock Torch MPS FP32 24.918 ms 95.265 ms 497.856 ms 65.581 ms 586.594 ms
Multilingual MLX FP16 7.390 ms 27.386 ms 127.565 ms 37.635 ms 389.487 ms
Multilingual MLX FP32 7.988 ms 32.337 ms 151.387 ms 47.208 ms 451.331 ms
Multilingual stock Torch MPS FP32 19.349 ms 43.158 ms 194.171 ms 52.939 ms 534.492 ms

Sources : Laya FP16, Laya FP32, Laya MPS, multilingue FP16, multilingue FP32 et multilingue MPS. Les entrées longues contiennent 512 tokens pour Laya et 1024 pour le multilingue ; comparer leurs latences sur entrée longue n’est donc pas une comparaison à longueur de séquence égale. Les longueurs courtes paddées sont 93 et 91, respectivement. Les comparaisons Torch doivent conserver le label FP32 : elles combinent un changement de backend avec un changement de précision lorsqu’on les compare à MLX FP16.

La variabilité d’exécution est significative. Par exemple, l’exécution multilingue FP16 longue à 10 questions a un p50 de 389.487 ms, un p95 de 462.319 ms et un maximum de 619.663 ms. Sa médiane de passe avant courte à une question est de 8.023 ms, tandis que la médiane de bout en bout mesurée indépendamment est de 7.390 ms. Soustraire ces médianes produirait un temps de prétraitement négatif absurde. Les fichiers actuels n’isolent pas le tokenizer, le dispatch Python, les kernels GPU individuels ni les coûts de synchronisation. Ils établissent des références utiles, pas un diagnostic de goulot d’étranglement au niveau kernel.

Les rapports de validation consignent 63/63 d’accord sur l’argmax pour chacun des trois checkpoints en FP32 et FP16, et 100 appels répétés finis et déterministes par variante : 378/378 accords de réponse et 600 appels répétés au total. Ce sont des contrôles de non-régression sur un petit corpus de test, incluant des questions répétées. Ils ne prouvent pas qu’une future quantization ou un changement d’architecture préservera l’exactitude générale sur les tâches.

Pourquoi la multiplication matricielle dense mérite la priorité

L’implémentation du modèle applique les projections QKV et de sortie, un MLP d’encodeur à porte et deux couches de transformer conventionnelles de tête de décision à chaque token paddé. Soit D la taille cachée, I la taille intermédiaire de l’encodeur, N le nombre de couches de l’encodeur et H le nombre de couches de la tête de décision. Le nombre de poids matriciels utilisés par token dans ces blocs est :

A = N * (4 * D^2 + 3 * D * I) + H * 12 * D^2
dense FLOPs per batch ~= 2 * B * L * A
dense attention FLOPs ~= 4 * B * (N + H) * L^2 * D

Ces estimations comptent la multiplication et l’addition séparément et excluent la normalisation, les activations, les embeddings, le scoring, le masquage, le mouvement de mémoire et le coût des kernels. C’est un modèle arithmétique, pas un profil d’exécution.

Famille de checkpoint D / I / couches d’encodeur Poids d’embedding de token Poids matriciels principaux par token A Couches globales / locales de l’encodeur
Laya / typed decisions 1024 / 2624 / 28 51,576,832 368,312,320 10 / 18
Multilingual 768 / 1152 / 22 196,608,000 124,452,864 8 / 14

Les quelque 322 millions de paramètres totaux du checkpoint multilingue incluent 196.6 millions de paramètres d’embedding. Seules certaines lignes d’embedding sont rassemblées pour l’inférence ; ce n’est pas une projection complète du vocabulaire. Sa charge matricielle principale par token est d’environ un tiers de celle du modèle anglais, bien que leurs nombres totaux de paramètres semblent bien plus proches. Cela est cohérent avec l’écart de latence mesuré sur petits lots, sans toutefois prouver un goulot d’étranglement matériel particulier. Une quantization du seul embedding réduirait surtout la taille des poids résidents, en particulier pour le multilingue ; elle n’améliorerait pas nécessairement la latence d’inférence.

Avec le chemin d’attention dense actuel, les produits d’attention représentent environ 1.5% des FLOPs modélisés à L=93 pour Laya, 7.9% à L=512 pour Laya et 23.3% à L=1024 pour le multilingue. Leur part du temps d’horloge murale peut différer substantiellement. Un profileur devrait distinguer les kernels matriciels, les kernels d’attention, les kernels élément par élément, la construction du graphe côté CPU et les intervalles d’inactivité avant de s’engager dans un travail Metal personnalisé.

Expériences priorisées

Priorité Expérience Meilleure cible Principal compromis / condition d’acceptation
P0 Profiler une forme courte et une forme longue ; compiler le modèle ou les blocs d’encodeur Latence d’une requête et dispatch Python Garder des sorties identiques dans la tolérance de précision existante ; mesurer la compilation de première utilisation séparément
P0 Régler la mise en lots selon le budget de tokens réel et la distribution des longueurs Nombreuses questions distinctes et trafic à longueurs mixtes Optimiser le débit sous contrainte d’un budget de latence p95 et de mémoire ; tenir compte de la file d’attente
P1 Quantizer des couches linéaires sélectionnées du backbone, en commençant à 8 bits puis 4 bits Trafic de poids et potentiellement l’inférence dense Portes de qualité et de calibration ; la vitesse réelle sur M3 Max peut régresser
P1 Mettre en cache la tokenisation d’état partagée et les templates de questions stables Nombreuses questions partageant un état ou des grilles répétées Identité exacte des tokens ; cache borné ; résultats mis en cache et non mis en cache séparés
P1 Ne calculer que les sorties requises de la dernière couche de tête Toute charge, en particulier les séquences plus longues Élagage exact des dépendances ; économie arithmétique modeste sur le modèle complet
P2 Attention à fenêtre locale réelle avec bornes de tuiles Charges à 512/1024 tokens Complexité d’un nouveau kernel ; préserver la sémantique de fenêtre bidirectionnelle et de padding
P2 Fusionner residual/norm ou GELU/gate uniquement là où le profil le justifie Coût des petits kernels ou trafic d’activation Des kernels rapides existants couvrent déjà une bonne partie ; garder une sémantique GELU exacte
Fonctionnalité produit distincte Dédupliquer les entrées de passe avant identiques Charges qui répètent réellement des questions Rapporter le nombre d’inférences uniques et les hits de cache ; ne pas le présenter comme un gain de vitesse général de kernel

Compiler le chemin d’inférence évalué complet

Agent.forward() construit actuellement des tableaux, appelle self.model et évalue le résultat. Il n’y a pas de mx.compile englobant au niveau du modèle ou du bloc. La compilation à forme fixe peut réduire la construction du graphe Python et fusionner les opérations prises en charge. MLX documente la spécialisation de forme et la capture explicite d’état ; il avertit aussi que la compilation sans forme ne peut pas préserver en toute sécurité des opérations Python arbitraires dépendant de la forme. Voir le guide officiel de compilation.

Commence par un callable compilé créé après chargement, cast et évaluation des poids, avec une spécialisation de forme normale. Garde le callable non compilé existant pour la comparaison de parité et la compatibilité CPU. Pour un modèle d’inférence figé, les poids peuvent rester capturés pour cette instance de modèle ; si les poids ou la structure des modules changent, reconstruis le callable ou capture explicitement l’état concerné. Ne réutilise pas une fermeture compilée à travers des remplacements de checkpoint.

Teste un wrapper sur tout le modèle et, si les limitations de traçage ou le coût de compilation rendent cela peu attrayant, compile les blocs d’encodeur et la tête séparément. Le code actuel lit x.shape dans des entiers Python, remet en forme avec des valeurs explicites de lot et de longueur, crée des masques arange(length) et indexe les marqueurs via une plage de lignes dérivée de la forme. Appliquer shapeless=True à ce graphe complet sans refonte est dangereux. Déplacer la construction dynamique des masques hors d’un bloc compilé et utiliser des opérations flatten/unflatten indépendantes de la forme peut permettre une variante sans forme ultérieure ; vérifie-la sur différents B, L et nombres de marqueurs.

Les buckets de forme peuvent limiter le retraçage, mais le padding a un coût de calcul. Padding de L=93 à 96 ajoute environ 3.2% de travail par token ; le padding à 128 ajoute environ 37.6%. Compare la compilation à forme exacte avec de petits multiples de longueur et un ensemble borné de buckets informés par la charge. Inclus (batch size, padded length, marker slots, dtype, device/model instance) dans les décisions de politique de cache, et mesure la latence de compilation à froid et la mémoire retenue sous changement de forme.

Le benchmark forward autonome dans worker.py appelle agent.model directement. Si la compilation n’est ajoutée que dans Agent.forward, le benchmark forward actuel la contournerait tandis que le benchmark de bout en bout l’utiliserait. Les deux chemins doivent sélectionner explicitement la même implémentation candidate pour une comparaison significative. Garde mx.eval et la synchronisation GPU dans la procédure de mesure : mesurer la seule construction du graphe ne mesurerait pas l’inférence.

Mettre en lots par tokens utiles, puis inspecter l’ordonnancement matriciel

collate_items padd à droite chaque bloc jusqu’à sa plus longue séquence ; le runtime regroupe les questions dans l’ordre d’insertion. Pour un trafic hétérogène, trie ou bucketise par longueur préparée, utilise un budget de tokens en plus d’un plafond de nombre de questions, et restaure les IDs de question d’origine et l’ordre de sortie. Compare des lots de 1, 2, 4, 8, 16, 32 et 64 uniquement là où ils sont représentatifs du service. Pour les requêtes en ligne, inclus le temps d’attente d’un lot ; le nombre de questions/secondes hors ligne seul peut masquer une latence inacceptable.

La charge actuelle de 50 questions courtes gaspille environ 8.9% des tokens paddés pour Laya et 5.8% pour le multilingue. Les lignes longues du benchmark n’ont aucun gaspillage de padding. Par conséquent, le dépadding ou le tri seul a un avantage arithmétique limité sur ces jeux de test. Une distribution à longueurs mixtes, incluant une longue question parmi beaucoup de courtes, est nécessaire pour révéler le bénéfice en production. Le retrait du padding doit préserver les positions RoPE par exemple, les positions des marqueurs et les frontières d’attention ; concaténer des exemples en une seule séquence sans masque d’isolation change le modèle.

Pour les kernels denses, inspecte les formes et les strides réels. QKV est déjà une projection unique, et les deux branches d’entrée du MLP de l’encodeur partagent déjà une projection. Les séparer sans discernement ajouterait des lancements. Compare l’aplatissement explicite de l’entrée contiguë [B,L,D] en [B*L,D] uniquement si le profileur ou la trace de dispatch MLX montre des GEMM par lots indésirables ; le framework aplatit peut-être déjà efficacement. L’inspection du code source seule ne justifie pas de revendiquer une optimisation GEMM manquée.

N’insère pas de synchronisation après chaque couche dans l’implémentation de production. Le runtime actuel évalue une fois par bloc. Des attentes supplémentaires pourraient supprimer le chevauchement CPU/GPU et masquer une amélioration d’ordonnancement ; le profilage au niveau des couches devrait être une exécution de diagnostic séparée.

Quantization : cibler le backbone et implémenter son contrat de stockage

L’implémentation de couche quantizée de MLX 0.32.2 installé fournit nn.quantize(..., class_predicate=...) et QuantizedLinear, avec une multiplication matricielle poids uniquement via mx.quantized_matmul. La quantization affine groupée prend en charge les expériences 8 bits et 4 bits. Commence par les couches linéaires de l’encodeur à une taille de groupe de 64, en gardant les activations, les normes, les embeddings de type, le scorer et la tête d’action en FP16. Ajoute ensuite indépendamment des couches linéaires de tête de décision et éventuellement une quantization de l’embedding. Mesure chaque variante ; les kernels poids uniquement peuvent perdre face aux GEMM FP16 à nombre de tokens élevé.

Il y a des risques d’intégration concrets dans le loader et le modèle actuels :

  1. Agent.__init__ caste chaque poids stocké vers un dtype flottant et n’instancie que des modules denses avant le chargement strict. Un checkpoint quantizé nécessite des métadonnées décrivant les modules sélectionnés, la taille de groupe, la largeur de bits et le mode ; instancie les modules quantizés correspondants avant le chargement et préserve les poids entiers empaquetés. Un cast flottant de poids empaquetés n’est pas un chargement valide.
  2. La première couche linéaire de la tête d’action a une largeur d’entrée D+4, soit 1028 ou 772, qui n’est pas divisible par une taille de groupe affine de 32, 64 ou 128. Un appel de quantization global est donc inadapté. Vérifie la largeur d’entrée de chaque couche sélectionnée avant conversion.
  3. DecisionModel.__call__ choisit le dtype d’entrée de la tête d’action à partir de self.act_head.layers[0].weight.dtype. Pour une couche quantizée, ce poids serait un stockage entier empaqueté, pas le dtype d’activation souhaité. Exclure la tête d’action évite ce chemin au départ ; le prendre en charge plus tard exige un contrat explicite de dtype d’activation.
  4. La quantization change les logits et les probabilités calibrées. L’accord actuel sur le petit jeu de test FP16 ne suffit pas à prouver une qualité 4 bits. Utilise des tâches choice, score et noul étiquetées sur des données mises de côté, des entrées multilingues, des décisions serrées, différents nombres d’options et des exemples d’escalade. Suis l’accord sur l’argmax, l’exactitude des tâches, l’erreur de score, la dérive de probabilité, la calibration et les probabilités d’action. Des sorties d’action saturées peuvent masquer de grands changements de logits d’action.

Pour des échelles et décalages affines FP16 avec une taille de groupe de 64, le stockage matriciel approximatif est de bits/8 + 4/64 octet par paramètre : 1.0625 octet à 8 bits et 0.5625 octet à 4 bits, contre 2 octets en FP16. Ce sont des estimations de stockage pour les matrices quantizées, excluant les autres tenseurs et le surcoût d’empaquetage ; ce ne sont pas des estimations de gain de vitesse. L’API quantize officielle décrit la divisibilité des groupes et les formats.

Les formats basse précision plus récents devraient être évalués contre le backend M3 Max réel, et non supposés utiliser du matériel de puces Apple plus récentes. La vérification de disponibilité de NAX dans MLX exige une génération d’architecture plus récente que l’appareil applegpu_g15s enregistré. Voir la vérification d’appareil de MLX 0.32.2.

Réutiliser la préparation CPU là où les entrées sont réellement identiques

build_sequence sérialise, assainit et tokenise le même état séparément pour chaque question. Il tokenise aussi séparément chaque instruction et chaque option. Le tokenizer Rust est déjà utilisé directement ; remplacer la tokenisation de Transformers n’est pas une optimisation restante.

Sérialise et assainis l’état une fois par appel prepare, encode-le une fois, et découpe ses IDs de token selon la place disponible de chaque question. Mets en cache les préfixes de question préparés immuables quand la même grille est utilisée sur plusieurs états, avec des clés incluant l’identité/révision du tokenizer, le type de question, les critères ordonnés, la sérialisation des instructions, l’assainissement des tokens spéciaux et les budgets de tokens. Les caches bornés ne doivent pas réutiliser de résultats après un changement de tokenizer ou de configuration. L’encodage groupé du tokenizer est une autre expérience, à condition que sa sortie corresponde exactement à la séquence actuelle d’encodages indépendants.

Ne tokenise pas un prompt nouvellement concaténé comme substitut à la concaténation de pièces encodées indépendamment : les frontières de sous-mots peuvent changer. Vérifie octet par octet les IDs d’entrée, les masques d’attention, les positions des marqueurs, les qtypes, le comportement de troncature, les critères structurés, les littéraux de masque, les entrées vides et le mapping de sortie.

Pour le benchmark long, l’état répète une phrase 200 fois avant troncature. Éviter N encodages d’état répétés pourrait aider la préparation CPU, mais la différence de mesure existante entre bout en bout et forward ne mesure pas cette économie. Mesure prepare, la construction collate/tableaux, le forward et le post-traitement indépendamment, puis confirme le résultat de bout en bout avec un corpus non répétitif mis de côté.

Un cache KV de décodeur ne s’applique pas à cet encodeur. Sa première couche est une attention bidirectionnelle globale ; les représentations des tokens d’état dépendent de la question, des options et de leurs positions. Réutiliser des états cachés d’état ou K/V entre différentes questions change les résultats. La tokenisation et des résultats d’entrée complète identiques peuvent être mis en cache ; un état d’encodeur contextuel arbitraire ne peut pas.

Élaguer exactement les sorties finales de la tête de décision

Après le dernier HeadLayer, seuls le token [CLS] et les tokens marqueurs d’option sont consommés. Les couches de tête antérieures doivent encore produire tous les tokens, car la dernière couche lit leurs K/V. Dans la dernière couche seule :

  1. Normalise tous les tokens d’entrée et calcule tous les K/V.
  2. Rassemble Q à [CLS] et aux positions d’option valides, et exécute ces requêtes contre la séquence K/V masquée complète.
  3. Applique la projection de sortie, le résidu, la seconde norme et le réseau feed-forward uniquement à ces positions sélectionnées.
  4. Utilise la sortie [CLS] sélectionnée pour la tête d’action et les sorties de marqueur sélectionnées pour le scoring ; préserve le padding des marqueurs et l’ordre d’origine.

Une première implémentation peut conserver la projection QKV fusionnée complète et rassembler Q ensuite. Une variante plus agressive sépare ses poids en une projection KV pleine longueur et une projection Q des tokens sélectionnés. Cela économise plus d’arithmétique mais peut rendre l’ordonnancement GEMM moins efficace. Des indices de marqueur paddés en double sont inoffensifs uniquement si les résultats masqués restent inobservables. Les cas à 1 option, à nombreuses options et à marqueurs variables ont besoin de contrôles de parité explicites. Comme moins de requêtes peuvent sélectionner un kernel SDPA différent, l’équivalence mathématique n’implique pas des résultats à virgule flottante identiques bit à bit.

Avec R = 1 + number of option slots, conserver le QKV complet retire environ 18 * B * (L-R) * D^2 FLOPs denses et 4 * B * L * (L-R) * D FLOPs d’attention de la dernière couche de tête. Séparer Q/KV fait passer le coefficient dense de 18 à 20. Par rapport à l’estimation arithmétique sur le modèle complet ci-dessus, avec R=5 :

Checkpoint / longueur Garder le QKV complet fusionné Calculer aussi uniquement le Q sélectionné
Laya, L=93 2.44% 2.70%
Laya, L=512 2.60% 2.86%
Typed decisions, L=1024 2.66% 2.90%
Multilingual, L=93 4.03% 4.47%
Multilingual, L=1024 4.22% 4.58%

Ce sont des réductions statiques de FLOPs, pas des réductions de latence prédites. La technique retire la majeure partie du travail d’une couche de tête, pas la majeure partie du travail du modèle. Elle est utile parce qu’elle préserve les dépendances et est implémentable sans réentraînement, non parce qu’elle promet un multiple de vitesse sur le modèle complet.

Construire une véritable attention locale seulement après en avoir mesuré la contribution

Tous les appels d’attention de l’encodeur utilisent déjà mx.fast.scaled_dot_product_attention ; RoPE est déjà mx.fast.rope ; nn.LayerNorm appelle la primitive de normalisation rapide. L’API d’attention de MLX accepte des masques booléens et effectue le softmax en FP32. La dimension de tête actuelle est de 64. Le dispatch Metal de MLX 0.32.2 prend en charge cette forme avec des masques de tableau et ne sélectionne pas le repli non fusionné pour elle pendant l’inférence. Rien ne prouve que le masque booléen de Laya désactive l’attention fusionnée. force_fused=True, disponible dans la version installée, est utile comme assertion de diagnostic mais ne doit pas être présenté ici comme un nouveau chemin rapide.

La limitation restante est la parcimonie structurée. L’implémentation construit un masque booléen local dense de forme [B,1,L,L]. Dans le kernel d’attention Metal conventionnel, la boucle non causale parcourt toute la plage de tuiles KV ; le masque de tableau est appliqué aux scores après la multiplication QK. Il préserve la sémantique d’attention locale sans exploiter une plage de tuiles locale.

Un kernel spécialisé exact peut limiter chaque tuile de requête à la fenêtre K/V qui se recouvre, conserver l’accumulation softmax en FP32 et éviter un masque dense L par L. La fenêtre correcte est bidirectionnelle et inclusive : abs(query_position - key_position) <= 64. Les requêtes intérieures peuvent voir 129 positions, malgré le nom de configuration local_attention=128. Les couches à attention complète et les deux couches de tête de décision doivent rester globales. Les clés paddées doivent rester exclues, et les requêtes paddées inutilisées ont besoin d’un comportement fini défini.

Un prototype moins coûteux peut regrouper des blocs de requêtes avec des tranches K/V qui se recouvrent et appeler le SDPA existant avec un masque exact plus petit. Utilise des Q/K RoPE déjà positionnés, ou conserve explicitement les offsets absolus. Préfère des blocs mis en lots à de nombreux appels Python, et tiens compte de la matérialisation K/V en double. Ce prototype peut perdre face au kernel actuel sur les longueurs courtes ; c’est une expérience de correction et de seuil de rentabilité avant de maintenir du Metal personnalisé.

La réduction maximale des FLOPs modélisés du modèle complet en retirant toutes les paires d’attention locale interdites est faible pour les entrées courtes et plus prometteuse pour les longues :

Checkpoint / longueur Réduction idéale totale de FLOPs par parcimonie locale exacte
Laya, L=93 0.086%
Laya, L=512 3.61%
Typed decisions, L=1024 7.69%
Multilingual, L=93 0.147%
Multilingual, L=1024 11.92%

Les estimations utilisent local_pairs = L*(2*r+1) - r*(r+1) pour L>r, avec r=64. Elles incluent toutes les projections denses et les deux couches de tête de décision à attention complète. Elles excluent la génération de masques et le trafic mémoire. Le bénéfice à l’exécution peut dépasser ou rester en dessous de la fraction de FLOPs, car l’attention et les GEMM ont des efficacités différentes ; seul le profilage peut l’établir. À 8192 tokens, le compromis serait différent, mais les agents livrés limitent les entrées à 512 ou 1024, donc une revendication sur 8192 tokens exigerait une charge de travail prise en charge séparément.

Fusion au-delà de la compilation

Le nn.gelu exact installé est déjà décoré d’une compilation sans forme, et nn.Linear utilise déjà un addmm tenant compte du biais quand c’est approprié. La compilation de blocs entiers peut encore fusionner le GELU avec sa multiplication de porte, les additions résiduelles, les casts, les masques et les petites opérations de caractéristiques de score. Inspecte le graphe de kernels compilé avant d’implémenter un kernel personnalisé équivalent.

Si le trafic d’activation reste significatif, prototyper une fusion exacte GELU-et-porte ou résidu-et-LayerNorm. Préserve le GELU exact actuel fondé sur erf ; une approximation tanh ou sigmoïde change le modèle et nécessite des mesures de qualité séparées. Inspecte les strides et copies Q/K/V réels avant d’ajouter des conversions de disposition : l’implémentation d’attention complète de MLX accepte une dimension de tête contiguë avec d’autres strides et écrit une disposition de sortie pratique pour fusionner les têtes. Une copie contiguë inconditionnelle peut ajouter du travail.

Le softmax des marqueurs, le tri des deux premiers, l’entropie, la petite tête d’action et le formatage des résultats NumPy sont des cibles ultérieures légitimes uniquement si elles sont mesurées. Il y a peu de positions d’option comparé à des centaines d’opérations de transformer pleine largeur, donc les optimiser en premier a peu de chances de traiter le chemin dominant.

Protéger le sens du benchmark tout en poursuivant des gains agressifs

Le générateur de charge actuel cycle sur trois définitions de question pour construire 5, 10 ou 50 questions. Il y a au plus trois entrées de modèle uniques dans ces lots. La déduplication exacte par appel peut éviter une inférence redondante dans une vraie application, mais elle améliorerait de façon disproportionnée ces jeux de test. Garde cette fonctionnalité séparée de l’optimisation des kernels et rapporte questions, unique_forward_inputs, les hits de cache et les tokens réellement évalués. Reconstruis chaque réponse d’origine à l’aide de ses propres libellés, critères ordonnés et métadonnées de calibration. Conserve une suite de 50 questions réellement différentes et une suite à entrées délibérément dupliquées.

N’utilise pas de cache de résultats inter-appels pour le benchmark d’inférence principal : il appelle de façon répétée exactement la même requête. Rapporte toute expérience de cache comme telle. Les benchmarks de cache de tokenizer devraient inclure à la fois un scénario à grille répétée et un scénario à entrée neuve.

Pour chaque candidat, utilise cette conception d’expérience :

  1. Garder la cible constante. Consigne la révision/hachage source, la révision du modèle, le dtype, les drapeaux de compilation, les modules quantizés sélectionnés, les IDs de token ou leur hachage, la forme, le nombre de marqueurs, la politique de mise en lots, le warmup, la synchronisation et l’appareil. Le input_sha256 existant hache l’état/les questions plutôt que les tenseurs de token réels ; il n’établit pas l’identité des tenseurs entre tokenizers. Relance la référence depuis la même révision source finale car les hachages de source des JSON historiques diffèrent.
  2. Séparer les classes de charge. Utilise des formes fixes pour des comparaisons de kernels contrôlées ; des longueurs variables réelles pour l’ordonnancement et la compilation ; des questions distinctes pour le débit ; des grilles répétées pour un cache CPU légitime ; et des questions délibérément dupliquées pour la déduplication. Inclus des queues de longueur et différents nombres d’options. Garde le texte source et la troncature identiques au sein de chaque comparaison de backend.
  3. Mesurer le comportement à froid et à chaud. Consigne séparément le chargement du modèle et la première compilation. Mesure la préparation, la construction du graphe, le forward évalué synchronisé, la conversion de sortie et la latence de bout en bout sans traiter des médianes indépendantes comme additives. Profile les kernels dans une exécution séparée, car le traçage peut perturber la latence.
  4. Changer une optimisation à la fois. Présélectionne avec le nombre d’itérations existant, puis répète les finalistes par blocs alternés référence/candidat et collecte assez d’échantillons pour un p95 crédible, par exemple au moins 200 requêtes mesurées par charge. Utilise un seul benchmark GPU actif, des conditions de puissance/thermiques cohérentes, et préserve les échantillons bruts. Exige une amélioration supérieure à la variabilité d’exécution mesurée.
  5. Vérifier correction et stabilité. Compare à MLX au même dtype et à la référence FP32 existante ; impose l’identité des tokens pour les changements d’ordonnancement et de préparation CPU. Exerce des longueurs autour de 64, 128 et des frontières de buckets ; des lots autour des limites de blocs ; une et de nombreuses options ; des entrées multilingues ; le padding ; et des changements de forme après compilation. Répète des requêtes à forme changeante, surveille la mémoire active et de cache après warmup, et vérifie des résultats finis déterministes dans chaque configuration.
  6. Appliquer des portes plus strictes aux changements approximatifs. La quantization, l’approximation d’activation, l’élagage de tokens, les sorties anticipées et la distillation exigent des résultats de tâche et de calibration sur données mises de côté, au-delà du petit jeu de test de régression. Préserve une identité de modèle et un label de benchmark distincts lorsqu’on change le comportement entraîné. Un checkpoint multilingue plus rapide ou un modèle distillé est un modèle différent, pas un gain de vitesse du checkpoint Laya identique.

Pour tout point chaud mesuré occupant une fraction f du temps de bout en bout et accéléré d’un facteur s, utilise la borne d’Amdahl 1 / (1 - f + f/s) pour évaluer l’impact total attendu. Utilise une fraction de temps mesurée pour f ; les fractions arithmétiques ci-dessus n’en sont pas des substituts. La prochaine décision d’ingénierie concrète devrait suivre l’ablation compile/batching et un profil de kernel court vs long, pas un multiplicateur non vérifié.

Notes de documentation et de reproductibilité

La recherche documentaire a utilisé le workflow Context7 requis : une résolution library MLX, puis des requêtes séparées dans la documentation officielle pour la compilation et l’attention rapide, en utilisant /websites/ml-explore_github_io_mlx_build_html (trois commandes au total). Les .pyi et sources Python installés ont été inspectés pour vérifier le comportement de MLX 0.32.2, y compris force_fused, les API de quantization, le GELU compilé et le LayerNorm rapide. Des sources amont C++/Metal épinglées par version ont été lues pour les détails de dispatch et de boucle de tuiles. Aucune bibliothèque n’a été mise à niveau pour cette recherche.

Les deux fichiers de référence FP16 utilisés pour les observations détaillées avaient ces digests SHA-256 au moment de l’inspection :

laya-mlx-float16.json
63146dd664d039dde1a728b17aad896e491bd01ea36ea0786953691180e55b09

laya-multilingual-mlx-float16.json
9af74bd5a11e4edc15e6a8c9dc929a7b9fd2d19cb06f348cd0e04a076f912473

Le processus de benchmark principal produisait encore des artefacts supplémentaires pendant cette recherche. Le tableau cite délibérément des fichiers de référence complets déjà disponibles au moment de la revue et n’émet aucune prétention sur des implémentations candidates non mesurées.