| 格式 | 存储 | 随机访问 | 适合 |
|---|---|---|---|
| COO | (行,列,值)三元组 | 差 | 转换中间格式 |
| CSR | 行指针+列索引+值 | 行优先好 | 稀疏矩阵×向量 |
| CSC | 列指针+行索引+值 | 列优先好 | 矩阵乘法 |
稀疏引擎:检测零值跳过MAC,统计活跃PE数。CSC解码器:从压缩格式恢复稀疏矩阵行索引和值。
选择合适的稀疏格式对硬件效率至关重要:
// CSR格式:3个数组
// val[]: 非零值
// col_idx[]: 列索引
// row_ptr[]: 行指针(row_ptr[i]到row_ptr[i+1]是该行的非零元素)
// 示例矩阵:
// [1 0 0 2]
// [0 3 4 0]
// [5 0 6 0]
// val = [1,2,3,4,5,6]
// col_idx = [0,3,1,2,0,2]
// row_ptr = [0,2,4,6]
SpMV是稀疏计算的核心操作,瓶颈在于不规则的内存访问模式。
| 稀疏度 | 理论加速 | 实际加速(非结构化) | 实际加速(2:4结构化) |
|---|---|---|---|
| 50% | 2× | 1.2-1.5× | 1.8-1.9× |
| 75% | 4× | 1.5-2.0× | N/A |
| 90% | 10× | 2.0-3.0× | N/A |
| 95% | 20× | 3.0-5.0× | N/A |
完成本课后,你已掌握 稀疏计算 的核心概念与硬件实现方法。
不同稀疏度需要不同的硬件策略:
// 门控MAC:检测零值后跳过
// always_ff @(posedge clk)
// if (|operand_a && |operand_b) // 非零检测
// result <= result + operand_a * operand_b;
// else
// result <= result; // 保持不变,省功耗
//
// 面积开销:<5%(加一个比较器)
// 功耗节省:与稀疏度成正比
// 性能提升:几乎为0(仍然占用周期)
// 2:4结构化稀疏引擎:
// 每4个权重2个非零,硬件直接处理
//
// 硬件结构:
// [权重解码器] → [2个MAC] → [累加器]
// 2-bit索引 → 选择2个激活值
// 2个MAC同时执行非零乘法
//
// 带宽节省:50%(只传输非零权重+2-bit索引)
// 计算节省:50%(2个MAC代替4个)
// 面积开销:<10%(索引解码器)
// CSC稀疏矩阵解码器:
// 输入:col_ptr[], row_idx[], nz_vals[]
// 输出:按列输出的非零元素+行索引
//
// 处理流程:
// 1. 从col_ptr[c]到col_ptr[c+1]获取第c列的非零元素
// 2. row_idx[i]指示该非零值的行位置
// 3. 激活值按row_idx从输入向量中索引
// 4. 执行MAC: result[col] += nz_vals[i] * input[row_idx[i]]
//
// 关键挑战:不规则的内存访问模式
// 优化:预取+缓存行索引对应的激活值
| 方案 | 稀疏度 | 面积开销 | 功耗节省 | 延迟加速 |
|---|---|---|---|---|
| 门控MAC | 30% | <5% | 20% | ~1× |
| 2:4结构化 | 50% | 10% | 45% | ~1.9× |
| CSC解码器 | 90% | 20% | 80% | ~3× |
| 专用稀疏引擎 | 95% | 40% | 90% | ~5× |
本节提供该模块的详细实现指南、常见bug和调试方法:
每个模块应遵循统一的接口规范:
// 标准模块接口模板:
// module xxx #(
// parameter DATA_W = 16,
// parameter ...
// )(
// input wire clk, // 时钟
// input wire rst_n, // 异步复位,低有效
// input wire en, // 模块使能
// // 数据输入
// input wire [DATA_W-1:0] data_in,
// input wire data_valid,
// // 数据输出
// output reg [DATA_W-1:0] data_out,
// output reg data_out_valid,
// // 状态
// output wire busy,
// output wire error
// );
//
// 设计规则:
// 1. 所有时序逻辑使用posedge clk
// 2. 复位使用异步复位(negedge rst_n)
// 3. 参数化设计,便于配置
// 4. valid-ready握手协议
// 5. 每个模块有独立的en信号
| 症状 | 可能原因 | 调试方法 | 解决方案 |
|---|---|---|---|
| 输出全零 | 复位后未初始化 | 检查复位逻辑 | 确保复位释放后en有效 |
| 数据错位 | 流水线级数不匹配 | 波形对比输入输出 | 对齐valid信号延迟 |
| 溢出 | 累加器位宽不够 | 监测累加器最高位 | 增加位宽或加饱和逻辑 |
| 死锁 | valid-ready互等 | 追踪握手信号 | 确保下游始终能接收 |
| 时序违例 | 组合逻辑路径太长 | 查看综合报告 | 插入流水线寄存器 |
// Step 1: 语法检查
// verilator --lint-only module.v
//
// Step 2: 编写Testbench
// module tb;
// reg clk, rst_n;
// // 实例化DUT
// // 生成测试向量
// // 自动比对结果
// endmodule
//
// Step 3: 编译仿真
// verilator --cc module.v tb.v
// make -C obj_dir -f Vmodule.mk
// ./obj_dir/Vmodule
//
// Step 4: 波形调试(如果需要)
// verilator --trace --cc module.v tb.v
// 在testbench中: $dumpfile("wave.vcd"); $dumpvars;
// 模块集成清单:
// 1. 确认接口信号匹配(位宽、协议)
// 2. 时钟域是否一致(跨域需FIFO)
// 3. 复位策略是否统一
// 4. 信号命名是否规范
// 5. 是否有环路依赖(需打破)
// 6. 参数是否正确传递
完整的稀疏计算引擎包含多个子模块:
// 稀疏引擎架构:
// [CSC解码器] → [索引匹配] → [MAC阵列] → [结果收集]
//
// CSC解码器:
// 输入: col_ptr, row_idx, nz_vals
// 输出: 非零值 + 对应行索引
// 面积: ~1000门
//
// 索引匹配:
// 根据row_idx从激活向量中选取对应元素
// 使用SRAM + 地址生成器
// 面积: ~2000门(含小SRAM)
//
// MAC阵列:
// 与密集MAC相同,但输入已经筛选
// 面积: 同等规模密集引擎
//
// 结果收集:
// 将MAC输出写回对应输出位置
// 面积: ~500门
| 概念 | 定义 | 硬件影响 |
|---|---|---|
| 计算密度 | FLOPs/访存字节数 | 决定计算/带宽受限 |
| 数据复用 | 同一数据被多个PE使用 | 减少DRAM访问次数 |
| 流水线 | 多级操作重叠执行 | 提高吞吐量,增加延迟 |
| 双缓冲 | 两组缓冲区交替使用 | 隐藏数据加载延迟 |
| 分块计算 | 大矩阵分成小块处理 | 适配有限片上存储 |
| 饱和算术 | 溢出时钳位到极值 | 防止精度崩溃 |
| 时钟门控 | 空闲模块关闭时钟 | 降低动态功耗 |
将算法原理映射到硬件实现时,需要解决多个层次的抽象差距:
硬件设计没有唯一最优解,只有Pareto前沿上的多个平衡点:
| 设计点 | 面积(mm²) | 功耗(mW) | 算力(GOPS) | 能效比 |
|---|---|---|---|---|
| 最小面积 | 0.5 | 10 | 3.2 | 320 |
| 平衡设计 | 2.0 | 50 | 25.6 | 512 |
| 最高性能 | 5.0 | 200 | 102.4 | 512 |
| 最低功耗 | 1.0 | 5 | 6.4 | 1280 |
先写testbench,再写RTL,确保每个功能都有对应的测试:
// 验证驱动开发流程:
// 1. 根据规格编写testbench框架
// 2. 定义输入输出接口和预期行为
// 3. 编写RTL代码使测试通过
// 4. 添加边界条件测试
// 5. 添加随机化测试(约束随机)
// 6. 覆盖率分析确保测试充分
//
// 优势:
// - 减少bug引入率
// - 测试即文档
// - 重构有安全网
// - 提高设计质量