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

Far girare uno Small Language Model su Xbox Series S

Un resoconto ingegneristico onesto sul porting di uno small language model su Xbox Series S: cosa funziona oggi a 71 tok/s sulla CPU Zen 2 e i vincoli che non ho ancora risolto.

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.

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. 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?

Non riesco ancora a confermarlo. Il modello da 360M carica sotto l'execution provider DirectML senza un crash di out-of-memory e produce output a circa la stessa velocità del percorso CPU. Quella somiglianza è esattamente l'aspetto che avrebbe un fallback silenzioso su CPU, e senza profiling D3D on-device non riesco a distinguere i due casi. Il risultato confermato oggi è inferenza solo su CPU.

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?

Budget di disco e di memoria. La partizione Dev Mode ha solo pochi gigabyte liberi, il deploy richiede circa il doppio della dimensione del pacchetto durante l'installazione, e il pool di memoria accessibile alla GPU sembra essere intorno ai 768 MB. Un modello INT4 da 403 MB entra in tutti e tre i vincoli; un modello da diversi gigabyte fallisce il controllo del disco prima ancora di caricare.


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:

Articoli correlati

  • RAG in produzione: correggi chunking e re-ranking prima di toccare gli embedding

    La maggior parte delle pipeline RAG fallisce sul chunking o sul re-ranking prima ancora che sulla qualità degli embedding. Un metodo diagnostico per individuare e correggere il collo di bottiglia giusto.

    20 dic 202411 min di lettura
    #RAG#Retrieval#LLM#Production
  • Dove CrewAI fallisce in produzione — e cosa usare al suo posto

    L’astrazione dei ruoli in CrewAI funziona per le demo e mostra limiti sotto carico di produzione. Quattro modalità di fallimento specifiche e i pattern LangGraph che le hanno sostituite.

    15 gen 202511 min di lettura
    #CrewAI#Multi-Agent#LangGraph#Production
  • Cosa insegna il key management bancario sugli eval harness per agenti

    Cinque discipline dalla sicurezza bancaria — stato persistente, recovery deterministica, dual control, audit trail — applicate agli agenti LLM.

    18 apr 20267 min di lettura
    #Agent Evals#Verifiable Systems#LLM Production#Banking#MCP