| 类型 | 粒度 | 加速效果 | 精度影响 |
|---|---|---|---|
| 非结构化 | 单个权重 | 低(需稀疏硬件) | 最小 |
| 行/列剪枝 | 整行/列 | 高 | 中等 |
| 通道剪枝 | 整个通道 | 最高 | 较大 |
支持非结构化(逐权重阈值)和结构化(行级)剪枝。内置稀疏度统计计数器。
剪枝粒度决定了理论稀疏度到实际加速的转化效率:
| 粒度 | 示例 | 最大稀疏度 | 加速转化率 | 硬件支持 |
|---|---|---|---|---|
| 单个权重 | |w|<θ→0 | 95%+ | 10-30%(需稀疏索引) | 专用稀疏引擎 |
| 2:4结构化 | NVIDIA方案 | 50% | 90%+ | 硬件直接跳过 |
| 行/列 | 整行=0 | 70% | 95%+ | 地址跳过 |
| 通道 | 整个通道=0 | 50% | 100% | 无需特殊硬件 |
| 层 | 整层=0 | 30% | 100% | 调度跳过 |
每4个权重中恰好2个为零,硬件用2-bit索引编码非零位置:
这是目前最实用的结构化稀疏方案——在稀疏度和硬件效率间取得完美平衡。
一次性大幅剪枝会严重损害精度,渐进式剪枝(Iterative Pruning)分多步进行:
其中s_f是最终稀疏度,s_i=0是初始稀疏度,三次方曲线保证开始慢后面快。
完成本课后,你已掌握 权值剪枝 的核心概念与硬件实现方法。
剪枝的理论基础是Lottery Ticket Hypothesis(彩票假设):
这意味着我们可以安全地移除大量权重而不损失精度——关键是找到正确的子网络。
| 标准 | 公式 | 最优稀疏度 | 硬件友好 |
|---|---|---|---|
| 幅度剪枝 | |w| < θ | 90%+ | 否(非结构化) |
| 梯度剪枝 | |∂L/∂w| × |w| < θ | 85%+ | 否 |
| Fisher信息 | w² × ∂²L/∂w² < θ | 90%+ | 否 |
| L1行/列范数 | ‖w_row‖₁ < θ | 60-70% | 是(结构化) |
| 几何中值 | 距几何中值最近 | 70-80% | 是(通道级) |
// NVIDIA 2:4 稀疏格式:
// 每4个权重中恰好2个为零
// 压缩存储:2个非零值 + 2-bit索引
//
// 示例:[w0, 0, w2, 0] → 存储[w0, w2] + 索引[00,10]
// (索引00=w0在位置0, 10=w2在位置2)
//
// 硬件实现:
// 1. 索引解码器:2-bit→4路MUX选择激活值
// 2. 2个MAC单元:同时计算2个非零乘法
// 3. 累加:2个部分和相加
//
// 面积:与密集PE相当(索引解码器≈MAC面积)
// 计算:50%MAC + 索引开销≈5% → 净省45%
本节提供该模块的详细实现指南、常见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. 参数是否正确传递
剪枝减少计算量但增加了控制逻辑:
| 剪枝类型 | 计算节省 | 面积节省 | 控制开销 | 净面积变化 |
|---|---|---|---|---|
| 非结构化 | 90% | 5%(需要索引) | +15% | +10% |
| 2:4结构化 | 50% | 40% | +10% | -30% |
| 通道剪枝 | 50% | 50% | +0% | -50% |
| 行/列剪枝 | 60% | 55% | +5% | -50% |
| 概念 | 定义 | 硬件影响 |
|---|---|---|
| 计算密度 | 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引入率
// - 测试即文档
// - 重构有安全网
// - 提高设计质量