CP shared-KV now uses CP-local current rows consistently across MLA/index current reuse, passes fp8 current-index K through the tai-kernel uint8 ABI, and clears the transient EAGLE CP-local hidden marker after draft capture. The disaggregation bootstrap also fingerprints the runtime source contract so prefill/decode mismatches fail fast instead of silently exchanging incompatible KV metadata. Constraint: CP shared-KV batch paths flatten current K/V rows in CP-rank-local valid order, not global request order. Constraint: tai-kernel current-index prepare validates current_index_k as uint8 bytes for fp8 payloads. Rejected: Keep using global extend offsets for bs>1 current-index reuse | corrupts request-local bases once current_index_kv is CP-local. Rejected: Infer CP-local EAGLE hidden semantics from tensor shape | static padding and bs>1 can make shape-based inference unsafe. Confidence: medium Scope-risk: moderate Directive: Do not reintroduce forward_batch.out_cache_loc slicing in CP shared-KV current reuse without verifying CP-local owner-lane layout. Tested: Remote container py_compile for touched runtime/test files. Tested: Remote PYTHONPATH=python pytest -q test/registered/unit/layers/test_nsa_cp_utils.py test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py test/registered/unit/disaggregation/test_common_conn_runtime_fingerprint.py (198 passed, 2 subtests passed). Not-tested: Full remote ETE traffic after this commit; accept length and garbage-output recovery still require a fresh prefill/decode run. Co-authored-by: OmX <omx@oh-my-codex.dev>
2798 lines
107 KiB
Python
2798 lines
107 KiB
Python
# temp NSA debugging environ
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from itertools import accumulate
|
|
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
|
|
|
import torch
|
|
import torch.nn.functional as F
|
|
import triton
|
|
import triton.language as tl
|
|
|
|
from sglang.srt.environ import envs
|
|
from sglang.srt.distributed.device_communicators.pynccl_allocator import (
|
|
use_symmetric_memory,
|
|
)
|
|
from sglang.srt.layers.dp_attention import (
|
|
DpPaddingMode,
|
|
attn_cp_all_gather_into_tensor,
|
|
get_attention_cp_group,
|
|
get_attention_cp_rank,
|
|
get_attention_cp_size,
|
|
get_attention_dp_rank,
|
|
is_allocation_symmetric,
|
|
)
|
|
from sglang.srt.server_args import get_global_server_args
|
|
from sglang.srt.utils.common import ceil_align, ceil_div
|
|
|
|
if TYPE_CHECKING:
|
|
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_CP_DRAFT_SHARED_KV_DEBUG_COUNTS = {}
|
|
|
|
|
|
def log_cp_draft_shared_kv_debug(
|
|
key: str,
|
|
message: str,
|
|
*args,
|
|
limit: int = 128,
|
|
) -> None:
|
|
if not envs.SGLANG_CP_DRAFT_SHARED_KV_DEBUG.get():
|
|
return
|
|
count = _CP_DRAFT_SHARED_KV_DEBUG_COUNTS.get(key, 0)
|
|
if count >= limit:
|
|
return
|
|
_CP_DRAFT_SHARED_KV_DEBUG_COUNTS[key] = count + 1
|
|
logger.info("[CP_DRAFT_SHARED_KV] " + message, *args)
|
|
|
|
|
|
def log_cp_shared_kv_direct_write_fallback(
|
|
reason: str,
|
|
message: str,
|
|
*args,
|
|
) -> None:
|
|
"""Log every direct-write fallback event.
|
|
|
|
Warmup can hit the same fallback reason as a later real request, so
|
|
de-duplicating by reason hides correctness/performance issues after startup.
|
|
"""
|
|
logger.warning(
|
|
"[CP_SHARED_KV_FALLBACK][direct_write] reason=%s " + message,
|
|
reason,
|
|
*args,
|
|
)
|
|
|
|
|
|
def raise_cp_shared_kv_direct_write_error(
|
|
reason: str,
|
|
message: str,
|
|
*args,
|
|
) -> None:
|
|
"""Fail fast when CP shared-KV direct-write invariants are violated."""
|
|
|
|
formatted = message % args if args else message
|
|
error_msg = (
|
|
f"[CP_SHARED_KV_FAIL_FAST][direct_write] reason={reason} {formatted}"
|
|
)
|
|
logger.error(error_msg)
|
|
raise RuntimeError(error_msg)
|
|
|
|
|
|
def compute_nsa_seqlens(original_seq_lens, nsa_index_topk: int):
|
|
return original_seq_lens.clamp(max=nsa_index_topk)
|
|
|
|
|
|
def is_nsa_enable_prefill_cp():
|
|
return get_global_server_args().enable_nsa_prefill_context_parallel
|
|
|
|
|
|
def is_nsa_prefill_cp_in_seq_split():
|
|
return (
|
|
is_nsa_enable_prefill_cp()
|
|
and get_global_server_args().nsa_prefill_cp_mode == "in-seq-split"
|
|
)
|
|
|
|
|
|
def is_nsa_prefill_cp_round_robin_split():
|
|
return (
|
|
is_nsa_enable_prefill_cp()
|
|
and get_global_server_args().nsa_prefill_cp_mode == "round-robin-split"
|
|
)
|
|
|
|
|
|
def _is_cp_shared_kv_forward_batch(forward_batch: "ForwardBatch") -> bool:
|
|
return bool(getattr(forward_batch, "uses_cp_shared_kv", False))
|
|
|
|
|
|
def _is_cp_shared_kv_draft_extend(forward_batch: "ForwardBatch") -> bool:
|
|
"""Return whether EAGLE/NextN draft extend should keep CP-local semantics."""
|
|
|
|
if not _is_cp_shared_kv_forward_batch(forward_batch):
|
|
return False
|
|
if not envs.SGLANG_CP_DRAFT_SHARED_KV.get():
|
|
return False
|
|
forward_mode = getattr(forward_batch, "forward_mode", None)
|
|
is_draft_extend = getattr(forward_mode, "is_draft_extend", None)
|
|
if not callable(is_draft_extend):
|
|
return False
|
|
try:
|
|
return bool(is_draft_extend(include_v2=True))
|
|
except TypeError:
|
|
return bool(is_draft_extend())
|
|
|
|
|
|
def _fail_if_cp_shared_kv_round_robin(
|
|
forward_batch: "ForwardBatch",
|
|
*,
|
|
op: str,
|
|
) -> None:
|
|
if forward_batch is None or not _is_cp_shared_kv_forward_batch(forward_batch):
|
|
return
|
|
try:
|
|
round_robin_split = is_nsa_prefill_cp_round_robin_split()
|
|
except ValueError:
|
|
round_robin_split = False
|
|
if not round_robin_split:
|
|
return
|
|
|
|
error_msg = (
|
|
"[CP_SHARED_KV_FAIL_FAST][round_robin_unsupported] "
|
|
"CP shared KV only supports in-seq zigzag split. "
|
|
f"op={op} nsa_prefill_cp_mode=round-robin-split"
|
|
)
|
|
logger.error(error_msg)
|
|
raise RuntimeError(error_msg)
|
|
|
|
|
|
def can_nsa_prefill_cp_round_robin_split(forward_batch: "ForwardBatch"):
|
|
_fail_if_cp_shared_kv_round_robin(
|
|
forward_batch,
|
|
op="can_nsa_prefill_cp_round_robin_split",
|
|
)
|
|
if not forward_batch.forward_mode.is_context_parallel_extend():
|
|
return False
|
|
cp_size = get_attention_cp_size()
|
|
seq_len = sum(forward_batch.extend_seq_lens_cpu)
|
|
return (
|
|
is_nsa_prefill_cp_round_robin_split()
|
|
and seq_len > 0
|
|
and seq_len >= cp_size
|
|
and cp_size > 1
|
|
)
|
|
|
|
|
|
def nsa_cp_round_robin_split_data(input_: Union[torch.Tensor, List]):
|
|
"""
|
|
# for round-robin-split, split the tokens evenly according to the rule of token_idx % cp_size.
|
|
| +-----------before split------------+|
|
|
| token0, token1, token2, token3, token4, token5, token6, token7, ...
|
|
|
|
|
| +--------------result-------------------+
|
|
| dp_atten_tp0: token0, token4, token8, token12, token16, ... |
|
|
| dp_atten_tp1: token1, token5, token9, token13, token17, ... |
|
|
| dp_atten_tp2: token2, token6, token10, token14, token18, ... |
|
|
| dp_atten_tp3: token3, token7, token11, token15, token19, ... |
|
|
| +-------------------------+
|
|
"""
|
|
cp_size = get_attention_cp_size()
|
|
cp_rank = get_attention_cp_rank()
|
|
if isinstance(input_, (tuple, list)):
|
|
indices = range(cp_rank, len(input_), cp_size)
|
|
return input_[indices]
|
|
|
|
tokens = len(input_)
|
|
if tokens % cp_size != 0:
|
|
cur_len = tokens // cp_size + (tokens % cp_size > cp_rank)
|
|
if cur_len == 0:
|
|
return input_.new_empty(0, *input_.shape[1:])
|
|
indices = torch.arange(cp_rank, tokens, cp_size, device=input_.device)
|
|
return input_[indices]
|
|
|
|
# for torch device tensor
|
|
return input_.view(-1, cp_size, *input_.shape[1:])[:, cp_rank].contiguous()
|
|
|
|
|
|
def cal_padded_tokens(forward_batch: "ForwardBatch"):
|
|
# Consistent with the padding calculation logic in ForwardBatch.prepare_mlp_sync_batch,
|
|
# calculate the actual token length after padding when attn_tp_size > 1 or in the MAX_LEN padding mode.
|
|
global_num_tokens = forward_batch.global_num_tokens_cpu.copy()
|
|
sync_group_size = len(global_num_tokens)
|
|
attn_cp_size = get_attention_cp_size()
|
|
for i in range(sync_group_size):
|
|
global_num_tokens[i] = ceil_align(global_num_tokens[i], attn_cp_size)
|
|
dp_padding_mode = DpPaddingMode.get_dp_padding_mode(
|
|
forward_batch.is_extend_in_batch, global_num_tokens
|
|
)
|
|
if dp_padding_mode.is_max_len():
|
|
tokens = max(global_num_tokens)
|
|
elif len(global_num_tokens) > 1:
|
|
tokens = global_num_tokens[get_attention_dp_rank()]
|
|
else:
|
|
tokens = global_num_tokens[0]
|
|
if can_nsa_prefill_cp_round_robin_split(forward_batch):
|
|
tokens = ceil_div(tokens, attn_cp_size)
|
|
return tokens
|
|
|
|
|
|
def pad_nsa_cache_seqlens(forward_batch: "ForwardBatch", nsa_cache_seqlens):
|
|
attn_cp_size = get_attention_cp_size()
|
|
needs_cp_pad = attn_cp_size > 1 and can_nsa_prefill_cp_round_robin_split(
|
|
forward_batch
|
|
)
|
|
needs_dp_pad = forward_batch.global_num_tokens_cpu is not None
|
|
if not needs_cp_pad and not needs_dp_pad:
|
|
return nsa_cache_seqlens
|
|
tokens = cal_padded_tokens(forward_batch)
|
|
pad_len = tokens - nsa_cache_seqlens.shape[0]
|
|
if pad_len > 0:
|
|
nsa_cache_seqlens = torch.cat(
|
|
[
|
|
nsa_cache_seqlens,
|
|
nsa_cache_seqlens.new_zeros(pad_len, *nsa_cache_seqlens.shape[1:]),
|
|
]
|
|
)
|
|
return nsa_cache_seqlens
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PageAlignedCacheExtent:
|
|
valid_tokens: int
|
|
padded_pages: int
|
|
padded_tokens: int
|
|
padding_tokens: int
|
|
|
|
|
|
def build_page_aligned_cache_extent(
|
|
*, valid_tokens: int, page_size: int
|
|
) -> PageAlignedCacheExtent:
|
|
if valid_tokens < 0:
|
|
raise ValueError(f"valid_tokens must be non-negative, got {valid_tokens}")
|
|
if page_size <= 0:
|
|
raise ValueError(f"page_size must be positive, got {page_size}")
|
|
|
|
padded_pages = ceil_div(valid_tokens, page_size) if valid_tokens > 0 else 0
|
|
padded_tokens = padded_pages * page_size
|
|
return PageAlignedCacheExtent(
|
|
valid_tokens=valid_tokens,
|
|
padded_pages=padded_pages,
|
|
padded_tokens=padded_tokens,
|
|
padding_tokens=padded_tokens - valid_tokens,
|
|
)
|
|
|
|
|
|
@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
|
|
extend_valid_tokens: int = 0
|
|
extend_padded_pages: int = 0
|
|
extend_padded_tokens: int = 0
|
|
extend_padding_tokens: int = 0
|
|
|
|
|
|
@dataclass
|
|
class NSAContextParallelMetadata:
|
|
split_list: List[int] = None
|
|
split_list_tensor: torch.Tensor = None
|
|
split_prefix_tensor: torch.Tensor = None
|
|
max_rank_len: List[int] = None
|
|
zigzag_index: List[int] = None
|
|
per_rank_actual_token: List[int] = None
|
|
reverse_split_len: List[int] = None
|
|
cp_reverse_index: List[int] = None
|
|
kv_len_prev: int = -1
|
|
kv_len_next: int = -1
|
|
actual_seq_q_prev: int = -1
|
|
actual_seq_q_next: int = -1
|
|
kv_len_prev_tensor: torch.Tensor = None
|
|
kv_len_next_tensor: torch.Tensor = None
|
|
actual_seq_q_prev_tensor: torch.Tensor = None
|
|
actual_seq_q_next_tensor: torch.Tensor = None
|
|
actual_seq_q_prev_cu_tensor: torch.Tensor = None
|
|
actual_seq_q_next_cu_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
|
|
extend_valid_tokens: int = 0
|
|
extend_padded_pages: int = 0
|
|
extend_padded_tokens: int = 0
|
|
extend_padding_tokens: int = 0
|
|
batch_size: int = 1
|
|
request_token_offsets: List[int] = None
|
|
request_padded_token_offsets: List[int] = None
|
|
request_page_offsets: List[int] = None
|
|
request_extend_lens: List[int] = None
|
|
request_prefix_lens: List[int] = None
|
|
request_padded_pages: List[int] = None
|
|
request_padded_tokens: List[int] = None
|
|
request_padding_tokens: List[int] = None
|
|
request_split_lists: List[List[int]] = None
|
|
request_zigzag_indices: List[List[int]] = None
|
|
request_segment_page_starts: List[List[int]] = None
|
|
request_segment_page_ends: List[List[int]] = None
|
|
request_rank_local_tokens: List[int] = None
|
|
request_rank_local_offsets: List[int] = None
|
|
request_kv_len_prev: List[int] = None
|
|
request_kv_len_next: List[int] = None
|
|
request_actual_seq_q_prev: List[int] = None
|
|
request_actual_seq_q_next: List[int] = None
|
|
request_kv_len_prev_tensor: torch.Tensor = None
|
|
request_kv_len_next_tensor: torch.Tensor = None
|
|
request_actual_seq_q_prev_tensor: torch.Tensor = None
|
|
request_actual_seq_q_next_tensor: torch.Tensor = None
|
|
request_actual_seq_q_prev_cu_tensor: torch.Tensor = None
|
|
request_actual_seq_q_next_cu_tensor: torch.Tensor = None
|
|
request_last_token_owner: List[int] = None
|
|
request_last_token_local_offset: List[int] = None
|
|
output_collect_mode: str = None
|
|
flat_split_list: List[int] = None
|
|
flat_zigzag_index: List[int] = None
|
|
flat_segment_request_ids: List[int] = None
|
|
flat_segment_offsets: List[int] = None
|
|
compute_padding_enabled: bool = False
|
|
request_valid_split_lists: List[List[int]] = None
|
|
request_valid_segment_page_starts: List[List[int]] = None
|
|
request_valid_segment_page_ends: List[List[int]] = None
|
|
request_valid_padded_pages: List[int] = None
|
|
request_valid_padded_tokens: List[int] = None
|
|
request_valid_padding_tokens: List[int] = None
|
|
request_valid_rank_local_tokens: List[int] = None
|
|
request_valid_rank_local_offsets: List[int] = None
|
|
request_valid_actual_seq_q_prev: List[int] = None
|
|
request_valid_actual_seq_q_next: List[int] = None
|
|
request_valid_seq_q_prev: List[int] = None
|
|
request_valid_seq_q_next: List[int] = None
|
|
request_valid_query_row_spans: List[List[Tuple[int, int]]] = None
|
|
request_compute_split_lists: List[List[int]] = None
|
|
request_compute_segment_page_starts: List[List[int]] = None
|
|
request_compute_segment_page_ends: List[List[int]] = None
|
|
request_compute_padded_pages: List[int] = None
|
|
request_compute_padded_tokens: List[int] = None
|
|
request_compute_padding_tokens: List[int] = None
|
|
request_compute_padded_token_offsets: List[int] = None
|
|
request_compute_rank_local_tokens: List[int] = None
|
|
request_compute_rank_local_offsets: List[int] = None
|
|
request_compute_actual_seq_q_prev: List[int] = None
|
|
request_compute_actual_seq_q_next: List[int] = None
|
|
request_compute_seq_q_prev: List[int] = None
|
|
request_compute_seq_q_next: List[int] = None
|
|
batch_plan: object = None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CPSharedKVBatchPlan:
|
|
batch_size: int
|
|
page_size: int
|
|
cp_size: int
|
|
cp_rank: int
|
|
request_token_offsets: List[int]
|
|
request_padded_token_offsets: List[int]
|
|
request_page_offsets: List[int]
|
|
request_extend_lens: List[int]
|
|
request_prefix_lens: List[int]
|
|
request_padded_pages: List[int]
|
|
request_padded_tokens: List[int]
|
|
request_padding_tokens: List[int]
|
|
request_split_infos: List[PageAlignedInSeqSplitInfo]
|
|
request_split_lists: List[List[int]]
|
|
request_zigzag_indices: List[List[int]]
|
|
request_segment_page_starts: List[List[int]]
|
|
request_segment_page_ends: List[List[int]]
|
|
request_rank_local_tokens: List[int]
|
|
request_rank_local_offsets: List[int]
|
|
request_kv_len_prev: List[int]
|
|
request_kv_len_next: List[int]
|
|
request_actual_seq_q_prev: List[int]
|
|
request_actual_seq_q_next: List[int]
|
|
request_last_token_owner: List[int]
|
|
request_last_token_local_offset: List[int]
|
|
output_collect_mode: str
|
|
flat_split_list: List[int]
|
|
flat_zigzag_index: List[int]
|
|
flat_segment_request_ids: List[int]
|
|
flat_segment_offsets: List[int]
|
|
compute_padding_enabled: bool
|
|
request_valid_split_lists: List[List[int]]
|
|
request_valid_segment_page_starts: List[List[int]]
|
|
request_valid_segment_page_ends: List[List[int]]
|
|
request_valid_padded_pages: List[int]
|
|
request_valid_padded_tokens: List[int]
|
|
request_valid_padding_tokens: List[int]
|
|
request_valid_rank_local_tokens: List[int]
|
|
request_valid_rank_local_offsets: List[int]
|
|
request_valid_actual_seq_q_prev: List[int]
|
|
request_valid_actual_seq_q_next: List[int]
|
|
request_valid_seq_q_prev: List[int]
|
|
request_valid_seq_q_next: List[int]
|
|
request_valid_query_row_spans: List[List[Tuple[int, int]]]
|
|
request_compute_split_lists: List[List[int]]
|
|
request_compute_segment_page_starts: List[List[int]]
|
|
request_compute_segment_page_ends: List[List[int]]
|
|
request_compute_padded_pages: List[int]
|
|
request_compute_padded_tokens: List[int]
|
|
request_compute_padding_tokens: List[int]
|
|
request_compute_padded_token_offsets: List[int]
|
|
request_compute_rank_local_tokens: List[int]
|
|
request_compute_rank_local_offsets: List[int]
|
|
request_compute_actual_seq_q_prev: List[int]
|
|
request_compute_actual_seq_q_next: List[int]
|
|
request_compute_seq_q_prev: List[int]
|
|
request_compute_seq_q_next: List[int]
|
|
|
|
|
|
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 _prefix_offsets(lengths: List[int]) -> List[int]:
|
|
offsets: List[int] = []
|
|
cursor = 0
|
|
for length in lengths:
|
|
offsets.append(cursor)
|
|
cursor += int(length)
|
|
return offsets
|
|
|
|
|
|
def _build_full_page_unit_split(
|
|
*,
|
|
page_units: int,
|
|
extend_prefix_len: int,
|
|
page_size: int,
|
|
cp_size: int,
|
|
) -> Tuple[List[int], List[int], List[int]]:
|
|
"""Split full physical pages across in-seq CP segments.
|
|
|
|
This is the compute-side counterpart of
|
|
`build_page_aligned_in_seq_split_list`: every assigned page contributes a
|
|
full `page_size` rows because dummy compute rows fill any valid-token tail.
|
|
"""
|
|
|
|
if page_units < 0:
|
|
raise ValueError(f"page_units must be non-negative, got {page_units}")
|
|
if page_size <= 0:
|
|
raise ValueError(f"page_size must be positive, got {page_size}")
|
|
if cp_size <= 0:
|
|
raise ValueError(f"cp_size must be positive, got {cp_size}")
|
|
if extend_prefix_len < 0 or extend_prefix_len % page_size != 0:
|
|
raise ValueError(
|
|
"extend_prefix_len must be non-negative and page-aligned, "
|
|
f"got extend_prefix_len={extend_prefix_len} page_size={page_size}"
|
|
)
|
|
|
|
cp_segment_num = cp_size * 2
|
|
base_units = page_units // cp_segment_num
|
|
remainder_units = 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)
|
|
unit_cursor += unit_count
|
|
segment_page_ends.append(base_page + unit_cursor)
|
|
split_list.append(unit_count * page_size)
|
|
|
|
return split_list, segment_page_starts, segment_page_ends
|
|
|
|
|
|
def build_batch_page_aligned_in_seq_split_plan(
|
|
*,
|
|
extend_lens: List[int],
|
|
prefix_lens: List[int],
|
|
page_size: int,
|
|
cp_size: int,
|
|
cp_rank: int,
|
|
) -> CPSharedKVBatchPlan:
|
|
"""Build per-request page-aligned in-seq CP metadata for a real batch.
|
|
|
|
The contract is intentionally request-first: each request is split and
|
|
page-rounded independently, then flattened for runtime consumers. This
|
|
preserves phase1 narrow-output collection for bs>1 without treating the
|
|
batch as one long sequence.
|
|
"""
|
|
|
|
if len(extend_lens) != len(prefix_lens):
|
|
raise ValueError(
|
|
"extend_lens and prefix_lens must have the same length, "
|
|
f"got {len(extend_lens)} and {len(prefix_lens)}"
|
|
)
|
|
if cp_size <= 0:
|
|
raise ValueError(f"cp_size must be positive, got {cp_size}")
|
|
if cp_rank < 0 or cp_rank >= cp_size:
|
|
raise ValueError(f"cp_rank must be in [0, {cp_size}), got {cp_rank}")
|
|
if page_size <= 0:
|
|
raise ValueError(f"page_size must be positive, got {page_size}")
|
|
|
|
request_extend_lens = [int(x) for x in extend_lens]
|
|
request_prefix_lens = [int(x) for x in prefix_lens]
|
|
for req_id, (extend_len, prefix_len) in enumerate(
|
|
zip(request_extend_lens, request_prefix_lens)
|
|
):
|
|
if extend_len <= 0:
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][batch_gt1_empty_extend] "
|
|
f"CP shared-KV batch planning requires positive extend lens. "
|
|
f"req_id={req_id} extend_len={extend_len}"
|
|
)
|
|
if prefix_len < 0:
|
|
raise ValueError(f"prefix_len must be non-negative, got {prefix_len}")
|
|
if prefix_len % page_size != 0:
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][batch_gt1_non_page_aligned_prefix] "
|
|
"CP shared-KV batch planning requires page-aligned prefixes. "
|
|
"The radix/HiCache match path should floor cache hits to the "
|
|
"previous page boundary before planning. "
|
|
f"req_id={req_id} prefix_len={prefix_len} "
|
|
f"extend_len={extend_len} page_size={page_size}"
|
|
)
|
|
|
|
cp_segment_num = cp_size * 2
|
|
request_split_infos: List[PageAlignedInSeqSplitInfo] = []
|
|
request_split_lists: List[List[int]] = []
|
|
request_zigzag_indices: List[List[int]] = []
|
|
request_segment_page_starts: List[List[int]] = []
|
|
request_segment_page_ends: List[List[int]] = []
|
|
request_padded_pages: List[int] = []
|
|
request_padded_tokens: List[int] = []
|
|
request_padding_tokens: List[int] = []
|
|
request_rank_local_tokens: List[int] = []
|
|
request_kv_len_prev: List[int] = []
|
|
request_kv_len_next: List[int] = []
|
|
request_actual_seq_q_prev: List[int] = []
|
|
request_actual_seq_q_next: List[int] = []
|
|
request_last_token_owner: List[int] = []
|
|
request_last_token_local_offset: List[int] = []
|
|
request_valid_split_lists: List[List[int]] = []
|
|
request_valid_segment_page_starts: List[List[int]] = []
|
|
request_valid_segment_page_ends: List[List[int]] = []
|
|
request_valid_padded_pages: List[int] = []
|
|
request_valid_padded_tokens: List[int] = []
|
|
request_valid_padding_tokens: List[int] = []
|
|
request_valid_rank_local_tokens: List[int] = []
|
|
request_valid_actual_seq_q_prev: List[int] = []
|
|
request_valid_actual_seq_q_next: List[int] = []
|
|
request_valid_seq_q_prev: List[int] = []
|
|
request_valid_seq_q_next: List[int] = []
|
|
request_valid_query_row_spans: List[List[Tuple[int, int]]] = []
|
|
request_compute_split_lists: List[List[int]] = []
|
|
request_compute_segment_page_starts: List[List[int]] = []
|
|
request_compute_segment_page_ends: List[List[int]] = []
|
|
request_compute_padded_pages: List[int] = []
|
|
request_compute_padded_tokens: List[int] = []
|
|
request_compute_padding_tokens: List[int] = []
|
|
request_compute_rank_local_tokens: List[int] = []
|
|
request_compute_actual_seq_q_prev: List[int] = []
|
|
request_compute_actual_seq_q_next: List[int] = []
|
|
request_compute_seq_q_prev: List[int] = []
|
|
request_compute_seq_q_next: List[int] = []
|
|
flat_split_list: List[int] = []
|
|
flat_zigzag_index: List[int] = []
|
|
flat_segment_request_ids: List[int] = []
|
|
flat_segment_offsets: List[int] = []
|
|
|
|
for req_id, (extend_len, prefix_len) in enumerate(
|
|
zip(request_extend_lens, request_prefix_lens)
|
|
):
|
|
split_list, split_info = build_page_aligned_in_seq_split_list(
|
|
total_len=extend_len,
|
|
extend_len=extend_len,
|
|
extend_prefix_len=prefix_len,
|
|
page_size=page_size,
|
|
cp_size=cp_size,
|
|
)
|
|
if not split_info.page_aligned:
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][batch_gt1_page_split_fallback] "
|
|
"CP shared-KV batch planning must not use token-balanced "
|
|
f"fallback. req_id={req_id} prefix_len={prefix_len} "
|
|
f"extend_len={extend_len} page_size={page_size}"
|
|
)
|
|
|
|
compute_pages = max(split_info.extend_padded_pages, cp_size)
|
|
compute_split_list, compute_page_starts, compute_page_ends = (
|
|
_build_full_page_unit_split(
|
|
page_units=compute_pages,
|
|
extend_prefix_len=prefix_len,
|
|
page_size=page_size,
|
|
cp_size=cp_size,
|
|
)
|
|
)
|
|
owner, local_offset = _get_in_seq_last_token_owner_and_offset(
|
|
split_list=compute_split_list,
|
|
cp_size=cp_size,
|
|
actual_token_count=extend_len,
|
|
)
|
|
zigzag_index = [cp_rank, cp_segment_num - cp_rank - 1]
|
|
prefix_sum_list = list(accumulate(split_list))
|
|
mirror_idx = cp_segment_num - cp_rank - 1
|
|
rank_local_tokens = (
|
|
split_list[cp_rank] + split_list[mirror_idx]
|
|
)
|
|
compute_rank_local_tokens = (
|
|
compute_split_list[cp_rank] + compute_split_list[mirror_idx]
|
|
)
|
|
split_prefix_list = [0] + prefix_sum_list[:-1]
|
|
|
|
request_split_infos.append(split_info)
|
|
request_split_lists.append(split_list)
|
|
request_zigzag_indices.append(zigzag_index)
|
|
request_segment_page_starts.append(split_info.segment_page_starts)
|
|
request_segment_page_ends.append(split_info.segment_page_ends)
|
|
request_padded_pages.append(split_info.extend_padded_pages)
|
|
request_padded_tokens.append(split_info.extend_padded_tokens)
|
|
request_padding_tokens.append(split_info.extend_padding_tokens)
|
|
request_rank_local_tokens.append(rank_local_tokens)
|
|
request_kv_len_prev.append(prefix_sum_list[cp_rank])
|
|
request_kv_len_next.append(prefix_sum_list[mirror_idx])
|
|
request_actual_seq_q_prev.append(compute_split_list[cp_rank])
|
|
request_actual_seq_q_next.append(compute_split_list[mirror_idx])
|
|
request_last_token_owner.append(owner)
|
|
request_last_token_local_offset.append(local_offset)
|
|
request_valid_split_lists.append(split_list)
|
|
request_valid_segment_page_starts.append(split_info.segment_page_starts)
|
|
request_valid_segment_page_ends.append(split_info.segment_page_ends)
|
|
request_valid_padded_pages.append(split_info.extend_padded_pages)
|
|
request_valid_padded_tokens.append(split_info.extend_padded_tokens)
|
|
request_valid_padding_tokens.append(split_info.extend_padding_tokens)
|
|
request_valid_rank_local_tokens.append(rank_local_tokens)
|
|
request_valid_actual_seq_q_prev.append(split_list[cp_rank])
|
|
request_valid_actual_seq_q_next.append(split_list[mirror_idx])
|
|
request_valid_seq_q_prev.append(split_list[cp_rank])
|
|
request_valid_seq_q_next.append(split_list[mirror_idx])
|
|
request_valid_query_row_spans.append(
|
|
[
|
|
(0, split_list[cp_rank]),
|
|
(compute_split_list[cp_rank], split_list[mirror_idx]),
|
|
]
|
|
)
|
|
request_compute_split_lists.append(compute_split_list)
|
|
request_compute_segment_page_starts.append(compute_page_starts)
|
|
request_compute_segment_page_ends.append(compute_page_ends)
|
|
request_compute_padded_pages.append(compute_pages)
|
|
request_compute_padded_tokens.append(compute_pages * page_size)
|
|
request_compute_padding_tokens.append(compute_pages * page_size - extend_len)
|
|
request_compute_rank_local_tokens.append(compute_rank_local_tokens)
|
|
request_compute_actual_seq_q_prev.append(compute_split_list[cp_rank])
|
|
request_compute_actual_seq_q_next.append(compute_split_list[mirror_idx])
|
|
request_compute_seq_q_prev.append(compute_split_list[cp_rank])
|
|
request_compute_seq_q_next.append(compute_split_list[mirror_idx])
|
|
flat_split_list.extend(split_list)
|
|
segment_base = req_id * cp_segment_num
|
|
flat_zigzag_index.extend(segment_base + idx for idx in zigzag_index)
|
|
flat_segment_request_ids.extend([req_id] * cp_segment_num)
|
|
flat_segment_offsets.extend(split_prefix_list)
|
|
|
|
return CPSharedKVBatchPlan(
|
|
batch_size=len(request_extend_lens),
|
|
page_size=page_size,
|
|
cp_size=cp_size,
|
|
cp_rank=cp_rank,
|
|
request_token_offsets=_prefix_offsets(request_extend_lens),
|
|
request_padded_token_offsets=_prefix_offsets(request_padded_tokens),
|
|
request_page_offsets=_prefix_offsets(request_padded_pages),
|
|
request_extend_lens=request_extend_lens,
|
|
request_prefix_lens=request_prefix_lens,
|
|
request_padded_pages=request_padded_pages,
|
|
request_padded_tokens=request_padded_tokens,
|
|
request_padding_tokens=request_padding_tokens,
|
|
request_split_infos=request_split_infos,
|
|
request_split_lists=request_split_lists,
|
|
request_zigzag_indices=request_zigzag_indices,
|
|
request_segment_page_starts=request_segment_page_starts,
|
|
request_segment_page_ends=request_segment_page_ends,
|
|
request_rank_local_tokens=request_rank_local_tokens,
|
|
request_rank_local_offsets=_prefix_offsets(request_rank_local_tokens),
|
|
request_kv_len_prev=request_kv_len_prev,
|
|
request_kv_len_next=request_kv_len_next,
|
|
request_actual_seq_q_prev=request_actual_seq_q_prev,
|
|
request_actual_seq_q_next=request_actual_seq_q_next,
|
|
request_last_token_owner=request_last_token_owner,
|
|
request_last_token_local_offset=request_last_token_local_offset,
|
|
output_collect_mode="narrow_last_token",
|
|
flat_split_list=flat_split_list,
|
|
flat_zigzag_index=flat_zigzag_index,
|
|
flat_segment_request_ids=flat_segment_request_ids,
|
|
flat_segment_offsets=flat_segment_offsets,
|
|
compute_padding_enabled=any(
|
|
compute_tokens != valid_tokens
|
|
for compute_tokens, valid_tokens in zip(
|
|
request_compute_padded_tokens, request_valid_padded_tokens
|
|
)
|
|
),
|
|
request_valid_split_lists=request_valid_split_lists,
|
|
request_valid_segment_page_starts=request_valid_segment_page_starts,
|
|
request_valid_segment_page_ends=request_valid_segment_page_ends,
|
|
request_valid_padded_pages=request_valid_padded_pages,
|
|
request_valid_padded_tokens=request_valid_padded_tokens,
|
|
request_valid_padding_tokens=request_valid_padding_tokens,
|
|
request_valid_rank_local_tokens=request_valid_rank_local_tokens,
|
|
request_valid_rank_local_offsets=_prefix_offsets(request_valid_rank_local_tokens),
|
|
request_valid_actual_seq_q_prev=request_valid_actual_seq_q_prev,
|
|
request_valid_actual_seq_q_next=request_valid_actual_seq_q_next,
|
|
request_valid_seq_q_prev=request_valid_seq_q_prev,
|
|
request_valid_seq_q_next=request_valid_seq_q_next,
|
|
request_valid_query_row_spans=request_valid_query_row_spans,
|
|
request_compute_split_lists=request_compute_split_lists,
|
|
request_compute_segment_page_starts=request_compute_segment_page_starts,
|
|
request_compute_segment_page_ends=request_compute_segment_page_ends,
|
|
request_compute_padded_pages=request_compute_padded_pages,
|
|
request_compute_padded_tokens=request_compute_padded_tokens,
|
|
request_compute_padding_tokens=request_compute_padding_tokens,
|
|
request_compute_padded_token_offsets=_prefix_offsets(
|
|
request_compute_padded_tokens
|
|
),
|
|
request_compute_rank_local_tokens=request_compute_rank_local_tokens,
|
|
request_compute_rank_local_offsets=_prefix_offsets(
|
|
request_compute_rank_local_tokens
|
|
),
|
|
request_compute_actual_seq_q_prev=request_compute_actual_seq_q_prev,
|
|
request_compute_actual_seq_q_next=request_compute_actual_seq_q_next,
|
|
request_compute_seq_q_prev=request_compute_seq_q_prev,
|
|
request_compute_seq_q_next=request_compute_seq_q_next,
|
|
)
|
|
|
|
|
|
def get_cp_shared_kv_batch_plan(forward_batch: "ForwardBatch"):
|
|
metadata = getattr(forward_batch, "nsa_cp_metadata", None)
|
|
if metadata is None:
|
|
return None
|
|
plan = getattr(metadata, "batch_plan", None)
|
|
if plan is not None:
|
|
return plan
|
|
if isinstance(metadata, CPSharedKVBatchPlan):
|
|
return metadata
|
|
if getattr(metadata, "batch_size", 1) > 1:
|
|
return metadata
|
|
return None
|
|
|
|
|
|
def _get_forward_batch_static_padded_tokens(
|
|
forward_batch: "ForwardBatch",
|
|
) -> Optional[int]:
|
|
static_padded_tokens = getattr(forward_batch, "extend_num_tokens", None)
|
|
if static_padded_tokens is None:
|
|
return None
|
|
try:
|
|
return int(static_padded_tokens)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def split_tensor_by_cp_batch_plan(
|
|
tensor: torch.Tensor,
|
|
plan,
|
|
*,
|
|
mode: str = "data",
|
|
split_kind: str = "compute",
|
|
static_padded_tokens: Optional[int] = None,
|
|
) -> torch.Tensor:
|
|
"""Split a flattened batch tensor by per-request in-seq CP plan.
|
|
|
|
`mode` is kept explicit for future shape-specific kernels. The current
|
|
CPU/Python planner path splits along dim0 for 1d, position, and data views.
|
|
`split_kind="compute"` materializes padded compute rows. Cache writes must
|
|
use `split_kind="valid"` so dummy compute rows never receive cache locs.
|
|
"""
|
|
|
|
if mode not in ("1d", "data", "position"):
|
|
raise ValueError(f"unsupported CP batch split mode={mode!r}")
|
|
if split_kind not in ("compute", "valid"):
|
|
raise ValueError(f"unsupported CP batch split_kind={split_kind!r}")
|
|
|
|
request_extend_lens = getattr(plan, "request_extend_lens", None)
|
|
compute_padding_enabled = bool(getattr(plan, "compute_padding_enabled", False))
|
|
if split_kind == "valid":
|
|
request_split_lists = getattr(
|
|
plan, "request_valid_split_lists", None
|
|
) or getattr(plan, "request_split_lists", None)
|
|
request_target_lens = request_extend_lens
|
|
elif compute_padding_enabled:
|
|
request_split_lists = getattr(plan, "request_compute_split_lists", None)
|
|
request_target_lens = getattr(plan, "request_compute_padded_tokens", None)
|
|
else:
|
|
request_split_lists = getattr(plan, "request_split_lists", None)
|
|
request_target_lens = request_extend_lens
|
|
request_zigzag_indices = getattr(plan, "request_zigzag_indices", None)
|
|
batch_size = int(getattr(plan, "batch_size", 1) or 1)
|
|
if (
|
|
request_extend_lens is None
|
|
or request_target_lens is None
|
|
or request_split_lists is None
|
|
or request_zigzag_indices is None
|
|
or len(request_extend_lens) != batch_size
|
|
or len(request_target_lens) != batch_size
|
|
or len(request_split_lists) != batch_size
|
|
or len(request_zigzag_indices) != batch_size
|
|
):
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][batch_gt1_missing_split_metadata] "
|
|
"CP shared-KV batch split requires per-request split metadata."
|
|
)
|
|
|
|
expected_tokens = sum(int(x) for x in request_extend_lens)
|
|
input_tokens = int(tensor.shape[0])
|
|
if input_tokens != expected_tokens:
|
|
max_compute_tokens = (
|
|
sum(int(x) for x in request_target_lens)
|
|
if split_kind == "compute" and compute_padding_enabled
|
|
else expected_tokens
|
|
)
|
|
max_static_tokens = (
|
|
int(static_padded_tokens)
|
|
if static_padded_tokens is not None
|
|
else expected_tokens
|
|
)
|
|
max_allowed_tokens = max(max_compute_tokens, max_static_tokens)
|
|
if (
|
|
split_kind == "compute"
|
|
and input_tokens > expected_tokens
|
|
and input_tokens <= max_allowed_tokens
|
|
and (
|
|
compute_padding_enabled
|
|
or (
|
|
static_padded_tokens is not None
|
|
and int(static_padded_tokens) > expected_tokens
|
|
)
|
|
)
|
|
):
|
|
tensor = tensor[:expected_tokens]
|
|
else:
|
|
expected_detail = f"expected={expected_tokens}"
|
|
if split_kind == "compute" and compute_padding_enabled:
|
|
expected_detail += f" max_compute={max_compute_tokens}"
|
|
if split_kind == "compute" and static_padded_tokens is not None:
|
|
expected_detail += f" static_padded={int(static_padded_tokens)}"
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][batch_gt1_split_input_len_mismatch] "
|
|
f"input tokens={input_tokens} {expected_detail}"
|
|
)
|
|
|
|
local_chunks = []
|
|
request_tensors = torch.split(tensor, [int(x) for x in request_extend_lens], dim=0)
|
|
for req_id, (req_tensor, target_len, split_list, zigzag_index) in enumerate(
|
|
zip(
|
|
request_tensors,
|
|
request_target_lens,
|
|
request_split_lists,
|
|
request_zigzag_indices,
|
|
)
|
|
):
|
|
req_tensor = _pad_cp_request_tensor_for_split(
|
|
req_tensor,
|
|
target_len=int(target_len),
|
|
mode=mode,
|
|
req_id=req_id,
|
|
)
|
|
split_total = sum(int(x) for x in split_list)
|
|
if split_total != int(req_tensor.shape[0]):
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][batch_gt1_split_target_len_mismatch] "
|
|
"request split rows must equal padded request rows. "
|
|
f"req_id={req_id} split_total={split_total} "
|
|
f"request_rows={int(req_tensor.shape[0])} mode={mode}"
|
|
)
|
|
req_segments = list(torch.split(req_tensor, [int(x) for x in split_list], dim=0))
|
|
local_chunks.extend(req_segments[int(index)] for index in zigzag_index)
|
|
|
|
if not local_chunks:
|
|
return tensor.new_empty((0, *tensor.shape[1:]))
|
|
return torch.cat(local_chunks, dim=0).view(-1, *tensor.shape[1:])
|
|
|
|
|
|
def _get_cp_local_valid_row_indices_cache(forward_batch, plan, device: torch.device):
|
|
cached = getattr(forward_batch, "cp_local_valid_row_indices_for_cache_write", None)
|
|
cached_expected_rows = getattr(
|
|
forward_batch, "cp_local_valid_compute_rows_for_cache_write", None
|
|
)
|
|
if cached is not None and cached_expected_rows is not None:
|
|
if cached.device == device:
|
|
return cached, int(cached_expected_rows)
|
|
|
|
batch_size = int(getattr(plan, "batch_size", 1) or 1)
|
|
request_compute_split_lists = getattr(plan, "request_compute_split_lists", None)
|
|
request_valid_split_lists = getattr(
|
|
plan, "request_valid_split_lists", None
|
|
) or getattr(plan, "request_split_lists", None)
|
|
request_zigzag_indices = getattr(plan, "request_zigzag_indices", None)
|
|
if (
|
|
request_compute_split_lists is None
|
|
or request_valid_split_lists is None
|
|
or request_zigzag_indices is None
|
|
or len(request_compute_split_lists) != batch_size
|
|
or len(request_valid_split_lists) != batch_size
|
|
or len(request_zigzag_indices) != batch_size
|
|
):
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][cache_write_valid_selector_metadata] "
|
|
"CP shared-KV cache writes require valid and compute split metadata "
|
|
"when compute padding is enabled."
|
|
)
|
|
|
|
chunks = []
|
|
local_cursor = 0
|
|
for req_id in range(batch_size):
|
|
compute_split = request_compute_split_lists[req_id]
|
|
valid_split = request_valid_split_lists[req_id]
|
|
if len(compute_split) != len(valid_split):
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][cache_write_valid_selector_metadata] "
|
|
"valid and compute split metadata disagree. "
|
|
f"req_id={req_id} compute_segments={len(compute_split)} "
|
|
f"valid_segments={len(valid_split)}"
|
|
)
|
|
for segment_id in request_zigzag_indices[req_id]:
|
|
segment_id = int(segment_id)
|
|
compute_len = int(compute_split[segment_id])
|
|
valid_len = int(valid_split[segment_id])
|
|
if valid_len > compute_len:
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][cache_write_valid_selector_metadata] "
|
|
"valid segment is longer than compute segment. "
|
|
f"req_id={req_id} segment_id={segment_id} "
|
|
f"valid_len={valid_len} compute_len={compute_len}"
|
|
)
|
|
if valid_len > 0:
|
|
chunks.append(
|
|
torch.arange(
|
|
local_cursor,
|
|
local_cursor + valid_len,
|
|
device=device,
|
|
dtype=torch.long,
|
|
)
|
|
)
|
|
local_cursor += compute_len
|
|
|
|
if chunks:
|
|
indices = torch.cat(chunks, dim=0)
|
|
else:
|
|
indices = torch.empty((0,), device=device, dtype=torch.long)
|
|
forward_batch.cp_local_valid_row_indices_for_cache_write = indices
|
|
forward_batch.cp_local_valid_compute_rows_for_cache_write = local_cursor
|
|
return indices, local_cursor
|
|
|
|
|
|
def select_cp_local_valid_rows_for_cache_write(
|
|
forward_batch,
|
|
local_tensor: torch.Tensor,
|
|
) -> torch.Tensor:
|
|
"""Drop compute-padding rows before writing CP shared KV into persistent cache."""
|
|
|
|
plan = get_cp_shared_kv_batch_plan(forward_batch)
|
|
if plan is None or not bool(getattr(plan, "compute_padding_enabled", False)):
|
|
return local_tensor
|
|
|
|
indices, expected_compute_rows = _get_cp_local_valid_row_indices_cache(
|
|
forward_batch,
|
|
plan,
|
|
local_tensor.device,
|
|
)
|
|
local_rows = int(local_tensor.shape[0])
|
|
if local_rows != expected_compute_rows:
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][cache_write_valid_selector_shape] "
|
|
"CP shared-KV cache write tensor must contain local compute rows "
|
|
"before valid-row selection. "
|
|
f"local_rows={local_rows} expected_compute_rows={expected_compute_rows} "
|
|
f"valid_rows={int(indices.numel())}"
|
|
)
|
|
if indices.numel() == local_rows:
|
|
return local_tensor
|
|
if indices.numel() == 0:
|
|
return local_tensor.new_empty((0, *local_tensor.shape[1:]))
|
|
return local_tensor.index_select(0, indices)
|
|
|
|
|
|
def _pad_cp_request_tensor_for_split(
|
|
tensor: torch.Tensor,
|
|
*,
|
|
target_len: int,
|
|
mode: str,
|
|
req_id: int,
|
|
) -> torch.Tensor:
|
|
current_len = int(tensor.shape[0])
|
|
if target_len < current_len:
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][batch_gt1_compute_padding_len_mismatch] "
|
|
"target split length is shorter than request valid rows. "
|
|
f"req_id={req_id} target_len={target_len} current_len={current_len}"
|
|
)
|
|
pad_len = target_len - current_len
|
|
if pad_len == 0:
|
|
return tensor
|
|
if current_len <= 0:
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][batch_gt1_compute_padding_empty_request] "
|
|
f"cannot compute-pad an empty request. req_id={req_id}"
|
|
)
|
|
|
|
if mode == "position":
|
|
if tensor.dim() != 1:
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][batch_gt1_position_padding_rank] "
|
|
"position compute padding expects a 1D position tensor. "
|
|
f"req_id={req_id} shape={tuple(tensor.shape)}"
|
|
)
|
|
start = tensor[-1] + 1
|
|
padding = torch.arange(
|
|
pad_len,
|
|
device=tensor.device,
|
|
dtype=tensor.dtype,
|
|
) + start
|
|
else:
|
|
padding = tensor.new_zeros((pad_len, *tensor.shape[1:]))
|
|
return torch.cat([tensor, padding], dim=0)
|
|
|
|
|
|
def build_flat_page_owner_plan(plan) -> List[int]:
|
|
from sglang.srt.mem_cache.cp_shared_kv_compute_owner import (
|
|
build_in_seq_page_compute_owners,
|
|
)
|
|
|
|
owners: List[int] = []
|
|
for req_id, (extend_len, prefix_len) in enumerate(
|
|
zip(plan.request_extend_lens, plan.request_prefix_lens)
|
|
):
|
|
request_owners = build_in_seq_page_compute_owners(
|
|
extend_len=int(extend_len),
|
|
extend_prefix_len=int(prefix_len),
|
|
page_size=int(plan.page_size),
|
|
cp_size=int(plan.cp_size),
|
|
)
|
|
if request_owners is None:
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][batch_gt1_page_owner_plan_unavailable] "
|
|
f"req_id={req_id} extend_len={extend_len} prefix_len={prefix_len} "
|
|
f"page_size={plan.page_size} cp_size={plan.cp_size}"
|
|
)
|
|
owners.extend(request_owners)
|
|
return owners
|
|
|
|
|
|
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=[],
|
|
extend_valid_tokens=0,
|
|
extend_padded_pages=0,
|
|
extend_padded_tokens=0,
|
|
extend_padding_tokens=0,
|
|
)
|
|
|
|
|
|
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.
|
|
|
|
The split remains valid-token based, but page metadata covers the physical
|
|
page units. Short chunks keep the page-aligned contract by assigning zero
|
|
valid tokens to surplus zigzag segments instead of falling back to
|
|
token-balanced splits that would poison later shared-KV/HiCache reuse.
|
|
"""
|
|
|
|
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
|
|
|
|
extent = build_page_aligned_cache_extent(
|
|
valid_tokens=extend_len,
|
|
page_size=page_size,
|
|
)
|
|
full_pages = extend_len // page_size
|
|
tail_tokens = extend_len % page_size
|
|
num_page_units = extent.padded_pages
|
|
cp_segment_num = cp_size * 2
|
|
|
|
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,
|
|
extend_valid_tokens=extent.valid_tokens,
|
|
extend_padded_pages=extent.padded_pages,
|
|
extend_padded_tokens=extent.padded_tokens,
|
|
extend_padding_tokens=extent.padding_tokens,
|
|
)
|
|
|
|
|
|
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 _build_batch_metadata_from_plan(plan: CPSharedKVBatchPlan):
|
|
communication_split_lists = (
|
|
plan.request_compute_split_lists
|
|
if plan.compute_padding_enabled
|
|
else plan.request_split_lists
|
|
)
|
|
per_rank_actual_token = []
|
|
for rank in range(plan.cp_size):
|
|
rank_tokens = 0
|
|
mirror = plan.cp_size * 2 - rank - 1
|
|
for split_list in communication_split_lists:
|
|
rank_tokens += split_list[rank] + split_list[mirror]
|
|
per_rank_actual_token.append(rank_tokens)
|
|
max_rank_token = max(per_rank_actual_token) if per_rank_actual_token else 0
|
|
max_rank_len = [max_rank_token for _ in range(plan.cp_size)]
|
|
|
|
# Scalar fields remain populated for compatibility, but scalar-only
|
|
# consumers must not use them when batch_size > 1.
|
|
first_split = communication_split_lists[0] if communication_split_lists else []
|
|
first_valid_split = (
|
|
plan.request_valid_split_lists[0]
|
|
if plan.request_valid_split_lists
|
|
else plan.request_split_lists[0]
|
|
if plan.request_split_lists
|
|
else []
|
|
)
|
|
first_info = plan.request_split_infos[0] if plan.request_split_infos else None
|
|
first_zigzag = plan.request_zigzag_indices[0] if plan.request_zigzag_indices else []
|
|
first_valid_prefix_sum = list(accumulate(first_valid_split))
|
|
first_kv_len_prev = (
|
|
first_valid_prefix_sum[plan.cp_rank] if first_valid_prefix_sum else 0
|
|
)
|
|
first_mirror = plan.cp_size * 2 - plan.cp_rank - 1
|
|
first_kv_len_next = (
|
|
first_valid_prefix_sum[first_mirror] if first_valid_prefix_sum else 0
|
|
)
|
|
first_actual_seq_q_prev = first_split[plan.cp_rank] if first_split else 0
|
|
first_actual_seq_q_next = first_split[first_mirror] if first_split else 0
|
|
flat_communication_split_list = [
|
|
token_count
|
|
for split_list in communication_split_lists
|
|
for token_count in split_list
|
|
]
|
|
first_reverse_split_len = [
|
|
element
|
|
for i in range(plan.cp_size)
|
|
for element in (first_split[i], first_split[plan.cp_size * 2 - i - 1])
|
|
]
|
|
first_cp_reverse_index = (
|
|
list(range(0, plan.cp_size * 2, 2))
|
|
+ list(range(plan.cp_size * 2 - 1, 0, -2))
|
|
if first_split
|
|
else []
|
|
)
|
|
|
|
return NSAContextParallelMetadata(
|
|
split_list=first_split,
|
|
split_list_tensor=torch.tensor(
|
|
flat_communication_split_list, device="cuda", dtype=torch.int32
|
|
),
|
|
split_prefix_tensor=torch.tensor(
|
|
[0] + list(accumulate(flat_communication_split_list))[:-1],
|
|
device="cuda",
|
|
dtype=torch.int32,
|
|
),
|
|
max_rank_len=max_rank_len,
|
|
zigzag_index=first_zigzag,
|
|
per_rank_actual_token=per_rank_actual_token,
|
|
reverse_split_len=first_reverse_split_len,
|
|
cp_reverse_index=first_cp_reverse_index,
|
|
kv_len_prev=first_kv_len_prev,
|
|
kv_len_next=first_kv_len_next,
|
|
actual_seq_q_prev=first_actual_seq_q_prev,
|
|
actual_seq_q_next=first_actual_seq_q_next,
|
|
kv_len_prev_tensor=torch.tensor(
|
|
first_kv_len_prev, device="cuda", dtype=torch.int32
|
|
),
|
|
kv_len_next_tensor=torch.tensor(
|
|
first_kv_len_next, device="cuda", dtype=torch.int32
|
|
),
|
|
actual_seq_q_prev_tensor=torch.tensor(
|
|
first_actual_seq_q_prev, device="cuda", dtype=torch.int32
|
|
),
|
|
actual_seq_q_next_tensor=torch.tensor(
|
|
first_actual_seq_q_next, device="cuda", dtype=torch.int32
|
|
),
|
|
actual_seq_q_prev_cu_tensor=torch.tensor(
|
|
[0, first_actual_seq_q_prev], device="cuda", dtype=torch.int32
|
|
),
|
|
actual_seq_q_next_cu_tensor=torch.tensor(
|
|
[0, first_actual_seq_q_next], device="cuda", dtype=torch.int32
|
|
),
|
|
total_seq_lens=torch.tensor(max_rank_token * plan.cp_size),
|
|
page_aligned=True,
|
|
page_size=plan.page_size,
|
|
extend_prefix_len=(
|
|
first_info.extend_prefix_len if first_info is not None else 0
|
|
),
|
|
segment_page_starts=(
|
|
first_info.segment_page_starts if first_info is not None else []
|
|
),
|
|
segment_page_ends=(
|
|
first_info.segment_page_ends if first_info is not None else []
|
|
),
|
|
extend_valid_tokens=(
|
|
first_info.extend_valid_tokens if first_info is not None else 0
|
|
),
|
|
extend_padded_pages=(
|
|
first_info.extend_padded_pages if first_info is not None else 0
|
|
),
|
|
extend_padded_tokens=(
|
|
first_info.extend_padded_tokens if first_info is not None else 0
|
|
),
|
|
extend_padding_tokens=(
|
|
first_info.extend_padding_tokens if first_info is not None else 0
|
|
),
|
|
batch_size=plan.batch_size,
|
|
request_token_offsets=plan.request_token_offsets,
|
|
request_padded_token_offsets=plan.request_padded_token_offsets,
|
|
request_page_offsets=plan.request_page_offsets,
|
|
request_extend_lens=plan.request_extend_lens,
|
|
request_prefix_lens=plan.request_prefix_lens,
|
|
request_padded_pages=plan.request_padded_pages,
|
|
request_padded_tokens=plan.request_padded_tokens,
|
|
request_padding_tokens=plan.request_padding_tokens,
|
|
request_split_lists=plan.request_split_lists,
|
|
request_zigzag_indices=plan.request_zigzag_indices,
|
|
request_segment_page_starts=plan.request_segment_page_starts,
|
|
request_segment_page_ends=plan.request_segment_page_ends,
|
|
request_rank_local_tokens=plan.request_rank_local_tokens,
|
|
request_rank_local_offsets=plan.request_rank_local_offsets,
|
|
request_kv_len_prev=plan.request_kv_len_prev,
|
|
request_kv_len_next=plan.request_kv_len_next,
|
|
request_actual_seq_q_prev=plan.request_actual_seq_q_prev,
|
|
request_actual_seq_q_next=plan.request_actual_seq_q_next,
|
|
request_kv_len_prev_tensor=torch.tensor(
|
|
plan.request_kv_len_prev, device="cuda", dtype=torch.int32
|
|
),
|
|
request_kv_len_next_tensor=torch.tensor(
|
|
plan.request_kv_len_next, device="cuda", dtype=torch.int32
|
|
),
|
|
request_actual_seq_q_prev_tensor=torch.tensor(
|
|
plan.request_actual_seq_q_prev, device="cuda", dtype=torch.int32
|
|
),
|
|
request_actual_seq_q_next_tensor=torch.tensor(
|
|
plan.request_actual_seq_q_next, device="cuda", dtype=torch.int32
|
|
),
|
|
request_actual_seq_q_prev_cu_tensor=torch.tensor(
|
|
[0] + list(accumulate(plan.request_actual_seq_q_prev)),
|
|
device="cuda",
|
|
dtype=torch.int32,
|
|
),
|
|
request_actual_seq_q_next_cu_tensor=torch.tensor(
|
|
[0] + list(accumulate(plan.request_actual_seq_q_next)),
|
|
device="cuda",
|
|
dtype=torch.int32,
|
|
),
|
|
request_last_token_owner=plan.request_last_token_owner,
|
|
request_last_token_local_offset=plan.request_last_token_local_offset,
|
|
output_collect_mode=plan.output_collect_mode,
|
|
flat_split_list=plan.flat_split_list,
|
|
flat_zigzag_index=plan.flat_zigzag_index,
|
|
flat_segment_request_ids=plan.flat_segment_request_ids,
|
|
flat_segment_offsets=plan.flat_segment_offsets,
|
|
compute_padding_enabled=plan.compute_padding_enabled,
|
|
request_valid_split_lists=plan.request_valid_split_lists,
|
|
request_valid_segment_page_starts=plan.request_valid_segment_page_starts,
|
|
request_valid_segment_page_ends=plan.request_valid_segment_page_ends,
|
|
request_valid_padded_pages=plan.request_valid_padded_pages,
|
|
request_valid_padded_tokens=plan.request_valid_padded_tokens,
|
|
request_valid_padding_tokens=plan.request_valid_padding_tokens,
|
|
request_valid_rank_local_tokens=plan.request_valid_rank_local_tokens,
|
|
request_valid_rank_local_offsets=plan.request_valid_rank_local_offsets,
|
|
request_valid_actual_seq_q_prev=plan.request_valid_actual_seq_q_prev,
|
|
request_valid_actual_seq_q_next=plan.request_valid_actual_seq_q_next,
|
|
request_valid_seq_q_prev=plan.request_valid_seq_q_prev,
|
|
request_valid_seq_q_next=plan.request_valid_seq_q_next,
|
|
request_valid_query_row_spans=plan.request_valid_query_row_spans,
|
|
request_compute_split_lists=plan.request_compute_split_lists,
|
|
request_compute_segment_page_starts=plan.request_compute_segment_page_starts,
|
|
request_compute_segment_page_ends=plan.request_compute_segment_page_ends,
|
|
request_compute_padded_pages=plan.request_compute_padded_pages,
|
|
request_compute_padded_tokens=plan.request_compute_padded_tokens,
|
|
request_compute_padding_tokens=plan.request_compute_padding_tokens,
|
|
request_compute_padded_token_offsets=plan.request_compute_padded_token_offsets,
|
|
request_compute_rank_local_tokens=plan.request_compute_rank_local_tokens,
|
|
request_compute_rank_local_offsets=plan.request_compute_rank_local_offsets,
|
|
request_compute_actual_seq_q_prev=plan.request_compute_actual_seq_q_prev,
|
|
request_compute_actual_seq_q_next=plan.request_compute_actual_seq_q_next,
|
|
request_compute_seq_q_prev=plan.request_compute_seq_q_prev,
|
|
request_compute_seq_q_next=plan.request_compute_seq_q_next,
|
|
batch_plan=plan,
|
|
)
|
|
|
|
|
|
def should_use_replicated_compute_for_short_radix_hit(
|
|
forward_batch: "ForwardBatch",
|
|
cp_size: int,
|
|
) -> bool:
|
|
"""Return whether a short radix-hit suffix should avoid CP splitting.
|
|
|
|
Kept as a compatibility hook for older callers. The page-aligned cache
|
|
contract no longer uses replicated compute for short suffixes: zero-length
|
|
CP segments are preferred over breaking the physical page-owner pattern.
|
|
"""
|
|
|
|
return False
|
|
|
|
|
|
def should_skip_cp_shared_kv_cp_split_for_short_page_extent(
|
|
forward_batch: "ForwardBatch",
|
|
cp_size: int,
|
|
) -> bool:
|
|
"""Compatibility hook for the old tiny-suffix skip gate.
|
|
|
|
Compute padding now handles suffixes with fewer physical pages than CP
|
|
lanes, so this function validates the page-aligned shared-KV contract and
|
|
no longer requests a skip.
|
|
"""
|
|
|
|
if not _is_cp_shared_kv_forward_batch(forward_batch) or cp_size <= 1:
|
|
return False
|
|
_validate_cp_shared_kv_cp_split_plan_inputs(forward_batch, cp_size)
|
|
return False
|
|
|
|
|
|
def _validate_cp_shared_kv_cp_split_plan_inputs(
|
|
forward_batch: "ForwardBatch",
|
|
cp_size: int,
|
|
) -> bool:
|
|
if not _is_cp_shared_kv_forward_batch(forward_batch):
|
|
return False
|
|
if cp_size <= 1:
|
|
return False
|
|
|
|
extend_seq_lens_cpu = getattr(forward_batch, "extend_seq_lens_cpu", None)
|
|
extend_prefix_lens_cpu = getattr(forward_batch, "extend_prefix_lens_cpu", None)
|
|
token_to_kv_pool = getattr(forward_batch, "token_to_kv_pool", None)
|
|
page_size = int(getattr(token_to_kv_pool, "page_size", 0) or 0)
|
|
if (
|
|
extend_seq_lens_cpu is None
|
|
or extend_prefix_lens_cpu is None
|
|
or page_size <= 0
|
|
):
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][cp_split_missing_page_plan_inputs] "
|
|
"CP shared KV NSA in-seq split requires extend lengths, prefix "
|
|
"lengths, and token_to_kv_pool.page_size before planning. "
|
|
f"extend_seq_lens_cpu={extend_seq_lens_cpu} "
|
|
f"extend_prefix_lens_cpu={extend_prefix_lens_cpu} "
|
|
f"page_size={page_size} cp_size={cp_size}"
|
|
)
|
|
if len(extend_seq_lens_cpu) != len(extend_prefix_lens_cpu):
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][cp_split_length_mismatch] "
|
|
"CP shared KV NSA in-seq split requires one prefix length per "
|
|
"extend length. "
|
|
f"extend_seq_lens_cpu={extend_seq_lens_cpu} "
|
|
f"extend_prefix_lens_cpu={extend_prefix_lens_cpu} "
|
|
f"cp_size={cp_size}"
|
|
)
|
|
if len(extend_seq_lens_cpu) == 0:
|
|
return False
|
|
|
|
has_extend = False
|
|
for req_id, (extend_len_raw, prefix_len_raw) in enumerate(
|
|
zip(extend_seq_lens_cpu, extend_prefix_lens_cpu)
|
|
):
|
|
extend_len = int(extend_len_raw)
|
|
prefix_len = int(prefix_len_raw)
|
|
if extend_len < 0 or prefix_len < 0:
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][cp_split_negative_len] "
|
|
"CP shared KV NSA in-seq split received a negative length. "
|
|
f"req_id={req_id} prefix_len={prefix_len} "
|
|
f"extend_len={extend_len} page_size={page_size} "
|
|
f"cp_size={cp_size}"
|
|
)
|
|
if prefix_len % page_size != 0:
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][cp_split_non_page_aligned_prefix] "
|
|
"CP shared KV NSA in-seq split requires a page-aligned prefix. "
|
|
"The radix/HiCache match path should floor cache hits to the "
|
|
"previous page boundary before CP split planning. "
|
|
f"req_id={req_id} prefix_len={prefix_len} "
|
|
f"extend_len={extend_len} page_size={page_size} "
|
|
f"cp_size={cp_size}"
|
|
)
|
|
has_extend = has_extend or extend_len > 0
|
|
return has_extend
|
|
|
|
|
|
def can_cp_split(seq_len: int, cp_size: int, use_nsa: bool, forward_batch):
|
|
_fail_if_cp_shared_kv_round_robin(forward_batch, op="can_cp_split")
|
|
if is_nsa_prefill_cp_round_robin_split():
|
|
cur_cp_seq_len = seq_len // cp_size
|
|
assert seq_len % cp_size == 0, (
|
|
f"seq_len {seq_len} is not divisible by cp_size {cp_size} when nsa_prefill_cp_mode is round-robin-split"
|
|
)
|
|
else:
|
|
# TODO current just support prefill batch=1 and len(input_ids) > self.cp_size * 2
|
|
# Note: (self.cp_size * 2) To achieve load balancing for seq computation,
|
|
# the seq data needs to be divided and recombined at twice the size of cp_size.
|
|
if should_use_replicated_compute_for_short_radix_hit(forward_batch, cp_size):
|
|
return False
|
|
if _is_cp_shared_kv_forward_batch(forward_batch):
|
|
cur_cp_seq_len = (
|
|
1
|
|
if _validate_cp_shared_kv_cp_split_plan_inputs(
|
|
forward_batch, cp_size
|
|
)
|
|
else 0
|
|
)
|
|
else:
|
|
cur_cp_seq_len = seq_len // (cp_size * 2)
|
|
extend_token_count = sum(forward_batch.extend_seq_lens_cpu)
|
|
if _is_cp_shared_kv_forward_batch(forward_batch):
|
|
min_extend_token_count = 1
|
|
else:
|
|
min_extend_token_count = cp_size
|
|
is_context_parallel_extend = (
|
|
forward_batch.forward_mode.is_context_parallel_extend()
|
|
or _is_cp_shared_kv_draft_extend(forward_batch)
|
|
)
|
|
if (
|
|
cur_cp_seq_len != 0
|
|
and cp_size > 1
|
|
and use_nsa
|
|
and is_context_parallel_extend
|
|
and is_nsa_enable_prefill_cp()
|
|
and extend_token_count >= min_extend_token_count
|
|
):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def cp_split_and_rebuild_data(forward_batch, input_: torch.Tensor):
|
|
_fail_if_cp_shared_kv_round_robin(forward_batch, op="cp_split_and_rebuild_data")
|
|
if is_nsa_prefill_cp_round_robin_split():
|
|
cp_size = get_attention_cp_size()
|
|
assert input_.shape[0] % cp_size == 0, (
|
|
f"Expect input shape 0 can divided by cp size, but got input shape {input_.shape}, cp size {cp_size}"
|
|
)
|
|
return nsa_cp_round_robin_split_data(input_)
|
|
|
|
metadata = forward_batch.nsa_cp_metadata
|
|
if get_cp_shared_kv_batch_plan(forward_batch) is not None:
|
|
return _cp_split_and_rebuild_batch_in_seq(forward_batch, input_)
|
|
|
|
input_list = list(
|
|
torch.split(input_, metadata.split_list, dim=0)
|
|
)
|
|
result = torch.cat(
|
|
[input_list[i] for i in metadata.zigzag_index], dim=0
|
|
).view(-1, *input_.shape[1:])
|
|
return result
|
|
|
|
|
|
def cp_split_and_rebuild_1d(forward_batch, input_: torch.Tensor):
|
|
_fail_if_cp_shared_kv_round_robin(forward_batch, op="cp_split_and_rebuild_1d")
|
|
try:
|
|
round_robin_split = is_nsa_prefill_cp_round_robin_split()
|
|
except ValueError:
|
|
round_robin_split = False
|
|
if round_robin_split:
|
|
return nsa_cp_round_robin_split_data(input_)
|
|
|
|
metadata = forward_batch.nsa_cp_metadata
|
|
if get_cp_shared_kv_batch_plan(forward_batch) is not None:
|
|
return _cp_split_and_rebuild_batch_in_seq(forward_batch, input_)
|
|
|
|
input_list = list(
|
|
torch.split(input_, metadata.split_list, dim=0)
|
|
)
|
|
return torch.cat(
|
|
[input_list[i] for i in metadata.zigzag_index], dim=0
|
|
).view(-1)
|
|
|
|
|
|
def _cp_split_and_rebuild_batch_in_seq(forward_batch, input_: torch.Tensor):
|
|
return split_tensor_by_cp_batch_plan(
|
|
input_,
|
|
get_cp_shared_kv_batch_plan(forward_batch),
|
|
mode="1d" if input_.dim() == 1 else "data",
|
|
static_padded_tokens=_get_forward_batch_static_padded_tokens(forward_batch),
|
|
)
|
|
|
|
|
|
def get_cp_local_embedding_padded_token_count(forward_batch, local_num_tokens: int):
|
|
metadata = getattr(forward_batch, "nsa_cp_metadata", None)
|
|
max_rank_len = getattr(metadata, "max_rank_len", None)
|
|
if not max_rank_len:
|
|
return None
|
|
|
|
try:
|
|
padded_token_count = int(max_rank_len[0])
|
|
except (TypeError, ValueError, IndexError):
|
|
return None
|
|
|
|
if padded_token_count < int(local_num_tokens):
|
|
return None
|
|
|
|
return padded_token_count
|
|
|
|
|
|
def pad_cp_local_input_ids_for_embedding(
|
|
forward_batch,
|
|
local_input_ids: torch.Tensor,
|
|
*,
|
|
pad_token_id: int = 0,
|
|
):
|
|
local_num_tokens = local_input_ids.shape[0]
|
|
padded_token_count = get_cp_local_embedding_padded_token_count(
|
|
forward_batch, local_num_tokens
|
|
)
|
|
if padded_token_count is None:
|
|
return None
|
|
|
|
if padded_token_count == local_num_tokens:
|
|
return local_input_ids
|
|
|
|
pad_input_ids = local_input_ids.new_full(
|
|
(padded_token_count - local_num_tokens,), pad_token_id
|
|
)
|
|
return torch.cat((local_input_ids, pad_input_ids), dim=0)
|
|
|
|
|
|
def get_cp_shared_kv_local_out_cache_loc(forward_batch: "ForwardBatch"):
|
|
"""Return this CP rank's local logical out_cache_loc for direct writes.
|
|
|
|
`None` means the batch should keep using the compatibility path. This path
|
|
is intentionally conservative: it only enables direct writes after Phase 4
|
|
page-aligned split and after the logical page ids prove they are owned by
|
|
this CP rank.
|
|
"""
|
|
|
|
cached = getattr(forward_batch, "cp_local_out_cache_loc", None)
|
|
if cached is not None:
|
|
return cached
|
|
|
|
if not getattr(forward_batch, "uses_cp_shared_kv", False):
|
|
return None
|
|
metadata = getattr(forward_batch, "nsa_cp_metadata", None)
|
|
layout = getattr(forward_batch, "cp_shared_kv_layout", None)
|
|
out_cache_loc = getattr(forward_batch, "out_cache_loc", None)
|
|
if metadata is None:
|
|
raise_cp_shared_kv_direct_write_error(
|
|
"missing_metadata",
|
|
"nsa_cp_metadata is missing",
|
|
)
|
|
if layout is None:
|
|
raise_cp_shared_kv_direct_write_error(
|
|
"missing_layout",
|
|
"cp_shared_kv_layout is missing",
|
|
)
|
|
if out_cache_loc is None:
|
|
raise_cp_shared_kv_direct_write_error(
|
|
"missing_out_cache_loc",
|
|
"out_cache_loc is missing",
|
|
)
|
|
if not getattr(metadata, "page_aligned", False):
|
|
raise_cp_shared_kv_direct_write_error(
|
|
"not_page_aligned",
|
|
"metadata is not page-aligned: page_size=%s extend_prefix_len=%s",
|
|
getattr(metadata, "page_size", None),
|
|
getattr(metadata, "extend_prefix_len", None),
|
|
)
|
|
try:
|
|
in_seq_split = is_nsa_prefill_cp_in_seq_split()
|
|
except ValueError:
|
|
in_seq_split = True
|
|
if not in_seq_split:
|
|
raise_cp_shared_kv_direct_write_error(
|
|
"not_in_seq_split",
|
|
"nsa_prefill_cp_mode is not in-seq-split",
|
|
)
|
|
batch_plan = get_cp_shared_kv_batch_plan(forward_batch)
|
|
if batch_plan is not None:
|
|
split_tokens = sum(int(x) for x in getattr(batch_plan, "request_extend_lens", []))
|
|
mismatch_reason = "batch_split_out_cache_len_mismatch"
|
|
else:
|
|
split_tokens = sum(int(x) for x in metadata.split_list)
|
|
mismatch_reason = "split_out_cache_len_mismatch"
|
|
out_cache_tokens = int(out_cache_loc.numel())
|
|
if split_tokens != out_cache_tokens:
|
|
static_padded_tokens = _get_forward_batch_static_padded_tokens(forward_batch)
|
|
if (
|
|
static_padded_tokens is not None
|
|
and out_cache_tokens > split_tokens
|
|
and out_cache_tokens <= static_padded_tokens
|
|
):
|
|
# Model-runner/speculative warmup can append one global block of
|
|
# static padding rows after the valid flattened batch. These rows
|
|
# may carry dummy cache locs, but CP shared-KV direct-write is a
|
|
# valid-token operation: never split/write dummy compute rows.
|
|
#
|
|
# Do not use CP compute-padding metadata as an implicit allowance
|
|
# here. Page-tail/owner-lane compute padding is produced by
|
|
# split_tensor_by_cp_batch_plan() after valid input rows are split;
|
|
# only forward_batch.extend_num_tokens proves that out_cache_loc
|
|
# already contains global trailing static padding rows.
|
|
out_cache_loc = out_cache_loc[:split_tokens]
|
|
else:
|
|
raise_cp_shared_kv_direct_write_error(
|
|
mismatch_reason,
|
|
"split_list tokens=%s out_cache_loc tokens=%s static_padded=%s",
|
|
split_tokens,
|
|
out_cache_tokens,
|
|
static_padded_tokens,
|
|
)
|
|
|
|
if batch_plan is not None:
|
|
local_out_cache_loc = split_tensor_by_cp_batch_plan(
|
|
out_cache_loc.contiguous(),
|
|
batch_plan,
|
|
mode="1d",
|
|
split_kind="valid",
|
|
)
|
|
else:
|
|
local_out_cache_loc = cp_split_and_rebuild_1d(
|
|
forward_batch,
|
|
out_cache_loc.contiguous(),
|
|
)
|
|
if local_out_cache_loc.numel() == 0:
|
|
forward_batch.cp_local_out_cache_loc = local_out_cache_loc
|
|
return local_out_cache_loc
|
|
|
|
valid_locs = local_out_cache_loc[local_out_cache_loc > 0]
|
|
if valid_locs.numel() > 0 and not torch.all(layout.owned_by_this_rank(valid_locs)):
|
|
raise_cp_shared_kv_direct_write_error(
|
|
"local_loc_owner_mismatch",
|
|
"local out_cache_loc contains pages not owned by this rank: cp_rank=%s cp_size=%s page_size=%s",
|
|
layout.cp_rank,
|
|
layout.cp_size,
|
|
layout.page_size,
|
|
)
|
|
|
|
forward_batch.cp_local_out_cache_loc = local_out_cache_loc
|
|
return local_out_cache_loc
|
|
|
|
|
|
def get_cp_shared_kv_local_physical_out_cache_loc(forward_batch: "ForwardBatch"):
|
|
"""Return cached physical rows for this CP rank's shared-KV direct writes.
|
|
|
|
`get_cp_shared_kv_local_out_cache_loc` returns logical shared-KV locs because
|
|
radix/scheduler/PD metadata are expressed in the global logical address
|
|
space. Persistent writes into this rank's compact physical pool need the
|
|
same locs remapped through `CpSharedKVLayout`. The logical loc tensor is
|
|
batch-scoped, not layer-scoped, so cache the physical remap on ForwardBatch
|
|
and reuse it for both MLA KV and NSA index K/scale writes across layers.
|
|
"""
|
|
|
|
cached = getattr(forward_batch, "cp_local_physical_out_cache_loc", None)
|
|
if cached is not None:
|
|
return cached
|
|
|
|
local_out_cache_loc = get_cp_shared_kv_local_out_cache_loc(forward_batch)
|
|
if local_out_cache_loc is None:
|
|
return None
|
|
|
|
if local_out_cache_loc.numel() == 0:
|
|
forward_batch.cp_local_physical_out_cache_loc = local_out_cache_loc
|
|
return local_out_cache_loc
|
|
|
|
layout = getattr(forward_batch, "cp_shared_kv_layout", None)
|
|
if layout is None:
|
|
log_cp_shared_kv_direct_write_fallback(
|
|
"missing_layout",
|
|
"cp_shared_kv_layout is missing while building physical out_cache_loc",
|
|
)
|
|
return None
|
|
|
|
physical_out_cache_loc = layout.logical_locs_to_physical(
|
|
local_out_cache_loc
|
|
).contiguous()
|
|
log_cp_draft_shared_kv_debug(
|
|
"physical_out_loc",
|
|
"physical_out_loc cp_rank=%s cp_size=%s page_size=%s tokens=%s "
|
|
"physical_tokens=%s pool=%s",
|
|
layout.cp_rank,
|
|
layout.cp_size,
|
|
layout.page_size,
|
|
local_out_cache_loc.numel(),
|
|
physical_out_cache_loc.numel(),
|
|
getattr(forward_batch, "token_to_kv_pool", None).__class__.__name__,
|
|
)
|
|
forward_batch.cp_local_physical_out_cache_loc = physical_out_cache_loc
|
|
return physical_out_cache_loc
|
|
|
|
|
|
def cp_split_and_rebuild_position(forward_batch, positions: torch.Tensor):
|
|
_fail_if_cp_shared_kv_round_robin(forward_batch, op="cp_split_and_rebuild_position")
|
|
if is_nsa_prefill_cp_round_robin_split():
|
|
cp_size = get_attention_cp_size()
|
|
assert positions.shape[0] % cp_size == 0, (
|
|
f"Expect positions shape 0 can divided by cp size, but got positions shape {positions.shape}, "
|
|
f"cp size {cp_size}"
|
|
)
|
|
return nsa_cp_round_robin_split_data(positions)
|
|
|
|
if get_cp_shared_kv_batch_plan(forward_batch) is not None:
|
|
return split_tensor_by_cp_batch_plan(
|
|
positions,
|
|
get_cp_shared_kv_batch_plan(forward_batch),
|
|
mode="position",
|
|
static_padded_tokens=_get_forward_batch_static_padded_tokens(forward_batch),
|
|
)
|
|
|
|
position_id_list = list(
|
|
torch.split(positions, forward_batch.nsa_cp_metadata.split_list, dim=-1)
|
|
)
|
|
positions = torch.cat(
|
|
[position_id_list[i] for i in forward_batch.nsa_cp_metadata.zigzag_index],
|
|
dim=-1,
|
|
)
|
|
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,
|
|
out_seqs_ptr,
|
|
bs_idx_ptr,
|
|
tokens: tl.constexpr,
|
|
cp_size: tl.constexpr,
|
|
cp_rank: tl.constexpr,
|
|
):
|
|
extra_seq = 0
|
|
bs_idx = 0
|
|
for bs in range(tokens):
|
|
cur_len = tl.load(in_seqs_ptr + bs)
|
|
cur_len += extra_seq
|
|
cur_seq = cur_len // cp_size + (cur_len % cp_size > cp_rank)
|
|
if cur_seq > 0:
|
|
tl.store(bs_idx_ptr + bs_idx, bs)
|
|
tl.store(out_seqs_ptr + bs_idx, cur_seq)
|
|
bs_idx += 1
|
|
extra_seq = cur_len - cur_seq * cp_size
|
|
|
|
|
|
def nsa_cp_round_robin_split_q_seqs_cpu(extend_seqs):
|
|
cp_size = get_attention_cp_size()
|
|
cp_rank = get_attention_cp_rank()
|
|
extra_seq = 0
|
|
q_seqs = []
|
|
for bs, cur_len in enumerate(extend_seqs):
|
|
cur_len += extra_seq
|
|
cur_seq = cur_len // cp_size + int(cur_len % cp_size > cp_rank)
|
|
q_seqs.append(cur_seq)
|
|
extra_seq = cur_len - cur_seq * cp_size
|
|
bs_idx = list([i for i, x in enumerate(q_seqs) if x > 0])
|
|
q_seqs = [q_len for q_len in q_seqs if q_len > 0]
|
|
return q_seqs, bs_idx
|
|
|
|
|
|
def nsa_cp_round_robin_split_q_seqs(
|
|
extend_seqs_cpu, extend_seqs
|
|
) -> Tuple[List, torch.Tensor, List, torch.Tensor]:
|
|
"""
|
|
round-robin-split distributes tokens across ranks based on token_idx % cp_size.
|
|
|
|
Return:
|
|
ret_q_lens_cpu(List) and ret_q_lens(torch.Tensor): the partitioned length (excluding zeros) on the current cp rank
|
|
for each sequence after distribution across cp ranks.
|
|
bs_idx_cpu(List) and bs_idx(torch.Tensor): marks which sequences are ultimately selected,
|
|
i.e., those with a partitioned length greater than zero.
|
|
"""
|
|
cp_size = get_attention_cp_size()
|
|
cp_rank = get_attention_cp_rank()
|
|
# len(ret_q_lens_cpu) == len(bs_idx_cpu)
|
|
ret_q_lens_cpu, bs_idx_cpu = nsa_cp_round_robin_split_q_seqs_cpu(extend_seqs_cpu)
|
|
ret_q_lens = torch.empty(
|
|
(len(bs_idx_cpu),), device=extend_seqs.device, dtype=extend_seqs.dtype
|
|
)
|
|
bs_idx = torch.empty(
|
|
(len(bs_idx_cpu),), device=extend_seqs.device, dtype=torch.int32
|
|
)
|
|
grid = (1,)
|
|
nsa_cp_round_robin_split_q_seqs_kernel[grid](
|
|
extend_seqs, ret_q_lens, bs_idx, len(extend_seqs), cp_size, cp_rank
|
|
)
|
|
return ret_q_lens_cpu, ret_q_lens, bs_idx_cpu, bs_idx
|
|
|
|
|
|
def nsa_use_prefill_cp(forward_batch, nsa_enable_prefill_cp=None):
|
|
if nsa_enable_prefill_cp is None:
|
|
nsa_enable_prefill_cp = is_nsa_enable_prefill_cp()
|
|
forward_mode = getattr(forward_batch, "forward_mode", None)
|
|
is_context_parallel_extend = (
|
|
forward_mode is not None
|
|
and forward_mode.is_context_parallel_extend()
|
|
) or _is_cp_shared_kv_draft_extend(forward_batch)
|
|
if (
|
|
forward_batch.nsa_cp_metadata is not None
|
|
and nsa_enable_prefill_cp
|
|
and is_context_parallel_extend
|
|
):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def _cp_attn_tp_all_gather_padded_tensor(
|
|
input_: torch.Tensor, total_len, attn_tp_size, forward_batch, stream_op
|
|
):
|
|
"""
|
|
Allgather communication for context_parallel(kv_cache, index_k, hidden_states).
|
|
This implementation mainly consists of three parts:
|
|
Step 1, padding the input shape to unify the shape for allgather communication (the shape must be the same).
|
|
Step 2, allgather communication(async).
|
|
Step 3, removing the padding and reassembling the data according to the actual tokens.
|
|
"""
|
|
# step1
|
|
max_len = (total_len + attn_tp_size - 1) // attn_tp_size
|
|
pad_size = max_len - input_.shape[0]
|
|
if pad_size > 0:
|
|
input_ = torch.cat(
|
|
(
|
|
input_,
|
|
input_.new_zeros((pad_size, *input_.shape[1:])),
|
|
),
|
|
dim=0,
|
|
)
|
|
with use_symmetric_memory(
|
|
get_attention_cp_group(), disabled=not is_allocation_symmetric()
|
|
):
|
|
input_tensor_all = torch.empty(
|
|
max_len * attn_tp_size,
|
|
*input_.shape[1:],
|
|
device=input_.device,
|
|
dtype=input_.dtype,
|
|
)
|
|
# step2
|
|
get_attention_cp_group().cp_all_gather_into_tensor_async(
|
|
input_tensor_all, input_, stream_op
|
|
)
|
|
return input_tensor_all
|
|
|
|
|
|
def _trim_cp_rank_padding_after_all_gather(input_tensor_all, forward_batch):
|
|
outputs_list_max = list(
|
|
torch.split(input_tensor_all, forward_batch.nsa_cp_metadata.max_rank_len, dim=0)
|
|
)
|
|
return torch.cat(
|
|
[
|
|
outputs_list_max[index][:per_rank_len]
|
|
for index, per_rank_len in enumerate(
|
|
forward_batch.nsa_cp_metadata.per_rank_actual_token
|
|
)
|
|
],
|
|
dim=0,
|
|
)
|
|
|
|
|
|
def cp_attn_tp_all_gather_reorganazied_into_tensor(
|
|
input_: torch.Tensor, total_len, attn_tp_size, forward_batch, stream_op
|
|
):
|
|
input_tensor_all = _cp_attn_tp_all_gather_padded_tensor(
|
|
input_, total_len, attn_tp_size, forward_batch, stream_op
|
|
)
|
|
# step3
|
|
outputs = _trim_cp_rank_padding_after_all_gather(input_tensor_all, forward_batch)
|
|
return outputs
|
|
|
|
|
|
def _log_tai_in_seq_rerange_fallback(reason: str, message: str, *args) -> None:
|
|
logger.warning(
|
|
"[CP_SHARED_KV_FALLBACK][tai_in_seq_rerange] reason=%s " + message,
|
|
reason,
|
|
*args,
|
|
)
|
|
|
|
|
|
def _try_tai_in_seq_all_gather_rerange(
|
|
input_tensor_all: torch.Tensor,
|
|
forward_batch: "ForwardBatch",
|
|
*,
|
|
hidden_size: int,
|
|
cp_size: int,
|
|
) -> torch.Tensor | None:
|
|
metadata = forward_batch.nsa_cp_metadata
|
|
split_lens = getattr(metadata, "split_list_tensor", None)
|
|
split_prefix = getattr(metadata, "split_prefix_tensor", None)
|
|
split_list = getattr(metadata, "split_list", None)
|
|
max_rank_len = getattr(metadata, "max_rank_len", None)
|
|
if split_lens is None or split_prefix is None:
|
|
_log_tai_in_seq_rerange_fallback(
|
|
"missing_metadata",
|
|
"split_list_tensor or split_prefix_tensor is missing",
|
|
)
|
|
return None
|
|
if split_list is None or max_rank_len is None:
|
|
_log_tai_in_seq_rerange_fallback(
|
|
"missing_cpu_metadata",
|
|
"split_list or max_rank_len is missing",
|
|
)
|
|
return None
|
|
if not input_tensor_all.is_cuda or input_tensor_all.dim() != 2:
|
|
return None
|
|
|
|
total_tokens = int(sum(split_list))
|
|
if total_tokens == 0:
|
|
return input_tensor_all.new_empty((0, hidden_size))
|
|
|
|
max_segment_len = int(max(split_list))
|
|
max_rank_token = int(max_rank_len[0]) if len(max_rank_len) > 0 else 0
|
|
if max_segment_len <= 0 or max_rank_token <= 0:
|
|
return None
|
|
if input_tensor_all.shape[0] < max_rank_token * cp_size:
|
|
return None
|
|
|
|
try:
|
|
from tai_kernel.nsa_prefill import in_seq_all_gather_rerange
|
|
except Exception as exc:
|
|
_log_tai_in_seq_rerange_fallback(
|
|
"import_failed",
|
|
"failed to import tai_kernel.nsa_prefill.in_seq_all_gather_rerange: %s",
|
|
exc,
|
|
)
|
|
return None
|
|
|
|
try:
|
|
return in_seq_all_gather_rerange(
|
|
input_tensor_all,
|
|
split_lens,
|
|
split_prefix,
|
|
total_tokens=total_tokens,
|
|
hidden_size=hidden_size,
|
|
max_segment_len=max_segment_len,
|
|
max_rank_token=max_rank_token,
|
|
cp_size=cp_size,
|
|
)
|
|
except Exception as exc:
|
|
_log_tai_in_seq_rerange_fallback(
|
|
"kernel_failed",
|
|
"tai-kernel in-seq rerange failed: %s",
|
|
exc,
|
|
)
|
|
return None
|
|
|
|
|
|
def _torch_in_seq_all_gather_rerange(
|
|
input_tensor_all: torch.Tensor,
|
|
forward_batch: "ForwardBatch",
|
|
hidden_size: int,
|
|
) -> torch.Tensor:
|
|
output_tensor = _trim_cp_rank_padding_after_all_gather(
|
|
input_tensor_all, forward_batch
|
|
)
|
|
outputs_list = list(
|
|
torch.split(
|
|
output_tensor, forward_batch.nsa_cp_metadata.reverse_split_len, dim=0
|
|
)
|
|
)
|
|
output_tensor = torch.cat(
|
|
[outputs_list[i] for i in forward_batch.nsa_cp_metadata.cp_reverse_index], dim=0
|
|
)
|
|
return output_tensor.view(-1, hidden_size)
|
|
|
|
|
|
def _raise_batch_rerange_error(reason: str, message: str, *args) -> None:
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][batch_gt1_full_rerange_unsupported] "
|
|
+ f"reason={reason} "
|
|
+ (message % args if args else message)
|
|
)
|
|
|
|
|
|
def _torch_batch_in_seq_all_gather_rerange(
|
|
input_tensor_all: torch.Tensor,
|
|
forward_batch: "ForwardBatch",
|
|
*,
|
|
cp_size: int,
|
|
) -> torch.Tensor:
|
|
"""Restore rank-major batched in-seq CP all-gather rows to request order.
|
|
|
|
The row payload is intentionally opaque. MLA bf16 latent rows, packed fp8
|
|
KV rows, and NSA index byte rows all share the same first-dimension token
|
|
order contract, so this helper only slices/copies rows and never interprets
|
|
the dtype or payload shape.
|
|
"""
|
|
|
|
metadata = getattr(forward_batch, "nsa_cp_metadata", None)
|
|
batch_size = int(getattr(metadata, "batch_size", 1) or 1)
|
|
request_split_lists = getattr(metadata, "request_split_lists", None)
|
|
max_rank_len = getattr(metadata, "max_rank_len", None)
|
|
if metadata is None:
|
|
_raise_batch_rerange_error("missing_metadata", "nsa_cp_metadata is missing")
|
|
if batch_size <= 1:
|
|
_raise_batch_rerange_error(
|
|
"not_batch",
|
|
"batch-aware rerange requires batch_size > 1, got %s",
|
|
batch_size,
|
|
)
|
|
if request_split_lists is None or len(request_split_lists) != batch_size:
|
|
_raise_batch_rerange_error(
|
|
"missing_request_split_lists",
|
|
"request_split_lists is missing or incomplete. batch_size=%s value=%s",
|
|
batch_size,
|
|
request_split_lists,
|
|
)
|
|
if max_rank_len is None or len(max_rank_len) < cp_size:
|
|
_raise_batch_rerange_error(
|
|
"missing_max_rank_len",
|
|
"max_rank_len is missing or shorter than cp_size. cp_size=%s value=%s",
|
|
cp_size,
|
|
max_rank_len,
|
|
)
|
|
if cp_size <= 0:
|
|
_raise_batch_rerange_error("bad_cp_size", "cp_size must be positive: %s", cp_size)
|
|
|
|
split_lists: List[List[int]] = []
|
|
for req_id, split_list in enumerate(request_split_lists):
|
|
if split_list is None or len(split_list) != cp_size * 2:
|
|
_raise_batch_rerange_error(
|
|
"bad_request_split",
|
|
"request split must have 2 * cp_size entries. req_id=%s "
|
|
"cp_size=%s split=%s",
|
|
req_id,
|
|
cp_size,
|
|
split_list,
|
|
)
|
|
split_lists.append([int(x) for x in split_list])
|
|
|
|
max_rank_token = int(max_rank_len[0])
|
|
if max_rank_token < 0:
|
|
_raise_batch_rerange_error(
|
|
"bad_max_rank_token",
|
|
"max_rank_len[0] must be non-negative, got %s",
|
|
max_rank_token,
|
|
)
|
|
required_rows = max_rank_token * cp_size
|
|
if int(input_tensor_all.shape[0]) < required_rows:
|
|
_raise_batch_rerange_error(
|
|
"input_rows_short",
|
|
"input rows=%s required=%s max_rank_token=%s cp_size=%s",
|
|
int(input_tensor_all.shape[0]),
|
|
required_rows,
|
|
max_rank_token,
|
|
cp_size,
|
|
)
|
|
|
|
rank_request_offsets: List[List[int]] = []
|
|
for source_rank in range(cp_size):
|
|
mirror = cp_size * 2 - source_rank - 1
|
|
offsets: List[int] = []
|
|
cursor = 0
|
|
for split_list in split_lists:
|
|
offsets.append(cursor)
|
|
cursor += split_list[source_rank] + split_list[mirror]
|
|
if cursor > max_rank_token:
|
|
_raise_batch_rerange_error(
|
|
"rank_payload_exceeds_max",
|
|
"rank local rows exceed max_rank_token. rank=%s rows=%s "
|
|
"max_rank_token=%s",
|
|
source_rank,
|
|
cursor,
|
|
max_rank_token,
|
|
)
|
|
rank_request_offsets.append(offsets)
|
|
|
|
total_tokens = sum(sum(split_list) for split_list in split_lists)
|
|
output_tensor = input_tensor_all.new_empty(
|
|
(total_tokens, *input_tensor_all.shape[1:])
|
|
)
|
|
if total_tokens == 0:
|
|
return output_tensor
|
|
|
|
output_request_base = 0
|
|
for req_id, split_list in enumerate(split_lists):
|
|
segment_prefix = [0] + list(accumulate(split_list))[:-1]
|
|
for segment_id, segment_len in enumerate(split_list):
|
|
if segment_len <= 0:
|
|
continue
|
|
if segment_id < cp_size:
|
|
source_rank = segment_id
|
|
source_segment_offset = 0
|
|
else:
|
|
source_rank = cp_size * 2 - segment_id - 1
|
|
source_segment_offset = split_list[source_rank]
|
|
source_start = (
|
|
source_rank * max_rank_token
|
|
+ rank_request_offsets[source_rank][req_id]
|
|
+ source_segment_offset
|
|
)
|
|
output_start = output_request_base + segment_prefix[segment_id]
|
|
output_tensor[output_start : output_start + segment_len].copy_(
|
|
input_tensor_all[source_start : source_start + segment_len]
|
|
)
|
|
output_request_base += sum(split_list)
|
|
|
|
return output_tensor
|
|
|
|
|
|
def cp_all_gather_rerange_output(input_tensor, cp_size, forward_batch, stream):
|
|
"""
|
|
# for in-seq-split
|
|
| +-----------before allgather------------+|
|
|
| | dp_atten_tp0: block0, block7 |
|
|
| | dp_atten_tp1: block1, block6 |
|
|
| | dp_atten_tp2: block2, block5 |
|
|
| | dp_atten_tp3: block3, block4 |
|
|
|
|
|
| +----------before rerange---------------+|
|
|
| block0 | block7 | block1 | block6 | block2 | block5 | block3 | block4 |
|
|
|
|
|
| +--------------result-------------------+
|
|
| block0 | block1 | block2 | block3 | block4 | block5 | block6 | block7 |
|
|
| +-------------------------+
|
|
|
|
# for round-robin-split
|
|
| +-----------before allgather------------+|
|
|
| dp_atten_tp0: token0, token4, token8, token12, token16, ... |
|
|
| dp_atten_tp1: token1, token5, token9, token13, token17, ... |
|
|
| dp_atten_tp2: token2, token6, token10, token14, token18, ... |
|
|
| dp_atten_tp3: token3, token7, token11, token15, token19, ... |
|
|
|
|
|
| +--------------result-------------------+
|
|
| token0, token1, token2, token3, token4, token5, token6, token7, ...
|
|
| +-------------------------+
|
|
"""
|
|
_fail_if_cp_shared_kv_round_robin(
|
|
forward_batch,
|
|
op="cp_all_gather_rerange_output",
|
|
)
|
|
metadata = getattr(forward_batch, "nsa_cp_metadata", None)
|
|
batch_size = int(getattr(metadata, "batch_size", 1) or 1)
|
|
if batch_size > 1:
|
|
if input_tensor.dim() < 2:
|
|
_raise_batch_rerange_error(
|
|
"bad_input_dim",
|
|
"input tensor must have token rows plus payload dims, got shape=%s",
|
|
tuple(input_tensor.shape),
|
|
)
|
|
if getattr(metadata, "request_split_lists", None) is None:
|
|
_raise_batch_rerange_error(
|
|
"missing_request_split_lists",
|
|
"request_split_lists is required for batch-aware current rerange. "
|
|
"batch_size=%s",
|
|
batch_size,
|
|
)
|
|
total_seq_lens = getattr(metadata, "total_seq_lens", None)
|
|
if total_seq_lens is None:
|
|
_raise_batch_rerange_error(
|
|
"missing_total_seq_lens",
|
|
"total_seq_lens is required for batch-aware current rerange. "
|
|
"batch_size=%s",
|
|
batch_size,
|
|
)
|
|
input_tensor_all = _cp_attn_tp_all_gather_padded_tensor(
|
|
input_tensor,
|
|
total_seq_lens,
|
|
cp_size,
|
|
forward_batch,
|
|
stream,
|
|
)
|
|
return _torch_batch_in_seq_all_gather_rerange(
|
|
input_tensor_all,
|
|
forward_batch,
|
|
cp_size=cp_size,
|
|
)
|
|
|
|
if is_nsa_prefill_cp_round_robin_split():
|
|
with use_symmetric_memory(
|
|
get_attention_cp_group(), disabled=not is_allocation_symmetric()
|
|
):
|
|
output_tensor = input_tensor.new_empty(
|
|
(input_tensor.shape[0] * cp_size, *input_tensor.shape[1:]),
|
|
)
|
|
attn_cp_all_gather_into_tensor(
|
|
output_tensor,
|
|
input_tensor,
|
|
)
|
|
out_shape = output_tensor.shape
|
|
output_tensor = (
|
|
output_tensor.view(cp_size, -1, *out_shape[1:])
|
|
.transpose(0, 1)
|
|
.reshape(out_shape)
|
|
)
|
|
return output_tensor
|
|
|
|
bs_seq_len, hidden_size = input_tensor.shape
|
|
input_tensor_all = _cp_attn_tp_all_gather_padded_tensor(
|
|
input_tensor,
|
|
forward_batch.nsa_cp_metadata.total_seq_lens,
|
|
cp_size,
|
|
forward_batch,
|
|
stream,
|
|
)
|
|
output_tensor = _try_tai_in_seq_all_gather_rerange(
|
|
input_tensor_all,
|
|
forward_batch,
|
|
hidden_size=hidden_size,
|
|
cp_size=cp_size,
|
|
)
|
|
if output_tensor is not None:
|
|
return output_tensor
|
|
return _torch_in_seq_all_gather_rerange(
|
|
input_tensor_all,
|
|
forward_batch,
|
|
hidden_size,
|
|
)
|
|
|
|
|
|
def calculate_cp_seq_idx(cp_chunks_len, seqs_len):
|
|
"""Used to obtain the index of the seq corresponding
|
|
to each cp block in the forwardbatch, and the starting
|
|
and ending positions of the corresponding seq in the cp block"""
|
|
j = 0
|
|
tuple_len = [] # Only keep this result list
|
|
cumulative = {} # Used to track cumulative values for each index
|
|
|
|
for i in range(len(cp_chunks_len)):
|
|
current_dict = {}
|
|
current_tuples = []
|
|
c_val = cp_chunks_len[i]
|
|
|
|
while j < len(seqs_len):
|
|
s_val = seqs_len[j]
|
|
if s_val == c_val:
|
|
idx = j
|
|
current_dict[idx] = s_val
|
|
# Update cumulative value for this index
|
|
cumulative[idx] = cumulative.get(idx, 0) + s_val
|
|
j += 1
|
|
break
|
|
elif s_val > c_val:
|
|
idx = j
|
|
current_dict[idx] = c_val
|
|
# Update cumulative value for this index
|
|
cumulative[idx] = cumulative.get(idx, 0) + c_val
|
|
seqs_len[j] = s_val - c_val
|
|
break
|
|
else: # s_val < c_val
|
|
idx = j
|
|
current_dict[idx] = s_val
|
|
# Update cumulative value for this index
|
|
cumulative[idx] = cumulative.get(idx, 0) + s_val
|
|
c_val -= s_val
|
|
j += 1
|
|
|
|
# Build tuple: (index, historical cumulative, historical+current)
|
|
for idx, val in current_dict.items():
|
|
# Subtract current value to get historical cumulative
|
|
prev_cum = cumulative.get(idx, 0) - val
|
|
current_cum = prev_cum + val
|
|
current_tuples.append((idx, prev_cum, current_cum))
|
|
|
|
tuple_len.append(current_tuples)
|
|
return tuple_len
|
|
|
|
|
|
def prepare_input_dp_with_cp_dsa(
|
|
kv_len,
|
|
cp_rank,
|
|
cp_size,
|
|
seqs_len,
|
|
*,
|
|
forward_batch: "ForwardBatch" = None,
|
|
page_size: int = None,
|
|
):
|
|
_fail_if_cp_shared_kv_round_robin(
|
|
forward_batch,
|
|
op="prepare_input_dp_with_cp_dsa",
|
|
)
|
|
if is_nsa_prefill_cp_round_robin_split():
|
|
return True
|
|
"""prepare_input_dp_with_cp_dsa-zigzag index
|
|
Example (DP_ATTENT_TP == CP_SIZE == 4):
|
|
Description:
|
|
1. Start with a full-length request.
|
|
2. Split the request into multiple blocks (block0 to block7).
|
|
3. Rearrange these blocks to balance computational
|
|
load across different DP ranks.
|
|
4. Assign the rearranged blocks to different DP attention
|
|
time points (dp_atten_tp0 to dp_atten_tp3).
|
|
+---------------------------------+
|
|
| cp_split_tokens |
|
|
+---------------------------------+
|
|
| |
|
|
| request_with_full_length |
|
|
| | split (cp_size * 2) |
|
|
| +-------------------------+ |
|
|
| | block0 | block1 | block2 | block3 | block4 | block5 | block6 | block7 |
|
|
| +-------------------------+ |
|
|
| | rerange |
|
|
| +---------------------------------+
|
|
| | block0 | block7 | block1 | block6 | block2 | block5 | block3 | block4 |
|
|
| +---------------------------------+
|
|
| |
|
|
| +-------------------------+
|
|
| | dp_atten_tp0: block0, block7 |
|
|
| | dp_atten_tp1: block1, block6 |
|
|
| | dp_atten_tp2: block2, block5 |
|
|
| | dp_atten_tp3: block3, block4 |
|
|
| +-------------------------+
|
|
|
|
Why zigzag rearrange?
|
|
- Attention calculations must follow causal attention principles.
|
|
- Simply slicing by rank order can lead to computational load imbalance:
|
|
* First rank may focus on fewer historical key-value tokens (less computation)
|
|
* Last rank may focus on more tokens (more computation)
|
|
- To mitigate uneven load, the input hissenstate needs to be sliced by cp_size*2 and rearranged.
|
|
"""
|
|
if (
|
|
forward_batch is not None
|
|
and getattr(forward_batch, "uses_cp_shared_kv", False)
|
|
and getattr(forward_batch, "extend_seq_lens_cpu", None) is not None
|
|
and getattr(forward_batch, "extend_prefix_lens_cpu", None) is not None
|
|
):
|
|
if page_size is None:
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][cp_shared_missing_page_size] "
|
|
"CP shared-KV batch planning requires token_to_kv_pool.page_size"
|
|
)
|
|
batch_plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[int(x) for x in forward_batch.extend_seq_lens_cpu],
|
|
prefix_lens=[int(x) for x in forward_batch.extend_prefix_lens_cpu],
|
|
page_size=int(page_size),
|
|
cp_size=cp_size,
|
|
cp_rank=cp_rank,
|
|
)
|
|
return _build_batch_metadata_from_plan(batch_plan)
|
|
|
|
# scalar compatibility path
|
|
kv_len_int = int(kv_len)
|
|
kv_len = torch.tensor(kv_len_int)
|
|
bs_per_cp_group = 1
|
|
# get zigzag index
|
|
cp_segment_num = cp_size * 2
|
|
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(
|
|
range(
|
|
cp_segment_num - cp_rank - 1,
|
|
bs_per_cp_group * cp_segment_num,
|
|
cp_segment_num,
|
|
)
|
|
)
|
|
|
|
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)
|
|
for element in (split_list[i], split_list[cp_size * 2 - i - 1])
|
|
]
|
|
# get zigzag reverse index
|
|
cp_reverse_index = []
|
|
for batch_id in range(bs_per_cp_group):
|
|
cp_reverse_index.extend(
|
|
list(range(batch_id, cp_segment_num * bs_per_cp_group, 2 * bs_per_cp_group))
|
|
+ list(
|
|
range(
|
|
(cp_segment_num - 1) * bs_per_cp_group + batch_id,
|
|
0,
|
|
-2 * bs_per_cp_group,
|
|
)
|
|
)
|
|
)
|
|
prefix_sum_list = list(accumulate(split_list))
|
|
split_prefix_list = [0] + prefix_sum_list[:-1]
|
|
|
|
# TODO Support multi-batch-cp-split, multi-batch-cp support has accuracy issues
|
|
# cp_seq_index = calculate_cp_seq_idx(split_list[:], seqs_len[:])
|
|
kv_len_prev = prefix_sum_list[cp_rank]
|
|
kv_len_next = prefix_sum_list[cp_size * 2 - cp_rank - 1]
|
|
actual_seq_q_prev = split_list[cp_rank]
|
|
actual_seq_q_next = split_list[cp_size * 2 - cp_rank - 1]
|
|
kv_len_prev_tensor = torch.tensor(kv_len_prev).to(device="cuda", dtype=torch.int32)
|
|
kv_len_next_tensor = torch.tensor(kv_len_next).to(device="cuda", dtype=torch.int32)
|
|
actual_seq_q_prev_tensor = torch.tensor(actual_seq_q_prev).to(
|
|
device="cuda", dtype=torch.int32
|
|
)
|
|
actual_seq_q_next_tensor = torch.tensor(actual_seq_q_next).to(
|
|
device="cuda", dtype=torch.int32
|
|
)
|
|
actual_seq_q_prev_cu_tensor = torch.tensor(
|
|
[0, actual_seq_q_prev], device="cuda", dtype=torch.int32
|
|
)
|
|
actual_seq_q_next_cu_tensor = torch.tensor(
|
|
[0, actual_seq_q_next], device="cuda", dtype=torch.int32
|
|
)
|
|
|
|
nsa_cp_metadata = NSAContextParallelMetadata(
|
|
split_list=split_list,
|
|
split_list_tensor=torch.tensor(split_list, device="cuda", dtype=torch.int32),
|
|
split_prefix_tensor=torch.tensor(
|
|
split_prefix_list, device="cuda", dtype=torch.int32
|
|
),
|
|
max_rank_len=max_rank_len,
|
|
zigzag_index=zigzag_index,
|
|
per_rank_actual_token=per_rank_actual_token,
|
|
reverse_split_len=reverse_split_len,
|
|
cp_reverse_index=cp_reverse_index,
|
|
kv_len_prev=kv_len_prev,
|
|
kv_len_next=kv_len_next,
|
|
actual_seq_q_prev=actual_seq_q_prev,
|
|
actual_seq_q_next=actual_seq_q_next,
|
|
kv_len_prev_tensor=kv_len_prev_tensor,
|
|
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,
|
|
actual_seq_q_prev_cu_tensor=actual_seq_q_prev_cu_tensor,
|
|
actual_seq_q_next_cu_tensor=actual_seq_q_next_cu_tensor,
|
|
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,
|
|
extend_valid_tokens=page_split_info.extend_valid_tokens,
|
|
extend_padded_pages=page_split_info.extend_padded_pages,
|
|
extend_padded_tokens=page_split_info.extend_padded_tokens,
|
|
extend_padding_tokens=page_split_info.extend_padding_tokens,
|
|
)
|
|
return nsa_cp_metadata
|
|
|
|
|
|
def cp_collect_last_token_hidden(
|
|
hidden_states: torch.Tensor,
|
|
forward_batch: "ForwardBatch",
|
|
cp_size: int,
|
|
) -> torch.Tensor:
|
|
_fail_if_cp_shared_kv_round_robin(
|
|
forward_batch,
|
|
op="cp_collect_last_token_hidden",
|
|
)
|
|
if is_nsa_prefill_cp_round_robin_split():
|
|
return _round_robin_collect_last_token(hidden_states, forward_batch, cp_size)
|
|
return _in_seq_collect_last_token(hidden_states, forward_batch, cp_size)
|
|
|
|
|
|
def _round_robin_collect_last_token(
|
|
hidden_states: torch.Tensor,
|
|
forward_batch: "ForwardBatch",
|
|
cp_size: int,
|
|
) -> torch.Tensor:
|
|
cp_rank = get_attention_cp_rank()
|
|
bs = len(forward_batch.extend_seq_lens_cpu)
|
|
|
|
request_offsets = _prefix_offsets([int(x) for x in forward_batch.extend_seq_lens_cpu])
|
|
owners = []
|
|
local_offsets = []
|
|
for req_offset, req_len in zip(request_offsets, forward_batch.extend_seq_lens_cpu):
|
|
req_len = int(req_len)
|
|
if req_len <= 0:
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][batch_gt1_empty_extend] "
|
|
f"round-robin last-token collect got non-positive req_len={req_len}"
|
|
)
|
|
global_last = int(req_offset) + req_len - 1
|
|
owners.append(global_last % cp_size)
|
|
local_offsets.append(global_last // cp_size)
|
|
|
|
local_last = hidden_states.new_zeros((bs, hidden_states.shape[1]))
|
|
for req_id, (owner, local_offset) in enumerate(zip(owners, local_offsets)):
|
|
if cp_rank != owner:
|
|
continue
|
|
if local_offset >= int(hidden_states.shape[0]):
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][batch_gt1_last_token_oob] "
|
|
"round-robin last-token owner metadata points past local hidden. "
|
|
f"req_id={req_id} local_offset={local_offset} "
|
|
f"local_tokens={int(hidden_states.shape[0])}"
|
|
)
|
|
local_last[req_id] = hidden_states[local_offset]
|
|
|
|
gathered = hidden_states.new_empty((cp_size * bs, hidden_states.shape[1]))
|
|
attn_cp_all_gather_into_tensor(gathered, local_last)
|
|
gather_indices = torch.tensor(
|
|
[owner * bs + req_id for req_id, owner in enumerate(owners)],
|
|
device=hidden_states.device,
|
|
dtype=torch.long,
|
|
)
|
|
return gathered.index_select(0, gather_indices)
|
|
|
|
|
|
def _get_in_seq_last_token_owner_and_offset(
|
|
split_list: List[int],
|
|
cp_size: int,
|
|
actual_token_count: int,
|
|
) -> Tuple[int, int]:
|
|
"""Return the CP rank and local offset for the real last token.
|
|
|
|
In in-seq split, each rank owns two logical segments:
|
|
rank r gets segment r followed by segment (2 * cp_size - r - 1).
|
|
`split_list` is built from the padded model input length, while
|
|
`actual_token_count` is the non-padded extend length. The real last token
|
|
can therefore sit in a middle segment instead of rank 0's trailing segment.
|
|
"""
|
|
if cp_size <= 0:
|
|
raise ValueError(f"cp_size must be positive, got {cp_size}")
|
|
cp_segment_num = cp_size * 2
|
|
if len(split_list) != cp_segment_num:
|
|
raise ValueError(
|
|
"in-seq split_list length must equal 2 * cp_size, "
|
|
f"got len={len(split_list)} cp_size={cp_size}"
|
|
)
|
|
if actual_token_count <= 0:
|
|
raise ValueError(
|
|
f"actual_token_count must be positive, got {actual_token_count}"
|
|
)
|
|
|
|
total_split_tokens = sum(split_list)
|
|
if actual_token_count > total_split_tokens:
|
|
raise ValueError(
|
|
"actual_token_count exceeds split capacity: "
|
|
f"actual={actual_token_count} split_total={total_split_tokens}"
|
|
)
|
|
|
|
token_idx = actual_token_count - 1
|
|
segment_idx = 0
|
|
offset_in_segment = token_idx
|
|
for idx, segment_len in enumerate(split_list):
|
|
if offset_in_segment < segment_len:
|
|
segment_idx = idx
|
|
break
|
|
offset_in_segment -= segment_len
|
|
else:
|
|
raise ValueError(
|
|
"failed to locate last token in split_list: "
|
|
f"actual={actual_token_count} split_list={split_list}"
|
|
)
|
|
|
|
if segment_idx < cp_size:
|
|
return segment_idx, offset_in_segment
|
|
|
|
owner = cp_segment_num - segment_idx - 1
|
|
local_offset = split_list[owner] + offset_in_segment
|
|
return owner, local_offset
|
|
|
|
|
|
def _in_seq_collect_last_token(
|
|
hidden_states: torch.Tensor,
|
|
forward_batch: "ForwardBatch",
|
|
cp_size: int,
|
|
) -> torch.Tensor:
|
|
cp_rank = get_attention_cp_rank()
|
|
bs = len(forward_batch.extend_seq_lens_cpu)
|
|
metadata = getattr(forward_batch, "nsa_cp_metadata", None)
|
|
if get_cp_shared_kv_batch_plan(forward_batch) is not None or bs > 1:
|
|
return _in_seq_collect_last_token_batch(hidden_states, metadata, cp_size, cp_rank, bs)
|
|
|
|
owner = 0
|
|
local_offset = hidden_states.shape[0] - bs
|
|
|
|
if bs == 1 and metadata is not None:
|
|
actual_token_count = sum(int(x) for x in forward_batch.extend_seq_lens_cpu)
|
|
owner, local_offset = _get_in_seq_last_token_owner_and_offset(
|
|
split_list=metadata.split_list,
|
|
cp_size=cp_size,
|
|
actual_token_count=actual_token_count,
|
|
)
|
|
|
|
if cp_rank == owner and hidden_states.shape[0] >= local_offset + bs:
|
|
local_last = hidden_states[local_offset : local_offset + bs].contiguous()
|
|
else:
|
|
local_last = hidden_states.new_zeros((bs, hidden_states.shape[1]))
|
|
|
|
gathered = hidden_states.new_empty((cp_size * bs, hidden_states.shape[1]))
|
|
attn_cp_all_gather_into_tensor(gathered, local_last)
|
|
return gathered[owner * bs : owner * bs + bs]
|
|
|
|
|
|
def _in_seq_collect_last_token_batch(
|
|
hidden_states: torch.Tensor,
|
|
metadata: NSAContextParallelMetadata,
|
|
cp_size: int,
|
|
cp_rank: int,
|
|
bs: int,
|
|
) -> torch.Tensor:
|
|
if metadata is None:
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][batch_gt1_missing_metadata] "
|
|
"CP in-seq bs>1 last-token collection requires batch metadata."
|
|
)
|
|
plan = getattr(metadata, "batch_plan", None)
|
|
owners = _get_cp_last_token_metadata_list(
|
|
metadata, plan, "request_last_token_owner"
|
|
)
|
|
local_offsets = _get_cp_last_token_metadata_list(
|
|
metadata, plan, "request_last_token_local_offset"
|
|
)
|
|
compute_padding_enabled = bool(
|
|
getattr(metadata, "compute_padding_enabled", False)
|
|
or bool(getattr(plan, "compute_padding_enabled", False))
|
|
)
|
|
if compute_padding_enabled:
|
|
rank_offsets = _get_cp_last_token_metadata_list(
|
|
metadata, plan, "request_compute_rank_local_offsets"
|
|
)
|
|
else:
|
|
rank_offsets = _get_cp_last_token_metadata_list(
|
|
metadata, plan, "request_rank_local_offsets"
|
|
)
|
|
if (
|
|
owners is None
|
|
or local_offsets is None
|
|
or rank_offsets is None
|
|
or len(owners) != bs
|
|
or len(local_offsets) != bs
|
|
or len(rank_offsets) != bs
|
|
):
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][batch_gt1_missing_last_token_metadata] "
|
|
"CP in-seq bs>1 narrow output requires per-request owner, "
|
|
"local offset, and rank-local offset metadata."
|
|
)
|
|
|
|
local_last = hidden_states.new_zeros((bs, hidden_states.shape[1]))
|
|
for req_id, (owner, local_offset, rank_offset) in enumerate(
|
|
zip(owners, local_offsets, rank_offsets)
|
|
):
|
|
owner = int(owner)
|
|
if owner < 0 or owner >= cp_size:
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][batch_gt1_invalid_last_token_owner] "
|
|
f"req_id={req_id} owner={owner} cp_size={cp_size}"
|
|
)
|
|
if cp_rank != owner:
|
|
continue
|
|
global_local_offset = int(rank_offset) + int(local_offset)
|
|
if global_local_offset >= int(hidden_states.shape[0]):
|
|
raise RuntimeError(
|
|
"[CP_SHARED_KV_FAIL_FAST][batch_gt1_last_token_oob] "
|
|
"in-seq last-token metadata points past local hidden. "
|
|
f"req_id={req_id} owner={owner} rank_offset={rank_offset} "
|
|
f"local_offset={local_offset} local_tokens={int(hidden_states.shape[0])}"
|
|
)
|
|
local_last[req_id] = hidden_states[global_local_offset]
|
|
|
|
gathered = hidden_states.new_empty((cp_size * bs, hidden_states.shape[1]))
|
|
attn_cp_all_gather_into_tensor(gathered, local_last)
|
|
gather_indices = torch.tensor(
|
|
[int(owner) * bs + req_id for req_id, owner in enumerate(owners)],
|
|
device=hidden_states.device,
|
|
dtype=torch.long,
|
|
)
|
|
return gathered.index_select(0, gather_indices)
|
|
|
|
|
|
def _get_cp_last_token_metadata_list(
|
|
metadata: NSAContextParallelMetadata,
|
|
plan,
|
|
field_name: str,
|
|
):
|
|
value = getattr(metadata, field_name, None)
|
|
if value is not None:
|
|
return value
|
|
if plan is not None:
|
|
return getattr(plan, field_name, None)
|
|
return None
|