【Bug已解决】[Bug] GPU coredump during FlashInfertrtllm_bf16_moeautotune with Qwen3.5-35B-A3B on B200 (DP2 EP) 解决方案一、现象长什么样在 B200Blackwell, sm_100上跑Qwen3.5-35B-A3BMoE并行配置是DP2 EP首次推理时在 FlashInfer 的 MoE 内核autotune自动调优阶段整个进程直接GPU coredumpGPU 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-A3BDP2 EP这个组合下炸单独 EP 或更小模型有时能过。崩的是进程级 coredumpPython 层面抓不到异常因为 GPU 硬件故障直接 SIGSEGV不会变成 Python 异常只能重启。本质FlashInfer 的 autotune 会枚举一堆候选 kernel 配置不同 tile 大小、stage 数、workspace 大小逐一试跑找出最快的。其中某个候选配置在 B200 DP2EP 的特定张量形状下算出的 workspace / scratch 尺寸或 tile 维度越界触发 GPU 硬件缺页 → coredump。autotune 在试跑前没有校验候选的「可行性」于是坏候选一launch就炸穿进程。二、背景先说 autotune 是什么。GPU kernel 的性能对「块大小block size、流水线级数num stages、shared memory 用量」这些参数极度敏感。FlashInfer 为了在给定形状下跑最快会在第一次遇到某个形状时枚举一组候选配置每个都实际 launch 一次、测耗时选最快的。这个过程叫 autotune结果会缓存起来供后续复用。问题出在「枚举的候选里有一个在 B200 上是坏的」B200 是 sm_100shared memory / 寄存器 / workspace 上限和 sm_90 不同。某个候选用了一个在 sm_90 合法、但在 sm_100 上溢出的 tile / stage 组合。Qwen3.5-35B-A3BDP2 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 大小的算法不一致是典型根因。第三层DP2EP 放大了触发概率。EP 下 token 要先 all-to-all 重排token 在每张卡上的「每 expert 的 token 数」分布特殊DP2 又让这个分布翻倍变化。autotune 正是在这个特殊形状下枚举候选恰好选中坏候选的概率被这两个并行维度显著放大。一句话autotune 枚举的候选里有一个在 B200DP/EP 形状下 workspace/tile 越界launch 即 GPU coredump且 Python 无法捕获只能预防。四、最小可运行复现下面用纯 Python 模拟「autotune 枚举候选、不校验设备能力就 launch、坏候选触发 crash不可恢复的 SIGSEGV 等价物」的控制流不需要 GPUclass 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(sm100, max_shared_mem256, max_workspace8192) 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( modelQwen3.5-35B-A3B, # 关键禁用 MoE kernel autotune使用默认经验安全配置 override_pooler_configNone, # 或通过环境变量 / 编译配置固定 compilation_config{ cudagraph_capture_mode: kernel, }, )更具体FlashInfer 的 MoE 常支持环境变量跳过 autotune# 禁用 FlashInfer 的 trtllm_moe autotune不同版本变量名可能不同 export FLASHINFER_DISABLE_MOE_AUTOTUNE1 # 或固定 workspace 上限 export TRTLLM_MOE_MAX_WORKSPACE8192 vllm serve Qwen3.5-35B-A3B --tensor-parallel-size 1 ...如果你确认哪个候选是坏的也可以把并行度降一档比如 DP1 或 EP 减小组避开那个触发形状——这是临时规避的常用手段。六、解决方案第二层结构性改进第一层是「避开 autotune」第二层是「让 autotune 在 launch 前校验候选的设备可行性并给 workspace 留硬上限」从设计上消灭坏候选被 launchfrom 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, keylambda 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(sm100, max_shared_mem256, max_workspace8192, max_tile256) feasible [c for c in candidate_configs() if limits.feasible(c)] # 那个 workspace99999 的坏候选必须被排除 assert all(c[workspace] 8192 for c in feasible) def test_autotune_safe_returns_feasible(): limits DeviceLimits(sm100, max_shared_mem256, max_workspace8192, max_tile256) best autotune_safe(limits, dp2_ep) assert limits.feasible(best) def test_autotune_raises_when_none_feasible(): limits DeviceLimits(sm100, max_shared_mem1, max_workspace1, max_tile1) with pytest.raises(RuntimeError): autotune_safe(limits, dp2_ep) def test_workspace_alloc_checked(): limits DeviceLimits(sm100, max_shared_mem256, max_workspace8192, max_tile256) bad {tile: 64, stages: 2, workspace: 99999} with pytest.raises(AssertionError): allocate_workspace(bad, limits) def test_no_coredump_shape_dp_ep(): # 端到端DP2EP 形状下autotune 不应选中坏候选 limits DeviceLimits(sm100, max_shared_mem256, max_workspace8192, max_tile256) 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_autotuneTrue, deviceB200) assert cfg[autotune] is False assert cfg[tile] 256 # 用经验安全默认不枚举八、排查清单看是否崩在 autotune 阶段日志里有autotune/trtllm_bf16_moe/tuning且是进程级 coredump非 Python 异常→ 坐实本问题。是否只在 B200sm_100炸、H100 能过 → 是设备相关候选越界。临时救火FLASHINFER_DISABLE_MOE_AUTOTUNE1关 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 DP2EP 形状下 workspace/tile 越界的坏配置launch 即触发 GPU 硬件故障、进程级 coredump且 Python 无法捕获只能预防。最小修复是关 autotune 或降并行度避开触发形状结构性修复是 autotune 前按设备能力过滤候选、workspace 分配加断言最后用 pytest 把「坏候选剔除」「workspace 不越界」「B200 专用候选集」锁死。抓住「不可恢复的 GPU 故障只能预防、不能 try/except」这条所有 autotune/coredump 类问题都要从「不让坏配置 launch」入手。