4. 权重重排与量化:利用FINN的量化感知训练(QAT)输出,将权重按位切片重组,减少BRAM的位宽浪费
好,咱们来聊聊权重重排和量化。这个话题,说白了就是怎么把训练好的模型权重,塞进FPGA的BRAM里,还不浪费空间。
我刚开始做FINN部署的时候,踩过一个坑。模型量化完了,精度也还行,一综合发现BRAM用了80%多。后来一查,好家伙,每个权重存一个完整的8位或16位数据,但实际有效位宽只有2位或4位。这不就是拿大炮打蚊子吗?
所以这节课,咱们就专门解决这个问题。
4.1 量化感知训练(QAT)到底输出了什么?
先搞清楚一件事。FINN的QAT训练完,你拿到的不是普通的浮点权重。它输出的是量化后的整型权重,比如2位、4位、8位。
举个例子,一个4位量化模型,每个权重值范围是0到15。但FPGA的BRAM最小存储单位通常是8位或16位。你直接把4位数据存进去,另一半位宽就空着了。
我见过有人这么干:
// 错误示范:直接存4位数据到8位BRAM
// 每个地址只存一个4位权重,浪费4位
weight_bram[addr] = quantized_weight; // 4位数据占8位空间
这其实挺浪费的。你想想看,一个BRAM能存1024个8位数据,结果你只用了512个4位数据。容量直接砍半。
4.2 按位切片重组:把权重拆开再拼起来
那怎么办?FINN的做法是按位切片。
什么叫按位切片?就是把每个权重的二进制位拆开,然后重新组合。比如4位量化,每个权重有4个bit:bit3、bit2、bit1、bit0。我们把所有权重的bit3收集起来,组成一个切片;bit2组成另一个切片,以此类推。
这样做的好处是什么?
- 位宽对齐:每个切片只有1位宽,可以紧凑地存进BRAM
- 并行计算:多个切片可以同时读取,适合流水线处理
- 减少浪费:BRAM的每个bit都用上了,没有空洞
我给你们画个图理解一下:
原始4位权重数组(8个权重):
权重0: 1011 (bit3=1, bit2=0, bit1=1, bit0=1)
权重1: 0110 (bit3=0, bit2=1, bit1=1, bit0=0)
权重2: 1100 (bit3=1, bit2=1, bit1=0, bit0=0)
...
按位切片后:
切片3(bit3): 1 0 1 ... → 存到BRAM0
切片2(bit2): 0 1 1 ... → 存到BRAM1
切片1(bit1): 1 1 0 ... → 存到BRAM2
切片0(bit0): 1 0 0 ... → 存到BRAM3
你看,每个切片只有1位宽。如果BRAM是8位宽的,一个地址可以存8个权重的同一个bit。这样BRAM的利用率就接近100%了。
4.3 FINN中的具体实现
FINN的编译器会自动做这件事。但作为工程师,你得理解它背后的逻辑,不然出了问题都不知道怎么调。
在FINN的HLS代码里,权重重排通常是这样实现的:
// 伪代码:权重重排
// 输入:quantized_weights[N][W] N个权重,每个W位
// 输出:sliced_weights[W][N/W_BRAM] 每个切片存到独立BRAM
for (int slice = 0; slice < W; slice++) {
for (int i = 0; i < N; i++) {
int bram_addr = i / BRAM_WIDTH; // BRAM_WIDTH通常是8或16
int bit_pos = i % BRAM_WIDTH;
// 提取第slice位,存入对应BRAM的对应bit位置
sliced_weights[slice][bram_addr] |=
((quantized_weights[i] >> slice) & 0x1) << bit_pos;
}
}
嗯,这里要注意。BRAM_WIDTH不是随便选的。它取决于你的FPGA型号和BRAM配置。我一般习惯用8位,因为大多数FPGA的BRAM原生支持8位模式。
4.4 量化位宽的选择与BRAM节省
量化位宽越低,节省的BRAM越多。但精度也会下降。这是个trade-off。
我整理了一个表格,大家可以参考:
| 量化位宽 | 原始BRAM需求(8位存储) | 切片后BRAM需求 | 节省比例 | 典型精度损失 |
|---|---|---|---|---|
| 8位 | 100% | 100% | 0% | 几乎无 |
| 4位 | 100% | 50% | 50% | 1-3% |
| 2位 | 100% | 25% | 75% | 3-8% |
| 1位(二值化) | 100% | 12.5% | 87.5% | 5-15% |
你看,4位量化就能省一半BRAM。我做过一个项目,原本需要4块BRAM的卷积层,量化到4位后只用2块。省下来的BRAM可以放更大的模型,或者做其他逻辑。
核心要点:权重重排的本质,是把「每个权重占一个完整存储单元」变成「每个bit占一个存储单元」。这样BRAM的位宽利用率从W/8提升到接近100%。
4.5 避坑指南
我曾经踩过的坑:
- 切片顺序搞反:有一次我把MSB和LSB的顺序弄反了,结果推理结果全错。排查了半天才发现是bit索引的问题。建议在代码里加个断言,验证切片重组后的数据能和原始数据对应上。
- BRAM位宽不匹配:有些FPGA的BRAM支持可变位宽,但不同型号有差异。我建议统一用8位模式,兼容性最好。
- 忽略符号位:如果权重是有符号数,符号位扩展要小心。FINN的QAT默认输出无符号整型,但如果你自己做了修改,记得检查。
我的个人习惯:
在写HLS代码之前,我会先用Python写一个权重重排的仿真脚本。把量化后的权重导出来,用Python做切片重组,再写回文件。然后用一个简单的测试向量验证结果是否正确。这一步虽然花点时间,但能避免后期在硬件上调试的痛苦。
4.6 实战建议
如果你正在用FINN做部署,我建议你这样做:
- 先跑QAT:用FINN提供的QAT脚本训练模型,得到量化权重
- 检查量化位宽:看每个层的权重实际用了多少位。有时候你设了4位,但某些层可能只需要2位
- 手动验证切片:用Python脚本把权重按位切片,再重组回去,确保数据一致
- 查看BRAM报告:综合后看BRAM利用率,如果某个层特别高,考虑降低量化位宽
说白了,权重重排这个技巧,就是让你用更少的BRAM干更多的活。在资源紧张的FPGA上,这招特别管用。我做过一个边缘端的模型,原本需要12块BRAM,优化后只用4块,剩下的资源还能加个后处理模块。
好,这节课就到这里。下节课咱们聊聊激活值的量化与动态范围调整,这也是个容易出问题的地方。