13f5cbc317
Concurrency fix (completes the cube_base change): the decode kernel's
output store o_base still used the global cube_id while every load used
the user-local cube_local. For a single user (cube_base=0) they coincide,
so it was masked; a second batched user (cube_base>0) overshot its o shard
into an unmapped VA, mis-decoded to a phantom sip0.cube0.pe8 and raised a
RoutingError. Switch o_base to cube_local (one line); single-user results
are byte-identical (decode smoke/correctness pass).
Batch harness (un-skipped): create all users' tensors first, then submit
all launches deferred and drain together, so deploys don't drive an
already-submitted launch and serialize the batch. Concurrent users on
disjoint CUBE groups now overlap to within 3-5% of the single-user
latency. Swept B in {1,2,capacity} at 8K (high-B dense runs are the
expensive ones; throughput is near-linear in B).
Measured result: aggregate throughput at a full SIP rises from 0.86
(1-kv-per-cube, 2 users) to 1.41 requests/us (8-kv-per-cube, 16 users) —
the dense mapping wins throughput, the spread mapping wins latency.
S5.2 batch paragraph updated projected -> measured; add batch_scaling
figure and plot_batch_scaling.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
194 lines
7.5 KiB
Python
194 lines
7.5 KiB
Python
"""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]
|
||
|
||
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())
|
||
# Deploy ALL users' tensors first. Each deploy blocks (advances the
|
||
# clock), so if a launch were already submitted a later deploy would
|
||
# drive it to completion and serialize the batch. Creating every
|
||
# tensor before any launch keeps the deploys from touching kernels.
|
||
users = []
|
||
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}")
|
||
users.append((u, cs, q, k, v, o))
|
||
# Now submit every launch deferred (no blocking wait between them),
|
||
# then drain together so all B run concurrently on the shared clock.
|
||
pending = []
|
||
for u, cs, q, k, v, o in users:
|
||
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
|
||
def test_decode_batch_sweep():
|
||
"""Sweep 4 modes × contexts × B ∈ {1, 2, capacity}; dump batch CSV.
|
||
|
||
Throughput is near-linear in B (aggregate latency stays within a few
|
||
percent of the single-user latency across the batch), so the endpoints
|
||
plus a low point capture the scaling; the high-B runs of the dense
|
||
mappings are the expensive ones and full 1..capacity granularity is not
|
||
needed for the trade-off.
|
||
"""
|
||
rows = []
|
||
for mode, kv_per_cube, C in MODES:
|
||
capacity = CUBES_PER_SIP // C
|
||
batch_sizes = sorted({1, 2, capacity})
|
||
for S_kv in CONTEXT_LENGTHS:
|
||
for B in batch_sizes:
|
||
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)")
|