Ottimizzazione dell'inferenza on-device per Apple Silicon

Un motore locale personalizzato che migliora il throughput di prefill e decode.

AutoriPerplexity Engineering

Il calcolo ibrido su Apple silicon orchestra un'attività tra intelligenza di frontiera nel cloud e un modello locale sul Mac. I modelli nel cloud gestiscono ricerca e ragionamento, mentre un modello locale opera con file e app privati sul Mac.

Affinché questa divisione del lavoro risulti fluida, l'inferenza locale deve tenere il passo con il resto dell'attività. Ciò richiede un motore in grado di elaborare i prompt rapidamente e di sostenere un'elevata velocità di generazione dei token.

Lily, il nostro motore di inferenza locale leggero, è stato progettato specificamente per il silicio Apple e Qwen3.6-35B-A3B, con ottimizzazioni separate per prefill e decode. Una demo autonoma è pubblicamente disponibile su GitHub.

Introduzione

Un modo comune per eseguire LLM su un Mac consiste nell'utilizzare MLX, il framework di machine learning open-source di Apple per Apple silicon. La sua libreria complementare, MLX-LM, aggiunge i componenti necessari per caricare e generare testo con un'ampia gamma di modelli linguistici. Insieme, MLX e MLX-LM forniscono uno stack pronto all'uso e di uso generale per l'inferenza di LLM locali.

Qwen3.6-35B-A3B è un modello ibrido e sparso: utilizza il routing mixture-of-experts (MoE) e combina stati ricorrenti a dimensione fissa con l'attenzione completa. Queste scelte architetturali riducono la quantità di calcolo richiesta, ma creano anche carichi di lavoro irregolari. I token vengono instradati verso pesi di esperti differenti e gli stati ricorrenti sono di natura sequenziale.

MLX-LM seleziona già kernel ottimizzati per le fasi di inferenza e le forme comuni dei carichi di lavoro, ma le sue operazioni riutilizzabili devono supportare molte architetture di modelli. Un motore dedicato a Qwen può specializzarsi a livello di modello e di runtime, coordinando kernel, movimento dei dati e pianificazione attorno alla struttura fissa del modello.

Lily implementa questa specializzazione end-to-end in un unico processo. Un runtime in Rust carica il checkpoint del modello e gestisce lo stato della Sessione e il ciclo di generazione, un'API di completamento della chat compatibile con OpenAI accetta richieste e trasmette token in streaming, e i kernel Metal personalizzati eseguono operazioni specifiche per Qwen. Né PyTorch né MLX si trovano nel percorso di esecuzione.

Dove si collocano generalità e specializzazione nei due stack di inferenza. MLX-LM descrive il modello come operazioni su array MLX componibili, che MLX pianifica tramite kernel riutilizzabili. Lily inserisce invece la struttura del modello, i piani di esecuzione specifici per fase e la selezione dei kernel in un unico runtime in Rust sviluppato attorno a Qwen e Apple silicon.

Misuriamo le prestazioni di prefill e decode separatamente. Il throughput di prefill rileva la rapidità con cui il motore elabora il prompt; il throughput di decode rileva la rapidità con cui genera i token di output.

Confrontiamo Qwen3.6-35B-A3B su un singolo MacBook Pro alimentato da un M5 Max con GPU a 40 core e 128 GB di memoria unificata. Su dieci lunghezze del prompt per il prefill e dieci lunghezze di contesto per il decode, da 256 a 128K token (K = 1.024), il motore registra in media un throughput di prefill 1,22× quello di MLX-LM e un throughput di decode 1,32× superiore. Con un prompt da 4K token e un contesto di decode da 4K token, il motore personalizzato raggiunge 5.749,9 token di prefill al secondo e 186,6 token di decode al secondo, rispetto a 4.737,5 e 140,9 di MLX-LM. In una sessione multi-turno, questi risparmi di tempo si accumulano a ogni chiamata aggiuntiva del modello.

Throughput medio aritmetico su dieci lunghezze equiponderate da 256 a 128K token. Lily raggiunge in media 4.135 token/s di prefill contro 3.383 di MLX-LM (1,22×), e 167,1 token/s di decode contro 126,9 (1,32×). Il prefill varia con la lunghezza del prompt; il decode varia con la lunghezza del contesto.

Successivamente, spieghiamo in che modo l'architettura di Qwen crea opportunità di ottimizzazione specifiche per il modello su Apple silicon. Esaminiamo quindi le modifiche conseguenti relative a prefill e decode. Trattiamo inoltre gli aspetti in cui un'ulteriore ottimizzazione cessa di essere vantaggiosa, prima di concludere con un confronto end-to-end rispetto a MLX-LM.

Opportunità di ottimizzazione specifiche per Qwen su Apple silicon

Qwen crea tre distinte forme di carico di lavoro

Qwen3.6-35B-A3B contiene 35 miliardi di parametri ma ne attiva solo circa 3 miliardi per ciascun token. Un router assegna un punteggio a 256 subnetwork di esperti e ne seleziona otto, insieme a un esperto condiviso che elabora ogni token. Questo design MoE sparso riduce il calcolo ma produce lavoro irregolare: gli esperti ricevono un numero diverso di token e ciascun token richiede i pesi da una diversa combinazione di esperti.

Qwen combina inoltre 10 layer con attenzione completa e 30 layer Gated DeltaNet. Questi due tipi di layer conservano le informazioni precedenti in modi differenti.

I layer di attenzione utilizzano l'attenzione a query raggruppate (GQA). Qwen dispone di 16 teste di query e due teste di chiave-valore (KV), con otto teste di query che condividono ciascuna testa KV. La condivisione rende la KV cache più piccola e consente di riutilizzare i dati memorizzati nella cache tra le teste di query. La cache memorizza comunque nuove chiavi e nuovi valori per ogni token, pertanto ciascun passaggio di decode legge una quantità maggiore di dati all'aumentare del contesto.

Gated DeltaNet comprime invece le informazioni precedenti in uno stato ricorrente a dimensione fissa. Un gate appreso controlla quanta parte dello stato esistente trattenere, mentre un aggiornamento delta incorpora le informazioni del token corrente. Il modello definisce questi aggiornamenti in modo ricorrente, pertanto ciascun token dipende dallo stato prodotto dal token precedente. Durante il prefill, tuttavia, un motore può valutare lo stesso calcolo in due modi. Può scansionare direttamente i token trasportando in avanti lo stato, oppure riorganizzare gli aggiornamenti in blocchi che espongono un numero maggiore di operazioni di matrici e parallelismo a livello di token. Quale approccio sia più veloce dipende dalle dimensioni del modello, dal carico di lavoro e dall'hardware.

Insieme, queste strutture creano tre pattern computazionali: gruppi di esperti non uniformi, attenzione su una cache in crescita e una ricorrenza a dimensione fissa che può essere valutata direttamente o a blocchi.

Apple silicon fornisce percorsi differenti per carichi di lavoro differenti

Il prefill elabora molte righe di attivazione dei token del prompt contemporaneamente. Il carico di lavoro locale qui considerato decodifica in genere una richiesta alla volta (batch 1) ed elabora una nuova riga per passaggio. Questa differenza modifica il modo in cui vengono utilizzati gli stessi pesi del modello. Il prefill può riutilizzare ciascun blocco di pesi su centinaia o migliaia di righe. Il decode in gran parte non può farlo, poiché ciascun nuovo token richiede un ulteriore passaggio attraverso i pesi.

Apple silicon posiziona CPU e GPU dietro la memoria unificata, un unico pool di memoria fisica accessibile a entrambe. Ciò consente al modello di rimanere residente senza mantenere una copia GPU separata, ma non rende gratuito il movimento dei dati. La lettura di pesi e valori intermedi consuma comunque larghezza di banda della memoria, mentre i registri e gli altri storage on-chip sono più veloci ma molto più piccoli.

La GPU M5 fornisce inoltre percorsi di calcolo differenti. I layer lineari del prefill utilizzano la moltiplicazione generale matrice-matrice (GEMM), applicando una matrice di pesi a molte righe contemporaneamente. I GEMM compatibili possono utilizzare il Neural Accelerator in ciascun core della GPU tramite le operazioni tensoriali di Metal 4. Il decode batch-1 utilizza invece la moltiplicazione generale matrice-vettore (GEMV), applicando gli stessi pesi a una riga. Con un ridotto riutilizzo dei pesi, il GEMV è limitato principalmente dalla larghezza di banda della memoria ed è più adatto alle unità di logica aritmetica (ALU) vettoriali della GPU piuttosto che ai Neural Accelerators progettati per operazioni su matrici con un maggiore riutilizzo dei dati.

Questi percorsi di esecuzione non sono esclusivi di Lily. MLX opera sulla stessa memoria unificata e seleziona kernel di matrici e vettori ottimizzati in base alla forma del carico di lavoro. L'implementazione di Qwen in MLX-LM raggruppa già il lavoro degli esperti, valuta Gated DeltaNet con un kernel Metal ricorrente fuso e utilizza l'attenzione consapevole di GQA. Queste funzionalità costituiscono il punto di partenza condiviso per un'inferenza efficiente di Qwen su Apple silicon.

Strategia di ottimizzazione

Il raggio d'azione più limitato di Lily gli consente di coordinare questi percorsi di esecuzione condivisi attorno all'architettura e alle dimensioni esatte di Qwen. Utilizza percorsi GPU specifici per fase, mappa i carichi di lavoro degli esperti, ricorrenti e di attenzione di Qwen per ridurre al minimo il movimento dei dati e seleziona kernel e layout in base alla forma misurata del carico di lavoro. La strategia si compone di tre parti:

  1. Adattare il percorso della GPU alla fase di inferenza. Utilizzare un'esecuzione orientata alle matrici quando il prefill può riutilizzare i pesi su molte righe e un'esecuzione orientata ai vettori quando il decode batch-1 elabora una riga alla volta.
  2. Mappare la struttura di Qwen sulla GPU riducendo al minimo il movimento dei dati. Mantenere i pesi compressi finché non vengono utilizzati, organizzare il lavoro degli esperti instradati senza tornare alla CPU, trattenere lo stato di Gated DeltaNet on-chip attraverso la sua scansione ricorrente e riutilizzare i dati KV condivisi dall'attenzione a query raggruppate.
  3. Adattare i kernel alla forma del carico di lavoro. All'interno di ciascuna fase, selezionare le dimensioni dei tile, i layout di esecuzione e i percorsi di attenzione in base al numero di righe disponibili, alla distribuzione delle righe tra gli esperti, alle dimensioni dell'operazione e alla lunghezza corrente del contesto.

Le sezioni seguenti spiegano queste scelte. Per le ottimizzazioni valutate in ablazioni abbinate su un M5 Max, stimiamo i loro effetti confrontando configurazioni del motore altrimenti identiche che differiscono unicamente per l'ottimizzazione in esame. Poiché questi esperimenti confrontano versioni del nostro motore con se stesso, spiegano i meccanismi piuttosto che scomporre i risultati finali rispetto a MLX-LM.

Prefill: riutilizzare i pesi e mantenere il routing sulla GPU

Il prefill espone molte righe di token contemporaneamente, ma Qwen instrada tali righe in modo irregolare tra gli esperti e aggiorna lo stato ricorrente attraverso la sequenza. Le sue ottimizzazioni si suddividono in tre gruppi: organizzare il lavoro degli esperti sparsi attorno alle righe instradate, mantenere la scansione di Gated DeltaNet on-chip e suddividere i prompt lunghi in blocchi delimitati.

Esecuzione e residenza dei dati per un blocco di prefill delimitato attraverso un layer di Qwen. L'attenzione estende la KV cache, mentre Gated DeltaNet trasporta il proprio stato ricorrente di lavoro nei registri. I metadati di routing degli esperti rimangono sulla GPU, i pesi Q4 rimangono compressi finché non vengono diquantizzati all'interno del GEMM raggruppato e le attivazioni temporanee sono limitate al blocco corrente.

Ottimizzare il calcolo degli esperti sparsi

Diquantizzare i pesi durante la moltiplicazione di matrici

Il checkpoint di Qwen3.6-35B-A3B utilizza la quantizzazione affine a 4 bit basata su gruppi. Ciascun peso viene memorizzato come codice intero a 4 bit, mentre ogni gruppo di 64 pesi condivide una scala e un bias bfloat16 utilizzati per ricostruire i relativi valori. Ciò riduce il modello da 35 miliardi di parametri, passando da circa 70 GB di pesi bfloat16 a un checkpoint da 19,4 GB, rendendo praticico il mantenimento del modello residente sul Mac.

L'operazione tensoriale di Metal 4 utilizzata per la moltiplicazione di matrici consuma operandi bfloat16 anziché la rappresentazione compressa a 4 bit. Prima della moltiplicazione, la GPU deve ricostruire i pesi in bfloat16. Il GEMM raggruppato ottimizzato in Lily esegue questa conversione un piccolo tile di pesi alla volta e mantiene il risultato nella memoria del threadgroup on-chip solo per il tempo necessario a moltiplicarlo per le righe di attivazione instradate. L'accumulo utilizza la virgola mobile a 32 bit e l'output viene scritto in bfloat16. L'array di pesi espanso completo non viene mai creato nella memoria unificata.

Nell'ablazione, la diquantizzazione viene eseguita come un'operazione separata: espande i pesi a 4 bit in un array bfloat16 nella memoria unificata, dopodiché il kernel di matrice legge nuovamente tale array. Con un prompt di 512 token, lo spostamento della diquantizzazione all'interno del GEMM raggruppato ha aumentato il throughput di prefill end-to-end del 77,4% eliminando questa scrittura e lettura intermedia.

Mantenere il routing degli esperti sulla GPU

Il GEMM raggruppato richiede che le righe di attivazione assegnate a ciascun esperto siano memorizzate insieme. Dopo aver selezionato otto esperti per token, un istogramma conta quante assegnazioni sono andate a ciascun esperto. Una scansione dei prefissi trasforma tali conteggi in offset di partenza, un passaggio di dispersione (scatter) posiziona le righe nei rispettivi gruppi di esperti e una mappa di blocchi elenca i blocchi di matrici a dimensione fissa che il GEMM raggruppato deve elaborare.

Il percorso ottimizzato mantiene l'intera sequenza in un unico buffer di comandi, ovvero un batch ordinato di operazioni GPU, per ciascun blocco di prompt. Un'ablazione inserisce invece una pausa per consentire alla CPU di ispezionare gli intermedi di routing e inviare l'operazione successiva. Il mantenimento dell'istogramma e della scansione dei prefissi sulla GPU aggiunge due kernel ma rimuove la sincronizzazione CPU-GPU all'interno di ciascun layer MoE.

Con un prompt di 512 token, l'abilitazione del routing residente su GPU ha aumentato il prefill end-to-end dell'89%. Ciò dimostra anche il motivo per cui il solo conteggio dei kernel può essere fuorviante: il percorso più veloce avvia un numero maggiore di kernel ma non attende mai la CPU all'interno del layer.

Adattare la dimensione del tile al carico degli esperti

Con un prompt di 2K token, l'instradamento di ciascun token verso otto dei 256 esperti produce 16.384 assegnazioni token-esperto, ovvero una media di 64 righe di attivazione per esperto. La distribuzione effettiva è irregolare: alcuni esperti ricevono molte righe, mentre altri ne ricevono poche.

Il GEMM raggruppato divide l'output di ciascun esperto in tile, che sono piccoli blocchi rettangolari dell'output di una moltiplicazione di matrici. Ciascun tile viene assegnato a un threadgroup della GPU. Sulle GPU di Apple silicon, un threadgroup contiene uno o più simdgroup, ciascuno costituito da 32 thread che eseguono istruzioni in lockstep.

Tile più grandi distribuiscono il costo di configurazione su un numero maggiore di righe ed espongono una quantità maggiore di lavoro in parallelo, ma una parte di un tile di grandi dimensioni rimane inattiva quando un esperto riceve solo poche righe. La dimensione dei tile e il numero di simdgroup sono pertanto accoppiati.

Un'ablazione fissa il tile a 16 righe. Rispetto a tale controllo, l'abilitazione del tile a 32 righe con quattro simdgroup ha migliorato il prefill end-to-end del 13,2% a 2K token.

Mantenere lo stato ricorrente on-chip

Durante il prefill, ciascun layer Gated DeltaNet scansiona il prompt in ordine trasportando al contempo in avanti il proprio stato ricorrente. Con la residenza nei registri disabilitata, l'ablazione utilizza una scansione a blocchi. Con un prompt di 2K token, tale percorso sposta 256 MiB (mebibyte) di stato per layer e arresta ripetutamente i thread cooperativi in corrispondenza delle barriere, ovvero punti di sincronizzazione in cui tutti i thread partecipanti devono attendere gli altri.

Lo stato ricorrente è una matrice. Il kernel ottimizzato assegna ciascuna colonna a un simdgroup. Il simdgroup divide la colonna tra i propri thread, carica la colonna nei loro registri una sola volta e trasporta lo stato attraverso l'intera scansione. I thread si scambiano i risultati intermedi tramite operazioni di simdgroup anziché tramite la memoria del threadgroup, ovvero uno storage on-chip condiviso tra un threadgroup. Lo stato completato viene riscritto solo dopo la scansione.

Lo stato e il suo gate utilizzano un formato in virgola mobile a 32 bit poiché i piccoli errori di arrotondamento si aggravano attraverso gli aggiornamenti sequenziali. Le attivazioni di query e chiave rimangono in bfloat16.

Con un prompt di 2K token, l'abilitazione della scansione residente nei registri ha migliorato il prefill end-to-end del 5,6%. I GEMM degli esperti hanno rappresentato circa il 90% del tempo di prefill. La scansione sequenziale non espone una quantità sufficiente di lavoro di matrice riutilizzabile per trarre vantaggio dai Neural Accelerators.

Limitare la memoria temporanea con la suddivisione dei prompt in blocchi

Il runtime elabora un prompt lungo come una sequenza di blocchi delimitati, anziché mantenere in memoria contemporaneamente i dati temporanei per ciascun token del prompt. I pesi del modello rimangono residenti nella memoria unificata, mentre lo stato ricorrente e la KV cache trasportano il contesto da un blocco a quello successivo. Nessun contesto precedente viene scartato.

Senza la suddivisione in blocchi, gli array di attivazione temporanei crescono con l'intero prompt e competono con i pesi del modello, lo stato ricorrente e la KV cache per la memoria unificata. La suddivisione in blocchi mantiene attivi i valori temporanei di un solo segmento alla volta, rilasciando o riutilizzando quindi tale storage prima di elaborare il segmento successivo. Ciò pone un limite massimo alla memoria di lavoro di picco e consente al motore di elaborare prompt più lunghi senza modificare l'output del modello.

Il prefill suddiviso in blocchi è popolare in molti motori ed è cruciale per servire lunghe traiettorie multi-turno in questi ambienti con memoria limitata. Il tempo totale di prefill per i layer di attenzione rimane quadratico rispetto alla lunghezza del prompt, con un overhead aggiuntivo derivante dai caricamenti KV ripetuti dei blocchi precedenti.

Decode: minimizzare i byte spostati per token

Il decode batch-1 elabora una nuova riga alla volta. Con un ridotto riutilizzo dei pesi, il suo throughput dipende principalmente da quanti byte il motore sposta per ciascun token. Le modifiche al decode si suddividono in quattro gruppi: ottimizzare il percorso dei pesi a riga singola, mantenere ciascun passaggio sulla GPU, ridurre il traffico intermedio e di stato e leggere la cache di attenzione in modo efficiente.

Flusso di dati per un passaggio di decode batch-1 e due meccanismi che riducono il tempo di inattività e il traffico della cache. (A) La GPU trasmette in streaming i pesi Q4 e lo stato del modello attraverso l'attenzione, Gated DeltaNet, il routing e i kernel esperti fusi, quindi scrive il token selezionato direttamente nello slot di input del passaggio successivo inviando al contempo una copia alla CPU. (B) La schedulazione consapevole delle dipendenze consente ai kernel indipendenti di sovrapporsi. (C) Il raggruppamento GQA consente a quattro teste di query di condividere il carico di ciascuna riga KV, riducendo otto richieste indipendenti a due carichi condivisi.

Ottimizzare il percorso dei pesi a riga singola

MLX distribuisce già il lavoro a riga singola a kernel matrice-vettore specializzati. Poiché Lily non utilizza MLX, il runtime personalizzato deve fornire la stessa strategia di base. Il nostro GEMV parallelo per riga è progettato per una riga di attivazione. Un simdgroup collabora sull'output leggendo parti diverse della matrice dei pesi in parallelo.

Mantenere ciascun passaggio di decode sulla GPU

Mantenere il passaggio dei token sulla GPU

Ciascun passaggio di decode termina con la selezione del token successivo; il passaggio seguente inizia con quel token come input. L'invio della selezione alla CPU e poi di nuovo alla GPU aggiunge un punto di sincronizzazione a ogni token. Il nostro runtime alterna invece due buffer di comandi e due slot di token residenti su GPU. La GPU seleziona il token con il punteggio più alto e scrive il suo ID di token direttamente nello slot di input per il passaggio di decode successivo, mentre la CPU prepara il lavoro successivo.

Sovrapporre il lavoro GPU indipendente

In un passaggio di decode batch-1 registrato, la generazione di un token ha avviato 795 kernel GPU. Le loro dipendenze hanno formato 555 stadi sequenziali, lasciando alcuni kernel liberi di essere eseguiti in modo concorrente. La modalità di esecuzione seriale di Metal ha tuttavia eseguito ogni kernel in ordine.

Il percorso di decode ottimizzato registra le effettive dipendenze dei dati in un passaggio Metal concorrente. I rilasci di kernel indipendenti possono essere eseguiti contemporaneamente quando le risorse della GPU lo consentono. Viene inserita una barriera solo quando il lavoro successivo richiede un risultato precedente.

Ridurre il traffico intermedio e di stato

Kernel separati materializzano spesso un intermedio: un kernel scrive un risultato temporaneo in memoria e quello successivo legge nuovamente il risultato. Il percorso di decode ottimizzato fonde quattro catene: le due proiezioni di input degli esperti con la loro attivazione con gate; la proiezione di output dell'esperto con il suo punteggio di routing e il risultato dell'esperto condiviso; la preparazione di query e chiave prima dell'attenzione; e l'aggiornamento ricorrente con la sua normalizzazione. Ciascun kernel fuso mantiene i valori temporanei nei registri anziché inviarli attraverso la memoria.

La fusione accorcia inoltre il grafo delle dipendenze: quando una scrittura intermedia scompare, scompare anche la barriera che proteggeva il suo consumer.

Leggere la cache di attenzione in modo efficiente

Aggregare le letture della cache di attenzione

L'attenzione legge chiavi e valori dalla KV cache durante ogni passaggio di decode. Nell'ablazione, i thread GPU adiacenti non richiedono sempre byte adiacenti, costringendo il sistema di memoria a servire un numero maggiore di transazioni separate. L'abilitazione di carichi aggregati fa sì che thread adiacenti richiedano byte adiacenti in modo che l'hardware possa combinare le loro letture.

Nella configurazione bfloat16, l'aggregazione (coalescing) ha aumentato la larghezza di banda delle chiavi da 33,8 a 47,9 GB/s, ha aumentato la larghezza di banda dei valori da 42,0 a 61,8 GB/s e ha migliorato il decode end-to-end del 2,1% con un contesto di 3.840 token.

Raggruppare le teste di query per riutilizzare le righe KV

L'attenzione a query raggruppate consente a otto teste di query di condividere una testa KV. Nell'ablazione, ciascuna testa di query viene eseguita in un simdgroup separato, pertanto tutte e otto richiedono in modo indipendente la stessa riga KV memorizzata nella cache. Il kernel ottimizzato raggruppa quattro teste di query in un threadgroup, che carica ciascuna riga KV una sola volta e la riutilizza in quattro calcoli di attenzione. Un secondo threadgroup gestisce le restanti quattro teste.

Questa tecnica, comunemente chiamata raggruppamento GQA, esegue la stessa aritmetica e produce i medesimi byte di output riducendo al contempo otto richieste KV indipendenti a due carichi condivisi. Rispetto all'ablazione non raggruppata, ha migliorato il throughput di decode end-to-end del 23,8% con un contesto di 32K token.

Commutare i layout di attenzione in contesti lunghi

Ogni passaggio di decode in un layer con attenzione completa esamina la KV cache esistente. Un layout a blocchi fissi divide tale cache in parti uguali che la GPU può elaborare in parallelo. La sua schedulazione extra non è vantaggiosa quando la cache è di piccole dimensioni, ma il layout a blocchi fissi bilancia il lavoro in modo più uniforme man mano che il contesto cresce.

Per questo modello, il runtime mantiene il percorso di attenzione generale al di sotto di 32K token e utilizza il percorso a blocchi fissi a 32K o più. Lo switch si applica quando ciascuna testa ha 256 valori e otto teste di query condividono una testa KV; le altre forme rimangono sul percorso generale. Un'ablazione disattiva questo switch e utilizza sempre il percorso generale. L'abilitazione del percorso a blocchi fissi ha migliorato il decode end-to-end del 7,7% a 32K, del 27,4% a 64K e del 40,2% a 128K.

Limiti di un'ulteriore ottimizzazione

Alcune modifiche hanno migliorato un'operazione isolata ma non hanno migliorato l'inferenza end-to-end.

La decodifica speculativa, che utilizza un modello più piccolo per proporre token che il modello completo deve verificare, ha reso il decode batch-1 più lento del 18%. La verifica ha elaborato gruppi da due a cinque righe, una forma inefficiente per questo hardware, e le righe hanno spesso selezionato esperti differenti, aumentando la quantità di dati di pesi degli esperti letti. La riduzione del vocabolario di output del generatore ha migliorato il throughput del generatore del 4,7–5,1%, ma non ha reso più veloce il ciclo speculativo completo. Questo risultato è specifico del carico di lavoro: il nostro deployment di Qwen in batch su Blackwell utilizza la decodifica speculativa in condizioni differenti.

Altri esperimenti hanno incluso la riduzione dei rilasci di GPU, la sovrapposizione di intere fasi, l'utilizzo di tile di prefill più grandi, l'applicazione di una fusione più ampia, l'accelerazione del router e la combinazione della proiezione di output con la selezione dei token. Nessuno ha migliorato il ciclo di inferenza completo.

Le misurazioni dei limiti hardware hanno inoltre evidenziato un margine di miglioramento residuo minimo nelle principali operazioni di prefill e decode. I GEMM e GEMV MoE hanno raggiunto rispettivamente il 97,9% e il 90,3% delle velocità di lettura dei pesi sostenute più elevate per i rispettivi pattern di accesso. La rimozione dell'aritmetica dal GEMV sparso ha modificato il throughput solo dello 0,2%, confermando che la lettura dei pesi, anziché il calcolo, rappresentava la risorsa limitante. La moltiplicazione di matrici del prefill ha raggiunto analogamente il 93% del limite teorico di matrice in isolamento e l'80–86% all'interno dei modelli testati.

Prestazioni end-to-end

Il confronto end-to-end carica i medesimi byte di checkpoint a 4 bit in entrambi i motori ed esegue una richiesta alla volta su un singolo M5 Max a 40 core e 128 GB. All'interno di ciascun round, i due motori vengono eseguiti in ordine alternato per ridurre i bias derivanti dal carico in background e dalle variazioni della temperatura del chip. Effettuiamo il confronto rispetto al percorso di generazione diretta più veloce di MLX-LM, e non al suo server, in modo che la misurazione si concentri sull'esecuzione del modello anziché sull'overhead di servizio.

L'esplorazione copre dieci lunghezze di prompt per il prefill e dieci lunghezze di contesto per il decode, da 256 a 128K token. Il throughput di prefill aumenta inizialmente man mano che il motore distribuisce i costi fissi di configurazione su un numero maggiore di token. Il prefill raggiunge il picco intorno a un prompt di 4K token, per poi diminuire poiché i dieci layer con attenzione completa eseguono una quantità maggiore di lavoro all'aumentare del prompt. Il decode rimane quasi piatto a contesti brevi e diminuisce una volta che la lettura della KV cache in crescita diventa significativa. Il motore personalizzato è più veloce a ogni lunghezza registrata.

Poiché l'esecuzione specializzata può modificare l'ordine delle operazioni in virgola mobile, abbiamo anche verificato la coerenza numerica rispetto a MLX-LM. In un confronto guidato da insegnante (teacher-forced), entrambi i motori hanno previsto il token successivo dallo stesso prefisso di riferimento in ciascuna delle 192 posizioni, impedendo a differenze precedenti di influenzare gli input successivi. La perplessità di Lily è stata superiore solo dello 0,04% e ha selezionato lo stesso token con il rango più alto nel 96,35% delle posizioni testate.

Throughput di prefill in base alla lunghezza del prompt e throughput di decode in base alla lunghezza del contesto per Qwen3.6-35B-A3B Q4, batch 1, su un singolo M5 Max con 40 core e 128 GB. Su dieci lunghezze da 256 a 128K token, Lily è più veloce in ogni punto registrato: 1,13–1,50× il throughput di prefill di MLX-LM e 1,26–1,34× il suo throughput di decode. Il confronto usa il percorso di generazione diretta più veloce di MLX-LM. Entrambi gli assi orizzontali usano scale logaritmiche; nessuno dei due assi verticali parte da zero.

Realizzato per la piattaforma locale

Apple silicon non è una GPU per datacenter ridotta. Si tratta di una piattaforma di inferenza locale completa con caratteristiche hardware e software proprie. La memoria unificata offre a un singolo nodo un limite massimo molto elevato sulla quantità di modello e di stato che può contenere. I Neural Accelerators di M5 assorbono il lavoro di matrici dense nel prefill. Le ALU vettoriali gestiscono la parte rimanente vincolata alla larghezza di banda e a basso riutilizzo nel decode.

Qwen aggiunge ulteriori opportunità di specializzazione: mantenere il routing degli esperti e lo stato ricorrente sulla GPU, eliminare intermedi non necessari, sovrapporre il lavoro indipendente, riutilizzare i dati KV condivisi e adattare i kernel alla forma del carico di lavoro.

Con un'ottimizzazione specifica per il modello e la piattaforma, un singolo Mac può eseguire un grande modello sparso in modo efficiente. Il lavoro futuro estenderà la copertura a diversi modelli, chip e carichi di lavoro di servizio, trasformando i meccanismi qui convalidati su una singola configurazione in criteri di runtime più generali.

Il principio più generale consiste nell'adattare il motore sia all'architettura del modello sia ai percorsi specifici di calcolo e memoria dell'hardware. Con l'evoluzione dei modelli open-weight di frontiera e dell'hardware, l'inferenza locale ad alte prestazioni dipenderà sempre più da motori personalizzati per entrambi, piuttosto che da motori che ne astraggono le differenze.

Citazione

BibTeX
@online{perplexity2026OptimizingOnDeviceInferenceForAppleSilicon,
  author = {{Perplexity} and Yibo Wu and Lequn Chen and Kevin Hu and Phi Hoang and Claire Beamer and Jeremy Yang and Denis Yarats},
  title = {{Optimizing on-device inference for Apple Silicon}},
  date = {2026-09-01},
  url = {https://www.perplexity.ai/hub/blog/optimizing-on-device-inference-for-apple-silicon}
}