完善PS端YOLO网络前向计算函数

发布时间 2023-07-30 00:46:31作者: 李白的白

完善PS端YOLO网络前向计算函数

  • 解决隐藏的bug

    • 在yolo_accel_ctrl.c文件中,修改读DMA时的命令,将原来的0x2改为与上一层卷积计算命令相或的结果,即cmd |= 0x2

    • 这样可以保持is_padding和is_pool等比特不变,避免影响PL端的池化模块中的FIFO IP的数据存储

    • FIFO IP中存储了上一层卷积结果中多余的一行数据,如果被清空,会影响最终结果的正确性

    修改代码中,在读DMA时给出正确的命令,即将0x2与之前的命令进行或运算,保留其他位不变,只改变第四位为2,表示read_start信号拉高

    // 原来的代码,在读DMA时只给了一个0x2的命令
    Xil_Out32(YOLO_ACCEL_CTRL_BASEADDR + YOLO_ACCEL_CTRL_CMD_OFFSET, 0x2);
    
    // 修改后的代码,在读DMA时给出正确的命令,即将0x2与之前的命令进行或运算
    Xil_Out32(YOLO_ACCEL_CTRL_BASEADDR + YOLO_ACCEL_CTRL_CMD_OFFSET, cmd | READ_START);
    

目标

  • 在PS端编写C语言代码,实现YOLO网络的前向计算功能
  • 利用PL端(可编程逻辑端)的硬件加速器,提高YOLO网络的计算速度和效率
  • 验证PS端和PL端之间的数据传输和控制信号是否正确

前提

  • 已经在PL端实现了YOLO网络的卷积层和池化层的硬件加速器模块
  • 已经在PS端定义了YOLO网络的各层参数和配置信息,以及相关的数据结构和函数
  • 已经在PS端实现了与PL端之间的DMA(直接内存访问)通信接口

流程图如下:

sequenceDiagram participant PS participant PL Note over PS: 初始化配置信息、地址、权重等 PS->>PL: 发送layer 0输入数据(8个通道) Note over PL: 在硬件加速器中进行卷积、池化等操作 PL->>PS: 发送layer 0输出数据(16个通道) Note over PS: 更新配置信息、地址等 PS->>PL: 发送layer 2输入数据(16个通道,分两批) Note over PL: 在硬件加速器中进行卷积、池化等操作 PL->>PS: 发送layer 2输出数据(32个通道,分四批) Note over PS: 更新配置信息、地址等 PS->>PL: 发送layer 3输入数据(32个通道,分四批) Note over PL: 在硬件加速器中进行卷积、池化等操作 PL->>PS: 发送layer 3输出数据(64个通道,分八批) Note over PS: 继续处理后续层或者输出最终结果

步骤

完善layer 2和layer 3的控制代码

- layer 2是一个卷积层,输入通道为16,输出通道为32,尺寸为208,权重、偏置、量化等信息:从Python打印结果中获取
- layer 3是一个池化层,输入通道为32,输出通道为32
  • 在yolo_accel_ctrl.c文件中,根据layer 2和layer 3的参数和配置信息,修改相应的变量值,如输入输出通道数、尺寸、量化系数、地址等

  • 在发送数据、更新命令、接收数据等子函数中,根据layer 2和layer 3与layer 0和layer 1之间的区别,修改相应的逻辑判断和操作

  • 主要的区别是:

    • layer 2和layer 3的输入输出通道数都大于8,需要分批次发送和接收数据,使用ch_in_batch_cnt和ch_out_batch_cnt来表示当前批次
    • layer 2和layer 3在发送数据时,需要根据ch_in_batch_cnt来偏移发送地址,以便发送不同批次的输入通道数据
    • layer 2和layer 3在更新命令时,需要根据ch_in_batch_cnt和ch_out_batch_cnt来修改batch_type比特,以便PL端识别不同批次的数据
    • layer 2和layer 3在接收数据时,需要根据ch_out_batch_cnt来偏移接收地址,以便接收不同批次的输出通道数据
    • layer 2在卷积计算完成后,需要根据ch_in_batch_cnt来判断是否需要再次发送数据或者跳转到读DMA状态
  • 在PS端编写代码,完成以下几个步骤:

    • 初始化函数

      • 定义变量和常量

        如输入输出通道数、特征图尺寸、权重和偏置等

      • 将变量和常量存储在PS端内存中

    • 发送数据函数

      • 将输入图像数据从PS端发送到PL端,根据不同层的通道数,可能需要分批次发送,并且每次发送前要更新命令寄存器,指示PL端进行相应的操作。
      • 每次发送前更新命令寄存器 XPAR_YOLO_ACCEL_CTRL_S_AXI_BASEADDR + 0x10, 0x2 | cmd
    • 命令更新函数

      • 根据不同层的类型(卷积或池化),更新命令寄存器的值

        如是否需要填充、是否需要池化、是否是第一批或最后一批等

    • 接收数据函数

      • 从PL端接收输出特征图数据,并将其存储在PS端的内存中,根据不同层的通道数,可能需要分批次接收
      • 每次接收后要更新接收地址和长度
    • 计数器更新函数

      • 更新发送次数、接收次数、输入通道批次、输出通道批次等计数器的值

        用于控制数据发送和接收的流程

  • 完善layer 2和layer 3的处理

    • layer 2

      • 卷积层
        • 参数
          输入通道:16

          输出通道:32

          • 尺寸:208
          • 权重、偏置、量化等信息:从Python打印结果中获取
            • mult: 30363
            • shift: 8
            • zero_point_in: 12
            • zero_point_out: 86
        • 发送地址和接收地址
          • 发送地址:0x30A9000,需要根据输入通道的批次进行偏移
          • 接收地址:0x30A9000 + 208*208*8,需要根据输出通道的批次进行偏移
        • 发送数据和命令
          • 需要分两批发送输入数据,每批8个通道,使用ch_in_batch_cnt表示批次计数
          • 需要分四批接收输出数据,每批8个通道,使用ch_out_batch_cnt表示批次计数
          • 根据不同的批次,更新命令中的batch_type字段,使用batch_type_update()函数
          • 根据不同的批次,更新权重缓存的索引值,使用weight_buffer_index_update()函数
        • 接收数据和命令
          • 在发送完所有输入数据后,发送DMA读命令,使用cmd |= 0x2
          • 在接收完所有输出数据后,跳转到下一层的处理,使用layer++
    • layer 3

      • 池化层
        • 参数
          • 输入通道:32
          • 输出通道:32
          • 尺寸:104
          • 权重、偏置、量化等信息:从Python打印结果中获取
            • mult: 1
            • shift: 0
            • zero_point_in: 86
            • zero_point_out: 86

        考虑区别和逻辑

        考虑layer 2和layer 0之间的区别,主要是输入输出通道数不同,导致需要分批次发送和接收数据。修改PS端控制代码中,关于计数器、地址偏移、命令更新等方面的逻辑。

        • 计数器:

          使用三个计数器来表示不同的批次和次数: ch_in_batch_cnt表示输入通道的批次计数,从0到ch_in_batch_cnt_end-1 tx_cnt表示发送数据的次数,从0到tx_cnt_end-1 ch_out_batch_cnt表示输出通道的批次计数,从0到ch_out_batch_cnt_end-1 每个计数器在达到最大值时清零,并使下一个计数器加一。

        • 发送地址和接收地址

          在发送数据时,根据输入通道的批次计数,对发送地址进行偏移,以发送不同的通道数据。 偏移量为feature_size * feature_size * (ch_in_batch_cnt << 3)

          • 发送地址:0x30A9000 + 208*208*8,需要根据输入通道的批次进行偏移
          • 接收地址:0x30A9000 + 208*208*16 + 104*104*8,需要根据输出通道的批次进行偏移
        • 发送数据和命令

          在发送数据时,根据输入输出通道的批次计数,对命令进行更新,以设置不同的batch_type位。 batch_type位表示当前批次是第一批、中间批还是最后一批。

          • 需要分四批发送输入数据,每批8个通道,使用ch_in_batch_cnt表示批次计数
          • 需要分四批接收输出数据,每批8个通道,使用ch_out_batch_cnt表示批次计数
          • 根据不同的批次,更新命令中的batch_type字段,使用batch_type_update()函数
        • 接收数据和命令
          • 在发送完所有输入数据后,发送DMA读命令,使用cmd |= 0x2
          • 在接收完所有输出数据后,跳转到下一层的处理,使用layer++
// 实现PS端layer2及后续层的控制代码
- 根据不同层的参数和特点,修改相应的变量和数组,例如输入通道数、输出通道数、特征图尺寸、量化参数、地址偏移量等
- 根据不同层输入通道数和输出通道数与8的倍数关系,修改相应的批次类型(batch_type)和批次计数器(ch_in_batch_cnt和ch_out_batch_cnt),以及相应的条件判断语句,例如是否需要分批发送

// 以下是layer2的控制代码示例
// layer2的参数
int layer_id = 2;
int ch_in = 32;
int ch_out = 64;
int size_in = 208;
int size_out = 104;
int quant_param = 8;
int addr_offset = 0x100000;

// layer2的批次类型
// 输入通道数为32,输出通道数为64,都是8的倍数,所以批次类型为0
int batch_type = 0;

// layer2的批次计数器
// 输入通道数为32,输出通道数为64,都是8的倍数,所以批次计数器都为1
int ch_in_batch_cnt = 1;
int ch_out_batch_cnt = 1;

// layer2的发送数据函数
void send_data(int layer_id, int tx_cnt, int ch_in_batch_cnt, int ch_out_batch_cnt) {
    // 计算输入数据的地址和长度
    int input_addr = YOLO_ACCEL_DDR_BASEADDR + addr_offset + tx_cnt * size_in * size_in * ch_in * sizeof(float);
    int input_len = size_in * size_in * ch_in * sizeof(float);

    // 设置DMA传输参数
    Xil_Out32(XPAR_AXI_DMA_0_BASEADDR + XAXIDMA_TX_OFFSET + XAXIDMA_CR_OFFSET, XAXIDMA_CR_RESET_MASK);
    Xil_Out32(XPAR_AXI_DMA_0_BASEADDR + XAXIDMA_TX_OFFSET + XAXIDMA_CR_OFFSET, XAXIDMA_CR_RUNSTOP_MASK);
    Xil_Out32(XPAR_AXI_DMA_0_BASEADDR + XAXIDMA_TX_OFFSET + XAXIDMA_SRCADDR_OFFSET, input_addr);
    Xil_Out32(XPAR_AXI_DMA_0_BASEADDR + XAXIDMA_TX_OFFSET + XAXIDMA_BUFFLEN_OFFSET, input_len);

    // 等待DMA传输完成
    while ((Xil_In32(XPAR_AXI_DMA_0_BASEADDR + XAXIDMA_TX_OFFSET + XAXIDMA_SR_OFFSET) & XAXIDMA_IRQ_IOC_MASK) == 0);
}

// layer2的接收数据函数
void recv_data(int layer_id, int tx_cnt, int ch_in_batch_cnt, int ch_out_batch_cnt) {
    // 计算输出数据的地址和长度
    int output_addr = YOLO_ACCEL_DDR_BASEADDR + addr_offset + tx_cnt * size_out * size_out * ch_out * sizeof(float);
    int output_len = size_out * size_out * ch_out * sizeof(float);

    // 设置DMA传输参数
    Xil_Out32(XPAR_AXI_DMA_0_BASEADDR + XAXIDMA_RX_OFFSET + XAXIDMA_CR_OFFSET, XAXIDMA_CR_RESET_MASK);
    Xil_Out32(XPAR_AXI_DMA_0_BASEADDR + XAXIDMA_RX_OFFSET + XAXIDMA_CR_OFFSET, XAXIDMA_CR_RUNSTOP_MASK);
    Xil_Out32(XPAR_AXI_DMA_0_BASEADDR + XAXIDMA_RX_OFFSET + XAXIDMA_DESTADDR_OFFSET, output_addr);
    Xil_Out32(XPAR_AXI_DMA_0_BASEADDR + XAXIDMA_RX_OFFSET + XAXIDMA_BUFFLEN_OFFSET, output_len);

    // 等待DMA传输完成
    while ((Xil_In32(XPAR_AXI_DMA_0_BASEADDR + XAXIDMA_RX_OFFSET + XAXIDMA_SR_OFFSET) & XAXIDMA_IRQ_IOC_MASK) == 0);
}

// layer2的更新命令函数
void update_cmd(int layer_id, int tx_cnt, int ch_in_batch_cnt, int ch_out_batch_cnt) {
    // 计算命令中的各个位
    int is_first_tx = (tx_cnt == 0);
    int is_last_tx = (tx_cnt == 1);
    int is_first_ch_in_batch = (ch_in_batch_cnt == 0);
    int is_last_ch_in_batch = (ch_in_batch_cnt == 1);
    int is_first_ch_out_batch = (ch_out_batch_cnt == 0);
    int is_last_ch_out_batch = (ch_out_batch_cnt == 1);
    int is_padding = 0;
    int is_pool = 1;

    // 组合命令
    int cmd = (is_first_tx << 7) | (is_last_tx << 6) | (is_first_ch_in_batch << 5) | (is_last_ch_in_batch << 4) | (is_first_ch_out_batch << 3) | (is_last_ch_out_batch << 2) | (is_padding << 1) | (is_pool << 0);

    // 发送命令
    Xil_Out32(YOLO_ACCEL_CTRL_BASEADDR + YOLO_ACCEL_CTRL_CMD_OFFSET, cmd);
}

// layer2的更新计数器函数
void update_cnt(int layer_id, int *tx_cnt, int *ch_in_batch_cnt, int *ch_out_batch_cnt) {
    // 更新tx_cnt
    (*tx_cnt)++;
    
    // 如果tx_cnt达到最大值,重置为0
    if (*tx_cnt == 2) {
        *tx_cnt = 0;
    }
}