[DCP] Drop the prefill index-selection syncs by taking each rank's rows by stride (#35084)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khoa Pham
2026-08-17 19:55:51 -07:00
committed by GitHub
co-authored by Claude Opus 5
parent fcdaaf8a5d
commit f44a130c5e
5 changed files with 122 additions and 5 deletions
+19
View File
@@ -51,6 +51,25 @@ def filter_dcp_local_kv_indices(kv_indices: torch.Tensor):
return kv_indices
def filter_dcp_local_chunk_kv_indices(
kv_indices: torch.Tensor,
chunk_starts_cpu: torch.Tensor,
chunk_seq_lens_cpu: torch.Tensor,
) -> torch.Tensor:
parallel = get_parallel()
if not parallel.dcp_enabled:
return kv_indices
dcp_size = parallel.dcp_size
parts = []
offset = 0
for start, length in zip(chunk_starts_cpu.tolist(), chunk_seq_lens_cpu.tolist()):
first = (parallel.dcp_rank - start) % dcp_size
parts.append(kv_indices[offset + first : offset + length : dcp_size])
offset += length
return torch.cat(parts) // dcp_size
def update_local_kv_lens_for_dcp(kv_len_arr):
"""In-place per-rank KV length: the start=0 case of get_dcp_lens.
+2 -3
View File
@@ -109,10 +109,9 @@ def prepare_decode_context_parallel_metadata(
extend_prefix_lens_sum,
parallel.dcp_size,
)
# Prefix lengths are dcp_size-aligned (widened allocator page), so no nonzero().
dcp_local_prefix_kv_indices = (
dcp_prefix_kv_indices[
dcp_prefix_kv_indices % parallel.dcp_size == parallel.dcp_rank
]
dcp_prefix_kv_indices[parallel.dcp_rank :: parallel.dcp_size]
// parallel.dcp_size
)
dcp_kv_buffer = torch.empty(
@@ -10,6 +10,7 @@ from sglang.kernels.ops.kvcache.kv_indices import (
create_flashinfer_kv_indices_triton,
)
from sglang.srt.environ import envs
from sglang.srt.layers.dcp.layout import filter_dcp_local_chunk_kv_indices
from sglang.srt.model_executor.forward_context import (
get_req_to_token_pool,
get_token_to_kv_pool,
@@ -84,6 +85,11 @@ class ForwardBatchDeepSeekMHAMixin:
chunk_kv_indices,
req_to_token.shape[1],
)
chunk_kv_indices = filter_dcp_local_chunk_kv_indices(
chunk_kv_indices,
self.prefix_chunk_starts_cpu[idx],
self.prefix_chunk_seq_lens_cpu[idx],
)
self.prefix_chunk_kv_indices.append(chunk_kv_indices)
# Here we suppose the length of each chunk is equal
@@ -450,7 +450,6 @@ class DeepseekMHAForwardMixin:
forward_batch: ForwardBatch,
):
if _is_cuda:
kv_indices = filter_dcp_local_kv_indices(kv_indices=kv_indices)
kv_a, k_pe = get_token_to_kv_pool().get_mla_kv_buffer(
self.attn_mha, kv_indices, dst_dtype
)
+95 -1
View File
@@ -22,7 +22,10 @@ import torch
from sglang.srt import runtime_context as rc
from sglang.srt.configs.model_config import ModelConfig
from sglang.srt.layers.attention.triton_backend import TritonAttnBackend
from sglang.srt.layers.dcp.layout import get_dcp_lens
from sglang.srt.layers.dcp.layout import (
filter_dcp_local_chunk_kv_indices,
get_dcp_lens,
)
from sglang.srt.layers.linear import QKVParallelLinear
from sglang.srt.mem_cache.allocator.paged import PagedTokenToKVPoolAllocator
from sglang.srt.mem_cache.kv_cache_configurator import KVCacheConfigurator
@@ -47,6 +50,97 @@ def _legacy_inplace_formula(length: int, n: int, rank: int) -> int:
return (length - rank - 1) // n + 1
class TestFilterDcpLocalChunkKvIndices(CustomTestCase):
PAGE = 64
def _build_chunk(self, starts, lens, dcp_size, seed=0):
g = torch.Generator().manual_seed(seed)
widened = self.PAGE * dcp_size
runs = []
for start, length in zip(starts, lens):
if length == 0:
runs.append(torch.empty(0, dtype=torch.int64))
continue
pos = torch.arange(start, start + length)
page_of = pos // widened
bases = (
torch.randint(0, 64, (int(page_of.max()) + 1,), generator=g) * widened
)
runs.append(bases[page_of] + pos % widened)
return torch.cat(runs) if runs else torch.empty(0, dtype=torch.int64)
def _owner_rule(self, kv, dcp_size, dcp_rank):
return kv[kv % dcp_size == dcp_rank] // dcp_size
def _run(self, starts, lens, dcp_size, dcp_rank, seed=0):
kv = self._build_chunk(starts, lens, dcp_size, seed)
with rc.get_parallel().override(
dcp_enabled=dcp_size > 1, dcp_size=dcp_size, dcp_rank=dcp_rank
):
got = filter_dcp_local_chunk_kv_indices(
kv, torch.tensor(starts), torch.tensor(lens)
)
return kv, got
def test_matches_owner_rule_on_unaligned_runs(self):
cases = [
([0], [1]),
([0, 0], [2048, 2048]),
([8192], [3]),
([5, 13, 31], [7, 0, 19]),
([1365, 1365, 1365], [1365, 1365, 1365]),
([43690, 43690], [43690, 17]),
([7, 7, 7, 7, 7], [11, 13, 0, 1, 40]),
]
for dcp_size in [2, 3, 4, 8]:
for dcp_rank in range(dcp_size):
for i, (starts, lens) in enumerate(cases):
kv, got = self._run(starts, lens, dcp_size, dcp_rank, seed=i)
torch.testing.assert_close(
got,
self._owner_rule(kv, dcp_size, dcp_rank),
msg=f"size={dcp_size} rank={dcp_rank} "
f"starts={starts} lens={lens}",
)
def test_row_count_matches_get_dcp_lens(self):
starts, lens = [1365, 2730, 0], [1365, 900, 37]
for dcp_size in [2, 3, 4, 8]:
for dcp_rank in range(dcp_size):
_, got = self._run(starts, lens, dcp_size, dcp_rank)
expected = get_dcp_lens(
torch.tensor(lens), dcp_size, dcp_rank, start=torch.tensor(starts)
)
self.assertEqual(
got.numel(),
int(expected.sum()),
f"size={dcp_size} rank={dcp_rank}",
)
def test_second_chunk_at_batch_three(self):
starts, lens = [2730, 2730, 2730], [1365, 1365, 1365]
for dcp_rank in range(8):
kv, got = self._run(starts, lens, 8, dcp_rank)
torch.testing.assert_close(got, self._owner_rule(kv, 8, dcp_rank))
naive = kv[dcp_rank::8] // 8
self.assertNotEqual(
got.numel(),
naive.numel(),
f"rank={dcp_rank}: phase-free stride happens to match here, "
f"so this case no longer guards the phase term",
)
def test_identity_without_dcp(self):
kv = torch.arange(37)
with rc.get_parallel().override(dcp_enabled=False, dcp_size=1, dcp_rank=0):
self.assertIs(
filter_dcp_local_chunk_kv_indices(
kv, torch.tensor([0]), torch.tensor([37])
),
kv,
)
class TestGetDcpLens(CustomTestCase):
def test_start_none_matches_owner_count(self):
for n in DCP_SIZES: