4390 lines
159 KiB
Python
4390 lines
159 KiB
Python
import ast
|
|
import os
|
|
from pathlib import Path
|
|
import unittest
|
|
import sys
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
from sglang.srt.environ import envs
|
|
from sglang.srt.layers.attention.nsa import utils as nsa_utils
|
|
from sglang.srt.layers.attention.nsa.utils import (
|
|
NSAContextParallelMetadata,
|
|
PageAlignedCacheExtent,
|
|
build_flat_page_owner_plan,
|
|
build_batch_page_aligned_in_seq_split_plan,
|
|
build_page_aligned_cache_extent,
|
|
_get_in_seq_last_token_owner_and_offset,
|
|
_build_batch_metadata_from_plan,
|
|
build_page_aligned_in_seq_split_list,
|
|
build_token_balanced_in_seq_split_list,
|
|
can_cp_split,
|
|
cp_all_gather_rerange_output,
|
|
cp_collect_last_token_hidden,
|
|
cp_split_and_rebuild_1d,
|
|
cp_split_and_rebuild_data,
|
|
cp_split_and_rebuild_position,
|
|
_torch_batch_in_seq_all_gather_rerange,
|
|
get_cp_shared_kv_batch_plan,
|
|
get_cp_shared_kv_local_out_cache_loc,
|
|
get_cp_shared_kv_local_physical_out_cache_loc,
|
|
get_cp_local_embedding_padded_token_count,
|
|
nsa_use_prefill_cp,
|
|
pad_cp_local_input_ids_for_embedding,
|
|
prepare_input_dp_with_cp_dsa,
|
|
select_cp_current_valid_rows_for_reuse,
|
|
select_cp_local_valid_rows_for_cache_write,
|
|
split_tensor_by_cp_batch_plan,
|
|
split_in_seq_cp_local_pair,
|
|
)
|
|
from sglang.srt.mem_cache.cp_shared_kv_layout import CpSharedKVLayout
|
|
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, ForwardMode
|
|
from sglang.srt.models.deepseek_nextn import DeepseekModelNextN
|
|
from sglang.srt.speculative.eagle_info import EagleDraftInput
|
|
from sglang.test.ci.ci_register import register_cpu_ci
|
|
|
|
register_cpu_ci(est_time=1, suite="stage-a-test-cpu")
|
|
|
|
|
|
class TestPageAlignedCacheExtent(unittest.TestCase):
|
|
def test_extent_uses_page_boundary_not_cp_size(self):
|
|
extent = build_page_aligned_cache_extent(valid_tokens=100, page_size=64)
|
|
|
|
self.assertEqual(extent.valid_tokens, 100)
|
|
self.assertEqual(extent.padded_pages, 2)
|
|
self.assertEqual(extent.padded_tokens, 128)
|
|
self.assertEqual(extent.padding_tokens, 28)
|
|
|
|
def test_extent_handles_empty_and_aligned_lengths(self):
|
|
self.assertEqual(
|
|
build_page_aligned_cache_extent(valid_tokens=0, page_size=64),
|
|
PageAlignedCacheExtent(
|
|
valid_tokens=0,
|
|
padded_pages=0,
|
|
padded_tokens=0,
|
|
padding_tokens=0,
|
|
),
|
|
)
|
|
self.assertEqual(
|
|
build_page_aligned_cache_extent(valid_tokens=128, page_size=64),
|
|
PageAlignedCacheExtent(
|
|
valid_tokens=128,
|
|
padded_pages=2,
|
|
padded_tokens=128,
|
|
padding_tokens=0,
|
|
),
|
|
)
|
|
|
|
|
|
class TestNSAInSeqCPUtils(unittest.TestCase):
|
|
def test_can_cp_split_ignores_decode_batch_without_extend_lens(self):
|
|
forward_batch = SimpleNamespace(
|
|
forward_mode=ForwardMode.DECODE,
|
|
extend_seq_lens_cpu=None,
|
|
uses_cp_shared_kv=True,
|
|
)
|
|
|
|
self.assertFalse(
|
|
can_cp_split(
|
|
seq_len=200, cp_size=8, use_nsa=True, forward_batch=forward_batch
|
|
)
|
|
)
|
|
|
|
def test_can_cp_split_fails_extend_batch_without_extend_lens(self):
|
|
forward_batch = SimpleNamespace(
|
|
forward_mode=ForwardMode.EXTEND,
|
|
extend_seq_lens_cpu=None,
|
|
uses_cp_shared_kv=True,
|
|
)
|
|
|
|
with self.assertRaisesRegex(RuntimeError, "missing_extend_seq_lens_cpu"):
|
|
can_cp_split(
|
|
seq_len=200, cp_size=8, use_nsa=True, forward_batch=forward_batch
|
|
)
|
|
|
|
def test_contiguous_valid_cp_query_count(self):
|
|
from sglang.srt.layers.attention.nsa.nsa_indexer import (
|
|
_compute_contiguous_valid_cp_query_count,
|
|
)
|
|
|
|
self.assertEqual(
|
|
_compute_contiguous_valid_cp_query_count(
|
|
cp_kv_end=1024,
|
|
actual_seq_q=128,
|
|
logical_kv_limit=1024,
|
|
),
|
|
128,
|
|
)
|
|
self.assertEqual(
|
|
_compute_contiguous_valid_cp_query_count(
|
|
cp_kv_end=1100,
|
|
actual_seq_q=128,
|
|
logical_kv_limit=1024,
|
|
),
|
|
52,
|
|
)
|
|
self.assertEqual(
|
|
_compute_contiguous_valid_cp_query_count(
|
|
cp_kv_end=1100,
|
|
actual_seq_q=64,
|
|
logical_kv_limit=1000,
|
|
),
|
|
0,
|
|
)
|
|
self.assertEqual(
|
|
_compute_contiguous_valid_cp_query_count(
|
|
cp_kv_end=100,
|
|
actual_seq_q=0,
|
|
logical_kv_limit=100,
|
|
),
|
|
0,
|
|
)
|
|
|
|
def assert_page_aligned_boundaries(
|
|
self, split_list, *, extend_prefix_len, extend_len, page_size
|
|
):
|
|
cursor = 0
|
|
for segment_len in split_list[:-1]:
|
|
cursor += segment_len
|
|
if cursor < extend_len:
|
|
self.assertEqual((extend_prefix_len + cursor) % page_size, 0)
|
|
|
|
def test_page_aligned_split_keeps_boundaries_on_pages(self):
|
|
split_list, info = build_page_aligned_in_seq_split_list(
|
|
total_len=32768,
|
|
extend_len=32768,
|
|
extend_prefix_len=0,
|
|
page_size=64,
|
|
cp_size=8,
|
|
)
|
|
|
|
self.assertTrue(info.page_aligned)
|
|
self.assertEqual(sum(split_list), 32768)
|
|
self.assertEqual(len(split_list), 16)
|
|
self.assertTrue(all(segment_len > 0 for segment_len in split_list))
|
|
self.assert_page_aligned_boundaries(
|
|
split_list, extend_prefix_len=0, extend_len=32768, page_size=64
|
|
)
|
|
|
|
def test_page_aligned_split_uses_prefix_for_boundary_alignment(self):
|
|
split_list, info = build_page_aligned_in_seq_split_list(
|
|
total_len=1024,
|
|
extend_len=1024,
|
|
extend_prefix_len=128,
|
|
page_size=64,
|
|
cp_size=8,
|
|
)
|
|
|
|
self.assertTrue(info.page_aligned)
|
|
self.assertEqual(sum(split_list), 1024)
|
|
self.assert_page_aligned_boundaries(
|
|
split_list, extend_prefix_len=128, extend_len=1024, page_size=64
|
|
)
|
|
|
|
def test_page_aligned_split_keeps_tail_partial_page_unsplit(self):
|
|
split_list, info = build_page_aligned_in_seq_split_list(
|
|
total_len=1100,
|
|
extend_len=1100,
|
|
extend_prefix_len=0,
|
|
page_size=64,
|
|
cp_size=8,
|
|
)
|
|
|
|
self.assertTrue(info.page_aligned)
|
|
self.assertEqual(sum(split_list), 1100)
|
|
self.assertEqual(split_list[-1], 12)
|
|
self.assert_page_aligned_boundaries(
|
|
split_list, extend_prefix_len=0, extend_len=1100, page_size=64
|
|
)
|
|
|
|
def test_page_aligned_split_exposes_padded_extent_without_padding_split_list(self):
|
|
split_list, info = build_page_aligned_in_seq_split_list(
|
|
total_len=100,
|
|
extend_len=100,
|
|
extend_prefix_len=0,
|
|
page_size=64,
|
|
cp_size=8,
|
|
)
|
|
|
|
self.assertTrue(info.page_aligned)
|
|
self.assertEqual(sum(split_list), 100)
|
|
self.assertEqual(split_list[:2], [64, 36])
|
|
self.assertEqual(split_list[2:], [0] * 14)
|
|
self.assertEqual(info.extend_valid_tokens, 100)
|
|
self.assertEqual(info.extend_padded_pages, 2)
|
|
self.assertEqual(info.extend_padded_tokens, 128)
|
|
self.assertEqual(info.extend_padding_tokens, 28)
|
|
|
|
def test_page_aligned_split_falls_back_when_prefix_is_not_page_aligned(self):
|
|
split_list, info = build_page_aligned_in_seq_split_list(
|
|
total_len=1024,
|
|
extend_len=1024,
|
|
extend_prefix_len=1,
|
|
page_size=64,
|
|
cp_size=8,
|
|
)
|
|
|
|
self.assertFalse(info.page_aligned)
|
|
self.assertEqual(split_list, build_token_balanced_in_seq_split_list(1024, 8))
|
|
|
|
def test_page_aligned_split_pads_zero_segments_when_page_units_are_short(self):
|
|
split_list, info = build_page_aligned_in_seq_split_list(
|
|
total_len=512,
|
|
extend_len=512,
|
|
extend_prefix_len=0,
|
|
page_size=64,
|
|
cp_size=8,
|
|
)
|
|
|
|
self.assertTrue(info.page_aligned)
|
|
self.assertEqual(sum(split_list), 512)
|
|
self.assertEqual(split_list[:8], [64] * 8)
|
|
self.assertEqual(split_list[8:], [0] * 8)
|
|
self.assert_page_aligned_boundaries(
|
|
split_list, extend_prefix_len=0, extend_len=512, page_size=64
|
|
)
|
|
|
|
def test_page_aligned_split_allows_radix_hit_suffix_with_one_page_per_rank(self):
|
|
split_list, info = build_page_aligned_in_seq_split_list(
|
|
total_len=512,
|
|
extend_len=512,
|
|
extend_prefix_len=54464,
|
|
page_size=64,
|
|
cp_size=8,
|
|
)
|
|
|
|
self.assertTrue(info.page_aligned)
|
|
self.assertEqual(sum(split_list), 512)
|
|
self.assertEqual(split_list[:8], [64] * 8)
|
|
self.assertEqual(split_list[8:], [0] * 8)
|
|
self.assert_page_aligned_boundaries(
|
|
split_list, extend_prefix_len=54464, extend_len=512, page_size=64
|
|
)
|
|
|
|
def test_page_aligned_split_keeps_short_radix_hit_suffix_page_aligned(self):
|
|
split_list, info = build_page_aligned_in_seq_split_list(
|
|
total_len=256,
|
|
extend_len=256,
|
|
extend_prefix_len=54464,
|
|
page_size=64,
|
|
cp_size=8,
|
|
)
|
|
|
|
self.assertTrue(info.page_aligned)
|
|
self.assertEqual(sum(split_list), 256)
|
|
self.assertEqual(split_list[:4], [64] * 4)
|
|
self.assertEqual(split_list[4:], [0] * 12)
|
|
self.assert_page_aligned_boundaries(
|
|
split_list, extend_prefix_len=54464, extend_len=256, page_size=64
|
|
)
|
|
|
|
def test_can_cp_split_uses_compute_padding_for_short_radix_hit_suffix(self):
|
|
class Mode:
|
|
def is_context_parallel_extend(self):
|
|
return True
|
|
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
extend_seq_lens_cpu=[65],
|
|
extend_prefix_lens_cpu=[54464],
|
|
token_to_kv_pool=SimpleNamespace(page_size=64),
|
|
forward_mode=Mode(),
|
|
)
|
|
|
|
with (
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
),
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_enable_prefill_cp",
|
|
return_value=True,
|
|
),
|
|
):
|
|
self.assertTrue(can_cp_split(128, 8, True, forward_batch))
|
|
|
|
def test_can_cp_split_uses_compute_padding_when_page_units_do_not_cover_all_lanes(
|
|
self,
|
|
):
|
|
class Mode:
|
|
def is_context_parallel_extend(self):
|
|
return True
|
|
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
extend_seq_lens_cpu=[256],
|
|
extend_prefix_lens_cpu=[54464],
|
|
token_to_kv_pool=SimpleNamespace(page_size=64),
|
|
forward_mode=Mode(),
|
|
)
|
|
|
|
with (
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
),
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_enable_prefill_cp",
|
|
return_value=True,
|
|
),
|
|
):
|
|
self.assertTrue(can_cp_split(256, 8, True, forward_batch))
|
|
|
|
def test_can_cp_split_uses_compute_padding_for_current_only_one_page_suffix(
|
|
self,
|
|
):
|
|
class Mode:
|
|
def is_context_parallel_extend(self):
|
|
return True
|
|
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
extend_seq_lens_cpu=[64],
|
|
extend_prefix_lens_cpu=[0],
|
|
token_to_kv_pool=SimpleNamespace(page_size=64),
|
|
forward_mode=Mode(),
|
|
)
|
|
|
|
with (
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
),
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_enable_prefill_cp",
|
|
return_value=True,
|
|
),
|
|
):
|
|
self.assertTrue(can_cp_split(64, 8, True, forward_batch))
|
|
|
|
def test_can_cp_split_enables_cp_draft_shared_kv_draft_extend(self):
|
|
class DraftMode:
|
|
def is_context_parallel_extend(self, include_draft_extend_v2=False):
|
|
return False
|
|
|
|
def is_draft_extend(self, include_v2=False):
|
|
return True
|
|
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
extend_seq_lens_cpu=[4],
|
|
extend_prefix_lens_cpu=[0],
|
|
token_to_kv_pool=SimpleNamespace(page_size=64),
|
|
forward_mode=DraftMode(),
|
|
)
|
|
|
|
with (
|
|
patch.dict(os.environ, {"SGLANG_CP_DRAFT_SHARED_KV": "1"}),
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
),
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_enable_prefill_cp",
|
|
return_value=True,
|
|
),
|
|
):
|
|
self.assertTrue(can_cp_split(8, 8, True, forward_batch))
|
|
|
|
def test_nsa_use_prefill_cp_enables_cp_draft_shared_kv_draft_extend(self):
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
forward_mode=ForwardMode.DRAFT_EXTEND,
|
|
nsa_cp_metadata=NSAContextParallelMetadata(batch_size=1),
|
|
)
|
|
|
|
with patch.dict(os.environ, {"SGLANG_CP_DRAFT_SHARED_KV": "1"}):
|
|
self.assertTrue(nsa_use_prefill_cp(forward_batch, True))
|
|
|
|
def test_can_cp_split_uses_compute_padding_per_request_for_batched_tiny_suffix(
|
|
self,
|
|
):
|
|
class Mode:
|
|
def is_context_parallel_extend(self):
|
|
return True
|
|
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
extend_seq_lens_cpu=[65, 64],
|
|
extend_prefix_lens_cpu=[54464, 8192],
|
|
token_to_kv_pool=SimpleNamespace(page_size=64),
|
|
forward_mode=Mode(),
|
|
)
|
|
|
|
with (
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
),
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_enable_prefill_cp",
|
|
return_value=True,
|
|
),
|
|
):
|
|
self.assertTrue(can_cp_split(129, 8, True, forward_batch))
|
|
|
|
def test_can_cp_split_fails_on_non_page_aligned_cp_shared_prefix(self):
|
|
class Mode:
|
|
def is_context_parallel_extend(self):
|
|
return True
|
|
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
extend_seq_lens_cpu=[1024],
|
|
extend_prefix_lens_cpu=[65],
|
|
token_to_kv_pool=SimpleNamespace(page_size=64),
|
|
forward_mode=Mode(),
|
|
)
|
|
|
|
with (
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
),
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_enable_prefill_cp",
|
|
return_value=True,
|
|
),
|
|
self.assertRaisesRegex(
|
|
RuntimeError,
|
|
r"\[CP_SHARED_KV_FAIL_FAST\]\[cp_split_non_page_aligned_prefix\]",
|
|
),
|
|
):
|
|
can_cp_split(1089, 8, True, forward_batch)
|
|
|
|
def test_can_cp_split_skips_page_plan_validator_for_target_verify(self):
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
extend_seq_lens_cpu=[64],
|
|
extend_prefix_lens_cpu=None,
|
|
token_to_kv_pool=SimpleNamespace(page_size=64),
|
|
forward_mode=ForwardMode.TARGET_VERIFY,
|
|
)
|
|
|
|
with (
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
),
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_enable_prefill_cp",
|
|
return_value=True,
|
|
),
|
|
):
|
|
self.assertFalse(can_cp_split(64, 8, True, forward_batch))
|
|
|
|
def test_can_cp_split_keeps_cp_for_radix_hit_suffix_with_one_page_per_rank(self):
|
|
class Mode:
|
|
def is_context_parallel_extend(self):
|
|
return True
|
|
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
extend_seq_lens_cpu=[512],
|
|
extend_prefix_lens_cpu=[54464],
|
|
token_to_kv_pool=SimpleNamespace(page_size=64),
|
|
forward_mode=Mode(),
|
|
)
|
|
|
|
with (
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
),
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_enable_prefill_cp",
|
|
return_value=True,
|
|
),
|
|
):
|
|
self.assertTrue(can_cp_split(512, 8, True, forward_batch))
|
|
|
|
def test_page_aligned_split_adds_padding_tokens_to_last_segment(self):
|
|
split_list, info = build_page_aligned_in_seq_split_list(
|
|
total_len=1040,
|
|
extend_len=1024,
|
|
extend_prefix_len=0,
|
|
page_size=64,
|
|
cp_size=8,
|
|
)
|
|
|
|
self.assertTrue(info.page_aligned)
|
|
self.assertEqual(sum(split_list), 1040)
|
|
self.assertEqual(split_list[-1], 80)
|
|
self.assert_page_aligned_boundaries(
|
|
split_list, extend_prefix_len=0, extend_len=1024, page_size=64
|
|
)
|
|
|
|
def test_last_token_owner_uses_actual_token_count_when_batch_is_padded(self):
|
|
# Padded prefill can have 64 model tokens while the real prompt has only
|
|
# 11 tokens. In in-seq split with cp_size=8, the real last token is in
|
|
# segment 2, not in rank 0's trailing padded segment.
|
|
split_list = [4] * 16
|
|
|
|
owner, local_offset = _get_in_seq_last_token_owner_and_offset(
|
|
split_list=split_list,
|
|
cp_size=8,
|
|
actual_token_count=11,
|
|
)
|
|
|
|
self.assertEqual(owner, 2)
|
|
self.assertEqual(local_offset, 2)
|
|
|
|
def test_last_token_owner_keeps_existing_unpadded_fast_path_location(self):
|
|
split_list = [4] * 16
|
|
|
|
owner, local_offset = _get_in_seq_last_token_owner_and_offset(
|
|
split_list=split_list,
|
|
cp_size=8,
|
|
actual_token_count=64,
|
|
)
|
|
|
|
self.assertEqual(owner, 0)
|
|
self.assertEqual(local_offset, 7)
|
|
|
|
def test_batch_page_aligned_plan_keeps_request_boundaries_and_last_token_owners(
|
|
self,
|
|
):
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[4, 9],
|
|
prefix_lens=[0, 8],
|
|
page_size=4,
|
|
cp_size=2,
|
|
cp_rank=0,
|
|
)
|
|
|
|
self.assertEqual(plan.batch_size, 2)
|
|
self.assertEqual(plan.request_split_lists, [[4, 0, 0, 0], [4, 4, 1, 0]])
|
|
self.assertEqual(plan.request_padded_pages, [1, 3])
|
|
self.assertEqual(plan.request_padded_tokens, [4, 12])
|
|
self.assertEqual(plan.request_token_offsets, [0, 4])
|
|
self.assertEqual(plan.request_padded_token_offsets, [0, 4])
|
|
self.assertEqual(plan.request_page_offsets, [0, 1])
|
|
self.assertEqual(plan.request_last_token_owner, [0, 1])
|
|
self.assertEqual(plan.request_last_token_local_offset, [3, 4])
|
|
self.assertEqual(plan.request_rank_local_tokens, [4, 4])
|
|
self.assertEqual(plan.request_rank_local_offsets, [0, 4])
|
|
self.assertEqual(plan.request_kv_len_prev, [4, 4])
|
|
self.assertEqual(plan.request_kv_len_next, [4, 9])
|
|
self.assertEqual(plan.request_actual_seq_q_prev, [4, 4])
|
|
self.assertEqual(plan.request_actual_seq_q_next, [0, 0])
|
|
self.assertEqual(plan.flat_segment_request_ids, [0, 0, 0, 0, 1, 1, 1, 1])
|
|
self.assertEqual(plan.flat_segment_offsets, [0, 4, 4, 4, 0, 4, 8, 9])
|
|
|
|
def test_batch_plan_exposes_compute_padding_without_inflating_valid_cache_extent(
|
|
self,
|
|
):
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[65],
|
|
prefix_lens=[40320],
|
|
page_size=64,
|
|
cp_size=8,
|
|
cp_rank=1,
|
|
)
|
|
|
|
self.assertTrue(plan.compute_padding_enabled)
|
|
self.assertEqual(
|
|
plan.request_valid_split_lists,
|
|
[[64, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
|
)
|
|
self.assertEqual(
|
|
plan.request_compute_split_lists,
|
|
[[64, 64, 64, 64, 64, 64, 64, 64, 0, 0, 0, 0, 0, 0, 0, 0]],
|
|
)
|
|
self.assertEqual(plan.request_valid_padded_pages, [2])
|
|
self.assertEqual(plan.request_valid_padded_tokens, [128])
|
|
self.assertEqual(plan.request_compute_padded_pages, [8])
|
|
self.assertEqual(plan.request_compute_padded_tokens, [512])
|
|
self.assertEqual(plan.request_compute_padding_tokens, [447])
|
|
self.assertEqual(plan.request_compute_rank_local_tokens, [64])
|
|
self.assertEqual(plan.request_compute_rank_local_offsets, [0])
|
|
self.assertEqual(plan.request_valid_rank_local_tokens, [1])
|
|
self.assertEqual(plan.request_valid_rank_local_offsets, [0])
|
|
self.assertEqual(plan.request_last_token_owner, [1])
|
|
self.assertEqual(plan.request_last_token_local_offset, [0])
|
|
|
|
# Compatibility aliases for cache/page accounting stay valid-token
|
|
# based. Query-length metadata exposes both valid and compute rows:
|
|
# consumers must choose the view that matches their actual q layout.
|
|
self.assertEqual(plan.request_split_lists, plan.request_valid_split_lists)
|
|
self.assertEqual(plan.request_padded_pages, plan.request_valid_padded_pages)
|
|
self.assertEqual(plan.request_actual_seq_q_prev, [64])
|
|
self.assertEqual(plan.request_actual_seq_q_next, [0])
|
|
self.assertEqual(plan.request_valid_seq_q_prev, [1])
|
|
self.assertEqual(plan.request_valid_seq_q_next, [0])
|
|
self.assertEqual(plan.request_compute_seq_q_prev, [64])
|
|
self.assertEqual(plan.request_compute_seq_q_next, [0])
|
|
|
|
def test_bs_gt1_debug_log_is_env_gated_and_limited(self):
|
|
nsa_utils._CP_SHARED_KV_BS_GT1_DEBUG_COUNTS.clear()
|
|
|
|
with envs.SGLANG_CP_SHARED_KV_BS_GT1_DEBUG.override(False):
|
|
with patch.object(nsa_utils.logger, "info") as info:
|
|
nsa_utils.log_cp_shared_kv_bs_gt1_debug(
|
|
"unit_test",
|
|
"bs=%s",
|
|
2,
|
|
)
|
|
self.assertFalse(info.called)
|
|
|
|
with envs.SGLANG_CP_SHARED_KV_BS_GT1_DEBUG.override(True):
|
|
with envs.SGLANG_CP_SHARED_KV_BS_GT1_DEBUG_LIMIT.override(1):
|
|
with patch.object(nsa_utils.logger, "info") as info:
|
|
nsa_utils.log_cp_shared_kv_bs_gt1_debug(
|
|
"unit_test",
|
|
"bs=%s",
|
|
2,
|
|
)
|
|
nsa_utils.log_cp_shared_kv_bs_gt1_debug(
|
|
"unit_test",
|
|
"bs=%s",
|
|
3,
|
|
)
|
|
self.assertEqual(info.call_count, 1)
|
|
self.assertIn(
|
|
"[CP_SHARED_KV_BS_GT1_DEBUG]",
|
|
info.call_args.args[0],
|
|
)
|
|
|
|
def test_index_topk_batch_lengths_follow_actual_q_rows_not_compute_alias(self):
|
|
import torch
|
|
|
|
from sglang.srt.layers.attention.nsa.nsa_indexer import (
|
|
_select_batch_topk_query_lengths,
|
|
)
|
|
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[40387],
|
|
prefix_lens=[0],
|
|
page_size=64,
|
|
cp_size=8,
|
|
cp_rank=0,
|
|
)
|
|
valid_local_rows = (
|
|
plan.request_valid_seq_q_prev[0] + plan.request_valid_seq_q_next[0]
|
|
)
|
|
compute_local_rows = (
|
|
plan.request_compute_seq_q_prev[0] + plan.request_compute_seq_q_next[0]
|
|
)
|
|
self.assertEqual(valid_local_rows, 4995)
|
|
self.assertEqual(compute_local_rows, valid_local_rows)
|
|
self.assertFalse(plan.compute_padding_enabled)
|
|
|
|
local = split_tensor_by_cp_batch_plan(
|
|
torch.arange(40387, dtype=torch.int64),
|
|
plan,
|
|
mode="1d",
|
|
)
|
|
self.assertEqual(local.numel(), valid_local_rows)
|
|
self.assertEqual(local[:2560].tolist(), list(range(2560)))
|
|
self.assertEqual(local[2560:4995].tolist(), list(range(37952, 40387)))
|
|
|
|
local_valid = select_cp_local_valid_rows_for_cache_write(
|
|
SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=1,
|
|
batch_plan=plan,
|
|
)
|
|
),
|
|
local,
|
|
)
|
|
self.assertEqual(local_valid.numel(), valid_local_rows)
|
|
self.assertEqual(local_valid.tolist(), local.tolist())
|
|
|
|
selected = _select_batch_topk_query_lengths(
|
|
cp_metadata=NSAContextParallelMetadata(batch_size=1, batch_plan=plan),
|
|
batch_plan=plan,
|
|
batch_size=1,
|
|
q_tokens=valid_local_rows,
|
|
weights_tokens=valid_local_rows,
|
|
)
|
|
|
|
self.assertFalse(selected.uses_compute_query_rows)
|
|
self.assertEqual(selected.request_seq_q_prev, plan.request_valid_seq_q_prev)
|
|
self.assertEqual(selected.request_seq_q_next, plan.request_valid_seq_q_next)
|
|
self.assertEqual(
|
|
selected.request_valid_seq_q_prev, plan.request_valid_seq_q_prev
|
|
)
|
|
self.assertEqual(
|
|
selected.request_valid_seq_q_next, plan.request_valid_seq_q_next
|
|
)
|
|
|
|
selected_compute_alias = _select_batch_topk_query_lengths(
|
|
cp_metadata=NSAContextParallelMetadata(batch_size=1, batch_plan=plan),
|
|
batch_plan=plan,
|
|
batch_size=1,
|
|
q_tokens=compute_local_rows,
|
|
weights_tokens=compute_local_rows,
|
|
)
|
|
|
|
self.assertFalse(selected_compute_alias.uses_compute_query_rows)
|
|
self.assertEqual(
|
|
selected_compute_alias.request_seq_q_prev, plan.request_compute_seq_q_prev
|
|
)
|
|
self.assertEqual(
|
|
selected_compute_alias.request_seq_q_next, plan.request_compute_seq_q_next
|
|
)
|
|
self.assertEqual(
|
|
selected_compute_alias.request_valid_seq_q_prev, plan.request_valid_seq_q_prev
|
|
)
|
|
self.assertEqual(
|
|
selected_compute_alias.request_valid_seq_q_next, plan.request_valid_seq_q_next
|
|
)
|
|
|
|
def test_batch_plan_keeps_long_page_tail_out_of_compute_padding(self):
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[40387],
|
|
prefix_lens=[0],
|
|
page_size=64,
|
|
cp_size=8,
|
|
cp_rank=0,
|
|
)
|
|
|
|
self.assertFalse(plan.compute_padding_enabled)
|
|
self.assertEqual(plan.request_valid_padded_pages, [632])
|
|
self.assertEqual(plan.request_valid_padded_tokens, [40448])
|
|
self.assertEqual(plan.request_compute_padded_pages, [632])
|
|
self.assertEqual(plan.request_compute_padded_tokens, [40387])
|
|
self.assertEqual(plan.request_compute_padding_tokens, [0])
|
|
self.assertEqual(
|
|
plan.request_compute_split_lists,
|
|
plan.request_valid_split_lists,
|
|
)
|
|
|
|
def test_batch_plan_compute_padding_only_pads_tiny_request_in_mixed_batch(self):
|
|
import torch
|
|
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[65, 40387],
|
|
prefix_lens=[40320, 0],
|
|
page_size=64,
|
|
cp_size=8,
|
|
cp_rank=1,
|
|
)
|
|
|
|
self.assertTrue(plan.compute_padding_enabled)
|
|
self.assertEqual(plan.request_valid_padded_pages, [2, 632])
|
|
self.assertEqual(plan.request_compute_padded_pages, [8, 632])
|
|
self.assertEqual(plan.request_compute_padded_tokens, [512, 40387])
|
|
self.assertEqual(plan.request_compute_padding_tokens, [447, 0])
|
|
self.assertEqual(
|
|
plan.request_compute_split_lists[1],
|
|
plan.request_valid_split_lists[1],
|
|
)
|
|
|
|
local = split_tensor_by_cp_batch_plan(
|
|
torch.arange(65 + 40387, dtype=torch.int64),
|
|
plan,
|
|
mode="1d",
|
|
)
|
|
self.assertEqual(local.numel(), sum(plan.request_compute_rank_local_tokens))
|
|
|
|
valid = select_cp_local_valid_rows_for_cache_write(
|
|
SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=2,
|
|
batch_plan=plan,
|
|
)
|
|
),
|
|
local,
|
|
)
|
|
self.assertEqual(valid.numel(), sum(plan.request_valid_rank_local_tokens))
|
|
|
|
def test_compute_padding_tiny_batch_valid_rows_are_not_suffix_maskable_for_moe(
|
|
self,
|
|
):
|
|
# Regression invariant for the warm-cache GSM8K bs>1 failure:
|
|
# compute padding gives every tiny request a fixed local page slot, but
|
|
# the valid rows inside those slots are per-request prefixes, not one
|
|
# contiguous tensor prefix. DeepEP/MoE top-k's scalar
|
|
# num_token_non_padded can only mask a suffix, so it cannot represent
|
|
# this CP-local row layout.
|
|
import torch
|
|
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[29, 9, 60, 9, 17],
|
|
prefix_lens=[704, 704, 640, 704, 704],
|
|
page_size=64,
|
|
cp_size=8,
|
|
cp_rank=0,
|
|
)
|
|
|
|
compute_rows = sum(plan.request_compute_rank_local_tokens)
|
|
valid_rows = sum(plan.request_valid_rank_local_tokens)
|
|
self.assertTrue(plan.compute_padding_enabled)
|
|
self.assertEqual(plan.request_compute_rank_local_tokens, [64] * 5)
|
|
self.assertEqual(plan.request_valid_rank_local_tokens, [29, 9, 60, 9, 17])
|
|
self.assertEqual(compute_rows, 320)
|
|
self.assertEqual(valid_rows, 124)
|
|
|
|
valid_mask = torch.zeros(compute_rows, dtype=torch.bool)
|
|
for req_offset, spans in zip(
|
|
plan.request_compute_rank_local_offsets,
|
|
plan.request_valid_query_row_spans,
|
|
):
|
|
for start, end in spans:
|
|
if end > start:
|
|
valid_mask[req_offset + start : req_offset + end] = True
|
|
|
|
# A suffix-padding scalar would keep rows [0, valid_rows) and mask the
|
|
# rest. The CP layout has dummy page-tail rows inside that prefix and
|
|
# later valid rows after it.
|
|
scalar_prefix_mask = torch.arange(compute_rows) < valid_rows
|
|
self.assertFalse(torch.equal(valid_mask, scalar_prefix_mask))
|
|
self.assertGreater(int((scalar_prefix_mask & ~valid_mask).sum().item()), 0)
|
|
self.assertGreater(int((valid_mask & ~scalar_prefix_mask).sum().item()), 0)
|
|
|
|
def test_compute_padding_non_owner_rank_scalar_non_padded_would_unmask_dummy_rows(
|
|
self,
|
|
):
|
|
# Same regression shape on a non-owner CP rank: the local tensor has
|
|
# compute rows but no valid rows. Reusing the global input-token count
|
|
# as num_token_non_padded would route dummy rows through MoE.
|
|
extend_lens = [29, 9, 60, 9, 17]
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=extend_lens,
|
|
prefix_lens=[704, 704, 640, 704, 704],
|
|
page_size=64,
|
|
cp_size=8,
|
|
cp_rank=1,
|
|
)
|
|
|
|
compute_rows = sum(plan.request_compute_rank_local_tokens)
|
|
valid_rows = sum(plan.request_valid_rank_local_tokens)
|
|
global_num_token_non_padded = sum(extend_lens)
|
|
|
|
self.assertTrue(plan.compute_padding_enabled)
|
|
self.assertEqual(plan.request_compute_rank_local_tokens, [64] * 5)
|
|
self.assertEqual(plan.request_valid_rank_local_tokens, [0] * 5)
|
|
self.assertEqual(compute_rows, 320)
|
|
self.assertEqual(valid_rows, 0)
|
|
self.assertGreater(global_num_token_non_padded, valid_rows)
|
|
|
|
def test_batch_plan_compute_padding_is_per_request_not_batch_total(self):
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[65, 1024],
|
|
prefix_lens=[40320, 8192],
|
|
page_size=64,
|
|
cp_size=8,
|
|
cp_rank=0,
|
|
)
|
|
|
|
self.assertTrue(plan.compute_padding_enabled)
|
|
self.assertEqual(plan.request_valid_padded_pages, [2, 16])
|
|
self.assertEqual(plan.request_compute_padded_pages, [8, 16])
|
|
self.assertEqual(plan.request_compute_padded_tokens, [512, 1024])
|
|
self.assertEqual(plan.request_compute_padding_tokens, [447, 0])
|
|
self.assertEqual(plan.request_compute_rank_local_tokens, [64, 128])
|
|
self.assertEqual(plan.request_compute_rank_local_offsets, [0, 64])
|
|
self.assertEqual(plan.request_valid_rank_local_tokens, [64, 128])
|
|
self.assertEqual(plan.request_valid_rank_local_offsets, [0, 64])
|
|
self.assertEqual(plan.request_last_token_owner, [1, 0])
|
|
|
|
def test_batch_plan_stable_helpers_split_and_build_page_owner_plan(self):
|
|
import torch
|
|
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[4, 9],
|
|
prefix_lens=[0, 8],
|
|
page_size=4,
|
|
cp_size=2,
|
|
cp_rank=1,
|
|
)
|
|
forward_batch = SimpleNamespace(nsa_cp_metadata=SimpleNamespace(batch_plan=plan))
|
|
|
|
self.assertIs(get_cp_shared_kv_batch_plan(forward_batch), plan)
|
|
self.assertEqual(build_flat_page_owner_plan(plan), [0, 0, 1, 1])
|
|
|
|
local_1d = split_tensor_by_cp_batch_plan(torch.arange(13), plan, mode="1d")
|
|
self.assertEqual(local_1d.tolist(), [0, 0, 0, 0, 8, 9, 10, 11, 12])
|
|
|
|
local_data = split_tensor_by_cp_batch_plan(
|
|
torch.arange(13 * 2).view(13, 2), plan, mode="data"
|
|
)
|
|
self.assertEqual(
|
|
local_data[:, 0].tolist(),
|
|
[0, 0, 0, 0, 16, 18, 20, 22, 24],
|
|
)
|
|
|
|
def test_collect_last_token_hidden_uses_batch_owner_metadata(self):
|
|
import torch
|
|
|
|
hidden_states = torch.tensor(
|
|
[[10.0], [11.0], [12.0], [13.0], [20.0], [21.0], [22.0], [23.0]]
|
|
)
|
|
forward_batch = SimpleNamespace(
|
|
extend_seq_lens_cpu=[4, 9],
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=2,
|
|
request_last_token_owner=[0, 1],
|
|
request_last_token_local_offset=[3, 4],
|
|
request_rank_local_offsets=[0, 4],
|
|
),
|
|
)
|
|
|
|
def fake_all_gather(output, local_last):
|
|
self.assertEqual(local_last.tolist(), [[13.0], [0.0]])
|
|
output.copy_(torch.tensor([[13.0], [0.0], [0.0], [99.0]]))
|
|
|
|
with (
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
),
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.get_attention_cp_rank",
|
|
return_value=0,
|
|
),
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.attn_cp_all_gather_into_tensor",
|
|
side_effect=fake_all_gather,
|
|
),
|
|
):
|
|
collected = cp_collect_last_token_hidden(hidden_states, forward_batch, 2)
|
|
|
|
self.assertEqual(collected.tolist(), [[13.0], [99.0]])
|
|
|
|
def test_collect_last_token_hidden_uses_compute_padding_for_single_request(self):
|
|
import torch
|
|
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[65],
|
|
prefix_lens=[40320],
|
|
page_size=64,
|
|
cp_size=8,
|
|
cp_rank=1,
|
|
)
|
|
hidden_states = torch.zeros((64, 1), dtype=torch.float32)
|
|
hidden_states[0] = 123.0
|
|
hidden_states[1] = 999.0
|
|
forward_batch = SimpleNamespace(
|
|
extend_seq_lens_cpu=[65],
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=1,
|
|
batch_plan=plan,
|
|
),
|
|
)
|
|
|
|
def fake_all_gather(output, local_last):
|
|
self.assertEqual(local_last.tolist(), [[123.0]])
|
|
output.zero_()
|
|
output[1] = local_last[0]
|
|
|
|
with (
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
),
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.get_attention_cp_rank",
|
|
return_value=1,
|
|
),
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.attn_cp_all_gather_into_tensor",
|
|
side_effect=fake_all_gather,
|
|
),
|
|
):
|
|
collected = cp_collect_last_token_hidden(hidden_states, forward_batch, 8)
|
|
|
|
self.assertEqual(collected.tolist(), [[123.0]])
|
|
|
|
def test_collect_last_token_hidden_uses_compute_rank_offsets_for_batch(self):
|
|
import torch
|
|
|
|
hidden_states = torch.zeros((8, 1), dtype=torch.float32)
|
|
hidden_states[0] = 10.0
|
|
hidden_states[1] = 99.0
|
|
hidden_states[4] = 20.0
|
|
forward_batch = SimpleNamespace(
|
|
extend_seq_lens_cpu=[5, 5],
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=2,
|
|
request_last_token_owner=[1, 1],
|
|
request_last_token_local_offset=[0, 0],
|
|
request_rank_local_offsets=[0, 1],
|
|
request_compute_rank_local_offsets=[0, 4],
|
|
compute_padding_enabled=True,
|
|
),
|
|
)
|
|
|
|
def fake_all_gather(output, local_last):
|
|
self.assertEqual(local_last.tolist(), [[10.0], [20.0]])
|
|
output.copy_(torch.tensor([[0.0], [0.0], [10.0], [20.0]]))
|
|
|
|
with (
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
),
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.get_attention_cp_rank",
|
|
return_value=1,
|
|
),
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.attn_cp_all_gather_into_tensor",
|
|
side_effect=fake_all_gather,
|
|
),
|
|
):
|
|
collected = cp_collect_last_token_hidden(hidden_states, forward_batch, 2)
|
|
|
|
self.assertEqual(collected.tolist(), [[10.0], [20.0]])
|
|
|
|
def test_collect_last_token_hidden_matches_parallel20_tiny_extend_layout(self):
|
|
import torch
|
|
|
|
# Regression shape from the parallel=20 GSM8K warm-cache probe:
|
|
# tiny extend requests are compute-padded to one page per request.
|
|
# All real current tokens and all last tokens are owned by CP0, while
|
|
# the local hidden rows remain laid out as fixed 64-row request slots.
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[19, 21, 61, 33],
|
|
prefix_lens=[704, 704, 640, 704],
|
|
page_size=64,
|
|
cp_size=8,
|
|
cp_rank=0,
|
|
)
|
|
self.assertEqual(plan.request_last_token_owner, [0, 0, 0, 0])
|
|
self.assertEqual(plan.request_last_token_local_offset, [18, 20, 60, 32])
|
|
self.assertEqual(plan.request_compute_rank_local_offsets, [0, 64, 128, 192])
|
|
|
|
hidden_states = torch.zeros((256, 1), dtype=torch.float32)
|
|
expected = [[100.0], [200.0], [300.0], [400.0]]
|
|
for value, rank_offset, local_offset in zip(
|
|
[100.0, 200.0, 300.0, 400.0],
|
|
plan.request_compute_rank_local_offsets,
|
|
plan.request_last_token_local_offset,
|
|
):
|
|
hidden_states[rank_offset + local_offset] = value
|
|
|
|
forward_batch = SimpleNamespace(
|
|
extend_seq_lens_cpu=[19, 21, 61, 33],
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=4,
|
|
batch_plan=plan,
|
|
),
|
|
)
|
|
|
|
def fake_all_gather(output, local_last):
|
|
self.assertEqual(local_last.tolist(), expected)
|
|
output.zero_()
|
|
output[:4] = local_last
|
|
|
|
with (
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
),
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.get_attention_cp_rank",
|
|
return_value=0,
|
|
),
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.attn_cp_all_gather_into_tensor",
|
|
side_effect=fake_all_gather,
|
|
),
|
|
):
|
|
collected = cp_collect_last_token_hidden(hidden_states, forward_batch, 8)
|
|
|
|
self.assertEqual(collected.tolist(), expected)
|
|
|
|
def test_collect_last_token_hidden_fails_fast_without_batch_owner_metadata(self):
|
|
import torch
|
|
|
|
forward_batch = SimpleNamespace(
|
|
extend_seq_lens_cpu=[4, 9],
|
|
nsa_cp_metadata=NSAContextParallelMetadata(batch_size=2),
|
|
)
|
|
|
|
with (
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
),
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.get_attention_cp_rank",
|
|
return_value=0,
|
|
),
|
|
self.assertRaisesRegex(
|
|
RuntimeError,
|
|
r"\[CP_SHARED_KV_FAIL_FAST\]\[batch_gt1_missing_last_token_metadata\]",
|
|
),
|
|
):
|
|
cp_collect_last_token_hidden(torch.zeros((8, 1)), forward_batch, 2)
|
|
|
|
def test_deepseek_nextn_cp_draft_bs_gt1_fails_fast_on_hidden_shape_fallback(
|
|
self,
|
|
):
|
|
import torch
|
|
|
|
model = DeepseekModelNextN.__new__(DeepseekModelNextN)
|
|
model._debug_cp_draft_shared_kv = lambda _message: None
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
batch_size=2,
|
|
extend_seq_lens_cpu=[4, 9],
|
|
nsa_cp_metadata=NSAContextParallelMetadata(batch_size=2),
|
|
)
|
|
|
|
with (
|
|
patch.dict(os.environ, {"SGLANG_CP_DRAFT_SHARED_KV": "1"}),
|
|
patch(
|
|
"sglang.srt.models.deepseek_nextn.get_attention_cp_rank",
|
|
return_value=0,
|
|
),
|
|
patch(
|
|
"sglang.srt.models.deepseek_nextn.get_attention_cp_size",
|
|
return_value=8,
|
|
),
|
|
self.assertRaisesRegex(
|
|
RuntimeError,
|
|
r"\[CP_SHARED_KV_FAIL_FAST\]\[draft_batch_gt1_spec_hidden_shape_mismatch\]",
|
|
),
|
|
):
|
|
model._get_cp_local_spec_hidden_states(
|
|
forward_batch,
|
|
torch.zeros((3, 2)),
|
|
full_num_tokens=13,
|
|
local_num_tokens=8,
|
|
)
|
|
|
|
def test_cp_draft_padding_keeps_local_hidden_when_static_tokens_are_shorter(self):
|
|
import torch
|
|
|
|
class FakeAttnBackend:
|
|
def get_cuda_graph_seq_len_fill_value(self):
|
|
return 0
|
|
|
|
model_runner = SimpleNamespace(attn_backend=FakeAttnBackend())
|
|
spec_info = EagleDraftInput(
|
|
hidden_states=torch.ones((64, 2), dtype=torch.float32),
|
|
verified_id=torch.tensor([1], dtype=torch.int64),
|
|
num_tokens_per_req=1,
|
|
num_tokens_for_logprob_per_req=1,
|
|
cp_local_hidden_states=True,
|
|
)
|
|
forward_batch = ForwardBatch(
|
|
forward_mode=ForwardMode.DRAFT_EXTEND,
|
|
batch_size=1,
|
|
input_ids=torch.arange(8, dtype=torch.int64),
|
|
req_pool_indices=torch.tensor([0], dtype=torch.int64),
|
|
seq_lens=torch.tensor([4], dtype=torch.int32),
|
|
out_cache_loc=torch.arange(8, dtype=torch.int64),
|
|
seq_lens_sum=4,
|
|
positions=torch.arange(8, dtype=torch.int64),
|
|
lora_ids=[None],
|
|
spec_info=spec_info,
|
|
uses_cp_shared_kv=True,
|
|
)
|
|
|
|
with patch.dict(os.environ, {"SGLANG_CP_DRAFT_SHARED_KV": "1"}):
|
|
forward_batch._pad_inputs_to_size(model_runner, num_tokens=8, bs=1)
|
|
|
|
self.assertEqual(tuple(forward_batch.spec_info.hidden_states.shape), (64, 2))
|
|
self.assertTrue(
|
|
torch.equal(
|
|
forward_batch.hidden_states_backup,
|
|
torch.ones((64, 2), dtype=torch.float32),
|
|
)
|
|
)
|
|
|
|
def test_cp_draft_padding_keeps_marked_cp_local_hidden_before_cp_flags_are_visible(
|
|
self,
|
|
):
|
|
import torch
|
|
|
|
class FakeAttnBackend:
|
|
def get_cuda_graph_seq_len_fill_value(self):
|
|
return 0
|
|
|
|
model_runner = SimpleNamespace(attn_backend=FakeAttnBackend())
|
|
spec_info = EagleDraftInput(
|
|
hidden_states=torch.ones((64, 2), dtype=torch.float32),
|
|
verified_id=torch.tensor([1], dtype=torch.int64),
|
|
num_tokens_per_req=1,
|
|
num_tokens_for_logprob_per_req=1,
|
|
cp_local_hidden_states=True,
|
|
)
|
|
forward_batch = ForwardBatch(
|
|
forward_mode=ForwardMode.DRAFT_EXTEND,
|
|
batch_size=1,
|
|
input_ids=torch.arange(8, dtype=torch.int64),
|
|
req_pool_indices=torch.tensor([0], dtype=torch.int64),
|
|
seq_lens=torch.tensor([4], dtype=torch.int32),
|
|
out_cache_loc=torch.arange(8, dtype=torch.int64),
|
|
seq_lens_sum=4,
|
|
positions=torch.arange(8, dtype=torch.int64),
|
|
lora_ids=[None],
|
|
spec_info=spec_info,
|
|
uses_cp_shared_kv=False,
|
|
)
|
|
|
|
forward_batch._pad_inputs_to_size(model_runner, num_tokens=8, bs=1)
|
|
|
|
self.assertEqual(tuple(forward_batch.spec_info.hidden_states.shape), (64, 2))
|
|
self.assertTrue(
|
|
torch.equal(
|
|
forward_batch.hidden_states_backup,
|
|
torch.ones((64, 2), dtype=torch.float32),
|
|
)
|
|
)
|
|
|
|
def test_cp_draft_padding_keeps_marked_cp_local_hidden_after_forward_mode_rewrite(
|
|
self,
|
|
):
|
|
import torch
|
|
|
|
class FakeAttnBackend:
|
|
def get_cuda_graph_seq_len_fill_value(self):
|
|
return 0
|
|
|
|
model_runner = SimpleNamespace(attn_backend=FakeAttnBackend())
|
|
spec_info = EagleDraftInput(
|
|
hidden_states=torch.ones((64, 2), dtype=torch.float32),
|
|
verified_id=torch.tensor([1], dtype=torch.int64),
|
|
num_tokens_per_req=1,
|
|
num_tokens_for_logprob_per_req=1,
|
|
cp_local_hidden_states=True,
|
|
)
|
|
forward_batch = ForwardBatch(
|
|
# prepare_mlp_sync_batch can temporarily rewrite draft extend to
|
|
# EXTEND while static DP padding is being prepared. The draft
|
|
# side-channel contract must therefore be carried by spec_info, not
|
|
# inferred from forward_mode.
|
|
forward_mode=ForwardMode.EXTEND,
|
|
batch_size=1,
|
|
input_ids=torch.arange(8, dtype=torch.int64),
|
|
req_pool_indices=torch.tensor([0], dtype=torch.int64),
|
|
seq_lens=torch.tensor([4], dtype=torch.int32),
|
|
out_cache_loc=torch.arange(8, dtype=torch.int64),
|
|
seq_lens_sum=4,
|
|
positions=torch.arange(8, dtype=torch.int64),
|
|
lora_ids=[None],
|
|
spec_info=spec_info,
|
|
uses_cp_shared_kv=True,
|
|
)
|
|
|
|
forward_batch._pad_inputs_to_size(model_runner, num_tokens=8, bs=1)
|
|
|
|
self.assertEqual(tuple(forward_batch.spec_info.hidden_states.shape), (64, 2))
|
|
self.assertTrue(
|
|
torch.equal(
|
|
forward_batch.hidden_states_backup,
|
|
torch.ones((64, 2), dtype=torch.float32),
|
|
)
|
|
)
|
|
|
|
def test_cp_draft_padding_rejects_unmarked_oversized_hidden(self):
|
|
import torch
|
|
|
|
class FakeAttnBackend:
|
|
def get_cuda_graph_seq_len_fill_value(self):
|
|
return 0
|
|
|
|
model_runner = SimpleNamespace(attn_backend=FakeAttnBackend())
|
|
spec_info = EagleDraftInput(
|
|
hidden_states=torch.ones((64, 2), dtype=torch.float32),
|
|
verified_id=torch.tensor([1], dtype=torch.int64),
|
|
num_tokens_per_req=1,
|
|
num_tokens_for_logprob_per_req=1,
|
|
)
|
|
forward_batch = ForwardBatch(
|
|
forward_mode=ForwardMode.DRAFT_EXTEND,
|
|
batch_size=1,
|
|
input_ids=torch.arange(8, dtype=torch.int64),
|
|
req_pool_indices=torch.tensor([0], dtype=torch.int64),
|
|
seq_lens=torch.tensor([4], dtype=torch.int32),
|
|
out_cache_loc=torch.arange(8, dtype=torch.int64),
|
|
seq_lens_sum=4,
|
|
positions=torch.arange(8, dtype=torch.int64),
|
|
lora_ids=[None],
|
|
spec_info=spec_info,
|
|
uses_cp_shared_kv=False,
|
|
)
|
|
|
|
with self.assertRaisesRegex(
|
|
RuntimeError,
|
|
r"\[CP_SHARED_KV_FAIL_FAST\]\[draft_hidden_static_padding_mismatch\]",
|
|
):
|
|
forward_batch._pad_inputs_to_size(model_runner, num_tokens=8, bs=1)
|
|
|
|
def test_full_rerange_fails_fast_for_batch_metadata(self):
|
|
import torch
|
|
|
|
forward_batch = SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(batch_size=2)
|
|
)
|
|
|
|
with self.assertRaisesRegex(
|
|
RuntimeError,
|
|
r"\[CP_SHARED_KV_FAIL_FAST\]\[batch_gt1_full_rerange_unsupported\]",
|
|
):
|
|
cp_all_gather_rerange_output(
|
|
torch.zeros((8, 1)), 2, forward_batch, stream=None
|
|
)
|
|
|
|
def test_cp_shared_kv_prepare_rejects_round_robin_mode(self):
|
|
class Mode:
|
|
def is_context_parallel_extend(self):
|
|
return True
|
|
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
extend_seq_lens_cpu=[128],
|
|
extend_prefix_lens_cpu=[0],
|
|
token_to_kv_pool=SimpleNamespace(page_size=64),
|
|
forward_mode=Mode(),
|
|
)
|
|
|
|
with (
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=True,
|
|
),
|
|
self.assertRaisesRegex(
|
|
RuntimeError,
|
|
r"\[CP_SHARED_KV_FAIL_FAST\]\[round_robin_unsupported\]",
|
|
),
|
|
):
|
|
prepare_input_dp_with_cp_dsa(
|
|
128,
|
|
cp_rank=0,
|
|
cp_size=2,
|
|
seqs_len=[128],
|
|
forward_batch=forward_batch,
|
|
page_size=64,
|
|
)
|
|
|
|
def test_cp_shared_kv_prepare_uses_batch_plan_for_bs1_compute_padding(self):
|
|
class Mode:
|
|
def is_context_parallel_extend(self):
|
|
return True
|
|
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
extend_seq_lens_cpu=[65],
|
|
extend_prefix_lens_cpu=[0],
|
|
token_to_kv_pool=SimpleNamespace(page_size=64),
|
|
forward_mode=Mode(),
|
|
)
|
|
|
|
with patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
):
|
|
metadata = prepare_input_dp_with_cp_dsa(
|
|
65,
|
|
cp_rank=1,
|
|
cp_size=8,
|
|
seqs_len=[65],
|
|
forward_batch=forward_batch,
|
|
page_size=64,
|
|
)
|
|
|
|
self.assertIsNotNone(metadata.batch_plan)
|
|
self.assertEqual(metadata.batch_size, 1)
|
|
self.assertTrue(metadata.compute_padding_enabled)
|
|
self.assertEqual(
|
|
metadata.request_valid_split_lists,
|
|
[[64, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
|
)
|
|
self.assertEqual(
|
|
metadata.request_compute_split_lists,
|
|
[[64, 64, 64, 64, 64, 64, 64, 64, 0, 0, 0, 0, 0, 0, 0, 0]],
|
|
)
|
|
self.assertEqual(metadata.split_list, metadata.request_compute_split_lists[0])
|
|
self.assertEqual(metadata.max_rank_len, [64] * 8)
|
|
self.assertEqual(metadata.per_rank_actual_token, [64] * 8)
|
|
self.assertEqual(metadata.actual_seq_q_prev, 64)
|
|
self.assertEqual(metadata.actual_seq_q_next, 0)
|
|
self.assertEqual(metadata.request_valid_seq_q_prev, [1])
|
|
self.assertEqual(metadata.request_valid_seq_q_next, [0])
|
|
|
|
def test_cp_shared_kv_all_gather_rejects_round_robin_mode(self):
|
|
import torch
|
|
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
split_list=[2, 2, 2, 2],
|
|
zigzag_index=[0, 3],
|
|
reverse_split_len=[2, 2, 2, 2],
|
|
cp_reverse_index=[0, 2, 3, 1],
|
|
total_seq_lens=torch.tensor(8),
|
|
),
|
|
)
|
|
|
|
with (
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=True,
|
|
),
|
|
self.assertRaisesRegex(
|
|
RuntimeError,
|
|
r"\[CP_SHARED_KV_FAIL_FAST\]\[round_robin_unsupported\]",
|
|
),
|
|
):
|
|
cp_all_gather_rerange_output(
|
|
torch.zeros((4, 1)), 2, forward_batch, stream=None
|
|
)
|
|
|
|
def test_batch_in_seq_all_gather_rerange_restores_request_order_bf16(self):
|
|
import torch
|
|
|
|
cp_size = 2
|
|
request_split_lists = [
|
|
[2, 1, 3, 0],
|
|
[1, 2, 0, 1],
|
|
]
|
|
input_tensor_all, expected = self._build_batch_rerange_case(
|
|
cp_size=cp_size,
|
|
request_split_lists=request_split_lists,
|
|
row_width=3,
|
|
dtype=torch.bfloat16,
|
|
)
|
|
forward_batch = SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=2,
|
|
request_split_lists=request_split_lists,
|
|
max_rank_len=[6, 6],
|
|
)
|
|
)
|
|
|
|
actual = _torch_batch_in_seq_all_gather_rerange(
|
|
input_tensor_all,
|
|
forward_batch,
|
|
cp_size=cp_size,
|
|
)
|
|
|
|
self.assertEqual(actual.dtype, torch.bfloat16)
|
|
self.assertTrue(torch.equal(actual, expected))
|
|
|
|
def test_batch_in_seq_all_gather_rerange_treats_fp8_payload_as_opaque_rows(self):
|
|
import torch
|
|
|
|
cp_size = 2
|
|
request_split_lists = [
|
|
[1, 2, 1, 0],
|
|
[2, 0, 1, 1],
|
|
]
|
|
input_tensor_all, expected = self._build_batch_rerange_case(
|
|
cp_size=cp_size,
|
|
request_split_lists=request_split_lists,
|
|
row_width=5,
|
|
dtype=torch.uint8,
|
|
)
|
|
forward_batch = SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=2,
|
|
request_split_lists=request_split_lists,
|
|
max_rank_len=[4, 4],
|
|
)
|
|
)
|
|
|
|
actual = _torch_batch_in_seq_all_gather_rerange(
|
|
input_tensor_all,
|
|
forward_batch,
|
|
cp_size=cp_size,
|
|
)
|
|
|
|
self.assertEqual(actual.dtype, torch.uint8)
|
|
self.assertTrue(torch.equal(actual, expected))
|
|
|
|
def test_batch_in_seq_all_gather_rerange_uses_compute_offsets_for_padded_source(self):
|
|
import torch
|
|
|
|
cp_size = 2
|
|
# Request 0 is a tiny suffix: valid output only has segment 0, but the
|
|
# rank-major source payload contains synthetic compute-padding rows in
|
|
# the rank-local mirror segment. Request 1 follows it on the same rank.
|
|
# Source offsets must therefore be computed from compute splits, while
|
|
# output rows must still be restored from valid splits only.
|
|
valid_split_lists = [
|
|
[1, 0, 0, 0],
|
|
[2, 0, 1, 0],
|
|
]
|
|
compute_split_lists = [
|
|
[1, 1, 1, 1],
|
|
[2, 0, 1, 0],
|
|
]
|
|
row_width = 2
|
|
max_rank_token = 4
|
|
input_tensor_all = torch.zeros((max_rank_token * cp_size, row_width))
|
|
|
|
# Build source rank-major payload by compute split. Values 900+ are
|
|
# synthetic padding rows and must never appear in the restored output.
|
|
req0_seg0 = torch.tensor([[10.0, 11.0]])
|
|
req0_seg1_pad = torch.tensor([[900.0, 901.0]])
|
|
req0_seg2_pad = torch.tensor([[902.0, 903.0]])
|
|
req0_seg3_pad = torch.tensor([[904.0, 905.0]])
|
|
req1_seg0 = torch.tensor([[20.0, 21.0], [22.0, 23.0]])
|
|
req1_seg2 = torch.tensor([[24.0, 25.0]])
|
|
|
|
# rank0 owns segment 0 then mirror segment 3 for each request.
|
|
input_tensor_all[0:1] = req0_seg0
|
|
input_tensor_all[1:2] = req0_seg3_pad
|
|
input_tensor_all[2:4] = req1_seg0
|
|
# rank1 owns segment 1 then mirror segment 2 for each request.
|
|
rank1 = max_rank_token
|
|
input_tensor_all[rank1 : rank1 + 1] = req0_seg1_pad
|
|
input_tensor_all[rank1 + 1 : rank1 + 2] = req0_seg2_pad
|
|
input_tensor_all[rank1 + 2 : rank1 + 3] = req1_seg2
|
|
|
|
expected = torch.cat([req0_seg0, req1_seg0, req1_seg2], dim=0)
|
|
forward_batch = SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=2,
|
|
request_split_lists=valid_split_lists,
|
|
request_compute_split_lists=compute_split_lists,
|
|
compute_padding_enabled=True,
|
|
max_rank_len=[max_rank_token, max_rank_token],
|
|
)
|
|
)
|
|
|
|
actual = _torch_batch_in_seq_all_gather_rerange(
|
|
input_tensor_all,
|
|
forward_batch,
|
|
cp_size=cp_size,
|
|
)
|
|
|
|
self.assertTrue(torch.equal(actual, expected))
|
|
|
|
def test_batch_in_seq_all_gather_rerange_matches_parallel20_tiny_extend_layout(self):
|
|
import torch
|
|
|
|
cp_size = 8
|
|
extend_lens = [19, 21, 61, 33]
|
|
prefix_lens = [704, 704, 640, 704]
|
|
plans = [
|
|
build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=extend_lens,
|
|
prefix_lens=prefix_lens,
|
|
page_size=64,
|
|
cp_size=cp_size,
|
|
cp_rank=rank,
|
|
)
|
|
for rank in range(cp_size)
|
|
]
|
|
valid_split_lists = plans[0].request_split_lists
|
|
compute_split_lists = plans[0].request_compute_split_lists
|
|
|
|
self.assertEqual(plans[0].request_valid_rank_local_tokens, extend_lens)
|
|
for rank in range(1, cp_size):
|
|
self.assertEqual(plans[rank].request_valid_rank_local_tokens, [0, 0, 0, 0])
|
|
|
|
max_rank_token = max(
|
|
sum(
|
|
split[rank] + split[cp_size * 2 - rank - 1]
|
|
for split in compute_split_lists
|
|
)
|
|
for rank in range(cp_size)
|
|
)
|
|
self.assertEqual(max_rank_token, 64 * len(extend_lens))
|
|
|
|
input_tensor_all = torch.zeros((max_rank_token * cp_size, 1), dtype=torch.float32)
|
|
expected_rows = []
|
|
rank0_cursor = 0
|
|
for req_id, extend_len in enumerate(extend_lens):
|
|
valid_rows = torch.arange(
|
|
req_id * 1000,
|
|
req_id * 1000 + extend_len,
|
|
dtype=torch.float32,
|
|
).view(-1, 1)
|
|
input_tensor_all[rank0_cursor : rank0_cursor + extend_len] = valid_rows
|
|
# Fill the rest of the 64-row compute slot with poison values that
|
|
# must not appear after valid-output rerange.
|
|
pad_len = 64 - extend_len
|
|
if pad_len:
|
|
input_tensor_all[
|
|
rank0_cursor + extend_len : rank0_cursor + 64
|
|
] = 900000.0 + req_id
|
|
expected_rows.append(valid_rows)
|
|
rank0_cursor += 64
|
|
# Non-owner ranks only have compute padding in this tiny-extend case.
|
|
input_tensor_all[max_rank_token:] = -777.0
|
|
|
|
forward_batch = SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=len(extend_lens),
|
|
request_split_lists=valid_split_lists,
|
|
request_compute_split_lists=compute_split_lists,
|
|
compute_padding_enabled=True,
|
|
max_rank_len=[max_rank_token] * cp_size,
|
|
)
|
|
)
|
|
|
|
actual = _torch_batch_in_seq_all_gather_rerange(
|
|
input_tensor_all,
|
|
forward_batch,
|
|
cp_size=cp_size,
|
|
)
|
|
|
|
expected = torch.cat(expected_rows, dim=0)
|
|
self.assertTrue(torch.equal(actual, expected))
|
|
|
|
def _build_batch_rerange_case(
|
|
self,
|
|
*,
|
|
cp_size,
|
|
request_split_lists,
|
|
row_width,
|
|
dtype,
|
|
):
|
|
import torch
|
|
|
|
rank_tokens = []
|
|
for rank in range(cp_size):
|
|
mirror = cp_size * 2 - rank - 1
|
|
rank_tokens.append(
|
|
sum(split[rank] + split[mirror] for split in request_split_lists)
|
|
)
|
|
max_rank_token = max(rank_tokens)
|
|
total_tokens = sum(sum(split) for split in request_split_lists)
|
|
input_tensor_all = torch.zeros(
|
|
(max_rank_token * cp_size, row_width),
|
|
dtype=dtype,
|
|
)
|
|
expected = torch.empty((total_tokens, row_width), dtype=dtype)
|
|
|
|
next_value = 1
|
|
request_segments = []
|
|
for split in request_split_lists:
|
|
segments = []
|
|
for segment_len in split:
|
|
if dtype == torch.uint8:
|
|
rows = (
|
|
torch.arange(
|
|
next_value,
|
|
next_value + segment_len * row_width,
|
|
dtype=torch.int64,
|
|
)
|
|
.remainder(251)
|
|
.to(torch.uint8)
|
|
.view(segment_len, row_width)
|
|
)
|
|
else:
|
|
rows = (
|
|
torch.arange(
|
|
next_value,
|
|
next_value + segment_len * row_width,
|
|
dtype=torch.float32,
|
|
)
|
|
.view(segment_len, row_width)
|
|
.to(dtype)
|
|
)
|
|
next_value += segment_len * row_width
|
|
segments.append(rows)
|
|
request_segments.append(segments)
|
|
|
|
output_cursor = 0
|
|
for segments in request_segments:
|
|
for rows in segments:
|
|
expected[output_cursor : output_cursor + rows.shape[0]] = rows
|
|
output_cursor += rows.shape[0]
|
|
|
|
for rank in range(cp_size):
|
|
mirror = cp_size * 2 - rank - 1
|
|
rank_cursor = rank * max_rank_token
|
|
for segments in request_segments:
|
|
for segment_id in (rank, mirror):
|
|
rows = segments[segment_id]
|
|
input_tensor_all[
|
|
rank_cursor : rank_cursor + rows.shape[0]
|
|
] = rows
|
|
rank_cursor += rows.shape[0]
|
|
|
|
return input_tensor_all, expected
|
|
|
|
def test_local_pair_split_uses_metadata_lengths_not_half_split(self):
|
|
import torch
|
|
|
|
tensor = torch.arange(9)
|
|
|
|
prev, next_ = split_in_seq_cp_local_pair(tensor, 6, 3)
|
|
|
|
self.assertEqual(prev.tolist(), [0, 1, 2, 3, 4, 5])
|
|
self.assertEqual(next_.tolist(), [6, 7, 8])
|
|
|
|
def test_local_pair_split_rejects_stale_metadata(self):
|
|
import torch
|
|
|
|
with self.assertRaisesRegex(RuntimeError, "local in-seq CP length mismatch"):
|
|
split_in_seq_cp_local_pair(torch.arange(9), 5, 5, name="q_fp8")
|
|
|
|
def test_cp_split_and_rebuild_1d_matches_in_seq_zigzag_order(self):
|
|
import torch
|
|
from types import SimpleNamespace
|
|
|
|
forward_batch = SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
split_list=[2, 2, 2, 2, 2, 2, 2, 2],
|
|
zigzag_index=[1, 6],
|
|
)
|
|
)
|
|
|
|
local_locs = cp_split_and_rebuild_1d(forward_batch, torch.arange(16))
|
|
|
|
self.assertEqual(local_locs.tolist(), [2, 3, 12, 13])
|
|
|
|
def test_cp_split_and_rebuild_data_keeps_batch_request_boundaries(self):
|
|
import torch
|
|
|
|
forward_batch = SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=2,
|
|
request_extend_lens=[4, 9],
|
|
request_split_lists=[[4, 0, 0, 0], [4, 4, 1, 0]],
|
|
request_zigzag_indices=[[0, 3], [0, 3]],
|
|
)
|
|
)
|
|
tensor = torch.arange(13 * 2).view(13, 2)
|
|
|
|
with patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
):
|
|
local = cp_split_and_rebuild_data(forward_batch, tensor)
|
|
|
|
self.assertEqual(local[:, 0].tolist(), list(range(0, 16, 2)))
|
|
|
|
def test_cp_split_and_rebuild_data_preserves_non_token_dimensions(self):
|
|
import torch
|
|
|
|
forward_batch = SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=2,
|
|
request_extend_lens=[4, 9],
|
|
request_split_lists=[[4, 0, 0, 0], [4, 4, 1, 0]],
|
|
request_zigzag_indices=[[0, 3], [0, 3]],
|
|
)
|
|
)
|
|
tensor = torch.arange(13 * 2 * 3, dtype=torch.float32).view(13, 2, 3)
|
|
|
|
with patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
):
|
|
local = cp_split_and_rebuild_data(forward_batch, tensor)
|
|
|
|
self.assertEqual(tuple(local.shape), (8, 2, 3))
|
|
self.assertTrue(torch.equal(local[0], tensor[0]))
|
|
|
|
def test_cp_split_and_rebuild_data_uses_compute_padding_rows(self):
|
|
import torch
|
|
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[65],
|
|
prefix_lens=[40320],
|
|
page_size=64,
|
|
cp_size=8,
|
|
cp_rank=1,
|
|
)
|
|
forward_batch = SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=1,
|
|
batch_plan=plan,
|
|
)
|
|
)
|
|
tensor = torch.arange(65 * 2, dtype=torch.float32).view(65, 2)
|
|
|
|
with patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
):
|
|
local = cp_split_and_rebuild_data(forward_batch, tensor)
|
|
|
|
self.assertEqual(local.shape, (64, 2))
|
|
self.assertEqual(local[0].tolist(), [128.0, 129.0])
|
|
self.assertTrue(torch.equal(local[1:], torch.zeros((63, 2))))
|
|
|
|
def test_cp_split_and_rebuild_data_ignores_trailing_static_padding_rows(self):
|
|
import torch
|
|
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[4],
|
|
prefix_lens=[0],
|
|
page_size=4,
|
|
cp_size=2,
|
|
cp_rank=1,
|
|
)
|
|
forward_batch = SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=1,
|
|
batch_plan=plan,
|
|
)
|
|
)
|
|
tensor = torch.arange(8 * 2, dtype=torch.float32).view(8, 2)
|
|
|
|
with patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
):
|
|
local = cp_split_and_rebuild_data(forward_batch, tensor)
|
|
|
|
self.assertEqual(local.shape, (4, 2))
|
|
self.assertTrue(torch.equal(local, torch.zeros((4, 2))))
|
|
|
|
def test_cp_split_and_rebuild_data_ignores_mlp_sync_static_padding_without_compute_padding(
|
|
self,
|
|
):
|
|
import torch
|
|
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[8],
|
|
prefix_lens=[0],
|
|
page_size=4,
|
|
cp_size=2,
|
|
cp_rank=1,
|
|
)
|
|
self.assertFalse(plan.compute_padding_enabled)
|
|
forward_batch = SimpleNamespace(
|
|
extend_num_tokens=9,
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=1,
|
|
batch_plan=plan,
|
|
),
|
|
)
|
|
tensor = torch.arange(9 * 2, dtype=torch.float32).view(9, 2)
|
|
expected = split_tensor_by_cp_batch_plan(
|
|
tensor[:8],
|
|
plan,
|
|
mode="data",
|
|
)
|
|
|
|
with patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
):
|
|
local = cp_split_and_rebuild_data(forward_batch, tensor)
|
|
|
|
self.assertTrue(torch.equal(local, expected))
|
|
|
|
def test_cp_split_valid_kind_rejects_trailing_padding_rows(self):
|
|
import torch
|
|
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[4],
|
|
prefix_lens=[0],
|
|
page_size=4,
|
|
cp_size=2,
|
|
cp_rank=1,
|
|
)
|
|
|
|
with self.assertRaisesRegex(
|
|
RuntimeError,
|
|
"batch_gt1_split_input_len_mismatch",
|
|
):
|
|
split_tensor_by_cp_batch_plan(
|
|
torch.arange(8),
|
|
plan,
|
|
mode="1d",
|
|
split_kind="valid",
|
|
)
|
|
|
|
def test_cp_split_and_rebuild_1d_keeps_batch_request_boundaries(self):
|
|
import torch
|
|
|
|
forward_batch = SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=2,
|
|
request_extend_lens=[4, 9],
|
|
request_split_lists=[[4, 0, 0, 0], [4, 4, 1, 0]],
|
|
request_zigzag_indices=[[1, 2], [1, 2]],
|
|
)
|
|
)
|
|
|
|
with patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
):
|
|
local = cp_split_and_rebuild_1d(forward_batch, torch.arange(13))
|
|
|
|
self.assertEqual(local.tolist(), [8, 9, 10, 11, 12])
|
|
|
|
def test_cp_split_and_rebuild_1d_uses_zero_compute_padding_rows(self):
|
|
import torch
|
|
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[65],
|
|
prefix_lens=[40320],
|
|
page_size=64,
|
|
cp_size=8,
|
|
cp_rank=1,
|
|
)
|
|
forward_batch = SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=1,
|
|
batch_plan=plan,
|
|
)
|
|
)
|
|
|
|
with patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
):
|
|
local = cp_split_and_rebuild_1d(forward_batch, torch.arange(65))
|
|
|
|
self.assertEqual(local.shape, (64,))
|
|
self.assertEqual(local[0].item(), 64)
|
|
self.assertEqual(local[1:].tolist(), [0] * 63)
|
|
|
|
def test_select_cp_local_valid_rows_filters_compute_padding_rows(self):
|
|
import torch
|
|
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[65],
|
|
prefix_lens=[0],
|
|
page_size=64,
|
|
cp_size=8,
|
|
cp_rank=1,
|
|
)
|
|
forward_batch = SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=1,
|
|
batch_plan=plan,
|
|
)
|
|
)
|
|
local_compute_rows = torch.full((64, 2), -1.0)
|
|
local_compute_rows[0] = torch.tensor([50.0, 51.0])
|
|
|
|
selected = select_cp_local_valid_rows_for_cache_write(
|
|
forward_batch, local_compute_rows
|
|
)
|
|
|
|
self.assertEqual(selected.tolist(), [[50.0, 51.0]])
|
|
|
|
def test_restore_cp_local_valid_rows_for_moe_keeps_dummy_rows_zero(self):
|
|
import torch
|
|
|
|
from sglang.srt.layers.attention.nsa.utils import (
|
|
restore_cp_local_valid_rows_for_moe,
|
|
)
|
|
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[29, 9, 60, 9, 17],
|
|
prefix_lens=[704, 704, 640, 704, 704],
|
|
page_size=64,
|
|
cp_size=8,
|
|
cp_rank=0,
|
|
)
|
|
forward_batch = SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=5,
|
|
batch_plan=plan,
|
|
)
|
|
)
|
|
local_compute_rows = torch.zeros(
|
|
(sum(plan.request_compute_rank_local_tokens), 2), dtype=torch.float32
|
|
)
|
|
compact_valid = torch.arange(
|
|
sum(plan.request_valid_rank_local_tokens) * 2, dtype=torch.float32
|
|
).view(-1, 2)
|
|
|
|
restored = restore_cp_local_valid_rows_for_moe(
|
|
forward_batch,
|
|
compact_valid,
|
|
local_compute_rows,
|
|
)
|
|
|
|
self.assertEqual(tuple(restored.shape), tuple(local_compute_rows.shape))
|
|
valid_rows = select_cp_local_valid_rows_for_cache_write(
|
|
forward_batch,
|
|
restored,
|
|
)
|
|
self.assertTrue(torch.equal(valid_rows, compact_valid))
|
|
|
|
valid_mask = torch.zeros(restored.shape[0], dtype=torch.bool)
|
|
cursor = 0
|
|
for compute_len, valid_len in zip(
|
|
plan.request_compute_rank_local_tokens,
|
|
plan.request_valid_rank_local_tokens,
|
|
):
|
|
valid_mask[cursor : cursor + valid_len] = True
|
|
cursor += compute_len
|
|
self.assertTrue(torch.equal(restored[~valid_mask], torch.zeros_like(restored[~valid_mask])))
|
|
|
|
def test_select_cp_current_valid_rows_accepts_global_rows_under_compute_padding(
|
|
self,
|
|
):
|
|
import torch
|
|
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[65],
|
|
prefix_lens=[0],
|
|
page_size=64,
|
|
cp_size=8,
|
|
cp_rank=1,
|
|
)
|
|
self.assertTrue(plan.compute_padding_enabled)
|
|
global_current = torch.arange(65 * 2, dtype=torch.float32).view(65, 2)
|
|
expected = split_tensor_by_cp_batch_plan(
|
|
global_current,
|
|
plan,
|
|
mode="data",
|
|
split_kind="valid",
|
|
)
|
|
forward_batch = SimpleNamespace(
|
|
extend_seq_lens_cpu=[65],
|
|
cp_local_out_cache_loc=torch.arange(expected.shape[0], dtype=torch.long),
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=1,
|
|
batch_plan=plan,
|
|
),
|
|
)
|
|
|
|
selected = select_cp_current_valid_rows_for_reuse(
|
|
forward_batch,
|
|
global_current,
|
|
)
|
|
|
|
self.assertIsNotNone(selected)
|
|
self.assertTrue(torch.equal(selected, expected))
|
|
|
|
def test_cp_split_and_rebuild_position_is_batch_aware_and_compute_padded(self):
|
|
import torch
|
|
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[65],
|
|
prefix_lens=[40320],
|
|
page_size=64,
|
|
cp_size=8,
|
|
cp_rank=1,
|
|
)
|
|
forward_batch = SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=1,
|
|
batch_plan=plan,
|
|
)
|
|
)
|
|
positions = torch.arange(40320, 40385, dtype=torch.int32)
|
|
|
|
with patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
):
|
|
local = cp_split_and_rebuild_position(forward_batch, positions)
|
|
|
|
self.assertEqual(local.shape, (64,))
|
|
self.assertEqual(local.tolist(), list(range(40384, 40448)))
|
|
|
|
def test_cp_split_and_rebuild_position_ignores_mlp_sync_static_padding_without_compute_padding(
|
|
self,
|
|
):
|
|
import torch
|
|
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[8],
|
|
prefix_lens=[0],
|
|
page_size=4,
|
|
cp_size=2,
|
|
cp_rank=1,
|
|
)
|
|
self.assertFalse(plan.compute_padding_enabled)
|
|
forward_batch = SimpleNamespace(
|
|
extend_num_tokens=9,
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=1,
|
|
batch_plan=plan,
|
|
),
|
|
)
|
|
positions = torch.arange(9, dtype=torch.int32)
|
|
expected = split_tensor_by_cp_batch_plan(
|
|
positions[:8],
|
|
plan,
|
|
mode="position",
|
|
)
|
|
|
|
with patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
):
|
|
local = cp_split_and_rebuild_position(forward_batch, positions)
|
|
|
|
self.assertTrue(torch.equal(local, expected))
|
|
|
|
def test_cp_local_embedding_pad_len_uses_metadata_max_rank_len(self):
|
|
from types import SimpleNamespace
|
|
|
|
import torch
|
|
|
|
forward_batch = SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(max_rank_len=[4096] * 8)
|
|
)
|
|
|
|
self.assertEqual(
|
|
get_cp_local_embedding_padded_token_count(forward_batch, 4040), 4096
|
|
)
|
|
self.assertEqual(
|
|
get_cp_local_embedding_padded_token_count(forward_batch, 4096), 4096
|
|
)
|
|
self.assertEqual(
|
|
pad_cp_local_input_ids_for_embedding(
|
|
SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(max_rank_len=[6] * 8)
|
|
),
|
|
torch.tensor([11, 12, 13, 14]),
|
|
).tolist(),
|
|
[11, 12, 13, 14, 0, 0],
|
|
)
|
|
self.assertEqual(
|
|
pad_cp_local_input_ids_for_embedding(
|
|
SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(max_rank_len=[4] * 8)
|
|
),
|
|
torch.tensor([11, 12, 13, 14]),
|
|
).tolist(),
|
|
[11, 12, 13, 14],
|
|
)
|
|
|
|
missing_metadata = SimpleNamespace(nsa_cp_metadata=None)
|
|
self.assertIsNone(
|
|
get_cp_local_embedding_padded_token_count(missing_metadata, 4040)
|
|
)
|
|
self.assertIsNone(
|
|
pad_cp_local_input_ids_for_embedding(
|
|
missing_metadata, torch.tensor([11, 12, 13, 14])
|
|
)
|
|
)
|
|
|
|
stale_metadata = SimpleNamespace(
|
|
nsa_cp_metadata=NSAContextParallelMetadata(max_rank_len=[4039] * 8)
|
|
)
|
|
self.assertIsNone(
|
|
get_cp_local_embedding_padded_token_count(stale_metadata, 4040)
|
|
)
|
|
|
|
def test_local_out_cache_loc_requires_compute_owner_pages(self):
|
|
import torch
|
|
from types import SimpleNamespace
|
|
|
|
page_size = 4
|
|
# Segment order for cp_size=4, cp_rank=1 is segment 1 then 6.
|
|
# The logical page ids below deliberately encode the same owners through
|
|
# (logical_page - 1) % cp_size:
|
|
# segment 1 -> page 2 owner 1
|
|
# segment 6 -> page 6 owner 1
|
|
segment_pages = [1, 2, 3, 4, 8, 7, 6, 5]
|
|
out_cache_loc = torch.cat(
|
|
[
|
|
torch.arange(page * page_size, (page + 1) * page_size)
|
|
for page in segment_pages
|
|
]
|
|
)
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
cp_shared_kv_layout=CpSharedKVLayout(
|
|
page_size=page_size,
|
|
cp_size=4,
|
|
cp_rank=1,
|
|
),
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
split_list=[page_size] * 8,
|
|
zigzag_index=[1, 6],
|
|
page_aligned=True,
|
|
page_size=page_size,
|
|
extend_prefix_len=0,
|
|
),
|
|
out_cache_loc=out_cache_loc,
|
|
)
|
|
|
|
local_locs = get_cp_shared_kv_local_out_cache_loc(forward_batch)
|
|
|
|
self.assertIsNotNone(local_locs)
|
|
self.assertEqual(
|
|
local_locs.tolist(),
|
|
list(range(2 * page_size, 3 * page_size))
|
|
+ list(range(6 * page_size, 7 * page_size)),
|
|
)
|
|
|
|
def test_batch_local_out_cache_loc_keeps_request_boundaries(self):
|
|
import torch
|
|
from types import SimpleNamespace
|
|
|
|
page_size = 4
|
|
# cp_size=2/cp_rank=1 selects segment 1 and 2 for each request.
|
|
# req0 has no rank-1 local rows. req1 contributes segment 1 (page 2)
|
|
# and segment 2 (tail page 4). The synthetic logical page ids encode
|
|
# the owner-lane invariant through (page_id - 1) % cp_size.
|
|
out_cache_loc = torch.cat(
|
|
[
|
|
torch.arange(1 * page_size, 2 * page_size), # req0 seg0 owner 0
|
|
torch.arange(3 * page_size, 4 * page_size), # req1 seg0 owner 0
|
|
torch.arange(2 * page_size, 3 * page_size), # req1 seg1 owner 1
|
|
torch.tensor([4 * page_size]), # req1 seg2 owner 1 tail
|
|
]
|
|
)
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
extend_num_tokens=8,
|
|
cp_shared_kv_layout=CpSharedKVLayout(
|
|
page_size=page_size,
|
|
cp_size=2,
|
|
cp_rank=1,
|
|
),
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=2,
|
|
split_list=[4, 0, 0, 0],
|
|
zigzag_index=[1, 2],
|
|
page_aligned=True,
|
|
page_size=page_size,
|
|
extend_prefix_len=0,
|
|
request_extend_lens=[4, 9],
|
|
request_split_lists=[[4, 0, 0, 0], [4, 4, 1, 0]],
|
|
request_zigzag_indices=[[1, 2], [1, 2]],
|
|
),
|
|
out_cache_loc=out_cache_loc,
|
|
)
|
|
|
|
local_locs = get_cp_shared_kv_local_out_cache_loc(forward_batch)
|
|
|
|
self.assertIsNotNone(local_locs)
|
|
self.assertEqual(
|
|
local_locs.tolist(),
|
|
list(range(2 * page_size, 3 * page_size)) + [4 * page_size],
|
|
)
|
|
|
|
def test_local_out_cache_loc_uses_valid_rows_under_compute_padding(self):
|
|
import torch
|
|
|
|
page_size = 64
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[65],
|
|
prefix_lens=[0],
|
|
page_size=page_size,
|
|
cp_size=8,
|
|
cp_rank=1,
|
|
)
|
|
out_cache_loc = torch.cat(
|
|
[
|
|
torch.arange(page_size, 2 * page_size),
|
|
torch.tensor([2 * page_size]),
|
|
]
|
|
).to(torch.int64)
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
cp_shared_kv_layout=CpSharedKVLayout(
|
|
page_size=page_size,
|
|
cp_size=8,
|
|
cp_rank=1,
|
|
),
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=1,
|
|
batch_plan=plan,
|
|
page_aligned=True,
|
|
page_size=page_size,
|
|
extend_prefix_len=0,
|
|
),
|
|
out_cache_loc=out_cache_loc,
|
|
)
|
|
|
|
with patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
):
|
|
local_locs = get_cp_shared_kv_local_out_cache_loc(forward_batch)
|
|
|
|
self.assertEqual(local_locs.tolist(), [2 * page_size])
|
|
|
|
def test_local_out_cache_loc_ignores_trailing_static_padding_locs(self):
|
|
import torch
|
|
|
|
page_size = 4
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[4],
|
|
prefix_lens=[0],
|
|
page_size=page_size,
|
|
cp_size=2,
|
|
cp_rank=0,
|
|
)
|
|
valid_locs = torch.arange(1 * page_size, 2 * page_size, dtype=torch.int64)
|
|
static_padding_locs = torch.arange(
|
|
99 * page_size, 100 * page_size, dtype=torch.int64
|
|
)
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
extend_num_tokens=8,
|
|
cp_shared_kv_layout=CpSharedKVLayout(
|
|
page_size=page_size,
|
|
cp_size=2,
|
|
cp_rank=0,
|
|
),
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=1,
|
|
batch_plan=plan,
|
|
page_aligned=True,
|
|
page_size=page_size,
|
|
extend_prefix_len=0,
|
|
),
|
|
out_cache_loc=torch.cat((valid_locs, static_padding_locs)),
|
|
)
|
|
|
|
with patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
):
|
|
local_locs = get_cp_shared_kv_local_out_cache_loc(forward_batch)
|
|
|
|
self.assertEqual(local_locs.tolist(), valid_locs.tolist())
|
|
|
|
def test_local_out_cache_loc_ignores_mlp_sync_static_padding_without_compute_padding(
|
|
self,
|
|
):
|
|
import torch
|
|
|
|
page_size = 4
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[8],
|
|
prefix_lens=[0],
|
|
page_size=page_size,
|
|
cp_size=2,
|
|
cp_rank=1,
|
|
)
|
|
self.assertFalse(plan.compute_padding_enabled)
|
|
static_padding_locs = torch.tensor([99 * page_size], dtype=torch.int64)
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
extend_num_tokens=9,
|
|
cp_shared_kv_layout=CpSharedKVLayout(
|
|
page_size=page_size,
|
|
cp_size=2,
|
|
cp_rank=1,
|
|
),
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=1,
|
|
batch_plan=plan,
|
|
page_aligned=True,
|
|
page_size=page_size,
|
|
extend_prefix_len=0,
|
|
),
|
|
out_cache_loc=torch.cat(
|
|
(
|
|
torch.arange(1 * page_size, 3 * page_size, dtype=torch.int64),
|
|
static_padding_locs,
|
|
)
|
|
),
|
|
)
|
|
|
|
with patch(
|
|
"sglang.srt.layers.attention.nsa.utils.is_nsa_prefill_cp_round_robin_split",
|
|
return_value=False,
|
|
):
|
|
local_locs = get_cp_shared_kv_local_out_cache_loc(forward_batch)
|
|
|
|
self.assertEqual(
|
|
local_locs.tolist(),
|
|
[
|
|
2 * page_size,
|
|
2 * page_size + 1,
|
|
2 * page_size + 2,
|
|
2 * page_size + 3,
|
|
],
|
|
)
|
|
|
|
def test_local_out_cache_loc_rejects_unproven_trailing_padding_even_with_compute_padding(
|
|
self,
|
|
):
|
|
import torch
|
|
|
|
page_size = 4
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[4],
|
|
prefix_lens=[0],
|
|
page_size=page_size,
|
|
cp_size=2,
|
|
cp_rank=0,
|
|
)
|
|
self.assertTrue(plan.compute_padding_enabled)
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
cp_shared_kv_layout=CpSharedKVLayout(
|
|
page_size=page_size,
|
|
cp_size=2,
|
|
cp_rank=0,
|
|
),
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=1,
|
|
batch_plan=plan,
|
|
page_aligned=True,
|
|
page_size=page_size,
|
|
extend_prefix_len=0,
|
|
),
|
|
out_cache_loc=torch.cat(
|
|
(
|
|
torch.arange(1 * page_size, 2 * page_size, dtype=torch.int64),
|
|
torch.arange(99 * page_size, 100 * page_size, dtype=torch.int64),
|
|
)
|
|
),
|
|
)
|
|
|
|
with self.assertRaisesRegex(RuntimeError, "static_padded=None"):
|
|
get_cp_shared_kv_local_out_cache_loc(forward_batch)
|
|
|
|
def test_batch_local_physical_out_cache_loc_reuses_layer_invariant_plan(self):
|
|
import torch
|
|
from types import SimpleNamespace
|
|
|
|
page_size = 4
|
|
out_cache_loc = torch.cat(
|
|
[
|
|
torch.arange(1 * page_size, 2 * page_size),
|
|
torch.arange(3 * page_size, 4 * page_size),
|
|
torch.arange(2 * page_size, 3 * page_size),
|
|
torch.tensor([4 * page_size]),
|
|
]
|
|
)
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
cp_shared_kv_layout=CpSharedKVLayout(
|
|
page_size=page_size,
|
|
cp_size=2,
|
|
cp_rank=1,
|
|
),
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=2,
|
|
split_list=[4, 0, 0, 0],
|
|
zigzag_index=[1, 2],
|
|
page_aligned=True,
|
|
page_size=page_size,
|
|
extend_prefix_len=0,
|
|
request_extend_lens=[4, 9],
|
|
request_split_lists=[[4, 0, 0, 0], [4, 4, 1, 0]],
|
|
request_zigzag_indices=[[1, 2], [1, 2]],
|
|
),
|
|
out_cache_loc=out_cache_loc,
|
|
)
|
|
|
|
physical_locs = get_cp_shared_kv_local_physical_out_cache_loc(forward_batch)
|
|
second_read = get_cp_shared_kv_local_physical_out_cache_loc(forward_batch)
|
|
|
|
self.assertIs(physical_locs, second_read)
|
|
self.assertEqual(
|
|
physical_locs.tolist(),
|
|
list(range(1 * page_size, 2 * page_size)) + [2 * page_size],
|
|
)
|
|
|
|
def test_local_physical_out_cache_loc_is_cached(self):
|
|
import torch
|
|
from types import SimpleNamespace
|
|
|
|
page_size = 4
|
|
segment_pages = [1, 2, 3, 4, 8, 7, 6, 5]
|
|
out_cache_loc = torch.cat(
|
|
[
|
|
torch.arange(page * page_size, (page + 1) * page_size)
|
|
for page in segment_pages
|
|
]
|
|
)
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
cp_shared_kv_layout=CpSharedKVLayout(
|
|
page_size=page_size,
|
|
cp_size=4,
|
|
cp_rank=1,
|
|
),
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
split_list=[page_size] * 8,
|
|
zigzag_index=[1, 6],
|
|
page_aligned=True,
|
|
page_size=page_size,
|
|
extend_prefix_len=0,
|
|
),
|
|
out_cache_loc=out_cache_loc,
|
|
)
|
|
|
|
physical_locs = get_cp_shared_kv_local_physical_out_cache_loc(forward_batch)
|
|
second_read = get_cp_shared_kv_local_physical_out_cache_loc(forward_batch)
|
|
|
|
self.assertIs(physical_locs, second_read)
|
|
self.assertEqual(
|
|
physical_locs.tolist(),
|
|
list(range(1 * page_size, 2 * page_size))
|
|
+ list(range(2 * page_size, 3 * page_size)),
|
|
)
|
|
|
|
def test_local_out_cache_loc_fails_fast_when_owner_mismatch(self):
|
|
import torch
|
|
from types import SimpleNamespace
|
|
|
|
page_size = 4
|
|
out_cache_loc = torch.arange(page_size * 8, page_size * 16)
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
cp_shared_kv_layout=CpSharedKVLayout(
|
|
page_size=page_size,
|
|
cp_size=4,
|
|
cp_rank=1,
|
|
),
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
split_list=[page_size] * 8,
|
|
zigzag_index=[1, 6],
|
|
page_aligned=True,
|
|
page_size=page_size,
|
|
extend_prefix_len=0,
|
|
),
|
|
out_cache_loc=out_cache_loc,
|
|
)
|
|
|
|
with self.assertRaisesRegex(
|
|
RuntimeError,
|
|
"CP_SHARED_KV_FAIL_FAST.*local_loc_owner_mismatch",
|
|
):
|
|
get_cp_shared_kv_local_out_cache_loc(forward_batch)
|
|
|
|
def test_local_out_cache_loc_fails_fast_every_invalid_event(self):
|
|
import torch
|
|
from types import SimpleNamespace
|
|
|
|
page_size = 4
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
cp_shared_kv_layout=CpSharedKVLayout(
|
|
page_size=page_size,
|
|
cp_size=4,
|
|
cp_rank=1,
|
|
),
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
split_list=[page_size] * 8,
|
|
zigzag_index=[1, 6],
|
|
page_aligned=False,
|
|
page_size=page_size,
|
|
extend_prefix_len=0,
|
|
),
|
|
out_cache_loc=torch.arange(page_size * 8, page_size * 16),
|
|
)
|
|
|
|
with self.assertLogs(
|
|
"sglang.srt.layers.attention.nsa.utils", level="ERROR"
|
|
) as cm:
|
|
with self.assertRaisesRegex(
|
|
RuntimeError,
|
|
"CP_SHARED_KV_FAIL_FAST.*not_page_aligned",
|
|
):
|
|
get_cp_shared_kv_local_out_cache_loc(forward_batch)
|
|
with self.assertRaisesRegex(
|
|
RuntimeError,
|
|
"CP_SHARED_KV_FAIL_FAST.*not_page_aligned",
|
|
):
|
|
get_cp_shared_kv_local_out_cache_loc(forward_batch)
|
|
|
|
self.assertEqual(len(cm.output), 2)
|
|
self.assertIn("[CP_SHARED_KV_FAIL_FAST][direct_write]", cm.output[0])
|
|
self.assertIn("metadata is not page-aligned", cm.output[0])
|
|
self.assertIn("[CP_SHARED_KV_FAIL_FAST][direct_write]", cm.output[1])
|
|
self.assertIn("metadata is not page-aligned", cm.output[1])
|
|
|
|
def test_indexer_direct_write_fails_fast_on_local_shape_mismatch(self):
|
|
import torch
|
|
|
|
from sglang.srt.layers.attention.nsa import nsa_indexer
|
|
from sglang.srt.layers.attention.nsa.nsa_indexer import Indexer
|
|
|
|
indexer = object.__new__(Indexer)
|
|
indexer.nsa_enable_prefill_cp = True
|
|
forward_batch = SimpleNamespace(nsa_cp_metadata=object())
|
|
|
|
with (
|
|
patch.object(nsa_indexer, "nsa_use_prefill_cp", return_value=True),
|
|
patch.object(
|
|
nsa_indexer,
|
|
"get_cp_shared_kv_local_out_cache_loc",
|
|
return_value=torch.tensor([1, 2], dtype=torch.int64),
|
|
),
|
|
):
|
|
with self.assertRaisesRegex(
|
|
RuntimeError,
|
|
"CP_SHARED_KV_FAIL_FAST.*index_local_shape_mismatch",
|
|
):
|
|
Indexer._store_cp_shared_local_index_k_cache(
|
|
indexer,
|
|
forward_batch,
|
|
layer_id=0,
|
|
local_key=torch.empty((1, 8)),
|
|
act_quant=None,
|
|
)
|
|
|
|
def test_indexer_direct_write_filters_compute_padding_rows(self):
|
|
import torch
|
|
|
|
from sglang.srt.layers.attention.nsa import nsa_indexer
|
|
from sglang.srt.layers.attention.nsa.nsa_indexer import Indexer
|
|
|
|
page_size = 64
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[65],
|
|
prefix_lens=[0],
|
|
page_size=page_size,
|
|
cp_size=8,
|
|
cp_rank=1,
|
|
)
|
|
out_cache_loc = torch.cat(
|
|
[
|
|
torch.arange(page_size, 2 * page_size),
|
|
torch.tensor([2 * page_size]),
|
|
]
|
|
).to(torch.int64)
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
cp_shared_kv_layout=CpSharedKVLayout(
|
|
page_size=page_size,
|
|
cp_size=8,
|
|
cp_rank=1,
|
|
),
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=1,
|
|
batch_plan=plan,
|
|
page_aligned=True,
|
|
page_size=page_size,
|
|
extend_prefix_len=0,
|
|
),
|
|
out_cache_loc=out_cache_loc,
|
|
token_to_kv_pool=SimpleNamespace(page_size=page_size),
|
|
)
|
|
indexer = object.__new__(Indexer)
|
|
indexer.nsa_enable_prefill_cp = True
|
|
calls = []
|
|
|
|
def fake_store_index_k_cache(**kwargs):
|
|
calls.append(kwargs)
|
|
|
|
indexer._store_index_k_cache = fake_store_index_k_cache
|
|
local_key = torch.full((64, 2), -1.0)
|
|
local_key[0] = torch.tensor([9.0, 10.0])
|
|
|
|
with patch.object(nsa_indexer, "nsa_use_prefill_cp", return_value=True):
|
|
stored = Indexer._store_cp_shared_local_index_k_cache(
|
|
indexer,
|
|
forward_batch,
|
|
layer_id=0,
|
|
local_key=local_key,
|
|
act_quant=None,
|
|
)
|
|
|
|
self.assertTrue(stored)
|
|
self.assertEqual(len(calls), 1)
|
|
self.assertEqual(calls[0]["key"].tolist(), [[9.0, 10.0]])
|
|
|
|
def test_mla_direct_write_fails_fast_on_local_shape_mismatch(self):
|
|
import torch
|
|
|
|
from sglang.srt.models.deepseek_common.attention_forward_methods import (
|
|
forward_mla,
|
|
)
|
|
|
|
mla = SimpleNamespace(attn_mqa=SimpleNamespace(layer_id=3))
|
|
forward_batch = SimpleNamespace()
|
|
|
|
with patch.object(
|
|
forward_mla,
|
|
"get_cp_shared_kv_local_out_cache_loc",
|
|
return_value=torch.tensor([1, 2], dtype=torch.int64),
|
|
):
|
|
with self.assertRaisesRegex(
|
|
RuntimeError,
|
|
"CP_SHARED_KV_FAIL_FAST.*mla_local_shape_mismatch",
|
|
):
|
|
forward_mla.DeepseekMLAForwardMixin._maybe_write_cp_shared_local_mla_kv(
|
|
mla,
|
|
forward_batch,
|
|
k_nope=torch.empty((1, 8)),
|
|
k_pe=torch.empty((2, 8)),
|
|
)
|
|
|
|
def test_mla_direct_write_filters_compute_padding_rows(self):
|
|
import torch
|
|
|
|
from sglang.srt.models.deepseek_common.attention_forward_methods import (
|
|
forward_mla,
|
|
)
|
|
|
|
page_size = 64
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[65],
|
|
prefix_lens=[0],
|
|
page_size=page_size,
|
|
cp_size=8,
|
|
cp_rank=1,
|
|
)
|
|
out_cache_loc = torch.cat(
|
|
[
|
|
torch.arange(page_size, 2 * page_size),
|
|
torch.tensor([2 * page_size]),
|
|
]
|
|
).to(torch.int64)
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
cp_shared_kv_layout=CpSharedKVLayout(
|
|
page_size=page_size,
|
|
cp_size=8,
|
|
cp_rank=1,
|
|
),
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=1,
|
|
batch_plan=plan,
|
|
page_aligned=True,
|
|
page_size=page_size,
|
|
extend_prefix_len=0,
|
|
),
|
|
out_cache_loc=out_cache_loc,
|
|
token_to_kv_pool=SimpleNamespace(page_size=page_size),
|
|
)
|
|
mla = SimpleNamespace(attn_mqa=SimpleNamespace(layer_id=0))
|
|
calls = []
|
|
|
|
def fake_tai_store(**kwargs):
|
|
calls.append(kwargs)
|
|
return True
|
|
|
|
k_nope = torch.full((64, 2), -1.0)
|
|
k_nope[0] = torch.tensor([1.0, 2.0])
|
|
k_pe = torch.full((64, 2), -1.0)
|
|
k_pe[0] = torch.tensor([3.0, 4.0])
|
|
|
|
with patch.object(forward_mla, "try_tai_fused_mla_store", fake_tai_store):
|
|
stored = (
|
|
forward_mla.DeepseekMLAForwardMixin._maybe_write_cp_shared_local_mla_kv(
|
|
mla,
|
|
forward_batch,
|
|
k_nope=k_nope,
|
|
k_pe=k_pe,
|
|
)
|
|
)
|
|
|
|
self.assertTrue(stored)
|
|
self.assertEqual(len(calls), 1)
|
|
self.assertEqual(calls[0]["k_nope"].tolist(), [[1.0, 2.0]])
|
|
self.assertEqual(calls[0]["k_rope"].tolist(), [[3.0, 4.0]])
|
|
self.assertEqual(calls[0]["logical_locs"].tolist(), [2 * page_size])
|
|
|
|
def test_index_partial_current_compose_accepts_local_valid_compute_padding_rows(
|
|
self,
|
|
):
|
|
import torch
|
|
|
|
from sglang.srt.layers.attention.nsa import nsa_indexer
|
|
from sglang.srt.layers.attention.nsa.nsa_indexer import Indexer
|
|
|
|
page_size = 64
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[65],
|
|
prefix_lens=[0],
|
|
page_size=page_size,
|
|
cp_size=8,
|
|
cp_rank=1,
|
|
)
|
|
|
|
class FakePool:
|
|
page_size = 64
|
|
index_head_dim = 2
|
|
|
|
def get_index_k_with_scale_buffer(self, layer_id):
|
|
return torch.zeros((4, 3), dtype=torch.float32)
|
|
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
cp_shared_kv_layout=CpSharedKVLayout(
|
|
page_size=page_size,
|
|
cp_size=8,
|
|
cp_rank=1,
|
|
),
|
|
cp_shared_kv_index_prefetcher=None,
|
|
token_to_kv_pool=FakePool(),
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=1,
|
|
batch_plan=plan,
|
|
page_aligned=True,
|
|
page_size=page_size,
|
|
extend_prefix_len=0,
|
|
),
|
|
out_cache_loc=torch.cat(
|
|
[
|
|
torch.arange(page_size, 2 * page_size),
|
|
torch.tensor([2 * page_size]),
|
|
]
|
|
).to(torch.int64),
|
|
extend_prefix_lens_cpu=[page_size],
|
|
extend_seq_lens_cpu=[65],
|
|
)
|
|
logical_page_table = torch.tensor([[1, 2, 3]], dtype=torch.int32)
|
|
current_index_kv = (
|
|
torch.tensor([[7.0, 8.0]], dtype=torch.float32),
|
|
torch.tensor([[0.5]], dtype=torch.float32),
|
|
)
|
|
materialize_calls = []
|
|
expected_buffer = torch.ones((3, 3), dtype=torch.float32)
|
|
expected_pages = torch.tensor([[0, 1, 2]], dtype=torch.int32)
|
|
indexer = object.__new__(Indexer)
|
|
|
|
def fake_materialize(**kwargs):
|
|
materialize_calls.append(kwargs)
|
|
return expected_buffer, expected_pages
|
|
|
|
with patch.object(
|
|
nsa_indexer,
|
|
"get_or_build_shared_paged_buffer_slot_remap",
|
|
return_value=torch.tensor([0, 1, 2], dtype=torch.int64),
|
|
), patch.object(
|
|
nsa_indexer,
|
|
"materialize_prefix_and_reuse_current_index_page_slots",
|
|
side_effect=fake_materialize,
|
|
):
|
|
dense_buffer, dense_pages = indexer._maybe_materialize_shared_index_buffer(
|
|
forward_batch,
|
|
layer_id=0,
|
|
logical_page_table=logical_page_table,
|
|
current_index_kv=current_index_kv,
|
|
)
|
|
|
|
self.assertIs(dense_buffer, expected_buffer)
|
|
self.assertIs(dense_pages, expected_pages)
|
|
self.assertEqual(len(materialize_calls), 1)
|
|
self.assertIs(materialize_calls[0]["current_index_k"], current_index_kv[0])
|
|
self.assertEqual(materialize_calls[0]["current_locs"].tolist(), [2 * page_size])
|
|
|
|
def test_indexer_current_reuse_compute_padding_selects_local_key_not_gathered_key(
|
|
self,
|
|
):
|
|
import torch
|
|
import types
|
|
|
|
from sglang.srt.layers.attention.nsa import nsa_indexer
|
|
from sglang.srt.layers.attention.nsa.nsa_indexer import Indexer
|
|
|
|
page_size = 64
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[65],
|
|
prefix_lens=[0],
|
|
page_size=page_size,
|
|
cp_size=8,
|
|
cp_rank=1,
|
|
)
|
|
metadata_obj = _build_batch_metadata_from_plan(plan)
|
|
|
|
class Mode:
|
|
def is_extend_without_speculative(self):
|
|
return True
|
|
|
|
def is_decode_or_idle(self):
|
|
return False
|
|
|
|
def is_target_verify(self):
|
|
return False
|
|
|
|
def is_draft_extend(self, include_v2=False):
|
|
return False
|
|
|
|
def is_context_parallel_extend(self):
|
|
return True
|
|
|
|
class AttnBackend:
|
|
def get_indexer_metadata(self, layer_id, forward_batch):
|
|
return object()
|
|
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
cp_shared_kv_layout=CpSharedKVLayout(
|
|
page_size=page_size,
|
|
cp_size=8,
|
|
cp_rank=1,
|
|
),
|
|
token_to_kv_pool=SimpleNamespace(page_size=page_size),
|
|
nsa_cp_metadata=metadata_obj,
|
|
out_cache_loc=torch.cat(
|
|
[
|
|
torch.arange(page_size, 2 * page_size),
|
|
torch.tensor([2 * page_size]),
|
|
]
|
|
).to(torch.int64),
|
|
extend_prefix_lens_cpu=[0],
|
|
extend_seq_lens_cpu=[65],
|
|
seq_lens_cpu=torch.tensor([65], dtype=torch.int64),
|
|
forward_mode=Mode(),
|
|
attn_backend=AttnBackend(),
|
|
hisparse_coordinator=None,
|
|
)
|
|
|
|
local_key = torch.full((64, 2), -1.0, dtype=torch.float32)
|
|
local_key[0] = torch.tensor([11.0, 12.0])
|
|
gathered_key = torch.full((64, 2), 99.0, dtype=torch.float32)
|
|
gathered_key[0] = torch.tensor([101.0, 102.0])
|
|
query = torch.zeros((64, 2), dtype=torch.float32)
|
|
act_quant_inputs = []
|
|
topk_current_index_kv = []
|
|
|
|
def fake_act_quant(tensor, block_size, scale_fmt):
|
|
act_quant_inputs.append(tensor.detach().clone())
|
|
return tensor.detach().clone(), torch.ones(
|
|
(int(tensor.shape[0]), 1), dtype=torch.float32
|
|
)
|
|
|
|
fake_triton_kernel = types.ModuleType(
|
|
"sglang.srt.layers.attention.nsa.triton_kernel"
|
|
)
|
|
fake_triton_kernel.act_quant = fake_act_quant
|
|
|
|
indexer = object.__new__(Indexer)
|
|
indexer.alt_stream = None
|
|
indexer.nsa_enable_prefill_cp = True
|
|
indexer.index_topk = 2
|
|
indexer.block_size = 64
|
|
indexer.scale_fmt = None
|
|
indexer._get_q_k_bf16 = (
|
|
lambda *args, **kwargs: (query, gathered_key, local_key)
|
|
)
|
|
indexer._store_cp_shared_local_index_k_cache = lambda **kwargs: True
|
|
indexer._can_reuse_current_index_kv = lambda forward_batch: True
|
|
indexer._get_logits_head_gate = (
|
|
lambda x_for_gate, q_scale: torch.zeros((64, 1), dtype=torch.float32)
|
|
)
|
|
|
|
def fake_topk(*args, **kwargs):
|
|
topk_current_index_kv.append(kwargs["current_index_kv"])
|
|
return torch.zeros((64, 2), dtype=torch.int32)
|
|
|
|
indexer._get_topk_in_seq_cp_pair = fake_topk
|
|
|
|
with (
|
|
patch.dict(
|
|
sys.modules,
|
|
{
|
|
"sglang.srt.layers.attention.nsa.triton_kernel": fake_triton_kernel
|
|
},
|
|
),
|
|
patch.object(nsa_indexer, "_is_cuda", True),
|
|
patch.object(nsa_indexer, "_is_hip", False),
|
|
patch.object(nsa_indexer, "_is_npu", False),
|
|
patch.object(
|
|
nsa_indexer,
|
|
"is_nsa_prefill_cp_in_seq_split",
|
|
return_value=True,
|
|
),
|
|
):
|
|
result = Indexer.forward_cuda(
|
|
indexer,
|
|
x=torch.zeros((64, 2), dtype=torch.float32),
|
|
q_lora=torch.zeros((64, 2), dtype=torch.float32),
|
|
positions=torch.arange(64, dtype=torch.int64),
|
|
forward_batch=forward_batch,
|
|
layer_id=0,
|
|
return_indices=True,
|
|
)
|
|
|
|
self.assertEqual(result.shape, (64, 2))
|
|
self.assertEqual(len(act_quant_inputs), 2)
|
|
self.assertEqual(act_quant_inputs[1].tolist(), [[11.0, 12.0]])
|
|
self.assertNotEqual(act_quant_inputs[1].tolist(), [[101.0, 102.0]])
|
|
self.assertEqual(len(topk_current_index_kv), 1)
|
|
self.assertEqual(topk_current_index_kv[0][0].tolist(), [[11.0, 12.0]])
|
|
|
|
def test_indexer_direct_write_does_not_log_missing_metadata_for_non_cp_batch(self):
|
|
import torch
|
|
from types import SimpleNamespace
|
|
|
|
from sglang.srt.layers.attention.nsa.nsa_indexer import Indexer
|
|
|
|
indexer = object.__new__(Indexer)
|
|
indexer.nsa_enable_prefill_cp = True
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
nsa_cp_metadata=None,
|
|
)
|
|
|
|
with self.assertNoLogs(
|
|
"sglang.srt.layers.attention.nsa.utils", level="INFO"
|
|
):
|
|
stored = Indexer._store_cp_shared_local_index_k_cache(
|
|
indexer,
|
|
forward_batch,
|
|
layer_id=0,
|
|
local_key=torch.empty(0),
|
|
act_quant=None,
|
|
)
|
|
|
|
self.assertFalse(stored)
|
|
|
|
def test_indexer_in_seq_cp_pair_materializes_index_once_for_prev_next(self):
|
|
import torch
|
|
|
|
from sglang.srt.layers.attention.nsa.nsa_indexer import Indexer
|
|
|
|
indexer = object.__new__(Indexer)
|
|
logical_pages = torch.tensor([[1, 2, 3, 4]], dtype=torch.int32)
|
|
materialized_index = torch.tensor([11], dtype=torch.int32)
|
|
dense_pages = torch.tensor([[1, 2, 3, 4]], dtype=torch.int32)
|
|
materialize_calls = []
|
|
topk_calls = []
|
|
|
|
class Metadata:
|
|
def get_page_table_64(self):
|
|
return logical_pages
|
|
|
|
def get_page_table_1(self):
|
|
return torch.empty((1, 1000), dtype=torch.int32)
|
|
|
|
def fake_materialize(forward_batch, layer_id, logical_page_table):
|
|
materialize_calls.append((layer_id, logical_page_table))
|
|
return materialized_index, dense_pages
|
|
|
|
def fake_get_topk(
|
|
forward_batch,
|
|
layer_id,
|
|
q_fp8,
|
|
weights,
|
|
metadata,
|
|
kv_len,
|
|
actual_seq_q,
|
|
cp_index=None,
|
|
current_index_kv=None,
|
|
shared_index_buffer=None,
|
|
shared_block_tables=None,
|
|
actual_seq_q_tensor=None,
|
|
actual_seq_q_cu_tensor=None,
|
|
):
|
|
topk_calls.append(
|
|
{
|
|
"kv_len": kv_len,
|
|
"actual_seq_q": actual_seq_q,
|
|
"actual_seq_q_tensor": actual_seq_q_tensor,
|
|
"actual_seq_q_cu_tensor": actual_seq_q_cu_tensor,
|
|
"shared_index_buffer": shared_index_buffer,
|
|
"shared_block_tables": shared_block_tables,
|
|
"current_index_kv": current_index_kv,
|
|
}
|
|
)
|
|
return torch.full((actual_seq_q, 2), len(topk_calls), dtype=torch.int32)
|
|
|
|
indexer._maybe_materialize_shared_index_buffer = fake_materialize
|
|
indexer._get_topk_ragged_with_cp = fake_get_topk
|
|
|
|
class Mode:
|
|
def is_extend_without_speculative(self):
|
|
return True
|
|
|
|
forward_batch = type(
|
|
"ForwardBatchStub",
|
|
(),
|
|
{
|
|
"forward_mode": Mode(),
|
|
"extend_prefix_lens_cpu": [0],
|
|
"extend_seq_lens_cpu": [5],
|
|
"seq_lens_cpu": torch.tensor([5], dtype=torch.int64),
|
|
"nsa_cp_metadata": NSAContextParallelMetadata(
|
|
kv_len_prev=5,
|
|
kv_len_next=9,
|
|
actual_seq_q_prev=3,
|
|
actual_seq_q_next=2,
|
|
actual_seq_q_prev_cu_tensor=torch.tensor([0, 3], dtype=torch.int32),
|
|
actual_seq_q_next_cu_tensor=torch.tensor([0, 2], dtype=torch.int32),
|
|
)
|
|
},
|
|
)()
|
|
q_fp8 = torch.arange(5 * 4, dtype=torch.float32).view(5, 4)
|
|
weights = torch.arange(5 * 2, dtype=torch.float32).view(5, 2)
|
|
|
|
result = Indexer._get_topk_in_seq_cp_pair(
|
|
indexer,
|
|
forward_batch,
|
|
layer_id=7,
|
|
q_fp8=q_fp8,
|
|
weights=weights,
|
|
metadata=Metadata(),
|
|
current_index_kv=None,
|
|
)
|
|
|
|
self.assertEqual(len(materialize_calls), 1)
|
|
self.assertIs(materialize_calls[0][1], logical_pages)
|
|
self.assertEqual(len(topk_calls), 2)
|
|
self.assertIs(topk_calls[0]["shared_index_buffer"], materialized_index)
|
|
self.assertIs(topk_calls[1]["shared_index_buffer"], materialized_index)
|
|
self.assertIs(topk_calls[0]["shared_block_tables"], dense_pages)
|
|
self.assertIs(topk_calls[1]["shared_block_tables"], dense_pages)
|
|
self.assertIsNone(topk_calls[0]["current_index_kv"])
|
|
self.assertEqual(topk_calls[0]["kv_len"], 5)
|
|
self.assertEqual(topk_calls[1]["kv_len"], 9)
|
|
self.assertEqual(topk_calls[0]["actual_seq_q_cu_tensor"].tolist(), [0, 3])
|
|
self.assertEqual(topk_calls[1]["actual_seq_q_cu_tensor"].tolist(), [0, 2])
|
|
self.assertEqual(result.tolist(), [[1, 1], [1, 1], [1, 1], [2, 2], [2, 2]])
|
|
|
|
def test_indexer_in_seq_cp_pair_batch_preserves_request_segment_order(self):
|
|
import torch
|
|
|
|
from sglang.srt.layers.attention.nsa.nsa_indexer import Indexer
|
|
|
|
indexer = object.__new__(Indexer)
|
|
indexer.index_topk = 2
|
|
logical_pages = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.int32)
|
|
materialized_index = torch.tensor([11], dtype=torch.int32)
|
|
dense_pages = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.int32)
|
|
materialize_calls = []
|
|
topk_calls = []
|
|
|
|
class Metadata:
|
|
def get_page_table_64(self):
|
|
return logical_pages
|
|
|
|
def get_page_table_1(self):
|
|
return torch.empty((2, 1000), dtype=torch.int32)
|
|
|
|
def fake_materialize(forward_batch, layer_id, logical_page_table):
|
|
materialize_calls.append((layer_id, logical_page_table))
|
|
return materialized_index, dense_pages
|
|
|
|
def fake_get_topk(
|
|
forward_batch,
|
|
layer_id,
|
|
q_fp8,
|
|
weights,
|
|
metadata,
|
|
kv_len,
|
|
actual_seq_q,
|
|
cp_index=None,
|
|
current_index_kv=None,
|
|
shared_index_buffer=None,
|
|
shared_block_tables=None,
|
|
actual_seq_q_tensor=None,
|
|
actual_seq_q_cu_tensor=None,
|
|
batch_idx=0,
|
|
):
|
|
topk_calls.append(
|
|
{
|
|
"batch_idx": batch_idx,
|
|
"kv_len": kv_len,
|
|
"actual_seq_q": actual_seq_q,
|
|
"cp_index": cp_index,
|
|
"q": q_fp8.flatten().tolist(),
|
|
"weights": weights.flatten().tolist(),
|
|
"actual_seq_q_tensor": actual_seq_q_tensor,
|
|
"actual_seq_q_cu_tensor": actual_seq_q_cu_tensor,
|
|
"shared_index_buffer": shared_index_buffer,
|
|
"shared_block_tables": shared_block_tables,
|
|
"current_index_kv": current_index_kv,
|
|
}
|
|
)
|
|
rows = int(q_fp8.shape[0])
|
|
return torch.arange(1, rows + 1, dtype=torch.int32).view(rows, 1).repeat(1, 2)
|
|
|
|
indexer._maybe_materialize_shared_index_buffer = fake_materialize
|
|
indexer._get_topk_ragged_with_cp = fake_get_topk
|
|
|
|
forward_batch = SimpleNamespace(
|
|
batch_size=2,
|
|
forward_mode=SimpleNamespace(
|
|
is_extend_without_speculative=lambda: True,
|
|
),
|
|
extend_prefix_lens_cpu=[0, 0],
|
|
extend_seq_lens_cpu=[1000, 1000],
|
|
seq_lens_cpu=torch.tensor([1000, 1000], dtype=torch.int64),
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=2,
|
|
kv_len_prev=100,
|
|
kv_len_next=200,
|
|
actual_seq_q_prev=2,
|
|
actual_seq_q_next=1,
|
|
actual_seq_q_prev_cu_tensor=torch.tensor([0, 2], dtype=torch.int32),
|
|
actual_seq_q_next_cu_tensor=torch.tensor([0, 1], dtype=torch.int32),
|
|
request_kv_len_prev=[2, 1],
|
|
request_kv_len_next=[3, 4],
|
|
request_actual_seq_q_prev=[2, 1],
|
|
request_actual_seq_q_next=[1, 3],
|
|
),
|
|
)
|
|
q_fp8 = torch.arange(7, dtype=torch.float32).view(7, 1)
|
|
weights = (torch.arange(7, dtype=torch.float32) + 100).view(7, 1)
|
|
|
|
result = Indexer._get_topk_in_seq_cp_pair(
|
|
indexer,
|
|
forward_batch,
|
|
layer_id=7,
|
|
q_fp8=q_fp8,
|
|
weights=weights,
|
|
metadata=Metadata(),
|
|
current_index_kv=None,
|
|
)
|
|
|
|
self.assertEqual(len(materialize_calls), 1)
|
|
self.assertIs(materialize_calls[0][1], logical_pages)
|
|
self.assertEqual(len(topk_calls), 1)
|
|
self.assertEqual(topk_calls[0]["batch_idx"], 0)
|
|
self.assertEqual(topk_calls[0]["actual_seq_q"], 7)
|
|
self.assertEqual(
|
|
topk_calls[0]["cp_index"],
|
|
[(0, 0, 2), (0, 2, 3), (1, 0, 1), (1, 1, 4)],
|
|
)
|
|
self.assertEqual(topk_calls[0]["q"], [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
|
|
self.assertEqual(
|
|
topk_calls[0]["weights"],
|
|
[100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 106.0],
|
|
)
|
|
self.assertIsNone(topk_calls[0]["actual_seq_q_tensor"])
|
|
self.assertIsNone(topk_calls[0]["actual_seq_q_cu_tensor"])
|
|
self.assertTrue(all(call["shared_index_buffer"] is materialized_index for call in topk_calls))
|
|
self.assertTrue(all(call["shared_block_tables"] is dense_pages for call in topk_calls))
|
|
self.assertTrue(all(call["current_index_kv"] is None for call in topk_calls))
|
|
self.assertEqual(
|
|
result.tolist(),
|
|
[[1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6], [7, 7]],
|
|
)
|
|
|
|
def test_indexer_in_seq_cp_pair_compute_padding_outputs_dummy_safe_rows(self):
|
|
import torch
|
|
|
|
from sglang.srt.layers.attention.nsa.nsa_indexer import Indexer
|
|
|
|
page_size = 64
|
|
plan = build_batch_page_aligned_in_seq_split_plan(
|
|
extend_lens=[65],
|
|
prefix_lens=[0],
|
|
page_size=page_size,
|
|
cp_size=8,
|
|
cp_rank=1,
|
|
)
|
|
metadata_obj = _build_batch_metadata_from_plan(plan)
|
|
indexer = object.__new__(Indexer)
|
|
indexer.index_topk = 2
|
|
logical_pages = torch.tensor([[1, 2]], dtype=torch.int32)
|
|
materialized_index = torch.tensor([11], dtype=torch.int32)
|
|
dense_pages = torch.tensor([[1, 2]], dtype=torch.int32)
|
|
materialize_calls = []
|
|
topk_calls = []
|
|
|
|
class Metadata:
|
|
def get_page_table_64(self):
|
|
return logical_pages
|
|
|
|
def get_page_table_1(self):
|
|
return torch.empty((1, 65), dtype=torch.int32)
|
|
|
|
def fake_materialize(forward_batch, layer_id, logical_page_table):
|
|
materialize_calls.append((layer_id, logical_page_table))
|
|
return materialized_index, dense_pages
|
|
|
|
def fake_get_topk(
|
|
forward_batch,
|
|
layer_id,
|
|
q_fp8,
|
|
weights,
|
|
metadata,
|
|
kv_len,
|
|
actual_seq_q,
|
|
cp_index=None,
|
|
current_index_kv=None,
|
|
shared_index_buffer=None,
|
|
shared_block_tables=None,
|
|
actual_seq_q_tensor=None,
|
|
actual_seq_q_cu_tensor=None,
|
|
batch_idx=0,
|
|
):
|
|
topk_calls.append(
|
|
{
|
|
"actual_seq_q": actual_seq_q,
|
|
"cp_index": cp_index,
|
|
"q": q_fp8.flatten().tolist(),
|
|
"weights": weights.flatten().tolist(),
|
|
"shared_index_buffer": shared_index_buffer,
|
|
"shared_block_tables": shared_block_tables,
|
|
}
|
|
)
|
|
rows = int(q_fp8.shape[0])
|
|
return (
|
|
torch.arange(1, rows + 1, dtype=torch.int32)
|
|
.view(rows, 1)
|
|
.repeat(1, 2)
|
|
)
|
|
|
|
indexer._maybe_materialize_shared_index_buffer = fake_materialize
|
|
indexer._get_topk_ragged_with_cp = fake_get_topk
|
|
|
|
forward_batch = SimpleNamespace(
|
|
batch_size=1,
|
|
forward_mode=SimpleNamespace(
|
|
is_extend_without_speculative=lambda: True,
|
|
),
|
|
extend_prefix_lens_cpu=[0],
|
|
extend_seq_lens_cpu=[65],
|
|
seq_lens_cpu=torch.tensor([65], dtype=torch.int64),
|
|
nsa_cp_metadata=metadata_obj,
|
|
)
|
|
q_fp8 = torch.arange(64, dtype=torch.float32).view(64, 1)
|
|
weights = (torch.arange(64, dtype=torch.float32) + 100).view(64, 1)
|
|
|
|
result = Indexer._get_topk_in_seq_cp_pair(
|
|
indexer,
|
|
forward_batch,
|
|
layer_id=7,
|
|
q_fp8=q_fp8,
|
|
weights=weights,
|
|
metadata=Metadata(),
|
|
current_index_kv=None,
|
|
)
|
|
|
|
self.assertEqual(len(materialize_calls), 1)
|
|
self.assertIs(materialize_calls[0][1], logical_pages)
|
|
self.assertEqual(len(topk_calls), 1)
|
|
self.assertEqual(topk_calls[0]["actual_seq_q"], 1)
|
|
self.assertEqual(topk_calls[0]["cp_index"], [(0, 64, 65)])
|
|
self.assertEqual(topk_calls[0]["q"], [0.0])
|
|
self.assertEqual(topk_calls[0]["weights"], [100.0])
|
|
self.assertIs(topk_calls[0]["shared_index_buffer"], materialized_index)
|
|
self.assertIs(topk_calls[0]["shared_block_tables"], dense_pages)
|
|
self.assertEqual(result.shape, (64, 2))
|
|
self.assertEqual(result[0].tolist(), [1, 1])
|
|
self.assertTrue(
|
|
torch.equal(result[1:], torch.full((63, 2), -1, dtype=torch.int32))
|
|
)
|
|
|
|
def test_indexer_in_seq_cp_pair_batch_materializes_partial_current_index_reuse_once(self):
|
|
import torch
|
|
|
|
from sglang.srt.layers.attention.nsa.nsa_indexer import Indexer
|
|
|
|
indexer = object.__new__(Indexer)
|
|
indexer.index_topk = 2
|
|
current_index_kv = (torch.arange(7), torch.arange(7))
|
|
logical_pages = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.int32)
|
|
materialized_index = torch.tensor([11], dtype=torch.int32)
|
|
dense_pages = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.int32)
|
|
materialize_calls = []
|
|
topk_calls = []
|
|
|
|
class Metadata:
|
|
def get_page_table_64(self):
|
|
return logical_pages
|
|
|
|
def get_page_table_1(self):
|
|
return torch.empty((2, 512), dtype=torch.int32)
|
|
|
|
def fake_materialize(
|
|
forward_batch,
|
|
layer_id,
|
|
logical_page_table,
|
|
current_index_kv=None,
|
|
):
|
|
materialize_calls.append(
|
|
{
|
|
"layer_id": layer_id,
|
|
"logical_page_table": logical_page_table,
|
|
"current_index_kv": current_index_kv,
|
|
}
|
|
)
|
|
return materialized_index, dense_pages
|
|
|
|
def fake_get_topk(
|
|
forward_batch,
|
|
layer_id,
|
|
q_fp8,
|
|
weights,
|
|
metadata,
|
|
kv_len,
|
|
actual_seq_q,
|
|
cp_index=None,
|
|
current_index_kv=None,
|
|
shared_index_buffer=None,
|
|
shared_block_tables=None,
|
|
actual_seq_q_tensor=None,
|
|
actual_seq_q_cu_tensor=None,
|
|
batch_idx=0,
|
|
):
|
|
topk_calls.append(
|
|
{
|
|
"batch_idx": batch_idx,
|
|
"actual_seq_q": actual_seq_q,
|
|
"cp_index": cp_index,
|
|
"q_rows": int(q_fp8.shape[0]),
|
|
"current_index_kv": current_index_kv,
|
|
"shared_index_buffer": shared_index_buffer,
|
|
"shared_block_tables": shared_block_tables,
|
|
"actual_seq_q_cu_tensor": actual_seq_q_cu_tensor,
|
|
}
|
|
)
|
|
rows = int(q_fp8.shape[0])
|
|
return torch.arange(1, rows + 1, dtype=torch.int32).view(rows, 1).repeat(1, 2)
|
|
|
|
indexer._maybe_materialize_shared_index_buffer = fake_materialize
|
|
indexer._get_topk_ragged_with_cp = fake_get_topk
|
|
|
|
forward_batch = SimpleNamespace(
|
|
batch_size=2,
|
|
forward_mode=SimpleNamespace(
|
|
is_extend_without_speculative=lambda: True,
|
|
),
|
|
extend_prefix_lens_cpu=[64, 64],
|
|
extend_seq_lens_cpu=[3, 4],
|
|
seq_lens_cpu=torch.tensor([67, 68], dtype=torch.int64),
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=2,
|
|
request_kv_len_prev=[2, 1],
|
|
request_kv_len_next=[3, 4],
|
|
request_actual_seq_q_prev=[2, 1],
|
|
request_actual_seq_q_next=[1, 3],
|
|
),
|
|
)
|
|
result = Indexer._get_topk_in_seq_cp_pair(
|
|
indexer,
|
|
forward_batch,
|
|
layer_id=7,
|
|
q_fp8=torch.empty(7, 1),
|
|
weights=torch.empty(7, 1),
|
|
metadata=Metadata(),
|
|
current_index_kv=current_index_kv,
|
|
)
|
|
|
|
self.assertEqual(len(materialize_calls), 1)
|
|
self.assertIs(materialize_calls[0]["logical_page_table"], logical_pages)
|
|
self.assertIs(materialize_calls[0]["current_index_kv"], current_index_kv)
|
|
self.assertEqual(len(topk_calls), 1)
|
|
self.assertEqual(topk_calls[0]["actual_seq_q"], 7)
|
|
self.assertEqual(
|
|
topk_calls[0]["cp_index"],
|
|
[(0, 0, 2), (0, 2, 3), (1, 0, 1), (1, 1, 4)],
|
|
)
|
|
self.assertEqual(topk_calls[0]["q_rows"], 7)
|
|
self.assertTrue(
|
|
all(call["current_index_kv"] is None for call in topk_calls)
|
|
)
|
|
self.assertTrue(
|
|
all(call["shared_index_buffer"] is materialized_index for call in topk_calls)
|
|
)
|
|
self.assertTrue(
|
|
all(call["shared_block_tables"] is dense_pages for call in topk_calls)
|
|
)
|
|
self.assertEqual([call["batch_idx"] for call in topk_calls], [0])
|
|
self.assertIsNone(topk_calls[0]["actual_seq_q_cu_tensor"])
|
|
self.assertEqual(
|
|
result.tolist(),
|
|
[[1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6], [7, 7]],
|
|
)
|
|
|
|
def test_indexer_in_seq_cp_pair_batch_composes_current_only_index_reuse(self):
|
|
import torch
|
|
|
|
from sglang.srt.layers.attention.nsa.nsa_indexer import Indexer
|
|
|
|
indexer = object.__new__(Indexer)
|
|
indexer.index_topk = 2
|
|
current_index_kv = (torch.arange(7), torch.arange(7))
|
|
logical_pages = torch.tensor([[1, 2], [3, 4]], dtype=torch.int32)
|
|
materialized_index = torch.tensor([17], dtype=torch.int32)
|
|
dense_pages = torch.tensor([[10, 11], [12, 13]], dtype=torch.int32)
|
|
materialize_calls = []
|
|
topk_calls = []
|
|
|
|
class Mode:
|
|
def is_extend_without_speculative(self):
|
|
return True
|
|
|
|
class Metadata:
|
|
def get_page_table_64(self):
|
|
return logical_pages
|
|
|
|
def get_page_table_1(self):
|
|
return torch.empty((2, 512), dtype=torch.int32)
|
|
|
|
def fake_materialize(
|
|
forward_batch,
|
|
layer_id,
|
|
logical_page_table,
|
|
current_index_kv=None,
|
|
):
|
|
materialize_calls.append(
|
|
{
|
|
"layer_id": layer_id,
|
|
"logical_page_table": logical_page_table,
|
|
"current_index_kv": current_index_kv,
|
|
}
|
|
)
|
|
return materialized_index, dense_pages
|
|
|
|
def fake_get_topk(
|
|
forward_batch,
|
|
layer_id,
|
|
q_fp8,
|
|
weights,
|
|
metadata,
|
|
kv_len,
|
|
actual_seq_q,
|
|
cp_index=None,
|
|
current_index_kv=None,
|
|
shared_index_buffer=None,
|
|
shared_block_tables=None,
|
|
actual_seq_q_tensor=None,
|
|
actual_seq_q_cu_tensor=None,
|
|
batch_idx=0,
|
|
):
|
|
topk_calls.append(
|
|
{
|
|
"batch_idx": batch_idx,
|
|
"actual_seq_q": actual_seq_q,
|
|
"cp_index": cp_index,
|
|
"q_rows": int(q_fp8.shape[0]),
|
|
"current_index_kv": current_index_kv,
|
|
"shared_index_buffer": shared_index_buffer,
|
|
"shared_block_tables": shared_block_tables,
|
|
"actual_seq_q_cu_tensor": actual_seq_q_cu_tensor,
|
|
}
|
|
)
|
|
rows = int(q_fp8.shape[0])
|
|
return torch.arange(1, rows + 1, dtype=torch.int32).view(rows, 1).repeat(1, 2)
|
|
|
|
indexer._maybe_materialize_shared_index_buffer = fake_materialize
|
|
indexer._get_topk_ragged_with_cp = fake_get_topk
|
|
|
|
forward_batch = SimpleNamespace(
|
|
batch_size=2,
|
|
forward_mode=Mode(),
|
|
extend_prefix_lens_cpu=[0, 0],
|
|
extend_seq_lens_cpu=[3, 4],
|
|
seq_lens_cpu=torch.tensor([3, 4], dtype=torch.int64),
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=2,
|
|
request_kv_len_prev=[3, 4],
|
|
request_kv_len_next=[3, 4],
|
|
request_actual_seq_q_prev=[2, 1],
|
|
request_actual_seq_q_next=[1, 3],
|
|
),
|
|
)
|
|
result = Indexer._get_topk_in_seq_cp_pair(
|
|
indexer,
|
|
forward_batch,
|
|
layer_id=7,
|
|
q_fp8=torch.empty(7, 1),
|
|
weights=torch.empty(7, 1),
|
|
metadata=Metadata(),
|
|
current_index_kv=current_index_kv,
|
|
)
|
|
|
|
self.assertEqual(len(materialize_calls), 1)
|
|
self.assertIs(materialize_calls[0]["logical_page_table"], logical_pages)
|
|
self.assertIs(materialize_calls[0]["current_index_kv"], current_index_kv)
|
|
self.assertEqual(len(topk_calls), 1)
|
|
self.assertTrue(all(call["current_index_kv"] is None for call in topk_calls))
|
|
self.assertTrue(
|
|
all(call["shared_index_buffer"] is materialized_index for call in topk_calls)
|
|
)
|
|
self.assertTrue(
|
|
all(call["shared_block_tables"] is dense_pages for call in topk_calls)
|
|
)
|
|
self.assertEqual(topk_calls[0]["batch_idx"], 0)
|
|
self.assertEqual(topk_calls[0]["actual_seq_q"], 7)
|
|
self.assertEqual(
|
|
topk_calls[0]["cp_index"],
|
|
[(0, 1, 3), (0, 2, 3), (1, 3, 4), (1, 1, 4)],
|
|
)
|
|
self.assertEqual(topk_calls[0]["q_rows"], 7)
|
|
self.assertIsNone(topk_calls[0]["actual_seq_q_cu_tensor"])
|
|
self.assertEqual(
|
|
result.tolist(),
|
|
[[1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6], [7, 7]],
|
|
)
|
|
|
|
def test_indexer_shared_index_materialize_accepts_current_only_compose(self):
|
|
import torch
|
|
|
|
from sglang.srt.layers.attention.nsa import nsa_indexer
|
|
from sglang.srt.layers.attention.nsa.nsa_indexer import Indexer
|
|
|
|
indexer = object.__new__(Indexer)
|
|
page_buffer = torch.zeros((8, 32), dtype=torch.uint8)
|
|
logical_pages = torch.tensor([[1, 2]], dtype=torch.int64)
|
|
current_index_kv = (
|
|
torch.zeros((2, 4), dtype=torch.uint8),
|
|
torch.zeros((2, 1), dtype=torch.float32),
|
|
)
|
|
compose_calls = []
|
|
|
|
class Pool:
|
|
page_size = 4
|
|
index_head_dim = 4
|
|
|
|
def get_index_k_with_scale_buffer(self, layer_id):
|
|
return page_buffer
|
|
|
|
class Mode:
|
|
def is_extend_without_speculative(self):
|
|
return True
|
|
|
|
forward_batch = SimpleNamespace(
|
|
uses_cp_shared_kv=True,
|
|
cp_shared_kv_layout=CpSharedKVLayout(page_size=4, cp_size=2, cp_rank=0),
|
|
token_to_kv_pool=Pool(),
|
|
cp_shared_kv_index_prefetcher=None,
|
|
forward_mode=Mode(),
|
|
extend_prefix_lens_cpu=[0],
|
|
extend_seq_lens_cpu=[5],
|
|
seq_lens_cpu=torch.tensor([5], dtype=torch.int64),
|
|
cp_local_out_cache_loc=torch.tensor([4, 5], dtype=torch.int64),
|
|
)
|
|
|
|
def fake_compose(**kwargs):
|
|
compose_calls.append(kwargs)
|
|
return torch.empty((3, 32), dtype=torch.uint8), torch.tensor([[1, 2]])
|
|
|
|
with patch.object(
|
|
nsa_indexer,
|
|
"materialize_prefix_and_reuse_current_index_page_slots",
|
|
side_effect=fake_compose,
|
|
):
|
|
materialized, dense_pages = Indexer._maybe_materialize_shared_index_buffer(
|
|
indexer,
|
|
forward_batch,
|
|
layer_id=7,
|
|
logical_page_table=logical_pages,
|
|
current_index_kv=current_index_kv,
|
|
)
|
|
|
|
self.assertEqual(len(compose_calls), 1)
|
|
self.assertEqual(compose_calls[0]["prefix_slot_spans"], [])
|
|
self.assertEqual(compose_calls[0]["current_slot_spans"], [(0, 2)])
|
|
self.assertIs(compose_calls[0]["current_index_k"], current_index_kv[0])
|
|
self.assertIs(compose_calls[0]["current_index_scale"], current_index_kv[1])
|
|
self.assertEqual(list(materialized.shape), [3, 32])
|
|
self.assertEqual(dense_pages.tolist(), [[1, 2]])
|
|
|
|
def test_indexer_ragged_cp_index_current_batch_does_not_materialize(self):
|
|
import contextlib
|
|
import torch
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
from sglang.srt.layers.attention.nsa import nsa_indexer
|
|
from sglang.srt.layers.attention.nsa.nsa_indexer import Indexer
|
|
|
|
indexer = object.__new__(Indexer)
|
|
indexer.index_topk = 2
|
|
indexer._with_real_sm_count = lambda: contextlib.nullcontext()
|
|
|
|
deep_gemm_calls = []
|
|
|
|
def fake_logits(q_fp8, kv_fp8, weights, ks, ke, clean_logits=False):
|
|
deep_gemm_calls.append(
|
|
{
|
|
"q_rows": int(q_fp8.shape[0]),
|
|
"kv_rows": int(kv_fp8[0].shape[0]),
|
|
"weights_rows": int(weights.shape[0]),
|
|
"ks": ks.tolist(),
|
|
"ke": ke.tolist(),
|
|
}
|
|
)
|
|
return torch.zeros((int(q_fp8.shape[0]), 8), dtype=torch.float32)
|
|
|
|
class Metadata:
|
|
def get_page_table_64(self):
|
|
raise AssertionError("current cp_index path must not materialize index pages")
|
|
|
|
def topk_transform(self, logits, topk, **kwargs):
|
|
return (
|
|
torch.arange(1, int(logits.shape[0]) + 1, dtype=torch.int32)
|
|
.view(-1, 1)
|
|
.repeat(1, topk)
|
|
)
|
|
|
|
forward_batch = SimpleNamespace(
|
|
token_to_kv_pool=SimpleNamespace(page_size=64),
|
|
seq_lens_cpu=torch.tensor([3, 4], dtype=torch.int64),
|
|
extend_seq_lens_cpu=[3, 4],
|
|
)
|
|
q_fp8 = torch.empty((7, 1), dtype=torch.float32)
|
|
weights = torch.empty((7, 1, 1), dtype=torch.float32)
|
|
current_index_kv = (
|
|
torch.arange(7, dtype=torch.uint8).view(7, 1),
|
|
torch.arange(7, dtype=torch.float32).view(7, 1),
|
|
)
|
|
|
|
with patch.object(
|
|
nsa_indexer,
|
|
"deep_gemm",
|
|
SimpleNamespace(fp8_mqa_logits=fake_logits),
|
|
):
|
|
result = Indexer._get_topk_ragged_with_cp(
|
|
indexer,
|
|
forward_batch,
|
|
layer_id=7,
|
|
q_fp8=q_fp8,
|
|
weights=weights,
|
|
metadata=Metadata(),
|
|
kv_len=0,
|
|
actual_seq_q=7,
|
|
cp_index=[(0, 1, 3), (0, 2, 3), (1, 3, 4), (1, 1, 4)],
|
|
current_index_kv=current_index_kv,
|
|
)
|
|
|
|
self.assertEqual(result.tolist(), [[1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6], [7, 7]])
|
|
self.assertEqual(len(deep_gemm_calls), 1)
|
|
self.assertEqual(deep_gemm_calls[0]["q_rows"], 7)
|
|
self.assertEqual(deep_gemm_calls[0]["weights_rows"], 7)
|
|
self.assertEqual(deep_gemm_calls[0]["kv_rows"], 14)
|
|
self.assertEqual(deep_gemm_calls[0]["ks"], [0, 0, 3, 6, 10, 10, 10])
|
|
self.assertEqual(deep_gemm_calls[0]["ke"], [2, 3, 6, 10, 12, 13, 14])
|
|
|
|
def test_indexer_ragged_cp_index_batch_uses_request_ragged_offsets(self):
|
|
import contextlib
|
|
import torch
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
from sglang.srt.layers.attention.nsa import nsa_indexer
|
|
from sglang.srt.layers.attention.nsa.nsa_indexer import Indexer
|
|
|
|
indexer = object.__new__(Indexer)
|
|
indexer.index_topk = 2
|
|
indexer._with_real_sm_count = lambda: contextlib.nullcontext()
|
|
|
|
topk_kwargs = []
|
|
|
|
def fake_prepare(**kwargs):
|
|
total_kv_len = int(kwargs["total_kv_len"])
|
|
return (
|
|
torch.zeros((total_kv_len, 1), dtype=torch.uint8),
|
|
torch.zeros((total_kv_len,), dtype=torch.float32),
|
|
torch.tensor([0, 0, 3, 6, 10, 10, 10], dtype=torch.int32),
|
|
torch.tensor([2, 3, 3, 4, 2, 3, 4], dtype=torch.int32),
|
|
)
|
|
|
|
def fake_logits(q_fp8, kv_fp8, weights, ks, ke, clean_logits=False):
|
|
return torch.zeros((int(q_fp8.shape[0]), 8), dtype=torch.float32)
|
|
|
|
class Metadata:
|
|
def get_page_table_64(self):
|
|
raise AssertionError("current cp_index path must not materialize index pages")
|
|
|
|
def topk_transform(self, logits, topk, **kwargs):
|
|
topk_kwargs.append(kwargs)
|
|
return torch.zeros((int(logits.shape[0]), topk), dtype=torch.int32)
|
|
|
|
forward_batch = SimpleNamespace(
|
|
token_to_kv_pool=SimpleNamespace(page_size=64, index_head_dim=1),
|
|
seq_lens_cpu=torch.tensor([3, 4], dtype=torch.int64),
|
|
extend_seq_lens_cpu=[3, 4],
|
|
)
|
|
current_index_kv = (
|
|
torch.arange(7, dtype=torch.uint8).view(7, 1),
|
|
torch.arange(7, dtype=torch.float32).view(7, 1),
|
|
)
|
|
|
|
with patch.object(
|
|
nsa_indexer,
|
|
"try_tai_prepare_cp_mqa_current_index_batch",
|
|
side_effect=fake_prepare,
|
|
create=True,
|
|
), patch.object(
|
|
nsa_indexer,
|
|
"deep_gemm",
|
|
SimpleNamespace(fp8_mqa_logits=fake_logits),
|
|
):
|
|
Indexer._get_topk_ragged_with_cp(
|
|
indexer,
|
|
forward_batch,
|
|
layer_id=7,
|
|
q_fp8=torch.empty((7, 1), dtype=torch.float32),
|
|
weights=torch.empty((7, 1, 1), dtype=torch.float32),
|
|
metadata=Metadata(),
|
|
kv_len=0,
|
|
actual_seq_q=7,
|
|
cp_index=[(0, 1, 3), (0, 2, 3), (1, 3, 4), (1, 1, 4)],
|
|
current_index_kv=current_index_kv,
|
|
)
|
|
|
|
self.assertEqual(len(topk_kwargs), 1)
|
|
offset = topk_kwargs[0].get("topk_indices_offset_override")
|
|
self.assertIsNotNone(offset)
|
|
# Batch cp_index compacts each CP segment's K into a temporary buffer,
|
|
# but flashmla_sparse consumes the normal ragged KV layout. The fused
|
|
# ragged topk offset therefore has to stay in request/KV coordinates,
|
|
# not compact segment coordinates or compact-q cu-seqlens.
|
|
self.assertEqual(offset.tolist(), [0, 0, 0, 3, 3, 3, 3])
|
|
|
|
def test_indexer_ragged_cp_index_shared_batch_uses_tai_prepare_once(self):
|
|
import contextlib
|
|
import torch
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
from sglang.srt.layers.attention.nsa import index_buf_accessor, nsa_indexer
|
|
from sglang.srt.layers.attention.nsa.nsa_indexer import Indexer
|
|
|
|
indexer = object.__new__(Indexer)
|
|
indexer.index_topk = 2
|
|
indexer._with_real_sm_count = lambda: contextlib.nullcontext()
|
|
|
|
prepare_calls = []
|
|
deep_gemm_calls = []
|
|
|
|
def fake_prepare(**kwargs):
|
|
prepare_calls.append(kwargs)
|
|
total_kv_len = int(kwargs["total_kv_len"])
|
|
total_q_count = int(kwargs["total_q_count"])
|
|
return (
|
|
torch.zeros((total_kv_len, 1), dtype=torch.uint8),
|
|
torch.zeros((total_kv_len,), dtype=torch.float32),
|
|
torch.tensor([0, 0, 3, 6, 10, 10, 10], dtype=torch.int32),
|
|
torch.tensor([2, 3, 3, 4, 2, 3, 4], dtype=torch.int32),
|
|
)
|
|
|
|
def fake_logits(q_fp8, kv_fp8, weights, ks, ke, clean_logits=False):
|
|
deep_gemm_calls.append(
|
|
{
|
|
"q_rows": int(q_fp8.shape[0]),
|
|
"kv_rows": int(kv_fp8[0].shape[0]),
|
|
"weights_rows": int(weights.shape[0]),
|
|
"ks": ks.tolist(),
|
|
"ke": ke.tolist(),
|
|
}
|
|
)
|
|
return torch.zeros((int(q_fp8.shape[0]), 8), dtype=torch.float32)
|
|
|
|
class Metadata:
|
|
def topk_transform(self, logits, topk, **kwargs):
|
|
return (
|
|
torch.arange(1, int(logits.shape[0]) + 1, dtype=torch.int32)
|
|
.view(-1, 1)
|
|
.repeat(1, topk)
|
|
)
|
|
|
|
forward_batch = SimpleNamespace(
|
|
token_to_kv_pool=SimpleNamespace(page_size=64, index_head_dim=1),
|
|
seq_lens_cpu=torch.tensor([3, 4], dtype=torch.int64),
|
|
extend_seq_lens_cpu=[3, 4],
|
|
)
|
|
q_fp8 = torch.empty((7, 1), dtype=torch.float32)
|
|
weights = torch.empty((7, 1, 1), dtype=torch.float32)
|
|
shared_index_buffer = torch.zeros((8, 264), dtype=torch.uint8)
|
|
shared_block_tables = torch.arange(8, dtype=torch.int64).view(2, 4)
|
|
|
|
with patch.object(
|
|
nsa_indexer,
|
|
"try_tai_prepare_cp_mqa_index_batch",
|
|
side_effect=fake_prepare,
|
|
create=True,
|
|
), patch.object(
|
|
index_buf_accessor.GetK,
|
|
"execute",
|
|
side_effect=AssertionError("batched path must not call per-segment GetK"),
|
|
), patch.object(
|
|
index_buf_accessor.GetS,
|
|
"execute",
|
|
side_effect=AssertionError("batched path must not call per-segment GetS"),
|
|
), patch.object(
|
|
nsa_indexer,
|
|
"deep_gemm",
|
|
SimpleNamespace(fp8_mqa_logits=fake_logits),
|
|
):
|
|
result = Indexer._get_topk_ragged_with_cp(
|
|
indexer,
|
|
forward_batch,
|
|
layer_id=7,
|
|
q_fp8=q_fp8,
|
|
weights=weights,
|
|
metadata=Metadata(),
|
|
kv_len=0,
|
|
actual_seq_q=7,
|
|
cp_index=[(0, 1, 3), (0, 2, 3), (1, 3, 4), (1, 1, 4)],
|
|
shared_index_buffer=shared_index_buffer,
|
|
shared_block_tables=shared_block_tables,
|
|
)
|
|
|
|
self.assertEqual(result.tolist(), [[1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6], [7, 7]])
|
|
self.assertEqual(len(prepare_calls), 1)
|
|
call = prepare_calls[0]
|
|
self.assertIs(call["index_buffer"], shared_index_buffer)
|
|
self.assertIs(call["block_tables"], shared_block_tables)
|
|
self.assertEqual(call["batch_indices"].dtype, torch.int32)
|
|
self.assertEqual(call["batch_indices"].tolist(), [0, 0, 1, 1])
|
|
self.assertEqual(call["kv_lens"].dtype, torch.int32)
|
|
self.assertEqual(call["kv_lens"].tolist(), [3, 3, 4, 4])
|
|
self.assertEqual(call["q_starts"].tolist(), [1, 2, 3, 1])
|
|
self.assertEqual(call["q_lens"].tolist(), [2, 1, 1, 3])
|
|
self.assertEqual(call["k_bases"].tolist(), [0, 3, 6, 10])
|
|
self.assertEqual(call["q_bases"].tolist(), [0, 2, 3, 4])
|
|
self.assertEqual(call["total_kv_len"], 14)
|
|
self.assertEqual(call["total_q_count"], 7)
|
|
self.assertEqual(call["max_kv_len"], 4)
|
|
self.assertEqual(call["max_q_len"], 3)
|
|
self.assertEqual(len(deep_gemm_calls), 1)
|
|
self.assertEqual(deep_gemm_calls[0]["q_rows"], 7)
|
|
self.assertEqual(deep_gemm_calls[0]["weights_rows"], 7)
|
|
self.assertEqual(deep_gemm_calls[0]["kv_rows"], 14)
|
|
self.assertEqual(deep_gemm_calls[0]["ks"], [0, 0, 3, 6, 10, 10, 10])
|
|
self.assertEqual(deep_gemm_calls[0]["ke"], [2, 3, 6, 10, 12, 13, 14])
|
|
|
|
def test_indexer_ragged_cp_index_current_batch_uses_tai_compact_once(self):
|
|
import contextlib
|
|
import torch
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
from sglang.srt.layers.attention.nsa import nsa_indexer
|
|
from sglang.srt.layers.attention.nsa.nsa_indexer import Indexer
|
|
|
|
indexer = object.__new__(Indexer)
|
|
indexer.index_topk = 2
|
|
indexer._with_real_sm_count = lambda: contextlib.nullcontext()
|
|
|
|
prepare_calls = []
|
|
deep_gemm_calls = []
|
|
|
|
def fake_prepare(**kwargs):
|
|
prepare_calls.append(kwargs)
|
|
total_kv_len = int(kwargs["total_kv_len"])
|
|
return (
|
|
torch.zeros((total_kv_len, 1), dtype=torch.uint8),
|
|
torch.zeros((total_kv_len,), dtype=torch.float32),
|
|
torch.tensor([0, 0, 3, 6, 10, 10, 10], dtype=torch.int32),
|
|
torch.tensor([2, 3, 3, 4, 2, 3, 4], dtype=torch.int32),
|
|
)
|
|
|
|
def fake_logits(q_fp8, kv_fp8, weights, ks, ke, clean_logits=False):
|
|
deep_gemm_calls.append(
|
|
{
|
|
"q_rows": int(q_fp8.shape[0]),
|
|
"kv_rows": int(kv_fp8[0].shape[0]),
|
|
"ks": ks.tolist(),
|
|
"ke": ke.tolist(),
|
|
}
|
|
)
|
|
return torch.zeros((int(q_fp8.shape[0]), 8), dtype=torch.float32)
|
|
|
|
class Metadata:
|
|
def get_page_table_64(self):
|
|
raise AssertionError("current cp_index path must not materialize index pages")
|
|
|
|
def topk_transform(self, logits, topk, **kwargs):
|
|
return (
|
|
torch.arange(1, int(logits.shape[0]) + 1, dtype=torch.int32)
|
|
.view(-1, 1)
|
|
.repeat(1, topk)
|
|
)
|
|
|
|
forward_batch = SimpleNamespace(
|
|
token_to_kv_pool=SimpleNamespace(page_size=64, index_head_dim=1),
|
|
seq_lens_cpu=torch.tensor([3, 4], dtype=torch.int64),
|
|
extend_seq_lens_cpu=[3, 4],
|
|
)
|
|
current_index_kv = (
|
|
torch.arange(7, dtype=torch.uint8).view(7, 1),
|
|
torch.arange(7, dtype=torch.float32).view(7, 1),
|
|
)
|
|
|
|
with patch.object(
|
|
nsa_indexer,
|
|
"try_tai_prepare_cp_mqa_current_index_batch",
|
|
side_effect=fake_prepare,
|
|
create=True,
|
|
), patch.object(
|
|
nsa_indexer,
|
|
"deep_gemm",
|
|
SimpleNamespace(fp8_mqa_logits=fake_logits),
|
|
):
|
|
result = Indexer._get_topk_ragged_with_cp(
|
|
indexer,
|
|
forward_batch,
|
|
layer_id=7,
|
|
q_fp8=torch.empty((7, 1), dtype=torch.float32),
|
|
weights=torch.empty((7, 1, 1), dtype=torch.float32),
|
|
metadata=Metadata(),
|
|
kv_len=0,
|
|
actual_seq_q=7,
|
|
cp_index=[(0, 1, 3), (0, 2, 3), (1, 3, 4), (1, 1, 4)],
|
|
current_index_kv=current_index_kv,
|
|
)
|
|
|
|
self.assertEqual(result.tolist(), [[1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6], [7, 7]])
|
|
self.assertEqual(len(prepare_calls), 1)
|
|
call = prepare_calls[0]
|
|
self.assertIs(call["current_index_k"], current_index_kv[0])
|
|
self.assertIs(call["current_index_scale"], current_index_kv[1])
|
|
self.assertEqual(call["current_bases"].tolist(), [0, 0, 3, 3])
|
|
self.assertEqual(call["kv_lens"].tolist(), [3, 3, 4, 4])
|
|
self.assertEqual(call["q_starts"].tolist(), [1, 2, 3, 1])
|
|
self.assertEqual(call["q_lens"].tolist(), [2, 1, 1, 3])
|
|
self.assertEqual(call["k_bases"].tolist(), [0, 3, 6, 10])
|
|
self.assertEqual(call["q_bases"].tolist(), [0, 2, 3, 4])
|
|
self.assertEqual(call["total_kv_len"], 14)
|
|
self.assertEqual(call["total_q_count"], 7)
|
|
self.assertEqual(len(deep_gemm_calls), 1)
|
|
self.assertEqual(deep_gemm_calls[0]["kv_rows"], 14)
|
|
self.assertEqual(deep_gemm_calls[0]["ks"], [0, 0, 3, 6, 10, 10, 10])
|
|
self.assertEqual(deep_gemm_calls[0]["ke"], [2, 3, 6, 10, 12, 13, 14])
|
|
|
|
def test_indexer_ragged_cp_index_current_batch_uses_cp_local_bases_and_uint8_k(
|
|
self,
|
|
):
|
|
import contextlib
|
|
import torch
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
from sglang.srt.layers.attention.nsa import nsa_indexer
|
|
from sglang.srt.layers.attention.nsa.nsa_indexer import Indexer
|
|
|
|
indexer = object.__new__(Indexer)
|
|
indexer.index_topk = 2
|
|
indexer._with_real_sm_count = lambda: contextlib.nullcontext()
|
|
|
|
prepare_calls = []
|
|
|
|
def fake_prepare(**kwargs):
|
|
prepare_calls.append(kwargs)
|
|
total_kv_len = int(kwargs["total_kv_len"])
|
|
return (
|
|
torch.zeros((total_kv_len, 1), dtype=torch.uint8),
|
|
torch.zeros((total_kv_len,), dtype=torch.float32),
|
|
torch.tensor([0, 0, 3, 3, 3], dtype=torch.int32),
|
|
torch.tensor([2, 3, 2, 3, 4], dtype=torch.int32),
|
|
)
|
|
|
|
class Metadata:
|
|
def get_page_table_64(self):
|
|
raise AssertionError("current cp_index path must not materialize index pages")
|
|
|
|
def topk_transform(self, logits, topk, **kwargs):
|
|
return (
|
|
torch.arange(1, int(logits.shape[0]) + 1, dtype=torch.int32)
|
|
.view(-1, 1)
|
|
.repeat(1, topk)
|
|
)
|
|
|
|
forward_batch = SimpleNamespace(
|
|
token_to_kv_pool=SimpleNamespace(page_size=64, index_head_dim=1),
|
|
seq_lens_cpu=torch.tensor([5, 7], dtype=torch.int64),
|
|
extend_seq_lens_cpu=[5, 7],
|
|
nsa_cp_metadata=NSAContextParallelMetadata(
|
|
batch_size=2,
|
|
batch_plan=SimpleNamespace(
|
|
request_rank_local_offsets=[0, 2],
|
|
request_valid_rank_local_offsets=[0, 2],
|
|
),
|
|
),
|
|
)
|
|
current_index_k = (
|
|
torch.arange(6, dtype=torch.float32)
|
|
.to(torch.float8_e4m3fn)
|
|
.view(6, 1)
|
|
)
|
|
current_index_scale = torch.arange(6, dtype=torch.float32).view(6, 1)
|
|
current_index_kv = (current_index_k, current_index_scale)
|
|
|
|
with patch.object(
|
|
nsa_indexer,
|
|
"try_tai_prepare_cp_mqa_current_index_batch",
|
|
side_effect=fake_prepare,
|
|
create=True,
|
|
), patch.object(
|
|
nsa_indexer,
|
|
"deep_gemm",
|
|
SimpleNamespace(
|
|
fp8_mqa_logits=lambda q_fp8, kv_fp8, weights, ks, ke, clean_logits=False: torch.zeros(
|
|
(int(q_fp8.shape[0]), 8), dtype=torch.float32
|
|
)
|
|
),
|
|
):
|
|
result = Indexer._get_topk_ragged_with_cp(
|
|
indexer,
|
|
forward_batch,
|
|
layer_id=7,
|
|
q_fp8=torch.empty((5, 1), dtype=torch.float32),
|
|
weights=torch.empty((5, 1, 1), dtype=torch.float32),
|
|
metadata=Metadata(),
|
|
kv_len=0,
|
|
actual_seq_q=5,
|
|
cp_index=[(0, 1, 3), (1, 1, 4)],
|
|
current_index_kv=current_index_kv,
|
|
)
|
|
|
|
self.assertEqual(result.tolist(), [[1, 1], [2, 2], [3, 3], [4, 4], [5, 5]])
|
|
self.assertEqual(len(prepare_calls), 1)
|
|
call = prepare_calls[0]
|
|
self.assertEqual(call["current_index_k"].dtype, torch.uint8)
|
|
self.assertEqual(call["current_bases"].tolist(), [0, 2])
|
|
self.assertEqual(call["kv_lens"].tolist(), [3, 4])
|
|
self.assertEqual(call["q_lens"].tolist(), [2, 3])
|
|
|
|
def test_eagle_capture_for_decode_clears_cp_local_hidden_marker(self):
|
|
import torch
|
|
|
|
from sglang.srt.layers.logits_processor import LogitsProcessorOutput
|
|
from sglang.srt.speculative.eagle_worker import EAGLEWorker
|
|
|
|
worker = object.__new__(EAGLEWorker)
|
|
worker.topk = 1
|
|
draft_input = EagleDraftInput(
|
|
hidden_states=torch.full((4, 2), -1.0),
|
|
cp_local_hidden_states=True,
|
|
)
|
|
draft_output_hidden = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
|
|
logits_output = LogitsProcessorOutput(
|
|
next_token_logits=torch.tensor([[0.1, 0.9], [0.7, 0.3]]),
|
|
hidden_states=draft_output_hidden,
|
|
)
|
|
|
|
worker.capture_for_decode(logits_output, draft_input)
|
|
|
|
self.assertIs(draft_input.hidden_states, draft_output_hidden)
|
|
self.assertFalse(draft_input.cp_local_hidden_states)
|
|
|
|
def test_eagle_v2_draft_extend_for_prefill_preserves_cp_local_hidden_marker(
|
|
self,
|
|
):
|
|
import torch
|
|
|
|
from sglang.srt.layers.logits_processor import LogitsProcessorOutput
|
|
from sglang.srt.speculative import eagle_worker_v2
|
|
|
|
worker = object.__new__(eagle_worker_v2.EagleDraftWorker)
|
|
worker.topk = 1
|
|
forward_batch = SimpleNamespace(mm_input_embeds=None)
|
|
|
|
class DraftRunner:
|
|
def forward(self, _forward_batch):
|
|
return SimpleNamespace(
|
|
logits_output=LogitsProcessorOutput(
|
|
next_token_logits=torch.tensor([[0.1, 0.9]]),
|
|
hidden_states=torch.tensor([[3.0, 4.0]]),
|
|
)
|
|
)
|
|
|
|
worker.draft_runner = DraftRunner()
|
|
batch = SimpleNamespace(
|
|
forward_mode=ForwardMode.EXTEND,
|
|
extend_seq_lens=[2],
|
|
input_ids=torch.tensor([10, 11]),
|
|
seq_lens=[2],
|
|
spec_info=None,
|
|
)
|
|
target_hidden_states = torch.tensor([[1.0, 2.0]])
|
|
next_token_ids = torch.tensor([99])
|
|
init_new_checks = []
|
|
|
|
def init_new_side_effect(_batch, _draft_runner):
|
|
init_new_checks.append(True)
|
|
self.assertTrue(_batch.spec_info.cp_local_hidden_states)
|
|
self.assertIs(_batch.spec_info.hidden_states, target_hidden_states)
|
|
return forward_batch
|
|
|
|
with patch.object(
|
|
eagle_worker_v2.ForwardBatch,
|
|
"init_new",
|
|
side_effect=init_new_side_effect,
|
|
):
|
|
next_draft_input = (
|
|
eagle_worker_v2.EagleDraftWorker._draft_extend_for_prefill(
|
|
worker,
|
|
batch,
|
|
target_hidden_states,
|
|
next_token_ids,
|
|
cp_local_hidden_states=True,
|
|
)
|
|
)
|
|
|
|
self.assertEqual(init_new_checks, [True])
|
|
self.assertTrue(next_draft_input.cp_local_hidden_states)
|
|
self.assertEqual(next_draft_input.hidden_states.tolist(), [[3.0, 4.0]])
|
|
|
|
def test_indexer_in_seq_cp_pair_composes_current_only_index_reuse(self):
|
|
import torch
|
|
|
|
from sglang.srt.layers.attention.nsa.nsa_indexer import Indexer
|
|
|
|
indexer = object.__new__(Indexer)
|
|
current_index_kv = (torch.tensor([1]), torch.tensor([2]))
|
|
logical_pages = torch.tensor([[1, 2]], dtype=torch.int32)
|
|
materialized_index = torch.tensor([7], dtype=torch.int32)
|
|
dense_pages = torch.tensor([[3, 4]], dtype=torch.int32)
|
|
materialize_calls = []
|
|
topk_calls = []
|
|
|
|
class Metadata:
|
|
def get_page_table_64(self):
|
|
return logical_pages
|
|
|
|
def get_page_table_1(self):
|
|
return torch.empty((1, 512), dtype=torch.int32)
|
|
|
|
def fake_materialize(
|
|
forward_batch,
|
|
layer_id,
|
|
logical_page_table,
|
|
current_index_kv=None,
|
|
):
|
|
materialize_calls.append(
|
|
{
|
|
"layer_id": layer_id,
|
|
"logical_page_table": logical_page_table,
|
|
"current_index_kv": current_index_kv,
|
|
}
|
|
)
|
|
return materialized_index, dense_pages
|
|
|
|
def fake_get_topk(
|
|
forward_batch,
|
|
layer_id,
|
|
q_fp8,
|
|
weights,
|
|
metadata,
|
|
kv_len,
|
|
actual_seq_q,
|
|
cp_index=None,
|
|
current_index_kv=None,
|
|
shared_index_buffer=None,
|
|
shared_block_tables=None,
|
|
actual_seq_q_tensor=None,
|
|
actual_seq_q_cu_tensor=None,
|
|
):
|
|
topk_calls.append(
|
|
{
|
|
"current_index_kv": current_index_kv,
|
|
"actual_seq_q_tensor": actual_seq_q_tensor,
|
|
"actual_seq_q_cu_tensor": actual_seq_q_cu_tensor,
|
|
"shared_index_buffer": shared_index_buffer,
|
|
"shared_block_tables": shared_block_tables,
|
|
}
|
|
)
|
|
return torch.full((actual_seq_q, 2), len(topk_calls), dtype=torch.int32)
|
|
|
|
indexer._maybe_materialize_shared_index_buffer = fake_materialize
|
|
indexer._get_topk_ragged_with_cp = fake_get_topk
|
|
|
|
class Mode:
|
|
def is_extend_without_speculative(self):
|
|
return True
|
|
|
|
forward_batch = type(
|
|
"ForwardBatchStub",
|
|
(),
|
|
{
|
|
"forward_mode": Mode(),
|
|
"extend_prefix_lens_cpu": [0],
|
|
"extend_seq_lens_cpu": [5],
|
|
"seq_lens_cpu": torch.tensor([5], dtype=torch.int64),
|
|
"nsa_cp_metadata": NSAContextParallelMetadata(
|
|
kv_len_prev=5,
|
|
kv_len_next=9,
|
|
actual_seq_q_prev=3,
|
|
actual_seq_q_next=2,
|
|
actual_seq_q_prev_cu_tensor=torch.tensor([0, 3], dtype=torch.int32),
|
|
actual_seq_q_next_cu_tensor=torch.tensor([0, 2], dtype=torch.int32),
|
|
)
|
|
},
|
|
)()
|
|
|
|
result = Indexer._get_topk_in_seq_cp_pair(
|
|
indexer,
|
|
forward_batch,
|
|
layer_id=7,
|
|
q_fp8=torch.empty(5, 4),
|
|
weights=torch.empty(5, 2),
|
|
metadata=Metadata(),
|
|
current_index_kv=current_index_kv,
|
|
)
|
|
|
|
self.assertEqual(len(materialize_calls), 1)
|
|
self.assertIs(materialize_calls[0]["logical_page_table"], logical_pages)
|
|
self.assertIs(materialize_calls[0]["current_index_kv"], current_index_kv)
|
|
self.assertEqual(len(topk_calls), 2)
|
|
self.assertIsNone(topk_calls[0]["current_index_kv"])
|
|
self.assertIsNone(topk_calls[1]["current_index_kv"])
|
|
self.assertIs(topk_calls[0]["shared_index_buffer"], materialized_index)
|
|
self.assertIs(topk_calls[1]["shared_block_tables"], dense_pages)
|
|
self.assertEqual(topk_calls[0]["actual_seq_q_cu_tensor"].tolist(), [0, 3])
|
|
self.assertEqual(topk_calls[1]["actual_seq_q_cu_tensor"].tolist(), [0, 2])
|
|
self.assertEqual(result.tolist(), [[1, 1], [1, 1], [1, 1], [2, 2], [2, 2]])
|
|
|
|
def test_paged_topk_transform_uses_cu_override_without_scan_metadata_ops(self):
|
|
import torch
|
|
|
|
from sglang.srt.layers.attention.nsa_backend import (
|
|
NSAMetadata,
|
|
NSAIndexerMetadata,
|
|
TopkTransformMethod,
|
|
)
|
|
|
|
cu_override = torch.tensor([0, 4], dtype=torch.int32)
|
|
attn_metadata = NSAMetadata(
|
|
page_size=64,
|
|
cache_seqlens_int32=torch.tensor([4], dtype=torch.int32),
|
|
max_seq_len_q=4,
|
|
max_seq_len_k=8,
|
|
cu_seqlens_q=torch.tensor([0, 4], dtype=torch.int32),
|
|
cu_seqlens_k=torch.tensor([0, 8], dtype=torch.int32),
|
|
page_table_1=torch.arange(8, dtype=torch.int32).view(1, 8),
|
|
real_page_table=torch.arange(8, dtype=torch.int32).view(1, 8),
|
|
nsa_cache_seqlens_int32=torch.tensor([4], dtype=torch.int32),
|
|
nsa_cu_seqlens_q=torch.arange(2, dtype=torch.int32),
|
|
nsa_cu_seqlens_k=torch.tensor([0, 4], dtype=torch.int32),
|
|
nsa_extend_seq_lens_list=[4],
|
|
nsa_seqlens_expanded=torch.arange(1, 5, dtype=torch.int32),
|
|
topk_indices_offset=torch.zeros(4, dtype=torch.int32),
|
|
)
|
|
metadata = NSAIndexerMetadata(
|
|
attn_metadata=attn_metadata,
|
|
topk_transform_method=TopkTransformMethod.PAGED,
|
|
)
|
|
logits = torch.zeros(4, 8)
|
|
lengths = torch.arange(1, 5, dtype=torch.int32)
|
|
expected = torch.full((4, 2), 7, dtype=torch.int32)
|
|
|
|
def fake_fused(**kwargs):
|
|
self.assertIs(kwargs["cu_seqlens_q"], cu_override)
|
|
self.assertIs(kwargs["lengths"], lengths)
|
|
return expected
|
|
|
|
fake_sgl_kernel = SimpleNamespace(
|
|
fast_topk_transform_fused=fake_fused,
|
|
fast_topk_transform_ragged_fused=lambda **_: (_ for _ in ()).throw(
|
|
AssertionError("ragged path should not run")
|
|
),
|
|
fast_topk_v2=lambda *_, **__: (_ for _ in ()).throw(
|
|
AssertionError("unfused path should not run")
|
|
),
|
|
)
|
|
|
|
with (
|
|
patch.dict(sys.modules, {"sgl_kernel": fake_sgl_kernel}),
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa_backend.envs.SGLANG_NSA_FUSE_TOPK.get",
|
|
return_value=True,
|
|
),
|
|
patch(
|
|
"sglang.srt.layers.attention.nsa_backend.compute_cu_seqlens",
|
|
side_effect=AssertionError("paged override should skip cumsum"),
|
|
),
|
|
patch(
|
|
"torch.repeat_interleave",
|
|
side_effect=AssertionError("paged topk should not build ragged offsets"),
|
|
),
|
|
):
|
|
actual = metadata.topk_transform(
|
|
logits,
|
|
topk=2,
|
|
cu_seqlens_q=torch.tensor([4], dtype=torch.int32),
|
|
ke_offset=lengths,
|
|
cu_seqlens_q_topk_override=cu_override,
|
|
)
|
|
|
|
self.assertIs(actual, expected)
|
|
|
|
def test_mla_partial_current_path_fails_fast_instead_of_compact_fallback(self):
|
|
backend_path = (
|
|
Path(__file__).resolve().parents[4]
|
|
/ "python/sglang/srt/layers/attention/nsa_backend.py"
|
|
)
|
|
source = backend_path.read_text()
|
|
tree = ast.parse(source)
|
|
compact_merge_calls = [
|
|
node
|
|
for node in ast.walk(tree)
|
|
if isinstance(node, ast.Call)
|
|
and isinstance(node.func, ast.Name)
|
|
and node.func.id == "merge_materialized_and_current_kv"
|
|
]
|
|
|
|
self.assertEqual(
|
|
compact_merge_calls,
|
|
[],
|
|
"MLA partial-current reuse must not fall back to compact "
|
|
"materialize/current merge when page-slot prefetch compose is unavailable.",
|
|
)
|
|
self.assertIn(
|
|
"[CP_SHARED_KV_FAIL_FAST][mla_partial_current_sync]",
|
|
source,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|