analytical-viz: roofline tab — PE memory budget, AI sensitivity, FLOPs/BW knobs

Three additions to the Chip Roofline tab:

1. **PE memory budget** section (two side-by-side stacked-area plots)
   - Left: per-PE memory vs S_kv (128..1M, log). Stacks: weights,
     KV, transient. HBM budget line + current-S_kv marker.
   - Right: per-PE memory vs B (1..256). Same stacks, current-B
     marker. KV grows linearly with either axis; weights are
     invariant of B and S_kv (fixed by CP·TP·PP sharding).
   Uses actual sharded per-PE quantities from memory_layout so the
   numbers match what the deployment would really allocate.

2. **AI sensitivity** section (two side-by-side line plots)
   - Left: AI vs chip-parameter multiplier. Two curves: scale FLOPs
     (AI grows linearly), scale HBM BW (AI shrinks inversely).
   - Right: B* vs multiplier. Same shape as AI (for BF16 where B*=AI).
   Vertical markers show current FLOPs/BW knob positions.

3. **Two new knobs** in the top row: FLOPs × multiplier and HBM BW ×
   multiplier (0.25..8, step 0.25). The AI-sensitivity plot markers
   move with them; the header caption shows scaled AI + B* for the
   selected point.

Also: formula annotation on the headline 'Latency per decode step'
plot header showing t_step = N·b/W + 2·N·B/C + B·S_kv·kv_bpt/W.

Pure additions in chip_roofline.py (all testable):
- MemoryBudgetPoint dataclass
- memory_budget_curve_vs_skv, memory_budget_curve_vs_batch
- AISensitivityPoint dataclass
- ai_sensitivity_curve(machine, model, mults, axis='flops'|'bw')

9 new tests cover linear scaling of KV with S_kv/B, weight
invariance across sweeps, over-budget flag flip, free_gb clamp,
FLOPs multiplier → AI linear, BW multiplier → AI inverse, B*
tracks AI for BF16, bad axis raises.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 14:10:02 -07:00
parent 9b4aee83da
commit a1c3c28f62
3 changed files with 398 additions and 97 deletions
+114 -2
View File
@@ -27,9 +27,9 @@ exactly at B*.
"""
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import dataclass, replace
from .model_config import MachineParams, ModelConfig
from .model_config import FullConfig, MachineParams, ModelConfig
# Per-token bytes of BF16 MAC arithmetic: one multiply + one add = 2 FLOPs.
@@ -304,3 +304,115 @@ def step_latency_curve(machine: MachineParams, model: ModelConfig,
total_s=total,
))
return points
# ── PE memory budget curves ───────────────────────────────────────
@dataclass
class MemoryBudgetPoint:
axis_val: int # S_kv or B being swept
weights_gb: float
kv_gb: float
transient_gb: float
used_gb: float
free_gb: float # max(0, hbm_gb - used_gb)
over_budget: bool
def _budget_point(weights_bytes: int, kv_bytes: int, transient_bytes: int,
hbm_bytes: int, axis_val: int) -> MemoryBudgetPoint:
used = weights_bytes + kv_bytes + transient_bytes
return MemoryBudgetPoint(
axis_val=axis_val,
weights_gb=weights_bytes / 1e9,
kv_gb=kv_bytes / 1e9,
transient_gb=transient_bytes / 1e9,
used_gb=used / 1e9,
free_gb=max(0, hbm_bytes - used) / 1e9,
over_budget=(used > hbm_bytes),
)
def memory_budget_curve_vs_skv(cfg: FullConfig,
s_kv_range: list[int],
batch: int) -> list[MemoryBudgetPoint]:
"""Per-PE memory as S_kv sweeps. Uses cfg's current sharding
(CP, TP, PP). Sets topo.b = batch. Returns one point per S_kv."""
from .memory_layout import (
per_pe_kv_cache_bytes, per_pe_transient_bytes, per_pe_weight_bytes,
)
hbm_bytes = int(cfg.machine.pe_budget_bytes)
weights_bytes = per_pe_weight_bytes(cfg)
transient_bytes = per_pe_transient_bytes(cfg)
points: list[MemoryBudgetPoint] = []
for skv in s_kv_range:
swept_topo = replace(cfg.topo, s_kv=int(skv), b=max(1, int(batch)))
swept_cfg = FullConfig(model=cfg.model, topo=swept_topo,
machine=cfg.machine)
kv_bytes = per_pe_kv_cache_bytes(swept_cfg)
points.append(_budget_point(weights_bytes, kv_bytes,
transient_bytes, hbm_bytes, int(skv)))
return points
def memory_budget_curve_vs_batch(cfg: FullConfig,
b_range: list[int],
s_kv: int) -> list[MemoryBudgetPoint]:
"""Per-PE memory as B sweeps. Sets topo.s_kv = s_kv."""
from .memory_layout import (
per_pe_kv_cache_bytes, per_pe_transient_bytes, per_pe_weight_bytes,
)
hbm_bytes = int(cfg.machine.pe_budget_bytes)
weights_bytes = per_pe_weight_bytes(cfg)
transient_bytes = per_pe_transient_bytes(cfg)
points: list[MemoryBudgetPoint] = []
for bs in b_range:
swept_topo = replace(cfg.topo, b=max(1, int(bs)), s_kv=int(s_kv))
swept_cfg = FullConfig(model=cfg.model, topo=swept_topo,
machine=cfg.machine)
kv_bytes = per_pe_kv_cache_bytes(swept_cfg)
points.append(_budget_point(weights_bytes, kv_bytes,
transient_bytes, hbm_bytes, int(bs)))
return points
# ── AI / B* sensitivity to hardware knobs ──────────────────────────
@dataclass
class AISensitivityPoint:
multiplier: float # scale factor applied to the base machine
peak_tflops: float
bw_gbs: float
ai: float # C / W
b_star: float # C·b/(2·W)
def ai_sensitivity_curve(machine: MachineParams, model: ModelConfig,
multipliers: list[float],
axis: str = "flops") -> list[AISensitivityPoint]:
"""Sweep FLOPs OR BW while holding the other fixed. Returns AI + B*
at each multiplier.
axis='flops' → scales peak_tflops_f16 (AI grows linearly).
axis='bw' → scales bw_hbm_gbs (AI shrinks inversely).
"""
if axis not in ("flops", "bw"):
raise ValueError(f"axis must be 'flops' or 'bw', got {axis!r}")
base_flops = machine.peak_tflops_f16
base_bw = machine.bw_hbm_gbs
points: list[AISensitivityPoint] = []
for k in multipliers:
if axis == "flops":
m = replace(machine, peak_tflops_f16=base_flops * k)
else:
m = replace(machine, bw_hbm_gbs=base_bw * k)
points.append(AISensitivityPoint(
multiplier=k,
peak_tflops=m.peak_tflops_f16,
bw_gbs=m.bw_hbm_gbs,
ai=arithmetic_intensity(m),
b_star=critical_batch(m, model),
))
return points