张量级FP8量化详解 | AI生成和翻译

Home 2026.07

问题:解释这个 _to_fp8() 函数以及张量级 FP8 量化背后的数学原理。

答案:

这是动态对称张量级量化

目标是:

在尽可能保留动态范围的前提下,将浮点张量转换为 FP8,然后返回矩阵乘法中恢复原始值所需的缩放因子。

关键在于:

FP8 张量本身没有用。 它总是与一个缩放因子配对使用。


第 0 步:禁用梯度

@torch.no_grad()

量化不属于反向传播的一部分。

这告诉 PyTorch:


第 1 步

fp8_max = torch.finfo(fp8_dtype).max

假设

fp8_dtype = torch.float8_e4m3fn

fp8_max ≈ 448

对于另一种 FP8 格式

e5m2

max ≈ 57344

这仅仅是

FP8 能表示的最大数

第 2 步

amax = x.float().abs().max()

假设

x =
[
    2,
   -5,
    9,
    1
]

amax = 9

注意

只有一个标量

作用于整个张量

这就是为什么它被称为

张量级缩放 (Tensorwise scaling)

而不是

行级 (Rowwise)
列级 (Columnwise)
块级 (Blockwise)

第 3 步

scale = fp8_max / amax

假设

fp8_max = 448

amax = 9

scale = 49.777...

为什么?

因为我们需要

9

↓

448

所以每个值都要乘以

448 / 9

可以理解为将张量拉伸,直到其最大值恰好填满 FP8 的范围。

原始范围

[-9,9]

缩放后

[-448,448]

这最大化了精度,因为它几乎使用了全部可表示的范围。


为什么需要 clamp?

clamp(min=EPS)

假设

amax = 0

那么

448 / 0

会产生无穷大。

取而代之

amax = max(amax, EPS)

避免了除以零。


为什么使用双精度?

amax.double()

注释中提到

compile
vs

eager

问题在于

float32 除法

可能会产生微小的舍入差异。

例如

448 / 0.3

根据优化方式的不同,

1493.333374

或

1493.333252

这些微小差异可能导致数值落在 FP8 舍入边界的不同侧。

使用

float64

使得两种执行模式都具有确定性。


第 4 步

x_scaled = x.float() * scale

假设

x =
[
2,
5,
9
]

缩放因子

49.777

结果

[
99.5,
248.9,
448
]

现在张量完全占满了 FP8 的数值范围。


第 5 步

x_clamped = x_scaled.clamp(
    -fp8_max,
     fp8_max
)

假设数值误差给出了

449.2

FP8 无法表示该值。

如果不进行 clamp,

转换时可能溢出或产生未定义行为(取决于实现和 dtype)。Clamping 确保值在转换前保持在有效的 FP8 范围内。


第 6 步

x_fp8 = x_clamped.to(fp8_dtype)

现在转换

248.9

为 FP8。

请记住

FP8 具有

因此

248.9

可能变成

248

256

具体取决于最近的 FP8 可表示值。

这就是引入量化误差的地方。


第 7 步

inv_scale = scale.reciprocal()

不是返回

scale

而是返回

1 / scale

为什么?

因为

torch._scaled_mm 期望:

real_value

=

fp8_value

×

inverse_scale

假设

原始值

9

缩放后

448

存储的

448 (FP8)

之后

448 × (9/448)

=

9

不需要显式的反量化步骤。在矩阵乘法过程中,torch._scaled_mm 会在内部将 FP8 操作数乘以各自的逆缩放因子,同时以更高精度累加。


整个流程

FP32 张量

      │

      ▼

找出 max(abs(x))

      │

      ▼

计算

scale

=

fp8_max / amax

      │

      ▼

乘以张量

      │

      ▼

Clamp

      │

      ▼

转换为 FP8

      │

      ▼

存储

(
FP8 张量,
逆缩放因子
)

为什么采用张量级缩放?

优点:

只有一个缩放因子

元数据极少

速度快

cuBLAS 直接支持

缺点:

假设

[
0.001,
0.002,
0.003,
100
]

缩放因子由

100

决定。

缩放后,

0.001

会变成一个非常小的 FP8 值,可能丢失大量精度,因为动态范围被大的离群值主导。

行级或块级缩放会计算多个缩放因子,为小值保留更多精度,但代价是增加额外的元数据和更复杂的内核。


为什么返回 (x_fp8, inv_scale) 而不是稍后反量化?

现代 FP8 GEMM 内核(例如 torch._scaled_mm 背后的内核)融合了计算:

FP8 A
FP8 B
↓

相乘

↓

应用逆缩放因子

↓

在 FP16/BF16/FP32 中累加

↓

输出

这避免了在内存中实例化反量化的 FP16 张量,减少了带宽并提高了性能。硬件实际上计算:

[ C = (A_{fp8} \times s_A^{-1}) \times (B_{fp8} \times s_B^{-1}) ]

同时以更高精度执行累加,这就是为什么该函数同时返回量化后的张量及其逆缩放因子,而不是先重建原始张量。


Back Donate