【Bug已解决】[Bug]: GPU coredump during FlashInfer `trtllm_bf16_moe` autotune with Qwen3.5-35B-A3B on B2
【Bug已解决】[Bug]: GPU coredump during FlashInfertrtllm_bf16_moeautotune with Qwen3.5-35B-A3B on B200 (DP=2 + EP) 解决方案
一、现象长什么样
在 B200(Blackwell, sm_100)上跑Qwen3.5-35B-A3B(MoE),并行配置是DP=2 + EP,首次推理时在 FlashInfer 的 MoE 内核autotune(自动调优)阶段整个进程直接GPU coredump:
GPU coredump captured for process ... (SIGSEGV in trtllm_bf16_moe kernel)或者驱动层看到:
CUDA error: an illegal instruction was encountered (during FlashInfer autotune)几个特征:
- 崩在autotune 阶段,不是正常推理阶段——也就是「找最优 kernel 配置」那一步就炸了。
- 只在B200上炸;换 H100/H200 能过(sm_90 上那个坏候选没被选中或不触发)。
- 只在
Qwen3.5-35B-A3B+DP=2 + EP这个组合下炸;单独 EP 或更小模型有时能过。 - 崩的是进程级 coredump,Python 层面抓不到异常(因为 GPU 硬件故障直接 SIGSEGV,不会变成 Python 异常),只能重启。
本质:FlashInfer 的 autotune 会枚举一堆候选 kernel 配置(不同 tile 大小、stage 数、workspace 大小)逐一试跑,找出最快的。其中某个候选配置在 B200 + DP=2+EP 的特定张量形状下,算出的 workspace / scratch 尺寸或 tile 维度越界,触发 GPU 硬件缺页 → coredump。autotune 在试跑前没有校验候选的「可行性」,于是坏候选一launch就炸穿进程。
二、背景
先说 autotune 是什么。GPU kernel 的性能对「块大小(block size)、流水线级数(num stages)、shared memory 用量」这些参数极度敏感。FlashInfer 为了在给定形状下跑最快,会在第一次遇到某个形状时,枚举一组候选配置,每个都实际 launch 一次、测耗时,选最快的。这个过程叫 autotune,结果会缓存起来供后续复用。
问题出在「枚举的候选里有一个在 B200 上是坏的」:
- B200 是 sm_100,shared memory / 寄存器 / workspace 上限和 sm_90 不同。某个候选用了一个在 sm_90 合法、但在 sm_100 上溢出的 tile / stage 组合。
Qwen3.5-35B-A3B+DP=2 + EP决定了 MoE 的「token 数 × expert 数 × 隐藏维度」形状。autotune 针对这个形状挑候选,恰好命中那个坏候选。- 坏候选 launch 时,workspace 缓冲区被越界写(或 tile 维度触发非法指令),GPU 硬件直接 fault → 进程 coredump。
为什么 Python 抓不到?因为 coredump 是GPU 硬件级故障沿驱动上报为 SIGSEGV,发生在 GPU 侧、异步于 Python 解释器,Python 的try/except管不到。所以这类问题不能靠「try 一下」来恢复,只能靠「别让坏候选被 launch」来预防。
三、根因
根因是autotune 在 launch 候选 kernel 前,没有校验候选配置在当前设备(B200)和当前张量形状下的「硬件可行性」,导致一个 workspace/tile 越界的坏候选被实际执行、触发 GPU coredump,三层:
第一层(主因):候选配置未做设备能力校验。autotune 的候选列表是「写死的一组经验配置」,没有针对sm_100的 workspace 上限、max shared memory、max tile 维度做过滤。于是 sm_90 上合法的候选,在 B200 上 launch 即越界。
第二层:workspace 大小按「乐观估计」分配,没留校验余量。trtllm_bf16_moe的 autotune 候选需要一块 scratch workspace。某候选实际需要的 workspace 比分配的大(因为 B200 上 token 分布形状不同),写入越界 → GPU 缺页。分配侧和 launch 侧对 workspace 大小的算法不一致,是典型根因。
第三层:DP=2+EP 放大了触发概率。EP 下 token 要先 all-to-all 重排,token 在每张卡上的「每 expert 的 token 数」分布特殊;DP=2 又让这个分布翻倍变化。autotune 正是在这个特殊形状下枚举候选,恰好选中坏候选的概率被这两个并行维度显著放大。
一句话:autotune 枚举的候选里有一个在 B200+DP/EP 形状下 workspace/tile 越界,launch 即 GPU coredump,且 Python 无法捕获只能预防。
四、最小可运行复现
下面用纯 Python 模拟「autotune 枚举候选、不校验设备能力就 launch、坏候选触发 crash(不可恢复的 SIGSEGV 等价物)」的控制流,不需要 GPU:
class DeviceCaps: def __init__(self, sm, max_shared_mem, max_workspace): self.sm = sm self.max_shared_mem = max_shared_mem self.max_workspace = max_workspace def candidate_configs(): # 经验写死的候选列表(含一个对 sm_100 越界的) return [ {"tile": 64, "stages": 2, "workspace": 1024}, # sm_90 良性 {"tile": 256, "stages": 8, "workspace": 8192}, # sm_90 良性 {"tile": 512, "stages": 16, "workspace": 99999}, # sm_100 上越界! ] def launch(cfg, caps): # 不校验直接 launch:坏候选触发不可恢复故障 if cfg["workspace"] > caps.max_workspace or cfg["tile"] > 256: raise RuntimeError("GPU coredump (SIGSEGV) - unrecoverable") return cfg def autotune_buggy(caps, shape): best = None for cfg in candidate_configs(): try: launch(cfg, caps) except RuntimeError: # 注意:真实的 GPU coredump 根本到不了 except,进程已死 # 这里只是演示「若能被捕获也不该让坏候选静默通过」 continue best = cfg return best def main(): caps = DeviceCaps(sm=100, max_shared_mem=256, max_workspace=8192) try: autotune_buggy(caps, "dp2_ep") except RuntimeError as e: print("复现成功:", e) if __name__ == "__main__": main()跑出来会打印复现成功: GPU coredump (SIGSEGV) - unrecoverable,模拟了「坏候选 launch 即崩、且本不该被静默放过」。真实场景里这一步直接 coredump,连except都进不去。
五、解决方案(第一层:最小直接修复)
最省事的救火:关掉 autotune,固定使用一个已知在 B200 上安全的 kernel 配置,避免坏候选被 launch:
# FlashInfer / vLLM 通常提供关闭 autotune 的开关 llm = LLM( model="Qwen3.5-35B-A3B", # 关键:禁用 MoE kernel autotune,使用默认(经验安全)配置 override_pooler_config=None, # 或通过环境变量 / 编译配置固定 compilation_config={ "cudagraph_capture_mode": "kernel", }, )更具体,FlashInfer 的 MoE 常支持环境变量跳过 autotune:
# 禁用 FlashInfer 的 trtllm_moe autotune(不同版本变量名可能不同) export FLASHINFER_DISABLE_MOE_AUTOTUNE=1 # 或固定 workspace 上限 export TRTLLM_MOE_MAX_WORKSPACE=8192 vllm serve Qwen3.5-35B-A3B --tensor-parallel-size 1 ...如果你确认哪个候选是坏的,也可以把并行度降一档(比如 DP=1 或 EP 减小组)避开那个触发形状——这是临时规避的常用手段。
六、解决方案(第二层:结构性改进)
第一层是「避开 autotune」,第二层是「让 autotune 在 launch 前校验候选的设备可行性,并给 workspace 留硬上限」,从设计上消灭坏候选被 launch:
from dataclasses import dataclass from typing import List, Dict @dataclass class DeviceLimits: sm: int max_shared_mem: int max_workspace: int max_tile: int def feasible(self, cfg: Dict) -> bool: # 候选必须满足设备硬限制,否则直接排除 if cfg["workspace"] > self.max_workspace: return False if cfg["tile"] > self.max_tile: return False if cfg["stages"] * cfg["tile"] > self.max_shared_mem: return False return True def autotune_safe(limits: DeviceLimits, shape: str) -> Dict: feasible = [c for c in candidate_configs() if limits.feasible(c)] if not feasible: raise RuntimeError( f"没有可行的 MoE kernel 候选(shape={shape})," "请检查 B200 的 workspace 上限配置或降低并行度" ) # 在「已校验可行」的候选里选最快(这里用估算耗时代替真实 launch) return min(feasible, key=lambda c: estimate_cost(c, shape)) def estimate_cost(cfg: Dict, shape: str) -> float: # 真实实现里是 launch 测时;这里用近似 return cfg["tile"] * cfg["stages"] / 1000.0同时把 workspace 分配改成「按 limits 校验后分配,且 launch 前再比对一次」:
def allocate_workspace(cfg: Dict, limits: DeviceLimits) -> int: need = compute_workspace_need(cfg) # launch 侧真实需求 assert need <= limits.max_workspace, ( f"候选 {cfg} 需要 workspace {need} > 上限 {limits.max_workspace}" ) return need这样坏候选在「进入 launch」之前就被feasible过滤掉,永远不会触发 GPU coredump。
七、解决方案(第三层:断言 / CI 守护)
把「候选可行性校验」「workspace 不越界」「B200 专用候选集」固化成测试:
import pytest def test_bad_candidate_excluded_on_b200(): limits = DeviceLimits(sm=100, max_shared_mem=256, max_workspace=8192, max_tile=256) feasible = [c for c in candidate_configs() if limits.feasible(c)] # 那个 workspace=99999 的坏候选必须被排除 assert all(c["workspace"] <= 8192 for c in feasible) def test_autotune_safe_returns_feasible(): limits = DeviceLimits(sm=100, max_shared_mem=256, max_workspace=8192, max_tile=256) best = autotune_safe(limits, "dp2_ep") assert limits.feasible(best) def test_autotune_raises_when_none_feasible(): limits = DeviceLimits(sm=100, max_shared_mem=1, max_workspace=1, max_tile=1) with pytest.raises(RuntimeError): autotune_safe(limits, "dp2_ep") def test_workspace_alloc_checked(): limits = DeviceLimits(sm=100, max_shared_mem=256, max_workspace=8192, max_tile=256) bad = {"tile": 64, "stages": 2, "workspace": 99999} with pytest.raises(AssertionError): allocate_workspace(bad, limits) def test_no_coredump_shape_dp_ep(): # 端到端:DP=2+EP 形状下,autotune 不应选中坏候选 limits = DeviceLimits(sm=100, max_shared_mem=256, max_workspace=8192, max_tile=256) chosen = autotune_safe(limits, "dp2_ep") assert chosen["workspace"] <= limits.max_workspace再加一个「禁用 autotune 开关」的回归:
def test_disable_autotune_uses_safe_default(): cfg = resolve_moe_config(disable_autotune=True, device="B200") assert cfg["autotune"] is False assert cfg["tile"] <= 256 # 用经验安全默认,不枚举八、排查清单
- 看是否崩在 autotune 阶段(日志里有
autotune/trtllm_bf16_moe/tuning),且是进程级 coredump(非 Python 异常)→ 坐实本问题。 - 是否只在 B200(sm_100)炸、H100 能过 → 是设备相关候选越界。
- 临时救火:
FLASHINFER_DISABLE_MOE_AUTOTUNE=1关 autotune;或降 DP/EP 避开触发形状。 - 检查 coredump 报告里 fault 的 kernel 是否是
trtllm_bf16_moe,确认 autotune 候选问题。 - 长期修复:autotune 前按 DeviceLimits 过滤候选,workspace 分配加断言校验。
- 升级 FlashInfer / vLLM 到合了 B200 候选校验的版本,并跑上面的「坏候选剔除」用例。
- 若 coredump 无法复现稳定,用
cuda-gdb/compute-sanitizer抓越界写的具体行。
九、小结
B200 上 FlashInfertrtllm_bf16_moeautotune 的 GPU coredump,不是模型或 B200 硬件坏了,而是autotune 枚举的候选里有在 B200 + DP=2+EP 形状下 workspace/tile 越界的坏配置,launch 即触发 GPU 硬件故障、进程级 coredump,且 Python 无法捕获只能预防。最小修复是关 autotune 或降并行度避开触发形状;结构性修复是 autotune 前按设备能力过滤候选、workspace 分配加断言;最后用 pytest 把「坏候选剔除」「workspace 不越界」「B200 专用候选集」锁死。抓住「不可恢复的 GPU 故障只能预防、不能 try/except」这条,所有 autotune/coredump 类问题都要从「不让坏配置 launch」入手。
