在对模型进行量化时,我们使用比例因子 $s$ 和零点 $z$ 将高精度浮点数映射到低精度整数。此时会有一个基本问题:我们应该对整个张量(例如一层中的所有权重)使用相同的 $s$ 和 $z$,还是对张量的不同部分使用不同的参数?这一选择决定了量化粒度。粒度级别对模型压缩、推理速度和潜在准确度损失之间的权衡有显著影响。逐张量量化最简单的方法是逐张量量化。在这种方法中,计算一个单独的比例因子 $s$ 和一个单独的零点 $z$,并将其应用于整个张量中的所有值。例如,如果考虑线性层的权重矩阵,我们将找到该矩阵中所有权重的最小值和最大值,以确定一对 $(s, z)$ 值。$$ \text{量化张量} = \text{裁剪}\left(\text{四舍五入}\left(\frac{\text{原始张量}}{s}\right) + z, Q_{\min}, Q_{\max}\right) $$其中 $s$ 和 $z$ 是根据 原始张量 的全局最小值和最大值计算得出。优点:简洁: 易于实现和管理。额外开销小: 每个张量只需存储一个 $s$ 和一个 $z$,存储成本可以忽略不计。缺点:对范围敏感: 如果张量包含少量大的异常值,整体范围 [最小值, 最大值] 会变得非常宽。这会迫使比例因子 $s$ 变大,可能导致大多数较小、出现频率较高的值只映射到几个整数级别,从而损失精度并可能大幅损害准确度。digraph G { rankdir=LR; node [shape=plaintext, fontsize=10]; subgraph cluster_tensor { label = "权重张量(例如,4x4)"; style=dashed; bgcolor="#e9ecef"; T [label=< <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR><TD>-1.2</TD><TD>0.5</TD><TD>8.1</TD><TD>-0.3</TD></TR> <TR><TD>0.8</TD><TD>-0.1</TD><TD>0.9</TD><TD>1.5</TD></TR> <TR><TD>-0.5</TD><TD>1.1</TD><TD>-0.8</TD><TD>0.2</TD></TR> <TR><TD>1.3</TD><TD>-0.9</TD><TD>0.4</TD><TD>-1.1</TD></TR> </TABLE> >]; } subgraph cluster_params { label = "逐张量参数"; style=dashed; bgcolor="#fff3bf"; P [label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> <TR><TD BGCOLOR="#ffe066">全局最小值:-1.2</TD></TR> <TR><TD BGCOLOR="#ffe066">全局最大值:8.1</TD></TR> <TR><TD> → 单一 (s, z) </TD></TR> </TABLE> >]; } T -> P [style=invis]; }一组量化参数 (s, z) 是从整个张量的最小值和最大值中得出的。大值 8.1 明显拓宽了范围。逐通道量化为了应对逐张量量化的局限性,特别是在卷积层或线性层等中,我们可以使用逐通道量化。对于线性层(形状为 [output_features, input_features])或卷积层(形状为 [output_channels, input_channels, height, width])中的权重张量,这通常意味着为每个输出通道计算单独的一对 $(s, z)$ 值。想象一下沿着输出通道维度切分权重张量。对于每个切片(代表连接到一个输出神经元或通道的权重),我们独立地找到最小值和最大值,并计算其特定的 $s$ 和 $z$。优点:准确度提升: 通过适应每个通道内值的特定范围,这种方法通常比逐张量量化更好地保持准确度,因为一个通道中的异常值不会影响其他通道的量化。适用范围广: 这是一种量化许多标准网络架构中权重的常用且有效的方法。缺点:额外开销增加: 现在需要为每个输出通道存储一对 $(s, z)$,与逐张量相比,存储开销有所增加(但通常仍相对于原始权重大小较小)。复杂度略高: 需要在量化/反量化期间进行通道级别的参数计算和应用。digraph G { rankdir=LR; node [shape=plaintext, fontsize=10]; subgraph cluster_tensor { label = "权重张量(例如,4x4)"; style=dashed; bgcolor="#e9ecef"; T [label=< <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR><TD BGCOLOR="#a5d8ff">-1.2</TD><TD BGCOLOR="#a5d8ff">0.5</TD><TD BGCOLOR="#a5d8ff">8.1</TD><TD BGCOLOR="#a5d8ff">-0.3</TD></TR> <TR><TD BGCOLOR="#96f2d7">0.8</TD><TD BGCOLOR="#96f2d7">-0.1</TD><TD BGCOLOR="#96f2d7">0.9</TD><TD BGCOLOR="#96f2d7">1.5</TD></TR> <TR><TD BGCOLOR="#ffc9c9">-0.5</TD><TD BGCOLOR="#ffc9c9">1.1</TD><TD BGCOLOR="#ffc9c9">-0.8</TD><TD BGCOLOR="#ffc9c9">0.2</TD></TR> <TR><TD BGCOLOR="#ffd8a8">1.3</TD><TD BGCOLOR="#ffd8a8">-0.9</TD><TD BGCOLOR="#ffd8a8">0.4</TD><TD BGCOLOR="#ffd8a8">-1.1</TD></TR> </TABLE> >]; } subgraph cluster_params { label = "逐通道参数"; style=dashed; bgcolor="#d0bfff"; P [label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> <TR><TD BGCOLOR="#a5d8ff">通道 1:最小值=-1.2,最大值=8.1 → (s1, z1)</TD></TR> <TR><TD BGCOLOR="#96f2d7">通道 2:最小值=-0.1,最大值=1.5 → (s2, z2)</TD></TR> <TR><TD BGCOLOR="#ffc9c9">通道 3:最小值=-0.8,最大值=1.1 → (s3, z3)</TD></TR> <TR><TD BGCOLOR="#ffd8a8">通道 4:最小值=-1.1,最大值=1.3 → (s4, z4)</TD></TR> </TABLE> >]; } T -> P [style=invis]; }为每个通道(本例中为行)计算单独的量化参数 (s, z)。由于 8.1,通道 1 的范围仍然很宽,但其他通道(例如通道 2、3、4)使用适合其特定值的范围,从而保留了更多精度。逐组量化为了进一步细化粒度,逐组量化将每个通道(或行/列)内的元素细分为更小的组,并为每个组计算单独的 $(s, z)$ 参数。对于形状为 [output_features, input_features] 的权重矩阵,分组通常沿 input_features 维度应用。常见的组大小为 32、64 或 128。例如,如果 input_features 为 1024 且组大小为 128,则每个输出通道的权重向量将被分成 $1024 / 128 = 8$ 个组,每个组将有自己的 $(s, z)$。优点:准确度潜力最高: 提供对局部值分布最精细的适应。这对于极低精度量化(例如 INT4、INT3)尤其有利,因为在这种情况下,捕捉局部变化对于最大限度地减少准确度损失非常重要。像 GPTQ 这样的技术通常依赖于逐组量化。处理局部变化: 可以有效处理即使在单个通道内值范围也变化明显的张量。缺点:最大额外开销: 需要存储最大数量的 $(s, z)$ 参数,与逐通道或逐张量相比,元数据开销大幅增加。复杂度增加: 由于在组级别管理参数,量化和推理期间的计算变得更复杂。digraph G { rankdir=LR; node [shape=plaintext, fontsize=10]; subgraph cluster_tensor { label = "权重张量(聚焦单个通道)"; style=dashed; bgcolor="#e9ecef"; T [label=< <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR><TD BGCOLOR="#a5d8ff" width="30">-1.2</TD><TD BGCOLOR="#a5d8ff" width="30">0.5</TD><TD BGCOLOR="#74c0fc" width="30">8.1</TD><TD BGCOLOR="#74c0fc" width="30">-0.3</TD><TD BGCOLOR="#4dabf7" width="30">0.8</TD><TD BGCOLOR="#4dabf7" width="30">-0.1</TD><TD BGCOLOR="#339af0" width="30">0.9</TD><TD BGCOLOR="#339af0" width="30">1.5</TD></TR> <TR><TD>...</TD><TD>...</TD><TD>...</TD><TD>...</TD><TD>...</TD><TD>...</TD><TD>...</TD><TD>...</TD></TR> </TABLE> >]; } subgraph cluster_params { label = "逐组参数(组大小 = 2)"; style=dashed; bgcolor="#c0eb75"; P [label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> <TR><TD BGCOLOR="#a5d8ff">通道 1,组 1:最小值=-1.2,最大值=0.5 → (s11, z11)</TD></TR> <TR><TD BGCOLOR="#74c0fc">通道 1,组 2:最小值=-0.3,最大值=8.1 → (s12, z12)</TD></TR> <TR><TD BGCOLOR="#4dabf7">通道 1,组 3:最小值=-0.1,最大值=0.8 → (s13, z13)</TD></TR> <TR><TD BGCOLOR="#339af0">通道 1,组 4:最小值=0.9,最大值=1.5 → (s14, z14)</TD></TR> <TR><TD>...(其他通道的参数)</TD></TR> </TABLE> >]; } T -> P [style=invis]; }在单个通道(行)内,权重被分为多个组(此处为演示目的,大小为 2)。每个组都有自己的参数 (s, z),从而能够更精细地适应值范围。异常值 8.1 现在只影响组 2 的参数。粒度选择量化粒度的选择取决于几个因素:目标精度: 对于侵略性较低的量化(例如 INT8),逐通道通常能提供良好的平衡。对于极低精度(INT4、INT3),逐组量化通常是保持可接受准确度所必需的。张量类型: 权重通常受益于更细的粒度(逐通道或逐组),因为它们的分布在通道之间甚至通道内部都可能明显不同。激活有时是逐张量量化的,尽管动态量化(逐令牌,通常是该令牌内的逐张量)也很常见。硬件/软件支持: 目标推理引擎或硬件可能对特定粒度有优化例程。额外开销容忍度: 更细的粒度会增加比例因子和零点所需的存储空间,并可能在推理设置或反量化步骤中增加计算开销。准确度要求: 最重要的因素。如果像逐张量这样更简单的方法导致模型在下游任务上的性能下降到不可接受的程度,那么更细的粒度就变得有必要。实践中,逐通道量化是权重的常见默认选择,而当需要降低精度或逐通道量化不足时,则采用逐组量化。逐张量最简单,但通常保留用于激活或对准确度影响最小的情况。理解这些选择能帮助您在对大语言模型进行量化时做出明智的决定。