Qwen 3.8-27B su un M1 Ultra: Da 12.1 a 20.3 tok/s misurando tutto
5 esperimenti controllati con Qwen 3.8-27B su un 128 GB M1 Ultra: scheduler QoS pinning, decodifica speculativa MTP, un challenger GGUF, e un MoE che è fallito per il sovraccarico di dispatch — eseguito con Kimi K3 in modalità high thinking, con le prove powermetrics per ogni chiamata.
Developed by Robert E. Beckner III (Merlin) | rbeckner.com
I pesi per Qwen3.8-27B sono arrivati al mattino, e lo stesso giorno ho iniziato un'avventura con Kimi K3 in modalità high-thinking. Ho iniziato tutto con una singola domanda: questo nuovo denso 27B — facilmente il modello open più forte nella sua classe di dimensioni — può servire i miei agenti di coding a velocità reale su un Mac Studio con un M1 Ultra, 20 core CPU, 128 GB di memoria unificata, e 800 GB/s di larghezza di banda della memoria? La conoscenza comune dice che i modelli densi decodificano lentamente su Apple Silicon perché sono limitati dalla larghezza di banda della memoria, e volevo capire se questa quantità di RAM e larghezza di banda potesse rompere il modello. I pesi MLX a 4 bit sono arrivati a 15 GB, il framework era aggiornato, non c'era altro in esecuzione sulla macchina, e il primo numero era 12.1 token per secondo. Mi sembrava sbagliato.
Avevo già circolato questo tipo di esperimento per un po'. A marzo, lavorando con un altro AI nel mio laboratorio di raffinazione autonoma, abbiamo tracciato un muro di sotto 20 tok/s per modelli 30B su un M4 Max a causa della fame di larghezza di banda a livello OS — e quell'indagine ha lasciato un playbook: powermetrics prima, campionamento per cluster, pinning della memoria prima di incolpare. La corsa questa volta è iniziata dalla pura curiosità: avevo sentito delle ottimizzazioni di Qwen3.8, e volevo sapere se funzionerebbe veloce fuori dalla scatola su questa macchina con questa quantità di RAM. Solo a metà esperimento la realtà limitata dalla larghezza di banda si è fatta sentire. Ciò che è seguito è stato uno sprint di esperimenti controllati che abbiamo progettato insieme — Kimi ha redatto le ipotesi e letto i contatori hardware al mio fianco mentre io tenevo le mani sul metallo. Ogni esperimento è stato costruito per accogliere o condannare un singolo strato dello stack. Le vittorie sono venute da 2 posti che il folklore dell'inferenza locale sottovaluta: suggerimenti del scheduler CPU e decodifica speculativa. Le alternative 2 alla moda — una build GGUF servita da llama.cpp, e un modello mixture-of-experts che sembrava imbattibile sul carta — hanno entrambe perso sulla misurazione. Questo articolo è l'intero percorso di prove che abbiamo costruito insieme, perché il metodo si è rivelato più prezioso di qualsiasi singolo numero.
Il relay di rete e il server erano i primi sospetti, e entrambi erano innocenti#
Il percorso di servire aveva 3 hop: la mia workstation, un relay esecutivo basato su SSH, e il server del modello sul loopback dello Studio. Incolpare il relay sarebbe stato facile, quindi lo abbiamo misurato prima. I tempi propri del server dicevano 12.1 tok/s di decodifica; un benchmark di generazione in-process raw, senza server e senza relay, ha prodotto 11.5 tok/s. Il trasporto costava circa 25% per overhead di connessione, e il modello stesso era lo strato lento.
Un bug di trasporto valeva comunque la pena di essere risolto, ed è il tipo di dettaglio che costa ore se non lo hai mai visto: un relay Python TCP che usa un read(65536) buffered si blocca contro un protocollo locale chiacchierone, perché il lettore buffered aspetta un buffer pieno o EOF prima di restituire. Passare a read1(), che restituisce dopo una singola lettura sottostante, ha fatto comportare il relay. Il sintomo sembrava un blocco di rete; la causa era la semantica stdio.
Con il trasporto acciaccato, abbiamo lavorato attraverso lo strato sysctl usuale. Aumentare il limite di memoria wired GPU (iogpu.wired_limit_mb) non ha cambiato nulla con 128 GB di RAM liberi, quindi l'abbiamo ripristinato. Il processo Python era nativo arm64, MLX riportava la GPU come suo dispositivo predefinito, e powermetrics mostrava la GPU al 54-62% di occupazione attiva disegnando 20-23 W durante la decodifica. Ogni sospetto in questa tornata è andato libero — che è stato stesso il risultato utile, perché ha indirizzato l'investigazione verso la CPU.
Il scheduler aveva messo l'inferenza sui core di efficienza#
Leggere powermetrics per cluster CPU invece che per macchina ha rivelato la prima scoperta reale. Durante il decode, i cluster di efficienza funzionavano al 75-93% occupato mentre i cluster di performance erano inattivi al 1-12%. Qualsiasi cosa lanciata oltre SSH eredita una classe di qualità di servizio che macOS legge come lavoro a bassa priorità, e il scheduler rispettava quel suggerimento mantenendo un carico di lavoro critico in termini di latenza sui core lenti.
La correzione era una riga di 1 ctypes, eseguita prima del caricamento dei pesi del modello:
Quella pin, più il caricamento della torre di testo del modello tramite mlx-lm invece che l'intero stack di visione, ha spostato il decode grezzo da 11,5 a 15,1 tok/s — un guadagno del 31% grazie al posizionamento del scheduler e a un loader più snello, senza alcun cambiamento al modello. Successivamente abbiamo scoperto che l'attacco della pin ha un limite: sposta il thread chiamante, e i thread worker di MLX mantengono la propria classe di scheduling. Per questo modello il thread principale portava abbastanza lavoro da fare la differenza.
Il decoding speculativo ha consegnato il salto che nessun sysctl poteva#
Il più grande singolo vantaggio è venuto da una funzionalità che il modello di base già possedeva. Qwen3.8 viene fornito con una testa di previsione multi-token — un piccolo modulo ausiliario che redige diversi token in anticipo — e il convertitore MLX elimina silenziosamente quei tensori 15 durante la conversione. Un pacchetto della comunità ri-hosta la testa come un 253 MB redattore autonomo, che mi ha permesso di riassociare con il modello con cui è stato addestrato.
Il modello di servizio è draft-then-verify: il drafter propone alcuni token, il modello completo li verifica in un pass1, e tutti i token accettati contano. Con --draft-model e --draft-kind mtp sul server, l'accettazione del draft ha misurato il 94% su prompt di coding e chat realistici, e il decode è passato da 15,1 a 20,3 tok/s lato server — 13,4 tok/s end-to-end tramite il relay, su 9,0. La GPU ha raccontato la stessa storia: 77% residency attiva, tutto a pieno 1296 MHz, consumando 48 W, con il cluster di prestazioni 97% occupato. Il prefill è migliorato nella stessa upgrade, da 14.7 tok/s a 18-55 a seconda della forma del prompt.
Decode speed by configuration (tok/s, M1 Ultra, 27B-4bit)Chart data
I post della community hanno posizionato llama.cpp con una bozza speculativa a 25-32 tok/s per questa classe di modelli su M1 Ultra, ben avanti rispetto ai nostri numeri MLX, quindi abbiamo dato al concorrente un ring equo: il Q4_K_M GGUF ufficiale, un modello di bozza solo MTP corrispondente, e una build corrente di llama.cpp con accelerazione Metal confermata attiva. Il percorso GGUF ha misurato 9.2 tok/s di base e 12.2 con la bozza — circa 40% dietro lo stack MLX che doveva deporre.
llama.cpp rimane un'eccellente ingegneria; il divario probabilmente risiede nel modo in cui i kernel Metal di ogni runtime gestiscono questo checkpoint su questo chip. La lezione duratura è più semplice: un numero che qualcun altro ha misurato sulla propria macchina, con la propria build e il proprio checkpoint è un'ipotesi sul tuo. I 18 GB di pesi del concorrente hanno lasciato la macchina lo stesso pomeriggio, e il binario llama.cpp di 50 MB è rimasto per future rivincite. Ho scritto di questa regola prima come benchmark-driven development — ha portato SEOReport from heuristics to a product — e qui ha salvato una migrazione.
Un MoE attivo 3B avrebbe dovuto vincere, e il metro GPU ha spiegato la perdita#
L'ultimo concorrente aveva la teoria più forte — la stessa conoscenza comune che avevamo deciso di testare all'inizio. I modelli densi decodificano lentamente su Apple Silicon perché ogni token generato paga per leggere quasi tutti i pesi; un modello mixture-of-experts con 35B parametri totali attiva solo circa 3B per token, quindi su una macchina limitata dalla larghezza di banda la sua decodifica dovrebbe essere più veloce di un denso 27B — ogni token legge circa l'11% dei pesi. Abbiamo scaricato il MoE 4-bit e il suo drafter MTP corrispondente, riscaldato il server, e misurato 14.7 tok/s grezzi: la stessa velocità del modello denso che doveva umiliare. Con la decodifica speculativa ha raggiunto 19 tok/s su prompt realistici e 26.3 su testo altamente prevedibile — un pareggio rispetto al 20.3 del modello denso, senza argomento di qualità per spezzare il pareggio.
powermetrics ha identificato il regime in 1 campione. Durante la decodifica MoE la GPU era al 0-3% di residency attiva mentre i cluster di efficienza scendevano al 60-90%. Il modello spende il suo budget per token sul lato CPU, e il timing a livello di operazione ha mostrato perché: ogni operazione dispatchata costa 50-100 microsecondi di overhead, e questa architettura ibrida — GatedDeltaNet linear attention più 256 esperti dietro uno stato nascosto piccolo 2048-wide — emette circa 78 operazioni per layer su 40 layer. A dimensione batch 1, la GPU finisce ogni piccolo kernel prima che la CPU possa mettere in coda il successivo. La macchina era limitata dal dispatch, e i carichi di lavoro limitati dal dispatch non guadagnano nulla leggendo meno pesi per token.
Diagram source
graph TB
A[Decodifica lenta
a batch 1] --> B{GPU occupata
durante la decodifica?}
B -->|sì| C[Limitato dalla larghezza di banda
meno byte vince]
B -->|no| D[Limitato dal dispatch
meno operazioni vince]
C --> E[MoE aiuta
quantizzazione più piccola aiuta]
D --> F[Decodifica speculativa
aiuta di più]
style E fill:transparent,stroke:#10B981,stroke-width:2px
style F fill:transparent,stroke:#3B82F6,stroke-width:2px
Per chiudere il ciclo, abbiamo provato che il percorso GPU stesso era sano con un hog sintetico: un loop di moltiplicazione matrice 8192³ ha raggiunto 10.8 TFLOPS in bfloat16 con la GPU al 97-100% di residency e 68 W. Le prove esterne hanno confermato la diagnosi locale — stime indipendenti mettono questa classe MoE vicino a 21.7 tok/s su un M2 Ultra, e un problema OpenVINO documenta lo stesso modello che perde contro un denso 8B su backend con percorsi di dispatch più deboli. Il denso 27B con il suo redattore MTP ha mantenuto lo slot di produzione, e 38,5 GB di MoE pesi sono stati eliminati la stessa sera.
GPU active residency during decode (powermetrics, %)Chart data
GPU active residency (%)
dense 27B + MTP
77
MoE 35B-A3B
2
matmul hog test
100
Cosa hanno lasciato gli esperimenti controllati 5#
La macchina ora serve Qwen 3.8-27B a 20.3 tok/s lato server, un miglioramento di 68% rispetto a dove è iniziato il giorno, e il rendimento più durevole è una checklist che riusiamo su ogni futura scatola:
Decodifica a dimensione batch 1 ha 2 regimi, limitato dalla larghezza di banda e limitato dalla distribuzione, e un campione di 2 secondi powermetrics identifica il tuo prima di spendere un download sul fix sbagliato.
La decodifica speculativa è il controllo di servizio a più alto impatto per una scatola a singolo utente. Ha aggiunto 34% qui e si complica con ogni altra ottimizzazione, perché i batch di verifica lavorano la GPU mentre il redattore assorbe le lacune inattive.
Scheduler QoS è un vero parametro di inferenza su macOS. Qualsiasi cosa lanciata oltre SSH dovrebbe fissare i suoi thread deliberatamente, e l'effetto del fissaggio è verificabile per cluster piuttosto che per vibrazioni.
Le affermazioni di throughput della comunità viaggiano male attraverso checkpoint, build e chip. Un benchmark locale è più economico di una migrazione.
La cancellazione è parte del flusso di lavoro. Ogni sfidante che ha perso ha lasciato il disco entro la giornata, il che mantiene il prossimo esperimento onesto e la macchina snella.
La parte soddisfacente di questa avventura è che le risposte erano tutte nei contatori hardware, in attesa che qualcuno faccia domande precise — e questa volta le abbiamo chieste insieme. Un modello locale che hai misurato vale più di un modello più grande che hai ipotizzato — lo stesso ritorno composto che ottengo trattando local AI gains as infrastructure piuttosto che trivia. Ho iniziato tutto la mattina in cui i pesi sono arrivati, e Kimi K3 è rimasta nella lotta per ogni esperimento, ogni lettura del contatore, e questa relazione dalle note di laboratorio della stessa sera. Il prossimo esperimento è già in coda: i grafici di decodifica compilati promettono di ridurre il costo di distribuzione per operazione che ha deciso il verdetto MoE, e quando una release MLX arriva, una, la rivincita richiederà un pomeriggio e 1 download.