feat: DeepSeek-V4 DSA context-parallel on NPU#1997
Open
LMxyzptlk wants to merge 9 commits into
Open
Conversation
Add prefill-only DSA context parallelism for DeepSeek-V4 on Ascend NPU, aligned with vllm-ascend dsa_cp.py. A single switch (--enable_dsa_cp) turns it on; the CP geometry is derived from TP (the TP group IS the CP token-split group), so there is no separate cp_size dimension to mis-configure. This replaces the earlier orthogonal cp_size approach, where the token split used cp_size while the reassembly gather ran over every TP member (cp_size(2) vs cp_group world_size(4)), corrupting the full-token stream. Core mechanism (DSAttention forward): - Derive cp_size/cp_rank/cp_group from TP when --enable_dsa_cp is set; q is computed on this rank's LOCAL token slice. - q_b and attn_sink are loaded FULL-head (replicated via the single-rank group) so each rank runs attention over ALL heads on its local tokens; decode / non-CP prefill slice both back to the TP head shard, keeping those paths byte-identical to the non-CP baseline. - Replace the reassembly gather with all_to_all_4D (scatter=2, gather=1): [T_local, num_heads, head_dim] -> [T_full, n_local_heads, head_dim]. Head shard r maps to TP rank r, so the standard TP o_proj runs unchanged. o_proj moves after the transpose. Metadata (deepseek_v4.h / deepseek_v4_mtp.h) applies the same CP=TP derivation so the precomputed sparse metadata (local cu_seqlens, right-aligned ori_kv via local_kv_start_loc) matches the local token slice the attention runs on. The MTP draft shares the DSAttention layer and needs the identical derivation. Validated on DeepSeek-V4-Flash-w8a8-mtp (dp=4, tp=4): gpqa_diamond recovers from 0.28 (broken cp=2) to ~0.9, matching / exceeding the cp=1 baseline (0.8), with MTP enabled.
LMxyzptlk
requested review from
Clement-Wang26,
DongheJin,
DragonFive,
JimHsiung,
Kang-Meng,
RobbieLeung,
XuZhang99,
liujinguang0125,
liutongxuan,
walsonyang,
xiao-yu-chen,
yingxudeng,
yq33victor and
zhang-minchao
as code owners
July 21, 2026 13:04
XuZhang99
reviewed
Jul 21, 2026
| "AllGather); other K (K divides cp_size) means KV is sharded " | ||
| "across K ranks while token-CP still uses cp_size."); | ||
|
|
||
| DEFINE_bool(enable_dsa_cp, |
Remove the dsa_cp_kv_interleave_size flag (declaration + definition).
When chunked prefill creates a tail chunk smaller than cp_size (e.g. 1 token, cp_size=2), some ranks get 0 local query tokens. The quantized matmul in q_a_proj (aclnnQuantMatmulV4) crashes on 0-row input tensors. Three guard points when cp_local_empty (cp_active && local_len==0): 1. run_dsv4_preprocess_fallback: skip q projections, still compute kv for cache writes — every rank must write its own KV replica. 2. indexer select_qli: skip when local is empty (M1 kv_split_size=1: all ranks derive identical index-cache entries from the same full hidden_states, so rank 0 already wrote the correct data). 3. sparse_attn_sharedkv: produce zero-row attn_output directly, avoiding a 0-element kernel launch. Empty rank contributes zeros through all_to_all (padded to tokens_per_rank), which are discarded after the post-all_to_all slice back to the real token count. o_proj runs normally on the gathered full-token output. kv_split_size=1 (replicated KV). Skipping the index-cache write for empty ranks is safe only while every rank computes routing from identical full hidden_states.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
DeepSeek-V4 DSA Context Parallel (Model 1, CP == TP)
背景 (Background)
问题
DeepSeek-V4 使用 DSA (DeepSeek Sparse Attention):MLA 低秩 + 滑窗 (SWA) + 压缩 KV (compressor,
compress_ratio ∈ {1,4,128}) + lightning indexer 稀疏 top-k 选择。在 xLLM 的 NPU (昇腾) 推理路径上,V4 的DSAttention前向 没有任何序列切分:q/kv 都在全量 token 上计算,只有 head 走 TP 分片。长序列 prefill 场景下,单卡需要承载全量 token 的 attention 计算和 KV,导致:
参考实现是 vllm-ascend 的
dsa_cp(vllm_ascend/attention/context_parallel/dsa_cp.py),它把 TP 组直接当作 CP 组 (get_tp_group()),token 流按 TP world size 均分,每个 rank 拿全量 head、本地 token 切片,算完再 all-to-all 转回标准 TP 头分片布局。目标 (M1,prefill-only)
FLAGS_enable_dsa_cp=false时所有路径与非 CP 基线字节级一致。非目标
设计 (Design)
采用与 vllm-ascend 一致的 CP == TP 拓扑,而不是 xLLM 已有的独立
cp_size维度:cp_size = tp_size,cp_rank = tp_rank,cp_group_ = tp_group_。cp_size == 1(world = dp * tp),CP 几何完全从 TP 派生,运行期由单个开关FLAGS_enable_dsa_cp触发。单层前向数据流 (prefill, CP active)
每个 rank 拿到 全量 token(worker 不切分,见下),在层内做以下变换:
关键点:
local_kv_start_loc≠local_query_start_loc:本地 query 的因果可见 KV 是右对齐的(最后一个 rank 看到全量 KV 流),所以ori_kv路径必须用local_kv_start_loc(local_seq_lens的 cumsum),不能复用本地 query 的 cu-seqlens。q_b_proj与attn_sink在 CP 开启时加载为全 head(复制),使每个 rank 能在本地 token 上算所有 head;decode / 非 CP prefill 仍切回n_local_heads的 TP 分片,保持基线路径不变。切分位置:模型侧 (model-side)
DSA-CP 采用 模型侧 token 切分,而不是 worker 侧:
DSAttention层内部切出本地 query 切片。Platform::supports_dsa_cp_model_partition()(NPU-only)+FLAGS_enable_dsa_cp+cp_size > 1gate,让 worker 跳过 worker 侧 head-tail CP 切分。代码实现 (Implementation)
改动分层如下(路径相对仓库根)。
1. 配置与开关
xllm/core/common/global_flags.hDECLARE_bool(enable_dsa_cp):DSA-CP 总开关(默认 false)。DECLARE_int32(dsa_cp_kv_interleave_size):KV 交错粒度,对齐 vllm-ascendcp_kv_cache_interleave_size。M1 prefill-only 路径忽略,为 M2 decode KV 分片预留。xllm/core/framework/config/parallel_config.{h,cpp}DEFINE_bool(enable_dsa_cp, false, ...)+DEFINE_int32(dsa_cp_kv_interleave_size, 1, ...)。enable_dsa_cp接入ParallelConfig(domain object):加入option_category()、PROPERTY(bool, enable_dsa_cp)、from_flags()、from_json()、append_config_json()。XLLM_CONFIG_ASSIGN_FROM_JSON会把 config.json 的值写回FLAGS_enable_dsa_cp,所以命令行 flag 与 config.json 两种起服务方式都能读到该开关。xllm/core/platform/platform.hsupports_dsa_cp_model_partition()(NPU-only,constexpr):报告平台是否支持 DSA-CP 模型侧切分。运行期是否真正走该路径由调用点额外用FLAGS_enable_dsa_cp && cp_size > 1gate。2. CP 本地元数据(核心新增)
xllm/core/layers/common/dsa_metadata.hstruct DSACPMetadata:local_query_start_loc/local_seq_lens/local_kv_start_loc/local_start/local_end/tokens_per_rank/num_tokens_pad/local_cos/local_sin。DSAMetadata新增cp_enabled/cp_size/cp_rank/cp_metadata,取代原来的cp_input_dict占位(占位保留向后兼容)。xllm/core/layers/common/dsa_metadata_builder.{h,cpp}build_cp_local_metadata(query_start_loc, seq_lens, cp_size, cp_rank, full_cos, full_sin):cp_size个 rank,返回当前 rank 的本地视图。对齐 vllm-ascend_build_local_token_metadata。num_tokens_pad(pad 到cp_size倍数)/tokens_per_rank/local_start/local_end;用全局 cu-seqlens 与本地区间求交得local_query_start_loc/local_seq_lens;local_kv_start_loc是local_seq_lens的右对齐 cumsum。cp_size == 1时返回 identity 视图(本地即全量),调用方走单一代码路径。3. DSAttention 前向(核心新增)
xllm/core/layers/npu_torch/deepseek_sparse_attention.{h,cpp}cp_size_/cp_rank_/cp_enabled_/cp_group_/cp_full_head_。cp_enabled_ = FLAGS_enable_dsa_cp && tp_size > 1;启用时cp_size_ = tp_size、cp_rank_ = tp_rank_、cp_group_ = tp_group_。cp_full_head_ = cp_enabled_。q_b_proj在 CP 时用single_rank_group_(world_size==1)的ColumnParallelLinear,加载全量(未分片)权重 + W8A8 scale,量化 matmul 路径不变。attn_sink同理按cp_full_head_决定 full-head 还是 TP 分片(构造 +load_state_dict两处)。forward():cp_active = cp_enabled_ && cp_prefill时走本地 q + 全量 KV,逆 RoPE 用 local cos/sin,然后:constant_pad_ndpad 到tokens_per_rank,转置后 slice 回真实 token 数,再走标准 TP o_proj。n_local_heads_,保证与基线一致。4. Indexer full/local 分离
xllm/core/layers/npu_torch/deepseek_v4_indexer.{h,cpp}select_qli新增compress_x/compress_cu_seqlens可选参数:定义时,index-cache-update 路径(compress_kv)用全量 token 张量,而 query/weights/top-k 仍用本地x/actual_seq_lengths_query。对齐 vllm-ascend_update_indexer_cache(full)vs_indexer_select_topk(local)。5. 模型前向装配
xllm/models/llm/deepseek_v4.hFLAGS_enable_dsa_cp && dp_local_tp_size_ > 1时cp_size_ = dp_local_tp_size_、cp_rank_ = rank % dp_local_tp_size_;否则回退到 configcp_size。build_cp_local_metadata把cu_seqlens_q/seqused_kv/batch_size/max_seqlen_q替换成本地几何;cu_seqlens_ori_kv用local_kv_start_loc(右对齐)。否则 kernel 会按全量几何索引 q,越过本地行数 → MTE DDR 越界。qli_metadata同样本地化(query_lens/key_lens/qli_max_seqlen_q),否则 top-k 塌缩到 position 0(topk_max==0)导致 attention 退化。aux_hidden_states输出(服务 MTP)。6. MTP / 投机解码
xllm/models/llm/deepseek_v4_mtp.hDSAttention层,在 CP 下也切本地 q,所以 draft 的 precomputed metadata 必须同样本地化。之前该文件缺少主模型有的 CP-local 分支,导致 draft crash(MTE 越界)。此 PR 在 MTP 里补齐了与deepseek_v4.h对称的build_cp_local_metadata分支。forward传递aux_hidden_states。7. Worker 侧 gate
xllm/core/runtime/worker_impl.cpp&xllm/core/runtime/llm_worker_impl.cppdsa_cp_model_side = FLAGS_enable_dsa_cp && cp_size > 1 && Platform::supports_dsa_cp_model_partition()。worker_impl.cpp:needs_cp_prefill_side增加!dsa_cp_model_side,跳过 worker 侧 head-tail 切分 + ATB cp-tensor 准备(那是给 V3.2 ATB 路径用的)。llm_worker_impl.cpp:uses_worker_cp_partition增加!dsa_cp_model_side,避免走 worker 侧 CP 路径(LmHead all-gather + 3-arg logits),V4 不实现该路径。8. 单测
tests/core/layers/npu_torch/dsa_cp_metadata_tests.cpp(新增)+CMakeLists.txtbuild_cp_local_metadata:DocstringRank0/1/2:三个 rank 的local_query_start_loc/local_seq_lens/local_kv_start_loc与手算一致。RanksPartitionAllQueryTokens:各 rank 本地 token 数之和 == 全量。CpSizeOneIsIdentity:cp_size==1返回 identity。PaddingAndBoundaryCross:padding + 跨 rank 边界请求的 KV 长度修正。使用方式
开关关闭时(默认),所有路径与非 CP 基线完全一致。
测试与验证
build_cp_local_metadataCPU 单测(本 PR 含)。enable_dsa_cp开/关的 prefill 输出在容差内一致(bf16 相对误差 < 1e-2);coveringcompress_ratio ∈ {1,4,128}。tp_size ∈ {2,4}。已知限制