Files
kernbench2/tests/attention/test_gqa_decode_batch_sweep.py
T
ywkang 711a9a257f gqa short-context: exclude KV deploy from wall; correct HBM peak; batch scaffold
Metric fixes (test harnesses, deploy artifact + wrong constant):
- wall = max(t_end) - min(t_start): exclude the one-time KV-cache deploy
  from the measured decode/prefill step (it was 92-99% of wall, mapping-
  invariant, and masked the real per-mapping separation).
- PEAK_PE_HBM_BPS 128 -> 256 B/ns (8 channels/PE x 32 GB/s); the old value
  was half the modeled per-PE HBM BW, so hbm_bw_util read >1.0 once wall
  was corrected. All six short-context sweep CSVs regenerated/repatched.

Result: the four KV mappings now separate along a {64,32,16,8}-active-PE
ladder (decode 8-kv/1-kv = 4.8x at 8K, 7.4x at 64K), not "modest/tied" as
before; decode is bandwidth-bound at 46-76% of the 256 GB/s per-PE ceiling.

Report (S5.2 rewrite):
- Replace the tied-wall / 8x-per-PE-util claims (both deploy artifacts)
  with the corrected separation and a density trade-off (per-CUBE KV
  footprint, Fig 16 top panel switched to a wall-invariant metric).
- Add a projected latency-vs-batched-throughput analysis (marked
  projected, not measured): dense mappings win throughput, 1-kv wins
  latency; converges at long context.
- Regenerate Fig 15/16/17/18; fix plot script hardcoded ROOT path.

Batch experiment (Part 2, cube_base):
- Add backward-compatible cube_base=0 scalar to the decode kernel so a
  batched user placed at DPPolicy.cube_start addresses 0-based shards.
  Default preserves single-user behavior (32 decode tests pass unchanged).
- New batch harness (skipped): concurrent B>=2 launches hit a sim routing
  issue (sip0.cube0.pe8); single-user path verified. Concurrency fix next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 16:25:04 -07:00

184 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Batch GQA decode sweep — concurrent independent users on ONE SIP.
Fixes the target to a single 16-cube SIP and scales the batch B = number
of concurrent decode users, each with its own (Q, K, V) placed on a
disjoint cube group ``[u*C, u*C + C)`` via ``DPPolicy.cube_start`` and
addressed by the kernel's ``cube_base`` local-index parameter. All B
launches are deferred (``_defer_wait=True``) so they overlap on the shared
discrete-event clock; ``run_bench`` drains them together.
Per mapping the SIP holds ``16 // C`` users at once (A1=2, A2=4, A4=8,
B=16). We sweep B up to that capacity and record aggregate latency,
throughput (users / latency), and mean per-user latency, to see which
mapping's throughput scales best with batch.
Metric convention matches the corrected short-context sweeps:
``wall = max(t_end) - min(t_start)`` over the op log (deploy excluded).
"""
from __future__ import annotations
import csv
from pathlib import Path
import pytest
from kernbench.ccl.install import load_ccl_config, resolve_algorithm_config
from kernbench.ccl.sfr_config import configure_sfr_intercube_multisip
from kernbench.policy.placement.dp import DPPolicy
from kernbench.runtime_api.bench_runner import run_bench
from kernbench.runtime_api.types import resolve_device
from kernbench.sim_engine.engine import GraphEngine
from kernbench.topology.builder import resolve_topology
TOPOLOGY_DEFAULT = Path(__file__).resolve().parents[2] / "topology.yaml"
D_HEAD = 64
DTYPE = "f16"
TILE_S_KV = 1024
P = 8
H_KV = 8
CUBES_PER_SIP = 16
# (mode, kv_per_cube, C); users-per-SIP capacity = CUBES_PER_SIP // C.
MODES = [("A1", 1, 8), ("A2", 2, 4), ("A4", 4, 2), ("B", 8, 1)]
CONTEXT_LENGTHS = [8 * 1024, 64 * 1024]
CSV_OUT = (Path(__file__).resolve().parents[2]
/ "docs" / "sweeps" / "decode_batch_sweep.csv")
def _ccl_cfg():
return resolve_algorithm_config(
load_ccl_config(), name="lrab_hierarchical_allreduce",
)
def _engine_factory(t, d):
return GraphEngine(getattr(t, "topology_obj", t), enable_data=True)
def _run_batch(*, kv_per_cube: int, C: int, S_kv: int, B: int, h_q: int = 64):
"""Launch B concurrent decode users on disjoint cube groups of SIP 0."""
from kernbench.benches.gqa_helpers.short_ctx._gqa_attention_decode_short import (
_validate_config as _validate_decode_config,
gqa_attention_decode_short_kernel,
)
T_q = 1
_validate_decode_config(kv_per_cube=kv_per_cube, T_q=T_q, P=P, C=C,
h_q=h_q, h_kv=H_KV, S_kv=S_kv)
Q_ROWS = kv_per_cube * T_q
Q_COLS = (h_q * D_HEAD) // kv_per_cube
k_rows = (H_KV * S_kv * D_HEAD) // TILE_S_KV
topo = resolve_topology(str(TOPOLOGY_DEFAULT))
def _bench_fn(ctx):
configure_sfr_intercube_multisip(ctx.engine, ctx.spec, _ccl_cfg())
pending = []
for u in range(B):
cs = u * C
dp_q = DPPolicy(cube="column_wise", pe="replicate",
num_cubes=C, num_pes=P, cube_start=cs)
dp_kv = DPPolicy(cube="row_wise", pe="row_wise",
num_cubes=C, num_pes=P, cube_start=cs)
dp_o = DPPolicy(cube="column_wise", pe="replicate",
num_cubes=C, num_pes=P, cube_start=cs)
q = ctx.zeros((Q_ROWS, Q_COLS), dtype=DTYPE, dp=dp_q,
name=f"q_u{u}")
k = ctx.zeros((k_rows, TILE_S_KV), dtype=DTYPE, dp=dp_kv,
name=f"k_u{u}")
v = ctx.zeros((H_KV * S_kv, D_HEAD), dtype=DTYPE, dp=dp_kv,
name=f"v_u{u}")
o = ctx.empty((Q_ROWS, Q_COLS), dtype=DTYPE, dp=dp_o,
name=f"o_u{u}")
# Defer wait so all B users go live concurrently on the shared
# clock; collect handles and drain them together below.
pending += ctx.launch(
f"gqa_decode_u{u}",
gqa_attention_decode_short_kernel,
q, k, v, o,
T_q, S_kv, h_q, H_KV, D_HEAD, C, P, kv_per_cube, cs,
_auto_dim_remap=False,
_defer_wait=True,
)
for h, _sip_id, meta in pending:
ctx.wait(h, _meta=meta)
return run_bench(
topology=topo, bench_fn=_bench_fn,
device=resolve_device("sip:0"), engine_factory=_engine_factory,
)
def _cube_of(rec) -> int:
"""Physical cube index of an op record, or -1 if not a PE op."""
cid = rec.component_id or ""
if ".cube" in cid:
try:
return int(cid.split(".cube")[1].split(".")[0])
except (ValueError, IndexError):
return -1
return -1
def _metrics(r, *, mode, kv_per_cube, C, S_kv, B):
op_log = r.engine.op_log
wall_ns = max(rec.t_end for rec in op_log) - min(rec.t_start for rec in op_log)
# Per-user latency: group ops by which user's cube band they touch.
per_user = []
for u in range(B):
lo, hi = u * C, u * C + C
starts = [rec.t_start for rec in op_log if lo <= _cube_of(rec) < hi]
ends = [rec.t_end for rec in op_log if lo <= _cube_of(rec) < hi]
if starts and ends:
per_user.append(max(ends) - min(starts))
mean_user_ns = sum(per_user) / len(per_user) if per_user else 0.0
agg_us = wall_ns / 1000
return {
"mode": mode,
"kv_per_cube": kv_per_cube,
"C": C,
"S_kv": S_kv,
"B": B,
"capacity": CUBES_PER_SIP // C,
"agg_latency_us": agg_us,
"mean_user_latency_us": mean_user_ns / 1000,
"throughput_users_per_us": (B / agg_us) if agg_us else 0.0,
}
@pytest.mark.slow
@pytest.mark.skip(
reason="concurrent multi-user launch (B>=2) hits a sim routing issue "
"(sip0.cube0.pe8 not found); single-user path works. Pending the "
"concurrency fix — see the batch-scaling projection in the report."
)
def test_decode_batch_sweep():
"""Sweep 4 modes × contexts × B=1..capacity; dump batch-scaling CSV."""
rows = []
for mode, kv_per_cube, C in MODES:
capacity = CUBES_PER_SIP // C
for S_kv in CONTEXT_LENGTHS:
for B in range(1, capacity + 1):
print(f">>> BATCH {mode} S_kv={S_kv//1024}K B={B}/{capacity}",
flush=True)
r = _run_batch(kv_per_cube=kv_per_cube, C=C, S_kv=S_kv, B=B)
assert r.completion.ok, (
f"BATCH {mode} S_kv={S_kv} B={B}: {r.completion}"
)
m = _metrics(r, mode=mode, kv_per_cube=kv_per_cube,
C=C, S_kv=S_kv, B=B)
rows.append(m)
print(f" agg={m['agg_latency_us']:.2f}us "
f"user={m['mean_user_latency_us']:.2f}us "
f"tput={m['throughput_users_per_us']:.3f} u/us",
flush=True)
CSV_OUT.parent.mkdir(parents=True, exist_ok=True)
with CSV_OUT.open("w", newline="") as f:
w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
w.writeheader()
w.writerows(rows)
print(f"\nWrote {CSV_OUT} ({len(rows)} rows)")