Close
0%
0%

Transformers on Retro Game Consoles

Real transformer language models running on stock NES, SNES, Game Boy Color, Genesis and N64 hardware, measured in actual cycles.

Similar projects worth following
0 followers
I run Elyan Labs, a small hardware lab in Louisiana built almost entirely out of pawn shop finds and eBay datacenter pulls. Most of what I do is getting modern workloads onto hardware that was never meant to run them, and then measuring the result properly instead of just claiming it works.

This project documents porting real transformer language models to retro game consoles. Not emulated approximations of the idea, and not a video of scrolling text: actual inference on period hardware, with the cycle counts to back it up.

Every port is open source with the ROMs included, along with the measurement harnesses and the full findings journals, mistakes included. If you load one onto a flash cart and tell me my numbers are wrong on real silicon, that is exactly the feedback I am here for.

Five consoles, five ports, and the numbers each one actually produced.

WHERE EACH PORT STANDS

NES — Ricoh 2A03 (6502) at 1.79 MHz. 0.624 to 0.634 seconds per token, about 1,117,000 to 1,134,000 cycles per token. The ternary kernel costs 10.688 cycles per MAC against an 8-cycle primitive. Output is token-exact against a host reference.

SNES — Ricoh 5A22 (65816). 7.030 tokens per second on SlowROM at 2.68 MHz, 8.019 on FastROM at 3.58 MHz. Ternary beats int8 by 2.02x even though the 65816 has a hardware multiply, which is the result I did not expect.

Sega Genesis — Motorola 68000. 1.674x over the int8 baseline, 1.77 to 2.00 tokens per second on screen, 2.21 headless. The on-screen rate is frame-locked, so 2.00 tok/s is exactly 30 frames per token.

Nintendo 64 — VR4300, MIPS III. 6,356,992 parameters. 1.23 tokens per second scalar, 2.19 with the RSP overlay, a 1.78x win.

Game Boy Color — SM83, which has neither a multiply nor a divide instruction. A six-patch optimization stack measured 10.09x end to end, roughly 498 seconds per token down to about 50. Contributed upstream rather than forked; those PRs are open, not merged.

HOW IT WAS MEASURED, AND WHERE I CANNOT YET CLAIM SILICON

This distinction matters more than the speeds do, so here it is plainly.

The Genesis runs on real hardware. The photo is a Model 1 from 1988 with an EverDrive, answering "When were you born?" on a real television. But the 1.674x figure is not from that console — it comes from MAME memory taps, exact bus cycles, reproducible to the integer across runs. The bench also runs with display and interrupts disabled for determinism, so the in-game cost is somewhat higher than the number suggests.

NES and SNES carry exact cycle counts.

The N64 and Game Boy Color figures are emulator measurements — ares for the N64, PyBoy and SameBoy cross-validated for the Game Boy — and neither has been run on silicon yet.

If you own the console and the flash cart and your numbers come out different from mine, that is the single most useful thing anyone could contribute here.

TWO NUMBERS I PUBLISHED THAT WERE WRONG

The N64 port shipped with an on-screen counter reading about 60 tokens per second. The counter was broken. The real figure is 1.23. A 4.769x speedup also circulated for that port; it was a ratio of CP0 cycle counts taken on an int8 blob that is not what ships, and on the ternary blob that does ship the RSP margin is 1.78x.

The Genesis port was first written up at 11.3x. That number was measured on an x86 host rather than on the 68000, in a document that opened by claiming everything was measured. The two machines differ most exactly where the two weight formats differ: x86 punishes the old format's data-dependent branch with 15 to 20 cycle mispredictions, and the 68000 has no branch predictor to mispredict. On the actual target it is 1.674x.

A documented Top-K attention result was retracted as well — the selection loop kept the first K survivors in ring-buffer scan order rather than the strongest K, so it never tested Top-K at all.

All of these corrections sit in the repos next to the original claims. An instrument that lies to you is worse than a slow result, and the entire point of quoting cycles is that somebody else can go check them.

WHY TERNARY

The 6502 and the SM83 have no multiply instruction at all, so ternary weights reduce the inner loop to add and subtract and a zero weight costs nothing whatsoever. That much is obvious going in. The surprise is the SNES, where a hardware multiplier exists and ternary still wins by 2.02x — because on these machines the scarce resource is moving operands, not multiplying them.

THE PART THAT KEEPS NAGGING AT ME

The Ricoh 2A03 shipped in 1975. Backpropagation was published in 1986. Cartridge bank switching and battery-backed RAM, which is everything the NES port leans on, were ordinary consumer technology by 1985. The transformer architecture was published in 2017.

A Cray-1 sustained roughly 160 MFLOPS in 1976. The training run behind the 6.36-million-parameter...

Read more »

  • A three-token test will pass a broken model. Use sixteen.

    Scott Boudreaux6 hours ago 0 comments

    This came out of the Game Boy Color work and it applies to every autoregressive port on this project, so it gets its own entry.

    The obvious way to check that an optimization did not break the model is to generate a few tokens and compare them against a reference. Three tokens is fast, it fits in a bench ROM, and it feels like enough. It is not enough.

    The model picks each token with an argmax over 512 classes, and argmax is a step function. You can inject a large amount of numeric drift into the logits and the winning index does not move, so the output stays byte-identical while the arithmetic underneath is quietly wrong. The test passes. Nothing looks broken.

    Two genuinely broken changes sailed through a three-token gate here. First, deleting the live range checks in the fixed-point clamp — those checks demonstrably fire during normal generation, so removing them is a real behavioural change, and three tokens did not care. Second, narrowing the key-value cache entries to int16, when keys in this model peak around 110,944, far outside int16 range. Forty-four overflows occur inside the three-token run itself. Still passed.

    A sixteen-token gate catches both, and the reason is structural rather than statistical. Sixteen tokens fills the context window, so errors enter the KV cache and every later position attends over the corrupted entries. The damage compounds instead of being absorbed, and the model collapses into a repeating cycle. That repeating-cycle signature is the same failure the Genesis ROM showed when it started rambling, which is how two ports ended up sharing one diagnosis.

    There is a performance version of the same mistake. A three-token bench understates KV and attention work by roughly four to five times, because the position index never exceeds two. Real wins get discarded as noise on that bench and real regressions hide in it.

    One instrument turned out to be useless entirely: static instruction counting. It swung from minus seventeen percent to plus one percent on a reorder of lines that changed no semantics whatsoever. Only frame counts under an emulator meant anything, and those were cross-validated across PyBoy and SameBoy, which agree to within poll granularity with tokens exact.

    The general rule: a correctness gate has to run long enough for errors to feed back into whatever carries state forward. For a transformer that means filling the context window. Anything shorter is measuring whether argmax is robust, which it is, rather than whether your change is correct, which is the actual question.

  • The tok/s counter was lying, and how I caught it

    Scott Boudreaux6 hours ago 0 comments

    The N64 port shipped with a token rate on screen, counting up live as the text generated. About 60 tokens per second. It is in the video. That number was wrong by roughly a factor of fifty. The real figure is 1.23 tokens per second.

    Here is what gave it away, eventually. The counter read essentially the same on the scalar build and on the RSP overlay build. It should not have — those two differ by a real 1.8x. A gauge that does not move when the thing it measures moves is not a gauge.

    The cause was that the rate came from a cycle-count read that does not advance against real time under emulation. Under ares this workload runs at roughly 0.35 to 0.63 times real time, drifting with load on the host GPU, so a rate computed that way tracks the host machine rather than the console.

    Fixing it meant two independent instruments that had to agree before I believed either. CP0 cycle counts, and counting vblanks. Vblanks are the honest clock here, because the console produces sixty of them a second no matter what the emulator is doing underneath. Both now put the scalar build at 1.23 tok/s and the RSP overlay at 2.19 — a 1.78x win for the RSP, and the two methods agree to within a tenth of a percent.

    A 4.769x figure also circulated for this port. That one was a ratio of CP0 cycle counts taken on an int8 blob, which is not what ships. The shipped blob is ternary, and on ternary the RSP margin drops to 1.78x, because ternary has already eliminated most of the multiply work the RSP was winning on. Two different builds, two different quantizations, one number quoted across both.

    The uncomfortable part is the part worth writing down. A broken counter made this port look roughly fifty times better than it was, and it sat there for weeks without anyone questioning it, myself very much included. Nobody audits a number that flatters them. If the counter had read 0.02 tok/s I would have found the bug that afternoon. That asymmetry is the real defect, and it is not in the code.

    Still outstanding: none of this has run on N64 silicon. Every figure above is ares. If you own the cartridge and the console, I would much rather be corrected than quoted.

View all 2 project logs

Enjoy this project?

Share

Discussions

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates