参数组 2/8 · 多流
代码基线 a71b766ce(2026-09-23)
讲解范围:vllm_ascend/ascend_config.py 顶层三参数
enable_cpu_binding(L458,默认 true)、multistream_dsv4_dsa_overlap(L459,默认 true)、
multistream_overlap_shared_expert(L461,默认 false),及三个实现位置:
worker/worker.py + cpu_binding.py(绑核)、
attention/dsa_v1.py(DSA prolog 双流)、ops/fused_moe/shared_experts.py(共享专家双流)。
一句话:这三个参数管"并发度"——一个管 host 侧稳不稳,两个管 device 侧挤不挤。 推理一步的耗时 = host 准备(组 batch、填元数据、启动算子)+ device 执行。host 准备的节奏不稳, 芯片就会"饿";device 侧如果所有算子排一条队,不同硬件单元也会互相干等。这三个参数各堵一个漏:
enable_cpu_binding(CPU 绑核):把每个 worker 进程钉在它那块 NPU 对应的 CPU 核上,
防止操作系统把进程挪来挪去造成喂卡节奏抖动;multistream_dsv4_dsa_overlap(DSA prolog 双流):DeepSeek 系注意力的 prolog(前处理)里
投影/量化/归一化在 Cube、Vector 两类硬件单元间交替,单流排队时互相干等——拆两条流流水起来;multistream_overlap_shared_expert(共享专家双流):MoE 里每个 token 都要过的
共享专家,把它的输入准备/计算挪到独立流,与路由专家路径同时跑。三个参数的生效位置分布很散,这也是它们被归成"一类"的唯一共同点——都关于并发: 绑核发生在 worker 进程初始化(vllm_ascend/worker/worker.py, 每个 worker 进程对应一块 NPU);两个双流开关发生在模型层的执行热路径—— DSA prolog 在注意力前处理(attention/dsa_v1.py,DeepSeek 系 MLA 注意力),共享专家在 MoE 层 (ops/fused_moe/shared_experts.py)。读代码时按"启动一次"和"每层每步"两个频率去找。
级 1 · NPU 的任务不是"立刻执行",是"提交进队列"。写下 c = a + b 这样的
张量运算,程序并没当场算完——它只是把一条"做一次加法"的指令提交到了芯片的任务队列里,
真正的执行由芯片异步完成。这个队列在昇腾/英伟达的编程模型里叫 stream(流):
同一流里的任务严格按提交顺序串行执行;不同流之间不保证顺序,可以并行。
默认情况下所有任务都提交到同一条"默认流"。
级 2 · 单流的问题:芯片单元之间的"空转隙"。昇腾芯片里干活的单元分两类: Cube(矩阵乘法专用单元,干大 GEMM)和 Vector(向量运算单元,干归一化/量化/逐元素)。 它们是两套独立硬件。但单流队列是串行的:前面的 Vector 算子(比如量化)没跑完, 后面的 Cube 算子(矩阵乘)只能干等——哪怕 Cube 单元此刻完全空闲。这种交替越频繁,空转隙越多。 注意力 prolog 恰好就是"Vector、Cube 交替"的重灾区。
级 3 · 双流 + 事件:把"无依赖"的活挪过去,依赖用事件声明。解法是把互不依赖的任务 提交到第二条流(辅助流)并行跑。但"无依赖"必须精确声明:如果辅助流上的任务 X 需要主流上 任务 Y 的产出,就要用 event(事件)——主流在 Y 完成处"记录事件",辅助流"等待该事件"后才跑 X。 事件就是跨流的"哨兵"。等两条流的活都干完,再汇合(主流等辅助流)继续。
级 4 · 昇腾上的三个落点。有了"流+事件"这套工具,回看推理链路上哪里在浪费并发: host 侧,worker 进程被操作系统在 CPU 核间搬家,喂卡节奏时快时慢——把它钉到专属核(绑核); device 侧,注意力 prolog 的 V/C 交替(图 3-2)用双流流水化解;MoE 的共享专家与路由专家 互不依赖却排一条队——也用双流并行。这就是三个参数的全部由来。
术语表(回头查阅用):stream=流,任务队列;event=事件,跨流依赖哨兵; Cube/Vector=昇腾的两类执行单元(矩阵乘 / 向量运算);prolog=注意力前处理(投影、量化、归一化); MoE=专家混合;shared expert=共享专家(每个 token 必经)与 routed expert=路由专家(按门控选择); affinity=CPU 亲和性(进程允许落在哪些核上);TP/SP/EP=张量/序列/专家并行; HCCL=华为集合通信库;GEMM=通用矩阵乘。
三个参数都在 --additional-config 顶层(不在 scheduler_config 里),注意默认值不同:
绑核与 DSA 双流默认开(成熟稳定),共享专家双流默认关(较新,且有互斥约束,见⑦)。
生效路径分叉:绑核在 worker 初始化时一次性执行;两个双流开关在每层的执行热路径上被读取。
vllm_ascend/ascend_config.py · AscendConfig 字段 · L458-461 # ---- user-input switches: bool/int/list/str, auto type validation ----
enable_cpu_binding: bool = True # ⚡默认开:绑核成熟稳定
multistream_dsv4_dsa_overlap: bool = True # ⚡默认开:DSA prolog 双流
enable_prefill_mc2: bool = False
multistream_overlap_shared_expert: bool = False # ⚡默认关:较新特性(见⑦互斥)
病:decode 每步都要 host 侧先干活(组 batch、算元数据、提交算子)。如果操作系统 看哪个核空闲就把 worker 进程搬过去,进程会遭遇缓存失效、跨 NUMA 访问、与其它进程争抢—— 喂卡节奏从稳定的 5ms 偶尔抖到 20ms,芯片跟着饿肚子。多卡服务器上问题更明显:几十个 worker 进程若无约束,可能挤到同几个核上。
治法:cpu_binding.py 在初始化时用 npu-smi 拓扑信息解析出
每块 NPU 亲和的 CPU 核列表(哪个核离哪块芯片近、属于哪个 NUMA 域),worker 按
local_rank 选出自己的核集合,写进进程的 CPU 亲和性掩码——从此 OS 只能把它调度在这些核上。
注意容错:绑核失败(权限/拓扑解析失败)只打 warning 不阻断启动。
vllm_ascend/worker/worker.py · NPUWorker 初始化 · L931-939 if get_ascend_config().enable_cpu_binding: # ⚡参数读取
try:
bind_cpus( # →NPU 见 cpu_binding.py
self.local_rank,
npu_id=current_platform.device_id_to_physical_device_id(self.local_rank),
)
except Exception as e:
logger.warning("Bind cpus failed in rank%s: %s Skip binding cpu.",
self.local_rank, e) # ⚡降级:绑核失败不影响服务启动
cpu_binding.py 里的 DeviceInfo(L59-66)负责脏活:调 npu-smi 解析
板卡拓扑(npu_map_info)、找出本进程可见的逻辑 NPU 列表、从拓扑算出
npu_affinity: dict[NPU, list[CPU]] 亲和字典;bind_cpus 按亲和字典设置掩码
(掩码按 32 位一组处理,MASK_BIT = 32,L20)。
病:DeepSeek 系 MLA 注意力的 prolog(前处理)序列是典型的 V/C 交替: q 路量化(V)→ q 投影(C)→ kv 路量化(V)→ kv 矩阵乘(C)→ kv 归一化(V)……单流串行时 两套硬件单元轮流空转。
治法:_mla_prolog_multistream(dsa_v1.py:1919)把它拆成三段两流的流水:
主流与辅助流(dsv4_dsa_overlap_stream(),dsa_v1.py:179 的单例流)交替推进,用
事件接力保证依赖。最值得学的是源码注释里点出的硬件约束:
让 Vector 型的 kv_norm 排在 Cube 型的 kv_matmul 之后于辅助流执行,是为了避免两者争抢 Cube 单元——
双流不是"随便并行",要按硬件单元分派任务。
vllm_ascend/attention/dsa_v1.py · _mla_prolog_multistream · L1919-2010(节选) def _mla_prolog_multistream(...):
aux_stream = dsv4_dsa_overlap_stream() # ⚡单例辅助流(L179)
...
e_q_quant_done = main_stream.record_event() # ⚡事件接力:q 量化完成
with npu_stream_switch(aux_stream, enabled=True):
torch.npu.current_stream().wait_event(e_q_quant_done)
...
e_kv_quant_done = torch.npu.current_stream().record_event()
e_part2_start = main_stream.record_event()
...
main_stream.wait_event(e_kv_quant_done)
...
with npu_stream_switch(aux_stream, enabled=True):
torch.npu.current_stream().wait_event(e_part3_start)
...
e_kv_matmul_done = torch.npu.current_stream().record_event()
...
# aux_stream) completes before q_b_matmul starts so they do not contend for
# the Cube units. kv_norm (Vector) follows kv_matmul on aux_stream and is
# ... ← ⚡原注释:按 Cube/Vector 单元排布,避免争抢
main_stream.wait_event(e_kv_matmul_done)
...
main_stream.wait_stream(aux_stream) # ⚡终局汇合
开关在 dsa_v1.py:1577 读入(self.multistream_dsv4_dsa_overlap = ...),
关闭时退回全串行的 prolog 路径——所以这个开关也是问题排查的退路:怀疑双流引入问题时关掉对照。
病:MoE 层里专家分两种:路由专家(每个 token 按门控分发给少数几个)和 共享专家(每个 token 都必须过,承担"公共知识")。两者逻辑上互不依赖, 但默认排同一条流:共享专家的输入聚合(all-gather)与计算,把路由路径的启动往后顶。
治法:shared_experts.py 给共享专家单独一条流
(shared_experts_calculation_stream()):prepare_input_async(L246)把
SP 模式下的输入 all-gather 挪到共享专家流,与路由侧的输入变换(如 Kimi-K3 的 latent 投影)重叠;
计算入口再按开关切换(L565 的 enabled=self.multistream_overlap)。
开关读取在 AscendSharedExperts.__init__(L92)。
vllm_ascend/ops/fused_moe/shared_experts.py · prepare_input_async · L246-266 def prepare_input_async(self, hidden_states: torch.Tensor) -> PreparedSharedExpertInput:
"""Start the SP-only input all-gather on the shared-expert stream.
The caller must wait for ``ready_event`` before enqueueing routed-path
collectives. This method is intended to overlap the gather only with
a routed input transform such as Kimi-K3's latent projection.
""" # ⚡重叠对象:路由侧的输入变换
if not (self.multistream_overlap and ...):
return PreparedSharedExpertInput(hidden_states=hidden_states)
input_ready = torch.npu.current_stream().record_event() # ⚡事件 1:输入就绪
with npu_stream_switch(shared_experts_calculation_stream(), enabled=True):
torch.npu.current_stream().wait_event(input_ready)
hidden_states = tensor_model_parallel_all_gather(hidden_states, dim=0)
all_gather_done = torch.npu.current_stream().record_event() # ⚡事件 2:聚合完成
return PreparedSharedExpertInput(hidden_states=..., ready_event=all_gather_done)
读完三处实现会发现它们用的是同一套流工具,这是 vllm-ascend 封装好的"双流姿势":
npu_stream_switch(stream, enabled=开关) 是一个上下文管理器——进入时把"当前流"切到目标流
(开关关闭时什么都不做,代码写一遍即可同时支持开/关两种模式),退出时切回;
dsv4_dsa_overlap_stream()(dsa_v1.py:179)和 shared_experts_calculation_stream()
都是单例获取函数——首次调用创建流,之后复用同一实例,避免反复建流的开销。
三处实现的代码形状因此完全一致:record_event → with switch: wait_event → 干活 → record_event。
enable_fused_mc2=1 时
multistream_overlap_shared_expert 被强制改回 false(ascend_config.py:705-706)——
配了也白配,日志无 error。排查"共享专家重叠为什么没生效"先查 mc2。vllm_ascend/ascend_config.py · derive_and_validate · L705-706 if self.enable_fused_mc2 == 1 and self.multistream_overlap_shared_expert:
self.multistream_overlap_shared_expert = False # ⚡静默改写:无 error,配置时极易踩
复述自检:①流是什么,"同流串行、跨流并行"怎么理解?②Cube/Vector 为什么要分派到不同流而不是随便并行? ③共享专家双流的死锁坑是什么(哪个操作必须留默认流)?④"配了共享专家双流却没生效"的第一排查动作是什么?四个都答上来就学会了。
| 章节 | 层次 | 小节标题 | 图数量 | 图编号+视角 | 单图最大节点数 | 代码块数 | 是否达标 |
|---|---|---|---|---|---|---|---|
| ①干嘛的 | 概览 | 这段代码是干嘛的 | 1 | 图1-1 结构 | 6 | 0 | ✓ |
| ②位置 | 概览 | 系统位置 | 1 | 图2-1 结构 | 8(跨三处实现的位置图) | 1(B1 计入④) | ✓ |
| ③爬坡 | 概览 | 概念爬坡(4 级)+术语表 | 4 | 图3-1~3-4 结构/数据/执行(各单视角) | 3 | 1(B1) | ✓ |
| ④主干 | 概览 | 生效链 | 1 | 图4-1 执行 | 5 | 1(B1) | ✓ |
| ⑤展开 | 细节 | 三个参数小节 | 3 | 图5-1 结构 / 5-2 执行 / 5-3 执行 | 5 | 3(B2-B4) | ✓ |
| ⑥流工具 | 细节 | npu_stream_switch 复用 | 1 | 图6-1 结构 | 4 | 0 | ✓ |
| ⑦边界 | 细节 | 互斥/死锁/降级 | 1 | 图7-1 结构 | 6 | 1(B5) | ✓ |
| ⑧小结 | 概览 | 一张图带走 | 1 | 图8-1 结构 | 6 | 0 | ✓ |
| 章节 | h3 小节标题 | 是否配图 | 图编号 | 备注 |
|---|---|---|---|---|
| ③ | 级1/级2/级3/级4 | ✓ | 3-1/3-2/3-3/3-4 | 术语表在爬坡后 |
| ⑤ | 5.1 / 5.2 / 5.3 | ✓ | 5-1 / 5-2 / 5-3 | 每节一图一代码 |
| ⑥ | 流工具复用 | ✓ | 6-1 | 结构视角 |
| ⑦ | 互斥/死锁/降级 | ✓ | 7-1 | 附 B5 校验代码 |
| ⑧ | 小结 + 复述自检 | ✓ | 8-1 | 附 4 道复述题 |
| 级别 | 引入的概念 | 上一级是什么 | 怎么从上一级推出 | 图编号 | 是否空降 |
|---|---|---|---|---|---|
| L0 | 通用编程/系统常识 | — | — | — | — |
| L1 | 任务提交、stream 流 | 程序逐行执行 | 张量运算是异步提交进队列的,队列即流 | 3-1 | 否 |
| L2 | Cube/Vector 单元、单流空转隙 | 单流串行 | 两套硬件单元交替排队,互相干等 | 3-2 | 否 |
| L3 | 双流、event 事件、汇合 | 单流有空隙 | 无依赖任务挪第二条流,依赖用事件声明 | 3-3 | 否 |
| L4 | 绑核 / prolog 流水 / 共享专家并行 | 流+事件工具 | 按"哪里浪费了并发"对号入座三个落点 | 3-4 | 否 |