- nsa_prefill_cp_all_gather.md: explains why there are two all-gathers in current NSA prefill CP (per-layer KV/index gather vs tail output hidden gather), with data flow diagrams and involved file lists - nsa_prefill_cp_phase1_narrow_output_collection.md: detailed Phase 1 design plan covering batch-level eligibility, fallback conditions, affected modules, KV transfer impact, risks, and test matrix
7.8 KiB
NSA Prefill CP 中两次 All-Gather 的作用说明
本文档描述 当前 SGLang 实现 中,GLM 路径在 NSA Prefill Context Parallel (CP) 下为什么会出现两类 all-gather,它们分别服务于什么语义边界,以及涉及哪些关键文件。
本文档只描述 当前行为,不讨论重构后的目标设计。
适用范围
- 只讨论 prefill CP
- 不讨论 decode 开启 CP 的情况
- 这里的 GLM 实际走的是:
python/sglang/srt/models/glm4_moe.pyGlmMoeDsaForCausalLM- 继承
DeepseekV2ForCausalLM
- 因此,相关逻辑实际落在 DeepSeek-NSA 这套实现上,而不是独立的
glm5.py
一句话总结
当前实现里的两次 gather 不是在重复做同一件事:
- attention 内的 gather:为当前层的 K 侧 / index 视图 服务
- prefill 尾部的 gather:为最终 hidden_states -> logits/logprobs 服务
更简洁地说:
第一次是给 attention/index 用,第二次是给输出/logprob 用。
总体流程图
[Request enters prefill]
|
v
ScheduleBatch.prepare_for_extend
|
+--> alloc_for_extend()
| - 分配 out_cache_loc
| - write_cache_indices()
| - req_to_token[req, token_pos] = loc
|
v
DeepseekV2ForCausalLM.forward
|
+--> prepare_input_dp_with_cp_dsa()
| - 生成 nsa_cp_metadata
|
v
DeepseekV2Model.forward
|
+--> cp_split_and_rebuild_data(hidden_states)
+--> cp_split_and_rebuild_position(positions)
| => 输入进入 CP-local token 布局
|
v
[每一层 NSA/MLA attention]
|
+--> 本地算 q / latent_cache / k_nope / k_pe
|
+--> Gather #1
| - 重建当前层需要的 K / latent KV / index key 全局顺序视图
|
+--> NSA index / topk / page-table
|
+--> 写入本地 KV pool
|
v
[所有层结束]
|
+--> Gather #2
| - 恢复最终 hidden_states 的原 token 顺序
|
+--> logits_processor
| - next token logits
| - input logprobs
| - top_logprobs / token_ids_logprobs
|
v
[Prefill ends]
第一次 All-Gather:attention 内的 gather
是为了什么
在 CP 模式下,prefill 一开始就会把 token 重新切分并重排,以便均衡负载。
但是当前 NSA/MLA 的实现仍然需要:
- 正确顺序的
K - 正确顺序的
latent KV - 正确顺序的
index key
只有这样,后续的:
- MLA attention
- NSA index / top-k
- page-table 生成
才能继续工作。
所以 attention 内部需要先 gather 一次,把当前层所需的 K 侧信息恢复成全局逻辑顺序。
gather 的对象
主要是两类:
latent_cache / k_nope / k_peindexer key
关键调用点
1. MLA KV 重建
python/sglang/srt/models/deepseek_common/attention_forward_methods/forward_mla.py:302-306python/sglang/srt/models/deepseek_v2.py:1475-1487
这里 rebuild_cp_kv_cache() 会调用:
cp_all_gather_rerange_output(...)
把局部的 MLA KV 重新拼成全局逻辑顺序。
2. NSA indexer key 重建
python/sglang/srt/layers/attention/nsa/nsa_indexer.py:336-344
这里同样会:
cp_all_gather_rerange_output(key, ...)
把 indexer 用的 key 恢复成全局顺序,再去做 top-k/index。
关键实现 helper
python/sglang/srt/layers/attention/nsa/utils.py:341-404
这里的 cp_all_gather_rerange_output(...) 注释已经明确说明,它会把被:
round-robin-splitin-seq-split
打散过的 token / block 重新恢复成原始逻辑顺序。
本质语义
这次 gather 的本质是:
KV / index 语义 gather
它不是为了最终输出,也不是为了 logits。
第二次 All-Gather:prefill 尾部的 gather
是为了什么
即使 attention 内部已经把当前层需要的 K/index 问题解决了,模型最终输出的 hidden_states 仍然还处在 CP 重排后的布局里。
但下游的:
logits_processor- input logprobs
- top_logprobs
- token_ids_logprobs
都默认假设:
hidden_states已经按原始 flatten token 顺序排好
因此,在模型尾部还要再 gather 一次,把最终 hidden 恢复回原 token 顺序。
gather 的对象
- 最终层输出的
hidden_states
关键调用点
1. 模型尾部 gather
python/sglang/srt/models/deepseek_v2.py:2046-2053
这里直接做:
cp_all_gather_rerange_output(hidden_states, ...)
2. 下游消费者
python/sglang/srt/layers/logits_processor.py:422-527
这里可以直接看到:
- 无 input logprob 时,会按每个请求最后 token 的原始位置取
hidden_states - 有 input logprob 时,会按原始 flatten token 顺序切
hidden_states
例如:
422-448:按每请求最后 token 位置取 hidden449-527:按原始 token 顺序构造sample_indices/input_logprob_indices/token_to_seq_idx
本质语义
这次 gather 的本质是:
output / logprob 语义 gather
它不是为了 attention 本身,而是为了让输出层继续消费“原 token 顺序 hidden”。
为什么会有两次 gather
因为它们处理的是两个不同的边界:
第一次 gather
- 发生在每层 attention/indexer 路径中
- 解决的是:
- 当前层要怎么“看见”完整且按正确顺序排列的 K / key / index 视图
第二次 gather
- 发生在 prefill 尾部
- 解决的是:
- 最终
hidden_states怎么重新和原始 token 顺序对齐 - 这样
logits/logprobs才能对得上输入
- 最终
所以当前实现下,这两次 gather 不是冗余,而是分别服务于:
- attention/index 边界
- output/logprob 边界
涉及的关键文件
| 文件 | 作用 |
|---|---|
python/sglang/srt/models/glm4_moe.py |
GLM 入口;GlmMoeDsaForCausalLM 继承 DeepseekV2ForCausalLM |
python/sglang/srt/models/deepseek_v2.py |
CP 主模型逻辑、MLA KV 重建、最终 hidden gather |
python/sglang/srt/models/deepseek_common/attention_forward_methods/forward_mla.py |
MLA prepare 阶段触发 KV 重建 |
python/sglang/srt/layers/attention/nsa/nsa_indexer.py |
index key gather、top-k/index 相关逻辑 |
python/sglang/srt/layers/attention/nsa/utils.py |
cp_all_gather_rerange_output(...) 与 split/rerange 逻辑 |
python/sglang/srt/layers/attention/nsa_backend.py |
NSA metadata、topk/page-table、KV 写入 |
python/sglang/srt/layers/logits_processor.py |
logits/logprob 消费原始 token 顺序 hidden |
python/sglang/srt/managers/schedule_batch.py |
构造 prefill/logprob 元数据 |
python/sglang/srt/model_executor/forward_batch_info.py |
batch 侧输出张量和 metadata |
python/sglang/srt/mem_cache/common.py |
alloc_for_extend() / write_cache_indices() |
python/sglang/srt/mem_cache/memory_pool.py |
set_mla_kv_buffer() / get_key_buffer() |
最容易混淆的一点
cp_all_gather_rerange_output(...) 这个 helper 会被复用。
所以不能只看“是不是调用了同一个函数”,而要看它 gather 的到底是什么 tensor:
-
如果 gather 的是:
latent_cachekeyk_nope / k_pe- 那它属于 attention/KV/index gather
-
如果 gather 的是:
hidden_states- 那它属于 输出/logprob gather
一个更准确的口头总结
如果要用一句简单但准确的话向别人解释当前实现:
每层 attention 中间的 all-gather,是为了重建当前层需要的完整 K/index 视图;请求结尾的 all-gather,是为了把最终 hidden 恢复成原 token 顺序,好让 logits/logprobs 对得上输入。
备注
- 本文档只描述 当前实现,不代表所有 context parallel 设计都必须这样。
- 如果未来改成:
- ring attention
- sharded KV
- shard-aware logits/logprob 输出 那么这两类 gather 都有可能被替换或减少。