4、稀疏权重存储格式概览:CSR、CSC、COO、BCSR、Blocked-Ellpack等格式的原理与适用场景
TinyML模型在设备端部署时,权重张量经过结构化剪枝或量化后,往往呈现出极高的稀疏度(通常超过80%,甚至达到95%以上)。如果仍然使用稠密矩阵格式存储这些权重,不仅浪费宝贵的Flash和RAM空间,还会导致大量无效的乘加运算。稀疏存储格式的核心目标,就是用“索引+非零值”的组合替代完整的稠密矩阵,从而在存储容量和计算效率两个维度同时获益。然而,不同的稀疏格式在压缩率、访存局部性、硬件友好性和可扩展性上差异显著,选择不当反而会引入额外的索引开销或导致计算单元空转。本章将系统梳理CSR、CSC、COO、BCSR和Blocked-Ellpack五种主流格式的原理、适用场景及工程选型要点。
上图给出了稀疏格式选型的基本决策路径。实际工程中,还需要结合具体的硬件架构(如是否有DMA、向量指令集、缓存大小)和模型结构(如卷积层还是全连接层)进行综合权衡。下面逐一剖析每种格式的内部机制与适用边界。
4.1 CSR(Compressed Sparse Row)格式:通用性与行访问友好的平衡
核心原理
CSR格式将稀疏矩阵按行压缩,使用三个一维数组存储:values(非零值)、column_indices(每个非零值对应的列号)、row_ptr(每行第一个非零值在values中的偏移位置)。row_ptr的长度为行数+1,其中row_ptr[i]和row_ptr[i+1]之间的区间即为第i行的非零元素范围。这种设计的核心优势在于:通过行偏移量即可快速定位任意行的数据起始位置,无需遍历索引。
| 数组 | 长度 | 示例(3×4矩阵,非零值5个) |
|---|---|---|
| values | nnz | [1.5, 2.0, 3.2, 4.1, 5.5] |
| column_indices | nnz | [0, 2, 1, 3, 2] |
| row_ptr | rows+1 | [0, 2, 3, 5] |
判断方法与操作流程
选择CSR格式前,需确认以下条件:①矩阵行数远大于列数,且行内非零元素分布相对均匀;②推理时主要执行稀疏矩阵乘稠密向量(SpMV)操作,且按行并行计算;③目标硬件支持对row_ptr的随机访问。操作流程为:先统计每行非零个数,计算前缀和生成row_ptr,再按行顺序填充values和column_indices。
案例拆解:全连接层权重压缩
假设一个TinyML语音识别模型中的全连接层,输入维度512,输出维度256,经过结构化剪枝后稀疏度为92%。原始稠密存储需要512×256×4字节=512KB。采用CSR后,非零值约10,486个,values占40KB,column_indices占40KB(int16),row_ptr占1KB,总计约81KB,压缩比达到6.3:1。在Cortex-M7内核上,配合CMSIS-DSP的稀疏矩阵函数,推理耗时从原始的12.3ms降至4.1ms。但需要注意,如果行内非零值分布极不均匀(如某行只有1个非零,另一行有50个),会导致并行线程负载失衡。
4.2 CSC(Compressed Sparse Column)格式:列访问优化的镜像方案
核心原理与适用场景
CSC与CSR完全对称,按列压缩存储,使用values、row_indices和col_ptr三个数组。其优势在于按列访问时具有极佳的缓存局部性。在TinyML中,CSC常用于以下场景:①权重矩阵需要与输入特征图按列方向做点积;②模型部署在具有列优先存储架构的DSP上;③需要对权重进行列级量化或剪枝操作。
实战对比:CSR vs CSC
| 维度 | CSR | CSC |
|---|---|---|
| 行访问 | O(1)定位,缓存友好 | 需遍历col_ptr |
| 列访问 | 需遍历所有行 | O(1)定位 |
| 典型应用 | SpMV、行并行 | 列并行、稀疏矩阵乘稀疏矩阵 |
| 内存开销 | row_ptr(rows+1)+col_idx(nnz) | col_ptr(cols+1)+row_idx(nnz) |
4.3 COO(Coordinate Format)格式:最直观但索引开销最大
核心原理
COO格式直接存储每个非零值的三元组(行号、列号、值),三个数组等长。它的最大优点是构建简单、插入删除方便,非常适合在模型剪枝过程中动态修改权重。但缺点也很明显:索引存储开销大(每个非零值需要两个整数索引),且无法直接支持高效的随机行/列访问。
适用场景与限制
在TinyML中,COO通常作为中间转换格式使用。例如,PyTorch的稀疏张量默认采用COO格式,在导出为TFLite或ONNX时,工具链会将COO转换为CSR或BCSR。如果直接在MCU上使用COO,每个非零值需要额外8字节(两个int32索引),对于高稀疏度矩阵,索引开销可能超过值本身的存储。
// COO格式示例:3×4矩阵,5个非零值
int row[] = {0, 0, 1, 2, 2};
int col[] = {0, 2, 1, 3, 2};
float val[] = {1.5f, 2.0f, 3.2f, 4.1f, 5.5f};
// 访问第i个非零元素:row[i], col[i], val[i]
4.4 BCSR(Block Compressed Sparse Row)格式:规则块结构的利器
核心原理
BCSR将矩阵划分为固定大小的块(如2×2、4×4),仅存储包含非零元素的块。它使用block_values、block_column_indices和row_ptr三个数组,但每个“非零”元素实际上是一个小矩阵块。这种格式特别适合结构化剪枝后的权重——当剪枝以块为单位进行时,非零元素天然聚集在规则块中。
判断方法与性能分析
选择BCSR需满足:①非零元素呈现明显的块状聚集,而非随机分布;②目标硬件支持块矩阵运算指令(如ARM的DOT指令或RISC-V的向量扩展);③块大小与硬件寄存器宽度匹配。以4×4块为例,相比CSR,BCSR的列索引数量减少为原来的1/4,同时可以利用SIMD指令一次性完成4×4矩阵乘。
| 块大小 | 索引开销/非零值 | 适合硬件 | 典型压缩率提升 |
|---|---|---|---|
| 2×2 | 1/4列索引 | 通用MCU | 15-25% |
| 4×4 | 1/16列索引 | ARM Cortex-M55/M85 | 30-40% |
| 8×8 | 1/64列索引 | 带矩阵加速器的NPU | 45-55% |
案例:卷积核的BCSR转换
考虑一个3×3卷积,输出通道32,输入通道16。经过N:M剪枝(每4个连续权重保留2个)后,非零模式呈现2×1或1×2的块状。直接使用CSR需要存储每个非零值的列索引,而采用2×2块BCSR后,块数量减少约60%。在STM32F746上测试,BCSR格式的卷积推理速度比CSR快1.8倍,主要得益于减少了索引查找次数和更好的数据复用。
4.5 Blocked-Ellpack格式:面向SIMD的定长存储方案
核心原理
Blocked-Ellpack(BELLPACK)结合了Ellpack和BCSR的优点。它将矩阵按块划分,但每个块行(block row)内使用定长数组存储所有块,不足部分用零块填充。这种“定长”特性使得硬件可以预计算内存偏移,非常适合向量化加载和SIMD并行。与BCSR相比,BELLPACK不需要row_ptr数组,而是通过固定的块行宽度计算偏移,减少了间接寻址开销。
适用场景与实现要点
BELLPACK最适合以下条件:①块行内非零块数量相对均匀;②目标硬件支持向量加载(如NEON或Helium);③需要极低延迟的推理场景。实现时需注意:块行宽度的选择直接影响内存浪费率——如果某行只有1个非零块,而块行宽度设为8,则浪费87.5%的块存储。
// BELLPACK格式示意(块大小2×2,块行宽度=3)
// block_values: 按块行存储,每行固定3个块
// block_col_indices: 每个块对应的列块号
// 第0块行: [块(0,0), 块(0,2), 零块]
// 第1块行: [块(1,1), 零块, 零块]
对比总结与选型决策
| 格式 | 索引开销 | 行访问 | 列访问 | SIMD友好 | 构建难度 | 典型场景 |
|---|---|---|---|---|---|---|
| CSR | 中 | 优 | 差 | 中 | 低 | 通用SpMV |
| CSC | 中 | 差 | 优 | 中 | 低 | 列并行计算 |
| COO | 高 | 差 | 差 | 差 | 最低 | 动态剪枝/转换中间格式 |
| BCSR | 低 | 优 | 中 | 优 | 中 | 结构化剪枝权重 |
| BELLPACK | 最低 | 优 | 中 | 最优 | 高 | SIMD加速器/向量DSP |
4.6 常见误区与工程复盘
误区二:忽略硬件特性盲目选择格式。例如,在无SIMD指令的Cortex-M0上使用BELLPACK,反而因内存对齐要求导致额外开销。
误区三:转换过程中丢失数据局部性。CSR按行存储,但如果推理时输入特征图按列访问,应转置为CSC。
复盘:一个真实的声音分类模型部署
某智能音箱端侧唤醒词模型,包含3个卷积层和2个全连接层,原始大小1.2MB。初始使用CSR格式部署在Cortex-M4上,推理耗时38ms,内存占用420KB。分析发现:第一层卷积权重稀疏度仅75%,CSR收益有限;而全连接层稀疏度97%,但非零值分布随机,导致CSR的列索引缓存命中率低。优化方案:对卷积层保持稠密格式但使用int8量化,对全连接层改用BCSR(块大小2×2),最终内存降至280KB,推理耗时降至21ms。关键教训:不同层应根据自身稀疏特征选择不同格式,而非全模型统一。