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
@@ -235,16 +235,18 @@ def _iter_reduced_parallelism(
def _best_parallelism_for_hw(
model: ModelConfig, machine: MachineParams,
s_kv: int, mode: str,
s_kv: int, mode: str, include_ffn: bool = True,
) -> ConfigScore | None:
"""Return the latency-minimum feasible parallelism for this HW.
Feasibility: fits memory + placement_valid. Returns None if nothing fits.
``include_ffn`` is forwarded to score_config so the "latency" ranked on
matches the caller's attention-only vs full-model choice.
"""
best: ConfigScore | None = None
for topo in _iter_reduced_parallelism(model, s_kv, mode):
cfg = FullConfig(model=model, topo=topo, machine=machine)
s = score_config(cfg)
s = score_config(cfg, include_ffn=include_ffn)
if not (s.fits_memory and s.placement_valid):
continue
if best is None or s.total_latency_ns < best.total_latency_ns:
@@ -254,7 +256,7 @@ def _best_parallelism_for_hw(
def _best_parallelism_two_stage(
model: ModelConfig, machine: MachineParams,
s_kv: int, mode: str,
s_kv: int, mode: str, include_ffn: bool = True,
) -> ConfigScore | None:
"""Fast fallback: use autosuggest's memory-min (CP,TP,PP) then score it."""
sug = auto_suggest(model, machine, s_kv, mode)
@@ -270,7 +272,7 @@ def _best_parallelism_two_stage(
cp_ring_variant="qoml" if mode == "decode" and sug.cp > 1 else "kv",
)
cfg = FullConfig(model=model, topo=topo, machine=machine)
s = score_config(cfg)
s = score_config(cfg, include_ffn=include_ffn)
return s if s.fits_memory and s.placement_valid else None
@@ -308,6 +310,7 @@ def compute_sensitivity(
baseline_hw: HardwareCandidate,
parallelism: ConfigScore,
model: ModelConfig, s_kv: int, mode: str,
include_ffn: bool = True,
) -> list[SensitivityRow]:
"""For each HW knob, double it (holding others at baseline) and measure
the latency change. Same parallelism used throughout so we isolate the
@@ -318,7 +321,7 @@ def compute_sensitivity(
baseline_cfg = FullConfig(
model=model, topo=baseline_topo, machine=baseline_machine,
)
baseline_score = score_config(baseline_cfg)
baseline_score = score_config(baseline_cfg, include_ffn=include_ffn)
baseline_latency = baseline_score.total_latency_ns
for knob in _SENSITIVITY_KNOBS:
@@ -327,7 +330,7 @@ def compute_sensitivity(
doubled_cfg = FullConfig(
model=model, topo=baseline_topo, machine=doubled_hw.as_machine(),
)
doubled_score = score_config(doubled_cfg)
doubled_score = score_config(doubled_cfg, include_ffn=include_ffn)
rows.append(SensitivityRow(
knob=knob,
baseline_value=getattr(baseline_hw, knob),
@@ -348,19 +351,28 @@ def joint_explore(
s_kv: int,
mode: str,
depth: str = "balanced",
include_ffn: bool = True,
) -> JointExploreResult:
"""Sweep HW candidates × parallelism, return joint Pareto + sensitivity.
Sensitivity is computed around the LATENCY-MINIMUM joint point (the
"best fast" config), doubling each HW knob one at a time.
``include_ffn=False`` restricts scoring to attention stages only —
both the per-HW parallelism search and the sensitivity ranking use
the same restriction.
"""
all_scores: list[JointScore] = []
for hw in enumerate_hardware(depth):
machine = hw.as_machine()
if depth == "two_stage":
par = _best_parallelism_two_stage(model, machine, s_kv, mode)
par = _best_parallelism_two_stage(
model, machine, s_kv, mode, include_ffn=include_ffn,
)
else:
par = _best_parallelism_for_hw(model, machine, s_kv, mode)
par = _best_parallelism_for_hw(
model, machine, s_kv, mode, include_ffn=include_ffn,
)
if par is None:
continue
all_scores.append(JointScore(
@@ -379,6 +391,7 @@ def joint_explore(
best = all_scores[0]
sensitivity = compute_sensitivity(
best.hardware, best.parallelism, model, s_kv, mode,
include_ffn=include_ffn,
)
return JointExploreResult(