Prhub

#27883 Fix fp16 NaN flake in spec CI: bf16 eagle fixture; sanitize NaN logits in sampler

原始 PR 作者 hnyls2002 合并时间 2026-06-11 16:16 文件变更 5 提交数 8 评论 2 代码增减 +80 / -3

执行摘要

修复 spec CI fp16 NaN 问题:测试改用 bf16 并清理 NaN logits

原作提到:fp16 activation overflow on a degenerate draft branch during EAGLE verify becomes Inf -> NaN under RMSNorm。导致 CI 中 test_spec_eagle_fa3.py 出现不稳定的 NaN 设备断言失败。

值得精读。特别是 _AsyncNanWarner 的设计(利用 pin_memory 避免同步)和 sanitize_nan_logits 的数值选择(+-1e30 而非 dtype min/max 以避免温度缩放后溢出)。

讨论亮点

无公开 review 讨论。作者通过提交序列逐步完善,从简单切 bf16 到加入通用 NaN 清理机制,并最终确定环境变量默认开启。

实现拆解

  1. async_probe.py 中新增 _AsyncNanWarner 类,利用 pin_memory 实现无同步的 NaN 检测与限速警告。
  2. 新增 sanitize_nan_logits 函数,在 CI 中做断言,在生产环境默认将 NaN/Inf 替换为安全数值。
  3. environ.py 注册 SGLANG_SANITIZE_NAN_LOGITS 环境变量(默认 True)。
  4. sampler.py_preprocess_logitseagle_info_v2.pysample 方法开头加入 sanitize_nan_logits 调用。
  5. 将测试 fixture 的 dtype 从 float16 改为 bfloat16,从根本上避免 fp16 溢出。
文件 模块 状态 重要度
python/sglang/srt/utils/async_probe.py 异步断言 modified 8.28
python/sglang/srt/speculative/eagle_info_v2.py 推测解码 modified 5.47
python/sglang/srt/layers/sampler.py 采样器 modified 5.07
python/sglang/srt/environ.py 配置层 modified 4.99
python/sglang/test/server_fixtures/spec_eagle_fixture.py 测试夹具 modified 3.99

关键符号

sanitize_nan_logits _AsyncNanWarner.check maybe_warn_nan Sampler._preprocess_logits EagleVerifyInput.sample

关键源码片段

python/sglang/srt/utils/async_probe.py core-logic

核心变更文件:新增 _AsyncNanWarner 异步 NaN 检测类、maybe_warn_nan 和 sanitize_nan_logits 函数,构成整个防御机制的核心。

class _AsyncNanWarner:
    """One-shot NaN monitor: device-side detection lands in pinned host
    memory without any stream sync; the host reads the (slightly stale) flag
    on a later call, warns once, and stops detecting."""
​
    def __init__(self):
        self._dev = None # device int32 tensor, initialized lazily
        self._host = None # pinned host mirror, read without sync
        self._warned = False
​
    def check(self, tensor: torch.Tensor, msg: str):
        # If already warned or tensor is not CUDA, skip
        if self._warned or not tensor.is_cuda:
            return
        # Lazily allocate device and pinned host buffers
        if self._dev is None:
            self._dev = torch.zeros(1, dtype=torch.int32, device=tensor.device)
            self._host = torch.zeros(1, dtype=torch.int32, pin_memory=True)
​
        # Report a hit enqueued on an earlier step (pinned read, no sync).
        if int(self._host[0]):
            logger.warning(
                "NaN detected in %s; values were sanitized before sampling. "
                "This usually indicates numerical overflow (e.g. fp16 "
                "activations) or an upstream bug producing NaN. "
                "Logged once; further occurrences are silent.",
                msg,
            )
            self._warned = True
            return
​
        # Enqueue this step's detection (async, no sync).
        self._dev.add_(torch.isnan(tensor).any().to(torch.int32))
        self._host.copy_(self._dev, non_blocking=True)
​
​
_nan_warner = _AsyncNanWarner()
​
​
def maybe_warn_nan(tensor: Optional[torch.Tensor], msg: str = ""):
    """Non-fatal counterpart of maybe_detect_nan: throttled sync-free warning
    instead of crashing. Callers sanitize the tensor themselves."""
    if envs.SGLANG_ENABLE_ASYNC_ASSERT.get():
        return # hard assert already covers detection
    if tensor is None:
        return
    _nan_warner.check(tensor, msg)
​
​
def sanitize_nan_logits(logits: torch.Tensor, msg: str = ""):
    """Detect NaN (assert in CI, throttled warning in prod), then sanitize in
    place: NaN logits (e.g. fp16 activation overflow) are undefined behavior
    in sampling kernels and can come back as out-of-vocab token ids. +-1e30
    rather than dtype min/max because callers divide logits by temperature,
    which would overflow dtype min/max to +-Inf and softmax back to NaN."""
    maybe_detect_nan(logits, msg)
    if not envs.SGLANG_SANITIZE_NAN_LOGITS.get():
        return
    maybe_warn_nan(logits, msg)
    torch.nan_to_num_(logits, nan=-1e30, posinf=1e30, neginf=-1e30)
python/sglang/srt/speculative/eagle_info_v2.py dependency-wiring

在 EAGLE 验证的 sample 方法中调用 sanitize_nan_logits,确保 NaN 不会进入采样 kernel。

# 在 import 中新增导入
from sglang.srt.utils.async_probe import (
    maybe_detect_nan,
    maybe_detect_oob,
    sanitize_nan_logits,
)# 在 sample 方法中,获取 logits 后立即清理
next_token_logits = logits_output.next_token_logits
sanitize_nan_logits(next_token_logits, "verify: target model logits")
# 之后才进行 penalty、grammar 等操作

评论区精华

NaN 处理方案 设计

PR body 描述了 fp16 溢出根因和修复方案:测试切 bf16 + 采样前清理 NaN。作者在提交历史中逐步迭代,从简单切 bf16 到增加通用 sanitize 机制。

结论:采用 bf16 测试 fixture 避免溢出,并通过 sanitize_nan_logits 在所有采样路径清理 NaN。 · 已解决

风险与影响

sanitize_nan_logits 使用 torch.nan_to_num_ 会带来小幅 CUDA kernel 开销,但通常可忽略。默认启用可能掩盖上游 NaN 产生的 bug,但 CI 中的 maybe_detect_nan 会在启用 SGLANG_ENABLE_ASYNC_ASSERT 时捕获。建议在关键推理场景中保持此功能开启。

直接影响:修复 spec EAGLE 测试的不稳定性,避免 CI 误报。间接影响:为所有采样路径提供 NaN 清理安全网,防止采样 kernel 产生越界 token。影响范围限定在 speculative decoding 和 sampler 模块。

数值精度变更 新增默认开启环境变量

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论