Taalas-stijl On-Chip Weights op een FPGA van $250: Een taalmodel met 60k tok/s

Ik was zo onder de indruk van de demo van chatjimmy.ai door Taalas, dat ik wilde zien wat ik in de fabric (de reconfigureerbare logica van de FPGA) van een FPGA van $250 kon persen. Door geen gebruik te maken van de beschikbare 4 GB DDR (het off-chip DRAM/hoofdgeheugen van het bord, ~20 GB/s), en alles in URAM (UltraRAM: groot, breed on-chip SRAM, ~18 Mb) en BRAM (Block RAM: kleine, flexibele on-chip SRAM-blokken, ~5 Mb totaal) te houden, stelde ik mezelf als doel 100.000 tok/s te bereiken.

Het resultaat, bit-exact en gemeten op silicon: 59.965 tokens per seconde op de fabric. Ter vergelijking: hetzelfde model op de Arm-cores van dit bord haalt 11 tok/s, en mijn laptop met een RTX 3050 Ti haalt 719 tok/s.

Taalas slaagde erin Llama 3.1 8B in hun chip te bakken voor een bruikbare chat. Ik had echter slechts ongeveer 3 MB aan geheugen om mee te werken, wat betekent dat het krachtigste model dat we kunnen passen van de TinyStories-klasse is: 3,16 miljoen parameters, ongeveer 1,5 MB bij INT4. Aanvankelijk wilde ik dit verder verkleinen door het trainingscorpus te lemmatiseren (woorden reduceren tot hun wortelvorm), waardoor het zou klinken als Kevin Malone uit The Office ("waarom veel woord gebruiken als weinig doen truc"). Omdat dit het model zelf niet kleiner maakte, maar enkel de snelheid met ~30% verhoogde, heb ik besloten dat de hardware tussen beide modellen kan schakelen.

De Live Demo

De live-widget is een echte WebSocket-verbinding met het bord. Uw woorden gaan via een Cloudflare-tunnel naar een serverbox, dan naar de Kria, de fabric in, en weer terug. Het is een verhalengenerator, geen assistent; hij begrijpt geen vragen. Als u "once upon a time" invoert, maakt hij het verhaal af.

De output is bewust telegrafisch: compressie is hier gelijk aan snelheid. Wanneer twee modellen in rotatie zijn, laat een badge zien welke live is en telt deze af naar de volgende swap; bij nul wordt de hele chip hergeprogrammeerd met het andere model (~25 s).

De Hardware: AMD KV260

Dit project draait op een AMD KV260 dev board met de K26 SOM (System-on-Module), een van de nieuwste SoC's in de Zynq-lijn (specifiek de Zynq UltraScale+ ZU5CG/ZU5EV).

Ik kocht de KV260 oorspronkelijk voor een deterministische vision-pipeline. Hoewel het bord wordt verkocht als "vision AI starter kit", draaien de meeste Vitis-demo's op de quad-core A53 Arm-cores in plaats van op de fabric. De A53 is echter een zwakke core zonder hardware matmul. Voor probabilistische AI op een CPU zou ik nooit een quad-core A53 kiezen.

De Geheugenmuur (The Wall)

Het genereren van één token per keer is memory bound, niet compute bound. Om het volgende token te produceren, moet elk gewicht in het model één keer worden gelezen. De rekenkracht is goedkoop, het lezen is de kostenpost.

Op de KV260 delen de A53's en de fabric één DDR-controller van ongeveer 20 GB/s. Als het model in het DDR-geheugen leeft, drinken de fabric en de CPU door hetzelfde rietje; de fabric voegt dan niets toe. De enige ontsnapping is een model dat klein genoeg is om volledig in het on-chip geheugen te passen, waar de bandbreedte honderden GB/s bedraagt. Dit is het inzicht van Taalas, Groq en Cerebras: de geheugenmuur is de vijand en on-chip weights zijn de oplossing. De KV260 biedt hiervoor ongeveer 3 MB.

DDR-resident (Geen ontsnapping)On-chip (De enige ontsnapping)
PL fabric → DDR (~20 GB/s, gedeeld)PL fabric → URAM + BRAM (honderden GB/s tot TB/s)
Fabric voegt niets toe: ~11 tok/sModel past in ~3 MB (firehose)
Eén controller, één rietjeINT4, 1.5 MB resident: tot 59.965 tok/s

Omdat 3 MB moet volstaan voor de gewichten, de activaties en de KV-cache, is er geen ruimte voor een "slim" model. De oplossing is om het model te verkleinen totdat het probleem verdwijnt.

Het model verkleinen (en de rol van Kevin)

Een transformer met 3,16M parameters in INT4 (4-bit integers) past in ongeveer 1,5 MB. Om de snelheid verder te verhogen, is het TinyStories-corpus door een tool gehaald die Engels stript tot enkel inhoudswoorden. "Why waste time saying a lot of words when a few words do the trick" wordt "why waste time say lot word when few word do trick".

Dit reduceerde het aantal woorden in het corpus van 371,7 miljoen naar 260,5 miljoen (ongeveer 70%). Hoewel het model qua parameters exact dezelfde grootte behield, zorgde de compressie ervoor dat een verhaal ongeveer 30% minder karakters nodig heeft. Hierdoor gaat de effectieve snelheid omhoog, ook al blijft de per-token rate identiek.

De jacht op de 100k tok/s

Mijn doel was 100.000 tok/s. Hoewel ik dat niet heb gehaald, kwam ik dichtbij. Het record is gebaseerd op 16 parallelle streams die één weight pass delen, volledig gesequenced in de fabric zonder tussenkomst van de CPU. Dit meet 59.965,5 tok/s bij 200 MHz.

Er is echter een nuance: deze zestien streams onthouden niets. Elk decodeert met een attention window van slechts één token. De resultaten zijn snel maar betekenisloos ("he he he he he").

Voor de daadwerkelijk uitrolbare chat heb ik een "getrouwe" build gebruikt: één stream, volledig getraind contextvenster en KV-caching. Deze draait op 19.242 tok/s (gemeten live tot ~21.300 tok/s bij 2.000 gelijktijdige verbindingen).

De Snelheidsladder

De weg van 11 tok/s naar 60k was een ladder waarbij elke trede op silicon is gemeten:

Akte I: CPU in de loop Hierbij wordt overhead verwijderd om de beperkingen van de A53 (~11 tok/s) te overwinnen.

  • Weights per token her-streamen → Resident weights in URAM.
  • Implementatie van KV-cache voor incrementele decode.
  • C MMIO-driver voor een gecompileerde AXI inner loop.

Akte II: De Sequencer (CPU buiten de loop) Een architecturale sprong waarbij de volledige forward pass in de fabric gebeurt.

  • HW sequencer @ 40 MHz.
  • Resident-read GEMV (geen reload per matmul).
  • PE met 256 wide lanes (256 MACs per cycle).
  • GELU stream + LN pipeline.
  • Wide P-lane datapath en BRAM sync-read scratch.

Akte III: Meerdere streams (aggregaat, T=1) 4 tot 16 parallelle streams delen elke weight pass.

  • Batch GEMM N=4 → Ping-pong N=8 → Single-pass merge N=8.
  • N=16 met DSP-packed banks.
  • Softmax latency reductie.
  • Split-brain N=14 (twee cohorten, dual-port URAM).
  • Record: N=16 @ 200 MHz met schedule pipelining → 59.966 tok/s.

Akte IV: Getrouwe uitvoering (N=1) Eén stream met volledig on-chip KV-venster, zodat elk token naar het hele gesprek kijkt.

  • Faithful N=1, T=128 window @ 142.9 MHz → 11.343 tok/s.
  • Optimalisatie van timing paths en MAC stage.
  • LN wide-word cut @ 200 MHz → Live record: ~21.300 tok/s.

Conclusies en Beperkingen

De on-chip techniek wint alleen zolang het model in het geheugen past; het omslagpunt ligt rond de 6,3 miljoen parameters. Daarboven moet er naar DDR worden uitgeweken, waardoor men weer tegen de geheugenmuur aanloopt. Dit is per definitie een techniek voor "speelgoedmodellen".

Ter referentie: Taalas etst gewichten in transistors op een ASIC; Cerebras houdt 44 GB SRAM op een wafer; Groq gebruikt 230 MB per chip en koppelt honderden chips aan elkaar. Het is hetzelfde basisinzicht, maar met budgetten die variëren van negen cijfers tot de prijs van een goed diner.

Ontwikkeling en Tools

Het project is geschreven in Verilog (HDL), zonder gebruik van HLS (High-Level Synthesis). Ik heb hierbij gebruikgemaakt van Claude Code voor de implementatie:

  • Architectuur: 80% mij, 20% Claude Code.
  • Implementatie: 20% mij, 80% Claude Code.

De workflow verliep volledig via de CLI; Vivado werd nooit handmatig geopend. Een volledige run (synthesis, place, route en bitstream generation) duurde ongeveer 30 minuten op een 20-core i7 met 32 GB RAM.

Eindresultaat:

  • Record: 59.965,5 tok/s (N=16).
  • Operationeel: ~21.300 tok/s (getrouw model, N=1).
  • Hardware: AMD KV260 ($250), herprogrammeerbaar in 25 seconden.