一位开发者展示了在廉价FPGA上运行的小型LLM,推理速度达每秒21000 token,附现场演示。
我对 Taalas 的 chatjimmy.ai 演示印象深刻,想看看能在价值 $250 的 FPGA 的可重构逻辑(fabric,即 PL)中塞入什么。整个模型就运行在那里。
没用 DDR(板子上有 4 GB 可用,约 20 GB/s,被 CPU 和 fabric 共享,这个共享控制器就是"带宽墙"),把所有东西都放在 URAM(UltraRAM:大容量宽位片上 SRAM,约 18 Mb,64 个块。存放常驻 INT4 权重镜像。关键特性是真正的双端口,支持两路"分脑"设计)和 BRAM(Block RAM:小巧灵活的片上 SRAM 块,约 5 Mb。存放激活值、中间数据和 KV 缓存)中,我给自己设定了 100,000 tok/s 的目标。

核心数据,位精确、实测于硅片:fabric 上达到 59,965 tokens/秒。同一个模型在这块板子自己的 Arm 核心上只有 11 tok/s。我笔记本的 RTX 3050 Ti 是 719。
Taalas 成功把 Llama 3.1 8B 塞进了他们的芯片,还能正常聊天。我只有约 3 MB 内存可用,这意味着能塞进去的最强模型是 TinyStories(一个约 210 万条极简儿童故事的合成语料库,设计初衷是让哪怕最小的模型也能学到连贯的英文)级别的:316 万参数,INT4 下约 1.5 MB。起初我想通过词形归并(lemmatising,把每个词还原到词根,如 'saying' 归并为 'say','words' 归并为 'word'。Keviniser 把语料库归并到实义词,减少约 30% 的字符数)训练语料并让它听起来像 Kevin Malone,"why use many word when few do trick",但由于这样做并不会让模型变小,只是提速约 30%,我后来又改了回来,就让硬件在两个模型之间切换。
这个工具通过 WebSocket(浏览器和服务器之间的持久双向连接,不同于普通 HTTP 的请求后挂断,正是它让聊天能流式输出字符)与板子建立真正的连接。你的文字经过 Cloudflare 隧道,到达服务主机,再到 Kria,进入 fabric,然后返回。如果状态点是绿色的,你就是在和 Wales 的一颗 FPGA 单独对话。这是一个故事生成器,不是助手。它不理解问题。给它"once upon a time"它会把它补完。
kevin on kria
connecting...
few word do trick. type something, kevin reply from inside the chip.
once upon time
there be little girl
the dog run fast
kevin like eat
一次真实的对话,316 万参数的模型完全运行在价值 $250 的 Kria KV260 FPGA 片上内存中。没有 GPU,token 循环中不涉及 DDR。输出故意做得像电报体:压缩即速度。当两个模型轮转时,徽章会显示哪个是活跃的,并倒数到下一次切换;到零时,整个芯片会用另一个模型重新编程(约 25 秒)。如果点是红色的,说明板子在睡眠或负载过重。打开完整演示。
希望大家没有抱太高期望,1.5 MB 权重能做的事情就那么多。
为什么我会有这块板子
这一切都运行在我的 AMD KV260 开发板上,它搭载 K26 SOM(System-on-Module:KV260 板卡围绕它插接的计算模块)。这是 Zynq 系列最新一代 SoC 之一(具体是 Zynq UltraScale+ ZU5CG/ZU5EV,好记吧?),是经典 7000 系列的继承者。天哪,10 年前我多想得到一块这种开发板啊。
我买 KV260 是为了另一个独立项目,一个确定性视觉流水线。板子被宣传为"视觉 AI 入门套件",但 Vitis(AMD/Xilinx 为其板卡开发的应用栈,KV260 的"视觉 AI"演示运行在上面,大部分跑在 Arm 核心而非 fabric 上)的目标检测运行在四核 A53(A53:KV260 上的四核 Arm Cortex-A53 CPU,约 1.33 GHz。是 fabric 的基准比照对象,也是 CPU 介入时的编排器)上,而 A53 是个弱核心,没有硬件矩阵乘法单元。Vitis 库最终不过是 Linux 上的 OpenCV,而非任何真正利用 fabric 的东西。如果我想在 CPU 上跑概率 AI,肯定不会选四核 A53。
我的确定性深度引擎确实用 Verilog 跑在了 fabric 上,但因为这是个机器人项目,硬件会因安全原因有大量停机时间。于是我在备用 SD 卡上装了新 Linux 镜像并配置好 SSH,这样我在零星空闲时间里可以从任何地方远程工作(事情很快失控了:另一台远程机器现在有了 JTAG 访问权限,PSU 接在 Zigbee 继电器上,所以可以远程强制重启)。
一个事实驱动着一切。逐个生成 token 是内存受限的,不是计算受限的。要产生下一个 token,你需要读一遍模型中的每一个权重。算术运算很便宜,读取才是成本。
在 KV260 上,A53 和 fabric 共享一个约 20 GB/s 的 DDR(片外 DRAM,板子的主存,约 20 GB/s,被 CPU 和 fabric 共享。这个共享控制器就是"带宽墙")控制器。如果模型放在 DDR 里,fabric 和 CPU 喝的是同一根吸管,fabric 相对 CPU 没有任何优势。去 DDR 或通过 AXI(连接 CPU 和 fabric 的片上总线协议家族)去 CPU 的往返是致命的。唯一的出路是模型小到可以完全放在片上内存,那里带宽是数百 GB/s。这就是 Taalas 的洞见,也是 Groq 和 Cerebras 的:内存墙是敌人,片上权重是出路。他们花数亿美元来扩大片上预算。KV260 给你约 3 MB。
单流解码是内存带宽受限的,不是计算受限的。如果权重在 DDR 里,Arm 核心和 fabric 共享一个约 20 GB/s 的控制器,fabric 不会比 CPU 快多少。唯一能赢的方法是把模型做得足够小,让它完全放进片上 SRAM,那里带宽是数百 GB/s 到 TB/s。项目其他所有设计都从这句话出发。
3 MB 要放下权重、激活值和 KV 缓存。这不够装下一个智能模型。勉强够装一个能把句子串起来的模型。所以第二个杠杆——大玩家们大多拉不动的那一个:把模型缩小到问题消失。
把模型做小(这就是 Kevin 的来历)
一个 316 万参数的 INT4(4 位整数,16 个层级。权重以 INT4 存储,正是这使得模型能够缩小到足以放入片上)transformer 约需 1.5 MB。预算达标了,但每一字节仍然重要,通向速度的最后一根杠杆一直是"让模型更蠢"。(或者说我是这么想的)
所以训练语料(TinyStories)先通过一个工具处理,把英语剥离到只剩实义词。"Why waste time saying a lot of words when a few words do the trick" 变成 "why waste time say lot word when few word do trick",灵感来自《办公室》中 Kevin Malone 的交流哲学,是的,模型因此说话像他。在完整语料上,压缩把 3.717 亿词降到 2.605 亿,约 70%,实测如此。
我原以为压缩语料训练的模型会更小。结果尺寸完全一样,事后看这显而易见:参数数量由架构决定,不是由语料决定。压缩改变的是输出分布。同一个故事用约少 30% 的字符讲完,所以有效速度提升了,尽管每 token 速率是一样的。
坦诚版本:归并语料带来约 1.5 倍收益。数量级的收益在片上对 DDR。Kevin 是辣椒上的点缀,不是主菜。
我的目标是 100,000 tok/s。没达到,但我真的拼尽了全力,最后甚至为了减少尽可能多的往返和竞争条件而牺牲了可用性,把注意力窗口缩减到 T=1。
最高纪录构建是 16 个并行流共享一个权重传递,完全在 fabric 内序列化,CPU 零触碰。在 200 MHz 下测得 59,965.5 tok/s,16 个流全部与整数基准位精确一致,三轮跑三次。坦白:16 条流什么都记不住。每条解码时注意力窗口只有一个 token。构建在其上的聊天每发出一个忠实字符就"扑街"了,"嘿嘿嘿嘿嘿"。它很快,但它没有意义,当这两个属性被推进一步时就变成了一回事。
所以部署的聊天是一个不同的、诚实可验证的构建。单流、完整训练上下文窗口、KV 缓存与完整重计算逐比特一致。该构建实测 fabric 达 19,242 tok/s,live 测量约 21,300 tok/s,在 1 到 2,000 并发连接的负载扫描中该数字保持平稳,零错误(观察峰值 21,479)。它会记住你上几轮对话,这就是上面小组件里的那个。这两个数字都不借记录帖的标题。
从 11 tok/s 到 60k 的路径是一架阶梯,每一级都经过硅片验证:
token/秒,对数刻度
在硅片上实测(3/3 次运行,token 流逐比特一致)
SIM(RTL 逐比特对比参考模型;无 bitstream)
A53 聊天 11
XPS15 torch CPU 356
RTX 3050 Ti 719
XPS15 ORT CPU 1,273
0.1
110
100
1k
10k
100k
第一幕:CPU 在循环中
每一级去掉开销,阶梯的渐近线是 A53 本身的约 11 tok/s。一旦 matmul 卸载完成,Arm 核心运行剩余的前向传播就是那道墙。
每个 token 重新流化权重
Python AXI,O(T²) 重计算
0.07
权重常驻 URAM
启动时加载一次
0.22
×3
KV 缓存
增量解码
2.71
×12
C MMIO 驱动
编译后 AXI 内部循环
10
×4
第二幕:序列器(CPU 退出循环)
架构跃升。单流,整个前向传播在 fabric 内进行;从这里开始游戏就是时钟周期。
硬件序列器 @ 40 MHz
超越 CPU 的飞跃
44
×4
常驻读取 GEMV
无需每次 matmul 重新加载
76
×1.7
PE = 256 宽通道
每周期 256 MAC
231
×3
GELU 流 + LN 流水线
PE=128 @ 125 MHz 硅片
752
×3
宽 P 通道数据通路
P=4 非线性 @ 100 MHz
1,883
×3
BRAM 同步读取暂存
P=8 @ 125 MHz
2,484
×1.3
P 宽 GEMV 边界
act-feed + 回读 P/周期
3,512
×1.4
LANES = 256
72 位 URAM 库
5,449
×1.6
周期 floor 削减 + 深流水线
fused RB+DQ+GELU @ 166.7 MHz
9,295
×1.7
3 级 act 量化,200 MHz
最后 sub-8ns 路径
11,144
×1.2
第三幕:多流(聚合,T=1)
4 到 16 条并行流共享每次权重传递,解码时注意窗口为 1。真实硅片,逐比特一致,诚实退化的文本:这些是聚合 token,不是单次对话。
批 GEMM N=44 流,单次权重传递
16,969
×1.5
乒乓 N=8
NL 引擎与 GEMM 重叠
17,741
+5%
单次合并 N=8
两组,单次传递
19,276
+9%
N=16,DSP 打包库
共享 LN/attn,106.5k LUT
24,134
×1.3
Softmax 延迟削减
硅片上 103,582 周期
25,745
+7%
分裂脑 N=14
两个队列,双端口 URAM
36,971
×1.4
N=16 @ 200 MHz
LN 未重定时 + AQ 范围证明
46,604
×1.3
调度流水线
AQ/RUN 重叠,每流 NL
56,263
×1.2
TMAX=16 + 每队列注意
N=16 波 @ 200 MHz
59,966
+7%
★ 聚合记录
第四幕:Kevin 记住了(忠实,N=1)
一个不同的指标,不是退步:单流配备完整芯片上 KV 窗口,因此每个 token 都能关注整个对话。这些 tok/s 组成真实的消息。
忠实 N=1,T=128 窗口
芯片上 KV @ 142.9 MHz
11,343
R5 锥形阶梯,166.7 MHz
7 条时序路径流水线化
13,162
×1.2
调度削减 + MAC 级
KVW/RB/LN 重叠
16,088
×1.2
LN 宽词削减,200 MHz
路由拥塞消除
19,242
×1.2
Live 基准,200 MHz
2,000 连接扫描
21,300
+11%
★ 运行记录
完整阶梯来自 repo 的 fabric/progress.py 加上 live 基准,共 28 级。点击某一级的数字可查看该步骤去掉了什么。第一幕到第三幕统计跨并行流的聚合 token;第四幕是带完整上下文的忠实单流指标,最终为 live 运行记录约 21,300 tok/s,从 1 到 2,000 并发连接保持平稳(峰值 21,479,零错误;按周期计数记录 19,242 @ 200 MHz)。
而且这个天花板是证明过的,不是假定的。有一个脚本能证伪将第三个 MACMAC:Multiply-ACcumulate:矩阵乘法所基于的一乘一加操作。是这里计算的基本单元。打包进 DSP48E2DSP48E2:FPGA 上专用的硬件乘法器块(该芯片上有 1248 个)。每个可打包两个 INT4×INT8 乘累加。——在 120 万个随机乘积上测试,N=32 需要 2,048 个 DSP,而芯片上没有那么多。这个架构在这块硅片上的真实极限是 62k 到 78k。100k 是我刚开始时的空想猜测,我就是觉得那会是个有趣的靶数字。达到 60k 对我来说已经足够接近,而且 >20k 可用已经超过了 Taalas 的数字,这是我给自己设定的另一个目标。
芯片上技巧只在模型能放入芯片时才能赢,交叉点大约在 6.3M 参数。超过这个数字就要溢出到 DDR,然后你就又碰到那道墙了。长上下文同样会溢出 KV 缓存。这是一个按构造定义的玩具模型技巧,输出故意做得很差——这并不代表它就是好的。它是一个带幽默感的测量仪器。
做个比较:Taalas 把权重蚀刻到每个模型的晶体管上( taped-out ASIC)。Cerebras 在晶圆上保留了 44 GB SRAM。Groq 每芯片保留 230 MB 并将数百个芯片串联。从九位数预算到一顿像样晚餐的价格,都是同一句话的洞见。
以上都是故事。工程细节在子文章里,那些文章里的每个数字在引用之前都经过硅片逐比特验证:
芯片内部,宽词 GEMV 技巧、双端口分裂脑、非线性砖块,以及证明 16 流是硬天花板。
不经询问的采样,一个未测量主机循环如何吃掉了每个回复的 58%,以及将每个 token 193 次读取压缩成一次种子写入的 Gumbel-max 恒等式。
向陌生人服务 FPGA,隧道、服务箱,以及为什么仪表板故意放在板子外面。
战史与方法,iverilog 如何说谎,硅片为何比时序报告快 1.3 到 1.76 倍,以及我赢下但还是放弃的那个优化。
全是手写和 LLM 写的 Verilog,没有 HLSHLS:High-Level Synthesis:将 C/C++ 编译成硬件的工具,这样你就不用手写 Verilog。这个项目没有 HLS;每个模块都是直接写出的 RTL,由我和 Claude Code 共同完成,从不来自 C 编译。Claude Code 做了不少活儿,毕竟是个副项目的副项目,不过把 FPGA 推到极限肯定不像用 TypeScript 写 CRUD 应用那么舒服。我大概会把比例放在:
架构设计:我 80%,Claude Code 20%
实现:我 20%,Claude Code 80%
而且尽管说写 HDLHDL:Hardware Description Language(Verilog、VHDL):描述电路而非执行指令的代码。整个项目使用 Verilog。并不舒服,它确实做到了。除了那个立体视觉项目,上次我接触 FPGA 还是大约 10 年前的 Lattice iCE40,或者更早的 Altera Cyclone IV 在 DE0-Nano 上。
我没有研究过开源综合和 bitstream 生成器,但我的工作流没有一次打开过 VivadoVivado:Xilinx 的 FPGA 设计套件:综合、布局、布线和 bitstream 生成。全是通过 CLI 调用。在我 20 核 i7 加 32 GB RAM 上,一次完整运行大约 30 分钟。随着我接近 100% 内存使用,布局和布线明显变慢,但能用这么少资源完成这些仍然令人惊叹。
59,965.5 tok/s @ 200 MHz,实测,N=16,逐比特一致,三次运行各三次。部署的聊天是忠实构建,按周期计数达 19,242 fabric tok/s,在 2,000 连接负载测试下实测约 21,300,部署于 chat.mikeayles.com 就在这里。和 AMD 刚收购的那家创业公司是同一论点,在 AMD 自家 250 美元的板上,可 25 秒内重新编程。
代码现已公开:github.com/MichaelAyles/kev-gpt,Keviniser、模型、RTL 和服务栈,包括可以在你笔记本上无需开发板运行的逐比特门级实现。