夜雨聆风学习资料网

ARTICLE · 1286343

iDMA 源码全栈解析:三层契约、数据结构、Scatter-Gather 双路径与 Tensor Stride 展开

iDMA 源码全栈解析:三层契约、数据结构、Scatter-Gather 双路径与 Tensor Stride 展开

🎯 一句话主旨:Midend 用级联计数器把 tensor stride 说清楚,Backend 用合法化器和桶形移位器把 1D 搬合法;两者之间的接口窄到只剩三个数字 (src_addr, dst_addr, length)——接口即自由。

📚 目录

  1. 1. iDMA 是什么:源码给出的身份定义
  2. 2. 三层契约:不可违反的接口边界
  3. 3. 数据结构逐层拆解
  4. 4. ND Midend:级联计数器与无乘法器展开
  5. 5. Scatter-Gather 路径一:描述符链的任意离散
  6. 6. Scatter-Gather 路径二:Snitch + ND 的规律离散
  7. 7. 合法化器:协议合规的唯一守门人
  8. 8. 传输层:数据通路的真实代码形状
  9. 9. 在途计算:移动中变换,不额外访存
  10. 10. 参数、限制与集成事实
  11. 11. 总结:源码告诉我们的设计意图

1. iDMA 是什么:源码给出的身份定义

1.1 官方定位的直译

README 的第一句话定义了 iDMA 的身份:

"一个模块化、可参数化、高度灵活的数据移动加速器架构,目标覆盖从超低功耗边缘节点到高性能计算系统的广泛平台。"

它是 PULP 平台的集群级 DMA,用于 Snitch Cluster 和 PULP Cluster——这是它的原生场景。

1.2 它不是"更好的 1D DMA"

传统 DMA 的"固定功能"困境可以用一句话概括:

🎯 擅长连续,失语于离散。

当访存模式变成多维张量切片、Scatter-Gather、转置时,1D DMA 只能退化为:

  • • CPU 逐元素拷贝(最慢)
  • • 海量单拍小传输(总线地址相位爆炸)

iDMA 的解法不是把 1D DMA 做得更快,而是把地址生成的复杂度从数据通路中解耦出去——用一个可选的、独立的 Midend 消化 stride,让 Backend 回到它最擅长的 1D 世界。

2. 三层契约:不可违反的接口边界

2.1 源码中的原始表述

README 用一句话定义了整个架构的拆分逻辑:

"iDMA 的核心思想是把 DMA 引擎拆成三个截然不同的部分:Frontend 发出传输请求;Midend 把前端请求变换为通用的 1D 传输,交给 Backend 处理;Backend 拿到一个 1D 传输 (src_addr, dst_addr, length) 并在传输协议的管理接口上执行它。"

2.2 三个契约的硬边界

层
输入
输出
绝对不感知
Frontend
软件 / 平台通信
idma_req_t
 或 idma_nd_req_t
协议时序、总线细节
Midend
ND 请求(可选)
1D 请求流
协议合法性、字节对齐
Backend
1D 请求
总线事务
stride、维数、上一个请求的地址

⚠️ Backend 只认识三个数字:源地址、目的地址、长度。 它收到  个这样的三元组,就视为  个独立事务,绝不跨三元组合并——哪怕它们在物理地址上相邻。

2.3 为什么这是"硬契约"而不是"建议"

如果 Midend 把 stride 信息"泄漏"给 Backend,三层就退化为一个无法独立验证的巨石。源码的选择是:

🧩 Midend 承担全部"说清楚"的复杂度;Backend 承担全部"搬合法"的复杂度。

两者之间的接口窄到只剩 3 个数字,所以可以独立替换、独立形式化验证。

2.4 三层数据流

3. 数据结构逐层拆解

iDMA 的三层拆分不仅是逻辑分层,更直接体现在数据结构的归属上。每一层有自己能读写的结构体,有自己永远看不到的字段。

3.1 数据结构在各层的位置

  • • 🔵 前端持有描述符或寄存器字段,产出 idma_nd_req_t 或直接产出 idma_req_t。
  • • 🟣 中间层持有 idma_nd_req_t,内部维护索引矩阵的计数器状态,逐 burst 产出 idma_req_t。
  • • 🟢 后端只持有 idma_req_t 和一组参数化配置;数据通路形状由参数决定,而不是由 stride 决定。

3.2 idma_req_t:后端唯一认识的 1D 结构体

idma_req_t 是三层之间的"通用货币"。源码中它的字段直接决定了后端能看到什么:

字段
类型
语义
不感知
lengthtf_len_t
最内维连续字节数 
不感知这是第几个 burst
src_addraddr_t
源字节级起点
不感知上一个 burst 的地址
dst_addraddr_t
目的字节级起点
不感知 stride
scale_addraddr_t
MX 量化比例平面地址
普通 stride 传输中不参与地址展开
optoptions_t
协议与后端控制
内嵌 compute_options_t

用带标签列的数组表达后端看到的"世界":

🎯 这个数组中没有任何 stride 行。后端收到  个这样的数组,就视为  个独立事务,绝不跨数组合并。

3.3 options_t 与 compute_options_t:请求上的控制平面

options_t 是 idma_req_t 中 opt 字段的类型,它本身是一个嵌套结构:

compute_op_e 的六个取值不是手写的 RTL 常量,而是从 idma_reg.rdl生成的:

"compute_op_e 编码了 COMPUTE_NONE、COMPUTE_TRANSPOSE、COMPUTE_MXQUANT、COMPUTE_MXQUANT_FP16、COMPUTE_MXDEQUANT、COMPUTE_MXDEQUANT_FP16。该枚举从 idma_reg.rdl 生成,所以 RTL 和软件头文件共享同一份编码。"

🎯 这意味着 options_t 的编码事实源是 RDL,而不是 SystemVerilog。软件驱动和硬件 RTL 通过同一份生成产物对齐,避免了枚举值漂移。

compute 的生效条件是一个逻辑与:

  • • 🧩 EnableCompute 是后端参数,决定硬件是否例化计算引擎。
  • • 🧩 ComputeOps 是后端参数,决定允许哪些操作。
  • • 🧩 opt.compute.op 是请求字段,决定这次传输做什么。

3.4 idma_rsp_t 与 err_payload_t:完成与错误的结构化表达

idma_rsp_t 是后端返回给前端的完成信号,它包含两个成分:

成分
语义
源码事实
完成状态
传输是否成功
所有 burst 完成后发出
错误载荷
哪个 burst 出错、错误来源
err_payload_t
 标识故障 burst 和来源

后端的 ErrorCap 参数决定错误处理能力:

ErrorCap
 取值
行为
NO_ERROR_HANDLING
默认;不处理错误
ERROR_HANDLING
监控总线响应并上报故障

用结构化数组表达错误载荷的定位能力:

🎯 这对于ND 展开后的长 burst 流尤其重要:中间层发出了  个 burst,如果第  个失败,err_payload_t 必须能回溯到具体是哪个 burst,而不是笼统报告"传输失败"。

3.5 idma_nd_req_t:中间层的维度描述符数组

ND 中间层的输入 idma_nd_req_t 是把 1D 基块和每维描述符数组打包在一起的结构:

字段拆解:

成分
内容
语义
burst_reqidma_req_t基础 1D 块
:首地址 + 长度 
d_req[j]
每维描述符
第  维的重复次数和源/目的 stride

每个 d_req[j] 至少包含:

⚠️ d_req 数组的长度是 NumDim - 1,不是 NumDim。原因:第一维(最内维)由 burst_req.length 隐含,不需要独立的 stride 字段。

ND 请求到 1D 请求流的映射:

3.6 desc64 描述符:256 位打包的链表节点

每个描述符是 256 位打包结构,字段的位域顺序和内存字节序相反:

"在 SystemVerilog packed struct 中,第一个字段(flags)占据最高位,最后一个字段(dest_addr)占据最低位。在内存中(小端),dest_addr 在最低字节地址。"

用带位域标签的打包数组表达概念布局:

⚠️ 实际 packed struct 的字段顺序以源码 idma_desc64_top.sv 为准。关键是:flags 在高位,dest_addr 在低位,小端内存中 dest_addr 在最低地址。

flags 是一个 32 位位域,每一位有明确语义:

位
字段
语义
0irq
完成时触发中断
2:1src_burst
源 burst 类型:00=FIXED,01=INCR,10=WRAP
4:3dst_burst
目的 burst 类型:同上
5decouple_rw完全解耦读写
(缓冲满时有死锁风险)
6decouple_aw安全解耦
:写地址等首个读数据到达
7reduce_len
缩减源/目的 burst 长度
11:8src_cache
源 AXI cache 属性
15:12dst_cache
目的 AXI cache 属性
23:16axi_id
传输的 AXI ID
31:24
保留
—

典型 flags 值:

  • • 0x00000000:默认(INCR,无解耦,无中断)
  • • 0x00000001:完成时中断
  • • 0x00000061:decouple_aw + decouple_rw + 中断

3.7 后端参数结构:决定数据通路形状的配置集

后端的数据结构不仅是 idma_req_t,还有一组决定数据通路形状的参数。这些参数不是运行时字段,而是例化时固定的配置:

参数
默认
对数据通路的影响
DataWidth
16
决定桶形移位器的字节车道宽度
BufferDepth
2(推荐 3)
决定重排缓冲的槽数;深度 2 在非对齐传输时 stall
NumAxInFlight
2
决定并发事务数,影响读/写 burst 队列深度
HardwareLegalizer
1
决定是否例化合法化器;为 0 时软件必须预切 burst
CombinedShifter
0
为 1 时只用一个桶形移位器,省面积但 buffer 中数据不再字对齐
MaskInvalidData
1
为 1 时无效字节清零,减少翻转功耗
ErrorCapNO_ERROR_HANDLING
决定是否例化错误处理器

3.8 数据结构在各层的映射总览

层
持有结构体
能读能写永远看不到
前端desc64
 描述符 / 寄存器字段
描述符全部字段 / MMIO
idma_nd_req_t
 或 idma_req_t
后端参数、合法化器段
中间层idma_nd_req_tburst_req
 + d_req[]
idma_req_t
 流
协议页大小、buffer 深度
后端idma_req_t
 + 参数集
length
 / 地址 / opt
总线事务
stride、维数、描述符

结构体在流水线中的生命周期:

💡 数据结构设计遵循一条原则:每个结构体只携带该层需要的信息,并且尽可能窄。idma_req_t 只有 5 个字段,窄到后端可以独立形式化验证;idma_nd_req_t 只增加维度描述符数组,不污染 1D 契约;desc64 用 256 位承载链表节点,但通过 reshaper 的硬编码主动牺牲吞吐换合规。

4. ND Midend:级联计数器与无乘法器展开

4.1 源码中的实现描述

官方文档对 ND Midend 的描述精确到了微架构选择层面:

"每完成一个 1D burst,中间层检查当前维度是否还有剩余重复次数。若有,就把该维 stride 加到地址上,发出下一个 burst。当一个维度耗尽重复次数时,更高一维的计数器递增。内部用级联计数器(每维一个)和一个基于 popcount 的选择器实现,一次处理多层同时溢出。"

两个核心参数:

  • • NumDim:维数,必须 ——第一维由后端的 1D burst 隐含。
  • • RepWidths:每维计数器位宽数组。例:NumDim=3, RepWidths='{32, 16, 8} 表示第一维最多  次重复,第二维 ,第三维 。

4.2 级联计数器 + popcount:代码结构的文字还原

源码中 idma_nd_midend.sv 的关键信号是 stage_done 和 stage_zero。一个被广泛引用的代码注释写道:

"popcount 用于识别最高已完成 stage,然后加到当前地址寄存器上。"

  • • 🔁 每握手一个 burst,最内维计数器 +1;如果未计满,不需要任何进位。
  • • ⚡ 计满时,stage_done[j] = 1;popcount 一次识别"哪几维同时溢出",避免链式进位的时序瓶颈。
  • • 🧮 地址只加最高已完成 stage 的 stride;低维计数器清零——这就是  的硬件实现,不需要计算净增量。

4.3 数学本质:索引矩阵的逐行遍历

设 ,,。

第  个 burst 的维度索引:

burst 地址向量:

关键对应: 的第  行就是硬件在发第  个 burst 时的计数器状态向量。硬件不构造完整矩阵,而是逐行遍历。

4.4 官方 2D 数值示例

官方文档给出的 2D 传输:64 字节行,重复 4 次,源 pitch 128,目的 pitch 64。

  • • 🎯 每一列就是一个 1D 请求,共 4 个,顺序发出。
  • • 🎯 索引矩阵 ,硬件计数器从 0 计到 3。
  • • ⚠️ 后端将这 4 列视为 4 个独立事务,不合并——这是"小 stride 协议税"的直接来源。

5. Scatter-Gather 路径一:描述符链的任意离散

5.1 描述符前端的源码事实

描述符前端 idma_desc64_top 的本质是用链表表达任意离散性:

"描述符前端使用共享内存中的传输描述符链表。硬件通过 AXI 读端口自主获取描述符,使软件可以连续入队多个传输,而无需轮询每个传输的完成。"

5.2 一个关键的源码硬编码:debursting

描述符前端的 reshaper 模块负责把描述符的 flag 位映射为后端选项,但它硬编码了某些值:

"src_max_llen 和 dst_max_llen 总是被设为 0(完全 debursting:每个传输被拆成单拍总线事务,用吞吐换最大的协议合规性),lock、prot、qos、region 也被清零。"

  • • 🚫 描述符路径优先保证协议合规,而不是吞吐。
  • • 🚫 即使源/目的地址是连续的,也被拆成单拍事务——没有 burst 合并。
  • • ✅ 这适合完全不规则的离散访问:网络包队列、随机稀疏张量、任意空洞地址。

5.3 链表遍历与投机预取

描述符是链表节点,next 指向下一个描述符的地址;0xFFFFFFFF_FFFFFFFF 标记链尾。

"NSpeculation 参数控制前端可以在后端消费之前预取多少个描述符。这隐藏了描述符取指延迟——当后端处理一个传输时,前端已经在读取后面 NSpeculation 个描述符。"

默认值 NSpeculation = 4。

  • • 📌 预取深度 4 意味着描述符取指延迟被部分隐藏。
  • • 📌 NSpeculation = 0 时禁用预取,每个描述符必须等上一个传输完成后才取。

6. Scatter-Gather 路径二:Snitch + ND 的规律离散

6.1 Snitch 前端的指令集

Snitch 前端通过 DMSRC / DMDST / DMCPY / DMSTR / DMREP 指令发起传输。其中 DMSTR 设置源/目的 stride,DMREP 设置重复次数,直接驱动 ND 中间层。

论文指出:"配置并启动一个 1D 传输只需 3 条指令,2D 传输最多 6 条指令。"

🎯 规律性 Scatter-Gather 的元数据开销是"几条指令",而不是一长串描述符。这正是 ND 中间层存在的理由。

6.2 两条路径的决策对比

维度
描述符链(Desc64)Snitch + ND Midend
离散性类型任意、不规则规律、张量化
元数据开销
高:256 位/描述符
极低
:几条指令
burst 策略强制 debursting
(src_max_llen=0)
由 ND 展开决定
硬件加速点
链表自主获取 + 投机预取
级联计数器 + popcount
适用场景
网络包队列、随机稀疏
Im2Col、tile 抽取、通道重排

决策流程图:

7. 合法化器:协议合规的唯一守门人

7.1 源码职责的精确定义

"合法化器把请求切分为协议合法的总线 burst——即*不跨页边界、且不超过协议最大拍数的 burst,同时尊重对齐约束。"*

后端的三个执行阶段:

  1. 1. 合法化器:页切分 + 拍切分,产生 offset / tailer / shift 控制信号。
  2. 2. 传输层:读 → 桶形移位 → 缓冲 → 写。
  3. 3. 错误处理器(可选):监控总线响应。

7.2 页切分的数学规则

设请求起点 ,长度 ,页大小 (AXI 为 4096)。

第一段:

后续段:

用带水平分隔的列数组表达三段结构:

7.3 一个关键的架构边界

❗ 合法化器对每个 1D 请求独立切分,绝不跨请求合并。

设计意图: 后端只保证每个 1D 请求内部合法,不负责重组跨请求的访问模式。这是"小 stride 协议税"的架构根源,不是实现缺陷。

8. 传输层:数据通路的真实代码形状

8.1 四个组件的源码描述

后端文档对传输层的描述精确到数据流方向:

"传输层执行每个 burst:读通道从源取数,桶形移位器重新对齐字节车道,数据缓冲吸收时序差,写通道把数据存到目的。"

组件
功能
源码事实
读通道
发出读请求,接收数据
与合法化器的读段流解耦
桶形移位器
字节车道对齐
shift
 信号来自合法化器
数据缓冲
吸收读写时序差
BufferDepth 默认 2,推荐 3
写通道
发出写请求
与读通道可以流水

8.2 BufferDepth = 3 的源码理由

后端文档明确写道:

2 = 最小,3 = 推荐,因为深度 2 的缓冲在读写偏移不同的非对齐传输上会阻塞,需要额外的缓冲槽给对齐流水线。"

用流水线阶段表达:

  • • 🧩 深度 2:只能容纳"正在写入"和"正在对齐"的数据;写通道取数时会发生争用。
  • • 🧩 深度 3:多一个槽给对齐后的数据,写通道可以独立取数,不 stall。

🎯 这是源码级别的性能事实,不是经验法则。

9. 在途计算:移动中变换,不额外访存

9.1 源码位置的精确定义

"计算引擎位于*传输层的写侧,在读数据流缓冲和写桶形移位器之间,所以变换在从源流向目的地的节拍流上运行,没有到内存的往返。"*

9.2 两种操作与合法化器的联动

操作
模块
效果
转置idma_otf_transpose
分块矩阵转置
MX 量化/反量化idma_otf_mxquant
 / mxdequant
FP32/FP16 ↔ MXFP8

"这些是改变尺寸的传输,在合法化器中合法化。"

🎯 计算改变读写长度,合法化器必须知道这一点——这是 compute 使能时合法化器额外推导写长度的原因。

10. 参数、限制与集成事实

10.1 源码推荐的核心参数

参数
默认
源码推荐
源码理由
DataWidth
16
匹配总线
必须是 2 的幂,16–1024
BufferDepth
2
3"depth-2 在非对齐传输时 stall"
HardwareLegalizer
1
1"除非你的软件预切 burst"
NSpeculation
4
4
描述符投机预取深度

10.2 源码承认的性能限制

限制
源码表述
含义
非对齐传输"任何小于总线宽度的传输都将不可避免地导致利用率的实质性下降"本质上限制
总线利用率
描述符 deburstsrc_max_llen = dst_max_llen = 0
每个传输拆成单拍,吞吐换合规
小 stride
"不跨请求合并"的架构边界
离散采样点各自付完整事务成本

10.3 固定延迟的源码事实

论文明确指出:"后端从接收 1D 传输到在协议端口发出读请求,有固定的两周期延迟。"

🎯 这是硬件流水线的固定开销,与传输大小无关。ND 中间层在此基础上额外增加一个周期延迟。

11. 总结:源码告诉我们的设计意图

源码确认的六个事实:

  1. 1. 🧩 三层解耦是硬契约,不是建议。 后端只认 (src_addr, dst_addr, length),stride 在中间层边界被完全消化。这是三层可以独立替换、独立验证的前提。
  2. 2. 📦 数据结构是层间契约的物理载体。idma_req_t 只有 5 个字段,窄到后端可以独立形式化验证;idma_nd_req_t 只增加维度描述符数组,不污染 1D 契约;desc64 用 256 位承载链表节点。options_t 的 compute_op_e 枚举从 RDL 生成,软件和 RTL 共享同一份编码。
  3. 3. 🔀 Scatter-Gather 有两条源码路径。 描述符链(desc64)处理任意不规则离散,代价是 256 位/描述符 + 强制 debursting;Snitch + ND 中间层处理规律性 stride,元数据是几条指令,硬件用级联计数器 + popcount 无乘法器展开。
  4. 4. ✂️ 合法化器是协议合规的唯一守门人,对每个 1D 请求独立切分,绝不跨请求合并。这是"小 stride 协议税"的直接来源,也是设计取舍而非缺陷。
  5. 5. 🚚 传输层数据通路是:读 → 桶形移位 → 缓冲 → 写。BufferDepth=3 是源码明确推荐值——深度 2 在非对齐读写偏移不同时会阻塞,因为对齐流水线需要额外一个缓冲槽。
  6. 6. ⚠️ 非对齐/小传输的总线利用率损失是源码承认的内在限制,不是可以通过参数调优消除的。描述符前端的 src_max_llen=0 是用吞吐换合规的显式设计选择。

💡 一句话记忆点:中间层用级联计数器把 stride 说清楚,后端用合法化器把 1D 搬合法;两条 Scatter-Gather 路径按"离散是否规律"分工,数据结构定义边界——接口即自由。

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料