# 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.py`
  - `GlmMoeDsaForCausalLM`
  - 继承 `DeepseekV2ForCausalLM`
- 因此，相关逻辑实际落在 **DeepSeek-NSA** 这套实现上，而不是独立的 `glm5.py`

## 一句话总结

当前实现里的两次 gather 不是在重复做同一件事：

1. **attention 内的 gather**：为当前层的 **K 侧 / index 视图** 服务
2. **prefill 尾部的 gather**：为最终 **hidden_states -> logits/logprobs** 服务

更简洁地说：

> 第一次是给 attention/index 用，第二次是给输出/logprob 用。

---

## 总体流程图

```text
[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_pe`
- `indexer key`

### 关键调用点

#### 1. MLA KV 重建

- `python/sglang/srt/models/deepseek_common/attention_forward_methods/forward_mla.py:302-306`
- `python/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-split`
- `in-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 位置取 hidden
- `449-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 不是冗余，而是分别服务于：

1. **attention/index 边界**
2. **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_cache`
  - `key`
  - `k_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 都有可能被替换或减少。
