LLM sur Sony PSP

Sony PSP-2000 estas 333 MHz MIPS-poŝaparato el 2007 kun 64 MB da RAM. La mia, ĉi-semajne, ruligas Transformer-on kun 15 milionoj da parametroj kaj transfluas anglan tekston sur la LCD-ekranon kun unu ĝis du ĵetonoj sekunde.

PSP-framebuffer ĉe la fino de 64-ĵetona bench-rulo. Indiko 'Once upon a time, there was a little girl named Layla', kompletigo sube, piednoto '64 tokens in 42.6s (1.5 tok/s)'.

La modelo estas la stories15M de Karpathy (TinyStories-kontrolpunkto), int8-kvantizita al ĉirkaŭ 17 MB. La rultempo estas ~1100 linioj de pura C krosskompilita kun pspdev/pspdev en Docker. Neniu Python, neniu libtorch, neniu helpema rultempo sur la aparato — la PSP estas unuproces-skatolo kiu ŝargas unu EBOOT.PBP de la memorstiko kaj donas al vi sceIo*, framebuffer-on kaj VFPU-n. Ĉion alian vi konstruas mem.

Ĉi tiu afiŝo estas la buĝeto. Kien iras ĉiu bajto, kiel aspektas la kernoj, kaj kio restas sur la tablo.

La aparataro

CPUMIPS Allegrex @ 333 MHz, en-orda
FPUskalara fp32 + 4×4 VFPU (vektora) koprocesoro
RAM64 MB (PSP-2000/3000); 32 MB sur la originala PSP-1000
OSXMB, neniu virtuala memoro, neniu mmap, neniu interŝanĝo
Eligo480×272 LCD, neniu stdout kiun la gastiganto povas legi

La linio “neniu mmap” estas tiu kiu mordas. Sur Linuksa maŝino vi mmap-us la pezofichon kaj lasus la paĝkaŝmemoron trakti ĝin. Sur la PSP vi havas sceIoLseek + sceIoRead kaj ununuran malloc‘itan areon. Vi legas ĉiujn 17 MB en RAM antaŭ la unua antaŭeniro, aŭ vi transfluas de la memorstiko je ĉirkaŭ la rapido de USB 1.1-poŝmemorilo kaj rigardas vian trafluan rapidecon kolapsi.

La 32 MB de PSP-1000 ne sufiĉas por lasi hipmemoran spacon por la pezoj plus KV-kaŝmemoro plus laborbuferoj. La 2000 kaj 3000 venas kun 64 MB. Ni bezonas la 64.

La modelo

stories15M estas la plej malgranda el la TinyStories-kontrolpunktoj de Karpathy — 6 transformer-tavoloj, kaŝita grandeco 288, 6 atento-kapoj, vortprovizo 32000. Entute ĉirkaŭ dek kvin milionoj da parametroj. En fp32 ~57 MB. En int8 q80 — simetria per-grupa kvantizado, grupa grandeco 64, unu fp32-skalo por grupo — ~17 MB.

Arkitekturo:   Llama-stila malĉifrilo, RoPE, SwiGLU FFN
Tavoloj:       6
Kaŝita:        288
Kapoj:         6 (head_dim 48)
Vortprovizo:   32000
Kunteksto:     256 ĵetonoj
Kvantizado:    int8 q80 (group=64, simetria)
Diskgrando:    17 MB

La modelpreparado havas sian propran Docker-bildon: python:3.11-slim + nur-CPU-torch + fiksite fiksitan commit-on de karpathy/llama2.c. Ĝi elŝutas stories15M.pt, ruligas export.py --version 2 por produkti la q80 model.bin, konstruas la BPE tokenizer.bin, kaj — grave — ankaŭ konstruas la referencan runq.c de Karpathy kun -ffp-contract=off -fno-fast-math kaj ruligas ĝin sur fiksita indiko por produkti tests/expected.txt. Tiu dosiero estas la bait-ekzakta x86-referenco kontraŭ kiu la PSP estas komparata. Pli pri tio post momento.

La memorbuĝeto

24 MB da hipo, deklaritaj foje ĉe modulo-ŝargo:

PSP_HEAP_SIZE_KB(24576);

Elspezitaj jene:

RegionoGrandecoNotoj
Pezoj (int8-kvantizitaj)~17 MBununura malloc‘ita areo, glutita per sceIoLseek + sceIoRead-bloketoj
KV-kaŝmemoro~3,5 MB6 tavoloj × 256 kunteksto × 288 kaŝita × 2 (K+V) × fp32
RunState-laborbuferoj~1 MBaktivaĵoj, atentaj poentaroj, specimenitaj logit-oj
Stako, libc, kadro~2 MBla superkostoj de PSPSDK
Marĝeno~0,5 MB

La ruzaĵo inda mencii: la ĵetona enkodiga tabelo restas kvantizita en la areo. La naiva portado malkvantizas ĝin unu fojon ĉe ŝargo, kio kostas ~36 MB kaj tuj kaŭzas OOM. Anstataŭe, ĉe ĉiu antaŭeniro ni malkvantizas ununuran vicon — la vicon por la nuna ĵetono — en malgrandan fp32-buferon. La kosto estas unu kroma malkvantizo por antaŭeniro; la gajno estas ~36 MB kiujn ni ne havas.

La kernoj

transformer.c estas la kutimaj suspektatoj: rmsnorm, softmax, quantize/dequantize, matmul, RoPE, atento, SwiGLU, specimenigisto. Ĉiu estas la lernolibra versio kun devigita -ffp-contract=off por ke la ordo de multobligo-aldono-operacioj kongruu kun runq.c sur x86. Tio gravas por la testa surfaco (vidu sube).

La matmul hodiaŭ estas skalara fp32 — tri nestitaj bukloj, unu fp32 multobligo-aldono samtempe. Sur vera aparataro ĝi atingas ~1–2 tok/s. Tio estas sufiĉe malrapida por ke 64-ĵetona kompletigo daŭru ĉirkaŭ minuton.

La matmul estas ankaŭ faktorita kiel anstataŭebla funkcia montrilulo. La v1-plano estas VFPU-kerno kiu uzas la 4×4 vektorajn operaciojn, kio devus atingi ~5–15 tok/s sur la sama aparataro. (La VFPU estas la sola peco de PSP-aparataro kiu bone maljuniĝas — vektora koprocesoro kun 128 registroj adresebla kiel ok 4×4-matricoj, kapabla disponigi 4×4-matric-multiplikaton en ununura instrukcieto.) Tio estas ŝanĝo de unu dosiero por enigi.

La uzanto-interfaco

La PSP havas sisteman ekran-klavaron kiun vi vokas per sceUtilityOsk*. Ĝi redonas tekston kiel UTF-16LE; vi konvertas ĝin al UTF-8 (nur BMP — la OSK de la PSP ne atingas surogat-parojn) kaj manĝigas ĝin al la BPE-ĵetonizisto.

La babilad-interfaco estas pspDebugScreen — la enkonstruita PSP-senarariga tiparo sur la framebuffer. Monospaca, 8×8 pikseloj, 60 kolumnoj × 34 vicoj sur la 480×272-ekrano. Dukolora aranĝo: la indiko supre, generitaj ĵetonoj transfluas sube signo post signo. Kiam la bufero atingas la malsupron de la ekrano, la bildigo translimas. Ne bela, sed legebla, kaj ĉiu signo sur la ekrano estas io kiun la modelo fakte eligis.

La demonstro

Indiko:

Once upon a time, there was a little girl named Layla

Generite (T=0, 64 ĵetonoj):

She was three years old and loved to explore. One day, she decided to go on an adventure. She put on her shoes and grabbed her bag. Layla walked outside and saw a big, tall tree.

Tiu eligo estas identa, bajto post bajto, al tio kion runq.c produktas sur x86_64 kun la sama modelo, la sama indiko, la sama temperaturo kaj kongruantaj FP-flagoj. Tiu ekvivalenteco estas la testa surfaco — diff -q state.txt tests/expected.txt aŭ redonas puran aŭ ĉiu tavolo de la infer-motoro estas erara. Ĝi estas multe pli forta ol la OCR-bazitaj testoj kiujn la pli frua Pong-konstruo en tiu sama deponejo sendis; la ekrano sur la PSP nun estas dekoracia, kaj la vero estas teksta dosiero sur la emulita memorstiko.

Flankŝarĝo al vera aparataro

Ĉio supre funkcias sub PPSSPP en la testa buklo. Por meti ĝin sur metalo:

PSP/
└── GAME/
    └── PspLlm/
        ├── EBOOT.PBP
        ├── model.bin
        └── tokenizer.bin

Faligu la tri dosierojn sur la memorstikon, foliumu al la ludo en la XMB, premu X. Nur PSP-2000 aŭ PSP-3000 — la 32 MB de PSP-1000 ne lasas hipan spacon — kaj la PSP bezonas kutiman firmvaron por ruli nesubskribitajn EBOOT.PBP (6.61 PRO-C2 aŭ 6.61 Infinity sur PSP; Adrenaline sur PS Vita). Malvarma starto al OSK estas ĉirkaŭ tri sekundoj. La unua-ĵetona prokrasto dependas preskaŭ tute de la indika longo; per-ĵetona post tio estas la matmul.

Kio sekvas

ElementoKialAtendita efiko
VFPU-matmulskalara fp32 lasas la solan bonan vektoran unuon sur la blato nenokupita~5–15 tok/s anstataŭ 1–2
Multivica KV-retenoĉiu indiko hodiaŭ refarigas la KV-kaŝmemoron de nulouzebla babilo anstataŭ unu-foja daŭrigoj
stories42Mtaŭgas je ~21 MB kvantizita; ankoraŭ en 24 MB hipopli riĉaj eligoj, sama interfaco
stories110Mne taŭgas; bezonas pez-transfluon de memorstikoverŝajne ne valoras la trafluan frapon

La limigo ne estas komputado — la VFPU-ĝisdatigo pagas reen magnitudan ordon. La limigo estas RAM. 64 MB estas la buĝeto, kaj post kiam vi pagis por la OS, la hipo, la KV-kaŝmemoro, kaj la laborbuferoj, vi havas ĝuste sufiĉan spacon por 17 MB-modelo kaj ne unu bajton pli. Sony sendis ĉi tiun aparataron por ludi MP3-ojn kaj ruli Wipeout. Ĉiu alia LLM kiun mi uzis ĉi-semajne funkciis sur GPU kiu kostis pli ol la PSP. Ĉi tiu funkcias sur la PSP.