Prhub

#46658 [ROCm][CI] Relax fused layernorm quant test tolerances for one-ULP outliers

原始 PR 作者 divakar-amd 合并时间 2026-06-27 10:44 文件变更 4 提交数 2 评论 4 代码增减 +90 / -55

执行摘要

放松 ROCm 融合层归一化量化测试的 ULP 容忍度

PR body 说明:融合 RMSNorm + quant kernel 与参考路径因 fp32 归约顺序和 FMA 收缩不同,偶尔导致单组 bf16 abs-max 翻转 1 ULP,或元素跨 E4M3 绑定边界,造成 CI 测试失败。这是跨平台精度伪影,而非 kernel 缺陷,因此需要放宽测试容忍度。

值得精读。PR 展示了处理跨平台精度问题的规范方法:分析根因(归约顺序)、定义 ULP 测量工具、在测试中设置平台特定容忍边界,并保持主要路径严格。其提取的 bf16_ulp_distancefp8_ulp_distance 可作为后续 ROCm 测试中的公共组件。

讨论亮点

mawong-amd 建议利用已在 #45681 中添加的 ULP 距离工具,并提到可将其通用化到 tests.kernels.utils。AndreasKaratzas 表示同意。tjtanaa 要求仅对 ROCm 放宽断言,CUDA 保留原样。divakar-amd 采纳了所有建议,重构并添加了 ROCm 守卫。最终 tjtanaa 批准。

实现拆解

  1. 提取通用 ULP 距离函数:将 test_fused_deepseek_v4_kv_insert.py 中的私有函数 _bf16_ulp_distance_fp8_ulp_distance 移动到 tests/kernels/utils.py 作为公共函数 bf16_ulp_distancefp8_ulp_distance,并保留相同的 bit 重新解释逻辑。
  2. 改进 test_fused_quant_layernorm.py 的 scale 检查:引入内嵌函数 scales_close,在 ROCm + bf16 + 分组量化的条件下,对 scale 比较允许较大的相对容差(rtol=1e-2),而其他路径(per-token、fp32、CUDA)维持原有严格 torch.allclose
  3. fp8 输出异常值容忍:在 test_fused_quant_layernorm.py 中,当 fp8 直接比较失败且为 ROCm 条件时,改用 fp8_ulp_distance 统计异常元素量,允许不超过 numel//100k + 8 个非零 ULP 元素,其余元素必须完全一致。
  4. 同步修改 test_layernorm.py:将原先全局的 assert_close(atol/rtol=1e-3) 替换为平台分派:ROCm 使用异常值计数检查,CUDA 保持原样。
  5. 清理测试文件:删除 test_fused_deepseek_v4_kv_insert.py 中重复的私有函数定义,改为从 utils 导入。
文件 模块 状态 重要度
tests/kernels/test_fused_deepseek_v4_qnorm_rope_kv_insert.py Deepseek 内核 modified 5.94
tests/kernels/core/test_fused_quant_layernorm.py 层归一化 modified 6.25
tests/kernels/utils.py 测试工具 modified 6.39
tests/kernels/core/test_layernorm.py 层归一化 modified 4.94

关键符号

bf16_ulp_distance fp8_ulp_distance scales_close

关键源码片段

tests/kernels/core/test_fused_quant_layernorm.py test-coverage

引入 `scales_close` 函数,为 ROCm 上 bf16 分组量化 scale 和 fp8 输出增加平台特定容忍逻辑。

    # Per-block bf16 scales: allow a small relative tolerance for a few groups
    # whose abs-max flips by one ULP between the fused and reference paths. The
    # per-token and fp32 paths stay strict.
    relax_block_rocm = (
        group_size is not None
        and dtype == torch.bfloat16
        and current_platform.is_rocm()
    )
​
    def scales_close(rtol: float, atol: float) -> bool:
        if torch.allclose(ref_scales, ops_scales, rtol=rtol, atol=atol):
            return True
        # ROCm bf16 block scales: relax relative tolerance to 1e-2 (~one bf16 ULP)
        return relax_block_rocm and torch.allclose(
            ref_scales, ops_scales, rtol=1e-2, atol=atol
        )
​
    if quant_dtype == torch.int8:
        assert scales_close(rtol=1e-5, atol=1e-6)
        assert torch.allclose(ref_out, ops_out, atol=1)
    else:
        assert scales_close(rtol=1e-5, atol=1e-8)
        a = ref_out.to(dtype=torch.float32)
        b = ops_out.to(dtype=torch.float32)
        ok = torch.allclose(a, b, atol=1e-6)
        if not ok:
            if relax_block_rocm:
                # ULP-flipped group scale can cross an E4M3 tie; tolerate a
                # bounded count of isolated fp8 outliers.
                ulp = fp8_ulp_distance(ref_out, ops_out)
                max_outliers = ulp.numel() // 100_000 + 8
                ok = int((ulp > 0).sum().item()) <= max_outliers
            else:
                # CUDA (& non-bf16): compare dequantized values with relaxed tolerance.
                if group_size is None:
                    a_deq = a * ref_scales.view(-1, 1)
                    b_deq = b * ops_scales.view(-1, 1)
                else:
                    a_deq = a * ref_scales.repeat_interleave(group_size[1], dim=1)
                    b_deq = b * ops_scales.repeat_interleave(group_size[1], dim=1)
                ok = torch.allclose(a_deq, b_deq, rtol=5e-2, atol=5e-2)
        assert ok
tests/kernels/utils.py test-coverage

新增 `bf16_ulp_distance` 和 `fp8_ulp_distance` 公共函数,供多个测试文件复用,是本次重构的核心。

def bf16_ulp_distance(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
    """返回两个 bf16 张量之间的可表示步长距离。    通过 reinterpret 为 int16 并应用 IEEE-754 全序映射,
    使得相邻可表示值的距离为 1。
    """
​
    def key(t: torch.Tensor) -> torch.Tensor:
        # 提取低 16 位并转换为 int64
        u = t.contiguous().view(torch.int16).to(torch.int64) & 0xFFFF
        # 全序映射:负数(MSB 为 1)翻转所有位,正数保持 + 0x8000
        return torch.where(u >= 0x8000, 0xFFFF - u, u + 0x8000)
​
    return (key(a) - key(b)).abs()
​
​
def fp8_ulp_distance(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
    """返回两个 fp8 张量之间的可表示步长距离。    输入必须具有相同的 fp8 编码(例如都是 float8_e4m3fn)。
    通过 reinterpret 为 uint8 并应用符号-幅度全序映射。
    """
​
    def key(t: torch.Tensor) -> torch.Tensor:
        u = t.contiguous().view(torch.uint8).to(torch.int64)
        # 全序映射:负数(MSB=1)翻转所有位,正数保持 + 0x80
        return torch.where(u >= 0x80, 0xFF - u, u + 0x80)
​
    return (key(a) - key(b)).abs()

评论区精华

复用已有的 ULP 距离工具 设计

mawong-amd 指出 #45681 中已添加类似的 ULP 距离检查,建议将其通用化到 tests.kernels.utils 并在此处使用。

结论:已采纳,将私有函数转移到 utils.py 并公开。 · 已解决

仅对 ROCm 放宽断言 正确性

tjtanaa 要求保持 CUDA 的原始严格断言,只对 ROCm 使用放宽条件。

结论:已添加 `current_platform.is_rocm()` 守卫,CUDA 路径不变。 · 已解决

风险与影响

主要风险是放宽测试容忍度可能掩盖真实 kernel 错误。但 PR 提供了充分分析:差异被确认为 1 ULP 级别,影响极少数元素(如 16k 组中 1 组、3M 元素中 2 个),且 CUDA 路径保留原始严格检查。ROCm 特定的容忍度有明确边界(异常值计数上限),整体风险可控。

直接影响 ROCm CI 的四个测试文件,使原本因精度极限差异而失败的 case 通过,消除 CI 噪声。不影响 CUDA、Intel GPU、CPU 等其他平台。对用户无功能影响,仅改测试代码。

测试容忍度放宽 仅限 ROCm 路径

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论