# Benchmark 8 lokalnych serwerów LLM dla Qwen 3.5 na Mac: nieoczekiwane wyniki w 2026 roku
Na MacBooku Pro z M2 Max przeprowadzono szczegółowy benchmark ośmiu lokalnych serwerów LLM do uruchamiania Qwen 3.5 35B MoE. Przy pracy z jednym użytkownikiem różnica w prędkości wyniosła zaledwie 2%, ale przy dwóch równoległych zapytaniach jeden framework pokazał 2,17-krotne przyspieszenie, a inny — degradację do 0,85x. Analiza ujawniła krytyczne niuanse continuous batching, pozorne wskaźniki prędkości i problemy z zarządzaniem pamięcią.
Po co uruchamiać LLM lokalnie: trzy techniczne powody
Dla programistów i inżynierów lokalne wdrażanie modeli rozwiązuje trzy kluczowe problemy:
- Prywatność danych. Przetwarzanie umów, specyfikacji i korespondencji z klientami bez przesyłania do chmurowych API. Lokalny model anonimizuje dane osobowe (imiona i nazwiska, dane rekwizytowe) w czasie rzeczywistym.
- Autonomiczne agenty kodujące. Narzędzia takie jak OpenCode i Aider wymagają endpointu kompatybilnego z OpenAI. Ustawienie
base_url: http://mac.local:8000/v1pozwala uruchamiać agentów bez telemetrii i limitów rate-limit, oszczędzając na tokenach chmurowych usług. - Brak opóźnień sieciowych. Model 35B w 4-bitowej kwantyzacji na M2 Max generuje odpowiedzi szybciej niż chmurowe API dzięki braku round-trip time. Na 64GB unified memory model zajmuje ~20GB, pozostawiając zasoby na KV cache.
Te czynniki czynią lokalny serwer na Macu realną alternatywą dla użytku profesjonalnego, zwłaszcza przy pracy z wrażliwymi danymi.
Dlaczego MLX jest lepsze od llama.cpp na Apple Silicon
Wybór MLX zamiast llama.cpp wynika z architektury Apple Silicon:
- Unified memory. W serii M CPU i GPU korzystają z wspólnego puli pamięci. Model 35B w 20GB jest przechowywany w jednym miejscu bez kopiowania między RAM a VRAM (w przeciwieństwie do NVIDIA).
- Wydajność. MLX jest stabilnie szybsze od llama.cpp o 10-30% dzięki natywnej kompilacji w Metal. M2 Max zapewnia ~400 GB/s memory bandwidth, co pozwala generować 50-80 tokenów/sek w czasie rzeczywistym.
- Natywny continuous batching. W ekosystemie MLX obsługa równoległego przetwarzania zapytań jest zaimplementowana „od razu”, podczas gdy w llama.cpp wymaga dodatkowej konfiguracji.
Ważne: Metal nie obsługuje podziału kontekstu GPU między procesami. Wszystkie testy przeprowadzono ściśle na jednym serwerze, co eliminuje konflikty zasobów.
Porównywane frameworki: selekcja i kryteria
W benchmarku znalazło się sześć serwerów z ekosystemu MLX (dwa odrzucono z powodu niekompatybilności z Qwen 3.5 35B MoE):
- mlx-openai-server (Python 3.11) — obsługa continuous batching, multi-model przez YAML, structured output.
- mlx-omni-server (Python 3.11+) — podwójne API (OpenAI + Anthropic), TTS/STT.
- Rapid-MLX (Python 3.10+) — prostota wdrożenia, 1900+ testów, integracja z Cursor/Aider.
- vllm-mlx (Python 3.10+) — paged KV cache, multimodalność, ale z krytycznym błędem w SSE-parserze.
- omlx (Python) — tiered KV cache (RAM + SSD), web-dashboard, ale z sztywnym limitem 32768 tokenów.
- mlx-vlm (Python 3.10+) — obsługa 40+ architektur, ale z patologicznym spowolnieniem na długich promptach.
Odrzucone:
- higgs (Rust) — nie obsługuje
qwen3_5_moe. - mlx-serve (Zig) — wymaga macOS 15 (Sequoia).
Kluczowe kryterium selekcji — kompatybilność z Qwen 3.5 35B MoE w natywnym formacie MLX bez konwersji.
Metodologia benchmarku: jak mierzono wydajność
Testy przeprowadzono na MacBooku Pro 16" M2 Max (64GB unified memory) z modelem mlx-community/Qwen3.5-35B-A3B-4bit. Harness benchmarkowy w Pythonie obejmował:
- Scenariusze obciążenia:
* run_single — sekwencyjne przetwarzanie 8 promptów (od 100 do 53000 tokenów).
* run_double_batch — dwa równoległe zapytania przez barierę 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)
- Metryki:
* wall_tps_p50 — mediana tokenów/sek po wall-clock (główny wskaźnik).
* ttft_p50 — czas do pierwszego tokena (TTFT).
* speedup — efektywność batchingu (stosunek wall_tps przy dwóch zapytaniach do jednego).
Aby wykluczyć anomalie, użyto mediany zamiast średniej z powodu ekstremalnego rozrzutu długości promptów. Każdy test powtarzano 5 razy.
Wyniki: single-user vs. przetwarzanie równoległe
Przy pracy z jednym użytkownikiem wszystkie serwery pokazały podobną wydajność (różnica w granicach 2%). Krytyczne różnice ujawniły się przy dwóch równoległych zapytaniach:
- mlx-openai-server osiągnął 2,17× speedup — jedyny framework z efektywnym continuous batching.
- vllm-mlx pokazywał pozorne 14000 tokens/sec z powodu błędu w SSE-parserze, fałszującego gen_tps.
- Rapid-MLX nie obsługuje post-stream generation, dlatego TTFT = całemu czasowi odpowiedzi.
- mlx-omni-server pokazał degradację do 0,85× speedup bez parametru
--workers 2. - omlx zwracał HTTP 400 na promptach >32768 tokenów.
- mlx-vlm miał cykliczne skoki prędkości (790 → 3,5 tok/s) z powodu problemów z GC.
Szczególną uwagę zasługuje kwadratowa złożoność attention w niektórych implementacjach — na promptach 52k tokenów prefill trwał 31 minut zamiast oczekiwanych 2-3 minut.
Co ważne: kluczowe wnioski dla programistów
- Continuous batching jest kluczowy dla scenariuszy wieloużytkownikowych. Tylko mlx-openai-server zapewnił realne przyspieszenie przy równoległych zapytaniach. Pozostałe albo nie przetwarzały ich równolegle, albo wymagały ręcznej konfiguracji workerów.
- Sprawdzaj metryki na rzeczywistych danych. Pozorne wskaźniki w vllm-mlx (14000 tok/s) wynikały z błędu w obsłudze strumienia SSE, co czyni gen_tps niewiarygodnym.
- Unified memory — zaleta, ale nie cudowne lekarstwo. Mimo wspólnego puli pamięci, tiered KV cache (jak w omlx) pozostaje aktualny dla bardzo długich kontekstów.
- Testuj na długich promptach. Patologiczne spowolnienie w mlx-vlm na promptach >30k tokenów może uczynić serwer nieprzydatnym do zadań z dużym kontekstem.
- Wersja Pythona wpływa na stabilność. Rapid-MLX z 1900+ testami pokazał najlepszą odporność, ale przegrał w prędkości batchingu.
— Editorial Team
Brak komentarzy.