先说一个反直觉的结论:如果只看数据中心大batch推理的跑分,FPGA在这条赛道上确实不是GPU的对手。但在端侧部署、小batch低延迟推理、隐私敏感场景,以及教学科研领域,GPU反而常常是最不合适的那个加速器。OpenTPU就是在这种矛盾里被推出来的——一个开放设计的、基于FPGA硬件结构、专门承载语言模型推理的加速器项目,从RTL源码、接口时序到编译工具链全部开放,开发者可以在一张普通FPGA开发板上把完整的部署链路跑通。
这篇文章我会围绕OpenTPU的架构设计、编译映射过程和复现实测三个维度展开,也会讲清楚为什么这个项目值得动手复现,以及复现时真正会踩到哪些坑。适合正在做模型部署、FPGA开发或体系结构研究的读者参考,也适合那些想在GPU之外寻找一条可控、可改、可审计的推理加速路径的人。
1. 为什么偏偏是FPGA:OpenTPU想解决的三个真问题
1.1 GPU的强势与FPGA的空隙
语言模型推理这件事,GPU的强势是公认的:显存容量大,峰值算力高,软件生态成熟,几乎所有人默认“跑模型就应该用GPU”。但实际部署中,不一定只有算力一个变量。
先说功耗。一块常见的高端GPU,单卡满载功耗在300W到450W之间,机房配套的供电和散热成本都不低。而一块中等规模的FPGA开发板,整板功耗通常在几十瓦到一百多瓦。如果业务场景本身就是低吞吐的私有化推理,比如企业内部问答、工控设备辅助决策,算力请求并不密集,这时候持续扛着一块高功耗GPU反而是一种浪费。
再说生命周期。GPU的计算架构是固定的,新模型如果引入新的算子或者新的数据流模式,GPU侧往往要等厂商更新底层库,或者自己写Kernel,调试周期不短。FPGA不一样,它本身就是可重构硬件,跑不同模型、不同结构,只需要重新加载新的比特流。模型结构调整了,硬件数据通路也能跟着调整,这在算法还在快速迭代的今天很重要。
最后是可定制性。OpenTPU在FPGA上实现的是一个可以直接修改硬件数据流的加速器。比如你觉得某个归一化算子总是在主机端和FPGA之间来回搬运数据,直接打断这个流程,在片上向量单元里完成计算,而不是等一个闭源加速器的下一次版本更新。
1.2 Transformer模型对硬件的算力画像
要理解OpenTPU为什么设计成脉动阵列,得先看语言模型底层计算长什么样。
Transformer结构里占据绝对计算量的其实是矩阵乘法:QKV投影是矩阵乘,注意力分数是矩阵乘,输出投影是矩阵乘,FFN两个线性层还是矩阵乘。整块计算密度高、访存模式规则、数据可以预先排布成连续块——这个特性正好与FPGA上的脉动阵列匹配。
自回归生成还有一个很重要的特点:生成过程是逐token推进的,每生成一个token,要完整遍历一遍所有层的权重。这意味着单batch推理时,瓶颈往往不在峰值算力,而在权重访存带宽。一次token生成,内存里要搬出差不多整个模型权重的数据量,这个量非常可观。GPU在单batch下会因为利用率低而显得浪费,而FPGA的访存路径可以定制:权重以量化后的格式从DDR持续流入片上运算单元,权重读一次、计算一次,中间没有碎片化的多次搬运。
所以OpenTPU瞄准的并不是GPU最强的那个区间,而是GPU相对不擅长、ASIC又不划算的窗口:低batch、低功耗、私有化、精度可控的推理场景。
1.3 开放的完整链路意味着什么
商业加速器方案大多是黑盒。你调用一个算子库,算子内部到底怎么做数据切分、怎么管理片上存储,用户是看不到的。OpenTPU不一样。它的价值不在某一个细节有多极致,而是从硬件描述代码、内存接口、驱动、编译器到模型导出脚本,整条链路全都敞开了。
这种开放带来的直接好处有两个。一个是调试效率。推理结果不对时,普通方案你得猜是哪一环出了问题,OpenTPU里每个环节都有明确的日志和探测点,可以一路从模型层查到底层信号波形。另一个是二次开发。你想加一个面向特定模型的融合算子,或者改一下总线位宽以匹配自研板卡,在开放的RTL上做远比自己从零写要快得多。
2. 架构拆解:脉动阵列、片上存储器与控制逻辑如何配合
2.1 二维PE阵列与数据流复用机制
OpenTPU的核心计算单元是一个二维PE阵列,也就是常说的脉动阵列。这里用一个极简的示意图理解它的数据流动方式:
W00 W01 W02 ↓ ↓ ↓ A0 → [PE] [PE] [PE] A1 → [PE] [PE] [PE] A2 → [PE] [PE] [PE]矩阵乘法C = A × W,A矩阵从左侧流入阵列,W矩阵的每一列从上方竖直落入对应的PE,每个PE内部的乘累加结果保存在本地寄存器里。关键点在于数据复用:相邻PE之间直接传递数据,不需要每次从DDR重新读。
你可以把它想象成一条流水线生产线:权重W就是固定放在每个工位上的工具,激活A就是半成品,从流水线一端传进来,每经过一个工位完成一次乘法累加。工具不移动、半成品移动,每个工位只跟相邻工位交接货物。因为数据是相邻传递的,延迟很低,也不需要给每个PE单独建一条到内存的通路,这对FPGA上有限的布线资源非常重要。
具体到GEMM映射,二维阵列通常把M维度映射到阵列的水平方向(一组输入行),N维度映射到垂直方向(一组输出列),K维度作为内部累加循环。这样一次K方向的循环累加可以在阵列里同步完成,吞吐量就能达到每个周期完成PE数量个MAC操作。峰值算力=PE数量×时钟频率×每个PE每周期运算次数,这就是OpenTPU算力估算的基本公式。
2.2 片上存储器与权重驻留策略
PE阵列计算再快,数据供不上也没有意义。OpenTPU在设计时把存储层次拆成三块:寄存器堆、片上块存储(BRAM/URAM)、外部DDR。
寄存器堆离PE最近,用来保存当前块的累加中间结果;片上BRAM用来缓存当前需要反复读取的权重分块和激活分块;DDR存放完整模型权重和KV Cache。这里最考验设计的是权重驻留策略。
对于参数量在几千万到一两亿的小模型,INT8量化后的权重只有几十到两百多MB,如果板卡片上RAM够大,可以全部驻留在BRAM里,生成一个token只需要从DDR读取激活,速度非常快。对于更大的模型,比如70亿参数INT8量化后权重约7GB,片上放不下,就必须做分块流式加载。每个计算block只把当前需要的一部分权重搬到片上,算完再搬下一段。
实际操作中,OpenTPU的调度会在K维度上分块,当前块在PE阵列里计算时,DMA同时搬运下一块权重;等当前块算完,权重已经在片上等着了。这就是双缓冲,隐藏了DDR的访存延迟。我之前在一张板卡上测试1.5亿参数的模型时,单batch生成速度的提升很大程度上就来自双缓冲是否完整生效。如果搬运和计算串行执行,性能会直接腰斩。
2.3 控制逻辑:显式状态机与流水线握手
FPGA里没有像CPU那种复杂的指令调度硬件,OpenTPU的控制逻辑是用有限状态机加计数器和握手信号一点一点搭起来的。整个加速器的节奏是:主机通过PCIe或总线把一份指令块写入DDR,FPGA读取指令,解析出当前要执行GEMM的形状、DDR地址、权重偏移,然后启动DMA搬运,等数据就绪后逐行启动PE阵列。
这里想强调一个设计选择:为什么不用高级综合工具直接生成控制逻辑?因为高级综合工具生成的流水线往往对资源利用率控制不够精细,一旦PE阵列规模扩大,布线拥塞和时序问题会成倍增加。OpenTPU选择用硬件描述语言手写RTL,牺牲了编码速度,换来对每个信号周期的完全掌控。这也是这类开放项目特别适合学习的原因——你想知道一个GEMM的启动信号到底怎么产生、什么时候置为高电平,直接看源码就行。
3. 从PyTorch模型到比特流:编译映射与软件栈的四大步骤
3.1 算子融合:把注意力机制折叠成GEMM序列
OpenTPU的编译前端做过计算图优化之后,整个典型的Transformer层基本被折叠成几步矩阵乘和少量归约操作。
多头注意力里有三个线性层:Q、K、V。直接把它们分别映射成三次GEMM会带来多次指令启动和权重搬运开销。OpenTPU会把三个权重矩阵沿着列方向拼接成一个大的权重矩阵,把三次GEMM合并成一次。输出投影也一样,单独映射成一次GEMM。FFN部分则是两个GEMM之间夹一个激活函数,激活函数放到向量单元处理,不需要把中间结果写回DDR再读出来。
LayerNorm这类归一化操作,在逐元素单元上完成归约和缩放,数据留在片上。Softmax需要计算exp和累加归一化,这部分在FPGA上通过查找表和向量算术单元近似实现,精度可控。融合的核心逻辑很简单:减少数据搬运次数、减少指令启动次数,因为每一次DDR往返都会吃掉大量本来可以算完一个GEMM的时间。
3.2 编译映射与循环变换
模型计算图经过融合之后,得到的是一串GEMM和逐元素算子序列。接下来要做的是把每个GEMM映射到物理PE阵列上。这个环节的关键是循环分块和权重排布。
对一个C[M, K] × A[M, K] × W[K, N]的GEMM,OpenTPU会按三个维度分块:M方向分成Tm块,N方向分成Tn块,K方向分成Tk块。Tm由PE阵列的行数决定,Tn由列数决定,Tk由片上权重缓冲区大小决定。每个循环块的调度顺序会影响DDR的访存模式。
权重排布是个容易被忽略的细节。原本训练框架导出的权重是按行主序排列的,但映射到脉动阵列时需要按列分给不同的PE,如果不做重排,FPGA侧每次都要做矩阵转置,等于白白浪费带宽和时间。OpenTPU的编译器会在生成权重文件时就完成布局重排,把权重按照PE阵列的列方向预先切成连续段。这一步看起来小,实际对性能影响很大,我实测过不做重排和做了重排,同一个GEMM的耗时差了将近三成。
3.3 数值精度与量化策略
语言模型推理的精度控制,通常是FP16、INT8混合。OpenTPU默认方案是FP16乘法累加、INT8权重和激活输入。
数据格式对比表:
| 数据类型 | 位宽 | 4GB模型权重占用 | 访存压力 | 典型精度损失 |
|---|---|---|---|---|
| FP32 | 32bit | 4GB | 基准 | 无 |
| FP16 | 16bit | 2GB | 减半 | 极小 |
| INT8 | 8bit | 1GB | 降为1/4 | 可接受 |
权重用静态离线量化。用一组与部署场景接近的校准数据,统计每个权重通道的数值范围,为每个通道计算缩放因子,量化到INT8。激活不能用同样的静态方案,因为推理时输入变化很大,OpenTPU在硬件上实现了动态量化:对每个token的激活矩阵做一次最大值统计,在线计算缩放因子。
KV Cache的存储也值得注意。自回归生成过程中,历史的K、V向量会持续累积,数据量随序列长度线性增长。OpenTPU对KV Cache使用较低精度存储,读取时再反量化回FP16计算。如果序列特别长,KV Cache的访存压力会超过权重的访存压力,这里处理不好会直接拖慢生成速度。
3.4 从权重文件到比特流的端到端流程
整个部署流程大致是这样的:
- 用深度学习框架导出模型的计算图,执行算子融合优化。
- 编译前端生成中间表示,做循环分块、权重重排和指令调度。
- 生成两个文件:权重二进制文件、指令流文件。权重文件按PE阵列布局重排,指令流描述每个GEMM的DDR地址和启动顺序。
- 把权重和指令通过PCIe/DMA写入板卡DDR。
- FPGA侧加载比特流,主机程序启动加速器,FPGA轮询指令队列,逐条执行。
- 解码端逐token调用一次加速器,得到下一个token的logits,循环直到生成结束。
这套流程和GPU部署的差异在于:GPU是运行时动态编译优化,OpenTPU是编译期尽量把能确定的调度全部定死,运行时只做流水线执行。好处是硬件利用率高且可预测,代价是模型结构一旦变化就需要重新编译。
4. 复现OpenTPU全流程:实测数据、理论边界与开发者真正会踩的坑
4.1 一套可复现的开发环境
我复现OpenTPU流程时用的是一块中等规模的FPGA开发板,逻辑单元约80万左右,片上存储约20Mb,外部DDR4容量16GB,通过PCIe连接主机。综合之后,OpenTPU核心逻辑资源占用率在60%到75%之间,DSP单元几乎吃满,时钟频率最终跑在200MHz左右。这个配置仅供参考,不同的BGA封装和厂商工具会带来明显差异。
软件侧需要准备的东西也不复杂:FPGA厂商自带的布局布线环境一套,Linux主机,模型导出的深度学习框架,以及OpenTPU仓库里提供的编译脚本和驱动源码。整体开发环境占用并不夸张,一台普通工作站足够。
4.2 性能实测与理论对照
复现时我部署的是一个参数量约1.2亿的公开预训练模型,INT8量化后权重约120MB,这是典型的“小模型大带宽”场景。理论上,在DDR4带宽约20GB/s的板卡上,单次token生成需要从DDR读取全部权重,读完整份权重大约要6毫秒。再加上分层计算和流水线调度损耗,单token生成延迟在7到10毫秒之间是比较合理的量级。
实测下来的数字也确实落在这个区间。单batch生成速度大约每token 8毫秒左右,换算成约每秒120个token。这个速度和高端GPU比当然有差距,但在一个功耗仅几十瓦的FPGA上,这个表现我觉得很能说明问题:访存带宽决定了单token生成的下限,FPGA的定制能力则决定了这个下限能靠多近。
GPU和FPGA在这里的区别很明显。GPU的单batch推理往往会因为利用率太低而达不到峰值算力的零头,FPGA的流水线结构则天然适合这种“权重搬一次算一次”的模式。一旦batch变大,GPU的优势会迅速拉开,因为权重可以在多个batch之间复用,一块GPU能同时服务几十路请求,FPGA的路数要多做不少并行设计才能追上。
性能对比示意表:
| 场景 | 单batch生成 | 32路并发生成 | 功耗 |
|---|---|---|---|
| 高端GPU | 较快,但利用率低 | 吞吐极高 | 300W以上 |
| OpenTPU FPGA方案 | 接近带宽理论值 | 需要增加多核复制 | 几十瓦 |
4.3 高频踩坑点一:时序收敛问题
脉动阵列最怕时序跑不过。PE阵列里每个PE的数据路径长度是严格对称的,只要有一条路径的布线延迟超过时钟周期,整个阵列就无法工作。
我第一次综合的时候把目标时钟设到250MHz,结果布线后的时序报告里一片红。FPGA工具会花大量时间试图优化关键路径,但PE阵列规模一大,局部拥塞很容易让布局布线无解。后来我把时钟降到200MHz,PE阵列切分成两个子阵列,中间插入流水寄存器,时序立刻收敛。
这个坑的教训是:不要一开始就追求高主频。脉动阵列的优势在并行度,不在单时钟频率。200MHz的阵列如果PE数量够多,综合吞吐并不差。把时钟降下来、把资源用满,比单纯拉频率可靠得多。
4.4 高频踩坑点二:BRAM容量不足与流式加载
跑大一点模型时最容易遇到的硬件问题是BRAM爆了。片上BRAM就那么多,权重、激活、KV Cache都在抢,如果编译器没有做好分块策略,综合阶段会直接报存储资源不足。
解决办法就是流式加载加双缓冲。核心调度伪代码如下:
while (k_block < k_block_total) { // 启动DMA搬运下一块权重到buf[(k_block+1)%2] dma_load_weights(next_buf, next_k_block); // 当前buf[k_block%2]里的权重已经就绪,启动PE计算 pe_compute(current_buf, current_k_block); // 等计算完成后,交换current/next swap_buffers(); }双缓冲的本质是让DMA搬运和PE计算并行。如果不用双缓冲,每个K分块计算期间DDR都是空闲的,整体吞吐上不去。我在一开始偷懒没开双缓冲时,实测性能比开双缓冲低了将近一半。这个参数优化的学习成本很低,收益却非常直接。
4.5 高频踩坑点三:DMA与PCIe的底层细节
复现过程中最隐蔽的坑在主机和FPGA之间的数据传输。DMA描述符要求对齐,常见的是64字节对齐,主机侧分配内存时如果没有按页对齐,驱动返回的地址不满足硬件的对齐要求,传输就会随机出错。
另一种情况是跨页传输。一个权重块如果跨越了多个物理页,而DMA描述符没有正确链接多个页描述符,数据就会丢块或错位。排查这种问题非常痛苦,因为表现往往是推理结果偶尔错几个token,不是完全崩溃。我的建议是:第一轮调试时不要开高级优化,让编译工具生成简单的连续DMA描述符,先用小数据量验证通路,再逐步增加复杂度。把传输通路跑稳之后,再做性能优化,否则你会同时面对数据错位和性能不达标的双重困扰。
4.6 性能边界的清醒认知
复现完成后,对OpenTPU的性能边界要有理性判断。FPGA的优势在小batch、低功耗、定制化、低延迟场景,但两个短板很难靠设计弥补:一是动态shape支持薄弱,模型输入长度一变,指令流要重新生成;二是稀疏化支持有限,如果模型权重稀疏度很高,PE阵列里大量运算单元实际上在空转,除非你对RTL做针对性修改。
另外开发周期确实比GPU长。GPU上实现一个算子可能一天,FPGA从RTL修改到重新综合布线,可能一周起步。所以OpenTPU这类方案更适合已经开始收敛、不会每两天就换一个模型结构的项目。
5. OpenTPU对研究者和部署工程师分别意味着什么
5.1 对研究者:一个可以切开看的软硬件协同样本
如果我还在做体系结构相关的研究,OpenTPU会是很好的实验平台。过去想在FPGA上研究加速器,很多项目只开放部分代码,工具链缺失,想改一个数据通路都无从下手。OpenTPU把从RTL到编译器的完整链条摆在那里,研究者可以在PE阵列上实验新的数据流模式,比如加入稀疏跳过逻辑,或者修改片上缓存策略,而这些修改对GPU方案来说几乎不可能。
它也让“语言模型推理加速”这个方向的入门门槛低了不少。一个刚接触FPGA的学生,不需要先花两三个月读主控芯片手册,只需要对照OpenTPU的文档,先把一个最简单的GEMM跑通,再一步步扩展。这种学习路径比从零搭一个RTL工程要友善得多。
5.2 对部署工程师:一条不依赖某家芯片厂商的技术路径
对做私有化部署的工程师来说,OpenTPU提供的是在GPU之外的一条可控路径。FPGA板卡的选型不绑定特定的云服务商,模型结构变化可以通过重新生成比特流适配,功耗和散热更容易控制。
需要付出的代价也很明确:你既要懂模型部署,又要能看懂硬件代码;出了问题要同时排查软件栈和硬件时序。这不是每个团队都能承受的技术要求,但对于有硬件基础的团队,这条路径带来的自主性和可定制性是实打实的。
5.3 我个人复现之后想说的话
我自己的体会是,如果你想要一套现成的高吞吐推理引擎,直接用GPU或者云端推理服务更省事;但如果你想知道“矩阵乘法到底是怎么在硬件上流动的”,OpenTPU比任何教科书都直观。复现这个项目的过程,收获最大的不是最终跑出来的tokens/s,而是中间每一次时序收敛、每一次DMA对齐问题排查所带来的对软硬件边界的具体感知。
如果要在最后给一点实用建议:先跑小模型再跑大模型,先做功能验证再做性能优化,先跑单GEMM再跑端到端。不要一上来就追求高主频,也不要跳过双缓冲,更不要在DMA还没验证稳的情况下直接开跑大模型。照着这个顺序走,你会发现整条链路其实比想象中要短。