analytical-viz: roofline tab — headline step-latency + cost-per-token plots, B knob

Two new plots at the top of the Chip Roofline tab (right after the
knob row), side by side:

1. **Latency per decode step** — one forward pass's total time as B
   grows. weight_fetch (flat in B), compute (linear), KV (linear),
   total. The SLO / TTFT view — bigger B = longer step.

2. **Cost per token** = step latency ÷ B. weight (shrinks 1/B),
   compute (flat), KV (flat), total. The efficiency / cost view —
   bigger B (up to ~2·B*) = cheaper per token.

Same underlying decomposition, two divisors. Puts the classic
throughput ↔ latency tradeoff in one glance.

Also added a **B (batch size) slider** next to the existing S_kv
slider. Both plots draw a vertical marker at the current B; the
regime KPI ("memory-bound / compute-bound / kv-bound") now uses the
slider B, so you can sweep B live and watch the label flip when you
cross B*.

step_latency_curve + StepLatencyPoint added to chip_roofline; 5 new
tests cover: weight_s batch-invariant, compute_s/kv_s linear in B,
step_total == per_token_total × B (definitional), and step ==
per_token at B=1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 13:40:37 -07:00
parent c99a238826
commit 221097ef08
3 changed files with 219 additions and 20 deletions
@@ -15,6 +15,7 @@ from tests.analytical_visualization.chip_roofline import (
knee_batch,
kv_bytes_per_token,
per_token_latency_curve,
step_latency_curve,
t_com,
t_mem_long,
t_mem_short,
@@ -281,6 +282,59 @@ def test_utilization_drops_monotonically_with_context():
assert b < a
# ── Step latency (undivided by B) ──────────────────────────────────
def test_step_weight_is_batch_invariant():
"""step_weight = N·b/W — same for every B (loaded once per step)."""
m = MachineParams()
model = PRESETS["Llama 3 8B"].model
pts = step_latency_curve(m, model, [1, 8, 64, 512], s_kv=1024)
ws = [p.weight_s for p in pts]
assert all(w == pytest.approx(ws[0]) for w in ws), ws
def test_step_compute_linear_in_batch():
"""step_compute = 2·N·B/C — doubling B doubles compute."""
m = MachineParams()
model = PRESETS["Llama 3 8B"].model
pts = step_latency_curve(m, model, [1, 2, 8], s_kv=1024)
assert pts[1].compute_s == pytest.approx(2 * pts[0].compute_s)
assert pts[2].compute_s == pytest.approx(8 * pts[0].compute_s)
def test_step_kv_linear_in_batch():
"""step_kv = B · S_kv · kv_bpt / W — linear in B."""
m = MachineParams()
model = PRESETS["Llama 3 8B"].model
pts = step_latency_curve(m, model, [1, 4], s_kv=1024)
assert pts[1].kv_s == pytest.approx(4 * pts[0].kv_s)
def test_step_total_equals_per_token_times_batch():
"""The step and per-token views are just ÷ B / × B of each other."""
m = MachineParams()
model = PRESETS["Llama 3 8B"].model
B_range = [1, 4, 16, 64]
s_kv = 1024
step_pts = step_latency_curve(m, model, B_range, s_kv=s_kv)
tok_pts = per_token_latency_curve(m, model, B_range, s_kv=s_kv)
for sp, tp in zip(step_pts, tok_pts):
assert sp.total_s == pytest.approx(tp.total_s * sp.batch)
def test_step_and_per_token_identical_at_b1():
"""At B=1, step latency == per-token cost (dividing by 1)."""
m = MachineParams()
model = PRESETS["Llama 3 8B"].model
sp = step_latency_curve(m, model, [1], s_kv=1024)[0]
tp = per_token_latency_curve(m, model, [1], s_kv=1024)[0]
assert sp.total_s == pytest.approx(tp.total_s)
assert sp.weight_s == pytest.approx(tp.weight_s)
assert sp.compute_s == pytest.approx(tp.compute_s)
assert sp.kv_s == pytest.approx(tp.kv_s)
# ── App wiring: tab exists on the Streamlit app ────────────────────