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

发布时间:2026/7/26 21:02:33

【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几个典型表征只在 DeepSeek-V3.2 这类大模型出现小模型正常说明不是 vLLM 框架本身的通用 bug而是这个模型的配置 / 结构特征触发了 worker 里的某个断言。报错只有 Assertion error 四个字根因被吞worker 进程把内部assert异常包成了RuntimeError(Worker failed with error ...)真正的断言内容expert_id num_experts藏在更底层的栈里不容易一眼看到。常在 MoE 路由 / 张量并行相关代码DeepSeek-V3.2 是 MoE 架构多专家 共享专家断言多在专家索引越界分组维度不匹配TP 切分后本地专家数算错这几类。这不是权重坏了而是模型配置专家数、TP 度、分组大小和 vLLM 对该模型的假设不一致导致 worker 里的 assert 触发。下面给出定位与修复流程。二、背景DeepSeek-V3.2 是 MoE混合专家模型每层有多个路由专家 若干共享专家前向往每个 token 选 top-k 个路由专家计算。vLLM 对这个模型做张量并行TP时会把专家按卡切分比如 256 个专家、TP8则每卡本地持有 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等字段模型版本更新后字段名或默认值变了读到的数不对断言在初始化就失败。下面用可运行代码复现专家索引归约越界这类断言并给出修复。三、根因拆成三条根因全局 expert_id 没归约到本地范围TP 切分后每卡只有num_experts / tp个本地专家。路由算出的全局 expert_id 必须mod到本地区间[0, num_experts_local)否则assert expert_id num_experts_local触发。根因是TP 下的 expert_id 映射逻辑没做取模/偏移。group / 块大小在 TP 下未对齐MoE 按 group 做路由如每 8 专家一组TP 切分专家数若不是 group 大小的整数倍分组边界错位断言失败。根因是TP 度选择没有保证num_experts % (tp * group_size) 0。模型 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 当本地索引TP1 时越界。 num_local num_experts // tp for g in global_ids: local g # 没做归约 assert local num_local, fexpert {local} num_local {num_local} return True # TP8, 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, fnum_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(fnum_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_size8, tp8) # 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, default0): for n in names: if n in raw: return raw[n] return default return MoEModelConfig( num_expertsget(num_experts, n_routed_experts), n_shared_expertsget(n_shared_experts, default0), moe_intermediate_sizeget(moe_intermediate_size, expert_hidden_size), num_experts_per_tokget(num_experts_per_tok, moe_topk), group_sizeget(moe_group_size, expert_group_size, default8), ) 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 为 0config 字段未正确读取) if self.cfg.num_experts % self.tp ! 0: problems.append(fnum_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(MoETP 配置不兼容:\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, tp8).validate()read_moe_config用多候选字段名容错 V3.2 的字段命名差异如num_expertsvsn_routed_expertsMoEWorkerValidator在 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} # TP8 整除、且 32 能被 group 8 整除 → OK check_moe_worker_invariants(raw, tp8) # TP7 不能整除 → 应在校验期就清晰报错而非 worker 崩溃 try: check_moe_worker_invariants(raw, tp7) raise AssertionError(TP7 不兼容却没拦下) except RuntimeError: pass print(OK: DeepSeek-V3.2 MoETP 不变量通过) if __name__ __main__: test_deepseek_v32_moe_tp()把test_deepseek_v32_moe_tp接进 CI任何config 读取漏字段或TP 不兼容却放行的改动都会立即红。八、排查清单DeepSeek-V3.2 worker 报 Assertion error按序查先看 worker 底层栈的真实断言AssertionError: ...那行才是根因如expert_id num_experts。vLLM 把异常包成RuntimeError(Worker failed...)时会吞掉细节去 worker 日志找原始AssertionError。确认 TP 度与专家数整除DeepSeek-V3.2 是 256 路由专家TP 必须能整除 256如 1/2/4/8/16。TP3/5/6/7 会直接让分组错位加载期就应选兼容 TP。检查 expert_id 映射是否做本地归约路由算出的全局 expert_id 在 TP 下必须mod num_local才能当本地索引漏了这步必越界断言。核对 config 字段名V3.2 可能用n_routed_experts/expert_group_size而非num_experts/group_size。用read_moe_config的多候选字段名读取避免读到 0。group_size 与本地专家数对齐num_experts/tp必须能被expert_group_size整除否则 MoE 分组边界错位触发断言。共享专家数n_shared_experts不参与路由但占用显存/计算config 读错会影响 shape 断言一并校验。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_localvalidate_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。

相关新闻