mizorewww

Laya-MLX

Les checkpoints pré-entraînés de Laya portés vers MLX pour une inférence locale sur Apple silicon, sans PyTorch ni API cloud. Un port indépendant, pas une publication officielle de Convai Innovations.

Vérifié le 2026-10-05

Laya MLX joue à Snake — décisions réelles, vitesse d’origine

Des décisions typées à poids ouverts, exécutées nativement sur Apple Silicon.

13.4 ms en médiane de bout en bout pour une courte décision typée en anglais. 7.4 ms avec le checkpoint multilingue. 0 token de sortie. Inférence MLX locale, sans PyTorch, sans runtime Transformers ni API cloud.

中文 · Benchmarks · Démo Snake · Poids Hugging Face

Le GIF est un rendu à vitesse d’origine d’une vraie partie de Snake locale. Chaque coup appelle Laya ; la couche de sécurité de cycle visible peut corriger les propositions dangereuses. Les chiffres de latence ci-dessus proviennent du benchmark d’API à une question, distinct du temps par image de la boucle Snake à trois questions. Regarder le MP4 de 30 secondes · Vitesse et stabilité de Snake.

Démarrage rapide

pip install laya-mlx
import laya_mlx as laya

agent = laya.load("aac6fef/laya-mlx")
result = agent.predict(
    "I was billed twice. Please refund the duplicate.",
    {
        "department": {
            "type": "choice",
            "instructions": "Who should handle this?",
            "criteria": ["billing", "technical", "sales"],
        }
    },
)
print(result["answers"]["department"])

Apple Silicon, Python 3.11+, macOS 14+. Le premier chargement télécharge le checkpoint ; les inférences suivantes sont entièrement locales. L’environnement mesuré est macOS 27.2, Python 3.12.13 et MLX 0.32.2. Cette version de MLX fournit des wheels pour macOS 14, 15 et 26 ; l’installateur local a sélectionné la wheel 26. Les versions macOS plus anciennes prises en charge n’ont pas été testées sur cette machine.

Lancer la démo dans le terminal :

pip install 'laya-mlx[demo]'
hf download aac6fef/laya-multilingual-mlx
laya-snake

Télécharge une fois avant la démo hors ligne. Utilise un terminal d’au moins 104 × 35 cellules. Espace met en pause, ↑/↓ change la vitesse, R réinitialise et Q quitte. laya-snake --max-speed prend une décision neuve à chaque coup, sans cadence. Enregistrement, commandes et signification exacte des métriques.

laya-snake --optimize --max-speed active le chemin de compilation et de réutilisation de préfixe testé : 75.40 coups/s sur 2,400 coups, zéro mort et 2 interventions de sécurité visibles dans le test apparié sur M3 Max. C’était environ 6.5% plus rapide que le témoin eager de la même exécution. Preuves de jeu, de performance et de correction.

Performance sur M3 Max

FP16, de bout en bout Laya 421M Multilingue 322M
Une question courte, P50 13.42 ms 7.39 ms
Une question courte, P95 13.92 ms 7.79 ms
Débit sur 50 questions 146.8 q/s 395.0 q/s
Allocation MLX maximale, une question courte 943.6 MiB 687.6 MiB

M3 Max, 40 cœurs GPU, 128 GiB de mémoire. La mesure du temps inclut la préparation du prompt, la tokenisation, les tenseurs, l’inférence synchronisée, la calibration et le formatage du résultat ; le chargement du modèle est exclu. La mesure sur 50 questions utilise batch_size=64 ; l’API est par défaut à 16. Des longueurs, des nombres de questions et des conditions d’exécution différents modifient la latence. Méthode complète et chaque échantillon de temps.

Fidélité du portage : les trois checkpoints correspondaient à la réponse sélectionnée en amont sur 63/63 questions de validation, en FP32 comme en FP16 — 378/378 comparaisons. Chaque configuration a aussi passé 100 appels répétés finis et déterministes, avec une croissance mesurée de la mémoire active nulle. Cela mesure la fidélité sur ces jeux de test, pas l’exactitude sur toutes les questions possibles. Erreurs de probabilité et validation.

Pourquoi des décisions typées ?

Un logiciel a souvent besoin d’un choix, d’un score de grille ou d’une probabilité. Laya répond à ces questions contraintes en une passe avant bidirectionnelle, sans décodage token par token ni JSON généré.

state + typed question → bidirectional encoder → decision heads → probabilities
  • choice : des probabilités sur des options nommées.
  • score : des probabilités sur des niveaux de grille ordonnés et leur score attendu.
  • noul : P(vrai) pour une proposition.

Les lignes de question sont mises en lots indépendamment. Leurs représentations par l’encodeur bidirectionnel dépendent à la fois de l’état et de la question ; ce runtime ne prétend pas encoder l’état une fois pour réutiliser ses états cachés sur des questions arbitraires.

L’encodeur, le Transformer de décision, la tête de scoring et la tête d’action tournent tous dans MLX. La tokenisation utilise le tokenizer Rust de Hugging Face. Les poids pré-entraînés d’origine, le formatage des questions, la calibration et le schéma de sortie sont conservés. C’est un portage MLX indépendant, pas une version officielle de Convai Innovations.

Checkpoints pris en charge

Modèle Encodeur Paramètres Limite de contexte Usage
convaiinnovations/laya ModernBERT-large 421M 512 Anglais
convaiinnovations/laya-multilingual mmBERT-base 322M 1,024 Entrée multilingue
convaiinnovations/laya-typed-decisions ModernBERT-large 421M 1,024 Workflows typed-decisions en amont

Le contexte inclut les instructions, les options et l’état. Les trois utilisent les poids d’origine, le formatage des prompts, la calibration de température et le schéma de sortie. Ce dépôt fournit l’inférence et la conversion ; l’entraînement RLCD et le fine-tuning restent dans le projet en amont. C’est un portage indépendant, pas une version officielle de Convai Innovations.

Des checkpoints FP16 pré-convertis sont publiés sur Hugging Face :

Charge-les directement avec laya.load("aac6fef/laya-mlx"), ou utilise les identifiants de checkpoint d’origine ci-dessus. Chaque checkpoint publié inclut sa fiche modèle, ses résultats de validation, sa provenance, sa licence et les sommes de contrôle des fichiers. Les 36 fichiers publiés ont passé une vérification stricte des sommes de contrôle distantes ; les révisions épinglées et les hachages de poids sont consignés dans hub-publication.json.

Installation pour le développement

gh repo clone mizorewww/laya-mlx
cd laya-mlx
uv sync --extra demo
uv run --extra demo laya-snake

Ou installe la dernière révision GitHub avec pip install 'git+https://github.com/mizorewww/laya-mlx.git'. Les poids du modèle sont téléchargés séparément et exclus de Git.

API Python

import laya_mlx as laya

agent = laya.load("aac6fef/laya-mlx", dtype="float16")
result = agent.predict(
    "I was billed twice. Please refund the duplicate today.",
    {
        "department": {
            "type": "choice",
            "instructions": "Which team should handle this request?",
            "criteria": {
                "billing": "invoices, payments, refunds",
                "technical": "bugs and outages",
                "sales": "new purchases",
            },
        },
        "urgency": {
            "type": "score",
            "instructions": "How urgent is this request?",
            "criteria": ["not urgent", "soon", "critical"],
        },
        "refund": {
            "type": "noul",
            "instructions": "Does the customer ask for money back?",
        },
    },
)
print(result["answers"])

system_one est un alias de predict. Les états peuvent être du texte, des dictionnaires JSON ou des listes de conversation. choice accepte un dictionnaire ou une liste de libellés uniques ; score renvoie le niveau de grille attendu, indexé à partir de zéro ; noul renvoie P(vrai). Les résultats conservent l’arrondi à quatre décimales en amont, action.act_probability et les champs d’utilisation de tokens.

La précision par défaut est FP16. Utilise dtype="float32" pour un accord numérique plus étroit. Les probabilités peuvent différer légèrement selon la précision, même quand le libellé sélectionné concorde ; voir les erreurs mesurées dans BENCHMARKS.md. BF16 peut être demandé mais ne fait pas partie de la matrice de validation publiée.

À la suite de la v0.3.5 en amont, les températures de calibration ajustées sont bornées à [0.5, 5.0] avant utilisation : le bucket livré choice:11+ vaut 0.1006, ce qui aiguiserait les logits d’environ 10x et rapporterait un pile ou face comme une quasi-certitude. Les valeurs brutes du checkpoint restent disponibles via agent.temperature_raw et agent.temperature_by_options_raw, et un RuntimeWarning nomme chaque bucket borné au chargement.

batch_size=16 plafonne le nombre de questions par passe avant ; les requêtes plus grandes sont traitées par blocs. Augmente-le quand la mémoire le permet. device="gpu" ou device="cpu" sélectionne explicitement un appareil ; sinon, l’appareil par défaut de MLX est utilisé.

Pour les charges de travail répétées, active compile=True, pad_to_multiple=16 et cache_prompts=True au chargement d’un Agent. Le cache de préfixes est borné à 128 questions et partage la tokenisation de l’état côté CPU, tandis que chaque question reçoit encore son propre calcul d’encodeur. La compilation a un coût de première utilisation et une spécialisation de forme ; le padding peut ralentir certaines charges. Les trois options sont désactivées par défaut. Ablation Snake mesurée et utilisation.

agent = laya.load("./models/laya", dtype="float32", batch_size=32)
# Select one checkpoint inside upstream's bundled repository:
multi = laya.load("convaiinnovations/laya", subfolder="multilingual")
# Pin a Hub revision for reproducibility:
agent = laya.load(
    "convaiinnovations/laya",
    revision="c5d78730f3493e4fe16d61507ef4b78eef7318cf",
)

Le chargement valide chaque nom et chaque forme de paramètre. Les encodeurs non pris en charge et les mises à l’échelle RoPE non par défaut échouent explicitement. Le motif d’attention global/local de ModernBERT, la frontière inclusive de la fenêtre glissante, les bases RoPE locale/globale distinctes et le comportement de normalisation de la première couche sont préservés.

Routage de langue et presets

from laya_mlx import Router, triage_questions

router = Router(dtype="float16", max_loaded=2)
result = router.predict({"message": "发票被重复扣款,请退款。"}, triage_questions())
print(result["routing"])  # multilingual

# Choose the specialized checkpoint explicitly:
result = router.predict(state, questions, task="typed_decisions")

Le routeur, les heuristiques de langue, les utilitaires d’e-mail et les presets d’application sont adaptés de l’amont. Router(preload=True) garde les trois checkpoints résidents ; attach, preload, unload, un lang= explicite et un model= explicite sont pris en charge. Le cycle de vie du modèle est protégé par un verrou réentrant, de sorte que des threads concurrents partagent un seul Agent chargé au lieu d’en construire des doublons ; l’inférence elle-même n’est pas sérialisée. La détection des workflows typed-decisions reste optionnelle. Le portage préserve les limites du modèle : les checkpoints anglais ne remplacent pas le checkpoint multilingue, et la confiance ne garantit pas l’exactitude.

Les langues à écriture latine non identifiées (roumain, polonais, tchèque, turc, …) sont routées vers le checkpoint multilingue sur leurs seules lettres non anglaises, plutôt que d’être silencieusement supposées anglaises. detect_language(state) rapporte les preuves : language_undecided et diacritic_rate aux côtés de language et is_english.

Présélection de grands ensembles de choix

Les options d’un choice partagent un même budget de tokens head_max_len, donc une question avec des centaines de libellés ne laisse que quelques tokens par libellé. predict_shortlist encode l’état et chaque libellé, garde les k premiers par similarité cosinus et exécute un seul predict sur l’ensemble réduit. C’est optionnel : Agent.predict note encore chaque critère qu’on lui donne.

import laya_mlx as laya

agent = laya.load("aac6fef/laya-mlx")
embed_fn = laya.embed_fn_from_agent(agent)  # mean-pools the loaded encoder; no extra weights
result = laya.predict_shortlist(agent, state, questions, embed_fn, k=20)
print(result["shortlist"])  # which labels were kept, with cosine scores

Un bi-encodeur dédié passé comme embed_fn présélectionne généralement mieux que l’encodeur propre du checkpoint de décision. Les probabilités d’un choice présélectionné ne portent que sur les libellés conservés.

Ligne de commande

uv run laya-mlx predict \
  --model aac6fef/laya-mlx \
  --state-file examples/state.json \
  --questions examples/questions.json

uv run laya-mlx predict \
  --model aac6fef/laya-multilingual-mlx \
  --state '发票被重复扣款,请退款。' \
  --questions examples/questions.json

Correctifs amont sélectionnés après la v0.3.5

Le runtime intègre sélectivement des correctifs d’entrée, de routage et d’e-mail venus de l’amont 4aa6761 (arborescence source v0.3.23). Cela n’ajoute pas les API amont de batch, de documents longs, de hooks ou de serveur. La parité d’architecture neuronale reste testée contre 573e5b6.

  • Les listes de conversation chronologiques conservent leurs tokens les plus récents quand le contexte se remplit ; les chaînes et les dictionnaires conservent leur début. Le cache de préfixes suit la même règle.
  • Les critères noul n’acceptent que les clés false/true (y compris les clés booléennes Python). Le paramètre optionnel labels={"false": "no", "true": "yes"} change les mots montrés au modèle, tandis que la réponse reste P(vrai). Les clés invalides lèvent désormais une erreur au lieu d’être ignorées.
  • Les instructions qui ne sont pas des chaînes préservent l’Unicode. Des instructions vides, des niveaux de score nuls et un état None lèvent une erreur côté appelant ; les erreurs de question nomment la question.
  • Chaque réponse ajoute answer_confidence, la probabilité d’option calibrée maximale. Le confidence existant conserve son sens fondé sur l’entropie pour choice/score et la probabilité maximale pour noul. Aucun des deux champs ne garantit l’exactitude sur une nouvelle tâche.
  • usage ajoute state_tokens, state_tokens_dropped (la plus grande perte parmi les questions), truncated et truncated_questions. usage.options n’apparaît que pour les questions dont les intervalles de tokens d’option se chevauchent, et rapporte total, distinct et tokens_per_option. Cela signale des distinctions perdues ; cela ne les récupère pas et ne supprime pas le biais de position.
  • Le Router.preload() incrémental préserve les modèles résidents ; preload([]) ne fait rien. Les indices vides ou neutres quant à la langue retombent sur la détection, et le texte latin indécis respecte Router(default=...). La détection examine les valeurs de chaîne imbriquées et le texte mixte.
  • Le nettoyage des e-mails préserve les demandes ordinaires qui mentionnent la confidentialité, remercient le destinataire ou commencent par From:, tout en reconnaissant les pieds de message multilingues.

Exporter un checkpoint MLX

uv run laya-mlx convert \
  --model convaiinnovations/laya \
  --dtype float16 \
  --output models/laya-mlx-fp16

uv run laya-mlx predict \
  --model models/laya-mlx-fp16 \
  --state-file examples/state.json \
  --questions examples/questions.json

L’export contient model.safetensors, les configurations de l’encodeur et de l’agent, les fichiers du tokenizer et mlx_config.json. Les répertoires de sortie existants ne sont jamais écrasés. C’est une conversion de nom de paramètre/de dtype, pas une quantization ni un réentraînement. Les checkpoints sources stockent déjà des poids FP16 ; choisir FP32 augmente la précision arithmétique, pas la précision des poids sources.

Tests et benchmarks

uv sync --extra dev --extra reference --extra benchmark --extra demo
source .venv/bin/activate
gh repo clone NandhaKishorM/laya .upstream
git -C .upstream checkout 573e5b62696ba441230cd6be71d593331b5d23af
pytest -q
python -m benchmarks.download
python -m benchmarks.validate --repeats 100
python -m benchmarks.run --iterations 50 --warmup 5
python -m benchmarks.accuracy --per-class 64
python -m benchmarks.report

Exécute les mesures GPU séquentiellement. Les tests unitaires utilisent de petits modèles aléatoires et incluent des comparaisons directes avec Transformers et la tête de décision amont épinglée. La validation sur checkpoint réel teste la tokenisation, les logits, les probabilités calibrées, les sorties répétées et la croissance de la mémoire active. Le benchmark exécute chaque backend/checkpoint dans un processus neuf et stocke chaque échantillon de temps dans benchmarks/results. Le rapport complet explique les frontières de mesure et les différences de précision.

GitHub Actions exécute les tests CPU sur petit modèle sur un runner macOS arm64. Les benchmarks GPU sur checkpoint complet sont mesurés localement et ne font pas partie de la CI hébergée.

Recherche sur la performance

Les investigations de performance comprennent à la fois une analyse mathématique et des expériences locales indépendantes :

experiments/ contient les scripts de recherche et leurs mesures brutes. Les résultats de performance et de validation du runtime publié se trouvent dans BENCHMARKS.md ; chaque variante expérimentale a ses propres résultats de temps et de correction.

L’investigation actuelle ne soutient pas un gain de vitesse universel supplémentaire de 10× avec les mêmes checkpoints. Des cas sélectionnés montrent des gains appariés en médiane d’environ 1.03–1.08× ; le rapport d’ingénierie donne les intervalles d’incertitude, les résultats de fidélité de la quantization et les mesures du kernel Metal personnalisé.

Pour préparer les fiches modèle et des exports vérifiés en vue de la publication, installe les extras de référence et exécute :

python -m scripts.prepare_hub --account YOUR_HF_USERNAME
hf upload YOUR_HF_USERNAME/laya-mlx models/hub/laya-mlx . --exclude '.cache/*'

Le script de préparation vérifie chaque tenseur exporté contre sa source FP16 d’origine. Téléverse les deux autres dossiers préparés de la même manière, puis utilise hf cache verify REPO_ID --local-dir EXPORT_PATH pour vérifier les fichiers distants.

Attribution et licence

Apache-2.0 ; voir LICENSE et NOTICE. Laya et ses poids pré-entraînés sont de Convai Innovations et des contributeurs en amont. La construction des prompts, le formatage de sortie, le routage de langue, les utilitaires d’e-mail et les presets sont adaptés de NandhaKishorM/laya au commit 573e5b62696ba441230cd6be71d593331b5d23af. L’architecture neuronale est réimplémentée dans MLX en suivant Laya et Hugging Face ModernBERT.

Maintenance et versions

Ce projet suit le comportement de Laya en amont au moyen d’une implémentation MLX native. Les correctifs compatibles avec l’amont ont la priorité sur les variantes de modèle indépendantes, les API de service et les démos supplémentaires. Cela reste un portage sélectif, non une revendication de parité complète avec l’API amont.

Pour publier, mets à jour la version dans pyproject.toml, laya_mlx/__init__.py et uv.lock, puis pousse un tag vX.Y.Z correspondant. GitHub Actions exécute la suite de tests macOS, valide la cohérence de version, construit et vérifie la wheel et la distribution source, les publie sur PyPI à l’aide du secret PYPI_API_TOKEN du dépôt, et crée une release GitHub. Un test ou une build en échec empêche la publication.