analytical-viz: roofline tab — 'How hyperscalers pick GPU count' section
New section at the bottom of the Chip Roofline tab implementing the three-axis GPU count decision: N_GPUs = max(capacity_floor, KV_headroom, throughput_SLO) × N_replicas Interactive inputs: concurrent users, avg context/user, TPOT SLO. Four KPI cards report each axis + the recommended total, with a 'binds:' delta chip showing which axis is the bottleneck. - Axis A (capacity): PEs to hold weights alone - Axis B (KV): PEs to hold weights + all KV of one replica's users - Axis C (throughput): replicas needed at B_at_SLO users each - Total: max(A,B) × N_replicas Contextual caption explains what to do about the binding axis. If SLO is infeasible (even B=1 exceeds SLO), an error block explains the options (loosen SLO, shorten context, or scale up FLOPs/BW knobs). Plus three collapsed expanders explaining the hybrid deployment pattern hyperscalers use: - Layer 1: one elastic pool + PagedAttention + continuous batching - Layer 2: length-tier routing (standard vs long-context) - Layer 3: disaggregated prefill/decode (DistServe, Splitwise) Pure additions in chip_roofline.py: - max_batch_within_slo(machine, model, s_kv, slo_s) — analytical inverse of step_latency to find the largest per-replica B under SLO - size_deployment(machine, model, n_users, avg_ctx, slo_s) — returns GpuSizingResult with all three axes + binding info 6 new tests cover: capacity scales with model size, KV grows with users/context, binding axis flips with workload shape, tight SLO shrinks max batch, weight-time-exceeds-SLO returns 0, total == per-replica × replicas. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -15,9 +15,11 @@ from tests.analytical_visualization.chip_roofline import (
|
||||
good_context,
|
||||
knee_batch,
|
||||
kv_bytes_per_token,
|
||||
max_batch_within_slo,
|
||||
memory_budget_curve_vs_batch,
|
||||
memory_budget_curve_vs_skv,
|
||||
per_token_latency_curve,
|
||||
size_deployment,
|
||||
step_latency_curve,
|
||||
t_com,
|
||||
t_mem_long,
|
||||
@@ -430,6 +432,79 @@ def test_ai_sensitivity_bad_axis_raises():
|
||||
[1.0], axis="bogus")
|
||||
|
||||
|
||||
# ── GPU sizing (three-axis) ────────────────────────────────────────
|
||||
|
||||
|
||||
def test_capacity_floor_scales_with_model_size():
|
||||
"""A model with 2× params needs ~2× PEs for weights alone (axis A)."""
|
||||
m = MachineParams()
|
||||
small = PRESETS["Llama 3 8B"].model
|
||||
big = PRESETS["Llama 3 70B"].model
|
||||
r_small = size_deployment(m, small, n_users=1, avg_ctx=1024,
|
||||
tpot_slo_s=1.0)
|
||||
r_big = size_deployment(m, big, n_users=1, avg_ctx=1024,
|
||||
tpot_slo_s=1.0)
|
||||
ratio = r_big.pes_axis_a_capacity / r_small.pes_axis_a_capacity
|
||||
# 70B / 8B ≈ 8.75× params — allow generous tolerance
|
||||
assert 5 < ratio < 12, (ratio, r_small, r_big)
|
||||
|
||||
|
||||
def test_kv_headroom_scales_with_users_and_context():
|
||||
"""Doubling n_users OR avg_ctx grows KV load roughly linearly."""
|
||||
m = MachineParams()
|
||||
model = PRESETS["Llama 3 8B"].model
|
||||
base = size_deployment(m, model, n_users=64, avg_ctx=4096,
|
||||
tpot_slo_s=1.0)
|
||||
twice_users = size_deployment(m, model, n_users=128, avg_ctx=4096,
|
||||
tpot_slo_s=1.0)
|
||||
twice_ctx = size_deployment(m, model, n_users=64, avg_ctx=8192,
|
||||
tpot_slo_s=1.0)
|
||||
# KV bytes doubled either way → axis-B PE count must strictly grow.
|
||||
assert twice_users.pes_axis_b_kv > base.pes_axis_b_kv
|
||||
assert twice_ctx.pes_axis_b_kv > base.pes_axis_b_kv
|
||||
|
||||
|
||||
def test_binding_axis_flips_with_workload():
|
||||
"""Huge N_users + short ctx → throughput binds.
|
||||
Huge ctx + few users → kv (or capacity) binds."""
|
||||
m = MachineParams()
|
||||
model = PRESETS["Llama 3 8B"].model
|
||||
big_users = size_deployment(m, model, n_users=10_000, avg_ctx=1024,
|
||||
tpot_slo_s=0.05)
|
||||
long_ctx = size_deployment(m, model, n_users=1, avg_ctx=500_000,
|
||||
tpot_slo_s=1.0)
|
||||
assert big_users.binding_axis == "throughput", big_users
|
||||
assert long_ctx.binding_axis in ("kv", "capacity"), long_ctx
|
||||
|
||||
|
||||
def test_max_batch_shrinks_with_shorter_slo():
|
||||
"""Tighter SLO → smaller B fits."""
|
||||
m = MachineParams()
|
||||
model = PRESETS["Llama 3 8B"].model
|
||||
loose = max_batch_within_slo(m, model, s_kv=1024, tpot_slo_s=1.0)
|
||||
tight = max_batch_within_slo(m, model, s_kv=1024, tpot_slo_s=0.05)
|
||||
assert tight < loose, (tight, loose)
|
||||
|
||||
|
||||
def test_max_batch_returns_zero_when_weight_time_exceeds_slo():
|
||||
"""If weight-fetch alone > SLO, no batch fits."""
|
||||
m = MachineParams()
|
||||
model = PRESETS["Llama 3 70B"].model
|
||||
# 70B in BF16 = 140 GB. Reading at 256 GB/s takes ~547 ms.
|
||||
# SLO of 100 ms cannot be met.
|
||||
assert max_batch_within_slo(m, model, s_kv=100,
|
||||
tpot_slo_s=0.1) == 0
|
||||
|
||||
|
||||
def test_total_pes_equals_per_replica_times_replicas():
|
||||
"""Sanity: total_pes = pes_per_replica × n_replicas."""
|
||||
m = MachineParams()
|
||||
model = PRESETS["Llama 3 8B"].model
|
||||
r = size_deployment(m, model, n_users=500, avg_ctx=4096,
|
||||
tpot_slo_s=0.1)
|
||||
assert r.total_pes == r.pes_per_replica * r.n_replicas
|
||||
|
||||
|
||||
# ── App wiring: tab exists on the Streamlit app ────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user