Salta al contenuto
Torna alla home
2026-06-2615 min di lettura
Edge InferenceOn-Device AIONNX RuntimeXboxSLMUWP

Far girare uno Small Language Model su Xbox Series S

Porting di uno SLM su Xbox Series S: 71 tok/s sulla CPU Zen 2, con il verdetto GPU misurato per workload.

Aggiornamento, luglio 2026. Tre dei sei problemi aperti qui sotto sono ora chiusi, inclusa la questione GPU — e due delle risposte smentiscono numeri di questo resoconto.

Aggiornamento, agosto 2026. Una misurazione successiva smentisce a sua volta la conclusione di luglio: il disco non è più il vincolo dominante. Le prestazioni correnti sono nel riepilogo dei benchmark generato dal progetto, non qui.

Il testo originale resta invariato come istantanea; entrambe le correzioni sono nelle sezioni di aggiornamento verso la fine.

Perché conta

Una Xbox Series S è una macchina Zen 2: otto core a 3.6 GHz con AVX2, 10 GB di GDDR6 unificata, una GPU RDNA 2 e uno sblocco Dev Mode una tantum da circa 19 dollari che permette di caricare codice proprio. Sulla carta è un substrato capace, economico e poco esplorato per l'inferenza locale. Così ho provato a far girare uno small language model su una di queste console, senza cloud nel percorso.

La versione breve: funziona. La console fa girare SmolLM2-360M-Instruct (INT4) a circa 71 token al secondo, interamente on-device, tramite ONNX Runtime GenAI. La versione lunga è più utile, perché la parte interessante di questo porting non è la demo. È l'insieme dei vincoli che ho incontrato, e quali di questi non ho risolto. Questo articolo parte di proposito dai problemi aperti. Preferisco pubblicare il confine di ciò che so piuttosto che uno screenshot che lo nasconde.

Mantengo il progetto, xllama, in modo aperto. Ogni numero qui sotto viene dai suoi log di benchmark e dalle note sui vincoli, non dalla memoria.

Cosa funziona davvero oggi

La configurazione che funziona è volutamente noiosa:

  • Modello: SmolLM2-360M-Instruct, INT4, 403 MB su disco, context window 2048, template di prompt ChatML.
  • Runtime: ONNX Runtime GenAI sull'execution provider CPU (Zen 2).
  • Pacchetto: un MSIX UWP autocontenuto con il modello incluso al suo interno, caricato tramite il Device Portal di Xbox.

Il ciclo di decodifica è la classica pompa di token di ORT GenAI, con un flag di abort così il tasto B del gamepad può annullare una generazione a metà:

while (!OgaGenerator_IsDone(gen.get())) {
    if (params.abort_flag && params.abort_flag->load()) break;
    oga_check(OgaGenerator_GenerateNextToken(gen.get()), "GenerateNextToken");

    const int32_t* next = nullptr;
    size_t n = 0;
    oga_check(OgaGenerator_GetNextTokens(gen.get(), &next, &n), "GetNextTokens");
    for (size_t i = 0; i < n; ++i) {
        const char* piece = nullptr;
        oga_check(OgaTokenizerStreamDecode(stream.get(), next[i], &piece), "decode");
        if (piece && *piece && params.on_token) params.on_token(piece);
    }
}

Misurato sulla console (ORT GenAI 0.13.2, INT4, n_ctx 2048):

| Thread | Decode tok/s | Picco working set | | ------ | ------------ | ----------------- | | auto | 66.9 | 704 MB | | 4 | 71.4 | 771 MB | | 6 | 68.0 | 772 MB | | 8 | 28.2 | 771 MB |

Questo è il baseline. Tutto quello che segue è ciò che separa questo risultato da qualcosa che chiamerei davvero finito.

I problemi aperti

1. Non riesco a dimostrare che la GPU faccia qualcosa

È quello che mi infastidisce di più. Tutta la ragione per scegliere una console invece di un Raspberry Pi è la GPU RDNA 2, e non riesco ancora a confermare di usarla.

SmolLM2-360M carica sotto l'execution provider DirectML senza andare in crash, una volta disattivati l'arena di memoria CPU e il pianificatore di pattern di memoria:

"session_options": {
  "provider_options": [
    { "dml": { "enable_cpu_mem_arena": "0", "enable_mem_pattern": "0" } }
  ]
}

Poi produce output a circa 71.7 tok/s, sospettosamente vicino al baseline CPU. Quel numero è il problema, non la rassicurazione. ORT può ricadere silenziosamente su CPU per gli operatori che il percorso GPU non supporta, e un risultato indistinguibile dal numero CPU è esattamente l'aspetto che ha un fallback silenzioso. Per distinguere i due casi mi serve un profiler D3D (PIX) o i contatori hardware della GPU, e sulla console non ho ancora alcuna strumentazione di profiling. Finché non ce l'avrò, la conclusione onesta è ristretta: il modello da 360M entra nel pool di memoria GPU, ma se venga eseguito sulla GPU resta non confermato.

2. Il pool di memoria GPU è piccolo, e questo limita tutto

I modelli più grandi non arrivano nemmeno a questo punto. Quando OgaCreateModel inizializza il provider DirectML per un modello i cui pesi superano il pool GPU disponibile, l'allocatore restituisce null e l'uso successivo di quel puntatore va in fault:

OgaCreateModel failed: SEH 0xC0000005 (STATUS_ACCESS_VIOLATION)

Osservando dove cade quel confine, il pool di memoria accessibile alla GPU per un'app UWP sulla Series S sembra di circa 768 MB. Phi-3.5-mini INT4 (~2.2 GB) va in OOM in modo affidabile; il modello da 360M (403 MB) no. Voglio essere cauto qui: quella cifra di 768 MB è un'inferenza dal comportamento di out-of-memory osservato nei miei test, non una specifica documentata della piattaforma Xbox. Non la tratto come un'affermazione autoritativa sul layout di memoria interno della console. Ma come tetto ingegneristico è coerente, e significa che qualsiasi modello vicino o sopra 1 GB è fuori discussione per il percorso GPU, a prescindere dal fatto che il problema 1 venga mai risolto.

3. Il budget di disco è più stretto di quello di RAM

Prima di poter andare in OOM, un modello deve entrare su disco. Una partizione Dev Mode appena attivata offre circa 2.2-2.5 GB di spazio libero, e il deploy ha bisogno per un istante di circa il doppio della dimensione del pacchetto, perché l'MSIX viene messo in staging prima di installarsi. In pratica questo significa un budget di modello su disco sotto i 600 MB se voglio includerlo nel pacchetto, e un deploy che fallisce con 0x80070070 (disco pieno) se lo supero. Il modello da 403 MB entra con margine. Un modello da 1.4 GB no, anche se la console ha 10 GB di RAM. Il primo muro è il disco, non la memoria.

4. Il percorso di download in-app è scritto ma non provato

La via d'uscita dal budget di disco è smettere di includere il modello e scaricarlo al primo avvio. L'ho implementata: un ModelDownloader che scarica a blocchi da un endpoint Hugging Face tramite HttpClient, con una catena di risoluzione che prova LocalState, poi il pacchetto installato, poi un fallback di download. Il codice esiste e compila. Non è mai stato eseguito davvero sulla console, perché la build che spedisco trova sempre prima il modello incluso e non raggiunge mai il fallback. Quindi se un semplice HTTPS verso Hugging Face funzioni dall'interno dell'AppContainer di Xbox resta una domanda aperta. Per testarlo devo spedire di proposito una build senza modello incluso, cosa che non ho fatto.

5. Più thread lo rendono più lento

La tabella dei thread qui sopra nasconde un dirupo netto. Quattro thread sono l'ottimo a 71.4 tok/s. Otto thread scendono a 28.2 tok/s, una regressione di circa il 60%. Il vincolo non sono i core; è la banda di memoria. La decodifica INT4 su Zen 2 satura la banda disponibile molto prima di saturare gli otto core, e aggiungere thread oltre quel punto aggiunge solo contesa. La soluzione non è codice ingegnoso, è un intra_op_num_threads=4 fissato. Ma è un promemoria che l'istinto abituale di "usa tutti i core" qui è attivamente sbagliato.

6. Gira solo in Dev Mode

Tutto questo dipende dallo sblocco Dev Mode da circa 19 dollari. Non c'è alcun percorso verso una console retail. Va bene per un baseline di ricerca e una build riproducibile, ed è un limite netto sul poter chiamare questo qualcosa che un utente normale potrebbe installare. Non ho intenzione di fingere il contrario.

Le cicatrici già pagate

Due problemi sono risolti, ma solo dopo che mi sono costati tempo vero, quindi vale la pena registrarli.

Il primo era un crash dentro OgaCreateModel su un modello che caricava senza problemi su Linux. ORT 1.24.4 chiama std::filesystem::weakly_canonical() per validare il path di un file di dati esterno .onnx.data, e su Windows questo percorre il path dalla radice del drive verso l'alto. Uno dei segmenti intermedi è la directory dello user manager dell'AppContainer di Xbox, che la sandbox non può leggere, quindi il percorso incontra ACCESS_DENIED e solleva un'eccezione. La soluzione è unire i dati esterni in un singolo model.onnx autocontenuto in fase di build, così il path di validazione non viene mai percorso. Un piccolo script Python in CI esegue l'unione.

Il secondo era il compilatore XAML che andava in crash (WMC9999) durante la build di un progetto C++/WinRT su un Windows SDK recente. Invece di combattere il compilatore di markup, costruisco l'intera UI in modo programmatico in C++ con Windows.UI.Xaml.Controls. Nessun file .xaml, nessun metadata provider, nessun passaggio di compilazione che possa andare in crash.

Cosa servirebbe per chiudere ciascuno

Per smettere di fare congetture sulla GPU, mi serve profiling D3D on-device così da confermare l'esecuzione dei kernel e misurare i tok/s su GPU contro il baseline CPU per lo stesso modello e la stessa quantizzazione. Per rendere il percorso GPU degno di conferma, voglio un candidato INT4 sotto i 400 MB come Qwen2.5-0.5B che entri comodamente nel pool. Per togliere di mezzo il budget di disco come vincolo dominante, devo validare che il download da Hugging Face dall'AppContainer funzioni davvero, e poi rimuovere il modello incluso dal pacchetto. Nessuna di queste è una domanda di ricerca. Sono strumentazione e lavoro manuale, che di solito è dove questi progetti vivono davvero.

Aggiornamento — luglio 2026

Due settimane dopo la pubblicazione, tre dei sei problemi aperti sono chiusi. Due delle risposte smentiscono numeri qui sopra, quindi il loro posto è qui e non in un changelog che nessuno legge.

Il problema 1 è risolto, e il sospetto era fondato. Il risultato di ~71.7 tok/s su DirectML era un fallback silenzioso su CPU. Il percorso GPU non si inizializzava mai: la device factory di ONNX Runtime GenAI entrava in conflitto con il device D3D12 che il compositor XAML crea all'attivazione della finestra (errore 887A0036). Eseguire il benchmark in un processo headless, pulito dal punto di vista D3D12, ha risolto il problema, e il fix sottostante è stato contribuito upstream e validato sulla console (onnxruntime-genai#2280). Con la GPU davvero in esecuzione, il verdetto risulta essere per workload, non binario. Nel decode della chat la CPU vince ancora a questa scala di modello: 68 tok/s su CPU INT4 contro 46.8 su GPU fp16, e solo 8.8 su GPU INT4, perché DirectML implementa la matmul a bassa precisione come dequantizzazione a fp16 più una GEMM completa, quindi INT4 muove più banda di fp16, non meno. Ma il prefill si inverte con la scala: intorno ai 1.000 token di prompt la GPU fa 354 tok/s contro i 198 della CPU, tagliando il tempo al primo token da 5.3 s a 3.0 s. E sul calcolo batch la GPU non è timida: SD-Turbo genera un'immagine 512×512 in circa 5.6 s, 11.1× più veloce della CPU sullo stesso workload di diffusione. L'app ora instrada per conversazione — CPU per la chat dominata dal decode, GPU per il prefill di prompt lunghi e la generazione di immagini.

Il numero del problema 2 era sbagliato. Con il percorso GPU che si inizializza davvero, il budget di memoria si è potuto misurare invece di dedurlo dai confini dei crash: il budget riportato è di 3.801 MB, non di circa 768 MB. Avevo segnalato quella cifra come un'inferenza dal comportamento OOM osservato e non come una specifica documentata, e la cautela si è ripagata — l'inferenza non è sopravvissuta al contatto con un percorso di inizializzazione funzionante. La conclusione corretta è quella a cui il problema 3 già puntava: il vincolo dominante è il disco, non la memoria GPU.

Il problema 4 è andato in produzione. L'MSIX non include più alcun modello — pesa circa 19 MB — e al primo avvio l'app scarica SmolLM2-360M (~417 MB) da un catalogo versionato su GitHub Release, con una barra di avanzamento. HTTPS dall'interno dell'AppContainer di Xbox funziona. La domanda aperta si è chiusa nel modo più noioso: spedendo la build che forza quel percorso di codice.

Anche i candidati sono cambiati. Questi numeri aggiornati vengono da ORT GenAI 0.14.1, dove la stessa configurazione CPU INT4 misura 66.3 tok/s (le tabelle sopra sono misurate su 0.13.2). Qwen2.5-0.5B, il candidato che avevo indicato, si è rivelato pesare ~822 MB nella sua build INT4 ONNX — domina l'embedding del vocabolario da 151k token — ed è fuori discussione. In compenso, la v1.1.0 compila sia ONNX Runtime GenAI sia llama.cpp in un unico binario e smista per modello, così il catalogo ora offre build GGUF di small model di generazione corrente (Qwen3.5-0.8B a 508 MB, LFM2.5-350M a 219 MB) che il model builder di ORT non può produrre. I loro benchmark su console sono in sospeso, quindi il modello di default non è cambiato.

I problemi 5 e 6 restano: quattro thread sono ancora l'ottimo, e serve ancora la Dev Mode.

Aggiornamento — agosto 2026

Una conclusione di questo resoconto è ora falsa, ed è proprio quella che l'aggiornamento di luglio aveva promosso.

Il disco non è più il vincolo dominante. Il problema 3 indicava lo storage come il primo muro, e l'aggiornamento di luglio lo aveva promosso a conclusione corretta. Entrambe le affermazioni erano giuste per una partizione Dev Mode appena attivata, che era ciò che avevo misurato. Entrambe hanno smesso di esserlo l'8 luglio 2026, quando ho portato l'allocazione Dev Mode a 90 GB tramite Dev Home, sotto Manage Dev Storage. Il valore da 2.2 a 2.5 GB ora descrive solo lo stato predefinito di una nuova attivazione. Oggi ciò che limita la dimensione dei modelli è il budget della GPU e la RAM, cioè il punto a cui si riferiva il problema 2. Rimane un limite sul disco: in Dev Mode è stato riportato un tetto di 2 GB per singolo file, che conta per i file ONNX uniti oltre quella dimensione.

I numeri di decodifica qui sono un'istantanea, e ne esistono di migliori. Ogni cifra sopra è corretta per la configurazione che nomina, per questo il testo resta invariato. Ma le tabelle sono state misurate su ORT GenAI 0.13.2 con quattro thread, mentre la configurazione distribuita è cambiata: sei thread, e un percorso di repack compilato ma mai attivato si è rivelato responsabile di una parte rilevante della perdita di throughput del prompt. I numeri correnti sono nel riepilogo dei benchmark generato dal progetto, costruito a partire da CSV versionati, dove ogni cifra porta con sé il comando per riprodurla e la CI fallisce quando un valore pubblicato diverge dalla sua evidenza. Leggi quelli invece di fidarti di un paragrafo datato in un articolo.

Il progetto è cresciuto oltre questo resoconto. A giugno era un solo modello da 360M tramite ONNX Runtime GenAI. Ora include un catalogo da 270M a 3B con entrambi i runtime, genera immagini sulla console e viene distribuito come pacchetto versionato con una suite di validazione sulla console composta da dieci gate. Il modello più veloce del catalogo, LFM2.5-350M, decodifica a 94.9 token al secondo sulla CPU, mediana di tre esecuzioni nello stesso riepilogo. È un altro articolo, non una correzione di questo.

FAQ

Quanto è veloce?

Circa 71 token al secondo di decodifica per SmolLM2-360M INT4 sull'execution provider CPU a quattro thread, con un picco di working set intorno ai 771 MB (misurato su ORT GenAI 0.13.2; la stessa configurazione misura 66.3 tok/s sulla 0.14.1). Passando a otto thread scende a circa 28 tok/s perché il collo di bottiglia è la banda di memoria, non il compute.

Il modello gira sulla GPU dell'Xbox?

Sì — e il numero che sembrava un fallback silenzioso su CPU lo era. Con il conflitto di inizializzazione risolto (vedi l'aggiornamento di luglio 2026), il verdetto misurato è per workload: la CPU vince il decode della chat (68 contro 46.8 tok/s), la GPU vince il prefill di prompt lunghi (354 contro 198 tok/s a ~1k token) e la generazione di immagini (11.1× su SD-Turbo). L'app instrada ogni conversazione verso il processore che vince quel workload.

Un utente normale può installarlo sulla sua Xbox?

No. Richiede la Dev Mode di Xbox, uno sblocco a pagamento una tantum, e non c'è alcun percorso verso una console retail. Questo è un baseline di ricerca riproducibile, non un'applicazione per il pubblico.

Perché un modello così piccolo?

Il disco, all'epoca. Una partizione Dev Mode appena attivata ha solo pochi gigabyte liberi e il deploy richiede circa il doppio della dimensione del pacchetto durante l'installazione: un modello INT4 da ~400 MB entrava con margine, mentre uno da diversi gigabyte falliva il controllo del disco prima ancora di essere caricato. Il pool di memoria GPU si è rivelato molto più grande della mia stima iniziale (3.801 MB misurati, non ~768 MB). Quel limite sul disco è stato rimosso nel luglio 2026 aumentando l'allocazione di storage Dev Mode; oggi la dimensione dei modelli è vincolata dal budget della GPU e dalla RAM. Vedi l'aggiornamento di agosto 2026.


La build completa, i log di benchmark e le note sui vincoli sono nel repository xllama. "Xbox" è un marchio Microsoft; questo è un progetto di ricerca indipendente e non è affiliato a Microsoft. Se hai eseguito inferenza su hardware da console e hai dati di profiling che a me mancano, mi piacerebbe confrontarci.

Condividi questo articolo

Articoli correlati

  • Agenti di ricerca autonomi: allucinazioni e ciclo critico

    Gli agenti planner-executor falliscono sulla verificabilità. Un critic con accesso alle fonti evidenzia claim non supportati e lacune di copertura.

    15 nov 202410 min di lettura
    #AI Agents#Research#LLM#Production