Revert the tail-chunk co-batching gate from54c056afbecause allowing a continued chunk to share the next CP bs>1 batch reopens the mixed chunk/page-tail scheduler risks we are currently avoiding. Keep the independent real-prefix budget accounting so chunked requests still contribute their carried prefix to CP cached-token and buffer estimates.\n\nConstraint: Chunked-prefill requests must remain solo until the CP split/page-tail contract is revalidated for mixed batches.\nRejected: Full revert of54c056af| it would also drop true-prefix budget accounting and under-estimate cache/buffer pressure for admitted chunks.\nConfidence: high\nScope-risk: moderate\nDirective: Do not reintroduce tail-chunk co-batching without tests covering page-tail split, CP buffer admission, and ETE chunked+cache-hit replay.\nTested: Local py_compile for environ.py, schedule_policy.py, cp_shared_kv_compose.py, test_prefill_adder.py, test_cp_shared_kv_compose_v2_8rank.py.\nTested: Remote cjy-glm5-new PYTHONPATH=python pytest -q test/registered/unit/managers/test_prefill_adder.py -> 25 passed.\nTested: Remote cjy-glm5-new PYTHONPATH=python pytest -q test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py -> 157 passed, 2 subtests passed.\nNot-tested: Full mixed replay with chunked-prefill traffic after service restart.
714 lines
26 KiB
Python
714 lines
26 KiB
Python
#!/usr/bin/env python3
|
|
"""8-rank GPU byte-exactness test: compose legacy vs v2 (Step A).
|
|
|
|
Runs the REAL materialize_prefix_and_reuse_current_{kv,index}_page_slots on
|
|
every rank with real NCCL collectives and (for v2) real tai-kernel CUDA-IPC
|
|
gathers, and asserts the composed dense buffers and locs are byte-identical
|
|
between the legacy per-span path and compose_v2.
|
|
|
|
Run inside the g0033 syh-dev-new container:
|
|
cd /mnt/beegfs/syh/sglang-stable && \
|
|
SGLANG_CP_SHARED_KV_USE_TAI_MATERIALIZE=1 \
|
|
PYTHONPATH=python:/mnt/beegfs/syh/tai-kernel/python \
|
|
torchrun --nproc-per-node=8 test/manual/test_cp_shared_kv_compose_v2_8rank.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
import torch
|
|
import torch.distributed as dist
|
|
|
|
from sglang.srt.environ import envs
|
|
from sglang.srt.layers.attention.nsa import cp_shared_kv_runtime as runtime
|
|
from sglang.srt.mem_cache.cp_shared_kv_layout import CpSharedKVLayout
|
|
|
|
|
|
class _CpGroupShim:
|
|
"""Minimal stand-in for the attention CP GroupCoordinator."""
|
|
|
|
def __init__(self, group: dist.ProcessGroup) -> None:
|
|
self.device_group = group
|
|
self.world_size = dist.get_world_size(group)
|
|
self.unique_name = "compose_v2_test"
|
|
self.pynccl_comm = None
|
|
|
|
def all_gather_into_tensor(self, out: torch.Tensor, t: torch.Tensor) -> None:
|
|
dist.all_gather_into_tensor(out, t, group=self.device_group)
|
|
|
|
def all_reduce(self, t: torch.Tensor) -> torch.Tensor:
|
|
dist.all_reduce(t, group=self.device_group)
|
|
return t
|
|
|
|
|
|
def _build_scenario(rank: int, cp_size: int, device: torch.device):
|
|
"""bs=4 with mixed prefix/extend lengths, extends crossing page bounds."""
|
|
page_size = 64
|
|
batch_size = 4
|
|
prefix_lens = [640, 1280, 320, 640]
|
|
extend_lens = [95, 130, 64, 200]
|
|
kv_dim = 656 # fp8 layout bytes/token
|
|
|
|
from sglang.srt.mem_cache.cp_shared_kv_compute_owner import (
|
|
build_in_seq_page_compute_owners,
|
|
)
|
|
|
|
g = torch.Generator().manual_seed(20260612)
|
|
logical_rows = []
|
|
current_locs_all = []
|
|
next_page = 1
|
|
prefix_pages_by_req = []
|
|
for req_id, (prefix_len, extend_len) in enumerate(zip(prefix_lens, extend_lens)):
|
|
prefix_pages = prefix_len // page_size
|
|
prefix_pages_by_req.append(prefix_pages)
|
|
req_pages = list(range(next_page, next_page + prefix_pages))
|
|
next_page += prefix_pages
|
|
owners = build_in_seq_page_compute_owners(
|
|
extend_len=extend_len,
|
|
extend_prefix_len=prefix_len,
|
|
page_size=page_size,
|
|
cp_size=cp_size,
|
|
)
|
|
current_pages = []
|
|
for owner in owners:
|
|
# pick a logical page owned by `owner`: owner = (page-1) % cp_size
|
|
candidate = next_page
|
|
while (candidate - 1) % cp_size != int(owner):
|
|
candidate += 1
|
|
current_pages.append(candidate)
|
|
next_page = candidate + 1
|
|
req_pages.extend(current_pages)
|
|
remaining = extend_len
|
|
for page_offset, logical_page in enumerate(current_pages):
|
|
valid = min(page_size, remaining)
|
|
for off in range(valid):
|
|
current_locs_all.append(
|
|
(req_id, logical_page * page_size + off)
|
|
)
|
|
remaining -= valid
|
|
logical_rows.append(req_pages)
|
|
|
|
max_pages = max(len(r) for r in logical_rows)
|
|
logical_pages = torch.zeros((batch_size, max_pages), dtype=torch.int64)
|
|
for req_id, pages in enumerate(logical_rows):
|
|
logical_pages[req_id, : len(pages)] = torch.tensor(pages, dtype=torch.int64)
|
|
logical_pages = logical_pages.to(device)
|
|
|
|
# logical locs per request row (token granularity, -1 padded)
|
|
max_tokens = max(
|
|
p + e for p, e in zip(prefix_lens, extend_lens)
|
|
)
|
|
logical_locs = torch.full((batch_size, max_tokens), -1, dtype=torch.int64)
|
|
loc_req_id_rows = []
|
|
for req_id, pages in enumerate(logical_rows):
|
|
total = prefix_lens[req_id] + extend_lens[req_id]
|
|
locs = []
|
|
for token_idx in range(total):
|
|
page = pages[token_idx // page_size]
|
|
locs.append(page * page_size + token_idx % page_size)
|
|
logical_locs[req_id, : len(locs)] = torch.tensor(locs, dtype=torch.int64)
|
|
logical_locs = logical_locs.to(device)
|
|
|
|
physical_pages = (next_page // cp_size + 3)
|
|
# Production KV dtype: float8_e4m3fn (caught index_copy_'s missing fp8
|
|
# kernel) — build payloads as bytes, view as fp8.
|
|
kv_cache = torch.zeros(
|
|
(physical_pages * page_size, 1, kv_dim), dtype=torch.uint8, device=device
|
|
)
|
|
layout = CpSharedKVLayout(page_size=page_size, cp_size=cp_size, cp_rank=rank)
|
|
|
|
# Fill this rank's owned PREFIX pages with deterministic payloads.
|
|
for req_id, pages in enumerate(logical_rows):
|
|
for slot, logical_page in enumerate(pages[: prefix_pages_by_req[req_id]]):
|
|
if (logical_page - 1) % cp_size != rank:
|
|
continue
|
|
phys_page = (logical_page - 1) // cp_size + 1
|
|
payload = (
|
|
torch.arange(page_size * kv_dim, dtype=torch.int64)
|
|
+ logical_page * 131
|
|
).remainder_(251).to(torch.uint8)
|
|
kv_cache[
|
|
phys_page * page_size : (phys_page + 1) * page_size
|
|
] = payload.view(page_size, 1, kv_dim).to(device)
|
|
|
|
# Current rows owned by this rank (writer = page owner in this scenario,
|
|
# so the symm writer list is the storage owner per current page).
|
|
current_page_writer_ranks = []
|
|
for req_pages, prefix_pages in zip(logical_rows, prefix_pages_by_req):
|
|
for page in req_pages[prefix_pages:]:
|
|
current_page_writer_ranks.append((page - 1) % cp_size)
|
|
cur_locs = [
|
|
loc for req_id, loc in current_locs_all
|
|
if (loc // page_size - 1) % cp_size == rank
|
|
]
|
|
cur_req = [
|
|
req_id for req_id, loc in current_locs_all
|
|
if (loc // page_size - 1) % cp_size == rank
|
|
]
|
|
current_locs = torch.tensor(cur_locs, dtype=torch.int64, device=device)
|
|
current_req_id = torch.tensor(cur_req, dtype=torch.int64, device=device)
|
|
current_kv = (
|
|
(
|
|
torch.arange(len(cur_locs), dtype=torch.int64).view(-1, 1, 1) * 7
|
|
+ torch.arange(kv_dim, dtype=torch.int64).view(1, 1, -1)
|
|
+ rank * 31
|
|
)
|
|
.remainder_(249)
|
|
.to(torch.uint8)
|
|
.to(device)
|
|
.view(torch.float8_e4m3fn)
|
|
)
|
|
kv_cache = kv_cache.view(torch.float8_e4m3fn)
|
|
|
|
prefix_slot_spans = runtime.build_batch_prefix_slot_spans(
|
|
logical_pages=logical_pages,
|
|
prefix_lens_cpu=prefix_lens,
|
|
page_size=page_size,
|
|
)
|
|
current_slot_spans = runtime.build_batch_current_slot_spans(
|
|
logical_pages=logical_pages,
|
|
prefix_lens_cpu=prefix_lens,
|
|
extend_lens_cpu=extend_lens,
|
|
page_size=page_size,
|
|
)
|
|
slot_remap = runtime.build_shared_token_kv_slot_remap(
|
|
kv_cache,
|
|
logical_locs,
|
|
logical_pages,
|
|
layout,
|
|
page_size,
|
|
)
|
|
loc_req_id = torch.repeat_interleave(
|
|
torch.arange(batch_size, dtype=torch.int64, device=device),
|
|
torch.tensor(
|
|
[max_tokens] * batch_size, dtype=torch.int64, device=device
|
|
),
|
|
)
|
|
|
|
return dict(
|
|
kv_cache=kv_cache,
|
|
logical_pages=logical_pages,
|
|
logical_locs=logical_locs.reshape(-1),
|
|
loc_req_id=loc_req_id,
|
|
current_kv=current_kv,
|
|
current_locs=current_locs,
|
|
current_req_id=current_req_id,
|
|
slot_remap=slot_remap,
|
|
layout=layout,
|
|
page_size=page_size,
|
|
prefix_slot_spans=prefix_slot_spans,
|
|
current_slot_spans=current_slot_spans,
|
|
current_page_writer_ranks=current_page_writer_ranks,
|
|
)
|
|
|
|
|
|
def _build_index_scenario(rank: int, cp_size: int, device: torch.device, s):
|
|
"""Index page buffer + current K/scale rows over the bs=4 geometry."""
|
|
page_size = s["page_size"]
|
|
index_page_nbytes = page_size * (128 + 4)
|
|
physical_pages = int(s["kv_cache"].shape[0]) // page_size
|
|
page_buffer = torch.zeros(
|
|
(physical_pages, index_page_nbytes), dtype=torch.uint8, device=device
|
|
)
|
|
# Owned PREFIX pages get deterministic payloads (mirrors the KV pool).
|
|
slot_pages = s["slot_remap"].slot_logical_pages.reshape(-1).cpu()
|
|
prefix_slots = set()
|
|
for start, end in s["prefix_slot_spans"]:
|
|
prefix_slots.update(range(int(start), int(end)))
|
|
for slot in sorted(prefix_slots):
|
|
logical_page = int(slot_pages[slot])
|
|
if logical_page <= 0 or (logical_page - 1) % cp_size != rank:
|
|
continue
|
|
phys_page = (logical_page - 1) // cp_size + 1
|
|
page_buffer[phys_page] = (
|
|
(torch.arange(index_page_nbytes, dtype=torch.int64) + logical_page * 37)
|
|
.remainder_(251)
|
|
.to(torch.uint8)
|
|
.to(device)
|
|
)
|
|
rows = int(s["current_locs"].numel())
|
|
current_k = (
|
|
(
|
|
torch.arange(rows, dtype=torch.int64).view(-1, 1) * 13
|
|
+ torch.arange(128, dtype=torch.int64).view(1, -1)
|
|
+ rank * 17
|
|
)
|
|
.remainder_(249)
|
|
.to(torch.uint8)
|
|
.to(device)
|
|
)
|
|
current_scale = (
|
|
torch.arange(rows, dtype=torch.float32, device=device).view(-1, 1) * 0.25
|
|
+ 1.0
|
|
+ rank
|
|
)
|
|
from sglang.srt.layers.attention.nsa import cp_shared_kv_runtime as rt
|
|
|
|
slot_remap = rt.build_shared_paged_buffer_slot_remap(
|
|
page_buffer,
|
|
s["logical_pages"],
|
|
s["layout"],
|
|
)
|
|
return dict(
|
|
page_buffer=page_buffer,
|
|
current_k=current_k,
|
|
current_scale=current_scale,
|
|
current_locs=s["current_locs"],
|
|
current_req_id=s["current_req_id"],
|
|
slot_remap=slot_remap,
|
|
)
|
|
|
|
|
|
def _check_prefetch_symm(rank: int, cp_size: int, device: torch.device) -> None:
|
|
"""bs=1 prefetch-path symm consume vs the legacy sync compose (MLA+index)."""
|
|
from sglang.srt.layers.attention.nsa import cp_shared_kv_runtime as rt
|
|
from sglang.srt.layers.attention.nsa.cp_shared_kv_prefetch import (
|
|
CpSharedKVIndexPrefetcher,
|
|
CpSharedKVIndexPrefetchHandle,
|
|
CpSharedKVMlaPrefetcher,
|
|
CpSharedKVMlaPrefetchHandle,
|
|
)
|
|
from sglang.srt.mem_cache.cp_shared_kv_compute_owner import (
|
|
build_in_seq_page_compute_owners,
|
|
)
|
|
|
|
page_size = 64
|
|
kv_dim = 656
|
|
index_page_nbytes = page_size * (128 + 4)
|
|
prefix_len, extend_len = 640, 200
|
|
prefix_pages = prefix_len // page_size
|
|
writers = [
|
|
int(o)
|
|
for o in build_in_seq_page_compute_owners(
|
|
extend_len=extend_len,
|
|
extend_prefix_len=prefix_len,
|
|
page_size=page_size,
|
|
cp_size=cp_size,
|
|
)
|
|
]
|
|
pages = list(range(1, prefix_pages + 1))
|
|
next_page = prefix_pages + 1
|
|
for owner in writers:
|
|
while (next_page - 1) % cp_size != owner:
|
|
next_page += 1
|
|
pages.append(next_page)
|
|
next_page += 1
|
|
total_slots = len(pages)
|
|
logical_pages = torch.tensor([pages], dtype=torch.int64, device=device)
|
|
total_tokens = prefix_len + extend_len
|
|
locs = torch.tensor(
|
|
[
|
|
pages[i // page_size] * page_size + i % page_size
|
|
for i in range(total_tokens)
|
|
],
|
|
dtype=torch.int64,
|
|
device=device,
|
|
)
|
|
physical_pages = (max(pages) - 1) // cp_size + 3
|
|
layout = CpSharedKVLayout(page_size=page_size, cp_size=cp_size, cp_rank=rank)
|
|
|
|
kv_cache = torch.zeros(
|
|
(physical_pages * page_size, 1, kv_dim), dtype=torch.uint8, device=device
|
|
)
|
|
page_buffer = torch.zeros(
|
|
(physical_pages, index_page_nbytes), dtype=torch.uint8, device=device
|
|
)
|
|
for logical_page in pages[:prefix_pages]:
|
|
if (logical_page - 1) % cp_size != rank:
|
|
continue
|
|
phys = (logical_page - 1) // cp_size + 1
|
|
kv_cache[phys * page_size : (phys + 1) * page_size] = (
|
|
(torch.arange(page_size * kv_dim, dtype=torch.int64) + logical_page * 53)
|
|
.remainder_(251)
|
|
.to(torch.uint8)
|
|
.view(page_size, 1, kv_dim)
|
|
.to(device)
|
|
)
|
|
page_buffer[phys] = (
|
|
(torch.arange(index_page_nbytes, dtype=torch.int64) + logical_page * 29)
|
|
.remainder_(251)
|
|
.to(torch.uint8)
|
|
.to(device)
|
|
)
|
|
|
|
cur_locs_all = [
|
|
pages[prefix_pages + i // page_size] * page_size + i % page_size
|
|
for i in range(extend_len)
|
|
]
|
|
mine = [
|
|
i for i in range(extend_len) if writers[i // page_size] == rank
|
|
]
|
|
current_locs = torch.tensor(
|
|
[cur_locs_all[i] for i in mine], dtype=torch.int64, device=device
|
|
)
|
|
rows = len(mine)
|
|
current_kv = (
|
|
(
|
|
torch.arange(rows, dtype=torch.int64).view(-1, 1, 1) * 11
|
|
+ torch.arange(kv_dim, dtype=torch.int64).view(1, 1, -1)
|
|
+ rank * 19
|
|
)
|
|
.remainder_(247)
|
|
.to(torch.uint8)
|
|
.to(device)
|
|
.view(torch.float8_e4m3fn)
|
|
)
|
|
kv_cache = kv_cache.view(torch.float8_e4m3fn)
|
|
current_k = (
|
|
(
|
|
torch.arange(rows, dtype=torch.int64).view(-1, 1) * 23
|
|
+ torch.arange(128, dtype=torch.int64).view(1, -1)
|
|
+ rank * 7
|
|
)
|
|
.remainder_(245)
|
|
.to(torch.uint8)
|
|
.to(device)
|
|
)
|
|
current_scale = (
|
|
torch.arange(rows, dtype=torch.float32, device=device).view(-1, 1) * 0.5
|
|
+ 2.0
|
|
+ rank
|
|
)
|
|
zeros_req = torch.zeros(rows, dtype=torch.long, device=device)
|
|
loc_req_id = torch.zeros(total_tokens, dtype=torch.long, device=device)
|
|
|
|
kv_remap = rt.build_shared_token_kv_slot_remap(
|
|
kv_cache, locs.view(1, -1), logical_pages, layout, page_size
|
|
)
|
|
idx_remap = rt.build_shared_paged_buffer_slot_remap(
|
|
page_buffer, logical_pages, layout
|
|
)
|
|
spans = dict(
|
|
prefix_slot_spans=[(0, prefix_pages)],
|
|
current_slot_spans=[(prefix_pages, total_slots)],
|
|
)
|
|
|
|
# --- references via the legacy sync compose (pure NCCL, no barriers) ---
|
|
with envs.SGLANG_CP_SHARED_KV_COMPOSE_V2.override(False):
|
|
ref_kv, ref_locs = rt.materialize_prefix_and_reuse_current_kv_page_slots(
|
|
kv_cache=kv_cache,
|
|
logical_locs=locs,
|
|
current_kv_cache=current_kv,
|
|
current_locs=current_locs,
|
|
slot_remap=kv_remap,
|
|
layout=layout,
|
|
page_size=page_size,
|
|
prefix_pages=0,
|
|
loc_req_id=loc_req_id,
|
|
current_req_id=zeros_req,
|
|
layer_id=60,
|
|
**spans,
|
|
)
|
|
ref_idx, ref_pages = rt.materialize_prefix_and_reuse_current_index_page_slots(
|
|
page_buffer=page_buffer,
|
|
current_index_k=current_k,
|
|
current_index_scale=current_scale,
|
|
current_locs=current_locs,
|
|
slot_remap=idx_remap,
|
|
layout=layout,
|
|
page_size=page_size,
|
|
index_head_dim=128,
|
|
prefix_pages=0,
|
|
current_req_id=zeros_req,
|
|
layer_id=60,
|
|
**spans,
|
|
)
|
|
torch.cuda.synchronize()
|
|
dist.barrier()
|
|
|
|
# --- prefetchers with a hand-built prefix handle (prefix already
|
|
# materialized + reduced, exactly what start_next_layer_prefix yields) ---
|
|
mla = CpSharedKVMlaPrefetcher(
|
|
layout=layout,
|
|
page_size=page_size,
|
|
prefix_pages=prefix_pages,
|
|
slot_logical_pages=kv_remap.slot_logical_pages,
|
|
page_inverse=kv_remap.page_inverse,
|
|
dense_num_pages=kv_remap.dense_num_pages,
|
|
slot_remap=kv_remap,
|
|
symm_writers=writers,
|
|
)
|
|
dense_kv = kv_cache.new_zeros(
|
|
(kv_remap.dense_num_pages * page_size, 1, kv_dim)
|
|
)
|
|
rt.materialize_local_token_kv_page_slots_into(
|
|
kv_cache=kv_cache,
|
|
dense_kv_cache=dense_kv,
|
|
slot_logical_pages=kv_remap.slot_logical_pages,
|
|
layout=layout,
|
|
page_size=page_size,
|
|
start_slot=0,
|
|
end_slot=prefix_pages,
|
|
)
|
|
prefix_rows = rt.slot_range_to_token_slice(page_size, 0, prefix_pages)
|
|
dist.all_reduce(dense_kv[prefix_rows].view(torch.uint8))
|
|
mla_event = torch.cuda.Event()
|
|
mla_event.record()
|
|
mla.handles[61] = CpSharedKVMlaPrefetchHandle(
|
|
layer_id=61, dense_kv_cache=dense_kv, prefix_rows=prefix_rows,
|
|
event=mla_event,
|
|
)
|
|
|
|
idx = CpSharedKVIndexPrefetcher(
|
|
layout=layout,
|
|
prefix_pages=prefix_pages,
|
|
slot_logical_pages=idx_remap.slot_logical_pages,
|
|
page_inverse=idx_remap.page_inverse,
|
|
dense_num_pages=idx_remap.dense_num_pages,
|
|
slot_remap=idx_remap,
|
|
symm_writers=writers,
|
|
)
|
|
dense_pb = page_buffer.new_zeros(
|
|
(idx_remap.dense_num_pages, index_page_nbytes)
|
|
)
|
|
rt.materialize_local_paged_buffer_page_slots_into(
|
|
page_buffer=page_buffer,
|
|
dense_page_buffer=dense_pb,
|
|
slot_logical_pages=idx_remap.slot_logical_pages,
|
|
layout=layout,
|
|
start_slot=0,
|
|
end_slot=prefix_pages,
|
|
)
|
|
dist.all_reduce(dense_pb[1 : prefix_pages + 1])
|
|
idx_event = torch.cuda.Event()
|
|
idx_event.record()
|
|
idx.handles[61] = CpSharedKVIndexPrefetchHandle(
|
|
layer_id=61, dense_page_buffer=dense_pb,
|
|
prefix_rows=slice(1, prefix_pages + 1), event=idx_event,
|
|
)
|
|
torch.cuda.synchronize()
|
|
dist.barrier()
|
|
|
|
with envs.SGLANG_CP_SHARED_KV_COMPOSE_V2.override(
|
|
True
|
|
), envs.SGLANG_CP_SHARED_KV_COMPOSE_SYMM.override(True):
|
|
out_idx = idx.consume_prefix_with_current(
|
|
layer_id=61,
|
|
logical_pages=logical_pages,
|
|
current_index_k=current_k,
|
|
current_index_scale=current_scale,
|
|
current_locs=current_locs,
|
|
page_size=page_size,
|
|
index_head_dim=128,
|
|
current_req_id=zeros_req,
|
|
)
|
|
out_mla = mla.consume_prefix_with_current(
|
|
layer_id=61,
|
|
kv_cache=kv_cache,
|
|
logical_locs=locs,
|
|
current_kv_cache=current_kv,
|
|
current_locs=current_locs,
|
|
loc_req_id=loc_req_id,
|
|
current_req_id=zeros_req,
|
|
)
|
|
torch.cuda.synchronize()
|
|
assert out_idx is not None and out_mla is not None, (
|
|
f"rank{rank}: prefetch consume unexpectedly missed"
|
|
)
|
|
sym_idx, sym_pages = out_idx
|
|
sym_kv, sym_locs = out_mla
|
|
assert torch.equal(ref_pages, sym_pages), f"rank{rank}: prefetch index pages"
|
|
assert torch.equal(ref_locs, sym_locs), f"rank{rank}: prefetch mla locs"
|
|
if not torch.equal(ref_idx, sym_idx):
|
|
bad = (ref_idx != sym_idx).any(dim=-1).nonzero().reshape(-1)[:8]
|
|
raise AssertionError(
|
|
f"rank{rank}: prefetch index symm mismatch at pages {bad.cpu().tolist()}"
|
|
)
|
|
ref_kv_b = ref_kv.view(torch.uint8)
|
|
sym_kv_b = sym_kv.view(torch.uint8)
|
|
if not torch.equal(ref_kv_b, sym_kv_b):
|
|
diff = (ref_kv_b != sym_kv_b).any(dim=-1).any(dim=-1)
|
|
bad = torch.nonzero(diff).reshape(-1)[:8].cpu().tolist()
|
|
raise AssertionError(
|
|
f"rank{rank}: prefetch mla symm mismatch at rows {bad} "
|
|
f"(of {int(diff.sum())})"
|
|
)
|
|
|
|
|
|
def _compose(s, layer_id: int, *, writers: list[int] | None = None):
|
|
return runtime.materialize_prefix_and_reuse_current_kv_page_slots(
|
|
kv_cache=s["kv_cache"],
|
|
logical_locs=s["logical_locs"],
|
|
current_kv_cache=s["current_kv"],
|
|
current_locs=s["current_locs"],
|
|
slot_remap=s["slot_remap"],
|
|
layout=s["layout"],
|
|
page_size=s["page_size"],
|
|
prefix_pages=0,
|
|
loc_req_id=s["loc_req_id"],
|
|
current_req_id=s["current_req_id"],
|
|
prefix_slot_spans=s["prefix_slot_spans"],
|
|
current_slot_spans=s["current_slot_spans"],
|
|
layer_id=layer_id,
|
|
current_page_writer_ranks=writers,
|
|
)
|
|
|
|
|
|
def main() -> None:
|
|
dist.init_process_group("nccl")
|
|
rank = dist.get_rank()
|
|
world = dist.get_world_size()
|
|
local_rank = int(os.environ.get("LOCAL_RANK", rank))
|
|
torch.cuda.set_device(local_rank)
|
|
device = torch.device("cuda", local_rank)
|
|
|
|
shim = _CpGroupShim(dist.group.WORLD)
|
|
runtime.get_attention_cp_group = lambda: shim # type: ignore[assignment]
|
|
|
|
s = _build_scenario(rank, world, device)
|
|
|
|
with envs.SGLANG_CP_SHARED_KV_COMPOSE_V2.override(False):
|
|
ref_kv, ref_locs = _compose(s, layer_id=0)
|
|
torch.cuda.synchronize()
|
|
dist.barrier()
|
|
|
|
with envs.SGLANG_CP_SHARED_KV_COMPOSE_V2.override(True):
|
|
v2_kv, v2_locs = _compose(s, layer_id=0)
|
|
torch.cuda.synchronize()
|
|
dist.barrier()
|
|
|
|
assert torch.equal(ref_locs, v2_locs), (
|
|
f"rank{rank}: locs mismatch legacy vs v2"
|
|
)
|
|
ref_kv = ref_kv.view(torch.uint8)
|
|
v2_kv = v2_kv.view(torch.uint8)
|
|
if not torch.equal(ref_kv, v2_kv):
|
|
diff = (ref_kv != v2_kv).any(dim=-1).any(dim=-1)
|
|
bad_rows = torch.nonzero(diff).reshape(-1)[:8].cpu().tolist()
|
|
raise AssertionError(
|
|
f"rank{rank}: dense kv mismatch at rows {bad_rows} "
|
|
f"(of {int(diff.sum())} differing rows)"
|
|
)
|
|
|
|
# Cross-rank: every rank must hold the SAME composed buffer.
|
|
ref_sum = ref_kv.view(torch.uint8).to(torch.float64).sum()
|
|
sums = torch.zeros(world, dtype=torch.float64, device=device)
|
|
dist.all_gather_into_tensor(
|
|
sums, ref_sum.reshape(1).to(device)
|
|
)
|
|
assert torch.allclose(sums, sums[0].expand_as(sums)), (
|
|
f"rank{rank}: composed buffers differ across ranks: {sums.tolist()}"
|
|
)
|
|
|
|
if rank == 0:
|
|
print(
|
|
"PASS: legacy vs compose_v2 byte-identical "
|
|
f"(dense rows={int(ref_kv.shape[0])}, world={world})",
|
|
flush=True,
|
|
)
|
|
|
|
# ---- Step B: compact symm staging (fill-to-staging + barrier + one
|
|
# mega slot-dense gather,
|
|
# zero NCCL in the current-page phase). Multiple layers exercise the
|
|
# staging parity halves; the arena is exercised in a second pass to
|
|
# prove the two knobs are independent. ----
|
|
def _check_symm(layer_id: int, tag: str) -> None:
|
|
symm_kv, symm_locs = _compose(
|
|
s, layer_id=layer_id, writers=s["current_page_writer_ranks"]
|
|
)
|
|
symm_kv = symm_kv.view(torch.uint8)
|
|
torch.cuda.synchronize()
|
|
assert torch.equal(ref_locs, symm_locs), (
|
|
f"rank{rank} layer{layer_id} [{tag}]: symm locs mismatch"
|
|
)
|
|
if not torch.equal(ref_kv, symm_kv):
|
|
diff = (ref_kv != symm_kv).any(dim=-1).any(dim=-1)
|
|
bad = torch.nonzero(diff).reshape(-1)[:8].cpu().tolist()
|
|
raise AssertionError(
|
|
f"rank{rank} layer{layer_id} [{tag}]: symm dense kv mismatch "
|
|
f"at rows {bad} (of {int(diff.sum())})"
|
|
)
|
|
|
|
with envs.SGLANG_CP_SHARED_KV_COMPOSE_V2.override(
|
|
True
|
|
), envs.SGLANG_CP_SHARED_KV_COMPOSE_SYMM.override(True):
|
|
for layer_id in range(4):
|
|
_check_symm(layer_id, "no-arena")
|
|
with envs.SGLANG_CP_SHARED_KV_COMPOSE_ARENA.override(True):
|
|
for layer_id in range(4, 8):
|
|
_check_symm(layer_id, "arena")
|
|
dist.barrier()
|
|
from sglang.srt.layers.attention.nsa.cp_shared_kv_compose import (
|
|
get_compose_staging,
|
|
)
|
|
|
|
staging = get_compose_staging(device)
|
|
assert staging.registered, "symm staging was not registered"
|
|
if rank == 0:
|
|
staged_mb = (
|
|
2
|
|
* staging.capacity_pages
|
|
* (staging.page_nbytes("token_kv") + staging.page_nbytes("index"))
|
|
/ (1 << 20)
|
|
)
|
|
print(
|
|
"PASS: compact-staging symm compose byte-identical to v2 across "
|
|
f"8 layers (staging {staging.capacity_pages} pages ~{staged_mb:.1f} "
|
|
"MiB, fill-to-staging + barrier + mega gather engaged; arena on/off)",
|
|
flush=True,
|
|
)
|
|
|
|
# ---- Phase 3: index sync compose, legacy vs symm (staging already
|
|
# registered by the token phase — the index compose never registers). ----
|
|
si = _build_index_scenario(rank, world, device, s)
|
|
|
|
def _compose_index(layer_id: int, *, writers=None):
|
|
return runtime.materialize_prefix_and_reuse_current_index_page_slots(
|
|
page_buffer=si["page_buffer"],
|
|
current_index_k=si["current_k"],
|
|
current_index_scale=si["current_scale"],
|
|
current_locs=si["current_locs"],
|
|
slot_remap=si["slot_remap"],
|
|
layout=s["layout"],
|
|
page_size=s["page_size"],
|
|
index_head_dim=128,
|
|
prefix_pages=0,
|
|
current_req_id=si["current_req_id"],
|
|
prefix_slot_spans=s["prefix_slot_spans"],
|
|
current_slot_spans=s["current_slot_spans"],
|
|
layer_id=layer_id,
|
|
current_page_writer_ranks=writers,
|
|
)
|
|
|
|
with envs.SGLANG_CP_SHARED_KV_COMPOSE_V2.override(False):
|
|
ref_idx, ref_dense_pages = _compose_index(40)
|
|
torch.cuda.synchronize()
|
|
dist.barrier()
|
|
with envs.SGLANG_CP_SHARED_KV_COMPOSE_V2.override(
|
|
True
|
|
), envs.SGLANG_CP_SHARED_KV_COMPOSE_SYMM.override(True):
|
|
for layer_id in range(41, 45):
|
|
symm_idx, symm_dense_pages = _compose_index(
|
|
layer_id, writers=s["current_page_writer_ranks"]
|
|
)
|
|
torch.cuda.synchronize()
|
|
assert torch.equal(ref_dense_pages, symm_dense_pages), (
|
|
f"rank{rank} layer{layer_id}: index dense_pages mismatch"
|
|
)
|
|
if not torch.equal(ref_idx, symm_idx):
|
|
bad = (
|
|
(ref_idx != symm_idx).any(dim=-1).nonzero().reshape(-1)[:8]
|
|
)
|
|
raise AssertionError(
|
|
f"rank{rank} layer{layer_id}: index symm mismatch at pages "
|
|
f"{bad.cpu().tolist()}"
|
|
)
|
|
dist.barrier()
|
|
if rank == 0:
|
|
print("PASS: index sync symm compose byte-identical (4 layers)", flush=True)
|
|
|
|
# ---- Phase 4+5: prefetch-path symm consume (MLA + index), bs=1. ----
|
|
_check_prefetch_symm(rank, world, device)
|
|
dist.barrier()
|
|
if rank == 0:
|
|
print(
|
|
"PASS: prefetch consume_prefix_with_current symm (MLA + index) "
|
|
"byte-identical to the sync compose",
|
|
flush=True,
|
|
)
|
|
dist.destroy_process_group()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|