analytical-viz: attention-only vs attn+FFN via two sweep buttons

Both auto tabs now accept a scope choice at run time:
  - "Run sweep — Attention"        → include_ffn=False
  - "Run sweep — Attn + FFN/MoE"   → include_ffn=True

Each button runs an independent sweep and caches its result under its
own session_state key. The most recently clicked button determines the
displayed view; both caches persist so users can flip between the two
scopes without re-running.

Tabs:
  - Renamed "Auto Explore" → "Auto Suggest Parallelism"
    (accurately reflects that it only varies parallelism knobs; HW is
    held at the sidebar values).
  - "Auto Hardware" tab unchanged.
  - Still 6 top-level tabs; no additional tabs added.

Core changes:
  auto_explore.py:
    - New include_ffn: bool = True parameter on _sum_visible_latency,
      _efficiency, score_config, run_auto_explore, compute_parallelism_
      sensitivity. False drops all FFN stages from the summed latency.

  auto_hardware.py:
    - New include_ffn: bool = True parameter on joint_explore,
      compute_sensitivity, _best_parallelism_for_hw, _best_parallelism_
      two_stage. Forwards to score_config.

Both defaults keep existing tests byte-identical.

Verified:
  - 23 pytest tests pass (added 3 new: attn-only latency lower, attn-only
    Pareto non-empty, joint HW attn-only faster than full).
  - Smoke: Llama 70B decode 128K:
      * Attn+FFN best latency: 12.85 ms (unchanged)
      * Attention-only best:    7.35 ms (~57% of full)
      * Both sensitivities top-rank bw_hbm_gbs (physics preserved).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 13:58:29 -07:00
parent 91b63eb9f6
commit b8e1a3322f
5 changed files with 264 additions and 75 deletions
+26 -9
View File
@@ -164,7 +164,7 @@ def enumerate_configs(
# ── Scoring ──────────────────────────────────────────────────────────
def _sum_visible_latency(cfg: FullConfig) -> float:
def _sum_visible_latency(cfg: FullConfig, include_ffn: bool = True) -> float:
"""Total single-request latency (seconds) across all model layers.
A single request traverses every layer sequentially, whether the layers
@@ -176,23 +176,30 @@ def _sum_visible_latency(cfg: FullConfig) -> float:
PP therefore does NOT reduce single-request latency; it only improves
throughput under batching. This function is the single-request cost, so
we multiply by full model.layers regardless of PP.
``include_ffn=False`` restricts to attention stages only — useful for
isolating attention-kernel tuning from FFN cost.
"""
attn = sum(s.visible_s for s in all_stages(cfg))
ffn = sum(s.visible_s for s in all_ffn_stages(cfg))
ffn = sum(s.visible_s for s in all_ffn_stages(cfg)) if include_ffn else 0.0
per_layer = attn + ffn
return per_layer * cfg.model.layers
def _efficiency(cfg: FullConfig, latency_s: float) -> float:
def _efficiency(cfg: FullConfig, latency_s: float,
include_ffn: bool = True) -> float:
"""Geo-mean of compute-util and BW-util. Range ~ (0, 1].
- compute_util = achieved_flops / (peak_flops × pes × latency)
- bw_util = achieved_bytes / (peak_bw × pes × latency)
``include_ffn=False`` mirrors the same restriction as
:func:`_sum_visible_latency` — attention stages only.
"""
if latency_s <= 0:
return 0.0
attn = all_stages(cfg)
ffn = all_ffn_stages(cfg)
ffn = all_ffn_stages(cfg) if include_ffn else []
layers = math.ceil(cfg.model.layers / cfg.topo.pp)
total_flops = layers * sum(s.flops for s in attn + ffn)
total_bytes = layers * sum(s.mem_bytes for s in attn + ffn)
@@ -209,19 +216,24 @@ def _efficiency(cfg: FullConfig, latency_s: float) -> float:
return math.sqrt(compute_util * bw_util)
def score_config(cfg: FullConfig) -> ConfigScore:
def score_config(cfg: FullConfig, include_ffn: bool = True) -> ConfigScore:
"""Compute all 4 objectives + info fields for one config.
Feasibility (memory + placement) is stored but does NOT gate scoring —
infeasible configs get returned with fits_memory=False so callers can
filter or display them.
``include_ffn=False`` restricts latency + efficiency to attention stages
only. Memory feasibility is unchanged — still checks weights+KV+transient
fit in per-PE HBM since the model still exists physically.
"""
mem = compute_memory(cfg)
placement_ok = cfg.topo.placement_valid
latency_s = _sum_visible_latency(cfg)
latency_s = _sum_visible_latency(cfg, include_ffn=include_ffn)
throughput = 1.0 / latency_s if latency_s > 0 else 0.0
efficiency = _efficiency(cfg, latency_s) if latency_s > 0 else 0.0
efficiency = (_efficiency(cfg, latency_s, include_ffn=include_ffn)
if latency_s > 0 else 0.0)
fits = not mem.over_budget
reason = ""
@@ -333,6 +345,7 @@ def compute_parallelism_sensitivity(
machine: MachineParams,
s_kv: int,
mode: str,
include_ffn: bool = True,
) -> list[ParallelismSensitivityRow]:
"""For each parallelism knob (CP, TP, PP, DP, EP), sweep its values
holding the OTHER knobs fixed at the baseline. Reports latency + memory
@@ -364,7 +377,7 @@ def compute_parallelism_sensitivity(
# Build a variant TopologyConfig with just this one knob changed.
trial = replace(baseline_topo, **{knob: v})
cfg = FullConfig(model=model, topo=trial, machine=machine)
score = score_config(cfg)
score = score_config(cfg, include_ffn=include_ffn)
latencies.append(score.total_latency_ns)
fits.append(score.fits_memory and score.placement_valid)
rows.append(ParallelismSensitivityRow(
@@ -386,19 +399,23 @@ def run_auto_explore(
machine: MachineParams,
s_kv: int,
mode: str = "decode",
include_ffn: bool = True,
) -> AutoExploreResult:
"""Enumerate all configs, score each, extract Pareto frontier.
Returns both the full ``all_scores`` list (for the table view) and the
``pareto_scores`` subset (for the scatter/highlight view). Both are sorted
by total_latency_ns ascending.
``include_ffn=False`` restricts to attention stages only — useful for
isolating attention-kernel tuning independent of FFN cost.
"""
all_scores: list[ConfigScore] = []
total_enumerated = 0
for topo in enumerate_configs(model, s_kv, mode):
total_enumerated += 1
cfg = FullConfig(model=model, topo=topo, machine=machine)
all_scores.append(score_config(cfg))
all_scores.append(score_config(cfg, include_ffn=include_ffn))
all_scores.sort(key=lambda s: s.total_latency_ns)
pareto = pareto_frontier(all_scores)