无论是非结构化还是结构化剪枝技术,都会在模型权重矩阵中引入稀疏性。然而,仅仅将权重设为零并不能自动转化为推理过程中延迟的降低或计算成本的减少。实现实际的性能提升需要软件栈的明确支持,特指编译器和推理运行时,来识别并优化处理这种稀疏性。如果没有这种支持,涉及零权重的计算仍会执行,不会带来任何加速。稀疏性的识别和表示软件栈的首要步骤是识别并表示剪枝引入的稀疏性。非结构化稀疏性: 这涉及权重矩阵中任意分布的零元素。常见的表示格式有压缩稀疏行 (CSR)、压缩稀疏列 (CSC) 或坐标列表 (COO) 等。这些格式仅存储非零值及其索引,与密集表示相比,能大幅减少内存占用,尤其在稀疏度较高时。然而,它们在计算过程中会引入索引查找的开销。结构化稀疏性: 这涉及移除整个块、通道、滤波器或注意力头,产生可预测、规则的零模式(例如,整行或整列)。结构化稀疏性通常通过修改模型架构定义(例如,减少通道或头的数量)或使用块稀疏格式来隐式表示,这些格式比逐元素稀疏格式更紧凑地表示较大的零区域。编译器针对稀疏性的优化优化技术包括:稀疏格式选择: 编译器可能分析稀疏模式和密度,以选择适用于目标硬件的最有效的稀疏存储格式(CSR、CSC、COO、块稀疏等)。稀疏核生成: 编译器不使用标准密集矩阵乘法例程,而是生成或选择专门针对稀疏矩阵操作设计的核。这些核旨在跳过涉及零的计算。对于结构化稀疏性,这可能涉及生成在较小、密集子矩阵上操作的代码。对于非结构化稀疏性,这通常涉及基于存储索引的间接内存访问。指令选择: 如果目标硬件具有稀疏计算的专用指令(例如,某些加速器或未来的CPU/GPU指令集),编译器会将稀疏操作映射到这些指令上。内存访问优化: 编译器尝试优化稀疏格式的内存访问模式,这些模式可能不规则并导致缓存未命中。可以应用数据重排序或分块等技术,尽管它们的有效性很大程度上取决于稀疏模式和硬件架构。运算符融合: 类似于密集模型编译,编译器可以融合涉及稀疏张量的操作,以减少内存流量和核启动开销。例如,将稀疏矩阵乘法与后续的激活函数融合。稀疏操作的运行时执行推理运行时环境,例如ONNX Runtime、TensorRT、TensorFlow Lite和PyTorch JIT,执行编译后的模型图。这些环境在稀疏模型操作中扮演着重要角色,其功能包括:库集成: 运行时通常链接到高度优化的数值计算库,例如用于CPU的英特尔MKL(数学核心库)或用于GPU的英伟达cuSPARSE。这些库提供常见稀疏线性代数操作(SpGEMM - 稀疏通用矩阵乘法)的高效实现。核分派: 运行时根据张量的表示以及可能的稀疏度,选择合适的计算核(密集或稀疏)。通常存在一个阈值,低于该阈值时使用密集核会更快,因为稀疏格式和核相关的开销。内存管理: 运行时负责高效管理稀疏张量的非零值和相关的索引元数据的内存。格式转换: 有时,如果执行图中的不同操作符需要不同的表示,运行时可能需要不同稀疏格式之间或在稀疏与密集格式之间进行转换。这种转换会带来开销。digraph G { rankdir=LR; node [shape=box, style=rounded, fontname="sans-serif", color="#495057", fillcolor="#e9ecef", style="filled,rounded"]; edge [color="#868e96", fontname="sans-serif", fontsize=10]; PrunedModel [label="剪枝后的LLM\n(稀疏权重)"]; Compiler [label="编译器\n(例如,XLA, TVM, Glow)"]; Runtime [label="推理运行时\n(例如,TensorRT, ONNX Runtime)"]; Hardware [label="硬件\n(CPU, GPU, NPU)"]; PrunedModel -> Compiler [label=" 模型定义\n 稀疏模式"]; Compiler -> Runtime [label=" 优化后的执行计划\n 稀疏核 / 指令"]; Runtime -> Hardware [label=" 核执行\n 稀疏库调用 (cuSPARSE, MKL)"]; Hardware -> Runtime [label=" 计算结果"]; subgraph cluster_compiler { label = "编译阶段"; bgcolor="#dee2e6"; Compiler; node [shape=ellipse, fillcolor="#ced4da"]; SparsityAnalysis [label="稀疏性分析"]; FormatSelection [label="格式选择 (CSR/CSC/块)"]; KernelGeneration [label="稀疏核生成"]; MemOpt [label="内存优化"]; Compiler -> SparsityAnalysis [style=dotted]; Compiler -> FormatSelection [style=dotted]; Compiler -> KernelGeneration [style=dotted]; Compiler -> MemOpt [style=dotted]; } subgraph cluster_runtime { label = "执行阶段"; bgcolor="#dee2e6"; Runtime; node [shape=ellipse, fillcolor="#ced4da"]; KernelDispatch [label="核分派"]; LibraryCall [label="稀疏库调用"]; MemManage [label="内存管理"]; Runtime -> KernelDispatch [style=dotted]; Runtime -> LibraryCall [style=dotted]; Runtime -> MemManage [style=dotted]; } }概述了从剪枝后的模型定义到通过编译和运行时在硬件上执行,稀疏性是如何处理的。挑战与考量有效处理稀疏性并不简单:开销: 稀疏格式需要额外的索引存储空间,稀疏核通常涉及间接内存访问和控制流开销。这种开销有时会抵消跳过零值计算的好处,尤其在稀疏度较低时(< 50-70%)。硬件支持: 性能提升高度依赖于目标硬件高效执行稀疏操作的能力。GPU,特别是带有稀疏张量核心(NVIDIA Ampere架构中引入)等专用单元的GPU,相对于CPU,通常在稀疏矩阵乘法方面显示出更好的加速,尤其对于非结构化稀疏性。结构化稀疏性通常能更高效地映射到现有密集硬件能力。稀疏度阈值: 在稀疏度方面,通常存在一个“盈亏平衡点”。低于此阈值,使用密集核可能仍然更快,因为稀疏计算的开销。该阈值根据硬件、软件栈、矩阵维度和稀疏模式而异。结构化与非结构化: 结构化剪枝通常带来更可预测和显著的性能提升,因为产生的稀疏模式是规则的,并且编译器和硬件更容易优化。加速高度非结构化、细粒度的稀疏性仍是一项挑战,通常需要非常高的稀疏度(>90%)才能有效。了解编译器和运行时如何与稀疏表示交互对于选择剪枝策略很重要。虽然非结构化剪枝理论上可能实现更高的压缩比,但结构化剪枝通常提供更可靠的推理加速,因为与现有硬件和软件优化能力有更好的匹配。评估端到端性能,考虑特定的编译器、运行时和目标硬件,对评估任何剪枝方法的实际效益是必要的。