FPGA部署神经网络
·
将各权重数据保存到.c文件后,需要将他们分别保存到dat文件,如何保存需要看在HLS中数组的是如何定义的:
LeNet.cpp:
#include "ap_fixed.h"
#include <hls_stream.h>
#include <cmath> // 用于expf的头文件
#define MAX(A, B) ((A < B) ? B : A)
// 卷积操作:输出逐元素置零(解决half未初始化NaN累加)
void conv2(half* input, const half* weight, half* output, int in_row, int out_row, int core) {
Row:
for (int r = 0; r < out_row; r++) {
Column:
for (int c = 0; c < out_row; c++) {
output[r * out_row + c] = 0.0f; // 卷积累加前先置零
Kernel_Row:
for (int kr = 0; kr < core; kr++) {
Kernel_Column:
for (int kc = 0; kc < core; kc++) {
output[r * out_row + c] += input[(r + kr) * in_row + (c + kc)] * weight[kr * core + kc];
}
}
}
}
}
// 池化操作(2x2最大池化)
void pool(half* input, half* output, int in_row, int out_row) {
for (int i = 0; i < out_row; i++) {
for (int j = 0; j < out_row; j++) {
output[i * out_row + j] = input[i * 2 * in_row + j * 2];
for (int k = 0; k < 2; k++) {
for (int m = 0; m < 2; m++) {
output[i * out_row + j] = MAX(output[i * out_row + j], input[(i * 2 + k) * in_row + (j * 2 + m)]);
}
}
}
}
}
// 目标指定的Sigmoid函数
void Sigmoid1(half* a, int size) {
for (int i = 0; i < size; i++) {
a[i] = 1 / (1 + expf(-a[i]));
}
}
// 核心修复:HLS三维half数组解析bug的兼容函数
inline const half* get_3d_weight(const half weight[16][6][25], int i, int j) {
return &weight[i][j][0]; // 返回(i,j,0)地址,等价于weight[i][j][k]
}
void LeNet(int input[784], float output[10]) {
#pragma HLS INTERFACE bram port=output
#pragma HLS INTERFACE bram port=input
#pragma HLS INTERFACE s_axilite port=return bundle=CRTL_BUS
// 输入初始化:显式置零 + 归一化(0~255→0~1)
half input1[1][784] = {0.0};
for (int i = 0; i < 784; i++) {
input1[0][i] = static_cast<half>(input[i] / 255.0f);
}
// 权重和偏置初始化(完整还原原始LeNet的所有层)
const half c1_weights[6][25] = {
#include "./weight/c1_weights.dat"
};
const half c1_biases[6] = {
#include "./weight/c1_biases.dat"
};
const half c3_weights[16][6][25] = {
#include "./weight/c3_weights.dat"
};
const half c3_biases[16] = {
#include "./weight/c3_biases.dat"
};
const half f5_weights[120][400] = {
#include "./weight/f5_weights.dat"
};
const half f5_biases[120] = {
#include "./weight/f5_biases.dat"
};
const half f6_weights[84][120] = {
#include "./weight/f6_weights.dat"
};
const half f6_biases[84] = {
#include "./weight/f6_biases.dat"
};
const half f7_weights[10][84] = {
#include "./weight/f7_weights.dat"
};
const half f7_biases[10] = {
#include "./weight/f7_biases.dat"
};
// 中间层存储:全部显式置零(无垃圾值)
half c1_value[6][784] = {0.0};
half c3_value[16][100] = {0.0};
half A2_value[6][196] = {0.0};
half A4_value[16][25] = {0.0};
half A4_value_flat[400] = {0.0}; // 扁平化后的数据(供F5层输入)
half f5_value[120] = {0.0};
half f6_value[84] = {0.0};
half f7_value[10] = {0.0};
// -------------------------- 卷积池化第一层(C1 + A2)--------------------------
// C1:6核×1通道,5×5卷积,28x28→28x28
for (int i = 0; i < 6; i++) {
half output1[784] = {0};
half output2[784] = {0};
conv2(input1[0], c1_weights[i], output1, 28, 28, 5);
for (int n = 0; n < 784; n++) {
output2[n] += output1[n];
}
for (int k = 0; k < 784; k++) {
c1_value[i][k] = output2[k] + c1_biases[i];
}
Sigmoid1(c1_value[i], 784); // 使用指定的Sigmoid函数
}
// A2:2x2最大池化,28x28→14x14
for (int i = 0; i < 6; i++) {
pool(c1_value[i], A2_value[i], 28, 14);
}
// -------------------------- 卷积池化第二层(C3 + A4)--------------------------
// C3:16核×6通道,5×5卷积,14x14→10x10
for (int i = 0; i < 16; i++) { // i:输出核索引
half output1[100] = {0};
half output2[100] = {0};
for (int j = 0; j < 6; j++) { // j:输入通道索引
const half* weight_ptr = get_3d_weight(c3_weights, i, j);
conv2(A2_value[j], weight_ptr, output1, 14, 10, 5);
for (int n = 0; n < 100; n++) {
output2[n] += output1[n];
}
}
for (int k = 0; k < 100; k++) {
c3_value[i][k] = output2[k] + c3_biases[i];
}
Sigmoid1(c3_value[i], 100); // 使用指定的Sigmoid函数
}
// A4:2x2最大池化,10x10→5x5
for (int i = 0; i < 16; i++) {
pool(c3_value[i], A4_value[i], 10, 5);
}
// -------------------------- 扁平化(供全连接层输入)--------------------------
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 25; j++) {
A4_value_flat[i*25 + j] = A4_value[i][j];
}
}
// -------------------------- 全连接层(F5 + F6 + F7)--------------------------
// F5:400→120(输入为扁平化后的400维特征)
for (int block = 0; block < 24; block++) {
for (int i = 0; i < 5; i++) {
int idx = block*5 + i;
f5_value[idx] = 0.0f;
for (int j = 0; j < 400; j++) {
f5_value[idx] += A4_value_flat[j] * f5_weights[idx][j];
}
f5_value[idx] += f5_biases[idx];
}
}
//Sigmoid1(f5_value, 120);
// F6:120→84
for (int block = 0; block < 28; block++) {
for (int i = 0; i < 3; i++) {
int idx = block*3 + i;
if (idx >= 84) break;
f6_value[idx] = 0.0f;
for (int j = 0; j < 120; j++) {
f6_value[idx] += f5_value[j] * f6_weights[idx][j];
}
f6_value[idx] += f6_biases[idx];
}
}
//Sigmoid1(f6_value, 84);
// F7:84→10(输出层)
for (int i = 0; i < 10; i++) {
f7_value[i] = 0.0f;
for (int j = 0; j < 84; j++) {
f7_value[i] += f6_value[j] * f7_weights[i][j];
}
f7_value[i] += f7_biases[i];
}
//Sigmoid1(f7_value, 10);
// 最终结果输出(half→float)
for (int i = 0; i < 10; i++) {
output[i] = float(f7_value[i]);
}
}
// // 初始化输出数组为0
// for (int i = 0; i < 10; i++) {
// output[i] = 0.0f;
// }
//
// // 提取前10个非零像素值
// int count = 0;
// for (int i = 0; i < 784; i++) {
// #pragma HLS PIPELINE II=1 // 流水线加速
// if (count >= 10) {
// break;
// }
// if (input[i] > 0) {
// output[count] = (float)input[i];
// count++;
// }
// }
比如我这里定义的c1_weights[6][25],相对应的文件里需要为这种格式才能被读取:
{ -1.150429, -1.136602, -0.016675, 0.778316, 0.408938, -1.098197, -0.848836, 0.007109, 1.107274, 0.658696, -1.100441, -1.158110, 0.202005, 1.291711, 0.846296, -0.740439, -1.309103, 0.047273, 1.037110, 0.681634, -0.618136, -1.272994, 0.123673, 1.334365, 0.681756, },
{ -0.098179, 0.929264, 0.619515, -0.148757, -0.729668, -0.145379, 1.118042, 0.534308, -0.146224, -0.286283, -0.740030, 0.705875, 0.506720, -0.014934, -0.544844, -0.614593, 0.809605, 0.610239, 0.120964, -0.420370, -1.059446, 0.046993, 0.248390, -0.015336, -0.452437, },
{ 0.234437, -0.622473, -0.720234, -0.460606, -0.392162, 0.410074, 1.436124, 4.386673, 4.153095, 0.281895, 0.653163, 1.452435, 3.843968, 3.670660, 0.585019, 0.019956, -0.078015, -0.518404, -0.072283, -0.036168, -0.732911, -1.211045, -0.972566, -0.678190, -1.203901, },
{ -0.132423, -1.175240, -2.461365, -2.389227, -0.881222, 0.706983, 0.857820, 0.448928, 0.374199, 0.833204, 1.003685, 4.060161, 9.481065, 8.224229, 1.921525, 0.575111, 1.179773, 4.281919, 4.589689, 0.737764, -0.473235, -0.924250, -1.462910, -0.606344, -0.212303, },
{ -0.294416, 1.107387, 0.759076, -0.808032, -0.653084, -0.147049, 1.338617, 1.155207, -0.995056, -0.690049, -0.535730, 1.790776, 1.717897, -0.766730, -0.910606, -0.939112, 1.504940, 1.371450, -0.335985, -0.583598, -1.270332, 0.624882, 1.028866, -0.321590, -0.154223, },
{ -1.505298, -0.242565, -0.057152, 0.100606, -0.591544, -1.435453, -0.497988, 0.105364, 0.250945, -0.592670, -0.947675, 0.402026, 0.877032, 1.244673, 0.366646, -1.172020, 0.056148, 1.329806, 1.734838, 0.664154, -1.179376, -0.022934, 0.852079, 1.113093, 0.136609, },
其他类似,一维数组没有加{}。
LeNet.h:
#include "stdio.h"
#include "math.h"
#include "hls_half.h"
#include "ap_int.h"
// LeNet 函数声明
void LeNet(int input[784], float output[10]);
void conv2(half* input, const half* weight, half* output, int in_row, int out_row, int core);
void SafeSigmoid(half* a, int size);
void pool(half *input,half *output,int in_row,int out_row);
以上两个其实就是把网络框架从python语言转换为c语言,相当于把模型重新写了一遍,我认为是这样的。
之后可以进行测试:
LeNet_tb.cpp
#include <iostream>
#include <cmath>
#include <cassert>
#include "LeNet.h"
int main() {
// 用于存储网络的输出结果
float output_data[10] = {0};
// 简单的测试输入数据(例如,28x28 图像的数据,直接输入为 784 个值)
// 假设 first_image.dat 文件中的数据已经是 half 类型,转换为 float 后读取
int input_data[784] = {
#include "./weight/first_image.dat" // 从外部文件加载输入数据
};
// 调用 LeNet 函数
LeNet(input_data, output_data);
// 打印输出结果
std::cout << "LeNet Output:" << std::endl;
for (int i = 0; i < 10; i++) {
std::cout << "Output[" << i << "]: " << output_data[i] << std::endl;
}
return 0;
}
把所有文件保存后,进行c仿真,顺利的话会打印出最后10个神经元的输出:
INFO: [SIM 2] *************** CSIM start ***************
INFO: [SIM 4] CSIM will launch GCC as the compiler.
Compiling ../../../src/LeNet_tb.cpp in release mode
Compiling ../../../src/LeNet.cpp in release mode
Generating csim.exe
LeNet Output:
Output[0]: 7.21875
Output[1]: 33.8438
Output[2]: -4.9375
Output[3]: 12.5078
Output[4]: 11.9141
Output[5]: -9.3125
Output[6]: 10.3594
Output[7]: -24.8594
Output[8]: -13.0391
Output[9]: -29.9844
INFO: [SIM 1] CSim done with 0 errors.
INFO: [SIM 3] *************** CSIM finish ***************
而后进行C synthesis,会生成一个报告,可以看一下有没有超出板子的极限。
然后 export RTL,即导出ip核,可以到vivado中进行bd设计。
关于我在HLS过程中遇到的问题或几点说明吧:
1.IP核端口的设置:
#pragma HLS INTERFACE bram port=output
#pragma HLS INTERFACE bram port=input
#pragma HLS INTERFACE s_axilite port=return bundle=CRTL_BUS
这个我还没有彻底搞明白,我现在只知道前两句是IP核能引出一个输入端口一个输出端口去读写bram,最后一句是一个控制端口,其他的我不懂了,如果有大佬明白可以帮我解惑一下,感谢!
2.输入输出需要32位:
void LeNet(int input[784], float output[10])
这个应该是vivado中bram默认的32位,如果改成half会报错,bd设计不通过。
3.权重固化:
const half c1_weights[6][25] = {
#include "./weight/c1_weights.dat"
};
const half c1_biases[6] = {
#include "./weight/c1_biases.dat"
};
const half c3_weights[16][6][25] = {
#include "./weight/c3_weights.dat"
};
const half c3_biases[16] = {
#include "./weight/c3_biases.dat"
};
const half f5_weights[120][400] = {
#include "./weight/f5_weights.dat"
};
const half f5_biases[120] = {
#include "./weight/f5_biases.dat"
};
const half f6_weights[84][120] = {
#include "./weight/f6_weights.dat"
};
const half f6_biases[84] = {
#include "./weight/f6_biases.dat"
};
const half f7_weights[10][84] = {
#include "./weight/f7_weights.dat"
};
const half f7_biases[10] = {
#include "./weight/f7_biases.dat"
};
const的意思(我认为)就是把后面的数据固化到FPGA上,会自动为这些数据分配ROM,FPGA需要的时候会自动去读取这些数据进行计算;选用half而不用float,half是16位,固化时用到少量的bram(60%左右),选用float会105%/120%,bd设计不通过(我这款板子)。
整体下来我认为难的地方在于数据存储要与数组匹配,刚开始接触FPGA端口设置也没大明白。
更多推荐

所有评论(0)