🏠

Pipeline Orchestration 模块深度解析

一句话概括

pipeline_orchestration 是 MUSIC(Multiple Signal Classification)方向估计算法的空间流水线编排器——它将一个数学上串行的矩阵分解与谱估计流程,映射为横跨 Versal AIE 阵列的 141 个异构内核组成的物理流水线,通过显式的 Tile 位置约束和流式数据连接,实现亚毫秒级实时 DOA(Direction of Arrival)估计。

想象一条汽车装配线:每个工位只负责一道工序,车身在传送带上依次经过焊接、喷漆、总装。MUSIC 算法在这里被拆解为类似的"数字装配线"——输入信号先经过 IO 适配,然后流经 QR 分解、SVD 降维、DOA 谱计算、峰值扫描、角度查找六个阶段,每个阶段由数十个 AIE 内核并行处理,数据像传送带上的零件一样在 Tile 间流动。


问题空间:为什么需要这个模块?

MUSIC 算法的计算挑战

MUSIC 是一种高分辨率波达方向估计算法,广泛应用于雷达、声纳和无线通信。其核心计算流程为:

  1. 接收信号\(M\) 个天线阵元接收 \(N\) 个窄带信号源(\(M > N\)
  2. QR 分解:对接收矩阵进行正交三角化,提取信号子空间结构
  3. SVD 奇异值分解:进一步分解得到噪声子空间
  4. DOA 谱计算:遍历角度网格,计算空间谱函数
  5. 峰值检测:找出谱峰对应的角度

对于 \(M=128, N=8\) 的配置,传统 CPU 实现面临以下瓶颈:

  • 内存墙\(128 \times 8\) 复数矩阵需要在缓存层次间反复搬运
  • 数据依赖:QR 必须在 SVD 之前完成,SVD 必须在 DOA 之前完成
  • 并行度不匹配:QR 适合块状并行,DOA 适合角度并行,单一架构难以兼顾

为什么不用简单的任务并行?

naive 的多线程方案会遇到:

  1. 同步开销:每阶段结束后的屏障同步消耗数百周期
  2. 负载不均:QR 的计算密度远高于 DOA 谱搜索
  3. 内存带宽:共享内存成为瓶颈

AIE 空间流水线的解决思路

Versal AIE 架构提供了独特的解决方案:

  • 分布式本地内存:每个 AIE Tile 拥有 32KB 本地内存,消除共享内存瓶颈
  • 流式互联:Tile 间通过专用流通道通信,无需全局同步
  • 空间映射:将算法阶段物理映射到不同 Tile 行,形成真正的流水线

核心抽象: mental model

理解这个模块需要把握三个关键抽象:

1. Graph-as-Pipeline(图即流水线)

class music_graph : public adf::graph {
    io_adapter_graph  io_adapter;   // Stage 0: IO 适配
    qrd_graph         qrd;          // Stage 1: QR 分解
    svd_graph         svd;          // Stage 2: SVD 分解
    doa_graph         doa;          // Stage 3: DOA 谱计算
    scanner_graph     scanner;      // Stage 4: 峰值扫描
    finder_graph      finder;       // Stage 5: 角度查找
};

每个 *_graph 不是简单的类封装,而是一个可部署的计算阶段,包含:

  • 一组同构或异构的 kernel 实例
  • 内部 kernel 间的数据流连接
  • 对外暴露的输入/输出端口

2. Kernel-as-Tile(内核即 tile)

adf::location<adf::kernel>(music.qrd.qrd_kernel[i]) = adf::tile(col, ROW_0);

每个 kernel 被显式绑定到一个物理 AIE Tile。这不是可选的优化,而是架构要求

  • AIE 编译器需要根据位置信息生成流路由
  • 相邻阶段的 Tile 位置决定了数据路径长度
  • 位置约束直接影响时序收敛和吞吐量

3. Stream-as-Contract(流即契约)

adf::connect<>(qrd.sig_o, svd.sig_i);
adf::dimensions(qrd_kernel[i].out[0]) = {ROW * COL + COL * COL};

流连接不仅是数据传输,更是类型化的生产-消费契约

  • 上游 kernel 承诺产生特定尺寸的数据块
  • 下游 kernel 期望按此尺寸消费
  • 尺寸不匹配会导致流挂起或数据损坏

架构全景

flowchart TB subgraph "dut_graph (Top Level)" PLIO0["PLIO_i_0
64-bit input"] PLIO1["PLIO_i_1
64-bit input"] PLIO_OUT["PLIO_o
64-bit output"] subgraph "music_graph (Block Level)" direction LR IO["io_adapter
1 kernel
Tile(11,0)"] QRD["qrd
36 kernels
Row 0, Col 12-47"] SVD["svd
38 kernels
Row 1, Col 10-47"] DOA["doa
64 kernels
Row 2-3"] SCAN["scanner
2 kernels
Row 3"] FIND["finder
16 kernels
Row 0-3"] end end PLIO0 --> IO PLIO1 --> IO IO --> QRD --> SVD --> DOA --> SCAN --> FIND --> PLIO_OUT

数据流追踪:一次完整的 DOA 估计

以单帧数据处理为例,跟踪数据从输入到输出的完整旅程:

Stage 0: IO 适配 (io_adapter_graph)

// 输入:两个 64-bit PLIO 流,分别携带复数样本的实部和虚部
sig_i[0] = adf::input_plio::create("PLIO_i_0", adf::plio_64_bits, "data/sig_i_0.txt");
sig_i[1] = adf::input_plio::create("PLIO_i_1", adf::plio_64_bits, "data/sig_i_1.txt");

// 内部连接:合并两路输入为一路输出
adf::connect(sig_i[0], io_adapter_kernel.in[0]);
adf::connect(sig_i[1], io_adapter_kernel.in[1]);
adf::connect(io_adapter_kernel.out[0], sig_o);

// 输出维度:ROW * COL = 128 * 8 = 1024 个复数样本
adf::dimensions(io_adapter_kernel.out[0]) = {ROW * COL};

设计意图:将外部 PL(Programmable Logic)域的 DMA 流格式转换为 AIE 域的 buffer 格式。这是一个协议转换层,处理了时钟域跨越和数据打包问题。

Stage 1: QR 分解 (qrd_graph)

static constexpr unsigned NUM_QRD_KERNELS = COL * (COL + 1) / 2;  // 36 kernels

QR 阶段采用三角形流水线拓扑

Norm0 → QR_01 → QR_02 → ... → QR_07
          ↓
        Norm1 → QR_12 → ... → QR_17
                  ↓
                Norm2 → ... → QR_27
                          ...
                        Norm7

关键设计决策

  • 使用模板参数区分不同列的归一化和旋转操作
  • 数据维度逐级递减:TOTAL_NUM_SEGMENTSTOTAL_NUM_SEGMENTS_7
  • 最后一个 Norm kernel 不需要输出 Q 矩阵(节省带宽)

Stage 2: SVD 分解 (svd_graph)

static constexpr unsigned NUM_SVD_KERNELS = (7+6+5+4+3+2+1) + (9+1);  // 38 kernels

SVD 采用Jacobi 迭代实现,每个 kernel 执行一对列向量的旋转消去:

// 模板参数:(IN_V_FLAG, OUT_V_START, P0, Q0, P1, Q1, P2, Q2)
svd_kernel[0] = adf::kernel::create_object<SVD<0, 0, COL_0, COL_1, COL_0, COL_2, COL_0, COL_3>>();
// 含义:不输入 V,输出 V[0],同时处理 (0,1), (0,2), (0,3) 三对列

设计洞察:Jacobi SVD 的天然并行性——每次迭代可以同时处理多对不相交的列。这里每轮迭代安排 9 个 kernel,共 4 轮迭代。

Stage 3: DOA 谱计算 (doa_graph)

static constexpr unsigned NUM_DOA_KERNELS = NUM_DOA_INTERVALS;  // 64 kernels
static constexpr unsigned DOA_INTERVAL_LEN = 4;  // 每个 kernel 处理 4 个角度点

DOA 阶段采用完全数据并行

template <unsigned START, unsigned END>
void doa_kernel_create(void) {
    if constexpr (START <= END) {
        doa_kernel[START] = adf::kernel::create_object<DOA<COL, 1, START * DOA_INTERVAL_LEN, (START + 1) * DOA_INTERVAL_LEN>>();
        doa_kernel_create<START + 1, END>();
    }
}

编译期递归模板生成 64 个 kernel,每个处理 4 个角度点的谱计算。这是 C++17 if constexpr 的典型应用,保证零运行时开销。

Stage 4-5: 峰值扫描与查找 (scanner_graph, finder_graph)

// Scanner: 2 个 kernel,各处理一半区域
scanner_kernel[0] = adf::kernel::create_object<Scanner<0, NUM_REGIONS / 2>>();
scanner_kernel[1] = adf::kernel::create_object<Scanner<NUM_REGIONS / 2, NUM_REGIONS>>();

// Finder: 16 个 kernel,每个处理 2 个区域
finder_kernel_create<0, 15>();  // 递归生成

分治策略:先在 scanner 中识别潜在峰值区域(标记),然后在 finder 中精确定位峰值角度。


组件深度解析

dut_graph —— 顶层编排器

class dut_graph : public adf::graph {
public:
    music_graph                   music;
    std::array<adf::input_plio,2> sig_i;
    adf::output_plio              sig_o;
    // ...
};

职责边界

  • 不负责:具体算法实现(委托给 music_graph
  • 负责
    1. PLIO 接口定义(连接 PL 域的 DMA)
    2. 物理位置约束(所有 kernel 的 Tile 分配)
    3. 系统级静态断言(编译期参数校验)

静态断言的设计哲学

static_assert(ROW > COL,         "ROW (M) is not greater than COL (N)");
static_assert(ROW % 8 == 0,      "ROW is not a multiple of 8");
static_assert(COL % 4 == 0,      "COL is not a multiple of 4");
static_assert(COL <= 8,          "COL is greater than 8");
static_assert(ROW * COL <= 4096, "ROW * COL is greater than 4096");  // 32KB / 8 bytes

这些断言在编译期捕获配置错误,避免在硬件上调试时才发现内存溢出或对齐问题。特别是 ROW * COL <= 4096,它保证了复数矩阵可以放入单个 Tile 的本地内存(32KB / sizeof(cfloat) ≈ 4096)。

位置约束的精妙之处

// IO Adapter: Row 0, Col 11 (靠近 PL 接口)
adf::location<adf::kernel>(music.io_adapter.io_adapter_kernel) = adf::tile(IO_ADAPTER_COL_START, ROW_0);

// QRD: Row 0, Col 12-47 (紧邻 IO Adapter,减少首级延迟)
for (unsigned i = 0, col = COL_START; i < qrd_graph::NUM_QRD_KERNELS; ++i, ++col)
    adf::location<adf::kernel>(music.qrd.qrd_kernel[i]) = adf::tile(col, ROW_0);

// SVD: Row 1, Col 47→10 (反向排列,与 QRD 末级相邻)
for (unsigned i = 0, col = COL_START + qrd_graph::NUM_QRD_KERNELS - 1; i < svd_graph::NUM_SVD_KERNELS; ++i, --col)
    adf::location<adf::kernel>(music.svd.svd_kernel[i]) = adf::tile(col, ROW_1);

蛇形布局(Snake Layout)

  • QRD 从左到右(Col 12→47)
  • SVD 从右到左(Col 47→10)
  • 这种"回头"布局使相邻阶段的末级-首级物理相邻,最小化跨行路由延迟

DOA 的双行分布

// Row 2: 39 kernels, Col 10→48
// Row 3: 25 kernels, Col 48→24

64 个 DOA kernel 分布在两行,利用水平方向的 Tile 资源,同时保持与上方 SVD 末级的近距离。

music_graph —— 逻辑流水线

class music_graph : public adf::graph {
public:
    std::array<adf::input_port,2> sig_i;
    adf::output_port              sig_o;
    io_adapter_graph              io_adapter;
    qrd_graph                     qrd;
    svd_graph                     svd;
    doa_graph                     doa;
    scanner_graph                 scanner;
    finder_graph                  finder;

    music_graph(void) {
        adf::connect<>(sig_i[0],         io_adapter.sig_i[0]);
        adf::connect<>(sig_i[1],         io_adapter.sig_i[1]);
        adf::connect<>(io_adapter.sig_o, qrd.sig_i);
        adf::connect<>(qrd.sig_o,        svd.sig_i );
        adf::connect<>(svd.sig_o,        doa.sig_i);
        adf::connect<>(doa.sig_o,        scanner.sig_i);
        adf::connect<>(scanner.sig_o,    finder.sig_i);
        adf::connect<>(finder.sig_o,     sig_o);
    }
};

分层设计模式

层级 职责 代表类
App Level 系统集成、物理约束 dut_graph
Block Level 算法阶段、内部连接 music_graph, qrd_graph
Kernel Level 计算实现、局部优化 QRD_Norm, SVD, DOA

这种三层架构实现了关注点分离:

  • 算法工程师专注于 block-level 的数据流设计
  • 系统工程师处理 app-level 的物理约束
  • 优化工程师深入 kernel-level 的 SIMD 向量化

设计权衡与决策

1. 显式位置约束 vs 自动布局

选择:所有 kernel 都使用 adf::location 显式绑定 Tile

权衡分析

方案 优点 缺点
显式约束 精确控制数据路径、可预测时序、便于调试 代码冗长、修改困难、可移植性差
自动布局 代码简洁、适应不同器件 可能产生次优路由、时序难收敛

为何选择显式约束

  • MUSIC 是性能关键型应用,每周期都重要
  • AIE 编译器的自动布局对复杂流水线往往产生"Z 字形"路由
  • 显式约束允许人类工程师利用领域知识(如蛇形布局)

2. 模板化 kernel 实例化 vs 运行时参数

选择:大量使用模板参数(COL_NORM, START, END 等)

qrd_kernel[index] = adf::kernel::create_object<QRD_Norm<COL_NORM_0, TOTAL_NUM_SEGMENTS, 0, TOTAL_NUM_SEGMENTS>>();

权衡分析

方案 编译期确定 运行时确定
代码体积 每种特化一份代码 统一代码
运行时开销 无参数检查/分支 需验证参数合法性
灵活性 需重新编译改配置 可动态调整

为何选择模板

  • AIE 内核资源紧张(程序内存有限),消除运行时分支至关重要
  • 配置参数(ROW, COL)在系统设计期已固定,无需运行时变化
  • 编译器可进行激进优化(循环展开、常量传播)

3. 流式接口 vs 缓冲接口

观察:模块内混合使用两种接口

// io_adapter: 输入流,输出 buffer
void run(input_stream<cfloat> * __restrict sig_i_0,
         input_stream<cfloat> * __restrict sig_i_1,
         adf::output_buffer<cfloat, ...> & __restrict out);

// QRD/SVD: 纯 buffer 接口
void run(adf::input_buffer<cfloat, ...> & __restrict in,
         adf::output_buffer<cfloat, ...> & __restrict out);

设计理由

  • 输入用流:来自 PL 的数据是 DMA 驱动的流式传输,无法随机访问
  • 内部用 buffer:kernel 间数据需要多次访问(如 Jacobi 迭代的列旋转),buffer 支持随机索引
  • 输出用流:最终结果是顺序输出的角度估计,流式更高效

4. 单精度浮点 vs 定点/半精度

观察:全程使用 cfloat(32-bit 复数浮点)

aie::vector<cfloat, SEGMENT_SIZE> Q[TOTAL_NUM_SEGMENTS];

权衡分析

格式 精度 吞吐量 资源占用
cfloat 高(32-bit 运算)
cint16 低(16-bit 运算)

为何选择 cfloat

  • QR 和 SVD 涉及大量除法和开方,定点容易溢出
  • MUSIC 的 DOA 谱计算需要分辨接近的角度,对动态范围要求高
  • 本设计优先考虑精度而非极致吞吐量

依赖关系与调用链

模块依赖图

dut_graph
├── music_graph
│   ├── io_adapter_graph
│   │   └── io_adapter.h (IO_Adapter class)
│   ├── qrd_graph
│   │   ├── qrd_norm.h (QRD_Norm template)
│   │   └── qrd_qr.h (QRD_QR template)
│   ├── svd_graph
│   │   └── svd.h (SVD template)
│   ├── doa_graph
│   │   └── doa.h (DOA template)
│   ├── scanner_graph
│   │   └── scanner.h (Scanner template)
│   └── finder_graph
│       └── finder.h (Finder template)
└── music_parameters.h (全局配置)

数据契约

每个 graph 间的连接都有隐式的数据契约:

连接 上游产出 下游期望 契约内容
io_adapter → qrd ROW×COL 复数矩阵 同上 按行优先排列,连续存储
qrd → svd N×N 矩阵 + R 矩阵 W 和 V 矩阵 QR 结果包含 Q 和 R
svd → doa N×N 噪声子空间 同上 SVD 输出已排序的奇异向量
doa → scanner N×N 噪声子空间 + NUM_POINTS 谱值 同上 累加所有 kernel 的贡献
scanner → finder NUM_POINTS 谱值 + NUM_REGIONS 标记 同上 标记潜在峰值区域
finder → output NUM_POINTS 谱值 + NUM_REGIONS 结果 最终角度估计 峰值精确定位

违反契约的后果

  • 维度不匹配:流挂起(upstream stall)或数据截断
  • 语义不匹配:静默的错误结果(最难调试)

新贡献者指南

如何阅读代码

建议的阅读顺序:

  1. 从宏观到微观music_app.cppmusic_graph.hmusic_parameters.h
  2. 选一条数据流追踪:从 io_adapter_graph.h 开始,跟随 sig_oqrd_graph.h
  3. 理解一个完整 stage:选一个感兴趣的算法阶段(如 svd_graph.h),阅读其 kernel 头文件

常见陷阱

1. 静态断言失败

static_assert(ROW * COL <= 4096, "...");

症状:编译错误,提示内存超限 原因:尝试增大矩阵尺寸但未考虑 Tile 内存限制 解决:减小 ROW/COL,或重构算法使用分块处理

2. 位置约束冲突

adf::location<adf::kernel>(...) = adf::tile(col, row);

症状:链接错误或布线失败 原因:两个 kernel 被分配到同一 Tile,或超出器件边界 解决:检查 col/row 范围,确保唯一性

3. 维度不匹配

adf::dimensions(kernel.out[0]) = {SIZE_A};
adf::dimensions(next_kernel.in[0]) = {SIZE_B};  // SIZE_A != SIZE_B

症状:仿真挂起或数据错乱 原因:上下游 kernel 对数据块大小的预期不一致 解决:统一使用 music_parameters.h 中的常量定义

4. 模板递归深度

doa_kernel_create<1, 63>();  // 递归实例化 63 个 kernel

症状:编译时间过长或内存耗尽 原因:过度使用递归模板 缓解:考虑改用循环 + 数组(如果 AIE API 支持)

扩展建议

如需修改或扩展该模块:

  1. 添加新的处理阶段

    • 创建 new_stage_graph.h,继承 adf::graph
    • music_graph.h 中插入到适当位置
    • 更新 dut_graph 中的位置约束
  2. 修改 kernel 数量

    • 更新 music_parameters.h 中的相关常量
    • 同步修改位置约束循环的范围
  3. 调整数据精度

    • 修改所有 cfloatcint16 或其他类型
    • 重新验证数值精度是否满足算法要求

与其他模块的关系


总结

pipeline_orchestration 模块展示了如何在 Versal AIE 架构上实现复杂的信号处理流水线。其核心设计思想包括:

  1. 空间流水线:将算法阶段映射到物理 Tile 行,实现真正的并行流水线
  2. 显式控制:通过位置约束和维度声明,精确控制数据流和资源分配
  3. 分层抽象:App/Block/Kernel 三级架构,分离关注点
  4. 编译期优化:模板元编程消除运行时开销

理解这个模块的关键在于建立"空间思维"——不要把它看作在 CPU 上执行的代码,而要想象数据在二维 Tile 阵列中流动的物理过程。每一个 adf::connect 都是一条真实的流通道,每一个 adf::location 都是一个确定的硅片位置。

On this page