Sony PSP पर एक LLM
Sony PSP-2000 एक 333 MHz MIPS हैंडहेल्ड है जो 2007 में बना था और जिसमें 64 MB RAM है। इस हफ्ते मेरा PSP 15 मिलियन पैरामीटर वाला Transformer चला रहा है और LCD स्क्रीन पर एक से दो टोकन प्रति सेकंड की गति से अंग्रेजी टेक्स्ट स्ट्रीम कर रहा है।

मॉडल Karpathy का stories15M है (एक TinyStories चेकपॉइंट), जिसे int8 में क्वांटाइज़ करके लगभग 17 MB किया गया है। रनटाइम ~1100 लाइनें शुद्ध C की है, जो Docker में pspdev/pspdev से क्रॉस-कम्पाइल है। कोई Python नहीं, कोई libtorch नहीं, डिवाइस पर कोई सहायक रनटाइम नहीं — PSP एक सिंगल-प्रोसेस बॉक्स है जो मेमोरी स्टिक से एक EBOOT.PBP लोड करता है और आपको sceIo*, एक फ्रेमबफर और एक VFPU देता है। बाकी सब कुछ आप खुद बनाते हैं।
यह पोस्ट बजट है। हर बाइट कहाँ जाती है, कर्नेल कैसे दिखते हैं, और टेबल पर क्या बचा है।
हार्डवेयर
| CPU | MIPS Allegrex @ 333 MHz, in-order |
| FPU | स्केलर fp32 + 4×4 VFPU (वेक्टर) कोप्रोसेसर |
| RAM | 64 MB (PSP-2000/3000); मूल PSP-1000 पर 32 MB |
| OS | XMB, कोई वर्चुअल मेमोरी नहीं, कोई mmap नहीं, कोई swap नहीं |
| आउटपुट | 480×272 LCD, कोई stdout नहीं जिसे होस्ट पढ़ सके |
“कोई mmap नहीं” वाली लाइन वह है जो काटती है। Linux बॉक्स पर आप weights फाइल पर mmap करते और page cache को बाकी काम सौंप देते। PSP पर आपके पास sceIoLseek + sceIoRead और एक malloc’d अरेना है। आप फॉरवर्ड पास #1 से पहले सभी 17 MB को RAM में पढ़ते हैं, या मेमोरी स्टिक से लगभग USB 1.1 फ्लैश ड्राइव की गति से स्ट्रीम करते हैं और अपना थ्रूपुट गिरते देखते हैं।
PSP-1000 के 32 MB weights + KV cache + वर्किंग बफर के लिए हीप रूम छोड़ने के लिए पर्याप्त नहीं हैं। 2000 और 3000 में 64 MB है। हमें 64 चाहिए।
मॉडल
stories15M Karpathy के TinyStories चेकपॉइंट में सबसे छोटा है — 6 transformer लेयर, hidden size 288, 6 attention head, vocab 32000। कुल मिलाकर लगभग पंद्रह मिलियन पैरामीटर। fp32 में ~57 MB। int8 q80 में — प्रति-समूह सममित क्वांटाइज़ेशन, group size 64, प्रति समूह एक fp32 स्केल — ~17 MB।
आर्किटेक्चर: Llama-स्टाइल डिकोडर, RoPE, SwiGLU FFN
लेयर: 6
हिडन: 288
हेड: 6 (head_dim 48)
वोकैब: 32000
कॉन्टेक्स्ट: 256 tokens
क्वांटाइज़: int8 q80 (group=64, सममित)
डिस्क साइज़: 17 MB
मॉडल तैयारी का अपना Docker image है: python:3.11-slim + cpu-only torch + karpathy/llama2.c का एक पिन किया हुआ commit। यह stories15M.pt डाउनलोड करता है, q80 model.bin बनाने के लिए export.py --version 2 चलाता है, BPE tokenizer.bin बनाता है, और — महत्वपूर्ण — Karpathy के runq.c reference को भी -ffp-contract=off -fno-fast-math के साथ बनाता है और एक निश्चित प्रॉम्प्ट पर चलाकर tests/expected.txt उत्पन्न करता है। वह फाइल बाइट-सटीक x86 reference है जिससे PSP की तुलना की जाती है। इसके बारे में थोड़ी देर में और।
मेमोरी बजट
24 MB हीप, मॉड्यूल लोड के समय एक बार घोषित:
PSP_HEAP_SIZE_KB(24576);
इस तरह खर्च:
| क्षेत्र | साइज़ | नोट्स |
|---|---|---|
| Weights (int8 क्वांटाइज़्ड) | ~17 MB | एकल malloc’d अरेना, sceIoLseek + sceIoRead chunks से पढ़ी गई |
| KV cache | ~3.5 MB | 6 layers × 256 ctx × 288 hidden × 2 (K+V) × fp32 |
RunState वर्किंग बफर | ~1 MB | activations, attention scores, sampled logits |
| Stack, libc, framework | ~2 MB | PSPSDK का overhead |
| Slack | ~0.5 MB |
उल्लेखनीय ट्रिक: token embedding table अरेना में क्वांटाइज़्ड रहती है। naive port इसे लोड के समय एक बार डीक्वांटाइज़ करता है, जिससे ~36 MB लगता है और तुरंत OOM होता है। इसके बजाय, प्रत्येक forward पर हम केवल एक row डीक्वांटाइज़ करते हैं — वर्तमान टोकन की row — एक छोटे fp32 बफर में। कीमत प्रति forward एक अतिरिक्त dequant है; लाभ ~36 MB है जो हमारे पास नहीं है।
कर्नेल
transformer.c में सामान्य संदिग्ध हैं: rmsnorm, softmax, quantize/dequantize, matmul, RoPE, attention, SwiGLU, sampler। प्रत्येक textbook version है जिसमें -ffp-contract=off forced है ताकि multiply-add operations का क्रम x86 पर runq.c से मेल खाए। यह test surface के लिए महत्वपूर्ण है (नीचे देखें)।
आज का matmul scalar fp32 है — तीन nested loops, एक बार में एक fp32 multiply-add। असली hardware पर ~1–2 tok/s मिलती है। इतना धीमा कि 64-टोकन completion में लगभग एक मिनट लगता है।
matmul को एक swappable function pointer के रूप में भी बनाया गया है। v1 plan एक VFPU kernel है जो 4×4 vector ops का उपयोग करता है, जो एक ही hardware पर ~5–15 tok/s देना चाहिए। (VFPU PSP hardware का वह एकमात्र हिस्सा है जो अच्छी तरह उम्र बढ़ाता है — 128 registers वाला एक vector coprocessor जो आठ 4×4 matrices के रूप में addressable है, एक ही instruction में 4×4 matrix multiply करने में सक्षम।) यह drop-in करने के लिए एक-फाइल का बदलाव है।
UI
PSP में एक system on-screen keyboard है जिसे आप sceUtilityOsk* के माध्यम से invoke करते हैं। यह UTF-16LE में text return करता है; आप इसे UTF-8 में convert करते हैं (केवल BMP — PSP का OSK surrogate pairs तक नहीं पहुँचता) और BPE tokenizer को feed करते हैं।
Chat UI pspDebugScreen है — PSP का built-in debug font framebuffer पर। Monospace, 8×8 pixels, 480×272 display पर 60 columns × 34 rows। दो-रंग का layout: ऊपर prompt, नीचे generated tokens character by character stream होते। जब buffer screen के नीचे तक पहुँचता है तो rendering wrap करती है। यह सुंदर नहीं है, लेकिन पठनीय है, और स्क्रीन पर हर character वह है जो model ने actually emit किया।
Demo
प्रॉम्प्ट:
Once upon a time, there was a little girl named Layla
Generated (T=0, 64 tokens):
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.
वह output बाइट-दर-बाइट उसी के समान है जो runq.c x86_64 पर same model, same prompt, same temperature और matching FP flags के साथ produce करता है। वह equivalence ही test surface है — diff -q state.txt tests/expected.txt या तो clean return करता है या inference engine की हर layer गलत है। यह उस OCR-based tests से बहुत मजबूत है जो इसी repo में earlier Pong build ने ship किया था; PSP का screen अब decorative है, और सच एक emulated memory stick पर text file में है।
असली Hardware पर Sideloading
ऊपर की सब कुछ PPSSPP के तहत test loop में चलती है। इसे metal पर लगाने के लिए:
PSP/
└── GAME/
└── PspLlm/
├── EBOOT.PBP
├── model.bin
└── tokenizer.bin
तीन files को memory stick पर डालें, XMB में game तक browse करें, X दबाएं। केवल PSP-2000 या PSP-3000 — PSP-1000 के 32 MB हीप रूम नहीं छोड़ते — और PSP को unsigned EBOOT.PBP चलाने के लिए custom firmware चाहिए (PSP पर 6.61 PRO-C2 या 6.61 Infinity; PS Vita पर Adrenaline)। Cold start से OSK तक लगभग तीन सेकंड। पहले टोकन की latency लगभग पूरी तरह prompt length पर निर्भर करती है; उसके बाद प्रति टोकन matmul है।
आगे क्या
| आइटम | क्यों | अपेक्षित प्रभाव |
|---|---|---|
| VFPU matmul | scalar fp32 chip की एकमात्र अच्छी vector unit को idle छोड़ता है | 1–2 की जगह ~5–15 tok/s |
| Multi-turn KV retention | हर prompt आज KV cache को zero से भरता है | one-shot continuations की जगह usable chat |
stories42M | quantized पर ~21 MB; अभी भी 24 MB heap में | richer outputs, same UI |
stories110M | fit नहीं होता; memory stick से weight streaming चाहिए | throughput hit के लायक शायद नहीं |
बाधा compute नहीं है — VFPU upgrade एक order of magnitude वापस देता है। बाधा RAM है। 64 MB budget है, और एक बार OS, heap, KV cache, और working buffers के लिए भुगतान करने के बाद, आपके पास 17 MB model के लिए बिल्कुल सही जगह बचती है, एक byte भी नहीं। Sony ने यह hardware MP3 चलाने और Wipeout run करने के लिए ship किया था। इस हफ्ते मैंने जो भी दूसरे LLM इस्तेमाल किए वे सब GPU पर चले जो PSP से ज्यादा महंगा था। यह PSP पर चलता है।