【Bug已解决】[Bug]: Deepseek v3.2 RuntimeError: Worker failed with error “Assertion error“ 解决方案

📅 2026/7/26 21:19:38 👁️ 阅读次数 📝 编程学习
【Bug已解决】[Bug]: Deepseek v3.2 RuntimeError: Worker failed with error “Assertion error“ 解决方案

【Bug已解决】[Bug]: Deepseek v3.2 RuntimeError: Worker failed with error "Assertion error" 解决方案

一、现象长什么样

在 vLLM 上加载 DeepSeek-V3.2(一个 MoE 大模型)做推理时,worker 进程在初始化或首次前向直接崩,报错只有一句:

RuntimeError: Worker failed with error "Assertion error"

或者更具体的:

[rank0]: AssertionError: expert_id < num_experts (../layers/moe/router.cu:142) [rank0]: RuntimeError: Worker failed with error "Assertion error"

几个典型表征:

  1. 只在 DeepSeek-V3.2 这类大模型出现,小模型正常:说明不是 vLLM 框架本身的通用 bug,而是这个模型的配置 / 结构特征触发了 worker 里的某个断言。
  2. 报错只有 "Assertion error" 四个字,根因被吞:worker 进程把内部assert异常包成了RuntimeError("Worker failed with error ..."),真正的断言内容(expert_id < num_experts)藏在更底层的栈里,不容易一眼看到。
  3. 常在 MoE 路由 / 张量并行相关代码:DeepSeek-V3.2 是 MoE 架构(多专家 + 共享专家),断言多在"专家索引越界""分组维度不匹配""TP 切分后本地专家数算错"这几类。

这不是权重坏了,而是模型配置(专家数、TP 度、分组大小)和 vLLM 对该模型的假设不一致,导致 worker 里的 assert 触发。下面给出定位与修复流程。

二、背景

DeepSeek-V3.2 是 MoE(混合专家)模型:每层有多个"路由专家" + 若干"共享专家",前向往每个 token 选 top-k 个路由专家计算。vLLM 对这个模型做张量并行(TP)时,会把专家按卡切分:比如 256 个专家、TP=8,则每卡本地持有 32 个专家,路由时先把全局 expert_id 映射到本地索引。

这个映射过程有几个容易出断言的地方:

  • 专家索引越界assert expert_id < num_experts_local,若全局 expert_id 没正确归约到本地范围,就超界;
  • 分组 / 块大小不匹配:MoE 常按 group(如 8 个专家一组做路由)组织,TP 切分后每卡的 group 数若没整除,断言失败;
  • 配置字段缺失 / 名称变更:vLLM 读config.json里的num_experts/n_shared_experts/moe_intermediate_size等字段,模型版本更新后字段名或默认值变了,读到的数不对,断言在初始化就失败。

下面用可运行代码复现"专家索引归约越界"这类断言,并给出修复。

三、根因

拆成三条根因:

  1. 全局 expert_id 没归约到本地范围TP 切分后,每卡只有num_experts / tp个本地专家。路由算出的全局 expert_id 必须mod到本地区间[0, num_experts_local),否则assert expert_id < num_experts_local触发。根因是TP 下的 expert_id 映射逻辑没做取模/偏移
  2. group / 块大小在 TP 下未对齐MoE 按 group 做路由(如每 8 专家一组),TP 切分专家数若不是 group 大小的整数倍,分组边界错位,断言失败。根因是TP 度选择没有保证num_experts % (tp * group_size) == 0
  3. 模型 config 字段读取错误vLLM 假设config.json里某字段存在/默认值正确,但 V3.2 的实际配置用了不同字段名或缺省,导致num_experts等读到错值,初始化断言失败。根因是config 解析缺容错与校验

修复方向:在加载模型时校验 config 与 TP 的兼容性(专家数能被 TP×group 整除)、在路由映射时对 expert_id 做本地归约、并对 config 字段做健壮读取。

四、最小可运行复现

下面复现"TP 下 expert_id 没归约到本地范围导致断言":

def naive_expert_dispatch(global_ids, num_experts, tp, rank): """现状:直接拿全局 id 当本地索引,TP>1 时越界。""" num_local = num_experts // tp for g in global_ids: local = g # 没做归约! assert local < num_local, f"expert {local} >= num_local {num_local}" return True # TP=8, 256 专家,每卡 32 个;全局 id 可能到 255 try: naive_expert_dispatch([0, 31, 200, 255], 256, 8, 0) except AssertionError as e: print("复现断言:", e) # expert 200 >= num_local 32

复现断言: expert 200 >= num_local 32即复现了 worker 里的expert_id < num_experts断言。下面是正确归约。

五、解决方案(第一层:最小直接修复)

最小修复:路由时把全局 expert_id 归约到本地区间(local = global_id % num_local),并在加载时校验 TP 兼容性。

def safe_expert_dispatch(global_ids, num_experts, tp, rank): """TP 下把全局 expert_id 归约到本卡本地区间 [0, num_local)。""" num_local = num_experts // tp # 校验:专家数必须能被 TP 整除,否则分组必然错位 assert num_experts % tp == 0, f"num_experts {num_experts} 不能被 TP {tp} 整除" for g in global_ids: local = g % num_local assert local < num_local, f"归约后仍越界: {local}" return True def validate_moe_tp_config(num_experts, group_size, tp): """加载时校验 MoE + TP 的兼容性。""" num_local = num_experts // tp if num_experts % tp != 0: raise ValueError(f"num_experts {num_experts} 必须能被 TP {tp} 整除") if num_local % group_size != 0: raise ValueError( f"每卡本地专家数 {num_local} 必须能被 group_size {group_size} 整除," f"否则 MoE 分组边界错位(请调整 TP 度)") return True # 用法 validate_moe_tp_config(256, group_size=8, tp=8) # OK safe_expert_dispatch([0, 31, 200, 255], 256, 8, 0) print("TP 专家归约 + 配置校验通过")

这一层改动让 worker 不再因 expert_id 越界崩,且加载时就能用清晰错误告诉用户"换个 TP 度"。

六、解决方案(第二层:结构化改进)

把"MoE + TP 配置校验 + expert 映射"做成结构化组件,覆盖 config 字段健壮读取(V3.2 字段名变更也能容错),并在 worker 初始化早期就跑校验。

from dataclasses import dataclass, field from typing import Optional @dataclass class MoEModelConfig: num_experts: int = 0 n_shared_experts: int = 0 moe_intermediate_size: int = 0 num_experts_per_tok: int = 0 group_size: int = 8 def read_moe_config(raw: dict) -> MoEModelConfig: """健壮读取 V3.2 的 config,字段缺失/改名都给默认值不崩。""" def get(*names, default=0): for n in names: if n in raw: return raw[n] return default return MoEModelConfig( num_experts=get("num_experts", "n_routed_experts"), n_shared_experts=get("n_shared_experts", default=0), moe_intermediate_size=get("moe_intermediate_size", "expert_hidden_size"), num_experts_per_tok=get("num_experts_per_tok", "moe_topk"), group_size=get("moe_group_size", "expert_group_size", default=8), ) class MoEWorkerValidator: def __init__(self, cfg: MoEModelConfig, tp: int): self.cfg = cfg self.tp = tp def validate(self): problems = [] if self.cfg.num_experts == 0: problems.append("num_experts 为 0,config 字段未正确读取") if self.cfg.num_experts % self.tp != 0: problems.append(f"num_experts {self.cfg.num_experts} 不能被 TP {self.tp} 整除") num_local = self.cfg.num_experts // self.tp if num_local % self.cfg.group_size != 0: problems.append(f"本地专家数 {num_local} 不能被 group {self.cfg.group_size} 整除") if problems: raise RuntimeError("MoE+TP 配置不兼容:\n" + "\n".join(problems)) return True # 用法:worker 初始化早期 raw_cfg = {"n_routed_experts": 256, "moe_topk": 8, "expert_group_size": 8} cfg = read_moe_config(raw_cfg) MoEWorkerValidator(cfg, tp=8).validate()

read_moe_config用"多候选字段名"容错 V3.2 的字段命名差异(如num_expertsvsn_routed_experts),MoEWorkerValidator在 worker 启动早期就校验,避免在首次前向才崩且只给 "Assertion error"。

七、解决方案(第三层:断言 / CI 守护)

这类 worker 崩溃最怕"线上才崩、且只给 Assertion error"。用断言 + 配置校验守两条不变量:

def check_moe_worker_invariants(raw_cfg, tp): cfg = read_moe_config(raw_cfg) MoEWorkerValidator(cfg, tp).validate() # 不变量 1:任意全局 expert_id 归约后都在本地区间 num_local = cfg.num_experts // tp for g in range(cfg.num_experts): assert g % num_local < num_local return True def test_deepseek_v32_moe_tp(): raw = {"n_routed_experts": 256, "moe_topk": 8, "expert_group_size": 8} # TP=8 整除、且 32 能被 group 8 整除 → OK check_moe_worker_invariants(raw, tp=8) # TP=7 不能整除 → 应在校验期就清晰报错,而非 worker 崩溃 try: check_moe_worker_invariants(raw, tp=7) raise AssertionError("TP=7 不兼容却没拦下") except RuntimeError: pass print("OK: DeepSeek-V3.2 MoE+TP 不变量通过") if __name__ == "__main__": test_deepseek_v32_moe_tp()

test_deepseek_v32_moe_tp接进 CI,任何"config 读取漏字段"或"TP 不兼容却放行"的改动都会立即红。

八、排查清单

DeepSeek-V3.2 worker 报 "Assertion error",按序查:

  1. 先看 worker 底层栈的真实断言AssertionError: ...那行才是根因(如expert_id < num_experts)。vLLM 把异常包成RuntimeError("Worker failed...")时会吞掉细节,去 worker 日志找原始AssertionError
  2. 确认 TP 度与专家数整除:DeepSeek-V3.2 是 256 路由专家,TP 必须能整除 256(如 1/2/4/8/16)。TP=3/5/6/7 会直接让分组错位,加载期就应选兼容 TP。
  3. 检查 expert_id 映射是否做本地归约:路由算出的全局 expert_id 在 TP 下必须mod num_local才能当本地索引,漏了这步必越界断言。
  4. 核对 config 字段名:V3.2 可能用n_routed_experts/expert_group_size而非num_experts/group_size。用read_moe_config的多候选字段名读取,避免读到 0。
  5. group_size 与本地专家数对齐num_experts/tp必须能被expert_group_size整除,否则 MoE 分组边界错位触发断言。
  6. 共享专家数n_shared_experts不参与路由,但占用显存/计算,config 读错会影响 shape 断言,一并校验。
  7. CI 接test_deepseek_v32_moe_tp:覆盖"字段缺失/TP 不整除"各类组合,保证加载期清晰报错而非 worker 崩。

九、小结

DeepSeek-V3.2 worker 报 "Assertion error" 的根因是MoE + 张量并行下的 expert_id 映射越界,或模型 config 字段读取错误导致初始化断言失败,且 worker 把原始断言吞成了笼统的 RuntimeError。三层修复:

  • 第一层:safe_expert_dispatch对全局 expert_id 做本地归约(mod num_local),validate_moe_tp_config加载期校验"专家数能被 TP 整除、本地专家数能被 group 整除";
  • 第二层:read_moe_config用多候选字段名容错 V3.2 的命名差异,MoEWorkerValidator在 worker 初始化早期就校验,避免首次前向才崩;
  • 第三层:CI 断言守住"任意全局 id 归约后不越界 / TP 不兼容必在加载期报错",任何 config 漏读或 TP 放行都立即红。

落实后,DeepSeek-V3.2 在 vLLM 上要么正常起、要么在加载期给出"换 TP 度/字段名变了"的清晰错误,而不是 worker 里只留一句 "Assertion error"。