Benchmark de 8 serveurs LLM locaux pour Qwen 3.5 sur Mac : Résultats surprenants en 2026
Un benchmark détaillé de huit serveurs LLM locaux pour exécuter Qwen 3.5 35B MoE a été réalisé sur un MacBook Pro avec M2 Max. Avec un seul utilisateur, l'écart de vitesse n'était que de 2 %, mais avec deux requêtes parallèles, un framework a offert un gain de vitesse de 2,17× tandis qu'un autre est tombé à 0,85×. L'analyse a révélé des nuances critiques en matière de batching continu, de métriques de vitesse fantômes et de problèmes de gestion de la mémoire.
Pourquoi exécuter des LLM localement : Trois raisons techniques
Pour les développeurs et les ingénieurs, exécuter des modèles localement répond à trois défis clés :
- Confidentialité des données. Traiter contrats, spécifications et discussions clients sans les envoyer vers des API cloud. Un modèle local anonymise les données personnelles (noms, identifiants) en temps réel.
- Agents de codage autonomes. Des outils comme OpenCode et Aider nécessitent un endpoint compatible OpenAI. Configurer
base_url: http://mac.local:8000/v1permet d'exécuter des agents sans télémétrie ni limites de taux, économisant des tokens cloud. - Aucune latence réseau. Un modèle 35B en quantification 4-bit sur M2 Max génère des réponses plus vite que les API cloud grâce à un temps d'aller-retour nul. Sur 64 Go de mémoire unifiée, le modèle occupe ~20 Go, laissant de la place pour le KV cache.
Ces facteurs font d'un serveur local sur Mac une alternative viable pour un usage professionnel, surtout avec des données sensibles.
Pourquoi MLX surpasse llama.cpp sur Apple Silicon
Le choix de MLX plutôt que llama.cpp découle de l'architecture d'Apple Silicon :
- Mémoire unifiée. Sur les puces M-series, CPU et GPU partagent un seul pool de mémoire. Le modèle 35B occupe 20 Go sans copie entre RAM et VRAM (contrairement à NVIDIA).
- Performances. MLX est constamment 10-30 % plus rapide que llama.cpp grâce à une compilation native Metal. M2 Max offre ~400 Go/s de bande passante mémoire, permettant 50-80 tokens/sec en temps réel.
- Batching continu natif. L'écosystème MLX gère les requêtes parallèles nativement, tandis que llama.cpp nécessite une configuration supplémentaire.
Important : Metal ne supporte pas le partage de contexte GPU entre processus. Tous les tests ont utilisé un seul serveur pour éviter les conflits de ressources.
Frameworks comparés : Sélection et critères
Le benchmark incluait six serveurs de l'écosystème MLX (deux exclus pour incompatibilité avec Qwen 3.5 35B MoE) :
- mlx-openai-server (Python 3.11) — support du batching continu, multi-modèles via YAML, sortie structurée.
- mlx-omni-server (Python 3.11+) — double API (OpenAI + Anthropic), TTS/STT.
- Rapid-MLX (Python 3.10+) — déploiement facile, 1900+ tests, intégration Cursor/Aider.
- vllm-mlx (Python 3.10+) — KV cache paginé, multimodalité, mais avec un bug critique dans l'analyseur SSE.
- omlx (Python) — KV cache hiérarchique (RAM + SSD), tableau de bord web, mais limite stricte à 32768 tokens.
- mlx-vlm (Python 3.10+) — support de 40+ architectures, mais ralentissement pathologique sur les prompts longs.
Exclus :
- higgs (Rust) — ne supporte pas
qwen3_5_moe. - mlx-serve (Zig) — nécessite macOS 15 (Sequoia).
Critère de sélection clé : compatibilité native avec le format MLX de Qwen 3.5 35B MoE, sans conversion nécessaire.
Méthodologie du benchmark : Comment les performances ont été mesurées
Les tests ont été exécutés sur un MacBook Pro 16" M2 Max (64 Go de mémoire unifiée) avec mlx-community/Qwen3.5-35B-A3B-4bit. Le harnais de benchmark Python incluait :
- Scénarios de charge :
* run_single — traitement séquentiel de 8 prompts (100 à 53000 tokens).
* run_double_batch — deux requêtes parallèles via barrière asyncio :
gate = asyncio.Event()
task_a = create_task(chat_stream(..., start_gate=gate))
task_b = create_task(chat_stream(..., start_gate=gate))
await asyncio.sleep(0.05)
gate.set()
res_a, res_b = await gather(task_a, task_b)
- Métriques :
* wall_tps_p50 — tokens/sec médian au temps réel (métrique principale).
* ttft_p50 — temps jusqu'au premier token (TTFT).
* speedup — efficacité du batching (rapport wall_tps pour deux vs. une requête).
La médiane a été utilisée plutôt que la moyenne pour éviter les outliers dus à la variance de longueur des prompts. Chaque test répété 5 fois.
Résultats : Utilisateur unique vs. traitement parallèle
Les performances en utilisateur unique étaient similaires entre serveurs (dans une fourchette de 2 %). Les écarts sont apparus avec deux requêtes parallèles :
- mlx-openai-server a atteint 2,17× de gain — le seul framework avec un batching continu efficace.
- vllm-mlx affichait un 14000 tokens/sec fantôme dû à une erreur d'analyseur SSE faussant gen_tps.
- Rapid-MLX manque de génération en streaming, donc TTFT égale le temps de réponse total.
- mlx-omni-server est tombé à 0,85× de gain sans
--workers 2. - omlx renvoyait HTTP 400 sur les prompts >32768 tokens.
- mlx-vlm présentait des chutes cycliques de vitesse (790 → 3,5 tok/s) dues à des problèmes GC.
Remarquable : la complexité quadratique de l'attention dans certaines implémentations faisait que les prefills de 52k tokens prenaient 31 minutes au lieu de 2-3.
Enseignements clés pour les développeurs
- Le batching continu est crucial pour les scénarios multi-utilisateurs. Seul mlx-openai-server a offert un vrai gain sur les requêtes parallèles. Les autres ne les traitaient pas en parallèle ou nécessitaient des ajustements manuels des workers.
- Validez les métriques sur des données réelles. Les 14000 tok/s fantômes de vllm-mlx venaient de bugs dans la gestion des flux SSE, rendant gen_tps peu fiable.
- La mémoire unifiée est un atout, mais pas une panacée. Malgré les pools partagés, un KV cache hiérarchique (comme omlx) compte pour les contextes très longs.
- Testez les prompts longs. Le ralentissement pathologique de mlx-vlm sur les prompts >30k tokens peut le rendre inutilisable pour les tâches à grand contexte.
- La version Python affecte la stabilité. Rapid-MLX avec 1900+ tests était le plus robuste mais perdait en vitesse de batching.
— Editorial Team
Aucun commentaire pour le moment.