"""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)")