第三章:HLS 数据类型与任意精度类型
各位同学,今天我们来聊聊 HLS 里一个特别基础、但又特别容易踩坑的话题——数据类型。
说实话,我刚开始用 HLS 那会儿,觉得数据类型嘛,不就是 int、float 这些?跟 C 语言差不多。结果第一次综合一个卷积核,发现资源消耗大得离谱,延迟也高得吓人。后来一查,问题就出在数据类型上。
嗯,这节课我们就把它彻底讲清楚。
3.1 为什么 float 在 HLS 中不香?
先回答一个灵魂拷问:为什么很多做软件的同学,一上来就喜欢用 float?
因为方便啊!精度高、范围大,写起来省事。但在 FPGA 里,float 可就没那么友好了。
核心原因:FPGA 没有原生的浮点运算单元。float 的加减乘除,都需要综合成大量的 LUT 和 DSP 块。
我给大家算笔账:
- 一个 float 乘法器,大概要消耗 3-5 个 DSP48E1 块
- 一个 float 加法器,也要 2-3 个 DSP 块
- 而且延迟通常在 5-10 个时钟周期
你想想看,一个卷积层里有多少乘加操作?如果全用 float,资源瞬间爆炸。
我的经验:有一次做图像分类加速器,用 float 做权重存储,结果 BRAM 不够用。后来换成 ap_fixed<16,6>,资源直接降了 60%,精度只损失了 0.3%。
还有一个问题:float 的位宽是固定的 32 位。但很多时候,我们根本不需要这么高的精度。比如卷积的输入是 8 位像素值,权重是 8 位整数,你用 float 去算,纯属浪费。
3.2 ap_int / ap_fixed:HLS 的杀手锏
那不用 float 用什么?答案就是任意精度类型。
HLS 提供了两个核心模板类:
- ap_int<W>:有符号整数,W 是位宽
- ap_uint<W>:无符号整数
- ap_fixed<W,I>:有符号定点数,W 是总位宽,I 是整数部分位宽
- ap_ufixed<W,I>:无符号定点数
说白了,你可以自定义数据的位宽。想要 7 位?可以。想要 13 位?也可以。FPGA 会综合出刚好匹配的硬件电路,不浪费一个比特。
举个例子:一个卷积层的权重范围在 [-1.0, 1.0] 之间,精度要求 0.001。
用 float:32 位,浪费。
用 ap_fixed<16,2>:总位宽 16 位,整数部分 2 位(够表示 -2 到 1.99),小数部分 14 位(精度 2^-14 ≈ 0.00006),完全够用。
3.3 定点数 vs 浮点数:性能对比
我整理了一个表格,大家直观感受一下:
| 指标 | float | ap_fixed<16,6> | ap_int<8> |
|---|---|---|---|
| DSP 消耗(乘法) | 3-5 个 | 1 个 | 0(LUT 实现) |
| 延迟(时钟周期) | 6-10 | 2-3 | 1 |
| 资源占用 | 高 | 中 | 低 |
| 精度 | 高(23 位小数) | 可配置 | 整数 |
看到了吧?定点数在资源和速度上,优势非常明显。
3.4 实战:卷积层的数据类型选择
假设我们要设计一个 3x3 的卷积加速器,输入是 8 位灰度图像,权重是 8 位整数。
// 不推荐:用 float 实现
void conv_float(float img[64][64], float kernel[3][3], float out[64][64]) {
for (int i = 1; i < 63; i++) {
for (int j = 1; j < 63; j++) {
float sum = 0.0f;
for (int ki = -1; ki <= 1; ki++) {
for (int kj = -1; kj <= 1; kj++) {
sum += img[i+ki][j+kj] * kernel[ki+1][kj+1];
}
}
out[i][j] = sum;
}
}
}
// 推荐:用 ap_fixed 实现
#include <ap_fixed.h>
typedef ap_fixed<16,8> fixed16;
void conv_fixed(fixed16 img[64][64], fixed16 kernel[3][3], fixed16 out[64][64]) {
for (int i = 1; i < 63; i++) {
#pragma HLS PIPELINE II=1
for (int j = 1; j < 63; j++) {
fixed16 sum = 0;
for (int ki = -1; ki <= 1; ki++) {
for (int kj = -1; kj <= 1; kj++) {
sum += img[i+ki][j+kj] * kernel[ki+1][kj+1];
}
}
out[i][j] = sum;
}
}
}
注意:使用 ap_fixed 时,要注意溢出问题。我曾经在项目中因为整数部分位宽设小了,导致累加结果溢出,调试了整整两天。建议先估算数据的动态范围,再设置 I 的值。
3.5 什么时候该用 float?
当然,float 也不是一无是处。我总结了几种场景:
- 训练阶段:需要高精度梯度更新,用 float 没问题
- 推理阶段:如果精度要求极高(比如科学计算),可以考虑 float
- 快速原型验证:先用 float 跑通功能,再转定点数优化
但记住一点:在 FPGA 上做推理加速,定点数是主流。
3.6 知识体系图
下面这张图,帮你理清今天的内容:
3.7 避坑指南
最后,分享几个我踩过的坑:
- 位宽对齐:ap_fixed 做乘法时,结果位宽会自动扩展。比如 ap_fixed<8,4> 乘 ap_fixed<8,4>,结果是 ap_fixed<16,8>。记得手动截断,否则资源会翻倍。
- 量化策略:不要直接拍脑袋设位宽。先用 Python 跑一遍数据分布,确定整数部分和小数部分的位宽。
- 仿真 vs 综合:ap_fixed 在 C 仿真里很快,但综合时如果位宽太大(比如超过 64 位),综合时间会暴增。我建议控制在 32 位以内。
一个小技巧:如果你不确定用多少位宽,可以先从 ap_fixed<16,8> 开始,然后逐步减小位宽,直到精度不满足要求为止。这样能找到资源与精度的最佳平衡点。
好了,关于数据类型就讲到这里。记住一句话:在 FPGA 上,每比特都是钱。用 ap_fixed,就是帮你省钱。