Enable compute-owner KV layout by page-aligning NSA CP split
Phase 5 needs each current KV page to have exactly one CP compute owner before local KV/index direct writes can be safe. This change teaches in-seq NSA prefill CP to produce page-aligned split metadata under shared-KV mode, threads page size into the metadata builders, and fixes local pair splitting so unequal page-aligned zigzag segments do not corrupt topk inputs. Constraint: Phase 5 direct-write layout requires page ownership to be expressible at page granularity Constraint: Short page-unit batches remain on the token-balanced fallback to avoid zero-page segment risk Rejected: Split local q/weights by half | page-aligned zigzag segments can have unequal token counts Confidence: medium Scope-risk: moderate Directive: Do not enable compute-owner direct writes unless nsa_cp_metadata.page_aligned is true and local loc ownership is verified Tested: python3 -m py_compile python/sglang/srt/layers/attention/nsa/utils.py python/sglang/srt/layers/attention/nsa/nsa_indexer.py python/sglang/srt/models/deepseek_v2.py python/sglang/srt/models/deepseek_nextn.py test/registered/unit/layers/test_nsa_cp_utils.py Not-tested: Local pytest collection is blocked in this environment by missing pybase64; container/runtime tests were not rerun during this commit step
This commit is contained in:
@@ -54,6 +54,7 @@ from sglang.srt.layers.attention.nsa.utils import (
|
||||
cp_all_gather_rerange_output,
|
||||
is_nsa_enable_prefill_cp,
|
||||
is_nsa_prefill_cp_in_seq_split,
|
||||
split_in_seq_cp_local_pair,
|
||||
)
|
||||
from sglang.srt.layers.communicator import ScatterMode
|
||||
from sglang.srt.layers.linear import ReplicatedLinear
|
||||
@@ -1374,11 +1375,17 @@ class Indexer(MultiPlatformOp):
|
||||
# cp_batch_seq_index_prev = forward_batch.nsa_cp_metadata["cp_batch_seq_index_prev"]
|
||||
# cp_batch_seq_index_next = forward_batch.nsa_cp_metadata["cp_batch_seq_index_next"]
|
||||
# TODO prev, next, combined into a single call
|
||||
q_fp8_prev, q_fp8_next = torch.split(
|
||||
q_fp8, (q_fp8.shape[0] + 1) // 2, dim=0
|
||||
q_fp8_prev, q_fp8_next = split_in_seq_cp_local_pair(
|
||||
q_fp8,
|
||||
actual_seq_q_prev,
|
||||
actual_seq_q_next,
|
||||
name="q_fp8",
|
||||
)
|
||||
weights_prev, weights_next = torch.split(
|
||||
weights, (weights.shape[0] + 1) // 2, dim=0
|
||||
weights_prev, weights_next = split_in_seq_cp_local_pair(
|
||||
weights,
|
||||
actual_seq_q_prev,
|
||||
actual_seq_q_next,
|
||||
name="weights",
|
||||
)
|
||||
topk_result_prev = self._get_topk_ragged_with_cp(
|
||||
forward_batch,
|
||||
|
||||
@@ -135,6 +135,15 @@ def pad_nsa_cache_seqlens(forward_batch: "ForwardBatch", nsa_cache_seqlens):
|
||||
return nsa_cache_seqlens
|
||||
|
||||
|
||||
@dataclass
|
||||
class PageAlignedInSeqSplitInfo:
|
||||
page_aligned: bool = False
|
||||
page_size: int = 1
|
||||
extend_prefix_len: int = 0
|
||||
segment_page_starts: List[int] = None
|
||||
segment_page_ends: List[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class NSAContextParallelMetadata:
|
||||
split_list: List[int] = None
|
||||
@@ -152,6 +161,146 @@ class NSAContextParallelMetadata:
|
||||
actual_seq_q_prev_tensor: torch.Tensor = None
|
||||
actual_seq_q_next_tensor: torch.Tensor = None
|
||||
total_seq_lens: torch.Tensor = None
|
||||
page_aligned: bool = False
|
||||
page_size: int = 1
|
||||
extend_prefix_len: int = 0
|
||||
segment_page_starts: List[int] = None
|
||||
segment_page_ends: List[int] = None
|
||||
|
||||
|
||||
def build_token_balanced_in_seq_split_list(total_len: int, cp_size: int) -> List[int]:
|
||||
if cp_size <= 0:
|
||||
raise ValueError(f"cp_size must be positive, got {cp_size}")
|
||||
if total_len < 0:
|
||||
raise ValueError(f"total_len must be non-negative, got {total_len}")
|
||||
|
||||
cp_segment_num = cp_size * 2
|
||||
base = total_len // cp_segment_num
|
||||
remainder = total_len % cp_segment_num
|
||||
return [base + (1 if i < remainder else 0) for i in range(cp_segment_num)]
|
||||
|
||||
|
||||
def _fallback_page_aligned_split_info(
|
||||
*,
|
||||
page_size: int,
|
||||
extend_prefix_len: int,
|
||||
) -> PageAlignedInSeqSplitInfo:
|
||||
return PageAlignedInSeqSplitInfo(
|
||||
page_aligned=False,
|
||||
page_size=page_size,
|
||||
extend_prefix_len=extend_prefix_len,
|
||||
segment_page_starts=[],
|
||||
segment_page_ends=[],
|
||||
)
|
||||
|
||||
|
||||
def build_page_aligned_in_seq_split_list(
|
||||
*,
|
||||
total_len: int,
|
||||
extend_len: int,
|
||||
extend_prefix_len: int,
|
||||
page_size: int,
|
||||
cp_size: int,
|
||||
) -> Tuple[List[int], PageAlignedInSeqSplitInfo]:
|
||||
"""Build an in-seq split list whose real-token boundaries do not cut pages.
|
||||
|
||||
Phase 4 deliberately uses a conservative gate: at least `2 * cp_size` page
|
||||
units are required so every zigzag segment has at least one page unit. When
|
||||
the gate does not hold, this helper falls back to the existing token-balanced
|
||||
split and marks the result as not page-aligned.
|
||||
"""
|
||||
|
||||
if extend_len < 0:
|
||||
raise ValueError(f"extend_len must be non-negative, got {extend_len}")
|
||||
if total_len < extend_len:
|
||||
raise ValueError(
|
||||
f"total_len must be >= extend_len, got total_len={total_len} "
|
||||
f"extend_len={extend_len}"
|
||||
)
|
||||
|
||||
fallback_split = build_token_balanced_in_seq_split_list(total_len, cp_size)
|
||||
fallback_info = _fallback_page_aligned_split_info(
|
||||
page_size=page_size,
|
||||
extend_prefix_len=extend_prefix_len,
|
||||
)
|
||||
if page_size <= 1 or extend_len <= 0 or extend_prefix_len % page_size != 0:
|
||||
return fallback_split, fallback_info
|
||||
|
||||
full_pages = extend_len // page_size
|
||||
tail_tokens = extend_len % page_size
|
||||
num_page_units = full_pages + (1 if tail_tokens > 0 else 0)
|
||||
cp_segment_num = cp_size * 2
|
||||
if num_page_units < cp_segment_num:
|
||||
return fallback_split, fallback_info
|
||||
|
||||
base_units = num_page_units // cp_segment_num
|
||||
remainder_units = num_page_units % cp_segment_num
|
||||
unit_counts = [
|
||||
base_units + (1 if i < remainder_units else 0)
|
||||
for i in range(cp_segment_num)
|
||||
]
|
||||
|
||||
split_list: List[int] = []
|
||||
segment_page_starts: List[int] = []
|
||||
segment_page_ends: List[int] = []
|
||||
unit_cursor = 0
|
||||
base_page = extend_prefix_len // page_size
|
||||
|
||||
for unit_count in unit_counts:
|
||||
segment_page_starts.append(base_page + unit_cursor)
|
||||
token_count = 0
|
||||
for _ in range(unit_count):
|
||||
if unit_cursor < full_pages:
|
||||
token_count += page_size
|
||||
else:
|
||||
token_count += tail_tokens
|
||||
unit_cursor += 1
|
||||
segment_page_ends.append(base_page + unit_cursor)
|
||||
split_list.append(token_count)
|
||||
|
||||
padding_tokens = total_len - extend_len
|
||||
if padding_tokens > 0:
|
||||
split_list[-1] += padding_tokens
|
||||
|
||||
return split_list, PageAlignedInSeqSplitInfo(
|
||||
page_aligned=True,
|
||||
page_size=page_size,
|
||||
extend_prefix_len=extend_prefix_len,
|
||||
segment_page_starts=segment_page_starts,
|
||||
segment_page_ends=segment_page_ends,
|
||||
)
|
||||
|
||||
|
||||
def _build_in_seq_split_for_forward_batch(
|
||||
*,
|
||||
total_len: int,
|
||||
cp_size: int,
|
||||
forward_batch: "ForwardBatch" = None,
|
||||
page_size: int = None,
|
||||
) -> Tuple[List[int], PageAlignedInSeqSplitInfo]:
|
||||
use_page_aligned_split = (
|
||||
forward_batch is not None
|
||||
and getattr(forward_batch, "uses_cp_shared_kv", False)
|
||||
and page_size is not None
|
||||
and getattr(forward_batch, "extend_seq_lens_cpu", None) is not None
|
||||
and getattr(forward_batch, "extend_prefix_lens_cpu", None) is not None
|
||||
and len(forward_batch.extend_seq_lens_cpu) == 1
|
||||
and len(forward_batch.extend_prefix_lens_cpu) == 1
|
||||
)
|
||||
if use_page_aligned_split:
|
||||
return build_page_aligned_in_seq_split_list(
|
||||
total_len=total_len,
|
||||
extend_len=int(forward_batch.extend_seq_lens_cpu[0]),
|
||||
extend_prefix_len=int(forward_batch.extend_prefix_lens_cpu[0]),
|
||||
page_size=int(page_size),
|
||||
cp_size=cp_size,
|
||||
)
|
||||
|
||||
split_list = build_token_balanced_in_seq_split_list(total_len, cp_size)
|
||||
return split_list, _fallback_page_aligned_split_info(
|
||||
page_size=int(page_size or 1),
|
||||
extend_prefix_len=0,
|
||||
)
|
||||
|
||||
|
||||
def can_cp_split(seq_len: int, cp_size: int, use_nsa: bool, forward_batch):
|
||||
@@ -214,6 +363,34 @@ def cp_split_and_rebuild_position(forward_batch, positions: torch.Tensor):
|
||||
return positions
|
||||
|
||||
|
||||
def split_in_seq_cp_local_pair(
|
||||
input_: torch.Tensor,
|
||||
prev_len: int,
|
||||
next_len: int,
|
||||
*,
|
||||
name: str = "input",
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Split a local in-seq CP tensor by its two logical segment lengths.
|
||||
|
||||
In-seq CP assigns each rank two logical segments: `rank` and
|
||||
`2 * cp_size - rank - 1`. The legacy token-balanced split made these
|
||||
segments almost equal, so splitting the local tensor in half happened to
|
||||
work. Page-aligned split can intentionally make the two segment lengths
|
||||
different, so consumers must split by metadata lengths instead of by half.
|
||||
"""
|
||||
|
||||
prev_len = int(prev_len)
|
||||
next_len = int(next_len)
|
||||
expected = prev_len + next_len
|
||||
actual = int(input_.shape[0])
|
||||
if actual != expected:
|
||||
raise RuntimeError(
|
||||
f"{name} local in-seq CP length mismatch: actual={actual}, "
|
||||
f"expected={expected}, prev_len={prev_len}, next_len={next_len}"
|
||||
)
|
||||
return torch.split(input_, (prev_len, next_len), dim=0)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def nsa_cp_round_robin_split_q_seqs_kernel(
|
||||
in_seqs_ptr,
|
||||
@@ -456,6 +633,9 @@ def prepare_input_dp_with_cp_dsa(
|
||||
cp_rank,
|
||||
cp_size,
|
||||
seqs_len,
|
||||
*,
|
||||
forward_batch: "ForwardBatch" = None,
|
||||
page_size: int = None,
|
||||
):
|
||||
if is_nsa_prefill_cp_round_robin_split():
|
||||
return True
|
||||
@@ -497,19 +677,17 @@ def prepare_input_dp_with_cp_dsa(
|
||||
- To mitigate uneven load, the input hissenstate needs to be sliced by cp_size*2 and rearranged.
|
||||
"""
|
||||
# just support batch = 1
|
||||
kv_len = torch.tensor(kv_len)
|
||||
kv_len_int = int(kv_len)
|
||||
kv_len = torch.tensor(kv_len_int)
|
||||
bs_per_cp_group = 1
|
||||
kv_len_origin = kv_len
|
||||
# get zigzag index
|
||||
cp_segment_num = cp_size * 2
|
||||
seq_per_batch = kv_len // cp_segment_num # seq_len for each batch and segment
|
||||
split_list = seq_per_batch.repeat_interleave(cp_segment_num).int().tolist()
|
||||
remainder = kv_len % (cp_segment_num)
|
||||
if remainder > 0:
|
||||
split_list[:remainder] = [x + 1 for x in split_list[:remainder]]
|
||||
|
||||
seq_max_rank_len = (kv_len + cp_size - 1) // cp_size
|
||||
max_rank_len = seq_max_rank_len.repeat_interleave(cp_size).int().tolist()
|
||||
split_list, page_split_info = _build_in_seq_split_for_forward_batch(
|
||||
total_len=kv_len_int,
|
||||
cp_size=cp_size,
|
||||
forward_batch=forward_batch,
|
||||
page_size=page_size,
|
||||
)
|
||||
zigzag_index = list(
|
||||
range(cp_rank, cp_rank + bs_per_cp_group * cp_segment_num, cp_segment_num)
|
||||
) + list(
|
||||
@@ -523,6 +701,9 @@ def prepare_input_dp_with_cp_dsa(
|
||||
per_rank_actual_token = list(
|
||||
split_list[i] + split_list[cp_size * 2 - i - 1] for i in range(cp_size)
|
||||
)
|
||||
max_rank_token = max(per_rank_actual_token) if per_rank_actual_token else 0
|
||||
max_rank_len = [max_rank_token for _ in range(cp_size)]
|
||||
comm_total_seq_lens = torch.tensor(max_rank_token * cp_size)
|
||||
reverse_split_len = [
|
||||
element
|
||||
for i in range(cp_size)
|
||||
@@ -573,7 +754,12 @@ def prepare_input_dp_with_cp_dsa(
|
||||
kv_len_next_tensor=kv_len_next_tensor,
|
||||
actual_seq_q_prev_tensor=actual_seq_q_prev_tensor,
|
||||
actual_seq_q_next_tensor=actual_seq_q_next_tensor,
|
||||
total_seq_lens=kv_len_origin,
|
||||
total_seq_lens=comm_total_seq_lens,
|
||||
page_aligned=page_split_info.page_aligned,
|
||||
page_size=page_split_info.page_size,
|
||||
extend_prefix_len=page_split_info.extend_prefix_len,
|
||||
segment_page_starts=page_split_info.segment_page_starts,
|
||||
segment_page_ends=page_split_info.segment_page_ends,
|
||||
)
|
||||
return nsa_cp_metadata
|
||||
|
||||
|
||||
@@ -252,6 +252,12 @@ class DeepseekV3ForCausalLMNextN(DeepseekV3ForCausalLM):
|
||||
self.cp_rank,
|
||||
self.cp_size,
|
||||
forward_batch.seq_lens_cpu.tolist(),
|
||||
forward_batch=forward_batch,
|
||||
page_size=getattr(
|
||||
getattr(forward_batch, "token_to_kv_pool", None),
|
||||
"page_size",
|
||||
None,
|
||||
),
|
||||
)
|
||||
hidden_states = self.model(input_ids, positions, forward_batch)
|
||||
return self.logits_processor(
|
||||
|
||||
@@ -2204,6 +2204,12 @@ class DeepseekV2ForCausalLM(nn.Module, DeepseekV2WeightLoaderMixin):
|
||||
self.cp_rank,
|
||||
self.cp_size,
|
||||
forward_batch.seq_lens_cpu.tolist(),
|
||||
forward_batch=forward_batch,
|
||||
page_size=getattr(
|
||||
getattr(forward_batch, "token_to_kv_pool", None),
|
||||
"page_size",
|
||||
None,
|
||||
),
|
||||
)
|
||||
|
||||
with get_attn_tp_context().maybe_input_scattered(forward_batch):
|
||||
|
||||
Reference in New Issue
Block a user