第三章: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 知识体系图

下面这张图,帮你理清今天的内容:

HLS 数据类型选择决策树 数据类型选择 定点数 (ap_fixed) 浮点数 (float) ✅ 资源少、延迟低、精度可配 ✅ 适合卷积、矩阵乘等运算 ⚠️ 资源多、延迟高、位宽固定 ⚠️ 仅用于训练或高精度场景 结论:推理加速首选 ap_fixed,精度不够再考虑 float

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,就是帮你省钱。

可可精品资料库