在大型语言模型(LLM)和高性能计算领域,模型量化技术正变得越来越重要。FP8(8位浮点数)作为一种新兴的数据类型,正在成为AI加速器(如NVIDIA H100)的关键特性。本文将深入剖析两种FP8量化算法:per_tensor_quant_fp8(张量级量化)和per_token_quant_fp8(令牌级量化),通过源代码解读其实现原理、数学本质和工程考量。
源代码参考sglang的实现添加链接描述
E4M3格式解析
在深入量化算法之前,我们需要理解FP8 E4M3格式。E4M3表示:
E4: 4位指数位(exponent)
M3: 3位尾数位(mantissa)
这种格式能表示的最大值为448.0,计算方式如下:
最大值 = 1.111(二进制)× 2^(14) = 1.875 × 16 = 448.0
其中指数最大值为14(指数位全1但排除特殊值),尾数最大值为0.875(3位全1)
FP8相比FP16和BF16:
存储节省: 内存占用减少50%
带宽效率: 数据传输量减半
计算吞吐: 理论上计算密度可提升2倍
per_tensor_quant_fp8
per_tensor_quant_fp8采用全局统一的量化尺度,对输入张量的所有元素使用相同的缩放因子。这种设计的核心思想是:保持张量整体的统计特性,以最简单的形式实现量化。
python代码参考
python
FP8_E4M3_MAX = 448.0
def per_tensor_quant_fp8_torch(x, symmetric):
if symmetric == False:
return
else:
absmax = x.flatten().abs().max()
if absmax == 0:
scale = torch.tensor(1.0, device=x.device, dtype=torch.float32)
q = torch.zeros_like(x, dtype=torch.float8_e4m3fn)
return q, scale, None
# 2. scale = absmax / FP8_MAX
scale = absmax / FP8_E4M3_MAX
# 3. 量化(注意:CUDA 里用的是 x * (1 / scale))
inv_scale = 1.0 / scale
x_scaled = x * inv_scale
# 4. clip 到 FP8 可表示范围
x_clamped = torch.clamp(x_scaled, -FP8_E4M3_MAX, FP8_E4M3_MAX)
# 5. cast to fp8 e4m3
q = x_clamped.to(torch.float8_e4m3fn)
return q, scale.float(), None
输入特性:
形状:任意维度(1D、2D、3D、4D等)
数据类型:通常为float32、float16或bfloat16
数值范围:理论上任意实数范围
输出特性:
q形状:与输入x完全相同
q数据类型:torch.float8_e4m3fn(FP8 E4M3格式)
scale形状:标量(0维张量)
scale数据类型:torch.float32
per_tensor量化优缺点
优点:
计算简单,只需一次reduce操作
存储开销小,只需一个scale值
适合整体分布均匀的张量
缺点:
对异常值敏感,可能导致量化分辨率下降
无法适应局部特征的差异性
适合per_tensor的场景:
激活值分布相对均匀
对计算延迟极度敏感
存储带宽受限的部署环境
我针对sglang的代码做了一个实现得到对应的cuda代码,参考
添加链接描述
per_token_quant_fp8
python代码参考
python
FP8_E4M3_MAX = 448.0
def per_token_quant_fp8_torch(x, symmetric):
if symmetric == False:
return
else:
assert x.dim() == 2, "per-token quant expects [num_tokens, hidden_dim]"
# ------------------------------------------------------------
# Pass-1: per-token absmax (对齐 CUDA 的 warpReduceMax)
# ------------------------------------------------------------
# CUDA: max_value = max_j |x[token_id, j]|
absmax = x.abs().amax(dim=1) # [num_tokens]
# ------------------------------------------------------------
# scale = absmax / FP8_E4M3_MAX
# CUDA 中 scale 是 per-token 写入 output_s[token_id]
# ------------------------------------------------------------
scale = absmax / FP8_E4M3_MAX # [num_tokens]
inv_scale = 1.0 / scale
inv_scale[scale == 0] = float("inf") # [num_tokens]
# ------------------------------------------------------------
# Pass-2: x * inv_scale
# CUDA: val = input * scale_inv
# ------------------------------------------------------------
x_scaled = x * inv_scale.unsqueeze(1) # broadcast to [N, H]
# ------------------------------------------------------------
# clip 到 FP8 E4M3 可表示范围
# CUDA: fmaxf(fminf(val, FP8_E4M3_MAX), -FP8_E4M3_MAX)
# ------------------------------------------------------------
x_clamped = torch.clamp(x_scaled, -FP8_E4M3_MAX, FP8_E4M3_MAX)
# ------------------------------------------------------------
# cast to FP8
# CUDA: static_cast<DST_DTYPE>(val)
# ------------------------------------------------------------
q = x_clamped.to(torch.float8_e4m3fn)
return q, scale.float(), None
输入形状要求:
必须为2维张量:[num_tokens, hidden_dim]
num_tokens:序列中的token数量
hidden_dim:每个token的特征维度
设计理由:
这种形状要求源于Transformer架构:
第一维(token维度):序列中的不同位置
第二维(特征维度):每个token的嵌入表示
输出特性:
q形状:[num_tokens, hidden_dim](与输入相同)
q数据类型:torch.float8_e4m3fn
scale形状:[num_tokens](每个token一个缩放因子)
scale数据类型:torch.float32
per_token量化优缺点
优点:
更好的适应性,每个token获得最优分辨率
对长尾分布有更好的处理能力
在Transformer类模型中表现优异
缺点:
计算开销大,需要per-token的reduce
存储多个scale值,增加元数据开销
实现复杂度高
适合per_token的场景:
Transformer-based语言模型
激活值存在明显的token间差异
对模型精度要求高
我针对sglang的代码做了一个实现得到对应的cuda代码,参考
添加链接描述
per_tensor_quant_fp8和per_token_quant_fp8代表了两种不同的量化哲学:统一性与适应性。前者追求简单高效,后者强调精度保持。在实际应用中,选择哪种算法需要权衡计算效率、存储开销和模型精度。随着AI硬件对FP8支持的成熟,这些量化技术将在模型部署中发挥越来越重要的作用。