eec61838b5
Wires a fourth variant into the Cube-SP × PE-SP long-context decode composite command-form study to surface the worst-case PE_CPU dispatch inflation that the coarse composite forms delegate to PE_SCHEDULER (ADR-0065): per-block DMA of Q/K/V slices, tl.dot per 16³ block, deferred K-inner sum outside the K loop. Renamed _tiled.py → _hand_tiled_16x16x16.py; coarse-primitive kernel retained for the 4-cases bench, paper scripts, and the golden byte-equal regression guard. New breakdown bar chart at S_kv=128K shows engine (~460 μs) dominates all three variants; hand-tiled adds ~68 μs PE_CPU dispatch on top — composite forms sit essentially on the memory-bound floor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
148 lines
5.0 KiB
Python
148 lines
5.0 KiB
Python
"""Comparative figure for the Case-6 composite-command decode study.
|
||
|
||
Reads sweep_decode_composite.json (emitted by milestone-1h-gqa, sweep
|
||
``composite``) and writes one two-panel PNG into the bench-output dir:
|
||
|
||
gqa_decode_long_ctx_composite.png
|
||
Left — end-to-end decode latency (µs) vs context length, per command
|
||
form (primitive / composite / composite_extended).
|
||
Right — PE_CPU command count vs context length: the hand-tiled
|
||
primitive kernel issues O(n_tiles) commands (rises with
|
||
context), while the coarse composite forms issue O(1) and
|
||
*saturate* — PE_SCHEDULER absorbs the per-tile fan-out.
|
||
|
||
The x-axis is the global context length S_kv; each PE owns
|
||
S_local = S_kv/(C·P=64) tokens, so at the 1M production point every PE
|
||
runs Q·Kᵀ of (G·T_q, d_head)·(d_head, 16384) and P·V of
|
||
(G·T_q, 16384)·(16384, d_head).
|
||
|
||
Run (after the bench):
|
||
GQA_1H_RUN=1 GQA_1H_SWEEPS=composite python -m kernbench.cli.main run \\
|
||
--bench milestone-1h-gqa --topology topology.yaml
|
||
python scripts/paper/paper_plot_gqa_decode_long_ctx_composite.py
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
|
||
import matplotlib
|
||
|
||
matplotlib.use("Agg")
|
||
import matplotlib.pyplot as plt # noqa: E402
|
||
|
||
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
||
_FIG_DIR = (
|
||
_REPO_ROOT / "src" / "kernbench" / "benches"
|
||
/ "1H_milestone_output" / "gqa" / "long_ctx"
|
||
)
|
||
_SWEEP_JSON = _FIG_DIR / "sweep_decode_composite.json"
|
||
_PAPER_FIG_DIR = (
|
||
_REPO_ROOT / "docs" / "report" / "1H-codesign-paper" / "figures"
|
||
)
|
||
|
||
_N_RANKS = 64 # C·P for the Case-6 64-way split.
|
||
|
||
# variant key → (display label, colour, marker)
|
||
_VARIANT_STYLE = {
|
||
"primitive_tiled": ("primitive hand-tiled (16×16×16)", "#8b5a2b", "D"),
|
||
"primitive": ("primitive (tl.dot, hand-tiled)", "#c0504d", "o"),
|
||
"composite": ("composite GEMM", "#3b6ea5", "s"),
|
||
"composite_extended": ("composite + softmax_merge", "#4f8a4f", "^"),
|
||
}
|
||
_ORDER = ("primitive_tiled", "composite", "composite_extended")
|
||
|
||
|
||
def _load() -> dict:
|
||
return json.loads(_SWEEP_JSON.read_text())
|
||
|
||
|
||
def _series(rows: list[dict], variant: str, key: str):
|
||
"""Sorted (S_kv, value) series for a variant, skipping null values
|
||
(latency is only measured over the tractable S_kv subset)."""
|
||
pts = sorted(
|
||
((r["S_kv"], r[key]) for r in rows
|
||
if r["variant"] == variant and r.get(key) is not None),
|
||
key=lambda t: t[0],
|
||
)
|
||
return [p[0] for p in pts], [p[1] for p in pts]
|
||
|
||
|
||
def _xticklabels(s_kvs: list[int]) -> list[str]:
|
||
out = []
|
||
for s in s_kvs:
|
||
if s >= 1 << 20:
|
||
out.append(f"{s // (1 << 20)}M")
|
||
else:
|
||
out.append(f"{s // 1024}K")
|
||
return out
|
||
|
||
|
||
def main() -> None:
|
||
sweep = _load()
|
||
rows = sweep["rows"]
|
||
s_kv_op = sweep["s_kv_opcount"]
|
||
s_kv_lat = sweep["s_kv_latency"]
|
||
|
||
fig, (ax_lat, ax_cmd) = plt.subplots(1, 2, figsize=(13.0, 4.8))
|
||
|
||
for v in _ORDER:
|
||
label, color, marker = _VARIANT_STYLE[v]
|
||
xs, lat = _series(rows, v, "latency_ns")
|
||
ax_lat.plot(xs, [y / 1e3 for y in lat], marker=marker,
|
||
color=color, label=label, lw=2)
|
||
xs, cmds = _series(rows, v, "pe_cpu_cmd_count")
|
||
ax_cmd.plot(xs, cmds, marker=marker, color=color, label=label, lw=2)
|
||
|
||
ax_lat.set_xticks(s_kv_lat)
|
||
ax_lat.set_xticklabels(_xticklabels(s_kv_lat))
|
||
ax_cmd.set_xticks(s_kv_op)
|
||
ax_cmd.set_xticklabels(_xticklabels(s_kv_op), fontsize=8)
|
||
for ax in (ax_lat, ax_cmd):
|
||
ax.set_xscale("log", base=2)
|
||
ax.set_xlabel(
|
||
r"context length $S_{kv}$ "
|
||
r"($S_{\mathrm{local}}=S_{kv}/64$ per PE)"
|
||
)
|
||
ax.grid(True, ls=":", alpha=0.5)
|
||
ax.legend(fontsize=9)
|
||
|
||
ax_lat.set_ylabel("end-to-end decode latency (µs)")
|
||
ax_lat.set_title(
|
||
"Case-6 decode latency per command form\n"
|
||
"(memory-bound: command form does not move the critical path)"
|
||
)
|
||
ax_cmd.set_ylabel("PE_CPU commands issued")
|
||
ax_cmd.set_title(
|
||
"PE_CPU command count per command form\n"
|
||
"(primitive O(n$_\\mathrm{tiles}$) rises; composite O(1) saturates)"
|
||
)
|
||
ax_cmd.axvline(1 << 20, color="#888", ls="--", lw=1, alpha=0.7)
|
||
ax_cmd.annotate("1M production\ncontext", xy=(1 << 20, 0),
|
||
xytext=(1 << 18, 0.6), fontsize=8,
|
||
textcoords=("data", "axes fraction"), ha="right",
|
||
color="#555")
|
||
|
||
fig.suptitle(
|
||
"Case-6 (Cube-SP × PE-SP) long-context decode — use of composite "
|
||
"commands\nLLaMA-3.1-70B single-KV-head group (8 cubes × 8 PEs), "
|
||
"$T_q{=}1$",
|
||
fontsize=11,
|
||
)
|
||
fig.tight_layout(rect=(0, 0, 1, 0.94))
|
||
|
||
out = _FIG_DIR / "gqa_decode_long_ctx_composite.png"
|
||
fig.savefig(out, dpi=150)
|
||
plt.close(fig)
|
||
print(f"wrote {out}")
|
||
|
||
# Mirror into the paper figures dir (derived artifact).
|
||
if _PAPER_FIG_DIR.is_dir():
|
||
dst = _PAPER_FIG_DIR / out.name
|
||
dst.write_bytes(out.read_bytes())
|
||
print(f"copied {dst}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|