昨天在 HN 上翻到一个仓库,标题只有一句话:An open-source AI accelerator, developed by AI.(一个由 AI 开发的、开源的 AI 加速器。)
我一开始是抱着看热闹的心态点进去的,毕竟「AI 自己设计芯片」这种说法今年听得太多了。但把这个 FeSens/openTPU 仓库读了一遍之后,我改了看法——它真正有意思的地方根本不是那句口号,而是它把一个 AI 加速器该有的东西全塞进了一个你能从头读到尾的 monorepo:SystemVerilog 硬件、一条自研的指令集、一个 bit-exact 的模拟器、一门叫 ol 的 kernel 语言和它的编译器、还有驱动真实 PCIe 卡的 host 软件。
它甚至真的在一张 Kintex-7 的 FPGA 卡上把 Qwen3-0.6B 和 LFM2.5-230M 跑了起来,而且卡上吐出来的 token 和模拟器逐 bit 一致。
这句话是全文最值得盯的地方,我后面会展开。
先把这个仓库几个容易踩的坑说清楚。openTPU 这个名字现在不止一个:GitHub 上排第一的是这个 FeSens/openTPU(2026-09-24 才建仓,两百多 star),另一个是 SKYNETAI1/OpenTPU(8 月建仓,定位是 ASIC-first 的 LLM 推理 IP,只有 4 个 star)。两者没有任何关系,我这篇讲的是前者。另外,仓库里那个 developed by AI 标签指的是硬件设计过程本身用 AI agent 来做,不是「这颗芯片的速度来自 AI」。这个区别值得单独讲。
「developed by AI」到底指什么
口号容易让人误会。openTPU 说它把 auto-arch-tournament 的经验搬到了加速器上,问两个问题:AI agent 在硬件设计上能走多远,以及——能不能造出跑自己推理的芯片。
落到代码里,这指的是 tools/tourney/ 那一套。它的工作方式是:
- 每个 RTL 组件(
otpu_mxu、otpu_vpu、otpu_quant、otpu_seq、otpu_tmem、otpu_dma、otpu_fp等)各有一个自己的 champion 分支; - 每一轮,K 个 agent 并行地在各自 worktree 里提出假设、改 RTL、跑门禁;
- 假设 agent 只准写
HYPOTHESIS.md,实现 agent 只准改允许的文件——改了别的文件,这个 slot 直接判 failed; - 门禁按顺序跑:sandbox(只许改本组件的文件)→ lint(Verilator)→ fast(RTL 与 ISA 的 bit-exact 子集测试)→ board(用板级微架构再跑一遍)→ perf(Qwen3 decode 代理,循环数不许比 champion 多 0.2%)→ synth(yosys 出 LUT/FF/DSP/BRAM 和 fmax 估计);
- 最后按接受规则挑赢家:A 是面积赢(fmax 达标且面积等效至少 -1%),B 是速度赢(fmax +3% 且面积至多 +1%)。
那个 area_eq 是个挺有意思的工程决定,它把不同资源折算成一个数:
area_eq = LUT + LUTRAM + 0.5·FF + 40·DSP + 80·BRAM36 + 40·BRAM18
fmax 的估计则是 1000 / (1.6·logic_ns + 0.5)。
这些系数(40 一个 DSP、80 一个 BRAM36)是作者对这块器件成本结构的经验假设。你可以不同意,但重点在于它被写死成规则、每一轮都一致地执行——不然 agent 之间没法比较。
我的判断是:这才是「developed by AI」真正的分量。它不是让 AI 去写一篇关于加速器的论文,而是把「改硬件 → 跑正确性 → 测性能 → 综合出面积和频率 → 择优」这个闭环做成了一条自动化流水线,让 agent 在一个有明确奖励函数的空间里搜索。而人类仍然握着最后一道闸:champion 分支永远不进 main,把一个 champion 合回主线是人工决定。
整个栈长什么样
作者在 README 里画了一张从 Python kernel 一直到 FPGA 引脚的图,我照着整理了一下:
Kernels in ol mlp, attention, full model layers
| @ol.jit
Language + compiler layouts, affine loop addressing, fusion
|
ISA 8 x 32-bit words per instruction
|
ISA simulator <======> RTL same bits, checked by the tests
(Python) (SystemVerilog)
| Vivado bitstream
FPGA card Kintex-7 xc7k480t
| PCIe
Host otpu-chat, otpu-smi, otpu-lens
从上到下几层,各自是什么:
| 层 | 实现 | 关键文件 |
|---|---|---|
| kernel | 一门叫 ol 的 Python DSL |
opentpu/kernels/*.py |
| 编译 | 把 ol 降到数据搬运指令(Triton/Gluon 风格) |
opentpu/compiler.py |
| ISA | 自研指令集,每条指令 8 个 32-bit 字 | opentpu/isa.py、docs/isa.md |
| 模拟器 | ISA 的 Python 参考实现(模拟器即规范) | opentpu/isasim.py |
| RTL | SystemVerilog 硬件,和模拟器逐 bit 对齐 | rtl/top/otpu_top.sv |
| 板 | Inspur YPCB-00338 / Kintex-7 xc7k480t + 两通道 DDR3 | boards/ypcb-00338/ |
| host | PCIe 驱动与用户态工具 | opentpu/host/ |
整条链上最硬的约束是:ISA 模拟器、RTL 和真实的卡,三者必须对每一个写进 TMEM 或 DRAM 的 bit 达成一致。这不是「大致对齐」,是逐 bit。后面你会看到,这个约束决定了整个设计里几乎每一个算术细节。
指令集:没有 cache,也没有隐藏调度
我花时间最多的是 docs/isa.md,因为作者自己说指令集是一切的底座。
机器模型是这样:有 S 个 slice,每个 slice 里有一个 16 × 32-bit 寄存器的 sequencer(R0 恒读 0)、指令内存、一片私有的 DRAM、TMEM、ACT RAM,以及一个 MXU(矩阵单元)、一个 VPU(向量单元)和一个 quantizer。slice 之间除了一个 collective 单元(GATHER、BAR)不共享任何东西。执行严格顺序:每条指令写完所有结果,下一条才开始。
每条指令编码成 8 个 32-bit 字,字 0 长这样:
w0 = opcode[7:0] | ra[11:8] | rb[15:12] | rc[19:16] | rd[23:20] | flags[31:24]
地址一律是「寄存器 + 立即数」。
这里有个我很喜欢的决定:没有 cache,也没有隐藏调度。数据怎么动,全是一条一条指令写死的:DMA 搬数据是 LD/ST,矩阵单元吃从 DRAM 流进来的 int8 权重是 MM,向量单元做 fp32 数学是 VOP,quantizer 把结果转回 int8 是 QACT。作者的原话是:
There is no cache and no hidden scheduling: every data movement is an instruction, so a trace shows exactly where the cycles go.
每一次数据搬动都是一条指令,所以一条 trace 就能直接告诉你时间花在哪了。对于一个教学和研究取向的设计来说,这比性能更重要——它让「为什么这一步慢」变成一个可以逐指令回答的问题,而不是丢给你一句「去 profile 吧」。
还有个细节让我确认作者是认真在设计 bit-exact:fp32 算术全部是 flush to zero(denormal 输入当带符号零、denormal 结果替换成带符号零),而且 exp2、recip、rsqrt、log2 这些超越函数被定义成固定的 add/mul 序列,好让每个实现都 bit-exact。比如 rsqrt 用的是那个著名的 0x5F3759DF 魔数种子,然后固定做三次牛顿迭代,再按指数缩放。规范甚至把 log2 的 minimax 拟合系数逐位列了出来:
3FB8AA3B BF38AA38 3EF639EB BEB8AE27 3E9369C2 BE74ADF2 3E5CE48E BE543E8E 3E00DB73
读到这里我就明白了:如果目标是「卡和模拟器逐 bit 一致」,那每一个浮点步骤、每一次舍入都必须被规定到 bit,否则模拟器的结果就没有意义。这是被那条硬约束逼出来的设计,不是为了炫技。
kernel 和编译器:layout 就是数据搬运
上层是一门叫 ol 的语言,写法很像 Triton/Gluon。一个 kernel 就是 @ol.jit 装饰的 Python 函数,按 slice 各 trace 一次,每个调用降成一到几条 ISA 指令。
编译器的核心概念是 layout:张量住在哪里,就是它的 layout;改变 layout 本身,就是一条数据搬运指令。作者列了一张表:
| 调用 | 指令 | 搬运 |
|---|---|---|
ol.load(t) |
LD |
DRAM → TMEM,burst |
ol.store(t, x) |
ST |
TMEM → DRAM,burst |
ol.quantize(x) |
QACT |
TMEM fp32 → ACT RAM int8 |
ol.dot(a, w, ...) |
MM(必要时加 QACT) |
把 w 从 DRAM 流一次,结果进 TMEM |
x + y、ol.exp2、ol.max… |
VOP |
TMEM → TMEM |
ol.all_gather(x) |
GATHER |
每个 slice 的 TMEM → 所有 slice 的 TMEM |
一个 MLP 的 kernel 长这样:
from opentpu import language as ol
@ol.jit
def mlp(h, gamma, w_gate, w_up, w_down, out, eps):
x = ol.load(h)
xs = ol.quantize(rmsnorm(x, ol.load(gamma), eps))
g = ol.dot(xs, w_gate)
u = ol.dot(xs, w_up)
a = ol.all_gather(silu(g) * u)
y = ol.all_gather(ol.dot(a, w_down))
if ol.program_id() == 0:
ol.store(out, x + y)
真正让我停下来的是 flash attention 那段。opentpu/kernels/attention.py 是一个 online-softmax(flash)attention,按 FlashAttention-3 的风格做了软件流水:VPU 和 quantizer 还在收尾 block b 的时候,MXU 已经在把 block b+1 的 q.K^T 往另一个 score buffer 里流了。循环体覆盖两个 block,所以每个临时量都是双缓冲:
def scores(t0, n, out):
ol.dot(qs, K[t0:t0 + n, :], out=out, rowmax=True)
def finish(s, t0, n):
vs = ol.load(VS[t0:t0 + n])
m_new = ol.maximum(m, s.rowmax)
p = ol.exp2(s - m_new[:, None])
alpha = ol.exp2(m - m_new)
pq = ol.quantize(p * vs[None, :])
ol.dot(pq, VT[:, t0:t0 + n], acc=acc, acc_scale=alpha)
l.set(l * alpha + ol.sum(p, axis=1))
m.set(m_new)
注意 rowmax=True——softmax 的 max 被折进了 MM 指令本身的 RMAX epilogue 标志位里,一次矩阵乘顺便把行最大值也写出来,省掉一趟单独遍历。类似的还有 acc 加 acc_scale=alpha,把 acc = acc*alpha + P.V 的 rescale 也塞进 MM 的 drain 阶段。
对 4 个 query head、d=32、32-token block、T=160 的情况,作者用 opentpu.isa.disassemble 打出来的循环体是 22 条指令加上 3 条地址步进。这个数字很能说明问题:从一门 Python DSL 到 22 条机器指令,中间没有任何被藏起来的调度或运行时。
MXU:一个会「流动」的二维 systolic 阵列
矩阵单元是整个设计里最吃资源的部分,也是作者记录得最细的。默认实现(MXU_IMPL=2)是一个 D × MCOLS 的 systolic 阵列:每推进一次,一个 D=128 字节的权重 chunk 去碰 MCOLS 个 activation block,每列把 128 个 int8 × int8 的乘积精确求和,再跑每列的 fp32 epilogue。
关键设计是:权重是流动的,不是广播的。chunk 只解码一次,在每条链的每个 stage 上 skew 一次,然后每过一个 column 就移动一个寄存器 hop。第 j 列在第 0 列之后 j 个 cycle 才看到它。每个 hop 寄存器只驱动一个 DSP 和下一个 hop——扇出是 2,不是 MCOLS。作者在文档里写明,正是原来的「广播 + fabric 加法树」让 MCOLS=4 都过不了时序。
4-bit 权重也有专门的通路(PAIR):列 j < M 取 chunk 的低 block,j >= M 取高 block,选择动作放在 DSP48E1 自己的 pre-adder 里做,省掉了每个位置的 fabric mux。
代价是 DSP 用得多:每列 128 个乘积 DSP、4 个 sub-block 乘子、外加 epilogue 的 fp 乘子,一共 138 个;MCOLS=8 时是 1106 个 DSP,占这块器件 1920 个 DSP 的一大半(其余设计才用约 150 个)。
作者把 MXU 单独拉出来做了 OOC 综合(Vivado 2026.1,xc7k480t-2,150 MHz 时钟,D=128),结果是这样:
| MXU | MCOLS | LUT | FF | slices | DSP | fmax |
|---|---|---|---|---|---|---|
| IMPL 0(加法树) | 4 | 26,389 | 15,504 | 9,479 | 282 | 153.5 MHz |
| IMPL 2, use_dsp | 4 | 24,426 | 31,540 | 12,232 | 554 | 157.9 MHz |
| IMPL 2, use_dsp | 8 | 47,822 | 60,926 | 23,706 | 1,106 | 151.0 MHz |
| IMPL 2, DSP48E1 | 8 | 44,779 | 67,997 | 24,214 | 1,106 | 151.5 MHz |
从 MCOLS=4 扩到 8,每列多花约 5.6K LUT、8.3K FF、2.5K slices 和 138 个 DSP。拿这些数字去对比一颗 2013 年发布的 Kintex-7,你会很直观地感受到:这是一个「能不能装得下」的工程,而不是「谁的算力更大」的比赛。
性能:真实的卡,真实的数字
作者在一张 Inspur YPCB-00338 卡(Kintex-7 xc7k480t 加两条 DDR3 通道)上跑了十个模型,权重用的是真实权重。我挑几个关键配置整理成表:
| 模型 | 权重 | Decode(设备) | Decode(wall) | Prefill | Decode 时 DRAM |
|---|---|---|---|---|---|
| LFM2.5-230M | int8 | 59.0 tok/s | 52.3 tok/s | 295.6 tok/s | 14.5 GB/s (85%) |
| LFM2.5-230M | 4-bit | 85.8 tok/s | 82.1 tok/s | 335.4 tok/s | 14.1 GB/s (82%) |
| Qwen3-0.6B | int8 | 21.6 tok/s | 21.3 tok/s | 92.1 tok/s | 14.4 GB/s (84%) |
| Qwen3-0.6B | 4-bit | 31.3 tok/s | 30.7 tok/s | 103.4 tok/s | 13.9 GB/s (82%) |
| Qwen3.5-0.8B | 4-bit | 24.5 tok/s | 23.3 tok/s | 66.7 tok/s | 14.1 GB/s (83%) |
| Phi-4-mini (3.8B) | int8 | 3.99 tok/s | 3.98 tok/s | 13.8 tok/s | 16.0 GB/s (94%) |
这张表里最重要的不是 tok/s,是最后一列。几乎所有配置在 decode 时都把 DDR3 打到了 82–94% 的峰值带宽。作者自己也点破了:
Decode is bound by DRAM.
decode 是被 DRAM 带宽绑死的,不是被算力。这个结论解释了一个很反直觉的现象:把 MXU 的 MCOLS 从 4 加到 8(算力翻倍),decode 一点都不会变快——瓶颈根本不在那里。它也解释了作者为什么把工程重心放在 LiteDRAM 控制器的校准、DRAM burst 效率、以及 4-bit 量化上:把每 token 的字节数砍掉约三分之一,直接换来 40–45% 的 decode 提速。
在一个只有两条 DDR3-1066(17.1 GB/s 峰值)的 2013 年器件面前,这几乎是一道物理墙。4-bit 量化之所以能把 LFM2.5-230M 从 59 干到 85.8 tok/s,本质上就是它在同一道内存墙前面少搬了三分之一的字节。
顺便说,KV cache 的布局也是为带宽服务的:V 被转置成 [d, cap],按 256 token 一块存成 [d, 256] 的连续布局。这样一来,P·V 每次流一个连续的 32 KB tile,走满 burst;不做这个,那 128 行里每一行都是相隔 cap 字节的一小块,带宽利用率会被打散。
两个容易被忽略的工程选择
既然 decode 卡在内存墙上,作者在「少搬字节」这件事上是下了功夫的,值得单独说说。
4-bit 权重不是随便截断的。它用的是 FP4 值加上两级 block scale,平均 4.25 bit 一个权重,而且专门把 LM head 留在 int8——因为最后一层直接决定输出 token,精度损失最敏感。作者在 docs/quant.md 里如实记录了每个模型 perplexity 的代价,没有藏。换来的是什么?每 token 的字节数少约三分之一,decode 提速 40%(Qwen3.5)到 45%(Qwen3、LFM2)。对一个内存墙绑死的设计,这个交换划算得几乎不用犹豫。
另一件事是 MoE。这块卡只有 4 GiB 逻辑 DRAM,怎么跑比卡内存还大的专家模型?
作者的做法是这样的:卡自己负责每个 token 的路由,并计算每一个专家;它把专家按层放在 DRAM 的 slot 里,host 只负责把缺失的专家从 pool 文件按 PCIe 的速率补进这些 slot。
| 模型 | 参数量/激活 | decode | 专家命中率 | 每 token 流式字节 |
|---|---|---|---|---|
| LFM2.5-8B-A1B | 8.5B / 1.7B | 10.6 tok/s | 98.5% | 5.2 MB |
| Qwen3.5-35B-A3B | 34.7B / 3.0B | 3.95 tok/s | 62% | 153 MB @ 1.41 GB/s |
同样是「能跑」,两者的体感差得很远。8B 那个命中率 98.5%、每 token 只流 5.2 MB,基本是在卡上跑;35B 那个命中率只有 62%、每 token 要流过 153 MB,PCIe 成了新的墙——它证明的是这套 offload 机制成立,而不是它好用。作者也没把它写成亮点,这一点挺诚实。
硬件是怎么一晚上「长」出来的
仓库里有一份 docs/status.md,标题是「2026-09-24,第一次 Vivado 构建之前」。它记录了一夜的优化,我觉得这是整份文档里最能说明「AI 参与设计」到底改变了什么的证据。
yosys 的综合估计(不含 XDMA 和 MIG IP),从「夜里开始」到「现在」:
| 指标 | 夜开始时 | 现在 |
|---|---|---|
| LUT | 131,915 | 82,224(xc7k480t 的 27.5%) |
| FF | 55,890 | 35,944 |
| DSP | 416 | 267 |
| BRAM36 | 603 | 603 |
| 逻辑延迟 | 14.94 ns | 5.56 ns |
| 估计 fmax | 41 MHz | 106 MHz |
一晚上把估计 fmax 从 41 MHz 抬到 106 MHz,LUT 砍掉近四成。作者把这一夜拆成两部分:一部分是人工做的跨单元时序修复(TMEM arbiter 改成写掩码仲裁、grant 只到 BRAM enable、给 VPU/quantizer/MXU 累加路径的输入寄存 TMEM 数据、把串行的冲突计算改成并行的一对一比较……),另一部分是组件锦标赛:让 Opus agent 一个一个单元去调,最终接受了 32 个胜出方案,每个单元估计 fmax 都过了 110 MHz,MXU 和 quantizer 还小了 40–60%。
这里我有一点保留。docs/status.md 里那句「人类仍然握着 merge 闸门」很关键——agent 能把局部单元调到更快更小,但「整颗芯片成不成」(跨单元时序、DRAM 校准、PCIe、复位极性)仍然是人在兜底。文档里甚至记了一个典型的人工介入案例:bd.tcl 里的 rst_core 把 MMCM 的 locked 信号接到了 active-high 的复位输入上,这在硬件上会把核心一直摁在复位里。这种 bug 不是性能问题,是「根本起不来」,agent 的局部指标看不出它。修完之后,作者给 make lint 加了一条规则,要求每个 proc_sys_reset 都必须声明极性。这个细节比任何「AI 设计硬件」的口号都更有信息量。
它到底给谁用
读完一圈,我对它的定位有了判断。
它不是 Nvidia 的替代品,也不是要跟 Ascend、Zhenwu 这种量产芯片抢地盘。它更像一座显微镜加试验台:
- 如果你想搞明白「一个 LLM 推理在硬件上究竟怎么跑」,
docs/那条阅读路线(ISA → kernel/compiler → isasim → rtl → 整模型 → 板)是很少见的、可以从 Python 的 matmul 一路读到导线的完整路径; - 如果你关心「AI agent 到底能不能做硬件设计」,这里是目前少有的、把 agent 循环、门禁、奖励函数和真实板级验证都摆到台面上的公开实验;
- 那个 bit-exact 的性质,让它变成一个可复现的参照系——你在模拟器上验证过的数值,能在真卡上逐 bit 重现,这在「AI 推理加速器」这个领域其实是很奢侈的事情。
缺陷也很明确,我列一下,免得读者买错预期:
- 板子是小众的(Inspur YPCB-00338),一个两百多 star 的项目,几乎只有作者一人在推,
pushed_at一直在动却始终没有正式 release; - 它只跑小模型。我看到的实测最大到 Qwen3.5-4B,3.8B 的 Phi-4-mini 在 int8 下只有 3.99 tok/s。MoE 大模型(Qwen3.5-35B-A3B)能把专家流式 offload 跑起来,但只有 3.95 tok/s,62% 的专家命中率说明它是一种「能跑」而非「好用」的形态;
- 名字撞车(前面提过),搜资料时要小心认错项目。
写在最后
2026 年,「AI 自己写代码」已经不稀奇了。这个仓库让我觉得值得写一篇,是因为它把 AI 生成的范围往前推了一格——从软件推到了可以直接烧进 FPGA 的硬件。
但更有意思的其实是它顺手暴露的另一件事:当 AI 的效率被推到硬件层,约束并没有消失,只是换了张脸。openTPU 里最扎眼的数字不是 85.8 tok/s,也不是 106 MHz,而是 decode 时那 82–94% 的 DRAM 占用率。算力可以靠 agent 一晚上翻倍,内存墙一步都动不了。
所以真正稀缺的,从来不是「设计得多快」,而是「知道墙在哪」。这一点,agent 目前还答不上来。
相关阅读
- 《DeepSeek 开源华为 Ascend 编程工具链!TileLang 剑指 CUDA,中国 AI 芯片生态加速独立!》 —— 同样在把 LLM 往自研硬件上拉,但走的是编程工具链的路,不是 RTL。
- 《豪掷 $530 亿!阿里发布最强 AI 芯片 Zhenwu V900…》 —— 量产 AI 芯片长什么样,可以拿来和这块「学习用」的 FPGA 对照。
- 《三百万沙箱一天!DeepSeek 开源弹性计算平台 DSec…》 —— 另一条「AI 推理与训练」的底层基建路线,在软件侧。