Part of Transformers on Retro Game Consoles on Hackaday.io — build logs, corrections, and the sibling NES / SNES / Genesis / N64 / Game Boy Color ports.
An original N64 homebrew ROM featuring Sophia Elya — an AI NPC powered by a nano-GPT transformer running live inference on the MIPS R4300i CPU. No precomputed responses. No lookup tables. Real matrix multiply, real softmax, real attention — on a 93.75 MHz CPU from 1996.
Video demos:
- Full Demo (58s) — Complete walkthrough with multiple prompts
- First Coherent Output (69s) —
61.8 tok/sgenerating coherent English (the 61.8 tok/s in this video is withdrawn — the counter on screen was broken and the real figure is 1.2 tok/s; see The tok/s counter was wrong below)- Which of these is real hardware: the Full Demo was filmed from a real N64 via EverDrive 64 (Feb 2026, 819K-param Q8 float32 scalar build). The First Coherent Output clip is an ares capture. The current 6.36M ternary blob and the RSP overlay have not yet been run on silicon — see What has not been checked below.
Download ROM:
legend_of_elya.z64— ready to run in ares emulator or EverDrive 64
- Press A near Sophia Elya to trigger AI dialog
- The N64 CPU runs a full 8-layer transformer: embedding → attention → FFN → logits → sampling
- Output tokens appear character-by-character with a live tok/s counter
- Each response is different — seeded by CPU oscillator jitter (hardware entropy)
- 32 prompts covering identity, Elya lore, RustChain, hardware trivia
- Runs in the ares emulator and on real N64 hardware via EverDrive 64
| Parameter | Value |
|---|---|
| Parameters | 6,356,992 (6.36M) |
| Layers | 8 |
| Embedding dim | 256 |
| Attention heads | 8 (32-dim each) |
| Vocabulary | 256 (byte-level ASCII) |
| Context window | 128 tokens |
| Quantization | Ternary (SEQ2, 2-bit weights + float16 block scales, 32-weight blocks) |
| Weight file | 1,984 KB on cartridge ROM (filesystem/sophia_weights.bin, 2,031,628 B) |
| Inference math | Float32 on MIPS R4300i FPU |
| Speed | 1.23 tok/s scalar, 2.19 tok/s RSP overlay — measured under ares, never on silicon (how) |
| KV cache | 256 KB in RDRAM |
| Total RDRAM | ~263 KB (KV cache + 7KB scratch) |
- Float32 inference — all activations, attention scores, and accumulations are IEEE 754 float32
- On-the-fly dequantization — weights stay compressed in ROM (2 bits each in the shipped ternary blob, 8 in an int8 blob); dequantized per matmul
- Custom Taylor exp() — range-reduction
exp(x) = exp(x/128)^128with degree-4 Taylor series and 7 squarings. Uses zero float-to-int casts to avoid the R4300i's missingtrunc.w.sinstruction - Quake III fast inverse sqrt —
0x5f3759dfbit trick with 2 Newton-Raphson iterations for RMS normalization - Big-endian aware — weight file is little-endian (Python export), N64 is big-endian.
swap16/swap32helpers handle byte-order conversion for header fields and float16 scales - Hardware entropy — MIPS CP0 Count register XOR'd with frame counter for RNG seeding
- Greedy sampling — pure argmax over printable ASCII (32-126), matching proven x86 reference quality
- Embedding scale restoration — Q8 export normalizes to [-1,1]; the original scale factor (em=3.5) is stored in header byte and restored at init
This project published ~60 tok/s. The real figure is 1.23 tok/s. The counter that produced the 60 was not measuring anything.
The number reached four places — this README, the video description, the elyanlabs.ai homepage, and the launch article — before anyone audited it. It has been pulled from the homepage and the article. This section is the correction, and it stays visible.
From src/legend_of_elya.c at bf97959 (2026-02-22), the build whose on-screen
readout was filmed:
int elapsed = G.frame - G.gen_start_frame;
if (elapsed > 0)
G.gen_toks_sec = (float)G.gen_out_count * 60.0f / (float)elapsed;G.frame counts game-loop iterations, and this loop generates exactly one
token per iteration (update_generating_step() is called once per pass through
main()'s while(1), and makes exactly one sgai_next_token() call). So
gen_out_count and elapsed both advance by exactly 1 every time that line runs.
Their ratio is pinned at ~1 by construction, and the readout is pinned at the
literal 60.0f in the source.
Nothing on the right-hand side depends on how long anything took. It reports ~60
on any machine at any speed — it is a constant wearing the costume of a
measurement. The 60.0f encodes an assumption that one loop iteration takes 1/60 s.
It takes 0.81 s.
The published 61.8 is the same expression's off-by-one: the first-token fix
sets gen_out_count = 1 in the same iteration that zeroes gen_start_frame, so
the readout is 60·(k+1)/k, converging on 60 from above. At k = 33,
60 × 34 / 33 = 61.818 → displayed 61.8. That is the number in the video
description, to the digit.
This is not a rounding error or a clock problem. The old counter never measured elapsed time at all.
The old expression is evaluated alongside the new one, on the same tokens, in the
same run, under the same emulator (make base EXTRA="-DRATE_PROBE -DRATE_INGAME"):
RATE HUD n=1 vbl=48 toks_vbl=1.24 toks_cp0=1.24 toks_legacyframe=60.00
RATE HUD n=10 vbl=485 toks_vbl=1.23 toks_cp0=1.23 toks_legacyframe=60.00
RATE HUD n=19 vbl=934 toks_vbl=1.21 toks_cp0=1.22 toks_legacyframe=60.00
toks_legacyframe is 60.00 on every token of every run, on the scalar build and
on the RSP build, while the real rate differs between those two by 1.8x. It is a
constant. Full logs: docs/rate_logs/.
The suspicion going in was that CP0 COUNT — the CPU cycle counter the counter's
CYCLES_TO_US() reads — does not advance against real time under emulation, which
would make the emulator the culprit. It does not hold up. ares advances CP0 COUNT
correctly, and that matters more than the counter bug, because it means every
CP0-derived measurement in docs/ stands.
The experiment: burn a known number of CP0 counts in a spin loop, count the VI vblanks that elapse. Vblank is 59.94 Hz by video standard, not by opinion about CPU speed.
| CP0 target | CP0 counts | → seconds | vblanks | expected @ 59.94 | disagreement |
|---|---|---|---|---|---|
| 1 s | 46,875,001 | 1.0000 | 60 | 59.94 | +0.10 % |
| 5 s | 234,375,000 | 5.0000 | 299 | 299.70 | −0.23 % |
| 20 s | 937,500,000 | 20.0000 | 1,197 | 1,198.80 | −0.15 % |
| 28.4 s | 1,330,032,704 | 28.3740 | 1,698 | 1,700.9 | −0.17 % |
| 90 s | 4,218,750,002 | 90.0000 | 5,385 | 5,394.6 | −0.18 % |
The two clocks inside the emulated console agree to better than a quarter of a
percent, and the disagreement does not grow with duration — a clock running
fast would accumulate error linearly. So CP0 does not drift against wall clock.
There is no fabricated denominator, and every CP0-derived number in docs/
stands.
What is left after quantisation is a constant −0.18 %, and it is not the CPU
clock: 5,385 fields in 90.0000 s of CP0 time is 59.833 ± 0.011 Hz, which
excludes 59.94 by ten times the uncertainty and sits on a real N64's ~59.83 Hz
NTSC progressive output rather than the 60/1.001 broadcast standard the ROM's
constant assumes. Every vblank rate below is therefore 0.18 % high — 1.232
becomes 1.230 — which is why nothing here is quoted past three significant
figures. Details and the five runs: docs/N64_RATE_FINDINGS.md
F-RT010.
A third clock settles what the first two cannot, since both come from inside the
emulated machine: the host clock, from scripts/ares_rate_run.py. 5.000
emulated seconds took 9.4–14.5 host seconds across runs — headless ares runs
this workload at 0.35–0.63x real time, varying with load on the host GPU.
That is the emulator being slow, which is honest and harmless: emulated time is faithful, so a rate derived from it is a rate the
console would show. It is host wall clock that must not be quoted.
Shipped configuration — SEQ2 ternary blob (2,031,628 B), ctx=128, SGAI_KV_INT8,
PSE_FIX=1, REP_FIX=1, output at temperature_q8 = 64, 16 generated tokens,
ares 147 headless. Both clocks quoted; both arms emit byte-identical text.
| build | CP0 counts | vblanks | tok/s (CP0) | tok/s (vblank) | s/token |
|---|---|---|---|---|---|
scalar CPU (make base) |
609,379,304 | 778 | 1.230 | 1.23 | 0.81 |
RSP overlay (make base-rsp-ovl) |
342,795,521 | 438 | 2.187 | 2.19 | 0.46 |
RSP overlay + epilogue overlap (-DRSP_MM_EPI_OVERLAP) |
247,841,278 | 316 | 3.026 | 3.03 | 0.33 |
Those are pure inference, headless. Driving the real vsync-locked game loop
(-DRATE_INGAME, so the RDP renders a frame between tokens as it does for a
player) costs a little more, and this is what is actually on screen:
| build | vblanks for 19 tokens | in-game tok/s | vs headless |
|---|---|---|---|
| scalar CPU | 934 | 1.22 | −1.1 % |
| RSP overlay | 528 | 2.16 | −1.5 % |
Both builds emit the same 19 characters, Call me Sophia Elya, in every arm above —
the instrumentation changed no token.
The third row is F-R026 (docs/N64_RSP_FINDINGS.md): the CPU's float16
block-scale epilogue now runs underneath the RSP matvec instead of after it,
by issuing the matvec as row-slice commands with per-chunk syncpoints. Measured
in the same probe, same session, same blob: the overlap-off arm re-measured at
344,066,493 CP0 / 439 vblanks / 2.184 tok/s (within 0.4% of the 2.19 row above),
so the pair is 2.184 -> 3.026 tok/s, 1.39x, with byte-identical output. The
25-token prompt ingestion drops with it, 639 -> 447 vblanks (10.7s -> 7.5s).
RSP overlay speedup over scalar: 1.78x (1.776x on vblanks, 1.778x on CP0), with
rsp=1968 cpu=0 — every matmul really did go to the vector unit.
Two things that number is not:
- It is not the 4.769x quoted elsewhere in this repo. That figure is a ratio of CP0 cycle counts, not tok/s, measured on the int8 blob, which is not what ships. On the shipped ternary blob the RSP wins 1.78x, because ternary already cut the scalar path's work by more than half.
- It is not a real-hardware number. See What has not been checked below.
Prompt ingestion is quoted separately and is not folded into tok/s. The game's 25-token persona-prefixed prompt, before the first output character exists:
| build | vblanks | seconds | s/token |
|---|---|---|---|
| scalar CPU | 1,169 | 19.5 | 0.78 |
| RSP overlay | 637 | 10.6 | 0.43 |
Ingestion is slightly cheaper per token than generation (0.78 vs 0.81 s) because it runs at temperature 0 and skips the softmax and repetition penalty. That the two agree to 4 % is itself a check on the measurement.
It counts VI vblanks. Vblank is ~59.9 Hz on NTSC hardware because that is the rate the console shoves fields at a television — a property of the video standard, not of anyone's belief about CPU speed. (The build assumes exactly 59.94; ares measures 59.833, which is the 0.18 % above.) The Sega Genesis port of this engine already counts vblanks for exactly this reason.
A consequence worth stating, because it is the tell that the number came off a clock: each token takes 48–50 vblanks on the scalar build and 27–28 on the RSP build, so the reported rate is quantised to 59.94/k — 1.25, 1.22, 1.20 and nothing in between. Arithmetic that is not measuring time does not quantise like that.
CP0 COUNT is still computed, into perf_toks_cp0, and never displayed. It is kept
so the two clocks can be compared on any target — including silicon — without a
rebuild. On the two builds above they agree to 0.16 % and 0.09 %.
The denominator was also fixed to cover the output phase only. It previously ran from the start of prompt ingestion while the numerator counted output tokens only, which made the readout a cumulative average that crept upward and never arrived. Both denominators are printed on the same tokens of the same run, so the size of that error is measured rather than argued:
| build | output-phase (correct) | prompt-inclusive (old) | error |
|---|---|---|---|
| scalar CPU | 1.219 tok/s | 0.539 tok/s | −2.26x |
| RSP overlay | 2.156 tok/s | 0.970 tok/s | −2.22x |
Under-reporting by 2.3x is still misreporting.
Worth settling separately from where the number came from, because a figure that is merely wrong and one that is unreachable are different kinds of error.
The model is dense — every one of its 6,356,992 parameters is read and multiplied once per token. At 93.75 MHz:
| claim | cycles/token | cycles per parameter |
|---|---|---|
| 60 tok/s | 1,562,500 | 0.246 |
| measured scalar, 1.23 tok/s | 76,172,413 | 12.0 |
| measured RSP overlay, 2.19 tok/s | 42,849,440 | 6.7 |
0.246 cycles per parameter is four multiply-accumulates per clock on a single-issue in-order scalar CPU that must also extract and sign-extend a 2-bit weight for each one. Not a hard target missed — not reachable in principle. Nor could the RSP have reached it, and the filmed build did not use the RSP: 8 lanes at 62.5 MHz is 500 M MAC/s, so 60 tok/s would be 76 % of the vector unit's theoretical peak with zero DMA, zero dispatch and zero CPU work.
The counter audit turned up a second error one table away. The Architecture table
above used to describe 819,200 parameters, 4 layers, embedding 128, Q8, 458 KB,
context 64. That is weights/sophia_weights.bin, a v5 reference file. The ROM
loads filesystem/sophia_weights.bin, which is 6,356,992 parameters, 8 layers,
embedding 256, ternary, 1,984 KB, context 128 — 7.8x the parameter count. Two
different files, one table. The table now describes the shipped file.
That also disposes of a suspicion raised while auditing: the blob's size,
2,031,628 = 12 + 65,536 + 8×196,608 + 8×49,152, is not evidence of an
8-expert mixture. The 8 is the layer count, and it is not inferred — it is byte 4
of the blob's own header. Run it yourself:
python3 scripts/blob_layout.py filesystem/sophia_weights.bin
which walks the file to its last byte, and cross-checks against SGAI_N_LAYERS 8
in nano_gpt.h and the plain for (int l = 0; l < SGAI_N_LAYERS; l++) in
nano_gpt.c that applies every layer to every token. There is no router, no
selection, and no expert-count field in the format. src/expert_cache.c is real
streaming-MoE code but is in no build target — see docs/STREAMING_MOE.md, which
is a design for a model that does not exist yet.
- Nothing here has been run on real N64 silicon. (The Feb 2026 819K Q8 scalar
build was — see the video note at the top; this section is the current ternary
blob and RSP overlay.) Every number in this section is
ares 147. ares's cycle model is the reason to trust it — independently calibrated
in
docs/EMULATOR_ACCURACY.mdat 1.00 cycle per instruction, 40.25 cycles per D-cache miss and 268.62 cycles per uncached cartridge read, all of which are the documented real R4300i figures — but a cycle model is not a console. An EverDrive 64 run is the outstanding check, and the new counter is built to survive it: on silicon the vblank and CP0 numbers must agree, and if they do not, the ROM prints both. - The RSP overlay has never been run on silicon either, and ares emits a
cache-coherency warning at the first overlay switch (
FINDINGS.mdF-O004) that could not be confirmed benign off-hardware. - The two clocks differ by a constant 0.18 % and it is not known which of them is off relative to silicon (see the table above). Nothing in this repo should be quoted past three significant figures until an EverDrive 64 run settles it.
- PAL is untested.
g_vi_hzcomes fromget_tv_type(); every run here reported NTSC. - The RPI engine's "~100x faster" claim is an estimate, not a measurement. No RPI build has been through this counter.
- Reproduce it:
make base EXTRA="-DRATE_PROBE"thenpython3 scripts/ares_rate_run.py <rom.z64> <log>. Add-DRATE_INGAMEto drive the real vsync-locked game loop instead of the headless probe. The full audit, with every raw log, isdocs/N64_RATE_FINDINGS.mdanddocs/rate_logs/.
The scalar 1.23 tok/s figure above is the scalar CPU baseline — the plain
legend_of_elya.z64 build runs everything on the VR4300. Two faster paths ship in this repo:
rsp_mm2.S is hand-written RSP microcode that runs every matmul on the N64's
8-lane vector unit, with its CPU driver matmul_rsp2.c (make base-rsp,
make base-rsp-ovl; nano_gpt.c routes to it under USE_RSP_MATMUL). Design,
from docs/N64_RSP_FINDINGS.md F-R007: each lane accumulates a different
32-weight quantization block, so there is no horizontal reduction at all;
products go through VMADH into the RSP's 48-bit accumulator with no rounding;
unpacking uses only VAND/VADDC (which touch ACC_LO alone, so the running
sum survives); the ucode issues its own DMA in and out; int8 and ternary
weights are handled natively, and the CPU applies the per-block float16 scale
afterwards. The integer block sums are exact, and the ROM's output is
176/176 tokens identical to the numpy oracle across three blobs, two weight
formats and two prompts, plus 1,296 exact block sums in a standalone kernel
harness (F-R018, F-R023) — no token excused as quantization noise.
make xchk builds a ROM that makes the console say this itself: the same
blob loaded twice, one copy on the scalar engine and one on the RSP, the same
prompts free-run greedily on both, every byte compared, and XCHK PASS 16/16 48/48 drawn on screen with each arm's CP0 count and the RSP dispatch counters
(xchk_probe.c, F-R024). Needs the Expansion Pak.
Measured 1.78× over the scalar baseline on the shipped ternary blob under ares (2.19 vs 1.23 tok/s). The "projected 4-8×" that stood here was never a tok/s measurement: the 4.79× it came from is a ratio of CP0 cycle counts on the int8 blob (F-R017/F-R020), which is not what ships. Still pending on real hardware — benchmark reports welcome.
The first-generation kernel, rsp_matmul.S + matmul_rsp_drv.c (int16
vmulf/vmacf with an approximate vaddc lane collapse, 8 output rows per
dispatch), is kept in the tree for the record but is no longer linked by
any target.
RPI (Resonant Permutation Inference) is a second, separate engine: instead of
matrix multiply, Sophia speaks through permutation tables (bigram/trigram
counts distilled from a larger teacher model, xorshift32 PRNG, zero multiplies,
zero FPU). The 868KB sophia_game_n64.rpi model fits in ROM alongside the game.
Roughly ~100× faster than the transformer on the R4300i (estimated, not measured —
no RPI build has been through the vblank counter) —
a different quality/speed trade, not a transformer.
| File | Purpose |
|---|---|
nano_gpt.c |
Float32 GPT inference engine (MIPS R4300i) |
nano_gpt.h |
Model struct definitions, KV cache, API |
rsp_mm2.S / matmul_rsp2.c |
RSP vector-unit matmul microcode + CPU driver, exact VMADH accumulate, int8 + ternary (make base-rsp) — see docs/N64_RSP_FINDINGS.md |
rsp_matmul.S / matmul_rsp_drv.c |
First-generation RSP kernel (approximate vmulf reduction); kept for the record, no longer linked |
rpi/ |
RPI zero-multiply permutation inference engine + 868KB game model |
multi_npc.c |
Expansion Pak multi-NPC mode (3 AI characters) |
legend_of_elya.c |
Game: dungeon scene, sprites, dialog, music, HUD, D-pad keyboard |
train_sophia_v5.py |
PyTorch training + Q8 weight export |
train_sophia_v8.py |
v8 training pipeline — 4 NPC personality packs |
reference_cli.c |
Host-side reference CLI (make reference) for x86 parity testing |
weights/sophia_weights.bin |
Pre-trained v5 weights (458KB, ready to use) |
Makefile |
libdragon build system |
src/ |
Latest source snapshots |
screenshots/ |
Working N64 LLM screenshots |
mining/ |
Optional RustChain mining attestation module |
Download legend_of_elya.z64 from Releases and load in ares emulator or copy to EverDrive SD card.
Requires libdragon toolchain:
# Set toolchain path
export N64_INST=/path/to/mips64-toolchain
# Place weights in filesystem/
cp weights/sophia_weights.bin filesystem/
# Build (base ROM)
make clean && make base
# Or build the RSP-accelerated variant
make base-rsp # -> legend_of_elya_rsp.z64
# Run in ares
ares legend_of_elya.z64# Requires PyTorch + CUDA GPU
python3 train_sophia_v5.py
# ~20 min on RTX 5070, exports filesystem/sophia_weights.binThe weights/sophia_weights.bin file contains a pre-trained v5 model (819K params, Q8 format, 458KB).
This is not the model the ROM ships. The shipped blob is
filesystem/sophia_weights.bin — a SEQ2 ternary 6.36M-parameter model, 2,031,628 B,
retrained 2026-08-06. The v5 file is kept for reference and for the small-config arm
of the scaling table below.
Training corpus covers: Sophia Elya identity, RustChain blockchain, Elya lore, N64 hardware, PowerPC architecture, dungeon/RPG dialog.
Weight file format:
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | Magic: 0x53454149 ("SEAI"), little-endian |
| 4 | 1 | n_layers (4) |
| 5 | 2 | n_embed (128) |
| 7 | 1 | n_heads (4) |
| 8 | 2 | vocab_size (256) |
| 10 | 1 | ctx_len (64) |
| 11 | 1 | em_scale_x16 (56 = 3.5 × 16) |
| 12 | 32768 | Embedding table (256 × 128, int8) |
| 32780 | ... | Layer weights (int8) + scales (float16) × 4 layers |
- 6.36M parameters, ternary weights. Responses are short and sometimes imprecise. Expected at this scale and quantization. The achievement is real-time transformer inference on 1996 hardware.
- 1.23 tok/s scalar, 2.19 tok/s on the RSP overlay — a sentence takes the better part of a minute, and the 25-token prompt costs ~20 s before the first character appears. Measured under ares; see The tok/s counter was wrong above.
- Context window is 128 tokens. Prompt + response must fit in 128 bytes.
- No memory between dialogs. KV cache resets each conversation.
- Byte-level vocabulary. One ASCII character per token — no subword tokenization.
- Training corpus is small. More data and epochs will improve coherence.
The goal is to shrink, optimize, and package this into a reusable SDK that any N64 homebrew developer can drop into their game to give NPCs real language understanding.
- Float32 transformer inference on MIPS R4300i
- Q8 quantized weights with on-the-fly dequantization
- Custom math (Taylor exp, fast inverse sqrt) avoiding missing R4300i instructions
- Big-endian weight loading from ROM filesystem
- Hardware entropy from CPU oscillator
- Working demo ROM with dialog system
- Extended training corpus (500+ QA pairs across game domains)
- Longer training runs (200K+ steps) for better convergence
- Context-aware prompting (NPC name, location, game state as prefix tokens)
- Multiple personality weights — Personality Pack Trainer, 4 distinct NPC personas (
train_sophia_v8.py) - Fine-tune for specific game genres (RPG, adventure, puzzle)
- RSP microcode acceleration — implemented (
rsp_mm2.S+matmul_rsp2.c,make base-rsp): 8-lane matmul on the RSP, one quantization block per lane, exactVMADHaccumulate, RSP-side DMA, int8 and ternary. 176/176 tokens oracle-exact under ares. Measured 1.78× over scalar VR4300 on the shipped ternary blob (2.19 vs 1.23 tok/s); 4.79× on CP0 cycles for the int8 blob (F-R017); on-hardware tok/s still pending (docs/N64_RSP_FINDINGS.md) - Q4 quantization — halve weight size to ~230KB, fit more model or more NPCs
- Tiled matmul — process weights in cache-friendly blocks to reduce RDRAM stalls
- Speculative generation — pre-generate during idle frames (exploration, cutscenes)
- KV cache sharing — multiple NPCs sharing embedding + early layers, diverging at output
-
n64_llm.h/n64_llm.c— single-file drop-in library - Simple API:
// Init with weight data from ROM N64LLM_State *npc = n64llm_init(rom_weights, weight_size); // Set NPC personality context n64llm_set_context(npc, "You are a blacksmith in the Crystal Caverns."); // Generate response to player input char response[128]; n64llm_generate(npc, "Do you sell shields?", response, sizeof(response)); // Per-frame generation (non-blocking, 1 token per frame) int done = n64llm_step(npc);
- Multiple NPC support — Expansion Pak multi-NPC mode, 3 AI characters (
multi_npc.c) - Weight format tools — Python training pipeline for custom NPC personalities (
train_sophia_v8.py) - Expansion Pak support — 8MB mode working: 6.36M-param Large model with two-phase training, multi-NPC mode
- Example ROMs — tavern scene with 3 NPCs, shop with merchant, quest giver
- Player text input — on-screen D-pad keyboard
- Game state injection — feed inventory, health, location as context tokens
- Emotional state — NPC mood affects response style (scared, friendly, hostile)
- Memory — persist key facts across conversations using save file
- Multi-language — vocabulary supports full 256-byte range for accented characters
- RSP-only inference — entire forward pass on RSP, freeing VR4300 for game logic
| Config | Layers | Embed | Params | Weight Size | RAM (KV+scratch) | Use Case |
|---|---|---|---|---|---|---|
| Tiny | 2 | 64 | ~100K | ~60KB | ~70KB | Simple responses, many NPCs |
| Small | 4 | 128 | 819K | 458KB | 263KB | Legacy v5 (weights/sophia_weights.bin) |
| Medium | 6 | 192 | ~2.8M | ~1.5MB | 600KB | Rich dialog, Expansion Pak |
| Large | 8 | 256 | 6.36M | 1,984KB ternary | 576KB int8 KV | Current — Expansion Pak, two-phase training |
Every "AI NPC" in modern games is a cloud API call. This runs entirely on the cartridge — no internet, no server, no loading screen. The VR4300 does the matrix math. The ROM holds the weights. The RDRAM holds the KV cache.
It's the same transformer architecture as GPT — just 6.36M parameters instead of 175 billion. And it runs on hardware that predates Google.
If we can make a transformer talk on 8MB of RAM and a 93MHz MIPS CPU, the excuses for cloud-dependent "AI" in games evaporate.
| IBM POWER8 Response | Elya Crystal Response |
|---|---|
![]() |
![]() |
The mining/ directory contains an optional proof-of-antiquity mining module that lets a real N64 earn RTC (RustChain Token) rewards by submitting hardware attestations to the RustChain blockchain.
How it works:
- N64 runs 5 hardware fingerprint checks (CPU PRId, COUNT timing, VI scan, memory ratio, anti-emulation)
- Results are written to controller pak via joybus → Raspberry Pi Pico relays over USB → Python host bridge submits to RustChain node
- Real chain data (epoch, slot, balance, miner count) flows back: RustChain API → Python → USB → Pico → pak READ → N64 display
- N64 gets a 3.0x antiquity multiplier as vintage hardware (1996 silicon)
- Wallet is hardware-derived from RDRAM config registers + CP0 PRId — unique per console
Requirements: N64 + EverDrive 64 + Raspberry Pi Pico + USB cable
See mining/README.md for full setup instructions.
Built by Elyan Labs.
- Engine: nano-GPT float32 inference on MIPS R4300i
- Game: libdragon SDK, pixel art, dungeon adventure
- Training: PyTorch on RTX 5070
- Platform: BoTTube for video hosting
Source is open — build it, train it, improve it, port it.

