突发传输通过在全局存储器上读取或写入大量数据区块来改善内核 I/O 的吞吐量。突发越大,吞吐量越高,该指标计算方式如下: ((传输的字节数)* (内核频率)/(时间))。最大内核接口带宽为 512 位,如果内核以 300 MHz 频率进行编译,那么理论上每个 DDR 可以实现 = (80-95% 的 DDR 效率)*(512 * 300 MHz)/1 秒 = ~17-19 GB/s。如前文所述,Vitis HLS 可以执行自动突发最优化,以智能化方式从用户代码汇总循环/函数的存储器访问,并在单个突发请求内执行特定大小的读取/写入操作。但正如 突发传输的前置条件和限制 中所述,突发传输也有要求,这些要求有时可能负担过重或者难以满足。
在某些情况下,如果自动突发访问失败,那么有效的解决方案是重写代码或者使用手动突发。在此类情况下,如果您熟悉 AXI4
m_axi
协议,并且理解硬件传输事务建模,那么您可以使用 hls::burst_maxi
类来实现手动突发传输,如下所述。请参阅 GitHub 上的 Vitis-HLS-Introductory-Examples/Interface/Memory/manual_burst,以获取这些概念的示例。另一个解决方案可能是在 AXI4 接口中通过 CACHE 编译指示或指令来使用高速缓存存储器。
hls::burst_maxi 类
hls::burst_maxi
类可以提供一种机制,用于对 DDR 存储器执行读/写访问。这些方法将把类方法的使用行为转换为相应的 AXI4 协议,并在 AXI4 总线信号上发送和接收请求:AW、AR、WDATA、BVALID 和 RDATA。这些方法可以控制 HLS 调度器的突发行为。适配器可从调度器接收命令,并负责向 DDR 存储器发送数据。这些请求将遵循用户指定的 INTERFACE 编译指示选项,例如,max_read_burst_length
和 max_write_burst_length
。类方法应仅在内核代码中使用,不得在测试激励文件中使用(类构造函数除外,如下所述)。
- 构造函数:
-
burst_maxi(const burst_maxi &B) : Ptr(B.Ptr) {}
-
burst_maxi(T *p) : Ptr(p) {}
重要: HLS 设计和测试激励文件必须位于不同文件内,因为构造函数burst_maxi(T *p)
仅在 C 语言仿真模型内可用。
-
- 读取方法:
-
void read_request(size_t offset, size_t len);
此方法用于对
m_axi
适配器执行读取请求。如果m_axi
适配器内部的读取请求队列未满,那么该函数就会立即返回,否则它会等待至空间变为可用为止。-
offset
:指定存储器偏移,将从该指定偏移位置读取数据 -
len
:指定调度器突发长度。此突发长度将发送至适配器,随后,适配器即可将其转换为标准 AXI AMBA 协议
-
-
T read();
此方法用于将数据从
m_axi
适配器传输至调度器 FIFO。如果数据不可用,read()
将执行阻塞。read()
方法应调用len
次,具体次数在read_request()
中指定。
-
- 写入方法:
-
void write_request(size_t offset, size_t len);
此方法用于对
m_axi
适配器执行写入请求。如果m_axi
适配器内部的写入请求队列未满,那么该函数就会立即返回。-
offset
:指定存储器偏移,数据应写入该指定偏移位置 -
len
:指定调度器突发长度。此突发长度将发送至适配器,随后,适配器即可将其转换为标准 AXI AMBA 协议
-
-
void write(const T &val, ap_int<sizeof(T)> byteenable_mask = -1);
此方法用于将数据从调度器的内部缓冲器传输到
m_axi
适配器。如果内部写入缓冲器已满,那么它会执行阻塞。byteenable_mask 用于在 WDATA 中启用字节。默认情况下,它将启用传输的所有字节。write()
方法应调用len
次,具体次数在write_request()
中指定。 -
void write_response();
此方法会执行阻塞,直至从全局存储器返回所有写入响应为止。此方法的调用次数应与
write_request()
相同。
-
在 HLS 设计中使用手动突发
在 HLS 设计中,如果您发现自动突发传输未按期望方式工作,并且您无法按需对设计进行最优化,那么您可以使用 hls::burst_maxi
对象来实现读取和写入传输事务。在此情况下,您将需要修改自己的代码,将原始指针实参替换为 burst_maxi
(作为函数实参)。这些实参必须供 burst_maxi
类的 read
和 write
显式方法访问,如以下示例所示。
void dut(int *A) {
for (int i = 0; i < 64; i++) {
#pragma pipeline II=1
... = A[i]
}
}
在以下修改后的代码中,该指针被替换为 hls::burst_maxi<>
类对象和方法。在此示例中,HLS 调度器将 4 个请求(len
均为 16)从端口 A
放置到 m_axi
适配器上。该适配器将其存储在 FIFO 内部,只要 AW/AR 总线可用,它就会将请求发送到全局存储器。在 64 次循环迭代中,read()
命令会发出阻塞调用,调用将等待全局存储器返回的数据。数据可用后,HLS 调度器将从 m_axi
适配器 FIFO 读取此数据。
#include "hls_burst_maxi.h"
void dut(hls::burst_maxi<int> A) {
// Issue 4 burst requests
A.read_request(0, 16); // request 16 elements, starting from A[0]
A.read_request(128, 16); // request 16 elements, starting from A[128]
A.read_request(256, 16); // request 16 elements, starting from A[256]
A.read_request(384, 16); // request 16 elements, starting from A[384]
for (int i = 0; i < 64; i++) {
#pragma pipeline II=1
... = A.read(); // Read the requested data
}
}
在以下示例 2 中,HLS 调度器/内核会将 2 个请求从端口 A 放置到适配器上,第一个请求的 len
为 2,第二个请求的 len
为 1,总计 2 个写入请求。随后,它会发出对应的请求,因为总计突发长度为 3 条写入命令。该适配器将这些请求存储在 FIFO 内部,只要 AW 和 W 总线可用,它就会将请求和数据发送到全局存储器。最后,使用 2 条 write_response
命令等待 2 个 write_requests
的响应。
void trf(hls::burst_maxi<int> A) {
A.write_request(0, 2);
A.write(x); // write A[0]
A.write_request(10, 1);
A.write(x, 2); // write A[1] with byte enable 0010
A.write(x); // write A[10]
A.write_response(); // response of write_request(0, 2)
A.write_response(); // response of write_request(10, 1)
}
在 C 语言仿真中使用手动突发
您可将常规阵列传递到顶层函数,构造函数将把此阵列自动转换为 hls::burst_maxi
。
burst_maxi(T
*p)
构造函数只能在 C 语言仿真模型内使用。#include "hls_burst_maxi.h"
void dut(hls::burst_maxi<int> A);
int main() {
int Array[1000];
dut(Array);
......
}
使用手动突发进行性能最优化
Vitis HLS 将突发行为划分为两种类型:流水线突发和顺序突发。
- 流水线突发
- 流水线突发通过在单个请求内读取或写入最大量的数来改善吞吐量。如果
read_request
、write_request
和write_response
调用位于循环外部,那么编译器就会推断流水线突发,如以下代码示例所示。在以下示例中,大小 (size) 是从测试激励文件发送的变量。9 int buf[8192]; 10 in.read_request(0, size); 11 for (int i = 0; i < size; i++) { 12 #pragma HLS PIPELINE II=1 13 buf[i] = in.read(); 14 out.write_request(0, size*NT); 17 for (int i = 0; i < NT; i++) { 19 for (int j = 0; j < size; j++) { 20 #pragma HLS PIPELINE II=1 21 int a = buf[j]; 22 out.write(a); 23 } 24 } 25 out.write_response();
- 顺序突发
-
此突发为数据大小较小的顺序突发,其中读取请求、写入请求和写入响应均位于循环主体内部,如以下代码片段中所示。顺序突发的缺点在于后续请求 (i+1) 依赖于先前请求 (i) 才能完成,因为它会等待读取请求、写入请求和写入响应完成,这将导致请求之间出现间隔。顺序突发效率不及流水线突发,因为它多次读取或写入少量数据,以补偿循环边界。虽然这将限制吞吐量改善,但是顺序突发仍好于无突发。
功能特性和局限性
-
如果
m_axi
元素为结构体:- 此结构体将打包到宽整型 (wide int) 内。不允许对此结构体执行分解。
- 结构体大小必须为 2 的幂,不应超过 1024 位或
config_interface -m_axi_max_bitwidth
命令指定的最大宽度。
-
不允许 burst_maxi 端口的 ARRAY_PARTITION 和 ARRAY_RESHAPE。
-
您可将 INTERFACE 编译指示或指令应用于
hls::burst_maxi
,以定义m_axi
接口。如果burst_maxi
端口与其他端口捆绑,则此捆绑中的所有端口都必须设置为hls::burst_maxi
并且必须具有相同的元素类型。void dut(hls::burst_maxi<int> A, hls::burst_maxi<int> B, int *C, hls::burst_maxi<short> D) { #pragma HLS interface m_axi port=A offset=slave bundle=gmem // OK #pragma HLS interface m_axi port=B offset=slave bundle=gmem // OK #pragma HLS interface m_axi port=C offset=slave bundle=gmem // Bad. C must also be hls::burst_maxi type, because it shares the same bundle 'gmem' with A and B #pragma HLS interface m_axi port=D offset=slave bundle=gmem // Bad. D should have 'int' element type, because it shares the same bundle 'gmem' with A and B }
- 您可以使用 INTERFACE 编译指示或指令来指定
num_read_outstanding
和num_write_outstanding
,并使用max_read_burst_length
和max_write_burst_length
来定义m_axi
适配器的内部缓冲器大小。void dut(hls::burst_maxi<int> A) { #pragma HLS interface m_axi port=A num_read_outstanding=32 num_write_outstanding=32 max_read_burst_length=16 max_write_burst_length=16 }
- 不支持 INTERFACE 编译指示或指令
max_widen_bitwidth
,因为 HLS 将不会更改hls::burst_maxi
端口的位宽。 - 您必须先发出
read_request
,再发出read
,或者先发出write_request
,再发出write
:void dut(hls::burst_maxi<int> A) { ... = A.read(); // Bad because read() before read_request(). You can catch this error in C-sim. A.read_request(0, 1); }
- 如果读取组的地址和生存期 (
read_request()
>read()
) 与写入组 (write_request()
>write()
>write_response()
) 重叠,那么该工具将无法保证访问顺序。C 语言仿真将报告错误。void dut(hls::burst_maxi<int> A) { A.write_request(0, 1); A.write(x); A.read_request(0, 1); ... = A.read(); // What value is read? It is undefined. It could be original A[0] or updated A[0]. A.write_response(); } void dut(hls::burst_maxi<int> A) { A.write_request(0, 1); A.write(x); A.write_response(); A.read_request(0, 1); ... = A.read(); // this will read the updated A[0]. }
- 如有多个
hls::burst_maxi
端口捆绑到同一个m_axi
适配器,并且其传输事务生存期重叠,那么行为将不可预测。void dut(hls::burst_maxi<int> A, hls::burst_maxi<int> B) { #pragma HLS INTERFACE m_axi port=A bundle=gmem depth = 10 #pragma HLS INTERFACE m_axi port=B bundle=gmem depth = 10 A.read_request(0, 10); B.read_request(0, 10); for (int i = 0; i < 10; i++) { #pragma HLS pipeline II=1 …… = A.read(); // get value of A[0], A[2], A[4] … …… = B.read(); // get value of A[1], A[3], A[5] … } }
- 不支持将读取/写入请求与读取/写入操作包含在不同数据流进程内。数据流检查程序将报告错误:
multiple writes in different dataflow processes are not allowed
。例如:void transfer(hls::burst_maxi<int> A) { #pragma HLS dataflow IssueRequests(A); // issue multiple wirte_request() of A Write(A); // multiple writes to A GetResponse(A); // write_response() of A }
潜在陷阱
以下是您在实现手动突发方法时必须留意的部分问题:
- 死锁:错误使用手动突发可能导致死锁。
在
read()
命令前发出过多read_requests
将导致死锁,因为read_request
循环将把请求推入读取请求 FIFO,仅在全局存储器读取操作完成后才会清空此 FIFO。read()
命令的工作是从适配器 FIFO 读取数据,并将请求标记为已完成,然后,将从 FIFO 迅速发出read_request
,随后即可将新请求推送到此 FIFO。//reads/writes. will deadlock if N is larger for (i = 0; i < N; i++) { A.read_request(i * 128, 16);} for (i = 0; i < 16 *N; i++) { … = A.read();} for (int i = 0; i < N; i++) { p.write_request(i * 128, 16); } for (int i = 0; i < N * 16; i++) { p.write(i); } for (int i = 0; i < N; i++) { p.write_response(); }
在以上示例中,如果 N 大于
read_request
,并且读取 FIFO 将满,因为它趋向于 N/2。读取请求循环将无法完成,读取命令循环将无法启动,从而导致死锁。注释: 对于write_request()
和write()
命令也同样如此。 - AXI 协议违例:写入请求与写入响应数量应相等。请求与响应数量不相等将导致发生 AXI 协议违例