From the nsys CPU-gap attribution (launch thread, one 78-layer forward: 374ms API time; 642 cudaStreamSynchronize blocking 89.5ms and overlapping 122ms of the 505ms GPU idle; ~44ms pure-Python before concat_mla_absorb_q): - memory_pool_host: skip validate_page_aligned_token_indices on CUDA tensors in _get_indexer_page_indices and _prepare_load_page_indices — torch.any/torch.equal there cost a queue-deep cudaStreamSynchronize per layer-group submit (~0.42ms each, ~12.7ms/forward measured). Same construction-based-invariant guard the CacheController pair check already documents; CPU/test tensors stay validated. - nsa_indexer: per-batch _CpRaggedIndexPlan replaces the per-F-layer rebuild of the O(total-q-tokens) topk offset list and the 6-7 int32 ragged descriptor tensors (segment records, kv_lens/q_starts/q_lens/ k_bases/q_bases/current_bases). All inputs are batch metadata; the plan is anchored on the forward batch with a content key over cp_index. - nsa_indexer forward_indexer: read seq_lens_cpu instead of a device seq_lens[i].item() per request per layer (one stream sync each). - cp_shared_kv_runtime: get_or_build_batch_slot_spans caches the layer-invariant prefix/current slot spans per batch (the builders read logical_pages only for its shape); nsa_backend x3 + nsa_indexer call sites switched. Microbenchmark (idle H200, traced batch shape bs=12 / 44.6K q tokens, test/manual/bench_cpu_gap_fixes.py, equality-checked): validator path 197.1us -> 59.2us per submit under a busy queue (x3.3); ragged plan 3238.6us -> 36.2us per layer (x90, ~128ms launch-thread time per forward at 40 F-layers); slot spans 20.1us -> 0.5us (x41). Layer suites A/B vs HEAD: identical failure set (5 pre-existing CPU-tensor indexer tests), no regressions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
221 lines
6.8 KiB
Python
221 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Micro-benchmark for the pre-attention CPU-gap fixes (task #14).
|
|
|
|
Scenario mirrors the traced production batch: bs=12, prefix 640..26304 tok,
|
|
extends ~3.7K tok, cp8 in-seq-split (2*cp segments/request), page 64.
|
|
|
|
1. page-aligned validator skip on CUDA tensors
|
|
(memory_pool_host._get_indexer_page_indices hot path) — measured with a
|
|
busy GPU queue, because torch.any/.equal sync for the whole queue.
|
|
2. ragged index descriptor plan: rebuild-per-layer (old) vs per-batch cache.
|
|
3. slot-span builders: rebuild-per-layer (old) vs per-batch cache.
|
|
|
|
Run (single GPU is enough):
|
|
PYTHONPATH=python python test/manual/bench_cpu_gap_fixes.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from types import SimpleNamespace
|
|
|
|
import torch
|
|
|
|
from sglang.srt.layers.attention.nsa import nsa_indexer
|
|
from sglang.srt.layers.attention.nsa.cp_shared_kv_runtime import (
|
|
build_batch_current_slot_spans,
|
|
build_batch_prefix_slot_spans,
|
|
get_or_build_batch_slot_spans,
|
|
)
|
|
from sglang.srt.mem_cache.page_index_utils import (
|
|
validate_page_aligned_token_indices,
|
|
)
|
|
|
|
DEV = torch.device("cuda", 0)
|
|
PAGE = 64
|
|
BS = 12
|
|
CP = 8
|
|
PREFIX_LENS = [19200, 256] + [26304] * 10
|
|
EXTEND_LENS = [3776, 7360] + [3347] * 10
|
|
REPS = 200
|
|
|
|
|
|
def timed(fn, reps=REPS, warmup=20):
|
|
for _ in range(warmup):
|
|
fn()
|
|
torch.cuda.synchronize()
|
|
t0 = time.perf_counter()
|
|
for _ in range(reps):
|
|
fn()
|
|
torch.cuda.synchronize()
|
|
return (time.perf_counter() - t0) / reps * 1e6 # us
|
|
|
|
|
|
def make_fake_batch():
|
|
fb = SimpleNamespace()
|
|
fb.seq_lens_cpu = torch.tensor(
|
|
[p + e for p, e in zip(PREFIX_LENS, EXTEND_LENS)], dtype=torch.int64
|
|
)
|
|
fb.extend_seq_lens_cpu = list(EXTEND_LENS)
|
|
fb.extend_prefix_lens_cpu = list(PREFIX_LENS)
|
|
return fb
|
|
|
|
|
|
def make_cp_index():
|
|
# 2*CP zigzag segments per request over the extend, page-aligned-ish.
|
|
cp_index = []
|
|
for req, extend in enumerate(EXTEND_LENS):
|
|
seg = max(PAGE, (extend // (2 * CP)) // PAGE * PAGE)
|
|
pos = 0
|
|
while pos < extend:
|
|
end = min(pos + seg, extend)
|
|
cp_index.append((req, pos, end))
|
|
pos = end
|
|
return cp_index
|
|
|
|
|
|
def bench_validator():
|
|
n_pages = 600 # ~one layer-group submit worth of pages
|
|
starts = torch.arange(n_pages, device=DEV, dtype=torch.int64) * PAGE
|
|
indices = (
|
|
starts[:, None] + torch.arange(PAGE, device=DEV, dtype=torch.int64)
|
|
).reshape(-1)
|
|
|
|
# Busy queue: enqueue ~0.5ms of GEMM before each validator call, the way
|
|
# the real submit lands behind a layer's compute.
|
|
a = torch.randn(2048, 2048, device=DEV, dtype=torch.bfloat16)
|
|
b = torch.randn(2048, 2048, device=DEV, dtype=torch.bfloat16)
|
|
|
|
def old_path():
|
|
for _ in range(4):
|
|
a @ b
|
|
validate_page_aligned_token_indices(indices, PAGE, "bench")
|
|
starts2 = indices.reshape(-1, PAGE)[:, 0] // PAGE
|
|
return starts2
|
|
|
|
def new_path():
|
|
for _ in range(4):
|
|
a @ b
|
|
if not indices.is_cuda:
|
|
validate_page_aligned_token_indices(indices, PAGE, "bench")
|
|
starts2 = indices.reshape(-1, PAGE)[:, 0] // PAGE
|
|
return starts2
|
|
|
|
# measure WALL time per call without trailing torch.cuda.synchronize in
|
|
# the loop (the sync inside the validator is exactly what we measure).
|
|
def wall(fn, reps=60, warmup=10):
|
|
for _ in range(warmup):
|
|
fn()
|
|
torch.cuda.synchronize()
|
|
t0 = time.perf_counter()
|
|
for _ in range(reps):
|
|
fn()
|
|
t1 = time.perf_counter() # NO sync: CPU-side blocking is the metric
|
|
torch.cuda.synchronize()
|
|
return (t1 - t0) / reps * 1e6
|
|
|
|
old = wall(old_path)
|
|
new = wall(new_path)
|
|
print(
|
|
f"1. validator (busy queue, {n_pages} pages): old={old:8.1f}us "
|
|
f"new={new:8.1f}us speedup x{old/new:.1f}"
|
|
)
|
|
|
|
|
|
def bench_ragged_plan():
|
|
fb = make_fake_batch()
|
|
cp_index = make_cp_index()
|
|
|
|
def old_build():
|
|
# the pre-fix per-layer work: full python loop + 7 tensor H2Ds
|
|
return nsa_indexer._build_cp_ragged_index_plan(fb, cp_index, DEV, None)
|
|
|
|
fb2 = make_fake_batch()
|
|
|
|
def cached():
|
|
return nsa_indexer._get_or_build_cp_ragged_index_plan(
|
|
fb2, cp_index, DEV, None
|
|
)
|
|
|
|
old = timed(old_build)
|
|
new = timed(cached)
|
|
n_tokens = sum(EXTEND_LENS)
|
|
print(
|
|
f"2. ragged index plan (bs={BS}, {len(cp_index)} segs, {n_tokens} q tok): "
|
|
f"per-layer rebuild={old:8.1f}us cached={new:8.1f}us speedup x{old/new:.0f}"
|
|
)
|
|
|
|
|
|
def bench_spans():
|
|
fb = make_fake_batch()
|
|
pages_per_req = max(
|
|
(p + e + PAGE - 1) // PAGE for p, e in zip(PREFIX_LENS, EXTEND_LENS)
|
|
)
|
|
logical_pages = torch.zeros((BS, pages_per_req), dtype=torch.int64)
|
|
|
|
def old_build():
|
|
prefix = build_batch_prefix_slot_spans(
|
|
logical_pages=logical_pages,
|
|
prefix_lens_cpu=PREFIX_LENS,
|
|
page_size=PAGE,
|
|
)
|
|
current = build_batch_current_slot_spans(
|
|
logical_pages=logical_pages,
|
|
prefix_lens_cpu=PREFIX_LENS,
|
|
extend_lens_cpu=EXTEND_LENS,
|
|
page_size=PAGE,
|
|
)
|
|
return prefix, current
|
|
|
|
def cached():
|
|
return get_or_build_batch_slot_spans(
|
|
fb,
|
|
logical_pages=logical_pages,
|
|
prefix_lens_cpu=PREFIX_LENS,
|
|
extend_lens_cpu=EXTEND_LENS,
|
|
page_size=PAGE,
|
|
want_prefix=True,
|
|
)
|
|
|
|
old = timed(old_build, reps=2000)
|
|
new = timed(cached, reps=2000)
|
|
print(
|
|
f"3. slot spans (bs={BS}): per-layer rebuild={old:8.1f}us "
|
|
f"cached={new:8.1f}us speedup x{old/new:.0f}"
|
|
)
|
|
|
|
|
|
def main():
|
|
torch.cuda.init()
|
|
print(f"device: {torch.cuda.get_device_name(0)}")
|
|
bench_validator()
|
|
bench_ragged_plan()
|
|
bench_spans()
|
|
# equality check: cached plan tensors match a fresh build
|
|
fb = make_fake_batch()
|
|
cp_index = make_cp_index()
|
|
p1 = nsa_indexer._build_cp_ragged_index_plan(fb, cp_index, DEV, None)
|
|
p2 = nsa_indexer._get_or_build_cp_ragged_index_plan(fb, cp_index, DEV, None)
|
|
assert torch.equal(p1.topk_indices_offset_override, p2.topk_indices_offset_override)
|
|
assert torch.equal(p1.kv_lens, p2.kv_lens) and torch.equal(p1.q_bases, p2.q_bases)
|
|
assert p1.segment_records == p2.segment_records
|
|
s1 = build_batch_current_slot_spans(
|
|
logical_pages=torch.zeros((BS, 512), dtype=torch.int64),
|
|
prefix_lens_cpu=PREFIX_LENS,
|
|
extend_lens_cpu=EXTEND_LENS,
|
|
page_size=PAGE,
|
|
)
|
|
_, s2 = get_or_build_batch_slot_spans(
|
|
SimpleNamespace(),
|
|
logical_pages=torch.zeros((BS, 512), dtype=torch.int64),
|
|
prefix_lens_cpu=PREFIX_LENS,
|
|
extend_lens_cpu=EXTEND_LENS,
|
|
page_size=PAGE,
|
|
want_prefix=False,
|
|
)
|
|
assert s1 == s2
|
|
print("EQUALITY CHECKS PASS")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|