DLAI-模型量化笔记-全-
DLAI 模型量化笔记(全)
001:课程介绍 🎯
在本课程中,我们将与Hugging Face合作,深入探讨模型量化的核心技术模块。量化是压缩大语言模型及其他模型的关键技术,属于AI软件栈的重要组成部分。我们将从零开始实现最常见的线性量化变体,并学习如何应用不同的量化粒度。课程结束时,你将能够构建自己的量化器,并使用每通道线性量化方案将任何模型量化为8位精度。
课程核心内容概述
上一段我们介绍了本课程的目标,接下来详细看看我们将要学习的具体内容。
以下是本课程涵盖的核心主题:
- 线性量化实现:从零实现对称与非对称两种主要量化模式。其核心区别在于压缩算法是否将原始表示中的零点映射到压缩表示中的零点。
- 公式:
量化值 = 舍入(原始值 / 缩放比例) + 零点偏移
- 公式:
- 量化粒度:使用PyTorch实现每张量、每通道和每组量化。这决定了你一次量化模型的多大部分。
- 代码示例:`# 伪代码示意不同粒度
每张量量化:整个权重张量使用同一组量化参数
scale, zero_point = calibrate(entire_weight_tensor)每通道量化:权重张量的每个通道使用独立的量化参数
scales, zero_points = calibrate_per_channel(weight_tensor)` - 构建量化器:构建一个量化器,使用之前学到的每通道量化方案,将任何模型量化为8位精度。该方法适用于文本、视觉、音频乃至多模态模型。
- 权重打包:学习并实现权重打包与解包算法。这是当前存储4位或2位低精度权重的常见方法,可将多个低精度张量打包到一个更大的8位张量中,无需分配额外内存。
- 前沿挑战与方案:探讨量化大模型(如LLM)时的其他挑战,回顾当前最先进的、旨在实现无损性能量化的方法,并了解如何在Hugging Face生态系统中进行操作。
讲师介绍
很高兴向大家介绍本课程的讲师。Eunice Dgoda是Hugging Face的机器学习工程师,在开源团队工作,涉足Transformers、PEFT等多种开源工具。Mark Sun同样是Hugging Face的机器学习工程师,在开源团队为Transformers、Accelerate等库做出贡献。他们二人都深度参与量化工作,致力于让大模型更易于社区使用。
学习价值与致谢
量化是当今大模型实际应用中的重要环节。深入理解它将帮助你更有效地构建、部署和使用模型。本课程的诞生离不开许多人的努力,特别感谢Hugging Face团队对课程内容的评审,以及Hugging Face社区对开源模型和量化方法的贡献。DeepLearning.AI的Eddie Shu也为此做出了贡献。



量化是一个技术性较强的主题。完成本课程后,希望你能够深入理解它,并可以自信地表示自己掌握了模型压缩技术,不再为此担忧。

让我们进入下一个视频,正式开始学习。
002:课程概述 🎯
在本节课中,我们将对模型量化技术进行一个全面的概述。量化方法用于使模型变得更小,从而让更广泛的AI社区能够更容易地使用它们。我们将了解量化是什么以及它是如何工作的。
课程回顾与引言
在上一节课程中,我们了解到量化是一个令人兴奋的领域,因为它使我们能够压缩模型,提高其可及性。本节课程中,我们将学习如何从零开始实现一些量化基本操作,并构建我们自己的模型量化器。同时,我们也会探讨在低比特量化(如权重打包)中可能遇到的一些挑战。
首先,让我们快速回顾一下在第一门课程中学到的内容。
第一门课程要点回顾
在第一门课程的介绍部分,我们列举了可用于压缩模型的通用技术。
以下是主要技术概览:
- 量化:旨在以更低的精度表示模型的参数。
- 知识蒸馏:你可以使用更大的教师模型的输出来训练一个学生模型。
- 剪枝:你可以简单地移除模型内部的一些连接(即移除权重),使模型更加稀疏。
我们还介绍了机器学习中常见的数据类型,例如 int8 或 float。我们使用 Hugging Face 的量化库,用几行代码执行了线性量化。最后,我们概述了量化如何在不同用例(如大语言模型微调)中发挥作用。
接下来,让我们看看在本门课程中具体要学习哪些内容。
本课程核心内容
本课程将深入探讨以下三个核心部分。
1. 深入线性量化内部原理 🔍


首先,我们将一起深入线性量化的内部原理,并从零开始实现其一些变体。
以下是主要的量化方案:
- 逐通道量化
- 逐张量量化
- 逐组量化
我们将研究每种方法的优点和缺点,并观察它们对一些随机张量的影响。
2. 构建自定义量化器 ⚙️
接下来,我们将尝试构建自己的量化器,使用前面介绍的量化方案之一,将任何模型量化为8位精度。
需要注意的是,量化方案与模型模态无关。这意味着只要你的模型包含线性层,你就可以将其应用于任何模型。从技术上讲,你将能够使用你的量化器来量化视觉、文本、音频甚至多模态模型。
3. 应对极端量化挑战 🧩
最后,我们将通过了解更多关于极端量化(如权重打包)时可能面临的挑战来结束本课程。这是当前常见的挑战。
截至我们讨论时,PyTorch 尚未原生支持2位或4位精度的权重。解决此问题的一种方法是将这些低精度权重打包到更高精度的张量中,例如 int8。我们将深入探讨这一点,并一起实现打包和解包算法。
我们将通过探讨量化大型模型(如LLMs)时的其他常见挑战,并一起回顾一些最先进的LLM量化方法来结束本课程。
现在,让我们直接开始,一起压缩一些模型吧!
总结

本节课中,我们一起回顾了模型压缩的背景知识,并概述了本门课程的学习路线。我们将从实现量化基本操作开始,逐步构建完整的量化器,并最终攻克低比特量化中的实际挑战。准备好迎接深度量化的实践之旅了吗?让我们开始吧。
003:张量的量化与反量化 🧮


在本节课中,我们将深入学习线性量化的理论。你将从头开始实现线性量化的非对称变体,并了解缩放因子和零点这两个核心概念。
概述
量化是指将一个大集合的值映射到一个更小集合的过程。本课程将专注于线性量化。我们将学习如何将一个浮点张量量化为低精度格式(如 int8),以及如何将其反量化回原始表示。理解这个过程是掌握模型量化技术的基础。
线性量化理论
上一节我们介绍了量化的基本概念,本节中我们来看看线性量化的具体理论。
线性量化使用线性映射,将高精度范围(例如 float32)映射到低精度范围(例如 int8)。线性量化涉及两个关键参数:
- 缩放因子:通常用
S表示,存储在与原始张量相同的数据类型中。 - 零点:通常用
Z表示,存储在与量化后张量相同的数据类型中。
量化值 Q 与原始值 R 之间的关系可以用以下公式表示:
R = S * (Q - Z)
根据这个公式,我们可以推导出反量化公式:
Q = round(R / S + Z)
实现量化函数
理解了理论公式后,现在让我们动手实现量化函数。我们将使用 PyTorch 框架。
首先,确保已安装必要的库。如果在本机运行,请执行:
pip install torch
以下是量化函数 linear_quantize_with_scale_and_zeropoint 的实现步骤:
- 计算缩放和偏移后的张量:根据公式
R / S + Z进行计算。 - 四舍五入:对结果进行取整,因为量化值必须是整数。
- 数值裁剪:确保结果在目标数据类型的表示范围内(例如
int8的 -128 到 127)。 - 类型转换:将结果转换为目标数据类型(如
torch.int8)。
以下是完整的代码实现:
import torch
def linear_quantize_with_scale_and_zeropoint(tensor, scale, zero_point, dtype=torch.int8):
# 步骤1: 计算缩放和偏移后的张量
scaled_and_shifted_tensor = tensor / scale + zero_point
# 步骤2: 四舍五入
rounded_tensor = torch.round(scaled_and_shifted_tensor)
# 步骤3: 数值裁剪到目标数据类型的范围
q_min = torch.iinfo(dtype).min
q_max = torch.iinfo(dtype).max
quantized_tensor = rounded_tensor.clamp(q_min, q_max)
# 步骤4: 类型转换
quantized_tensor = quantized_tensor.to(dtype)
return quantized_tensor
测试量化函数
现在,让我们用一个示例张量来测试我们实现的函数。我们将暂时为缩放因子和零点赋予随机值。
# 定义测试张量
test_tensor = torch.tensor([[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0]], dtype=torch.float32)
# 暂时使用随机缩放因子和零点
scale = 3.5
zero_point = -70
# 调用量化函数
quantized_tensor = linear_quantize_with_scale_and_zeropoint(test_tensor, scale, zero_point)
print("量化后的张量:")
print(quantized_tensor)
print(f"数据类型:{quantized_tensor.dtype}")
实现反量化函数
得到量化张量后,我们需要能够将其恢复为原始表示。这个过程称为反量化。
根据公式 R = S * (Q - Z),我们可以轻松实现反量化。需要注意的是,在计算 (Q - Z) 时,必须先将 Q 转换为浮点类型,以避免整数运算中的溢出或下溢问题。
以下是反量化函数 linear_dequantize 的实现:
def linear_dequantize(quantized_tensor, scale, zero_point):
# 将量化张量转换为浮点型后进行反量化计算
dequantized_tensor = scale * (quantized_tensor.float() - zero_point)
return dequantized_tensor
# 测试反量化
dequantized_tensor = linear_dequantize(quantized_tensor, scale, zero_point)
print("\n反量化后的张量:")
print(dequantized_tensor)
计算量化误差
为了评估量化过程的精度,我们可以计算量化误差,即原始张量与反量化后张量之间的绝对差值。

# 计算量化误差
quantization_error = torch.abs(test_tensor - dequantized_tensor)
print("\n量化误差张量:")
print(quantization_error)
print(f"\n平均量化误差:{quantization_error.mean().item():.4f}")
在这个例子中,由于我们为缩放因子和零点赋予了随机值,量化误差可能会比较大。这引出了下一个关键问题:如何找到最优的缩放因子和零点,以最小化量化误差。

总结


本节课中我们一起学习了线性量化的核心理论。我们定义了缩放因子 S 和零点 Z 这两个关键参数,并理解了它们与量化值 Q、原始值 R 之间的关系(R = S * (Q - Z))。我们从头实现了量化函数和反量化函数,并计算了量化误差。目前,我们使用了随机的 S 和 Z,导致误差较高。在下一节中,我们将探讨如何计算最优的缩放因子和零点,以显著降低量化误差,使量化后的模型保持高性能。
004:获取缩放因子与零点 🎯
在本节课中,我们将学习如何确定线性量化中的两个核心参数:缩放因子(Scale) 和 零点(Zero Point)。它们是连接原始浮点数值与量化后整数值的关键。我们将从理论推导开始,然后通过代码实现一个完整的量化函数。
概述

上一节我们介绍了线性量化的基本公式 Q = round(R / S + Z)。本节中,我们将解决如何为给定的张量计算最优的缩放因子 S 和零点 Z。核心思想是让原始数据的最小值和最大值分别映射到量化范围的最小值和最大值。

理论推导
我们需要根据原始张量的极值来确定 S 和 Z。设原始张量 R 的最小值为 R_min,最大值为 R_max。设量化后的整数类型(如 int8)的最小值为 Q_min,最大值为 Q_max。

根据映射关系,我们得到两个方程:
R_min映射到Q_min:Q_min = round(R_min / S + Z)R_max映射到Q_max:Q_max = round(R_max / S + Z)
为了简化,我们通常忽略 round 操作进行推导,得到近似公式:
Q_min ≈ R_min / S + ZQ_max ≈ R_max / S + Z
由于我们有两个未知数 S 和 Z,以及两个方程,可以求解。将第二个方程减去第一个方程,可以消去 Z,从而解出缩放因子 S:
缩放因子公式:
S = (R_max - R_min) / (Q_max - Q_min)
得到 S 后,将其代入第一个方程,即可解出零点 Z:
零点公式:
Z = Q_min - R_min / S
需要注意的是,计算出的 Z 需要四舍五入并转换为与量化值相同的数据类型(如 int)。Z 的设计目标是让原始数据中的 0 在量化后也能精确表示,这样在反量化时,0 值可以无损恢复。
零点越界处理
有时计算出的 Z 可能超出 [Q_min, Q_max] 的范围。为了避免溢出,我们需要进行截断:
- 如果
Z < Q_min,则令Z = Q_min。 - 如果
Z > Q_max,则令Z = Q_max。
代码实现



现在,让我们将理论转化为代码。我们将实现一个函数 get_q_scale_and_zero_point 来计算 S 和 Z,然后将其整合进一个完整的线性量化函数中。
首先,实现获取缩放因子和零点的函数。


import torch

def get_q_scale_and_zero_point(tensor, dtype=torch.int8):
"""
计算给定张量的量化缩放因子和零点。
参数:
tensor: 待量化的浮点张量。
dtype: 目标量化数据类型,默认为 torch.int8。
返回:
scale: 缩放因子 (浮点数)。
zero_point: 零点 (整数)。
"""
# 1. 获取量化数据类型的范围
q_min = torch.iinfo(dtype).min
q_max = torch.iinfo(dtype).max
# 2. 获取原始张量的范围
r_min = tensor.min().item()
r_max = tensor.max().item()
# 3. 计算缩放因子 S
scale = (r_max - r_min) / (q_max - q_min)
# 4. 计算零点 Z (初步)
zero_point = q_min - r_min / scale
# 5. 处理零点越界并转换为整数
if zero_point < q_min:
zero_point = q_min
elif zero_point > q_max:
zero_point = q_max
else:
zero_point = round(zero_point)
zero_point = int(zero_point)
return scale, zero_point

接下来,我们需要之前定义的量化与反量化函数。为了完整性,这里再次列出:

def linear_quantize_with_scale_and_zero_point(tensor, scale, zero_point, dtype=torch.int8):
"""使用给定的缩放因子和零点进行线性量化"""
quantized_tensor = torch.round(tensor / scale + zero_point).to(dtype)
return quantized_tensor


def linear_dequantize(quantized_tensor, scale, zero_point):
"""线性反量化"""
dequantized_tensor = scale * (quantized_tensor.float() - zero_point)
return dequantized_tensor

现在,我们可以将这些部分组合成一个完整的线性量化函数:


def linear_quantization(tensor, dtype=torch.int8):
"""
完整的线性量化函数。
参数:
tensor: 待量化的浮点张量。
dtype: 目标量化数据类型。
返回:
quantized_tensor: 量化后的张量。
scale: 使用的缩放因子。
zero_point: 使用的零点。
"""
# 1. 计算缩放因子和零点
scale, zero_point = get_q_scale_and_zero_point(tensor, dtype)
# 2. 执行量化
quantized_tensor = linear_quantize_with_scale_and_zero_point(tensor, scale, zero_point, dtype)
return quantized_tensor, scale, zero_point
示例与验证



让我们用一个随机张量来测试我们实现的量化器。
# 创建一个随机张量
random_tensor = torch.randn(4, 4)
print("原始张量:\n", random_tensor)
# 执行量化
quantized_tensor, scale, zero_point = linear_quantization(random_tensor)
print("\n量化后张量 (int8):\n", quantized_tensor)
print(f"\n缩放因子 S: {scale:.4f}")
print(f"零点 Z: {zero_point}")
# 执行反量化
dequantized_tensor = linear_dequantize(quantized_tensor, scale, zero_point)
print("\n反量化后张量:\n", dequantized_tensor)
# 计算量化误差 (均方误差)
quantization_error = ((dequantized_tensor - random_tensor) ** 2).mean().item()
print(f"\n量化误差 (MSE): {quantization_error:.6f}")

运行上述代码,你将看到量化后的张量、计算出的 S 和 Z,以及反量化结果。量化误差应该比使用随意选择的 S 和 Z 时小得多,这证明我们的方法是有效的。



总结


本节课中,我们一起学习了线性量化的核心步骤:计算缩放因子与零点。
- 我们从理论出发,推导了
S和Z的计算公式,其核心是让数据范围匹配。 - 我们实现了
get_q_scale_and_zero_point函数来计算这两个参数,并处理了零点越界的情况。 - 最后,我们将所有功能整合进
linear_quantization函数,形成了一个完整的、可用的线性量化工具。
现在你已经掌握了如何为任意张量自动计算合适的量化参数。建议你暂停视频,尝试用不同的输入张量测试这个量化器,观察其表现。



在下一课中,我们将深入线性量化的其他变体,如对称量化,并探讨不同的量化粒度(如每张量、每通道、每组量化)。我们还将学习如何对量化后的模型进行推理。
005:对称与非对称模式 🔄

在本节课中,我们将学习线性量化中的对称模式。我们还将实现不同粒度级别的量化,例如逐张量、逐通道和逐组量化。最后,我们将探讨如何在量化后的线性层上进行推理。让我们开始吧。

概述 📋
线性量化有两种主要模式。第一种是非对称模式,这是我们在上一课中已经实践过的,即将原始张量的最小值或最大值映射到量化范围的最小值或最大值。第二种是对称模式,这是本节课的重点,它将原始张量的负最大值和正最大值映射到量化范围的负最大值和正最大值。
对称模式详解 ⚖️
在对称模式中,我们不需要存储零点(zero point),因为它等于0。这是因为浮点数范围和量化范围都关于0点对称。因此,我们可以简化上一课的量化公式。
量化张量 Q 的计算公式为:
Q = round(T / S)
其中,缩放因子 S 的计算公式为:
S = R_max / Q_max
这里,R_max 是原始张量绝对值的最大值,Q_max 是量化数据类型的最大值。
代码实现 💻
现在,让我们通过代码来实现对称模式的线性量化。首先,我们需要导入必要的库。
import torch
接下来,我们定义一个函数来计算对称量化所需的缩放因子 S。
def get_q_scale_symmetric(tensor, dtype=torch.int8):
# 计算原始张量绝对值的最大值 R_max
r_max = tensor.abs().max().item()
# 获取量化数据类型的最大值 Q_max
q_max = torch.iinfo(dtype).max
# 计算缩放因子 S
scale = r_max / q_max
return scale
为了测试这个函数,我们创建一个随机张量并计算其缩放因子。
test_tensor = torch.randn(4, 4)
print("测试张量:", test_tensor)
scale = get_q_scale_symmetric(test_tensor)
print("计算得到的缩放因子:", scale)
现在,我们实现完整的对称线性量化函数。这个函数将返回量化后的张量和缩放因子。
def linear_q_symmetric(tensor, dtype=torch.int8):
# 获取缩放因子
scale = get_q_scale_symmetric(tensor, dtype)
# 使用上一课编写的辅助函数进行量化
# 假设该函数名为 linear_quantize_with_scale_zp
from helper import linear_quantize_with_scale_zp
quantized_tensor = linear_quantize_with_scale_zp(tensor, scale, zero_point=0, dtype=dtype)
return quantized_tensor, scale
让我们对测试张量应用这个函数。
quantized_tensor, scale = linear_q_symmetric(test_tensor)
为了评估量化效果,我们需要将量化后的张量反量化回浮点数,并计算量化误差。
# 反量化
from helper import linear_dequantize
dequantized_tensor = linear_dequantize(quantized_tensor, scale, zero_point=0)

# 计算并可视化量化误差
from helper import plot_quantization_error, quantization_error
plot_quantization_error(test_tensor, quantized_tensor, dequantized_tensor)
error = quantization_error(test_tensor, dequantized_tensor)
print("量化误差:", error)
模式对比与权衡 ⚖️
上一节我们介绍了对称模式的实现,本节我们来对比一下对称模式与非对称模式的权衡。
以下是两种模式的主要区别:
- 量化范围利用率:非对称模式能充分利用整个量化范围。而对称模式在原始数据范围偏向一侧时(例如,某层的输出总是正数),会导致部分量化范围被浪费,用于表示永远不会出现的值。
- 实现复杂度:对称模式更简单,因为它不需要计算和存储零点。
- 内存占用:对称量化不需要存储零点,因此在内存上略有优势。



在实践中,当我们进行8位量化时,通常使用对称模式。而当量化到更低的位数,如2、3或4位时,为了更充分地利用有限的表示范围,我们更常使用非对称量化。
总结 🎯

本节课中,我们一起学习了线性量化的对称模式。我们了解了其基本原理,即通过关于0点对称的映射来简化量化过程,无需零点。我们动手实现了计算缩放因子和进行对称量化的代码,并通过反量化和误差计算验证了量化效果。最后,我们对比了对称与非对称模式在范围利用率、复杂度和内存占用方面的权衡,并了解了它们在不同量化位数场景下的适用性。掌握这两种模式是深入理解模型量化的关键一步。
006:更细粒度带来更高精度

概述
在本节课中,我们将要学习量化粒度对模型精度的影响。我们将了解到,更细的量化粒度可以带来更高的精度,但也会占用更多内存。我们将通过一个简单的例子,回顾对称量化的过程,并比较不同粒度下的量化误差。
量化粒度与精度
量化粒度越细,量化结果就越精确。然而,这需要存储更多的量化参数,因此会占用更多内存。
在量化中,存在不同的粒度级别。我们有逐张量量化,但正如你所见,我们不必为整个张量使用相同的缩放因子和零点。
例如,我们可以为每个轴计算一个缩放因子和零点。这被称为逐通道量化。我们也可以选择一组N个元素来获取缩放因子和零点,并使用其自身的缩放因子和零点对每组进行量化。对于逐张量量化,这就是我们在之前实验中所做的。
回顾对称量化示例
让我们通过一个简单的例子来回顾一下。使用我们在上一个实验中使用的测试张量,这次对这个张量执行对称量化。我们将使用我们刚刚编写的 linear_q_symmetric 函数。
# 假设我们已经定义了 linear_q_symmetric 函数
quantized_tensor, scale = linear_q_symmetric(test_tensor)

我们将得到量化后的张量 quantized_tensor 和缩放因子 scale。这个 linear_q_symmetric 函数,我们只需要传入测试张量 test_tensor。

为了进行总结,你还需要将其反量化。我们将使用上一个实验中的 linear_dequantization 函数。
dequantized_tensor = linear_dequantization(quantized_tensor, scale, zero_point=0)
我们需要传入量化张量 quantized_tensor、缩放因子 scale 和零点 zero_point。但如你所记,对于对称量化,零点等于0。

现在我们有了绘制总结所需的一切。
结果分析
如图所示,量化效果相当好,数值非常接近。我们得到了量化误差张量,看起来相当不错。
让我们看一下量化误差,我们得到了2.5。如果你还记得上一个实验,当我们使用对称量化时,量化误差大约在1.5左右。

总结
本节课中,我们一起学习了量化粒度的重要性。更细的粒度(如逐通道量化)可以提高量化精度,但会增加内存开销。我们通过代码示例回顾了对称量化的过程,并观察了量化误差。理解这些权衡对于在实际应用中有效实施量化至关重要。
007:逐通道量化 🧮
在本节课中,我们将要学习逐通道量化(Per-Channel Quantization)的原理与实现。这是一种更精细的量化方法,它为张量的每个通道(例如矩阵的每一行或每一列)使用独立的缩放因子,从而减少异常值对整个张量量化精度的影响。
概述
上一节我们介绍了对称线性量化的基本概念。本节中我们来看看逐通道量化。这种方法的核心思想是,不再为整个张量使用单一的缩放因子和零点,而是为张量的每个通道(例如矩阵的每一行或每一列)分别计算并存储这些参数。这能有效隔离异常值的影响,通常能获得比逐张量量化更低的量化误差。
逐通道量化的原理
我们需要为每一行(如果沿行量化)或每一列(如果沿列量化)存储独立的缩放因子和零点。存储这些线性参数所需的内存非常小。在8位模型量化中,我们通常使用逐通道量化。
实现逐通道量化
现在让我们来编写逐通道量化的代码。为了简化工作,我们将自己限制在线性量化的对称模式中。因此,函数将被称为 linear_Q_symmetric_per_channel。
我们期望这个函数接收以下参数:张量、维度(即我们想沿行还是沿列量化,对于二维矩阵而言),并设置默认值等于 torch.int8。最后,我们期望得到量化后的张量和缩放因子。由于我们使用对称模式,因此不需要零点。
以下是实现步骤:
首先,定义一个测试张量,以便我们逐步理解代码。我们将使用之前定义的测试张量。
第一步是确定缩放因子张量的形状。由于我们进行逐通道量化,将会有多个缩放因子,我们需要创建一个张量来存储这些值。缩放因子张量的形状将取决于我们选择的维度。
如果我们要沿行量化,需要将维度设置为0;否则,如果我们要沿列量化,则需要将维度设置为1。
让我们检查输出维度。可以看到我们得到了3,确实我们需要三个缩放值:一个对应这些数字,一个对应这一行,最后一个对应这一行。
现在,我们可以使用 torch.zeros 创建缩放因子张量。这将创建一个形状为输出维度的张量,每个元素初始化为零。
现在,我们需要做的是遍历这些行中的每一行,并为每一行计算缩放因子。为此,我们将循环遍历输出维度。现在,我们需要获取每一行,例如第一行、第二行或第三行。为此,我们将使用 select 方法,并需要设置两个参数:维度和索引。

现在,为了确保正确,让我们检查一下子张量的样子。我们应该为每一行得到一个张量。确实,我们能够将每一行提取为一个张量。


现在,我们成功获取了子张量,接下来需要做的就是将 get_Q_scale_symmetric 函数应用于该子张量,以获取与该行相关的缩放因子,并将其存储在缩放因子张量中。
因此,我们需要在缩放因子张量的索引位置,设置该特定子张量的缩放因子。为此,我们将使用 get_Q_scale_symmetric 函数,并传递子张量。
现在,让我们检查缩放因子张量的样子。我们确实成功将与每一行相关的缩放因子存储在这个张量中。

现在我们已经存储了所有的缩放因子,我们需要进行一些处理,以重塑缩放因子张量的形状,使得当我们用原始张量除以缩放因子张量时,每一列都能被正确的缩放因子除。为此,我们定义缩放因子张量应具有的形状。
让我们看看这个缩放形状。它充满了1。然后,我们需要将缩放形状在索引 dim 处设置为 -1,这将给我们所需的形状。最后,我们需要使用 view 方法,利用我们刚刚定义的缩放形状来重塑缩放因子张量。
我们得到了以下缩放因子,这就是我们需要的缩放因子,以便能够将原始张量除以缩放因子张量,使得每一行被缩放因子的每个值除。我知道这有点复杂,因为它涉及到如何在PyTorch中实现张量除以张量。
让我们看一个例子,以理解 view 如何工作,以及如何以这种方式除张量,使得你能够除每一行或每一列。
假设我们有以下矩阵。并且我们有以下缩放因子,就像在前面的例子中一样。这个缩放因子的形状是3。我们可以重塑这个张量,使得第一个维度的大小为1,第二个维度可以包含其余部分。为此,我们可以使用 view 函数。缩放因子的形状是3。
我们可以使用 view 函数重塑它。例如,我们可以重塑它,使得第一个维度是1,第二个维度是3。正如预期的那样,我们得到了一个大小为1x3的张量。另一种方法是直接用 -1 替换3。这样做可以让你找到正确的形状,其中你放置了 -1。
你也可以重塑 S,使得第一个维度最终为3,最后一个维度为1。
现在,让我们尝试沿行除矩阵 M。因此,为了除每一行,我们需要的缩放因子是这个。正如你所看到的,缩放形状如下:第一个维度是3,第二个维度是1。让我们执行除法。
正如你所看到的,我们成功地沿行进行了除法。你可以看到这一行没有被改变,因为它被1除;第二行被5除;最后一行,第三行被10除。
如果我们使用以下缩放因子代替,具有以下形状:1x3。并且我们用这个特定的缩放因子除矩阵,我们看到在这种情况下,我们用缩放因子除了每一列。所以这里,正如你所看到的,这一列没有被改变。我们有1、4和7。第二列被5除,最后一列被10除。
现在,让我们回到量化我们的张量。如果你还记得,我们最终得到的缩放因子如下。如果我们检查这个缩放因子的形状。这是缩放因子的正确形状,以便量化每一行。
现在,我们需要做的就是量化张量。使用我们在上一个实验中调用的 linear_Q_with_scale_and_zero_point 函数,我们只需要传递测试张量、缩放因子和零点(由于我们进行对称量化,零点应为零)。

正如你所看到的,我们最终得到了以下量化张量。
现在,让我们把所有我们做的放入一个名为 linear_Q_symmetric_per_channel 的函数中。
正如你在这里看到的,我们获取输出维度。我们创建具有输出维度形状的缩放因子张量。我们遍历输出维度,对于每个索引,我们获取子张量,并将缩放因子存储在索引位置,然后我们重塑缩放因子。最后,我们使用 linear_Q_with_scale_and_zero_point 函数获取量化张量。
就这样,我们得到了量化张量和缩放因子。
现在,我们有了我们的函数,让我们检查一下我们是否确实能够沿特定维度量化。所以,我将重新粘贴我们之前定义的测试张量,这次我们将沿第一个维度和第二个维度量化,因此我们将有量化张量0和缩放因子0,我们通过使用 linear_Q_symmetric_per_channel 函数得到这些,并且我们需要指定我们量化的维度是0。

让我们对另一个维度做同样的事情。所以我们将称它为量化张量1和缩放因子1。
为了得到总结,我们还需要对每个张量进行反量化。所以,让我们先做维度等于0的情况。我们有反量化张量和缩放因子0。这等于线性反量化,我们需要指定量化张量_0、它的缩放因子和零点(因为零点等于0)。
现在,我们有了使用 plot_quantization_error 函数获取总结所需的一切。
就这样。正如你所看到的,我们确实沿行进行了量化。你可以看到我们在这里有最大量化值127,这里,这里和这里。量化效果相当好,正如你所看到的,原始张量非常接近反量化张量,并且量化误差张量并不那么糟糕。
让我们通过计算量化误差来获得一个更好的度量。我们得到了1.8的量化误差。如果我们还记得,当我们进行逐张量对称线性量化时,量化误差大约在2.5左右。
现在,让我们检查一下如果我们沿列量化会发生什么。我们将做与上面相同的事情,但是使用量化张量_1。所以,正如你在这里看到的,我们通过使用线性反量化定义了反量化张量_1,并传递了量化张量_1和缩放因子1,然后我们绘制了量化误差。这将给我们以下总结。正如你在这里看到的,我们这次确实成功地沿列进行了量化。量化误差甚至更低。你看到在这两种情况下,与逐张量量化相比,我们都得到了更低的量化误差。这是因为异常值只会影响它所在的通道,而不是整个张量。



总结



本节课中我们一起学习了逐通道量化的原理与实现。我们了解到,通过为张量的每个通道(行或列)使用独立的缩放因子,可以显著降低量化误差,尤其是当数据中存在异常值时。我们实现了对称模式下的逐通道量化函数,并通过实例验证了其优于逐张量量化的效果。
008:分组量化 🧩
在本节课中,我们将学习分组量化技术。这是一种更精细的量化方法,通过将张量元素分组并分别进行量化,可以在保持精度的同时,实现更低的存储开销。
上一节我们介绍了逐通道量化,本节中我们来看看如何将量化粒度进一步细化到元素组。
分组量化原理
分组量化对张量中每组 n 个元素执行量化。常见的 n 值为 32、64 或 128。分组量化有时需要较多内存。

假设我们想将一个张量量化为 4 比特,并选择组大小为 32。我们使用对称模式,这意味着零点等于 0。我们将缩放因子存储在 float16 格式中。这意味着我们实际上是以 4.5 比特来量化张量,因为每个元素用 4 比特存储,并且每 32 个元素需要存储一个 16 比特的缩放因子。
对于每个元素,你用 4 比特存储它,但你也有量化参数,需要每 32 个元素存储一次 16 比特的缩放因子。所以是每 32 个元素 16 比特。
代码实现
现在让我们开始编码。为了简单起见,我们将限制张量为二维,并使用对称模式。你不需要关注这段代码,因为我们将在笔记本中编码。
以下是实现分组量化的函数 linear_quantize_symmetric_per_group:
def linear_quantize_symmetric_per_group(tensor, group_size, dtype=torch.int8):
# 获取张量形状
original_shape = tensor.shape
# 确保张量是二维的,且行数能被组大小整除
assert len(original_shape) == 2, "Tensor must be 2D"
assert original_shape[1] % group_size == 0, "Row dimension must be divisible by group size"
# 重塑张量,使每行包含 `group_size` 个元素
# 使用 -1 让 PyTorch 自动推断第一维的大小
reshaped_tensor = tensor.view(-1, group_size)
# 使用之前编写的逐通道量化函数对重塑后的张量进行量化
# 沿行(维度1)进行量化
quantized_tensor, scale = linear_quantize_symmetric_per_channel(reshaped_tensor, dim=1, dtype=dtype)
# 将量化后的张量重塑回原始形状
quantized_tensor = quantized_tensor.view(original_shape)
return quantized_tensor, scale
现在我们已经编写了分组量化函数,接下来编写分组反量化函数 linear_dequantize_per_group 以验证结果。
def linear_dequantize_per_group(quantized_tensor, scale, group_size):
# 获取量化张量的形状
quantized_shape = quantized_tensor.shape
# 重塑量化张量,使每行包含 `group_size` 个元素
reshaped_quantized = quantized_tensor.view(-1, group_size)
# 使用之前编写的反量化函数进行反量化
# 对称量化下零点为0
zero_point = 0
dequantized_tensor = linear_dequantize(reshaped_quantized, scale, zero_point)
# 将反量化后的张量重塑回原始形状
dequantized_tensor = dequantized_tensor.view(quantized_shape)
return dequantized_tensor
测试实现
现在让我们测试我们的实现。我们将测试一个大小为 6x6 的随机张量,并将组大小设置为 3。
# 创建测试张量
test_tensor = torch.randn(6, 6)
group_size = 3
# 执行分组量化
quantized_tensor, scale = linear_quantize_symmetric_per_group(test_tensor, group_size)

# 执行分组反量化
dequantized_tensor = linear_dequantize_per_group(quantized_tensor, scale, group_size)

# 绘制量化误差图以总结量化过程
plot_quantization_error(test_tensor, quantized_tensor, dequantized_tensor)
观察量化张量,你会看到每一行中每三个元素的最大值为 127。这表明我们确实成功地沿着行方向对矩阵中每三个元素进行了量化。反量化张量与原始张量几乎相同。
让我们也使用反量化误差函数打印误差:
error = dequantization_error(test_tensor, dequantized_tensor)
print(f"Dequantization error: {error}")

确实,我们得到了非常低的量化误差。

现在是暂停视频并尝试一些操作的好时机。你可以尝试更改测试张量,或者更改组大小,以观察组大小对量化过程的影响。
总结



本节课中我们一起学习了分组量化技术。我们了解了其基本原理,即对张量元素进行分组并分别量化,从而在比特精度和模型精度之间取得平衡。我们实现了分组量化与反量化的核心函数,并通过实验验证了其有效性,观察到量化误差非常低。你可以通过调整组大小来探索其对量化效果的影响。
009:权重与激活值的量化推理 🧠

在本节课中,我们将学习如何在神经网络推理过程中,对权重和激活值应用线性量化。我们将探讨仅量化权重与同时量化权重和激活值两种场景的区别,并通过代码示例演示如何实现一个无偏置的量化线性层。
量化推理的两种模式 🔄
上一节我们介绍了线性量化的基本原理,本节中我们来看看如何在神经网络推理中应用它。
对权重和激活值进行量化时,存储和计算的方式并不相同。因此,量化策略主要分为两种模式:
- 仅量化权重 (W8A32):在这种模式下,模型的权重被量化为8位整数进行存储,但在计算时会被反量化为浮点数。因此,计算过程仍然使用浮点运算(如FP32、FP16或BF16)。
- 同时量化权重和激活值 (W8A8):在这种模式下,权重和激活值都被量化为8位整数。计算过程可以直接使用整数运算,这能带来更大的加速和内存节省。但请注意,并非所有硬件都支持高效的整数运算。
实现 W8A32 量化线性层 💻

现在,让我们看看当输入(激活值)保持为32位浮点数,而权重被量化为8位时,如何编写一个线性层。为了简化,我们实现一个无偏置的线性层。

以下是实现一个量化线性层(无偏置)所需的关键步骤:
def quantized_linear_no_bias(input, quantized_weight, scale, zero_point):
# 输入应为 torch.float32
# 量化权重应为 torch.int8
# 1. 将量化后的权重反量化回浮点数
dequantized_weight = quantized_weight.to(torch.float32) * scale + zero_point
# 2. 使用反量化后的权重执行标准的线性层计算
output = torch.matmul(input, dequantized_weight.T) # 假设权重已转置
return output
代码解析:
input:线性层的输入,数据类型为torch.float32。quantized_weight:已量化的权重,数据类型为torch.int8。scale和zero_point:用于权重反量化的比例因子和零点。- 函数首先将
int8权重重构为float32,然后执行矩阵乘法。
代码示例与验证 ✅
让我们通过一个简单的例子来验证上述函数。
首先,定义输入和权重:
import torch
# 定义输入和原始权重
input = torch.tensor([[1.0, 2.0, 3.0]])
weights = torch.tensor([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])
接着,使用对称线性量化对权重进行量化:
# 假设 linear_symmetric_quantize 是一个实现对称量化的函数
quantized_weight, scale = linear_symmetric_quantize(weights)
# 对于对称量化,zero_point 通常为 0
zero_point = 0
然后,使用我们的量化线性层函数进行计算:
output_quant = quantized_linear_no_bias(input, quantized_weight, scale, zero_point)
print("量化计算输出 (W8A32):", output_quant)

最后,我们使用原始浮点权重进行计算以作对比:
output_fp32 = torch.matmul(input, weights.T)
print("原始浮点计算输出 (FP32):", output_fp32)

运行后你会发现,output_quant 和 output_fp32 的值非常接近,这证明了我们权重量化与反量化过程的有效性。

总结 📝
本节课中我们一起学习了神经网络推理中的量化应用:
- 我们区分了 仅量化权重 (W8A32) 和 同时量化权重与激活值 (W8A8) 两种模式,前者使用浮点计算,后者使用整数计算但需要硬件支持。
- 我们重点实现了 W8A32 模式下的一个无偏置量化线性层。其核心步骤是:将存储的
int8权重通过scale和zero_point反量化为float32,再进行标准的矩阵乘法。 - 通过一个简单的代码示例,我们验证了量化计算的结果与原始浮点计算的结果基本一致。


在接下来的课程中,我们将利用在此学到的所有知识,构建一个完整的8比特量化器,并将其应用到真实的模型中。
010:构建自定义8位量化器 🛠️


在本节课中,我们将学习如何利用之前构建的工具,创建一个自定义的量化器,以便以8位精度量化任何模型。这个量化器是模型无关的,意味着你可以将其应用于视觉、音频、文本甚至多模态模型。让我们开始量化一些模型吧。


概述 📋
上一节我们介绍了线性量化的基本概念,本节中我们将动手实践,构建一个完整的量化器。我们将学习如何创建一个W8A16线性层类,用它替换模型中的所有线性层,并最终构建一个端到端的量化器。我们还将测试量化器在不同场景下的效果,并研究8位量化对模型的影响。
构建W8A16线性层类 🧱
我们的第一个任务是构建一个名为W8A16的线性层类。W8代表8位权重,A16代表16位激活值。我们将使用这个类来存储8位权重和缩放因子,就像上一课中看到的那样。
实现前向传播方法
首先,我们需要构建一个名为 W8A16_forward 的方法。这个方法将作为我们线性层前向传播的核心。
以下是该方法需要执行的步骤:
- 将8位权重转换为与输入相同的数据类型。
- 执行输入与转换后权重之间的矩阵乘法。
- 将结果与缩放因子相乘。
- 可选地加上偏置项。
让我们开始实现。首先导入必要的模块,并定义一些随机输入用于测试。
import torch
import torch.nn.functional as F
def W8A16_forward(input, int8_weights, scales, bias=None):
# 步骤1:将权重转换为与输入相同的数据类型
cast_weights = int8_weights.to(input.dtype)
# 步骤2:执行线性操作(矩阵乘法)
output = F.linear(input, cast_weights)
# 步骤3:乘以缩放因子
output = output * scales.view(1, -1) # 确保形状可广播
# 步骤4:可选地加上偏置
if bias is not None:
output = output + bias
return output
现在,让我们快速测试一下这个方法在有偏置和无偏置情况下的表现。
# 定义测试数据
batch_size, seq_len, in_features, out_features = 2, 5, 8, 4
hidden_states = torch.randn(batch_size, seq_len, in_features)
int8_weights = torch.randint(-128, 127, (out_features, in_features), dtype=torch.int8)
scales = torch.randn(out_features)
bias = torch.randn(out_features)
# 测试无偏置
output_no_bias = W8A16_forward(hidden_states, int8_weights, scales)
print(f"无偏置输出形状: {output_no_bias.shape}")
# 测试有偏置
output_with_bias = W8A16_forward(hidden_states, int8_weights, scales, bias)
print(f"有偏置输出形状: {output_with_bias.shape}")
实现线性层类的初始化方法
接下来,我们将利用刚刚创建的方法来构建完整的线性层类。这个类需要存储 int8 权重、缩放因子和可选的偏置。
一个关键的注意事项是:在PyTorch中,int8 张量目前无法直接计算梯度。因此,我们不能使用 nn.Parameter 来存储它们,而应该使用 register_buffer 方法。
class W8A16Linear(torch.nn.Module):
def __init__(self, in_features, out_features, bias=True, dtype=torch.float32):
super().__init__()
self.in_features = in_features
self.out_features = out_features
# 使用register_buffer存储int8权重和缩放因子
self.register_buffer("int8_weights", torch.randint(-128, 127, (out_features, in_features), dtype=torch.int8))
self.register_buffer("scales", torch.ones(out_features, dtype=dtype))
# 存储偏置
if bias:
self.register_buffer("bias", torch.zeros(out_features, dtype=dtype))
else:
self.bias = None
def forward(self, x):
# 调用我们之前定义的全局前向传播函数
return W8A16_forward(x, self.int8_weights, self.scales, self.bias)
让我们创建一个虚拟实例来验证属性是否正确保存。
dummy_layer = W8A16Linear(8, 4, bias=True)
print(f"权重形状: {dummy_layer.int8_weights.shape}")
print(f"缩放因子形状: {dummy_layer.scales.shape}")
print(f"偏置形状: {dummy_layer.bias.shape}")


实现量化方法
目前,我们的线性层权重是随机的。为了使其真正有用,我们需要一个 quantize 方法,将原始的高精度权重(例如FP16或BF16)量化为 int8 并计算相应的缩放因子。
量化的工作流程如下:
- 获取原始权重并将其转换为FP32以保证稳定性。
- 使用每通道绝对值最大(AbsMax)量化公式计算缩放因子。
- 使用缩放因子将权重量化为
int8。 - 将计算出的
int8权重和缩放因子赋值给我们的线性层。
以下是 quantize 方法的实现:
def quantize(self, original_weights):
# 步骤1:将权重转换为FP32
weights_fp32 = original_weights.to(torch.float32)
# 步骤2:计算每通道的缩放因子
# 公式: scale = max(abs(weight)) / 127
scales = torch.max(torch.abs(weights_fp32), dim=1).values / 127
scales = scales.to(original_weights.dtype) # 保持与原始权重相同的数据类型
self.scales.copy_(scales)
# 步骤3:量化权重为int8
# 公式: int8_weights = round(weights / scale)
int8_weights = torch.clamp(torch.round(weights_fp32 / scales.view(-1, 1)), -128, 127).to(torch.int8)
self.int8_weights.copy_(int8_weights)
现在,我们将这个方法添加到我们的 W8A16Linear 类中。
class W8A16Linear(torch.nn.Module):
def __init__(self, in_features, out_features, bias=True, dtype=torch.float32):
# ... 初始化代码与之前相同 ...
pass
def forward(self, x):
# ... 前向传播代码与之前相同 ...
pass
def quantize(self, original_weights):
weights_fp32 = original_weights.to(torch.float32)
scales = torch.max(torch.abs(weights_fp32), dim=1).values / 127
scales = scales.to(original_weights.dtype)
self.scales.copy_(scales)
int8_weights = torch.clamp(torch.round(weights_fp32 / scales.view(-1, 1)), -128, 127).to(torch.int8)
self.int8_weights.copy_(int8_weights)
让我们测试一下量化过程。
# 创建一个线性层实例
layer = W8A16Linear(8, 4, bias=True)
print("量化前的随机权重(部分):", layer.int8_weights[0, :4])
# 创建一些模拟的“原始”高精度权重
original_weights = torch.randn(4, 8, dtype=torch.float16) * 0.1 # 小权重便于观察
# 执行量化
layer.quantize(original_weights)
print("量化后的权重(部分):", layer.int8_weights[0, :4])
print("缩放因子:", layer.scales)
# 计算量化误差(反量化后与原始权重的差异)
dequantized_weights = layer.int8_weights.to(torch.float32) * layer.scales.view(-1, 1)
quant_error = torch.mean(torch.abs(dequantized_weights - original_weights.to(torch.float32)))
print(f"平均量化误差: {quant_error.item()}")
构建端到端量化器 🔄
现在我们已经有了一个功能完整的量化线性层,下一步是构建一个量化器,能够遍历整个模型,将其中的所有 torch.nn.Linear 层替换为我们的 W8A16Linear 层,并对权重进行量化。
以下是量化器需要执行的步骤:
- 遍历模型的所有模块。
- 识别出所有
torch.nn.Linear层。 - 用
W8A16Linear层替换它们,并复制原有的偏置设置。 - 调用新层的
quantize方法,传入原始权重。
def quantize_model(model):
for name, module in model.named_children():
# 如果当前模块是Linear层,则进行替换
if isinstance(module, torch.nn.Linear):
# 创建新的W8A16Linear层,继承原层的配置
new_layer = W8A16Linear(
in_features=module.in_features,
out_features=module.out_features,
bias=module.bias is not None,
dtype=module.weight.dtype
)
# 如果有偏置,复制偏置值
if module.bias is not None:
new_layer.bias.copy_(module.bias)
# 量化新层的权重
new_layer.quantize(module.weight.data)
# 用新层替换原层
setattr(model, name, new_layer)
else:
# 如果当前模块不是Linear层,则递归进入其子模块
quantize_model(module)
测试与应用 🧪
现在,让我们在一个简单的模型上测试我们的量化器,并观察其效果。
# 创建一个简单的测试模型
class SimpleModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.linear1 = torch.nn.Linear(10, 20)
self.relu = torch.nn.ReLU()
self.linear2 = torch.nn.Linear(20, 5)
def forward(self, x):
x = self.linear1(x)
x = self.relu(x)
x = self.linear2(x)
return x
# 实例化模型并转换为半精度(模拟常见场景)
model = SimpleModel().half() # 转换为FP16
print("量化前模型结构:", model)
# 应用我们的量化器
quantize_model(model)
print("\n量化后模型结构:", model)
# 准备测试输入
test_input = torch.randn(2, 10).half()
# 运行量化后的模型
with torch.no_grad():
output = model(test_input)
print(f"\n量化模型输出形状: {output.shape}")
总结 🎯
在本节课中,我们一起学习了如何从零开始构建一个自定义的8位量化器。我们首先深入探讨了如何创建 W8A16Linear 类,该类使用 int8 权重和 float16 激活值。我们解决了存储 int8 权重时无法计算梯度的问题,并通过 register_buffer 来正确存储它们。
接着,我们实现了关键的 quantize 方法,使用每通道AbsMax量化方案将原始高精度权重转换为 int8 格式并计算缩放因子。最后,我们构建了一个端到端的模型量化函数,能够自动遍历模型结构,替换线性层并完成权重量化。

通过本节课的学习,你现在已经掌握了构建一个模型无关、支持多种数据类型(FP16/BF16)的8位量化器的核心技能。这个量化器可以应用于视觉、音频、文本等各种模型,为在资源受限的设备上部署模型奠定了基础。
011:用量化层替换PyTorch层


在本节课中,我们将学习如何构建一个量化器。这个量化器将作为一个量化流水线,遍历原始模型中的所有线性模块,并用我们新创建的W8A16线性层模块替换它们,同时使用原始权重对替换后的模块进行量化。让我们一步步来实现这个过程。
构建模块替换方法
首先,我们需要构建一个名为 replace_linear_with_target 的方法。这个方法将遍历模型,识别出所有属于 torch.nn.Linear 类的模块,并用新的模块替换它们。



以下是该方法的签名:
def replace_linear_with_target(module, target_class, module_names_to_exclude):
module: 可以是整个模型,也可以是某个子模块。由于该方法将递归调用,所以命名为module。target_class: 用于替换原线性层的新类。module_names_to_exclude: 一个列表,包含在此替换逻辑中需要排除的模块名称。例如,在语言模型中,为了获得更好的效果,通常需要保持最后一个模块(如语言模型头)不被量化。这个参数将用于处理这类特定用例。
该方法的核心逻辑是遍历模型的所有命名子模块。以下是具体的步骤:
- 遍历
module.named_children()。 - 如果子模块是
torch.nn.Linear的实例,并且其名称不在module_names_to_exclude列表中,则执行模块替换。 - 获取原子模块的偏置(bias),因为创建新模块时需要用到它。
- 创建新的模块实例:
new_module = target_class(in_features, out_features, bias=old_bias is not None)。其中,输入特征数、输出特征数与原线性层保持一致。 - 使用
setattr(parent_module, name, new_module)将父模块中名为name的属性替换为new_module。 - 如果原子模块有偏置,则显式地将新模块的偏置设置为
old_bias。 - 如果当前子模块不符合替换条件,则递归地对该子模块调用
replace_linear_with_target方法。
测试模块替换方法

为了测试这个方法,我们创建一个用于测试的虚拟模型,它包含两个线性层和一个语言模型头(LM Head)。语言模型头通常是Transformer模型中的最后一个模块。
由于该方法会就地修改模型,我们将创建两个新模型进行测试:
model1: 用于测试module_names_to_exclude功能,我们将排除LM Head。model2: 用于测试替换所有线性层实例。
让我们测试第一种情况:
# 假设 LM Head 的名称为 ‘lm_head’
replace_linear_with_target(model1, W8A16Linear, module_names_to_exclude=[‘lm_head’])
测试结果表明,我们成功地将除了LM Head之外的所有线性层都替换成了新的量化层。
接下来,测试第二种情况,传入一个空列表:
replace_linear_with_target(model2, W8A16Linear, module_names_to_exclude=[])

正如预期的那样,第二个模型中的所有线性层实例都被替换成了目标类。



集成量化步骤


上一节我们成功实现了模块替换。现在,我们需要对这个方法进行一些调整,使其在替换模块后,能立即对新模块进行量化。

我们对 replace_linear_with_target 方法进行修改。在成功替换模块后,我们需要:
- 通过
getattr(parent_module, name)再次获取刚刚替换进去的新模块。 - 调用新模块的
.quantize(old_weight)方法,传入原始权重,完成量化。


同时,也需要更新递归调用的部分,确保所有层级的替换都包含量化步骤。

让我们用一个全新的虚拟模型来测试这个集成后的方法:


replace_linear_with_target(new_dummy_model, W8A16Linear, module_names_to_exclude=[])
测试结果显示,方法运行成功。我们不仅替换了线性层,还完成了权重量化。


总结

本节课中,我们一起学习了如何构建一个完整的量化流水线。我们首先创建了一个递归方法,用于遍历模型并用自定义的量化线性层替换标准的PyTorch线性层。接着,我们为该功能增加了排除特定模块(如语言模型头)的能力。最后,我们进一步完善了这个方法,使其在替换模块后能自动调用量化函数,从而完成从模块替换到权重量化的完整流程。现在,我们已经拥有了构建量化器所需的所有核心组件。
012:量化任意开源PyTorch模型 🚀
在本节课中,我们将学习如何将之前构建的量化器应用于真实、有用的开源模型,而不仅仅是测试用的虚拟模型。我们将以Hugging Face上的语言模型和视觉模型为例,演示完整的量化流程,并评估量化效果。
上一节我们介绍了量化器的核心实现,本节中我们来看看如何将其应用于实际场景。
代码生成模型的量化
首先,我们使用一个名为 Salesforce/CodeGen-350M-mono 的语言模型进行测试。这是一个拥有3.5亿参数、专门针对代码进行微调的模型。
以下是加载模型和分词器的代码:
from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
model_name = "Salesforce/CodeGen-350M-mono"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)
接着,我们使用文本生成管道进行代码补全任务测试:
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
prompt = "def print_hello_world():"
result = pipe(prompt, max_length=50)
print(result[0]['generated_text'])
模型成功生成了打印“Hello World”的Python函数,证明了其基础能力。
在量化之前,我们先查看原始模型结构。Transformer架构主要由线性层构成,这些层将是量化的主要目标。
以下是调用我们设计的量化API的代码:
# 假设 quantize_model 是我们之前实现的函数
# 我们不量化语言模型头,以避免自回归生成过程中的误差累积
quantized_model = quantize_model(model, skip_layers=['lm_head'])
量化后,我们可以检查模型结构,确认线性层已被替换为 W8A16Linear 层,而语言模型头仍保持为原始的 torch.nn.Linear 层。
再次使用量化后的模型进行生成测试:
pipe.model = quantized_model
result_quantized = pipe(prompt, max_length=50)
print(result_quantized[0]['generated_text'])
量化后的模型依然能够生成正确的代码,但需注意,对于更大的模型(例如超过60亿参数),量化误差在长序列生成中可能会累积,影响性能。这引出了LLM量化中的“精度退化”问题,已有许多论文(如LLM.int8、SmoothQuant、GPTQ等)致力于解决此问题,我们将在后续课程简要介绍其核心思想。
目标检测模型的量化
接下来,我们尝试量化其他模态的模型,以目标检测模型DETR为例。工作流程与之前类似。
首先,从Hugging Face加载DETR模型及其处理器:
from transformers import DetrImageProcessor, DetrForObjectDetection

processor = DetrImageProcessor.from_pretrained("facebook/detr-resnet-50")
model = DetrForObjectDetection.from_pretrained("facebook/detr-resnet-50")



在量化前,我们先获取模型的内存占用并测试其性能。我们使用一张包含多人的晚餐图片进行目标检测。

以下是运行检测并可视化结果的代码流程:
image = Image.open("dinner_picture.jpg")
inputs = processor(images=image, return_tensors="pt")
outputs = model(**inputs)
# 后处理并绘制检测框
plot_results(image, outputs)
模型成功检测出了图片中的人物、桌子、手机、杯子等多种物体。


现在,我们来量化这个模型。DETR模型包含大量卷积层和线性层。

以下是模型结构检查的要点:
- 卷积层不会被量化。
- 编码器和解码器中的线性层将是量化的目标。
- 我们同样会避免量化最后的边界框预测器和分类器,以保持输出精度。


调用量化函数,并指定要跳过的层:
skip_modules = ['bbox_predictor', 'class_labels_classifier']
quantized_detr_model = quantize_model(model, skip_layers=skip_modules)
量化后检查模型,确认卷积层保持不变,而编码器/解码器中的线性层已被正确量化。

使用量化后的模型再次进行目标检测可视化,结果与原始模型基本一致,成功检测到了相同的物体实例。


最后,我们对比量化前后的内存占用:
- 量化前:约 170 MB
- 量化后:约 120 MB
我们成功节省了大约 50 MB 的内存,压缩率约为 25-30%,同时基本保持了模型的性能。
实践与探索建议
现在,我邀请您亲自动手尝试。您可以:
- 将此量化方法应用于其他模型或其他模态(如视觉、音频、多模态模型)。
- 尝试“破坏”量化器,例如强制量化最后一层,观察这对模型性能有何影响。
- 请注意,我们的量化API是原地修改模型的。如果您想比较量化前后的版本,需要在量化前保存原始模型状态,或在量化后重新加载原始模型。




本节课中我们一起学习了如何将量化器应用于真实的开源PyTorch模型,包括代码生成模型和目标检测模型。我们验证了量化在减少内存占用的同时,能够较好地保持模型的核心功能,并指出了在大型语言模型上应用时需要注意的误差累积问题。
013:从Hugging Face Hub加载量化权重 🚀
在本节课中,我们将学习一种更优的模型量化部署流程。核心思路是:在一台资源充足的大机器上完成模型的量化,将量化后的权重上传到云端(如Hugging Face Hub),然后在资源受限的本地机器上直接加载量化后的模型,从而避免加载原始高精度模型对内存的巨大需求。
概述:优化量化模型部署流程
上一节我们介绍了如何使用量化API对模型进行量化。然而,当前的设计需要先以原始精度加载模型,这并非最优方案,因为它要求分配足够的内存来加载默认数据类型的模型,然后才能进行量化。
理想情况下,我们可以利用一台拥有较大内存的实例(大机器)来完成量化工作。量化完成后,将量化权重推送到云端,例如Hugging Face Hub。之后,便可以在本地机器上直接加载8位甚至更低精度的模型。本节我们将构建一个覆盖此方法的流程。
步骤一:在大实例上量化并上传模型


首先,我们假设处在一个拥有足够RAM的大实例中,可以在此量化模型。以下是为演示目的加载一个小模型(如OPT-125M)并进行量化的代码示例。

# 加载原始模型
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained("facebook/opt-125m")
# 初始化量化器并量化模型
from bitsandbytes.nn import Linear8bitLt
quantizer = ... # 初始化量化器
quantized_model = quantizer.quantize(model)
量化完成后,我们可以通过调用 model.state_dict() 来获取模型的量化权重字典,并将其保存到本地。
quant_state_dict = quantized_model.state_dict()
torch.save(quant_state_dict, "opt125m_quantized.pt")
接下来,使用Hugging Face Hub库的工具方法将这些量化权重推送到Hub上。


以下是使用API的示例代码:
from huggingface_hub import HfApi, create_repo
api = HfApi()
# 创建仓库(也可通过网站手动创建)
create_repo(repo_id="your-username/opt125m-quantized-deeplearning-ai", private=False)
# 上传文件
api.upload_file(
path_or_fileobj="opt125m_quantized.pt", # 本地文件路径
path_in_repo="pytorch_model.bin", # 在仓库中的目标路径
repo_id="your-username/opt125m-quantized-deeplearning-ai"
)
这样,你就在拥有GPU或大内存CPU的大实例上量化了模型,并将其推送到了Hugging Face Hub。


步骤二:在本地机器上加载量化模型
现在,转到本地机器。我们的目标是直接加载这些更小的量化状态字典,而无需先加载原始模型。这里将利用PyTorch的一个称为 meta device 的特性。
核心思想是:
- 首先加载模型的“骨架”(即架构),以获取正确的模块结构,但权重不实际初始化(使用meta device)。
- 将骨架中的所有线性层替换为我们的量化层。
- 最后,加载量化状态字典,将权重精确分配到对应的模块上。
这种方式节省了内存,因为你无需先加载原始模型,而是直接通过加载状态字典来获得模型的量化版本,同时利用了PyTorch的meta device特性,只需加载模型骨架而非整个模型。
代码实现如下:
首先,加载模型配置以获取架构细节,然后在 torch.device(‘meta’) 的上下文管理器中初始化模型。
from transformers import AutoConfig
import torch

config = AutoConfig.from_pretrained("facebook/opt-125m")
with torch.device('meta'):
model = AutoModelForCausalLM.from_config(config)
此时,如果尝试打印模型参数,你会看到类似下面的输出:



Parameter containing:
tensor(..., device='meta', dtype=torch.float32)
所有参数都是未初始化的元张量(metatensors),不占用任何RAM。你拥有了关于模型架构的所有信息(骨架),唯独没有权重。


接着,我们将线性层替换为量化层(注意,这里不是调用量化函数,而是替换层类型)。

from bitsandbytes.nn import Linear8bitLt
def replace_linear_with_quant(model):
for name, module in model.named_children():
if len(list(module.children())) > 0:
replace_linear_with_quant(module)
if isinstance(module, torch.nn.Linear):
# 创建新的量化线性层,保持输入输出特征数不变
new_layer = Linear8bitLt(module.in_features, module.out_features, bias=module.bias is not None)
setattr(model, name, new_layer)
return model
model = replace_linear_with_quant(model)
现在,下一步是加载量化状态字典。我们同样使用Hugging Face Hub库来下载文件。
from huggingface_hub import hf_hub_download


# 指定Hub上文件的路径进行下载,返回缓存路径
model_path = hf_hub_download(repo_id="your-username/opt125m-quantized-deeplearning-ai", filename="pytorch_model.bin")
# 加载状态字典
quant_state_dict = torch.load(model_path, map_location='cpu')
请注意,这个状态字典文件只有约166MB。对于一个1.25亿参数的模型,如果以半精度(FP16,每个参数2字节)存储原始状态字典,需要约250MB。而这里只有166MB,是因为大部分权重已被量化为8位精度(每个参数1字节),约125MB,其余部分可能是语言模型头或存储为float16的缩放因子。
由于模型加载在meta device上,我们需要以特定方式加载状态字典。


model.load_state_dict(quant_state_dict, strict=True, assign=True)

如果看到“All keys matched successfully”的提示,说明模型已成功加载,准备就绪。
现在,模型已经加载完毕,可以用于推理了。例如:
input_text = "Hello today I'm a student of the University of the"
inputs = tokenizer(input_text, return_tensors="pt").to(model.device)
outputs = model.generate(**inputs)
print(tokenizer.decode(outputs[0]))
请注意,这是一个小模型,如果上下文提供不足,可能会产生一些重复输出。通过使用采样方法或尝试更大的量化模型,可以获得更好的结果。
总结与下节预告
本节课中,我们一起学习了一种高效的模型量化部署工作流。我们利用大机器完成量化,将权重上传至Hugging Face Hub,然后在本地通过 PyTorch meta device 加载模型骨架并注入量化权重,从而实现了低内存消耗的模型加载。


在下一节课中,我们将探讨量化中的一些挑战。例如,我之前提到的大语言模型中的异常值特征问题。此外,我们还将学习如何存储更低比特的权重(如2位或4位),动手实践权重打包技术,并介绍当前最先进的方法,以解决在量化大语言模型时遇到的异常值特征挑战。敬请期待下一课!
014:权重打包

在本节课中,我们将探讨应用低比特量化(例如2位或4位)时可能遇到的一些常见挑战,并通过深入实现权重打包来理解它。此外,我们将通过了解一些前沿的量化方法,来为本课程画上句号。
让我们开始打包权重。
概述
在本节中,我们将讨论尝试低比特量化(如2位或4位)时可能面临的挑战,并从零开始实现权重打包。具体来说,你将学习:
- 为什么权重打包对于存储量化权重很重要。
- 如何以打包格式在
int8张量中存储和加载2位、4位权重。 - 量化生成模型(如大语言模型LLMs)的其他挑战。
- 快速回顾一些先进的LLM量化方法。
为什么需要权重打包?

在开始实验之前,我们先了解一下打包的重要性,以及为什么在存储量化权重时需要它。
假设你想将模型量化为4位精度,并希望将权重存储在PyTorch张量中。理想情况下,你希望调用类似下面的代码:
# 理想情况(目前不支持)
torch.tensor(values, dtype=torch.int4)
或者之后进行转换。但问题是,截至目前,PyTorch并没有原生支持4位权重。因此,我们需要找到一种高效的方式来存储这些4位权重。
目前唯一的解决方案是,将张量保存在8位精度下,因为这是PyTorch中可用的最小精度数据类型。但在实践中,这并不理想,因为每个数据点将占用8位,而实际上它只需要4位(因为你已将参数编码为4位精度)。对于大型模型,这无疑会增加相当大的开销。
因此,如果我们采用这种简单的方法(即在8位张量中存储4位权重),将模型量化为2位或4位就失去了意义,因为所有参数最终都以8位精度存储。为了解决这个问题,我们需要将4位权重打包到8位张量中。
权重打包的工作原理
接下来,我们详细看看打包是如何工作的。
考虑下面这个张量,它存储了四个可以用2位精度表示的值。回想一下,在2位精度下,可以编码四个值(0, 1, 2, 3)。假设我们有一个模型参数,已用2位精度编码,其值为 [1, 0, 3, 2]。

目前,在PyTorch中我们无法以2位存储模型权重,必须用8位精度存储。因此,我们最终会得到一个这样的张量,它在内存占用上需要 4 * 8位。当前,这个权重张量被编码为:1(占8位)、0(占8位)、3(占8位)、2(占8位)。
正如之前所说,这并不最优,因为你需要分配 4 * 8位 的内存来存储原本只需2位编码的权重。
那么,我们如何忽略这些不需要的位呢?打包正是为了解决这一挑战,它将所有相关的位打包到一个单一的8位张量中。
例如,我们将这四个权重打包到一个8位张量中。我们从最右边的权重开始,将其放入新8位参数的前几位:10(二进制)。然后放入下一个权重:00。接着是 11,最后是 01。
如果我们将其以8位存储,最终会得到一个只有一个值的新张量,但这个张量编码了所有以2位存储的参数。这个8位无符号整数值将是 0b10001101,即十进制的 141。
打包的优势在于,它反映了量化权重的真实内存占用。在简单方法中,我们需要分配 4 * 8位 的精度;而在打包情况下,我们只需要存储一个8位精度的参数,它包含了我们所有的2位参数。
当然,这需要付出代价。每当我们想要执行推理时,都需要解包权重以恢复原始状态,因为PyTorch中原生不支持2位或4位的操作。此外,解包后的张量形状需要是 (n_bits_per_packed_value / target_bits) 的倍数。例如,如果我们有5个参数,我们仍然需要分配一个额外的8位参数,但它只编码一个2位值。理想情况下,我们需要让单个张量中的参数数量是 8 / n_bits 的倍数(对于2位是4的倍数,对于4位是2的倍数)。
实现与应用
现在,让我们看看在实现中这是如何体现的,并进入实验环节。

在接下来的部分,我们将动手实现权重打包和解包的逻辑,并探讨在量化生成式模型(如LLMs)时遇到的其他挑战,例如激活值分布、异常值处理等。最后,我们将简要介绍一些如GPTQ、AWQ等先进的LLM量化方法。
总结

本节课中,我们一起学习了低比特量化中的一个关键技术——权重打包。我们理解了为什么在现有框架限制下需要打包,掌握了其将多个低精度值高效存储到更高精度数据类型(如int8)中的基本原理。我们还认识到,虽然打包节省了存储空间,但在推理前需要额外的解包步骤。最后,我们了解到量化大型生成模型还存在其他挑战,并预览了当前解决这些挑战的先进方法。
015:2比特权重打包 🧩
在本节课中,我们将学习如何使用纯PyTorch实现2比特权重的打包。我们将编写一个函数,将多个低比特数值高效地打包到一个更高位宽的数据类型中,以节省内存。
概述
权重打包是模型量化中的一项关键技术,它允许我们将多个低比特数值(例如2比特)组合存储在一个更高位宽的数据单元(例如8比特)中。这能显著减少模型的内存占用。本节我们将通过一个具体的例子,逐步讲解其实现原理和代码。
准备工作
首先,我们需要导入必要的库并定义函数签名。
import torch
def pack_weights(unpacked_tensor, num_bits):
"""
将低比特权重打包到更高位宽的数据类型中。
参数:
unpacked_tensor: 未打包的低比特权重张量,应为无符号整数类型。
num_bits: 每个原始值的比特数(例如2或4)。
返回:
打包后的张量。
"""


为了简化处理,我们使用无符号整数类型,这样可以避免处理符号位。
输入形状检查

为了确保算法正确运行,输入张量的形状最好是4的倍数。我们添加一个检查条件。
# 检查输入形状是否为 num_bits 的倍数
if unpacked_tensor.shape[0] % (8 // num_bits) != 0:
raise ValueError(f"输入形状需要是 {8 // num_bits} 的倍数。")

这个检查确保了我们可以将整数个低比特值完整地打包进8比特单元中。
计算预期输出大小
接下来,我们需要计算打包后张量应包含多少个值。
上一节我们介绍了输入检查,本节中我们来看看如何计算输出大小。
其核心公式是:
num_packed_values = (unpacked_tensor.shape[0] * num_bits) / 8
这个公式的原理是:总比特数(原始值数量 × 每个值的比特数)除以目标数据类型的比特数(8),就得到了打包后所需的数据单元数量。
# 计算打包后张量应包含的值的数量
num_packed_values = (unpacked_tensor.shape[0] * num_bits) // 8
确定处理步骤
每个打包后的值(例如一个8比特整数)将包含多个低比特值。我们需要确定每个打包单元能容纳多少个原始值。
以下是计算处理步骤的逻辑:
num_steps_per_pack = 8 // num_bits
例如,对于2比特权重,每个8比特单元可以容纳4个值。
# 计算每个打包值需要处理的低比特值数量
num_steps_per_pack = 8 // num_bits

初始化与循环
现在,我们初始化打包后的张量,并开始核心的打包循环。

以下是实现打包算法的步骤:
- 初始化索引和输出张量:我们创建一个全零的无符号8比特张量来存放结果。
- 外层循环:遍历每一个即将生成的打包值。
- 内层循环:对于每个打包值,循环处理
num_steps_per_pack个低比特值。


# 初始化打包后的张量
packed_tensor = torch.zeros(num_packed_values, dtype=torch.uint8)
# 用于跟踪当前正在处理的原始低比特值的索引
unpacked_index = 0
# 外层循环:遍历每个打包单元
for i in range(num_packed_values):
# 内层循环:将多个低比特值打包进当前单元
for j in range(num_steps_per_pack):
# 获取当前低比特值,并确保其类型为uint8
low_bit_value = unpacked_tensor[unpacked_index].to(torch.uint8)
# 将值左移相应的比特位
shifted_value = low_bit_value << (j * num_bits)
# 使用按位或操作将值“写入”打包单元
packed_tensor[i] |= shifted_value
# 移动到下一个低比特值
unpacked_index += 1
return packed_tensor

算法原理图解
让我们通过一个具体例子来理解上述代码。假设我们有一个包含四个2比特值的张量:[1, 0, 3, 2]。
我们的目标是将其打包成一个8比特整数。
- 第一次迭代 (
j=0):处理值1(01)。不移位,直接通过按位或放入打包单元。结果:00000001。 - 第二次迭代 (
j=1):处理值0(00)。左移2位,得到00000000。与之前结果按位或,结果不变:00000001。 - 第三次迭代 (
j=2):处理值3(11)。左移4位,得到00110000。按位或后,结果:00110001。 - 第四次迭代 (
j=3):处理值2(10)。左移6位,得到10000000。按位或后,得到最终结果:10110001,即十进制的177。
这个过程直观地展示了比特是如何被逐步组合起来的。
测试与验证
现在,让我们测试我们编写的函数。

# 创建一个示例张量,包含四个2比特值:1, 0, 3, 2
# 注意:在2比特表示中,这些值本身应小于4。
unpacked = torch.tensor([1, 0, 3, 2], dtype=torch.uint8)

# 调用打包函数
packed = pack_weights(unpacked, num_bits=2)
print(f"打包后的值: {packed.item()}") # 预期输出: 177

运行上述代码,如果得到177,则证明我们的打包函数工作正常。你可以尝试将177转换为二进制 (10110001),验证其是否对应 [1, 0, 3, 2] 的2比特表示。

扩展与练习

为了加深理解,你可以尝试以下练习:
- 实现4比特打包:修改函数,使其能处理4比特权重的打包。
- 边界测试:测试一些边界情况,例如所有值都为3(2比特最大值
11),看看打包结果是否为255。 - 优化代码:思考是否可以用更向量化的方式(减少显式循环)来实现这个功能。


总结

本节课中我们一起学习了2比特权重打包的核心原理与实现。我们了解了如何通过比特移位和按位或操作,将多个低比特数值高效地压缩到更高位宽的数据类型中。这项技术是模型量化中减少内存占用的关键步骤。通过动手实现,我们不仅掌握了算法,也加深了对底层比特操作的理解。
016:解包2比特权重 🔢
在本节课中,我们将学习如何将打包后的2比特权重张量解包回其原始值。解包是打包的逆过程,它允许我们从压缩的数据格式中提取出原始的权重值,以便进行后续的运算或分析。


概述

上一节我们介绍了如何将权重打包以节省存储空间。本节中,我们来看看如何执行反向操作——解包。我们将逐步解析解包算法的逻辑,并通过一个具体的例子来演示整个过程。
解包算法原理


解包算法的签名与打包算法类似。它接收一个打包后的uint8张量、以及量化的比特数。要计算解包后张量中预期的数值数量,可以使用以下公式:

公式:
预期数值数量 = (打包张量中的元素数量 * 8) / 比特数
对于每个打包的张量元素,我们预期从中提取出的数值数量为 8 / 比特数。以2比特为例,每个uint8字节可以解包出4个2比特的数值。
逐步解包过程
以下是解包算法的核心步骤。我们将采用一种简单直观的循环方法来实现。
首先,初始化一个全零的张量来存放解包后的结果。我们将遍历打包权重中的每一个元素,并逐步提取出其中存储的多个低比特数值。
考虑以下打包后的权重示例(一个uint8值):
0b01000011 (十进制为67)
我们的目标是将它解包为四个2比特的值:01, 00, 00, 11(即1, 0, 0, 3)。
第一步:初始化与循环
我们创建两个索引:i用于遍历打包张量,j用于遍历每个打包元素内部的多个值(对于2比特,j从0到3)。同时,初始化一个全零的解包结果张量。
第二步:位操作提取
在每次内部循环中,我们将当前打包值右移 (比特数 * j) 位。然后,通过按位或操作将这个移位后的值累积到解包张量的对应位置。

- 第一次迭代 (
j=0): 移位0位,解包张量第一个位置得到原始值0b01000011。 - 第二次迭代 (
j=1): 右移2位,得到0b00010000,通过按位或操作放到解包张量第二个位置。 - 后续迭代 (
j=2, 3): 继续右移4位和6位,并累积。
经过上述步骤,解包张量中的四个值目前包含了我们需要的低位比特,但高位还残留着不需要的比特。例如,第一个值目前是 0b01000011,而我们只想要最低的两位 01。
第三步:应用掩码清除高位
为了清除每个值中除了最低n比特之外的所有高位,我们需要使用一个掩码进行按位与操作。
掩码的计算公式为:掩码 = (2 ^ 比特数) - 1
对于2比特,掩码为 2^2 - 1 = 3,即二进制 0b00000011。

将这个掩码与解包张量中的每个值进行按位与操作,即可精确地保留最低两位,并将所有高位清零。


代码示例:
# 假设 unpacked_tensor 是经过移位和或操作后的中间结果
mask = (1 << num_bits) - 1 # 对于2比特,mask=3
unpacked_tensor = unpacked_tensor & mask



验证与扩展


让我们用之前的例子验证一下。打包值 67 (0b01000011) 经过上述解包和掩码操作后,应该得到四个值:1 (01), 0 (00), 0 (00), 3 (11)。
以上我们演示的是针对一个单元素向量(形状简单)的朴素算法。在实际应用中,你可能需要:
- 优化算法性能,减少循环。
- 扩展算法以处理任意形状的张量(例如多维权重矩阵)。
- 进行充分的测试以确保正确性。




总结

本节课中我们一起学习了2比特权重的解包过程。我们了解到,解包是打包的逆运算,核心步骤包括:根据公式计算输出大小、通过右移和按位或操作提取比特位、以及使用掩码进行按位与来清除高位冗余数据。掌握解包原理对于理解和实现完整的模型量化流程至关重要。



在接下来的课程中,我们将探讨在量化大型模型(如大语言模型)时可能遇到的其他挑战,并以此结束整个量化深度课程。
017:超越线性量化 🚀
在本节课中,我们将总结整个课程,并探讨大语言模型量化中的一个核心挑战——“涌现特征”,以及应对此挑战的几种前沿量化方法。
上一节我们介绍了线性量化的基本概念。本节中,我们来看看当模型规模变得非常大时,量化会遇到什么特殊问题。随着开源社区出现越来越多的大语言模型,研究人员在深入探索模型能力时,发现了一些所谓的“涌现特征”。涌现特征指的是模型在规模变大时才会显现出的某些特性或特征。具体来说,对于某些大模型,其预测的特征,即隐藏状态的大小,会变得非常大。这使得经典的量化方案变得不再适用,导致传统的线性量化算法在这些模型上失效。

自这些大语言模型开源以来,许多论文都致力于解决如何处理大语言模型中的离群特征这一具体挑战。离群特征简单来说就是数值非常大的隐藏状态。以下是一些有趣的论文,我们将简要解释每篇论文的核心思想,以提供解决此问题的潜在方案。
应对离群特征的量化方法 📄

以下是几种针对大语言模型离群特征问题提出的量化技术。
-
LLM.int8():该方法提出将线性层的底层矩阵乘法分解为两个阶段。具体思路是,将输入隐藏状态矩阵分解为两部分:离群部分(所有超过某个阈值的隐藏状态)和非离群部分。然后,对非离群部分执行 int8 精度的矩阵乘法(即量化、计算、再反量化),对离群部分则使用原始高精度数据类型(通常是半精度)进行经典计算,最后将两部分结果合并。这种方式已被证明可以保持模型的完整性能,而不会造成性能下降。
-
SmoothQuant:这种方法专门应用于 W8A8 方案,即权重和激活值都量化为 8 比特精度。该论文同样处理大语言模型中的离群特征问题,并提出通过平滑激活值和权重来缓解此问题。具体做法是根据输入激活值确定一个平滑因子,将量化难度从激活值迁移到权重上,使得权重和激活值的量化难度均衡。这样也能保留模型的全部能力。

- AWQ:这篇较新的论文也以特殊方式处理离群特征。它来自与 SmoothQuant 相同的实验室。其核心思想是,首先在一个校准数据集上进行迭代,以详细了解输入权重中哪些通道可能负责生成离群特征(称为显著权重)。然后,利用这一信息在量化前缩放模型权重,并在推理时使用相同的缩放因子来重新调整输入。
以上只是众多专门解决大语言模型高效量化问题的论文中的一小部分。如果你对此感到好奇,我邀请你详细阅读这些论文,深入理解它们。量化大语言模型时,除了离群特征,还存在其他挑战。


量化领域的其他挑战与资源 🔍
量化感知训练领域目前可能探索得还不够充分,在低比特下训练模型也是一个有趣的话题。此外,还有硬件支持有限的挑战。本课程主要关注 W8A16 方案,但对于更高效的 W8A8 方案,并非所有硬件都支持 8 比特运算。校准数据集的挑战也值得注意,某些量化方法需要校准数据集来进行模型预处理,以优化量化效果。当然,还有数据分布与打包的挑战。
如果你对这个话题真正感兴趣,我邀请你进行进一步的阅读。例如,可以查阅量化领域的综述论文。麻省理工学院的 HAN Lab 也发布过一些优秀的量化综述,提供了很好的学习资源。你还可以查看 Hugging Face Transformers 库的量化文档和博客文章。浏览 Llama.cpp 仓库的讨论区也能发现许多富有洞察力的实验和讨论。此外,Reddit 上有一个名为 r/LocalLLaMA 的子版块,那里分享了很多关于量化的精彩见解和新方法。
当然,可能还有很多其他资源未被提及,但这些是我所知道的。本节课就到这里,希望你通过本课程学到了很多,并能将我们展示的内容应用到你的工作或项目中。希望所有这些能给你带来一些灵感,去尝试一些很酷的事情。

感谢你学习本课程,我们将在下一个视频中再见。
018:总结与展望 🎯

在本节课中,我们将对这门关于模型量化的简短课程进行总结,回顾所学到的核心知识与技能,并展望未来的学习方向。

祝贺你完成了这门短期课程的学习。在课程中,你尝试了线性量化方法的不同变体,并使用 PyTorch 从零开始实现了它们。你还构建了一个量化器,用于将任何模型量化为 8 位精度。最后,你了解了量化过程中的一些重要挑战,例如权重打包,并手动实现了打包和解包算法。
我们鼓励你探索 Hugging Face Transformers 库中提供的其他量化方法。我们希望这门课程能为你提供量化任何模型所需的所有工具。
如果你觉得这门课程有帮助,或许可以与你的朋友们分享。
总结
本节课中我们一起学习了模型量化课程的总结。我们回顾了从实现线性量化方法、构建量化器到处理权重打包挑战的完整学习路径。课程为你提供了实践性的工具和理论基础,帮助你迈出模型量化的第一步。

浙公网安备 33010602011771号