vLLM Ascend · 参数源码教学 八类总览 ①干嘛的②位置③概念爬坡④主干流程 ⑤关键节点⑥流工具⑦边界异常⑧小结 自检表

多流类参数:绑核与双流重叠

参数组 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 侧如果所有算子排一条队,不同硬件单元也会互相干等。这三个参数各堵一个漏:

这张图回答什么问题:三个参数分别站在 host/device 的哪个位置、堵的哪个漏。
图 1-1 · 全景:host 一漏 + device 两漏(概览)
flowchart TD H["host 侧:组 batch/填元数据/启动算子
进程被 OS 挪来挪去 → 喂卡节奏抖"] --> P1["enable_cpu_binding
绑核:进程钉在专属 CPU 核"] D1["device 侧:注意力 prolog
Cube/Vector 算子单流排队互相干等"] --> P2["multistream_dsv4_dsa_overlap
prolog 拆双流流水"] D2["device 侧:MoE
共享专家与路由专家串行"] --> P3["multistream_overlap_shared_expert
共享专家挪独立流并行"] style P1 fill:#eef3ff,stroke:#5b6ee1 style P2 fill:#eef3ff,stroke:#5b6ee1 style P3 fill:#eef3ff,stroke:#5b6ee1

② 它们在系统中的位置

三个参数的生效位置分布很散,这也是它们被归成"一类"的唯一共同点——都关于并发: 绑核发生在 worker 进程初始化(vllm_ascend/worker/worker.py, 每个 worker 进程对应一块 NPU);两个双流开关发生在模型层的执行热路径—— DSA prolog 在注意力前处理(attention/dsa_v1.py,DeepSeek 系 MLA 注意力),共享专家在 MoE 层 (ops/fused_moe/shared_experts.py)。读代码时按"启动一次"和"每层每步"两个频率去找。

这张图回答什么问题:三个参数各自挂在执行框架的哪个位置、以什么频率生效。
图 2-1 · 系统位置(结构视角)
flowchart TD A["服务启动"] --> B["NPUWorker 初始化(每进程一次)
worker.py:931 → cpu_binding.py"] B --> C["enable_cpu_binding 生效
(频率:启动一次)"] A --> D["每层每步执行"] D --> E["注意力 prolog
dsa_v1.py:1577 读开关"] E --> F["multistream_dsv4_dsa_overlap 生效
(频率:每层每步)"] D --> G["MoE 层
shared_experts.py:92 读开关"] G --> H["multistream_overlap_shared_expert 生效
(频率:每层每步)"] style C fill:#dafbe1,stroke:#1a7f37 style F fill:#dafbe1,stroke:#1a7f37 style H fill:#dafbe1,stroke:#1a7f37

③ 概念爬坡:从"排队"走到"三个开关"

级 1 · NPU 的任务不是"立刻执行",是"提交进队列"。写下 c = a + b 这样的 张量运算,程序并没当场算完——它只是把一条"做一次加法"的指令提交到了芯片的任务队列里, 真正的执行由芯片异步完成。这个队列在昇腾/英伟达的编程模型里叫 stream(流): 同一流里的任务严格按提交顺序串行执行;不同流之间不保证顺序,可以并行。 默认情况下所有任务都提交到同一条"默认流"。

这张图回答什么问题:流是什么——为什么"同流串行、跨流并行"。
图 3-1 · 爬坡级 1:流 = 任务队列
flowchart LR A["程序提交任务
T1 → T2 → T3"] --> B["默认流(一条队列)
T1 → T2 → T3 严格串行"] B --> C["第二条流
T4 与 T1/T2 可以同时跑"] style B fill:#eef3ff,stroke:#5b6ee1 style C fill:#fff8e1,stroke:#9a6700

级 2 · 单流的问题:芯片单元之间的"空转隙"。昇腾芯片里干活的单元分两类: Cube(矩阵乘法专用单元,干大 GEMM)和 Vector(向量运算单元,干归一化/量化/逐元素)。 它们是两套独立硬件。但单流队列是串行的:前面的 Vector 算子(比如量化)没跑完, 后面的 Cube 算子(矩阵乘)只能干等——哪怕 Cube 单元此刻完全空闲。这种交替越频繁,空转隙越多。 注意力 prolog 恰好就是"Vector、Cube 交替"的重灾区。

这张图回答什么问题:为什么单流会让两套硬件单元互相干等。
图 3-2 · 爬坡级 2:单流时间线上的空转隙(数据视角)
flowchart LR A["单流串行:
q量化V → q投影C → kv量化V → kv矩阵乘C → kv_normV"] --> B["时间线:
Cube 单元在 V 算子执行期间空闲,反之亦然"] style B fill:#fff8e1,stroke:#9a6700

级 3 · 双流 + 事件:把"无依赖"的活挪过去,依赖用事件声明。解法是把互不依赖的任务 提交到第二条流(辅助流)并行跑。但"无依赖"必须精确声明:如果辅助流上的任务 X 需要主流上 任务 Y 的产出,就要用 event(事件)——主流在 Y 完成处"记录事件",辅助流"等待该事件"后才跑 X。 事件就是跨流的"哨兵"。等两条流的活都干完,再汇合(主流等辅助流)继续。

这张图回答什么问题:双流怎么配合——事件如何充当跨流的依赖哨兵。
图 3-3 · 爬坡级 3:双流 + 事件接力(执行视角)
sequenceDiagram participant M as 主流 participant A as 辅助流 M->>M: 任务 Y M->>A: record_event(Y 完成) A->>A: wait_event(Y) 后跑 X Note over M,A: Y 与 X 无数据依赖即可并行 M->>M: wait 辅助流(汇合)

级 4 · 昇腾上的三个落点。有了"流+事件"这套工具,回看推理链路上哪里在浪费并发: host 侧,worker 进程被操作系统在 CPU 核间搬家,喂卡节奏时快时慢——把它钉到专属核(绑核); device 侧,注意力 prolog 的 V/C 交替(图 3-2)用双流流水化解;MoE 的共享专家与路由专家 互不依赖却排一条队——也用双流并行。这就是三个参数的全部由来。

这张图回答什么问题:三个参数是怎么从"哪里浪费了并发"推出来的。
图 3-4 · 爬坡级 4:并发浪费点 → 三个参数(概览)
flowchart TD W1["host:进程漂移
喂卡节奏抖"] --> S1["enable_cpu_binding
绑核"] W2["device:prolog 的
Cube/Vector 交替空隙"] --> S2["multistream_dsv4_dsa_overlap
三段流水"] W3["device:共享专家与
路由专家串行"] --> S3["multistream_overlap_shared_expert
独立流并行"] style S1 fill:#eef3ff,stroke:#5b6ee1 style S2 fill:#eef3ff,stroke:#5b6ee1 style S3 fill:#eef3ff,stroke:#5b6ee1

术语表(回头查阅用):stream=流,任务队列;event=事件,跨流依赖哨兵; Cube/Vector=昇腾的两类执行单元(矩阵乘 / 向量运算);prolog=注意力前处理(投影、量化、归一化); MoE=专家混合;shared expert=共享专家(每个 token 必经)与 routed expert=路由专家(按门控选择); affinity=CPU 亲和性(进程允许落在哪些核上);TP/SP/EP=张量/序列/专家并行; HCCL=华为集合通信库;GEMM=通用矩阵乘。

④ 主干流程:从启动参数到生效位置

三个参数都在 --additional-config 顶层(不在 scheduler_config 里),注意默认值不同: 绑核与 DSA 双流默认开(成熟稳定),共享专家双流默认关(较新,且有互斥约束,见⑦)。 生效路径分叉:绑核在 worker 初始化时一次性执行;两个双流开关在每层的执行热路径上被读取。

这张图回答什么问题:三个参数各自的生效时机与位置。
图 4-1 · 生效链(执行视角)
flowchart TD A["--additional-config 顶层 JSON"] --> B["AscendConfig
L458-461 三个 bool 字段"] B --> C["enable_cpu_binding=true
→ NPUWorker 初始化时绑核(一次)"] B --> D["multistream_dsv4_dsa_overlap=true
→ dsa_v1.py:1577 每层读"] B --> E["multistream_overlap_shared_expert=false
→ shared_experts.py:92 每层读"] style C fill:#dafbe1,stroke:#1a7f37 style D fill:#dafbe1,stroke:#1a7f37 style E fill:#fff8e1,stroke:#9a6700
代码 B1 · 三个参数的定义与默认值
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  # ⚡默认关:较新特性(见⑦互斥)

⑤ 关键节点逐个展开:三个参数

5.1 enable_cpu_binding —— 把 worker 进程"钉"在专属 CPU 核上

病:decode 每步都要 host 侧先干活(组 batch、算元数据、提交算子)。如果操作系统 看哪个核空闲就把 worker 进程搬过去,进程会遭遇缓存失效、跨 NUMA 访问、与其它进程争抢—— 喂卡节奏从稳定的 5ms 偶尔抖到 20ms,芯片跟着饿肚子。多卡服务器上问题更明显:几十个 worker 进程若无约束,可能挤到同几个核上。

治法:cpu_binding.py 在初始化时用 npu-smi 拓扑信息解析出 每块 NPU 亲和的 CPU 核列表(哪个核离哪块芯片近、属于哪个 NUMA 域),worker 按 local_rank 选出自己的核集合,写进进程的 CPU 亲和性掩码——从此 OS 只能把它调度在这些核上。 注意容错:绑核失败(权限/拓扑解析失败)只打 warning 不阻断启动。

这张图回答什么问题:绑核的映射关系——每块 NPU 的 worker 被钉到哪组核。
图 5-1 · 细节:NPU → CPU 核亲和映射(结构视角)
flowchart TD A["npu-smi 拓扑
NPU0↔CPU0-7 · NPU1↔CPU8-15 ..."] --> B["DeviceInfo 解析
npu_affinity 字典"] B --> C["worker(local_rank=0) → NPU0 的核集合"] B --> D["worker(local_rank=1) → NPU1 的核集合"] C --> E["设置进程亲和掩码
OS 不再搬动它"] style E fill:#dafbe1,stroke:#1a7f37
代码 B2 · 绑核入口:失败只降级不阻断
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)。

5.2 multistream_dsv4_dsa_overlap —— 注意力 prolog 的三段流水

病: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 单元—— 双流不是"随便并行",要按硬件单元分派任务。

这张图回答什么问题:三段流水怎么排、事件在哪些节点接力。
图 5-2 · 细节:prolog 三段两流流水(执行视角,V=Vector C=Cube)
sequenceDiagram participant M as 主流 participant A as 辅助流 M->>M: 段1:q 投影 + q 量化(C/V) M->>A: e_q_quant_done A->>A: 段2:kv 量化(V)→ wait 后 kv_matmul(C) A->>M: e_kv_quant_done / e_kv_matmul_done M->>M: 段3:part3(wait e_kv_matmul_done) M->>A: e_part3_start A->>A: kv_norm(V)← 故意排后避免争抢 Cube M->>M: wait_stream(aux) 汇合
代码 B3 · 三段流水:事件接力与单元分派
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 路径——所以这个开关也是问题排查的退路:怀疑双流引入问题时关掉对照。

5.3 multistream_overlap_shared_expert —— 共享专家与路由路径并行

病: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)。

这张图回答什么问题:共享专家挪独立流后,哪两段活重叠了。
图 5-3 · 细节:共享专家与路由路径重叠(执行视角)
flowchart TD A["主流:路由路径
输入变换 → dispatch → 路由专家 → combine"] --> C["汇合:合并两路输出"] B["共享专家流:
all-gather 输入 → 共享 MLP"] --> C A -. input_ready 事件 .-> B B -. all_gather_done 事件 .-> C style B fill:#fff8e1,stroke:#9a6700 style C fill:#dafbe1,stroke:#1a7f37
代码 B4 · 共享专家输入的异步准备
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)

⑥ 流工具:npu_stream_switch 与两个单例流

读完三处实现会发现它们用的是同一套流工具,这是 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。

这张图回答什么问题:为什么三处双流代码长得一模一样——都站在同一套工具上。
图 6-1 · 流工具的复用(结构视角)
flowchart TD T["npu_stream_switch(stream, enabled)
上下文管理器:进入切流/退出切回"] --> U1["dsa_v1 prolog
dsv4_dsa_overlap_stream()"] T --> U2["shared_experts
shared_experts_calculation_stream()"] T --> U3["dsa_cp 等其它调用点"] U1 & U2 & U3 --> P["统一姿势:
record_event → with switch → wait_event → 干活"] style P fill:#dafbe1,stroke:#1a7f37

⑦ 边界与异常:互斥、死锁坑与降级

这张图回答什么问题:开这些开关前,哪些组合与坑要先核对。
图 7-1 · 互斥与降级关系(结构视角)
flowchart LR A["enable_fused_mc2=1"] -. "强制置 false(静默)" .-> B["multistream_overlap_shared_expert"] C["TP all-gather 挪共享专家流"] -. "跨进程组进度不一致 → HCCL 死锁" .-> D["红线:集合通信留默认流"] E["bind_cpus 失败"] -. "warning 降级,服务照常" .-> F["绑核可能未生效(查日志)"] style B fill:#ffebe9,stroke:#cf222e style D fill:#ffebe9,stroke:#cf222e
代码 B5 · 静默互斥: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,配置时极易踩

⑧ 小结:一张图带走

这张图回答什么问题:如果只记一张图——三个参数、各自堵哪个漏、生效在哪、注意什么。
图 8-1 · 全章收束(概览)
flowchart TD A["host:进程漂移喂卡抖"] --> B["enable_cpu_binding(默认开)
worker 初始化绑核 · 失败仅告警"] C["device:prolog V/C 交替空隙"] --> D["multistream_dsv4_dsa_overlap(默认开)
三段两流流水 + 事件接力"] E["device:共享专家串行干等"] --> F["multistream_overlap_shared_expert(默认关)
独立流与路由并行 · mc2 时静默关"] style B fill:#eef3ff,stroke:#5b6ee1 style D fill:#eef3ff,stroke:#5b6ee1 style F fill:#eef3ff,stroke:#5b6ee1

复述自检:①流是什么,"同流串行、跨流并行"怎么理解?②Cube/Vector 为什么要分派到不同流而不是随便并行? ③共享专家双流的死锁坑是什么(哪个操作必须留默认流)?④"配了共享专家双流却没生效"的第一排查动作是什么?四个都答上来就学会了。

自检表

表 A · 总览自检

章节层次小节标题图数量图编号+视角单图最大节点数代码块数是否达标
①干嘛的概览这段代码是干嘛的1图1-1 结构60✓
②位置概览系统位置1图2-1 结构8(跨三处实现的位置图)1(B1 计入④)✓
③爬坡概览概念爬坡(4 级)+术语表4图3-1~3-4 结构/数据/执行(各单视角)31(B1)✓
④主干概览生效链1图4-1 执行51(B1)✓
⑤展开细节三个参数小节3图5-1 结构 / 5-2 执行 / 5-3 执行53(B2-B4)✓
⑥流工具细节npu_stream_switch 复用1图6-1 结构40✓
⑦边界细节互斥/死锁/降级1图7-1 结构61(B5)✓
⑧小结概览一张图带走1图8-1 结构60✓

表 B · 逐节配图检查

章节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 道复述题

表 C · 概念爬坡检查

级别引入的概念上一级是什么怎么从上一级推出图编号是否空降
L0通用编程/系统常识————
L1任务提交、stream 流程序逐行执行张量运算是异步提交进队列的,队列即流3-1否
L2Cube/Vector 单元、单流空转隙单流串行两套硬件单元交替排队,互相干等3-2否
L3双流、event 事件、汇合单流有空隙无依赖任务挪第二条流,依赖用事件声明3-3否
L4绑核 / prolog 流水 / 共享专家并行流+事件工具按"哪里浪费了并发"对号入座三个落点3-4否
首页