vllm-ascend PR #18870 解读:FlashComm1 的"假开启"与 SP 归位
GLM DSA V1 配 PP>1 时 FlashComm1 名开实关——本 PR 五条件重开 decoder SP、o_proj 交出归约权、在每个 PP 出口把布局聚回全量
目录
PR 档案
问题:guard 一刀切,FlashComm1 名开实关(改动前)
核心思路:自己兑现上游的前提
主线讲解:从架构全貌到这次 PR
前后代码对照(带注释)
与我们排查问题的关系
收益、风险与待验证
术语速查
1. PR 档案
项目 内容
标题 [BugFix][Model][v0.30.0] Restore GLM V1 FlashComm1 with PP
作者 / 状态 aisong1988 · 开放中(作者自述:张量单测与 NPU 正确性/性能验证仍 pending)创建于 2026-10-15
目标分支 releases/v0.30.0——与前一篇 PR #18671 同一条版本线
改动规模 5 个文件,+176 / −28 行(2 个源码文件 + 3 个测试文件;diff 实测)
涉及模块 vllm_ascend/patch/worker/patch_deepseek_v2.py、vllm_ascend/worker/model_runner_v1.py(另含 3 个测试文件)
关联 PR #18671 (同链前作:PP=1 时 SP 布局贯穿 DSA-CP,本站已解读);跟随 #16819 的聚散方案
生效场景 GLM DSA V1 + 全局 SP(FlashComm1)+ PP>1 ,dense 前缀 + 连续 MoE 层——我们环境 PP=1,不直接命中(第 6 章详说)
读前须知:四个名词 30 秒(更多缩写见文末第 8 章术语速查)
TP(张量并行) :把一层的权重/计算切到多张卡各算一块——TP=16 即 16 卡一组;
SP(序列并行) :TP 组内再把输入序列横向切,每卡只拿一段连续 token;
PP(流水线并行) :把模型的 N 层按层切成分段(stage) ,各段在不同卡组上接力——本 PR 的开关就卡在 PP 上;
FlashComm1 :GLM V1 的 SP MoE 通信模式——decoder 开 SP、注意力投影不自带归约,归约交给统一收口。
一句话总结
上游 vLLM 有一个 PP=1 guard :PP>1 时一刀切关掉 decoder 级 SP(因为上游 PP transport 期望全量激活)。于是 PP>1 的 GLM V1 出现名实不符 :FlashComm1 状态上报为开启,decoder SP 实际被关,o_proj 还带着每层一次的白归约 。本 PR 的思路是自己兑现上游的前提 :五条件选路重开 stage 内 SP、o_proj 交出归约权、把"聚回全量"上移到每个 PP 出口、Runner 的 PP 中间张量口径改成全量——上游要什么布局,边界上就还给什么布局。
图 1 · PR 改动全景:2 个源码文件都在"壳"上,3 个测试文件组成回归网
PR #18870 改动全景:+176 / −28 · 5 个文件
源码 ×2(都在「壳」上:patch 管层外壳,Runner 管边界)
patch/worker/patch_deepseek_v2.py
运行时补丁:模型 init 五条件重开 SP、o_proj 交归约权(链上主战场)
forward:PP 出口聚全量上移到 transport 之前
worker/model_runner_v1.py
PP 中间张量口径:新增 _pp_intermediate_token_count,
接收切片 / dummy 估显存 / 最大 buffer 三处统一走它
测试 ×3(回归网)
test_patch_deepseek_v2.py
五条件选路矩阵(model_type×v2×pp×sp)+ PP 聚散
test_model_runner_v1.py
接收与 dummy 布局匹配模型 transport(新测试组)
test_deepseek_dsa_cp.py
配置补 pipeline_parallel_size / use_sequence_parallel_moe
门卫(ops/mla.py)与后端九步(sfa_v1.py)一行没动——本 PR 全部改在「开关与边界」,不在注意力数学。
前作 #18671 拆的是 PP=1 时"聚了又切";本篇处理 PP>1 时"SP 被 guard 关掉"——同一套布局契约的 PP 维度。
2. 问题:guard 一刀切,FlashComm1 名开实关(改动前)
先理清四个角色怎么协作。TP=16 把一层切到 16 卡;SP 在 TP 组内再把 token 横切成连续段(每卡一段);PP 再把模型的 N 层按层切成分段(stage)接力 ——层是 PP 的切法,token 是 SP 的切法,两者正交。GLM DSA V1 开全局 SP(FlashComm1 模式)时,decode 侧的分工是:注意力投影不 自带归约,由 decoder 收口统一归约——这样每层省一次集合通信,这正是 FlashComm1 的收益来源。
但上游 vLLM 立了一条 guard:PP>1 时关掉 decoder 级 SP 。原因在上游自己的传输约定——它的 PP transport 期望拿到全量激活 ,如果 stage 内跑 SP、出口传出的是分片,上游的接法就对不上。guard 一刀切之后,PP>1 的 GLM V1 就出现了下面的名实不符(PP 切层与 SP 的关系见图 3):
改动前 · PP>1:左=业务主线(原样),右=框架线(guard 与归约)
用户输入:3500 字 prompt
Token 化(词表 128k)
ids [1, 3500]
Token 嵌入 ≈ 1.05B
128k×8192 → [1, 3500, 8192]
Block × N(每层 ≈ 1.1B):前 k 层 dense 前缀 → 后续连续 MoE 层
LayerNorm
[1, 219, 8192]
self_attn(MLA/DSA)
latent KV · indexer top-2048 稀疏
Q/KV 低秩投影
latent 压缩(步 3)
稀疏注意力(top-2048)
Q·Kᵀ/√d → softmax → ·V
O 投影
残差连接 +
LayerNorm
[1, 219, 8192]
MoE(路由 + 共享专家)
共享专家
每 token 必走
路由专家
top-k 选通
共享 + 路由加权合并
SP:各卡各算 219 分片
already_sequence_parallel
残差连接 +
MTP 模块(投机解码,MTP=3)
draft 层:与主模型层同构
LM Head(与嵌入共享权重)
8192×128k → logits [1, 128k]
生成下一个 Token
decode 重复整条链,KV 从缓存取
框架 · SP 切片(TP=16)
每卡 [1, 219, 8192]
Ⓑ PP=1 guard(上游)
PP>1 关 decoder SP——transport
期望全量,索性一刀切
Ⓑ
FlashComm1 上报=开启
名实不符:配置开着,
decoder SP 实际被关
Ⓐ o_proj.reduce_results=True
每层投影后各归各的——白归约
本应由 decoder 统一归约
Ⓐ
PP 出口:全量交下一 stage
现状靠 SP 关换来的——本 PR 要改这里
读法:左列=GLM DSA V1 的模型架构(dense 前缀+连续 MoE+MLA/DSA+MTP);右列=框架线的三处错位;虚线=主线在框架上的挂点。Ⓑ 是根源,Ⓐ 是代价。
问题不在十行示例的数学,在开关:FlashComm1 的收益一分没拿到,还每层多付一次归约。
图 2 · 改动前:Ⓑ guard 关 SP 是根源,Ⓐ 每层白归约是代价,FlashComm1 名开实关
ⒶⒷ 速查(后文正文与代码注释均用此编号)
编号 名称 位置与性质
Ⓐ o_proj 自带归约 每层投影后 reduce_results=True 各归各的,FlashComm1 语义下应由 decoder 统一归约——白做
Ⓑ PP=1 guard 关 SP 上游在 PP>1 时关掉 decoder SP(transport 期望全量)——FlashComm1 名开实关的根源
PP 怎么切:按层切段(stage)接力;SP 在 stage 内切 token;PP 出口必须是全量
stage 0
层 0 至 层 k(dense 前缀+MoE)
stage 1
层 k+1 至 层 2k
…
更多 stage
层 = PP 的切法(本图示意 8 层 2 stage)
token = SP 的切法(stage 内每卡一段)
PP 出口约定:交给下一 stage 的必须是全量激活(上游 transport 的期望)
stage 内部随便怎么切——前提是出口自己把布局还回去。这就是本 PR 的立足点
本 PR 的 scope:GLM DSA V1 + 全局 SP + dense 前缀 + 连续 MoE 层
交错 dense/MoE 配置保持原行为;#18671 的 DSA-CP 本地优化不受影响
图 3 · PP 按层切段、SP 按 token 切卡:出口全量是上游约定,stage 内开 SP 的前提是出口自己恢复布局
3. 核心思路:自己兑现上游的前提
guard 的担忧是对的——上游 transport 确实只认全量。本 PR 不跟 guard 对着干,而是把 guard 的前提自己兑现 :stage 内部照开 SP(拿 FlashComm1 的收益),在每个 PP 出口把 hidden/residual 聚回全量再交给 transport;o_proj 的归约权从"每层各归各的"收归 decoder 统一收口;Runner 侧所有 PP 中间张量的口径同步改成全量。一句话:边界还上游一个全量,stage 内换回真正的 FlashComm1。 改后的样子见图 4:
改动后 · 业务主线与图 2 完全一致;只动框架线:灰=消失的白归约、绿=新增、蓝=变化
用户输入:3500 字 prompt
Token 化(词表 128k)
ids [1, 3500]
Token 嵌入 ≈ 1.05B
128k×8192 → [1, 3500, 8192]
Block × N(每层 ≈ 1.1B):前 k 层 dense 前缀 → 后续连续 MoE 层
LayerNorm
[1, 219, 8192]
self_attn(MLA/DSA)
latent KV · indexer top-2048 稀疏
Q/KV 低秩投影
latent 压缩(步 3)
稀疏注意力(top-2048)
Q·Kᵀ/√d → softmax → ·V
O 投影
残差连接 +
LayerNorm
[1, 219, 8192]
MoE(路由 + 共享专家)
共享专家
每 token 必走
路由专家
top-k 选通
共享 + 路由加权合并
SP:各卡各算 219 分片
already_sequence_parallel
残差连接 +
MTP 模块(投机解码,MTP=3)
draft 层:与主模型层同构
LM Head(与嵌入共享权重)
8192×128k → logits [1, 128k]
生成下一个 Token
decode 重复整条链,KV 从缓存取
框架 · SP 切片(不变)
每卡 [1, 219, 8192]
蓝 · 五条件全中:重开 decoder SP
V1·glm_moe_dsa·moe_freq=1·PP>1·SP
仅本 stage 的 MoE+MLA 层动手(图 13)
灰 · Ⓐ 消失:o_proj 不再每层归约
reduce_results=False,
归约权收归 decoder 统一收口
绿 · 新增:PP 出口聚全量上移
cat→all_gather→截断→split,
transport 之前每个出口都还全量
蓝 · Runner 口径改全量
_pp_intermediate_token_count 三处统一
读法:左列与图 2 逐格一致(=GLM DSA V1 架构),一个框都没动;右列三处互为因果——敢开 SP(蓝),是因为出口还全量(绿);Ⓐ 消失(灰),是 SP 开了归约权自然收归。
dense 前缀层不在选路范围(mlp 不是 MoE 的层不动);前作 #18671 的 DSA-CP 优化不受影响。
图 4 · 改动后:业务主线一个框都没动;五条件开 SP(蓝)、Ⓐ 白归约消失(灰)、PP 出口聚全量上移(绿)
4. 主线讲解:从架构全貌到这次 PR
先对齐地基。本章走两条线 :线一·模型架构线 ——从模型架构看到 vllm-ascend 框架,看模型的每一件住哪、谁调它,4.1 先摆这块推理的朴素 PyTorch 写法、定位到框架加的壳(图 5 给出全局定位 + 4.2 目录地图·图 6);线二·PyTorch 实现线 ——从十行示例的 PyTorch 实现看到框架实现,看它被谁一步步算出来、每步为什么(4.3 流水线·图 7 → 4.4 全链时序·图 8 → 4.5 逐步收益·图 9);两条线在 4.6 的问题处交汇,4.7 讲这次 PR(图 10~14),4.8~4.10 三张表收拢,每张图配一段讲解。先说结论:attention 的数学一行没动 ;本 PR 改的全是"开关与边界"——层的 SP 开关、o_proj 的归约权、PP 出口的布局。
4.1 先看这块推理的原始写法:朴素 PyTorch
老规矩,先立对比基准——这块推理(一层 Transformer 的前向)用 PyTorch 朴素写出来长什么样 。自己动手写模型推理就会这么写:单卡、没有任何并行概念(有删节)。十行示例管的是 attention 这一小块;这里把整层摆出来,MoE 一并看:
# 朴素 PyTorch 写法:一层 Transformer 的前向(单卡,无框架)
def layer_forward(hidden_states):
h = input_layernorm(hidden_states) # 进层先归一化
h = self_attn(h) # 注意力:QKV 投影 → QKᵀ/softmax/·V → o_proj(十行示例就是这段)
hidden_states = h + hidden_states # 残差 1
h = post_attention_layernorm(hidden_states) # MoE 前再归一化
h = mlp(hidden_states=h) # 前馈;GLM 这层是 MoE:路由选专家 → 专家 FFN → 加权合并
return h + hidden_states # 残差 2 → 交给下一层
# 模型级就是 for 循环把 N 层串起来,最后 norm + lm_head 出下一个 token
这次 PR 动的不是这段数学——数学一行没动。 动的是 vLLM 框架给这段朴素代码加的壳 :进了框架,这一层被接上三件并行零件——层的 SP 开关(Ⓑ )、o_proj 的归约接线(Ⓐ )、PP 出口的交接。裸 PyTorch 里根本没有这些东西,它们全是框架为多卡布局塞进来的;PP>1 时框架还一刀切关掉 SP,让 Ⓐ 每层白付一次归约,而 FlashComm1 配置位照样报开启——名实不符,问题全住在壳上 (ⒶⒷ 编号见第 2 章)。
这段代码在全局的什么位置?把模型架构到框架的全貌画出来(图 5):
模型架构 → 框架流程:模型的每一件,在 vllm-ascend 里住哪、谁调它
模型架构(自上而下)
框架落点 · 调用者(文件:行号实测)
Token 嵌入
词表 128k · hidden 8192
上游 runner 喂 ids [1,3500],嵌入成 [1,3500,8192];
pad 到 3504 在 Runner 做(model_runner_v1.py:2935)
× N 层 DecoderLayer
每层≈1.1B,外壳串四件
层外壳 forward:input LN → self_attn → post LN → MoE
(deepseek_v2.py:1315);★ 运行时补丁 patch/ 在这重开 SP、
o_proj 交出归约权(patch_deepseek_v2.py)
└ self_attn(十行示例)
MLA/DSA:latent KV+top-2048
门卫 ops/mla.py:151 放行 → 后端 sfa_v1.py:1503
九步流水线开算;GLM 与 DeepSeek 共用这条实现链
└ MoE(路由+共享)
SwiGLU,≈0.8B/层
SP MoE:各卡各算自己 219 个 token 的分片,天然免通信
(mlp 收 already_sequence_parallel=True,不再整理布局)
MTP 模块(投机解码)
环境 MTP=3
vllm_ascend/models/deepseek_mtp.py:投机解码模块,
本 PR 不动;
改的落点在层外壳与 PP 边界(见 ★)
LM Head(与嵌入共享)
logits [1,128k]
Runner 收回输出送采样;decode 每出一个 token,
再从第 2 行整条链走一遍(KV 从页表缓存取)
读法:左列=模型自上而下的架构件;右列=它在框架里的落点与调用者;★=本 PR(18870)落点:运行时补丁 patch/ 重开 SP。链子:Runner→层外壳→门卫→后端九步。
下一步:先认目录分工(图 6),再从十行示例进流水线(图 7);完整调用时序见时序图(图 8),每步为什么这么实现见展开图(图 9)。
图 5 · 模型架构 → 框架流程:模型的每一件在 vllm-ascend 里的落点与调用者(★ = 本 PR 改动落点)
看懂这张图,再定位本 PR。 架构件一个没变、链子一段没短——变的是"运行时补丁 patch/" 这一层的两个开关 :层外壳的 SP 开关(use_sequence_parallel_moe)和 o_proj 的归约开关(reduce_results)。这就是为什么图 5 的 ★ 落在层外壳行,而不是任何注意力算子上。
4.2 先认目录:与 attention 相关的目录,一个一个认
与 attention 相关的目录:左 = 目录名,右 = 它管什么、为什么有它
vllm_ascend/worker/
执行与喂料 ★
接引擎的批 → pad 到 3504 → 调模型、收回输出
为什么有它:上游执行器绑死 NVIDIA,昇腾必须有自己的执行器
vllm_ascend/models/
模型结构
层怎么搭、forward 怎么走 —— 十行示例的调用者
为什么有它:模型结构上游都有,昇腾只改个别层,继承上游小改
vllm_ascend/ops/
算子门面
把昇腾算子包成 vLLM 认识的"层";attention 的门卫(mla.py)在这
为什么有它:调用约定不同,模型层只认门面、不认具体硬件
vllm_ascend/attention/
注意力实现
self_attn 的后端:九步流水线(sfa_v1.py:1503)在这里真正开算
为什么有它:注意力后端可插拔(不同硬件/并行形态各一套),策略独立成模块
…/attention/context_parallel/
DSA-CP 特化(车间)
⑤ 校验 · ③ 取命中 KV · ⑥ 收口 —— 上下文并行的专属逻辑
为什么有它:上下文并行(KV 与计算再切多卡)的特化单独放一个子目录
patch/(运行时补丁)★ 本 PR 主战场之一:重开 SP、聚散搬移都在这;platform.py(平台注册)知道即可
图 6 · 目录介绍:★ = 本 PR 改动文件所在的目录(patch/ 与 worker/)
四个目录,一条链,外加一个补丁层。 worker/ 管执行与喂料,models/ 管模型结构,ops/ 管算子门面,attention/ 管注意力实现,patch/ 管运行时补丁——本 PR 的两个源码文件恰好一个在 patch/、一个在 worker/ :它不改结构、不改算子,改的是"结构与算子被怎么组装、边界按什么口径喂"。
4.3 实现线:attention 在 vllm-ascend 原本是怎么完成的
架构线走完,现在切到实现线:attention 数学(图 7 左列的十行,L1~L10)在 vllm-ascend 里被一步步完成——左列每行,中列都有落点:
十行 attention 示例 → vllm-ascend 怎么完成它:左列实线是十行的流向,虚线弧线指向它在昇腾里的落点
十行 attention(样例代码)
vllm-ascend 的实现流水线
在哪个模块 · 门牌(最全)
L1 Q 投影
q = x @ wq
L2 K/V 投影
k=x@wk, v=x@wv
L3 RoPE
只转 Q、K
L4 KV Cache
cat 追加历史
L5 GQA repeat
复用 KV 给 Q 头
L6 打分
q @ kᵀ/√dk
L7 因果遮罩
遮住未来 token
L8 softmax
归一化
L9 加权和
w @ v_full
L10 输出投影
out @ wo
层外壳发起 self_attn
门卫放行 → 后端
步3 Q/KV 联合低秩投影(KV 段)
步4 K 侧 rope · 步6 Q 侧 rope
步4 写本卡缓存 · 步7 全量写回
无此步(latent KV 全头共享)
步8 稀疏注意力算子(QKᵀ)
步8 内:causal 遮罩处理
步8 内:softmax 归一化
步8 内:·V 加权求和
步9 收口(o_proj)→交回层外壳
attention/ 步3:1576
(Q 低秩段)· 步6:1614
Q 上投影
矩阵乘原样,拆低秩两段
attention/ 步3(KV 段)
步4 :1595 exec_kv
投影+rmsnorm+写cache
融成一个昇腾算子
attention/ :1595(K 侧)
:1616(Q 侧)
同一旋转,没变
融进算子,数学一致
attention/ :1626
reshape_and_cache
slot 页表逐 token 定位
代替整块 cat 追加
attention/ MLA·DSA 特性
latent KV 全头共享
目的相同(省 KV)
手段不同,无独立步骤
attention/ 步8 :1683 内
QKᵀ/√dk 原样
只在 top-2048 块上算
稀疏,不扫全量历史
attention/ 步8 算子内
causal 遮罩由算子处理
prefill 生效
decode 无需遮罩
attention/ 步8 算子内
softmax 原样
归一化没变
数值在算子内部完成
attention/ 步8 算子内
·V 加权原样
输出 [219,全头]
等待收口
attention/ :1695
o_proj 原路:直接投影
本 PR:归约权转移
(reduce_results,见 4.7)
读法:左列实线 = 十行示例自上而下,一行一格、零省略;弯曲虚线 = 每一行在 vllm-ascend 里的落点;右列门牌最全(模块 / 文件:行号 / 为什么)。
此时还没有 PR 的事——这就是 vllm-ascend 平时跑这份十行示例的样子。
图 7 · 流水线(原本):十行示例在 vllm-ascend 里的完成过程(没有这次 PR 时的样子)
看懂这张图,记住三件事。
一、十行示例一步没少。 左列每一行,中列都有对应——投影、RoPE、写缓存、稀疏注意力、收口,数学原样;唯一的例外是 L5(GQA repeat),MLA 系用 latent KV 全头共享达到同样目的,所以没有独立步骤。
二、实现分散在三个模块,但只有一条调用链。 models/ 发起 → ops/ 放行 → attention/ 干活;右列就是每一站的地盘和门牌(文件:行号)。
三、本 PR 不碰这张图。 九步流水线是"stage 内怎么算",本 PR 管的是"层外壳的开关"与"PP 边界的布局"——往下看时序图时请带着这个分工。
4.4 全链时序:谁调用谁,每一棒先自己干再交棒
图 7 看的是"数学落在哪"。这份工作是谁发起、又一层层交给谁的?把整条调用链按时间拉开——六个类六条泳道,每个函数先标"自己干了啥",再标"委托给谁":
vllm-ascend 跑十行 attention 示例:全链时序——每个函数先"自己:"干完,再"委托:"下一棒(文件:行号实测)
上游 vLLM 引擎
组批·调度
core.py:595
Worker 昇腾插件
NPUWorker
worker.py:643→669
执行器·喂料
NPUModelRunner
model_runner_v1.py
:2035 主入口
模型层 models/
AscendDeepSeekMTP
deepseek_mtp.py:23
层外壳上游 :1315
门卫 ops/mla.py
AscendMultiHead
LatentAttention
:151/:163/:181
后端 attention/
AscendSFAImpl /
AscendSFADSACPImpl
sfa_v1.py:1503
sfa_cp.py:292
自己:组批——调度器把多条请求
拼成一个批;每条请求的 attention
各自算各自的(按请求边界遮罩,互不
串);批越大 NPU 一趟吃越满(core.py:595)
委托:execute_model(批)——整批交给 Worker
引擎此后只收 output,再组下一批
自己:不碰 attention 数学——监控统计;
PP 按层切段部署时,段与段之间收发
hidden(下一段 attention 就吃这份输入)
(worker.py:643→669)
委托:model_runner.execute_model(:669)
——喂料员接手,把批变成算子要的形状
自己:喂料——pad 3500→3504(16 的倍数,
昇腾矩阵单元才吃得满);再组注意力
元数据:每条请求的边界+KV slot 起点
——attention 算子按它切请求,免造
整块大 mask 张量(:2935·build :3294)
委托:模型 forward——层外壳逐层发起,
每层一次 attention+MoE
自己:MTP 投影层(draft,与主层同构——
这里也住着一个 attention)——加载期
rot 一次性把权重排成后端要的形状,
免得每次 forward 重排(deepseek_mtp.py:23)
自己(层外壳):LN→AG 聚全量(每卡 219
hidden 做 AG 拼成 3504——十行示例的
hidden_states 输入,就是聚完的它)→
self_attn→RS 散回→postLN→MoE(:1315)
委托:self_attn(positions,hidden)——
十行示例,从这一行进入框架
自己:预建输出张量 output——十行
的 attn_output 落这;预分配免碎片
(mla.py:151)
自己:custom op 直通车(:163)——
编译器到此止步,防图优化把后端
的融合算子拆散——护住省带宽的融合
自己:取层权重/KV 句柄/批元数据,
凑齐后端开算要的三样再递进去
(mla_forward 函数体)
委托:impl.forward(层名,hidden,kv,
元数据,output)(:181)——进车间
impl 谁干活,加载期 resolve_sfa_impl 四
选一定死(:1303)——运行时每层直接调,
零选路开销;开 DSA-CP 选
AscendSFADSACPImpl(sfa_cp.py:292)
步准备:DSACPContext 立坐标——本卡管
哪段:local_start=rank×219;全量
3504=round_up(3500,16)(:1527/CP:188)
后面九步全按这套坐标切活:Q 按卡
切段(各 219),KV 不切(全量各存)
步3:Q/KV 联合低秩投影(:1576)——十行
的 q/k/v_proj 三个矩阵乘合成一枪;
KV 压成低秩 latent——cache 按它记账
不按头数记,这就是省显存的源头
步4:exec_kv 融合算子(:1595)——十行
里 K/V 侧的投影+rmsnorm+rope+写
cache 融成一枪;中间结果不落显存(省带宽)
跨卡取命中 KV 拼全量(:1605)——算
QKᵀ 要全量历史 K,本卡只存了 1/16;
只取 indexer 命中的块,不全搬——
通信量按命中块数走,不按历史长度走
步6:Q 投影+Q 侧 rope(:1614/:1616)——
十行的 q_proj+rope(q);Q 不进 cache,
随算随转,不占显存
步7:写回全量 cache(_store_parallel_kv
:1626)——本卡算出的 latent KV 每卡
各存一份,供别卡日后跨卡来取——
代价 KV×16 份,拿显存换互通
选块:indexer 选 top-2048 块(:1670)——
十行里没有这步(DSA 加的):长历史里
真正相关的只有少数块,先挑后算
步8:稀疏注意力算子(:1683)——十行的
QKᵀ/遮罩/softmax/·V 四步数学在这
一个算子里出齐;分数矩阵全程不落
显存——省带宽,这正是融合的意义
步9:收口 o_proj(:1695)——十行的
o_proj 原样;输出只剩本卡的 219 段,
交回门卫→层外壳,后面 RS 散回各卡
(原路 :1384)
output 原路返回:后端→门卫→层外壳
→Runner→Worker→引擎,再进下一层
读法:"自己:"框=自己干的活——每框写清两件事:跟十行示例什么关系、省什么(显存/带宽/算力);"委托:"箭头=交给下一棒;右侧自转圈=九步流水线(同图7 中列)。
每层都走一遍右半边,本图只画一次;impl 在加载期定死。本 PR(18870)不动门卫与后端——它改层外壳的 SP 开关与 PP 边界(见 4.7)。
图 8 · 全链时序图:从引擎到后端,每个类"自己做什么 / 委托什么"——每框写明跟十行的关系与省什么(文件与行号本地实测)
读这张图抓三个节奏。
一、委托链只有一条,没有岔路。 引擎组批 → Worker 接批 → Runner 喂料 → 层外壳发起 → 门卫放行 → 后端开算;每一棒只把活交给下一棒,谁也不越级。
二、"自己干"的都是壳上的杂活,"委托"出去的才是正餐。 Worker 自己只做监控与流水线收发;Runner 自己只做 pad 和组元数据;把十行示例真正算出来的,是最右边后端的那串自转圈。
三、本 PR 动的是泳道之外的两样东西。 层外壳进来的姿势(SP 开没开、o_proj 归不归约)和 PP 出口交出去的姿势(全量还是分片)——泳道内的每一棒都没变。
4.5 为什么这么实现:逐步收益
图 7 回答了"落在哪",图 8 回答了"谁调用谁",还剩最后一个问题:每一步为什么这么实现?各自省的是什么? 把图 7 中列的流水线原样搬到最左列,向右逐层展开——怎么做(带门牌)→ 为什么这么设计 → 收益点:
每一步为什么这么实现:流水线(最左)→怎么做→为什么→收益点,逐层展开
昇腾实现流水线
(=图7 中列)
怎么做 · 门牌
(本地实测)
为什么这么设计
收益点(省什么)
层外壳 self_attn
门卫放行 → 后端
十行 L1 的调用者
层外壳串 LN→AG→attn
→RS;门卫 :151 放行
层管布局,门卫管接入——
模型代码不碰硬件细节
接法与算法分家
换后端只动门卫
模型层零改动
多卡逻辑收在壳上
步3 Q/KV 联合
低秩投影(KV 段)
fused_qkv_a_proj(:1576)
一次矩阵乘同出 Q 路 1536
与 KV latent,后段再上投影
MLA 先压低秩 latent,
数学等价;KV 账本按
latent 记,不按全头记
KV Cache 不随头数
翻倍——省显存是大头;
三投影合一省访存
步4 K 侧 rope
步6 Q 侧 rope
K 侧旋转融进 exec_kv
(:1595);Q 侧 rope_single
(:1616)就地转
K 转后才写 cache,
Q 不进 cache 随算随转
——顺序不能反
少一次中间张量读写;
旋转数学与十行的 L3
逐位一致
步4 写本卡缓存
步7 全量写回
slot 页表逐 token 定位写
(:1626);DSA-CP 跨卡拼
全量、每卡各存一份(:1605)
页表代替整块 cat;
每卡全存是跨卡取块
命中块的前提
无大拷贝、无碎片;
代价 KV×16 份——
拿显存换正确性
无此步(GQA)
latent 全头共享
GQA 的 repeat 没有对应物:
latent KV 生来全头共用
(:1595 段内)
GQA 用 repeat 凑复用;
MLA 把复用直接做进
结构里
显存不随 Q 头数翻倍;
省掉 repeat 的拷贝
与算力(对比十行的 L5)
步8 稀疏注意力
算子(QKᵀ)
indexer 先选 top-2048 块
(:1670);算子只在选中块
上 QKᵀ/√dk(:1683)
长历史里相关的只有
少数块;索引器先挑,
注意力再算
算力从扫全量历史降到
2048 块;序列越长
省得越多
步8 内:
causal 遮罩处理
遮罩由算子内部按
prefill/decode 自动处理
(:1683 算子内)
遮罩是算子固定语义,
不必在外面造 mask
张量
省 mask 张量的显存
与生成耗时;prefill
遮未来,decode 免遮
步8 内:
softmax 归一化
softmax 在算子内部完成,
与 QKᵀ 连续执行
(:1683 算子内)
三步数学融为一个
算子,中间结果不落地
分数矩阵不写显存——
省带宽,这正是融合
的意义
步8 内:
·V 加权求和
·V 加权同算子出结果
[219,全头],停在收口前
(:1683 算子内)
同一条算子流水线,
一次出全部分数结果
每卡只算自己 219 个
token 的输出,天然
按卡并行
步9 收口(o_proj)
→交回层外壳
_finalize_o_proj(:1695;
原路 :1384)投影后交回;
SP 时归约另有安排(18870)
输出投影保持原样;
通信只在 SP 需要
时才加
不开 SP 零额外通信;
开 SP 归约只做一次
(o_proj 两路图)
读法:最左列=图7 中列的流水线原样搬来;向右三列逐层展开——怎么做(带门牌)→为什么这么设计→收益点。口径:3504 全量/219 每卡/16 卡/top-2048 块。
本图讲"原本为什么这么实现"。本 PR(18870)九步一行没动——改的是层外壳 SP 开关与 PP 出口布局(见 4.7)。
图 9 · 昇腾实现展开图:最左列 = 图 7 中列流水线;向右逐层展开「怎么做 → 为什么 → 收益点」
逐行看收益,"省"分三种,省的东西不一样。
省显存的: 低秩 latent 让 KV Cache 不随头数翻倍;slot 页表定位省掉整块 cat 的大拷贝。
省带宽的: 融合算子让中间结果不落显存——投影+rmsnorm+rope+写缓存一个算子,QKᵀ→softmax→·V 一个算子。
省算力的: 稀疏注意力只算 indexer 选中的 top-2048 块,不扫全量历史;序列越长,省得越多。
而本 PR 要省的,是这张图之外的一次通信: FlashComm1 语义下 o_proj 本可以不归约(每层省一次集合通信)——PP>1 时被 guard 一刀切关掉。九步数学没变,变的是开关。
4.6 问题:PP>1 时,SP 被 guard 关掉了
从引擎点到这段代码:模块 → 类 → 方法(改前 · 补丁链)
引擎
模块(定位)
类
方法 · 在这
上游 vLLM · 引擎
照常组批调执行
patch/ · 运行时补丁
patch_deepseek_v2.py
DeepseekV2Model
_patched_forward :293
层外壳 forward
Ⓐ o_proj 自带归约
改前状态:层 SP 被 Ⓑ guard 关掉(use_sequence_parallel_moe=False)·o_proj.reduce_results=True 每层白归约·FlashComm1 名开实关
图 10 · 改前户口:模块 → 类 → 方法(patch_deepseek_v2.py 的 _patched_forward,层 SP 关、Ⓐ 白归约)
两条线在这里交汇:架构线上是 Ⓑ 开关问题,落到实现线上,它打在层外壳与 PP 边界——十行示例本身无恙。把问题钉在代码上。上游的 guard 长在层外壳的组装处:PP>1 时 use_sequence_parallel_moe 不会被置真,decoder SP 整段关闭;而 o_proj 是 RowParallelLinear,reduce_results=True 意味着每层投影后各自做一次归约 ——这在与 FlashComm1 语义(归约收归 decoder 统一做)组合时就是纯白做:
# 改前状态(上游 guard 生效时,有删节)
layer.use_sequence_parallel_moe = False # Ⓑ guard:PP>1 时 decoder SP 被关
layer.self_attn.o_proj.reduce_results = True # Ⓐ 每层投影后各归各的——白付一次集合通信
# 而配置侧 FlashComm1 仍是"开启"——名实不符就从这来
更隐蔽的是这份"名实不符"的迷惑性:看配置、看日志,FlashComm1 都像是在跑;只有对照层外壳才知道 SP 没开、归约白付。问题不在十行数学,在开关与它背后的前提 。
4.7 这次 PR:五条件重开 SP,边界还上游一个全量
从引擎点到这段代码:模块 → 类 → 方法(改后 · PR 新补丁)
引擎
模块(定位)
类
方法 · 在这
上游 vLLM · 引擎
照常调到这一层
patch/ · 运行时补丁
init 补丁 :326 五条件
DeepseekV2Model
本 stage 的 MoE+MLA 层
层外壳 forward
SP 开 · 归约权上交
改后:五条件全中才动手·层 use_sequence_parallel_moe=True·o_proj.reduce_results=False(归约交 decoder)·PP 出口聚全量上移 transport 前
图 11 · 改后户口:模型 init 补丁五条件选路(patch_deepseek_v2.py :326 起),层 SP 重开、o_proj 交出归约权
PR 的三处源码改动,一句话一个:
# PR 新增(patch_deepseek_v2.py :326,有删节):模型 init 补丁里的选路函数
def _enable_glm_pp_sequence_parallel(self, vllm_config) -> None:
# 上游在 PP 下关 decoder SP,因为 transport 期望全量——
# 本补丁的 forward 会在每个 PP 出口恢复全量,所以这里敢开
if vllm_config.use_v2_model_runner:
return # 条件1:仅 V1 runner
parallel_config = vllm_config.parallel_config
if (vllm_config.model_config.hf_text_config.model_type != "glm_moe_dsa"
or getattr(vllm_config.model_config.hf_text_config, "moe_layer_freq", 1) != 1
or parallel_config.pipeline_parallel_size <= 1
or not parallel_config.use_sequence_parallel_moe):
return # 条件2~5:GLM DSA / 层频=1 / PP>1 / SP 开
for layer in islice(self.layers, self.start_layer, self.end_layer):
if isinstance(layer.mlp, DeepseekV2MoE) and isinstance(layer.self_attn, DeepseekV2MLAAttention):
layer.use_sequence_parallel_moe = True # 重开 stage 内 SP(dense 层不动)
layer.self_attn.o_proj.reduce_results = False # 交出归约权:同一投影对象被 MLA 后端持有
选路是五条件与门 (图 13):任一不满足就整段返回,行为与改前完全一致。动到的层必须"mlp 是 MoE 且 self_attn 是 MLA"——dense 前缀层天然被跳过。
这次 PR 改了什么:左列实线是原本的流向,虚线弧线指向 PR 的做法,右列解释最全
vllm-ascend 原本(PP>1)
这次 PR 的代码 / 做法
解释(最全)
Ⓑ guard:PP>1 关 SP
FlashComm1 名开实关
Ⓐ o_proj 自带归约
每层各归各的(白做)
SP 开后:PP 出口是分片
transport 期望全量(矛盾)
Runner:中间张量按分片
接收/估显存/最大buffer
dense 前缀层
(mlp 非 MoE)
闸门:条件不满足
(行为=改前)
init 补丁五条件与门
全中才对本 stage
MoE+MLA 层动手
o_proj 交出归约权
reduce_results=False
归约收归 decoder 收口
聚全量上移 transport 前
cat→all_gather→截断→split
每个 PP 出口都还全量
口径统一走新 helper
_pp_intermediate_token_count:
GLM=全量;其它SP=ceil(n/tp)
dense 层不动
isinstance 双查:
mlp 是 MoE 且 attn 是 MLA
闸门:任一条件不满足
整段 return,行为=改前
guard 的担忧是对的
transport 确实只认全量;
错在一刀切,不是错在约束
(patch_deepseek_v2.py :326)
这就是 FlashComm1 的
本意:投影不归约,
decoder 统一收口
每层省一次集合通信
敢开 SP 的底气:
自己兑现上游的前提;
同一段聚散 #16819 已用,
本 PR 把它搬到每个出口
三处硬编码替换:
sync_and_slice 接收切片 /
_dummy_run 估显存 /
最大 buffer(model_runner_v1.py)
scope 边界:
dense 前缀+连续 MoE 才动;
交错 dense/MoE 保持原行为
任何一级不满足:
回左列老路;
选路矩阵单测钉死
读法:左列实线=原本每一步;弯曲虚线=PR 在这一步动了什么;右列解释最全。黄=问题,蓝粗体=改动。
门卫与后端九步一行没动:本 PR 全部改在开关(patch 补丁)与边界(Runner 口径)。
图 12 · 流水线(这次 PR):左列原本(黄 = 问题),中列 PR 做法,右列解释;六行改动行行对应
这张图就是整个 PR。 左列是原本(PP>1 的跑法),中列是 PR 的做法,逐行对着看:根源行(guard)由五条件与门取代;代价行(Ⓐ 白归约)由归约权上交消掉;矛盾行(SP 开则出口分片)由聚全量上移化解;口径行(Runner)由新 helper 统一;最后两行是 scope 与闸门——不满足条件就回老路。
生效条件:五条件与门,全中才动;任一不满足整段 return(行为=改前)
条件1 · V1 runner
use_v2_model_runner == False
条件2 · 模型家族
model_type == "glm_moe_dsa"
条件3 · 层结构
moe_layer_freq == 1(连续 MoE)
条件4 · 流水线
pipeline_parallel_size > 1
条件5 · 全局开关
use_sequence_parallel_moe == True
与门
五条全中
全中 → 动手
层 SP 重开 + o_proj
交归约权(仅 MoE+MLA 层)
任一不中 → return
维持原行为,
与改前完全一致
选路矩阵单测 5 组参数化钉死:glm_moe_dsa×PP2×SP→开;PP1/SP关/V2 runner/deepseek_v3→全部不动。
注:层级的"mlp 是 MoE 且 attn 是 MLA"是第六道隐形门(isinstance 双查),dense 层在这里被跳过。
图 13 · 生效条件树:五条件与门 + 层级 isinstance 双查;任一不满足维持原行为
第二处源码改动是聚散搬移 :同一段"聚回全量"的代码(cat → all_gather → 截断 → split),从"最后一个 rank 的 norm 之前"上移到"每个 PP 出口的 transport 之前"——改前 SP 关着这段代码形同虚设,改后 SP 开了它就是每个出口的必经站:
同一段聚全量代码:从"最后 rank 的 norm 前"上移到"每个 PP 出口的 transport 前"
改前 · 形同虚设:
层循环结束
SP 关,hidden=全量
聚全量块(在最后 rank norm 前)
hidden 长度==positions 长度,不触发
norm → 返回
PP 出口交全量(靠 SP 关换来的)
改后 · 每个出口的必经站:
层循环结束
SP 开,hidden=分片 219
绿 · 聚全量块上移到 transport 前
cat→all_gather→截断→split,219→3504
PP transport → norm
交出去的永远符合上游期望
这就是敢跟 guard 讲条件的兑现书:上游要全量,出口就给全量;stage 内部才有资格开 SP。同一段聚散,#16819 已在用,本 PR 只搬家。
注意 hidden/residual 一起搬:cat 拼双份再 split,residual 还要 contiguous()——两个张量布局必须同步恢复。
图 14 · 聚散搬移:改前形同虚设(norm 前、不触发),改后是每个 PP 出口的必经站(transport 前)
4.8 对照总表:原本 vs 这次 PR,逐项对照
项 原本(PP>1) 这次 PR 后
decoder SP Ⓑ guard 关掉(use_sequence_parallel_moe=False) 五条件全中时重开(仅本 stage 的 MoE+MLA 层)
o_proj 归约 Ⓐ reduce_results=True,每层白归约 reduce_results=False,归约收归 decoder 统一收口
PP 出口布局 全量(靠 SP 关换来的) 聚全量上移到 transport 前(cat→AG→截断→split),SP 开了也守约
Runner 中间张量 SP 时按分片 ceil(n/tp)(三处硬编码) 统一走 _pp_intermediate_token_count:GLM=全量,其它 SP 模型仍 ceil(n/tp)
dense 层 不涉及 isinstance 双查跳过,交错 dense/MoE 保持原行为
#18671 的 DSA-CP 优化 —— 明确不动(PP=1 场景,与本 PR 的 PP>1 互不重叠)
4.9 PR 落点总表
文件 挂在链上哪一环 改动一句话
patch/worker/patch_deepseek_v2.py层外壳(模型 init + forward 补丁) 五条件选路重开 SP + o_proj 交归约权 + 聚全量上移 transport 前
worker/model_runner_v1.pyPP 边界(中间张量口径) 新增 _pp_intermediate_token_count,三处硬编码统一
tests/ut/patch/worker/test_patch_deepseek_v2.py选路回归 五条件矩阵 5 组参数化 + PP 聚散传播用例补配置
tests/ut/worker/test_model_runner_v1.py口径回归 新测试组:接收与 dummy 布局必须匹配模型 transport
tests/ut/models/test_deepseek_dsa_cp.py既有用例适配 配置补 pipeline_parallel_size / use_sequence_parallel_moe 字段
4.10 测试(3 个文件,选路矩阵 + 口径匹配两道锁)
测试 覆盖
test_glm_pp_sp_selection(patch 补丁)五条件矩阵参数化 5 组:glm_moe_dsa×V1×PP2×SP→开;PP=1 / SP 关 / V2 runner / deepseek_v3→全部不动
TestPPIntermediateLayout(Runner)glm_moe_dsa / deepseek_v3 × SP 开关 × 3/4 token:接收切片与 dummy buffer 长度必须等于模型 transport 的口径(GLM 恒全量,其它 SP 模型 ceil(n/tp))
既有用例配置补齐(dsa_cp) 给 AscendDSACPModel 对比用例补 parallel_config 两个字段,适配新选路条件
5. 前后代码对照(带注释)
"改后"代码均摘自 PR 实际 diff(有删节,只保留主干);行号以 PR diff 为准。绿色为本 PR 新增逻辑;ⒶⒷ 编号对应图 2 / 图 4。
5.1 模型 init 补丁:五条件重开 SP、交归约权
改前 · init 补丁里没有选路(上游 guard 生效,有删节)
def _patched_deepseek_v2_model_init(self, *args, **kwargs):
_original_deepseek_v2_model_init(self, *args, **kwargs)
# (本 PR 之前)这里没有任何 PP/SP 选路——
# PP>1 时层 SP 维持上游 guard 的关闭状态,Ⓑ 生效
self._use_upstream_aux_relay = ...
self.topk_indices_buffer = ...
改后 · 一行调用 + 选路函数(PR 新增,:306/:326)
def _patched_deepseek_v2_model_init(self, *args, **kwargs):
_original_deepseek_v2_model_init(self, *args, **kwargs)
_enable_glm_pp_sequence_parallel(self, kwargs["vllm_config"]) # ← 新增调用
...
def _enable_glm_pp_sequence_parallel(self, vllm_config) -> None:
if vllm_config.use_v2_model_runner:
return # 条件1:仅 V1 runner
parallel_config = vllm_config.parallel_config
if (vllm_config.model_config.hf_text_config.model_type != "glm_moe_dsa"
or getattr(vllm_config.model_config.hf_text_config, "moe_layer_freq", 1) != 1
or parallel_config.pipeline_parallel_size <= 1
or not parallel_config.use_sequence_parallel_moe):
return # 条件2~5
for layer in islice(self.layers, self.start_layer, self.end_layer):
if isinstance(layer.mlp, DeepseekV2MoE) and isinstance(layer.self_attn, DeepseekV2MLAAttention):
layer.use_sequence_parallel_moe = True # 重开 SP(dense 层被 isinstance 挡住)
layer.self_attn.o_proj.reduce_results = False # 归约权上交;同一投影对象被 Ascend MLA 后端持有
5.2 PP 出口:聚全量上移到 transport 之前(图 14)
改前 · 聚全量块在"最后 rank 的 norm 前"(SP 关时不触发,形同虚设)
if not pp_group.is_last_rank:
pp_output = IntermediateTensors(...)
...
return pp_output
# ↓ 下面这段只在最后一个 rank 执行得到
if hidden_states.shape[0] != positions.shape[0]:
combined = torch.cat([hidden_states, residual], dim=-1)
combined = tensor_model_parallel_all_gather(combined, 0)
...
hidden_states, _ = self.norm(hidden_states, residual)
改后 · 整块上移:transport 之前先还全量(PR 新增位置)
# Restore full tokens before PP transport as well as the final norm.
if hidden_states.shape[0] != positions.shape[0]:
combined_states = torch.cat([hidden_states, residual], dim=-1)
combined_states = tensor_model_parallel_all_gather(combined_states, 0)
combined_states = combined_states[: positions.shape[0]]
hidden_size = self.hidden_size
hidden_states, residual = combined_states.split([hidden_size, hidden_size], dim=-1)
residual = residual.contiguous()
if not pp_group.is_last_rank:
pp_output = IntermediateTensors({"hidden_states": hidden_states, "residual": residual})
... # 出口交出去的必是全量
hidden_states, _ = self.norm(hidden_states, residual)
5.3 Runner:PP 中间张量口径统一(图 15)
改后 · 新 helper + 三处替换(model_runner_v1.py :3279 起,有删节)
def _pp_intermediate_token_count(self, num_tokens: int) -> int:
# GLM 的 decoder 在 stage 内用 SP,但补丁会在每个 PP 边界恢复全量
if self.model_config.hf_text_config.model_type == "glm_moe_dsa" or not enable_sp():
return num_tokens # GLM:全量
tp = self.vllm_config.parallel_config.tensor_parallel_size
return (num_tokens + tp - 1) // tp # 其它 SP 模型:分片 ceil(n/tp)
# 三处硬编码替换:sync_and_slice_intermediate_tensors 的接收切片、
# _dummy_run 的显存预估与最大 buffer——口径从此只有一份
一个口径函数管三处:PP 中间张量到底按多少 token 算
glm_moe_dsa(或 SP 未开)
口径 = 全量 num_tokens——边界已还全量
其它 SP 模型
口径 = ceil(n/tp) 分片——沿原约定
调用点1 · 接收切片
sync_and_slice:copy_len
调用点2 · dummy 估显存
_dummy_run:防止多算一次 AG
调用点3 · 最大 buffer
make_empty_intermediate_tensors
改前是三处各自硬编码(SP 时一律除以 tp);GLM 边界还全量后,统一走一个函数——口径只有一份,错也不会错三样。
图 15 · PP 中间张量口径:GLM=全量、其它 SP 模型=分片,三处调用点统一走 _pp_intermediate_token_count
5.4 归约权转移:一行改动的分量
layer.self_attn.o_proj.reduce_results = False
# 别看只是一行:o_proj 是 RowParallelLinear,reduce_results=True 时
# 每层投影后各自归约(Ⓐ);置 False 后归约权交给 decoder 统一收口——
# 注释原话:This is the same projection object held by the Ascend MLA backend.
# 即改的这个对象,就是门卫/后端链上真正干活的那个投影——改一处,全链生效
6. 与我们排查问题的关系
如实说:这个 PR 不直接打我们的场景
我们环境是双 A3、TP=16 DP=2、PP=1 ——guard 的条件(PP>1)在我们这根本不触发,所以 18671 的 DSA-CP 优化也不受本 PR 影响(它明确声明不动 #18671)。但它是同一套布局契约体系的 PP 维度补全 :18671 解决"PP=1 时聚了又切",18870 解决"PP>1 时 SP 被 guard 关掉"。将来若多机切 PP(stage 接力),这个 PR 就是 FlashComm1 能开的前提。
方法论上值得记一笔:guard 的约束(transport 期望全量)是对的,错的是一刀切的实现。 本 PR 的解法不是绕过约束,而是把约束的前提自己兑现——"边界还全量,stage 内才敢开 SP"。这个模式和 18671 的"布局契约"一脉相承:与其让开关一刀切,不如把布局责任钉在边界上。
PR 自述状态:ruff 通过;选路矩阵与口径匹配的回归用例已就位;张量单测与 NPU 正确性/性能验证仍 pending,此前一轮 CI 败在环境 (pip 证书与 apt 签名),还没跑到测试。
7. 收益、风险与待验证
收益
PP>1 的 GLM DSA V1 部署真正拿到 FlashComm1:decoder SP 生效、Ⓐ 每层白归约消失、归约收归统一收口。无需新配置项——条件满足自动生效。
安全性设计
五条件与门 + 层级 isinstance 双查:模型/runner/PP/SP 任一不满足即整段返回,行为与改前一致;dense 前缀层不动;交错 dense/MoE 不动;#18671 的 DSA-CP 优化明确不变。
待验证(PR 自述)
张量单测未跑完;NPU 正确性与性能验证 pending;无性能数据(作者明说"No performance gain has been measured");前轮 CI 败在环境未到测试。当前开放中、以合入版为准。
对我们的行动建议
现在不动:我们 PP=1,本 PR 行为对我们无变化;
跟进 CI 与 NPU 验证结论——若合入,它验证了"patch 层重开 SP + 边界还全量"这套模式的可行性,与 18671 是同一张蓝图的两块;
若将来上 PP(stage 接力),回看本篇第 4.7/5.2:出口聚全量是 FlashComm1 的前提。
8. 术语速查
术语 本 PR 语境下的含义
FlashComm1 GLM V1 的 SP MoE 通信模式:decoder 开 SP、注意力投影不自带归约,归约交给统一收口
PP / stage 流水线并行:按层把模型切段接力;本 PR 的开关卡在 PP>1
PP=1 guard 上游在 PP>1 时关闭 decoder 级 SP 的一刀切(transport 期望全量激活)——Ⓑ 的根源
reduce_results RowParallelLinear 的归约开关:True=每层投影后各自归约(Ⓐ 白做),False=归约权上交
_pp_intermediate_token_count 本 PR 新增的口径函数:GLM 恒全量、其它 SP 模型 ceil(n/tp),三处调用点统一
dense 前缀 连续 MoE 层之前的稠密层——被 isinstance 双查跳过,永不参与选路
patch 机制 vllm_ascend/patch/:运行时 monkeypatch 上游类(如 DeepseekV2Model.__init__/forward)——本 PR 的主战场
来源:github.com/vllm-project/vllm-ascend PR #18870(开放中,base: releases/v0.30.0,+176/−28,5 文件)· 本页为技术解读,行号以 PR diff 为准,数据均引自 PR 原文