[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:
co-authored by
Claude Opus 5
parent
fcdaaf8a5d
commit
f44a130c5e
@@ -51,6 +51,25 @@ def filter_dcp_local_kv_indices(kv_indices: torch.Tensor):
|
|||||||
return kv_indices
|
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):
|
def update_local_kv_lens_for_dcp(kv_len_arr):
|
||||||
"""In-place per-rank KV length: the start=0 case of get_dcp_lens.
|
"""In-place per-rank KV length: the start=0 case of get_dcp_lens.
|
||||||
|
|
||||||
|
|||||||
@@ -109,10 +109,9 @@ def prepare_decode_context_parallel_metadata(
|
|||||||
extend_prefix_lens_sum,
|
extend_prefix_lens_sum,
|
||||||
parallel.dcp_size,
|
parallel.dcp_size,
|
||||||
)
|
)
|
||||||
|
# Prefix lengths are dcp_size-aligned (widened allocator page), so no nonzero().
|
||||||
dcp_local_prefix_kv_indices = (
|
dcp_local_prefix_kv_indices = (
|
||||||
dcp_prefix_kv_indices[
|
dcp_prefix_kv_indices[parallel.dcp_rank :: parallel.dcp_size]
|
||||||
dcp_prefix_kv_indices % parallel.dcp_size == parallel.dcp_rank
|
|
||||||
]
|
|
||||||
// parallel.dcp_size
|
// parallel.dcp_size
|
||||||
)
|
)
|
||||||
dcp_kv_buffer = torch.empty(
|
dcp_kv_buffer = torch.empty(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from sglang.kernels.ops.kvcache.kv_indices import (
|
|||||||
create_flashinfer_kv_indices_triton,
|
create_flashinfer_kv_indices_triton,
|
||||||
)
|
)
|
||||||
from sglang.srt.environ import envs
|
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 (
|
from sglang.srt.model_executor.forward_context import (
|
||||||
get_req_to_token_pool,
|
get_req_to_token_pool,
|
||||||
get_token_to_kv_pool,
|
get_token_to_kv_pool,
|
||||||
@@ -84,6 +85,11 @@ class ForwardBatchDeepSeekMHAMixin:
|
|||||||
chunk_kv_indices,
|
chunk_kv_indices,
|
||||||
req_to_token.shape[1],
|
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)
|
self.prefix_chunk_kv_indices.append(chunk_kv_indices)
|
||||||
|
|
||||||
# Here we suppose the length of each chunk is equal
|
# Here we suppose the length of each chunk is equal
|
||||||
|
|||||||
@@ -450,7 +450,6 @@ class DeepseekMHAForwardMixin:
|
|||||||
forward_batch: ForwardBatch,
|
forward_batch: ForwardBatch,
|
||||||
):
|
):
|
||||||
if _is_cuda:
|
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(
|
kv_a, k_pe = get_token_to_kv_pool().get_mla_kv_buffer(
|
||||||
self.attn_mha, kv_indices, dst_dtype
|
self.attn_mha, kv_indices, dst_dtype
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ import torch
|
|||||||
from sglang.srt import runtime_context as rc
|
from sglang.srt import runtime_context as rc
|
||||||
from sglang.srt.configs.model_config import ModelConfig
|
from sglang.srt.configs.model_config import ModelConfig
|
||||||
from sglang.srt.layers.attention.triton_backend import TritonAttnBackend
|
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.layers.linear import QKVParallelLinear
|
||||||
from sglang.srt.mem_cache.allocator.paged import PagedTokenToKVPoolAllocator
|
from sglang.srt.mem_cache.allocator.paged import PagedTokenToKVPoolAllocator
|
||||||
from sglang.srt.mem_cache.kv_cache_configurator import KVCacheConfigurator
|
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
|
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):
|
class TestGetDcpLens(CustomTestCase):
|
||||||
def test_start_none_matches_owner_count(self):
|
def test_start_none_matches_owner_count(self):
|
||||||
for n in DCP_SIZES:
|
for n in DCP_SIZES:
|
||||||
|
|||||||
Reference in New Issue
Block a user