hls::stream在HLS DATAFLOW中的真正作用

在这里插入图片描述

澄清误解

这是许多HLS初学者的常见误解。hls::stream不是专门为PS-PL(处理系统和可编程逻辑)通信设计的,而是Vitis HLS提供的一种通用数据流抽象。在IP核内部使用hls::stream不仅是正确的做法,而且是实现高效DATAFLOW优化的关键技术。

hls::stream的真正用途

hls::stream是一种高级抽象,用于模拟硬件中的FIFO(先进先出)缓冲区。其主要用途包括:

  1. IP核内部的模块间通信:连接DATAFLOW区域中的不同计算单元
  2. 建立硬件流水线:支持任务级并行处理
  3. 提供数据流抽象:在C/C++代码中模拟硬件流的行为

在硬件实现中,hls::stream通常被综合为FIFO缓冲区,这是一种高效的硬件结构,非常适合流水线设计。

与AXI-Stream的关系(可能导致混淆的原因)

您的误解可能来源于hls::stream可以通过特定的接口指令映射到AXI-Stream接口:

void my_function(hls::stream<data_t> &input, hls::stream<data_t> &output) {
    #pragma HLS INTERFACE axis port=input
    #pragma HLS INTERFACE axis port=output
    // ...
}

但这只是hls::stream的一种用法,而非其主要用途。大多数情况下,hls::stream用于IP核内部的模块间通信,而不会映射到外部接口。

为什么在IP核内部使用hls::stream是正确的

1. 解耦计算阶段

在DATAFLOW优化中,我们将算法分解为多个可以并行执行的任务。这些任务需要一种高效的通信机制,hls::stream正好满足这一需求:

void image_filter(pixel_t input[1024], pixel_t output[1024]) {
    hls::stream<pixel_t> stream1, stream2;
    
    #pragma HLS DATAFLOW
    read_input(input, stream1);       // 生产者
    apply_filter(stream1, stream2);   // 中间处理
    write_output(stream2, output);    // 消费者
}

在这个例子中,三个函数通过流连接,形成一个流水线。每个函数可以在不等待其他函数完全完成的情况下处理数据,大大提高了吞吐量。

2. 减少内存使用和访问

与使用数组相比,hls::stream可以显著减少内存使用和访问:

// 使用数组 - 需要完整的中间缓冲区
void process_with_array(data_t input[1024], data_t output[1024]) {
    data_t buffer[1024];  // 需要存储1024个元素
    
    for(int i = 0; i < 1024; i++)
        buffer[i] = input[i] * 2;  // 第一阶段
        
    for(int i = 0; i < 1024; i++)
        output[i] = buffer[i] + 10;  // 第二阶段
}

// 使用stream - 只需要小型FIFO缓冲区
void process_with_stream(data_t input[1024], data_t output[1024]) {
    hls::stream<data_t> stream;
    #pragma HLS STREAM variable=stream depth=8  // 只需要8个元素的FIFO
    
    #pragma HLS DATAFLOW
    // 这两个循环可以并行执行
    for(int i = 0; i < 1024; i++)
        stream.write(input[i] * 2);  // 第一阶段
        
    for(int i = 0; i < 1024; i++)
        output[i] = stream.read() + 10;  // 第二阶段
}

在数组版本中,我们需要一个完整的1024元素缓冲区。而在流版本中,只需要一个小型FIFO(例如8个元素),就可以实现相同的功能,同时支持并行执行。

3. 提高吞吐量

使用hls::stream可以实现真正的流水线并行处理,显著提高吞吐量:

// 无流水线 - 顺序执行
void sequential_process(data_t input[1024], data_t output[1024]) {
    // 必须完全完成第一步才能开始第二步
    data_t temp[1024];
    
    for(int i = 0; i < 1024; i++)
        temp[i] = stage1(input[i]);
        
    for(int i = 0; i < 1024; i++)
        output[i] = stage2(temp[i]);
}

// 使用stream创建流水线
void pipelined_process(data_t input[1024], data_t output[1024]) {
    hls::stream<data_t> stream;
    
    #pragma HLS DATAFLOW
    // 这两个函数可以并行执行
    produce_data(input, stream);    // 处理一部分数据后就开始传输
    consume_data(stream, output);   // 一旦有数据可用就开始处理
}

在流水线版本中,produce_data函数处理完一部分数据后,就可以将结果传递给consume_data函数,而不必等待所有数据处理完成。这样,两个函数可以同时工作,大大提高了吞吐量。

4. 隐式同步和控制

hls::stream提供了隐式同步机制,简化了设计:

  • 读操作会阻塞:如果流为空,读操作会等待直到有数据可用
  • 写操作会阻塞:如果流已满,写操作会等待直到有空间可用
  • 无需显式同步:不需要额外的同步机制,减少了代码复杂性

这种行为与硬件FIFO非常相似,使得代码更加直观和易于理解。

与其他方法的比较

为什么使用hls::stream而不是其他方法?让我们比较一下:

方法优点缺点
共享数组简单直观需要显式同步;难以实现并行;资源使用效率低
乒乓缓冲区支持一定程度的并行需要手动管理切换;实现复杂;资源使用较多
hls::stream隐式同步;高效并行;资源使用少;硬件友好需要理解流的概念;可能需要调整缓冲区大小

实际例子:简单图像处理流水线

下面是一个简化的图像处理流水线示例,展示了hls::stream在IP核内部的实际应用:

#include "hls_stream.h"

typedef unsigned char pixel_t;
const int SIZE = 1024;

// 顶层函数
void image_filter(pixel_t input[SIZE], pixel_t output[SIZE]) {
    hls::stream<pixel_t> stream1;
    hls::stream<pixel_t> stream2;
    
    // 设置适当的缓冲区大小
    #pragma HLS STREAM variable=stream1 depth=16
    #pragma HLS STREAM variable=stream2 depth=16
    
    #pragma HLS DATAFLOW
    
    // 阶段1: 读取输入数据并乘以2
    for (int i = 0; i < SIZE; i++) {
        #pragma HLS PIPELINE II=1
        stream1.write(input[i] * 2);
    }
    
    // 阶段2: 应用滤波器(简单的3点平均)
    pixel_t buffer[3] = {0, 0, 0};
    for (int i = 0; i < SIZE; i++) {
        #pragma HLS PIPELINE II=1
        // 更新缓冲区
        buffer[0] = buffer[1];
        buffer[1] = buffer[2];
        buffer[2] = stream1.read();
        
        // 计算平均值
        pixel_t result = (buffer[0] + buffer[1] + buffer[2]) / 3;
        
        // 写入输出流
        stream2.write(result);
    }
    
    // 阶段3: 写回结果
    for (int i = 0; i < SIZE; i++) {
        #pragma HLS PIPELINE II=1
        output[i] = stream2.read();
    }
}

在这个例子中:

  1. 三个处理阶段通过hls::stream连接,形成流水线
  2. 每个阶段可以并行工作,提高吞吐量
  3. 使用小型FIFO缓冲区而不是大型数组存储中间结果
  4. 流的深度可以根据需要调整,以平衡资源使用和性能

总结

hls::stream是Vitis HLS中一个强大的抽象,主要用于:

  1. IP核内部的模块间通信,特别是在DATAFLOW优化中
  2. 创建硬件流水线,提高吞吐量
  3. 减少内存使用,通过FIFO替代大型缓冲区
  4. 解耦计算阶段,提高模块化和可维护性

它不是专门为PS-PL通信设计的,尽管在某些情况下可以映射到AXI-Stream接口用于外部通信。

在DATAFLOW优化中使用hls::stream是完全正确且推荐的做法,它是实现高性能HLS设计的关键工具之一。通过在IP核内部使用hls::stream,可以显著提高设计的吞吐量和资源利用率,这也是为什么它在HLS最佳实践中被广泛推荐。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐