bf5b659a3d
Both auto tabs now offer three sweep scopes via three buttons instead
of two:
- Run sweep — Attention → include_attention=T, include_ffn=F
- Run sweep — FFN/MoE → include_attention=F, include_ffn=T
- Run sweep — Attn + FFN/MoE → include_attention=T, include_ffn=T
Each button caches its result under its own session_state key; the most
recently clicked button drives the display. All three caches persist so
users can flip between scopes without re-running.
Core changes:
auto_explore.py + auto_hardware.py:
- New include_attention: bool = True param alongside include_ffn
- _sum_visible_latency, _efficiency, score_config, run_auto_explore,
compute_parallelism_sensitivity, joint_explore, compute_sensitivity,
_best_parallelism_for_hw, _best_parallelism_two_stage all wired.
- Attention and FFN are additive with no overlap: measured 7.35 ms
(attn) + 5.50 ms (ffn) = 12.85 ms (full) for Llama 70B decode 128K.
Bug fix (drive-by): the display block in both tab render functions was
incorrectly nested inside the "cached ctx is stale" warning branch, so
metrics/scatter/table/sensitivity/load only rendered when the cache
was stale. Un-indented and removed the dead trailing else-info block.
Verified:
- 24 pytest tests pass (added 1 new for FFN-only scope invariants)
- Smoke: Llama 70B decode 128K all three scopes produce sensible Pareto:
* Attention only: 7.35 ms (14 pareto configs)
* FFN / MoE only: 5.50 ms (34 pareto configs)
* Attn + FFN/MoE: 12.85 ms (6 pareto configs)
Sums add up exactly, confirming no overlap in stage summation.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
228 lines
9.3 KiB
Python
228 lines
9.3 KiB
Python
"""Interface + smoke tests for auto_explore.
|
|
|
|
Covers:
|
|
- enumerate_configs yields valid TopologyConfigs with expected pruning
|
|
- score_config returns a ConfigScore with all fields populated
|
|
- pareto_frontier is a subset of feasible and is non-empty for a
|
|
reasonable model+workload
|
|
- run_auto_explore returns a coherent AutoExploreResult (all_scores
|
|
sorted by latency, pareto ⊆ feasible ⊆ all_scores)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from tests.analytical_visualization.auto_explore import (
|
|
ConfigScore,
|
|
enumerate_configs,
|
|
pareto_frontier,
|
|
run_auto_explore,
|
|
score_config,
|
|
)
|
|
from tests.analytical_visualization.model_config import (
|
|
FullConfig, MachineParams,
|
|
)
|
|
from tests.analytical_visualization.model_presets import PRESETS
|
|
|
|
|
|
# ── Enumeration ─────────────────────────────────────────────────────
|
|
|
|
|
|
def test_enumerate_yields_valid_configs():
|
|
"""Enumerator produces TopologyConfigs with all 9 knobs set."""
|
|
model = PRESETS["Llama 3.1 70B"].model
|
|
it = enumerate_configs(model, s_kv=8192, mode="decode")
|
|
first = next(it)
|
|
for f in ("cp", "tp", "pp", "dp", "kv_shard_mode",
|
|
"ffn_shard_scope", "tp_placement", "cp_placement",
|
|
"cp_ring_variant"):
|
|
assert getattr(first, f) is not None, f"knob {f} not set"
|
|
assert first.s_kv == 8192
|
|
assert first.mode == "decode"
|
|
|
|
|
|
def test_enumerate_prunes_pp_beyond_layers():
|
|
"""PP > model.layers is skipped by the enumerator."""
|
|
model = PRESETS["Llama 3.1 70B"].model # layers=80
|
|
for cfg in enumerate_configs(model, s_kv=8192, mode="decode"):
|
|
assert cfg.pp <= model.layers
|
|
|
|
|
|
def test_enumerate_prunes_ffn_dp_when_dp_is_1():
|
|
"""ffn_shard_scope containing 'DP' is skipped when dp=1 (redundant)."""
|
|
model = PRESETS["Llama 3.1 70B"].model
|
|
for cfg in enumerate_configs(model, s_kv=8192, mode="decode"):
|
|
if cfg.dp == 1:
|
|
assert "DP" not in cfg.ffn_shard_scope
|
|
|
|
|
|
# ── Scoring ─────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_score_returns_populated_config_score():
|
|
"""score_config returns a fully-populated ConfigScore."""
|
|
model = PRESETS["Llama 3.1 70B"].model
|
|
topo = next(enumerate_configs(model, s_kv=8192, mode="decode"))
|
|
machine = MachineParams()
|
|
cfg = FullConfig(model=model, topo=topo, machine=machine)
|
|
score = score_config(cfg)
|
|
assert isinstance(score, ConfigScore)
|
|
assert score.total_latency_ns > 0
|
|
assert score.pes_used == topo.total_pes
|
|
assert 0.0 <= score.efficiency_score <= 1.0
|
|
assert score.hbm_utilization >= 0.0
|
|
|
|
|
|
# ── Pareto ──────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_pareto_subset_of_feasible():
|
|
"""Pareto scores are always feasible (memory + placement)."""
|
|
model = PRESETS["Llama 3.1 70B"].model
|
|
machine = MachineParams()
|
|
res = run_auto_explore(model, machine, s_kv=8192, mode="decode")
|
|
for p in res.pareto_scores:
|
|
assert p.fits_memory
|
|
assert p.placement_valid
|
|
|
|
|
|
def test_pareto_non_empty_when_feasible_configs_exist():
|
|
"""When at least one config fits, Pareto must have ≥ 1 entry."""
|
|
model = PRESETS["Llama 3.1 70B"].model
|
|
machine = MachineParams()
|
|
res = run_auto_explore(model, machine, s_kv=8192, mode="decode")
|
|
assert res.total_feasible > 0
|
|
assert len(res.pareto_scores) > 0
|
|
|
|
|
|
def test_pareto_frontier_non_dominated():
|
|
"""No Pareto entry is dominated by another."""
|
|
from tests.analytical_visualization.auto_explore import _dominates
|
|
model = PRESETS["Llama 3.1 70B"].model
|
|
machine = MachineParams()
|
|
res = run_auto_explore(model, machine, s_kv=8192, mode="decode")
|
|
for i, a in enumerate(res.pareto_scores):
|
|
for j, b in enumerate(res.pareto_scores):
|
|
if i == j:
|
|
continue
|
|
assert not _dominates(b, a), (
|
|
f"Pareto entry {i} is dominated by entry {j}"
|
|
)
|
|
|
|
|
|
# ── End-to-end sanity ───────────────────────────────────────────────
|
|
|
|
|
|
def test_run_auto_explore_shape():
|
|
"""all_scores sorted asc by latency; pareto ⊆ feasible ⊆ all_scores."""
|
|
model = PRESETS["Llama 3.1 70B"].model
|
|
machine = MachineParams()
|
|
res = run_auto_explore(model, machine, s_kv=8192, mode="decode")
|
|
|
|
assert res.total_enumerated == len(res.all_scores)
|
|
assert res.total_feasible == sum(
|
|
1 for s in res.all_scores if s.fits_memory and s.placement_valid
|
|
)
|
|
assert len(res.pareto_scores) <= res.total_feasible
|
|
|
|
latencies = [s.total_latency_ns for s in res.all_scores]
|
|
assert latencies == sorted(latencies)
|
|
|
|
|
|
def test_pp_does_not_reduce_single_request_latency():
|
|
"""A single request traverses all layers regardless of PP.
|
|
Latency-optimal Pareto configs should NOT prefer PP>1 for decode."""
|
|
model = PRESETS["Llama 3.1 70B"].model
|
|
machine = MachineParams()
|
|
res = run_auto_explore(model, machine, s_kv=8192, mode="decode")
|
|
# Sort Pareto by latency; the fastest should not need PP>1 to win.
|
|
fastest = res.pareto_scores[0]
|
|
assert fastest.pp == 1, (
|
|
f"expected PP=1 for the fastest decode config; got PP={fastest.pp}"
|
|
)
|
|
|
|
|
|
# ── Parallelism sensitivity ─────────────────────────────────────────
|
|
|
|
|
|
def test_parallelism_sensitivity_has_all_five_knobs():
|
|
"""compute_parallelism_sensitivity returns one row per knob."""
|
|
from tests.analytical_visualization.auto_explore import (
|
|
compute_parallelism_sensitivity,
|
|
)
|
|
model = PRESETS["Llama 3.1 70B"].model
|
|
machine = MachineParams()
|
|
res = run_auto_explore(model, machine, s_kv=8192, mode="decode")
|
|
baseline = res.pareto_scores[0]
|
|
rows = compute_parallelism_sensitivity(
|
|
baseline, model, machine, s_kv=8192, mode="decode",
|
|
)
|
|
knobs = {r.knob for r in rows}
|
|
assert knobs == {"cp", "tp", "pp", "dp", "ep"}
|
|
|
|
|
|
def test_attention_only_latency_is_lower_than_full():
|
|
"""include_ffn=False must produce lower latency than include_ffn=True
|
|
for the same model+workload (FFN cost is dropped)."""
|
|
model = PRESETS["Llama 3.1 70B"].model
|
|
machine = MachineParams()
|
|
r_full = run_auto_explore(model, machine, s_kv=8192, mode="decode",
|
|
include_ffn=True)
|
|
r_attn = run_auto_explore(model, machine, s_kv=8192, mode="decode",
|
|
include_ffn=False)
|
|
best_full = min(r_full.pareto_scores, key=lambda s: s.total_latency_ns)
|
|
best_attn = min(r_attn.pareto_scores, key=lambda s: s.total_latency_ns)
|
|
assert best_attn.total_latency_ns < best_full.total_latency_ns, (
|
|
f"attn-only ({best_attn.latency_us:.2f} us) should be less than "
|
|
f"full ({best_full.latency_us:.2f} us)"
|
|
)
|
|
|
|
|
|
def test_attention_only_pareto_non_empty():
|
|
"""The attention-only sweep must still produce a non-empty Pareto set."""
|
|
model = PRESETS["Llama 3.1 70B"].model
|
|
machine = MachineParams()
|
|
res = run_auto_explore(model, machine, s_kv=8192, mode="decode",
|
|
include_attention=True, include_ffn=False)
|
|
assert res.total_feasible > 0
|
|
assert len(res.pareto_scores) > 0
|
|
|
|
|
|
def test_ffn_only_latency_less_than_full_and_different_from_attn():
|
|
"""FFN-only latency must be > 0, < full-model latency, and != attn-only."""
|
|
model = PRESETS["Llama 3.1 70B"].model
|
|
machine = MachineParams()
|
|
r_full = run_auto_explore(model, machine, s_kv=8192, mode="decode",
|
|
include_attention=True, include_ffn=True)
|
|
r_attn = run_auto_explore(model, machine, s_kv=8192, mode="decode",
|
|
include_attention=True, include_ffn=False)
|
|
r_ffn = run_auto_explore(model, machine, s_kv=8192, mode="decode",
|
|
include_attention=False, include_ffn=True)
|
|
b_full = min(r_full.pareto_scores, key=lambda s: s.total_latency_ns)
|
|
b_attn = min(r_attn.pareto_scores, key=lambda s: s.total_latency_ns)
|
|
b_ffn = min(r_ffn.pareto_scores, key=lambda s: s.total_latency_ns)
|
|
assert b_ffn.total_latency_ns > 0
|
|
assert b_ffn.total_latency_ns < b_full.total_latency_ns
|
|
assert abs(b_ffn.total_latency_ns - b_attn.total_latency_ns) > 1.0, (
|
|
"FFN-only and attention-only latencies should not be identical"
|
|
)
|
|
|
|
|
|
def test_parallelism_sensitivity_includes_baseline_value():
|
|
"""Each row's values contain the baseline_value."""
|
|
from tests.analytical_visualization.auto_explore import (
|
|
compute_parallelism_sensitivity,
|
|
)
|
|
model = PRESETS["Llama 3.1 70B"].model
|
|
machine = MachineParams()
|
|
res = run_auto_explore(model, machine, s_kv=8192, mode="decode")
|
|
baseline = res.pareto_scores[0]
|
|
rows = compute_parallelism_sensitivity(
|
|
baseline, model, machine, s_kv=8192, mode="decode",
|
|
)
|
|
for r in rows:
|
|
# Baseline value must be sweepable OR mapped to something in values.
|
|
assert r.baseline_value in r.values or r.baseline_value == 1, (
|
|
f"knob {r.knob}: baseline_value={r.baseline_value} not in {r.values}"
|
|
)
|