第三章:FINN项目结构解析

好,咱们直接进入正题。这一章我带你拆解FINN的项目结构。说白了,就是搞清楚这个框架的「骨架」长什么样。我在刚开始接触FINN时,第一反应就是——这目录怎么这么多?别慌,我帮你理清楚。

3.1 源码目录结构

FINN的源码目录,我习惯把它分成三大块:核心库、编译工具、以及示例。你从GitHub拉下来后,会看到这样的结构:

finn/
├── finn-core/          # 核心库,别乱动
├── finn-experimental/  # 实验性功能,慎用
├── finn-hlslib/        # 高层次综合库
├── finn-rtllib/        # 运行时库
├── finn-quick-tests/   # 快速测试
├── docs/               # 文档
├── examples/           # 示例项目
└── setup.py            # 安装脚本

嗯,这里要注意:finn-core 是心脏,你自定义算子主要跟它打交道。我见过不少新手直接改核心库的代码,结果一更新就全乱了。我的建议是——核心库只读,别动。

3.1.1 finn-core 内部结构

咱们重点看 finn-core 里面长什么样:

finn-core/
├── src/
│   ├── finn/
│   │   ├── core/           # 核心数据流图
│   │   ├── transformation/ # 图变换
│   │   ├── custom_op/      # 自定义算子目录
│   │   ├── builder/        # 构建器
│   │   └── util/           # 工具函数
│   ├── tests/              # 单元测试
│   └── examples/           # 使用示例
├── include/
│   └── finn/
│       └── custom_op/      # 自定义算子头文件
└── CMakeLists.txt          # 构建配置

我个人习惯把自定义算子放在 custom_op/ 目录下。为什么?因为FINN的构建系统会自动扫描这个目录。你想想看,如果随便放个地方,构建时找不到,那多尴尬。

3.2 关键配置文件说明

FINN的配置,说白了就是几个YAML和JSON文件。我刚开始时被这些文件搞得头大,后来发现其实套路很固定。

3.2.1 核心配置文件

文件名 位置 作用
finn_config.yaml 项目根目录 全局配置,比如目标平台、精度设置
custom_op_config.json custom_op/ 自定义算子的注册信息
build_config.cmake build/ 目录 构建参数,比如是否启用调试

举个例子,custom_op_config.json 长这样:

{
  "custom_ops": [
    {
      "name": "my_conv2d",
      "type": "hls",
      "source": "src/my_conv2d.cpp",
      "header": "include/my_conv2d.h",
      "clock_period": 10,
      "target": "xczu9eg-ffvb1156-2-i"
    }
  ]
}

这里有个坑:clock_period 的单位是纳秒。我曾经设成10,结果综合出来时序不满足,折腾了两天才发现是单位搞错了。嗯,细节决定成败。

3.3 构建系统介绍

FINN用的是CMake + Make的构建系统。为什么不用别的?说白了,FPGA工具链(Vivado、Vitis)都支持CMake,兼容性最好。

3.3.1 构建流程

构建流程其实就三步:

  1. 配置cmake -B build -DCMAKE_BUILD_TYPE=Release
  2. 编译cmake --build build --target finn_core
  3. 安装cmake --install build --prefix /usr/local

我建议你第一次构建时加上 -DFINN_DEBUG=ON,这样能看到详细的编译日志。遇到问题也好排查。

3.3.2 自定义算子的构建集成

当你添加了自定义算子后,需要在 CMakeLists.txt 里注册一下。具体做法是:

# 在 finn-core/CMakeLists.txt 中添加
add_custom_op(my_conv2d
  SOURCES src/custom_op/my_conv2d.cpp
  HEADERS include/custom_op/my_conv2d.h
  DEPENDS hls_lib
)

为什么要加 DEPENDS hls_lib?因为你的自定义算子很可能依赖HLS库。我遇到过有人忘了加这个依赖,结果编译时一堆未定义引用错误。

3.4 自定义算子存放位置

好,这是重点中的重点。自定义算子到底放哪?

3.4.1 推荐目录结构

我个人习惯这样组织:

finn-core/
├── src/
│   └── custom_op/
│       ├── my_conv2d.cpp      # 实现文件
│       ├── my_pool.cpp        # 另一个算子
│       └── CMakeLists.txt     # 算子级构建
├── include/
│   └── finn/
│       └── custom_op/
│           ├── my_conv2d.h    # 头文件
│           └── my_pool.h
└── tests/
    └── custom_op/
        ├── test_my_conv2d.cpp # 单元测试
        └── test_my_pool.cpp

你想想看,这样分开放的好处是什么?解耦。每个算子独立编译,互不影响。我曾经见过有人把所有算子写在一个文件里,结果改一个就要重新编译全部,那叫一个慢。

3.4.2 避坑指南

⚠️ 注意:

  • 不要把你的算子放在 finn-core/src/finn/core/ 下,那是核心逻辑,不是给你放算子的
  • 头文件路径要跟 #include 一致,比如 #include "finn/custom_op/my_conv2d.h"
  • 记得在 custom_op_config.json 里注册,否则FINN找不到你的算子

💡 我的小技巧:

每次添加新算子后,先跑一下 finn-quick-tests 里的测试。如果测试通过,说明你的算子被正确集成了。如果失败,检查一下配置文件和路径。

3.5 实战:添加一个简单的自定义算子

光说不练假把式。咱们来走一遍流程。

3.5.1 步骤一:创建文件

# 在 finn-core/src/custom_op/ 下创建
touch my_add.cpp
# 在 finn-core/include/finn/custom_op/ 下创建
touch my_add.h

3.5.2 步骤二:编写代码

my_add.h

#ifndef MY_ADD_H
#define MY_ADD_H

#include "ap_int.h"
#include "hls_stream.h"

void my_add(
    hls::stream<ap_uint<16>> &in1,
    hls::stream<ap_uint<16>> &in2,
    hls::stream<ap_uint<16>> &out,
    int num_elements
);

#endif

my_add.cpp

#include "my_add.h"

void my_add(
    hls::stream<ap_uint<16>> &in1,
    hls::stream<ap_uint<16>> &in2,
    hls::stream<ap_uint<16>> &out,
    int num_elements
) {
    #pragma HLS INTERFACE axis port=in1
    #pragma HLS INTERFACE axis port=in2
    #pragma HLS INTERFACE axis port=out
    #pragma HLS INTERFACE s_axilite port=num_elements

    for (int i = 0; i < num_elements; i++) {
        #pragma HLS PIPELINE II=1
        ap_uint<16> a = in1.read();
        ap_uint<16> b = in2.read();
        out.write(a + b);
    }
}

3.5.3 步骤三:注册算子

custom_op_config.json 里添加:

{
  "name": "my_add",
  "type": "hls",
  "source": "src/custom_op/my_add.cpp",
  "header": "include/finn/custom_op/my_add.h",
  "clock_period": 10,
  "target": "xczu9eg-ffvb1156-2-i"
}

3.5.4 步骤四:构建测试

cd finn
cmake -B build -DFINN_DEBUG=ON
cmake --build build --target finn_core
# 如果构建成功,说明你的算子被正确集成了

嗯,到这里,你的第一个自定义算子就集成进去了。是不是比想象中简单?

📌 本章要点回顾:

  • FINN源码目录:核心库在 finn-core,自定义算子放 custom_op/
  • 关键配置:custom_op_config.json 是算子的「身份证」
  • 构建系统:CMake + Make,记得加 DEPENDS hls_lib
  • 存放位置:源文件在 src/custom_op/,头文件在 include/finn/custom_op/

下一章,咱们会深入自定义算子的接口规范。说白了,就是告诉FINN你的算子长什么样、怎么用。到时候我会分享一些我在实际项目中踩过的坑,保证让你少走弯路。