b7c4c680aa
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>
519 lines
19 KiB
Python
519 lines
19 KiB
Python
"""Chip-level roofline math: AI, B*, L*, per-token latency curves.
|
||
|
||
Surfaces the arithmetic-intensity story from LLM-serving practice:
|
||
|
||
- **AI** = C / W (peak FLOPs per byte of HBM bandwidth).
|
||
- **B\*** = C * b / (2 * W) * sparsity — the critical batch size at
|
||
which weight-fetch time equals compute time for one decode step.
|
||
Sparsity = N_total / N_active (MoE factor; 1 for dense).
|
||
- **L\*** = 2 * N_active / (AI * kv_bytes_per_token) — the balance
|
||
context length at which KV-read time equals compute time.
|
||
- **B_knee(S_kv)** = B* / (1 - S_kv/L*) — the batch size where the
|
||
cost curve bends (weight-fetch drops below the compute+KV floor).
|
||
Diverges at S_kv = L* and no knee exists past it.
|
||
|
||
Per-token decode-step latency, per PE (dense-approx, no comm):
|
||
|
||
t(B, S_kv) = N_active * b / (W * B) <-- weight fetch, 1/B
|
||
+ 2 * N_active / C <-- compute (peak), flat
|
||
+ S_kv * kv_bpt / W <-- KV read, flat
|
||
|
||
All numbers per PE / per one forward pass. **Peak roofline — no
|
||
utilization factor.** Comm cost and TP/CP sharding are intentionally
|
||
NOT in the roofline — this is the back-of-envelope chip-vs-model view
|
||
the transcript talks about, not the full latency model that
|
||
stage_latencies.py builds. With this convention weight_s == compute_s
|
||
exactly at B*.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass, replace
|
||
|
||
from .model_config import FullConfig, MachineParams, ModelConfig
|
||
|
||
|
||
# Per-token bytes of BF16 MAC arithmetic: one multiply + one add = 2 FLOPs.
|
||
_FLOPS_PER_PARAM_PER_TOKEN = 2
|
||
|
||
|
||
# ── Chip / model derived quantities ────────────────────────────────
|
||
|
||
|
||
def arithmetic_intensity(machine: MachineParams) -> float:
|
||
"""FLOPs per byte of HBM bandwidth. Peak roofline; utilization
|
||
is applied only in the compute-time formula, not here."""
|
||
return machine.peak_flops / machine.bw_hbm
|
||
|
||
|
||
def total_active_params(model: ModelConfig) -> int:
|
||
"""Full-model parameter count (attention + FFN, all layers).
|
||
|
||
Attention: 4 projections × hidden × H_q * d_head effective per layer.
|
||
(W_Q hidden×H_q*d_h, W_O H_q*d_h×hidden, W_K/W_V hidden×H_kv*d_h.)
|
||
FFN: 3 × hidden × ffn_dim per layer (gate, up, down).
|
||
"""
|
||
m = model
|
||
attn = (
|
||
m.hidden * m.h_q * m.d_head # W_Q
|
||
+ m.hidden * m.h_kv * m.d_head * 2 # W_K + W_V
|
||
+ m.h_q * m.d_head * m.hidden # W_O
|
||
)
|
||
ffn = 3 * m.hidden * m.ffn_dim
|
||
return (attn + ffn) * m.layers
|
||
|
||
|
||
def kv_bytes_per_token(model: ModelConfig) -> int:
|
||
"""Bytes of KV cache one new token adds across ALL layers, per one
|
||
sequence, un-sharded (K + V, H_kv heads * d_h * bytes)."""
|
||
m = model
|
||
return 2 * m.h_kv * m.d_head * m.bytes_per_elem * m.layers
|
||
|
||
|
||
def critical_batch(machine: MachineParams, model: ModelConfig,
|
||
sparsity: float = 1.0) -> float:
|
||
"""B* = C * b / (2 * W) * sparsity.
|
||
|
||
Sparsity = N_total / N_active (>= 1). Dense = 1. MoE 8-of-256 = 8.
|
||
"""
|
||
b = model.bytes_per_elem
|
||
ai = arithmetic_intensity(machine)
|
||
return ai * b / _FLOPS_PER_PARAM_PER_TOKEN * sparsity
|
||
|
||
|
||
def balance_context(machine: MachineParams, model: ModelConfig) -> float:
|
||
"""L* = 2 * N_active / (AI * kv_bpt).
|
||
|
||
Context length (in tokens) at which per-step KV read matches the
|
||
per-step compute cost. Beyond L*, the KV term is dominant and no
|
||
batch size gets you compute-bound.
|
||
"""
|
||
n_active = total_active_params(model)
|
||
ai = arithmetic_intensity(machine)
|
||
kv_bpt = kv_bytes_per_token(model)
|
||
return _FLOPS_PER_PARAM_PER_TOKEN * n_active / (ai * kv_bpt)
|
||
|
||
|
||
def knee_batch(machine: MachineParams, model: ModelConfig,
|
||
s_kv: int) -> float | None:
|
||
"""B_knee(S_kv) = B* / (1 - S_kv/L*).
|
||
|
||
Returns None when S_kv >= L* (no knee exists — the total-cost
|
||
curve never touches the compute floor).
|
||
"""
|
||
b_star = critical_batch(machine, model)
|
||
l_star = balance_context(machine, model)
|
||
r = s_kv / l_star
|
||
if r >= 1.0:
|
||
return None
|
||
return b_star / (1.0 - r)
|
||
|
||
|
||
# ── Per-token latency curves ───────────────────────────────────────
|
||
|
||
|
||
@dataclass
|
||
class RooflinePoint:
|
||
batch: int
|
||
weight_s: float # weight fetch time, 1/B
|
||
compute_s: float # compute time, flat
|
||
kv_s: float # KV read time, flat
|
||
total_s: float
|
||
|
||
|
||
def per_token_latency_curve(machine: MachineParams, model: ModelConfig,
|
||
batch_range: list[int],
|
||
s_kv: int) -> list[RooflinePoint]:
|
||
"""Per-token decode-step latency curve across a range of batch sizes.
|
||
|
||
Returns one point per batch. All times per PE, dense-approx,
|
||
utilization from machine.compute_util. Comm and TP/CP sharding are
|
||
excluded — this is the roofline model.
|
||
"""
|
||
n_active = total_active_params(model)
|
||
b = model.bytes_per_elem
|
||
weight_bytes = n_active * b
|
||
compute_flops = _FLOPS_PER_PARAM_PER_TOKEN * n_active
|
||
kv_read_bytes = s_kv * kv_bytes_per_token(model)
|
||
|
||
compute_s = compute_flops / machine.peak_flops
|
||
kv_s = kv_read_bytes / machine.bw_hbm
|
||
|
||
points: list[RooflinePoint] = []
|
||
for bs in batch_range:
|
||
weight_s = weight_bytes / machine.bw_hbm / max(1, bs)
|
||
points.append(RooflinePoint(
|
||
batch=bs,
|
||
weight_s=weight_s,
|
||
compute_s=compute_s,
|
||
kv_s=kv_s,
|
||
total_s=weight_s + compute_s + kv_s,
|
||
))
|
||
return points
|
||
|
||
|
||
def bound_regime(machine: MachineParams, model: ModelConfig,
|
||
batch: int, s_kv: int) -> str:
|
||
"""Which term dominates at the current (batch, S_kv) point.
|
||
|
||
Returns 'memory-bound' if weight_fetch is the largest term,
|
||
'kv-bound' if KV read is largest, 'compute-bound' if compute.
|
||
"""
|
||
pts = per_token_latency_curve(machine, model, [batch], s_kv)
|
||
p = pts[0]
|
||
parts = {"memory-bound": p.weight_s,
|
||
"kv-bound": p.kv_s,
|
||
"compute-bound": p.compute_s}
|
||
return max(parts, key=parts.get)
|
||
|
||
|
||
# ── Regime-dependent cost terms ────────────────────────────────────
|
||
|
||
|
||
def t_mem_short(machine: MachineParams, model: ModelConfig,
|
||
batch: int) -> float:
|
||
"""Per-token weight-fetch time (short-context regime term).
|
||
|
||
N_active · b / (W · B). Shrinks as B grows — this is what
|
||
batching amortizes.
|
||
"""
|
||
return (total_active_params(model) * model.bytes_per_elem
|
||
/ (machine.bw_hbm * max(1, batch)))
|
||
|
||
|
||
def t_mem_long(machine: MachineParams, model: ModelConfig,
|
||
s_kv: int) -> float:
|
||
"""Per-token KV-read time (long-context regime term).
|
||
|
||
S_kv · kv_bpt / W. Independent of B — each sequence reads its
|
||
own KV cache; batching doesn't help.
|
||
"""
|
||
return s_kv * kv_bytes_per_token(model) / machine.bw_hbm
|
||
|
||
|
||
def t_com(machine: MachineParams, model: ModelConfig) -> float:
|
||
"""Per-token compute time. Same in both regimes: 2·N/C, peak."""
|
||
return _FLOPS_PER_PARAM_PER_TOKEN * total_active_params(model) / machine.peak_flops
|
||
|
||
|
||
# ── "Good" batch / context recommendations ─────────────────────────
|
||
|
||
|
||
@dataclass
|
||
class BatchRecommendation:
|
||
target: float # Pope's rule: 2 × B*
|
||
b_star: float # B* itself
|
||
effective: float # what we recommend using
|
||
reason: str # short explanation
|
||
|
||
|
||
@dataclass
|
||
class ContextRecommendation:
|
||
l_star: float # balance context length
|
||
max_efficient: float # same as l_star (compute-friendly ceiling)
|
||
utilization_at: float # utilization at current s_kv
|
||
reason: str
|
||
|
||
|
||
def good_batch(machine: MachineParams, model: ModelConfig,
|
||
sparsity: float = 1.0) -> BatchRecommendation:
|
||
"""Recommended batch size: 2 × B* (Pope's rule of thumb).
|
||
|
||
Below B*: memory-bound, doubling B halves cost/token.
|
||
At 2×B*: 50% excess over compute floor — the sweet spot.
|
||
Beyond 3×B*: diminishing returns; latency keeps growing linearly.
|
||
"""
|
||
b_star = critical_batch(machine, model, sparsity)
|
||
target = 2 * b_star
|
||
return BatchRecommendation(
|
||
target=target, b_star=b_star, effective=target,
|
||
reason=(f"2·B* = 2 · {b_star:.0f} = {target:.0f}. "
|
||
"Below B*: memory-bound (doubling B halves cost/token). "
|
||
"Beyond 3·B*: diminishing returns."),
|
||
)
|
||
|
||
|
||
def good_context(machine: MachineParams, model: ModelConfig,
|
||
s_kv: int) -> ContextRecommendation:
|
||
"""Recommended max context: L* — the compute-friendly ceiling.
|
||
|
||
Below L*: KV read is cheap relative to compute → good utilization.
|
||
Above L*: KV bandwidth wall → utilization = 1/(1 + S_kv/L*).
|
||
"""
|
||
l_star = balance_context(machine, model)
|
||
util = utilization_at(s_kv, l_star)
|
||
return ContextRecommendation(
|
||
l_star=l_star, max_efficient=l_star, utilization_at=util,
|
||
reason=(f"L* = {l_star:,.0f} tokens. Below L*: compute-bound "
|
||
f"(good util). At {s_kv:,} tokens: peak utilization ≈ "
|
||
f"{util*100:.1f}% (1 / (1 + S_kv/L*))."),
|
||
)
|
||
|
||
|
||
def utilization_at(s_kv: int, l_star: float) -> float:
|
||
"""Peak compute utilization at context length s_kv, given L*.
|
||
|
||
util = compute / (compute + KV_read) = 1 / (1 + S_kv/L*).
|
||
At S_kv=0: 100%. At S_kv=L*: 50%. At 2·L*: 33.3%. At 5·L*: 16.7%.
|
||
"""
|
||
return 1.0 / (1.0 + s_kv / l_star)
|
||
|
||
|
||
# ── Per-step latency (undivided by B) ─────────────────────────────
|
||
|
||
|
||
@dataclass
|
||
class StepLatencyPoint:
|
||
batch: int
|
||
weight_s: float # N·b / W — flat in B (loaded once per step)
|
||
compute_s: float # 2·N·B / C — linear in B
|
||
kv_s: float # B · S_kv · kv_bpt / W — linear in B
|
||
total_s: float
|
||
|
||
|
||
def step_latency_curve(machine: MachineParams, model: ModelConfig,
|
||
batch_range: list[int],
|
||
s_kv: int) -> list[StepLatencyPoint]:
|
||
"""Total time of one decode step (one forward pass), across a
|
||
range of batch sizes. NOT divided by B — this is the SLO view.
|
||
|
||
step_weight = N·b / W (batch-invariant)
|
||
step_compute = 2·N·B / C (linear in B)
|
||
step_kv = B · S_kv · kv_bpt / W (linear in B)
|
||
|
||
Per-token cost = step_total / B — the two views trade off:
|
||
bigger B lowers cost/token but raises step latency.
|
||
"""
|
||
n_active = total_active_params(model)
|
||
b = model.bytes_per_elem
|
||
weight_bytes = n_active * b
|
||
kv_bpt = kv_bytes_per_token(model)
|
||
|
||
step_weight = weight_bytes / machine.bw_hbm
|
||
|
||
points: list[StepLatencyPoint] = []
|
||
for bs in batch_range:
|
||
B = max(1, bs)
|
||
step_compute = _FLOPS_PER_PARAM_PER_TOKEN * n_active * B / machine.peak_flops
|
||
step_kv = B * s_kv * kv_bpt / machine.bw_hbm
|
||
total = step_weight + step_compute + step_kv
|
||
points.append(StepLatencyPoint(
|
||
batch=bs,
|
||
weight_s=step_weight,
|
||
compute_s=step_compute,
|
||
kv_s=step_kv,
|
||
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)
|
||
|
||
|
||
# ── GPU count sizing (three-axis) ─────────────────────────────────
|
||
|
||
|
||
@dataclass
|
||
class GpuSizingResult:
|
||
# Inputs echoed back for display / debugging
|
||
n_users: int
|
||
avg_ctx_tokens: int
|
||
tpot_slo_s: float
|
||
# Per-replica values
|
||
pes_axis_a_capacity: int # PEs to hold weights alone (bare floor)
|
||
pes_axis_b_kv: int # PEs to hold weights + KV of this replica's users
|
||
pes_per_replica: int # max(A, B)
|
||
# Throughput axis
|
||
b_at_slo: int # largest per-replica B satisfying SLO
|
||
users_per_replica: int # min(n_users, b_at_slo), at least 1
|
||
n_replicas: int # ceil(n_users / users_per_replica)
|
||
# Grand total
|
||
total_pes: int
|
||
binding_axis: str # 'capacity' | 'kv' | 'throughput'
|
||
|
||
|
||
def max_batch_within_slo(machine: MachineParams, model: ModelConfig,
|
||
s_kv: int, tpot_slo_s: float) -> int:
|
||
"""Largest per-replica B such that decode step latency ≤ SLO.
|
||
|
||
step_latency(B) = N·b/W + 2·N·B/C + B·S_kv·kv_bpt/W
|
||
= weight_fetch + B · (compute + kv_read)
|
||
|
||
Returns 0 if even B=1 exceeds SLO (weight fetch alone too large,
|
||
or per-sequence term already blows the budget).
|
||
"""
|
||
n = total_active_params(model)
|
||
b_elem = model.bytes_per_elem
|
||
weight_time = n * b_elem / machine.bw_hbm
|
||
per_seq_time = (_FLOPS_PER_PARAM_PER_TOKEN * n / machine.peak_flops
|
||
+ s_kv * kv_bytes_per_token(model) / machine.bw_hbm)
|
||
if weight_time + per_seq_time > tpot_slo_s:
|
||
return 0
|
||
remaining = tpot_slo_s - weight_time
|
||
b_max = int(remaining / per_seq_time)
|
||
return max(1, b_max)
|
||
|
||
|
||
def size_deployment(machine: MachineParams, model: ModelConfig,
|
||
n_users: int, avg_ctx: int,
|
||
tpot_slo_s: float) -> GpuSizingResult:
|
||
"""Three-axis GPU count for a target workload.
|
||
|
||
Axis A — capacity floor: PEs to hold one replica's weights.
|
||
Axis B — KV headroom: PEs to hold weights + all KV of the
|
||
users assigned to this replica.
|
||
Axis C — throughput SLO: replicas needed so per-replica B ≤ b_at_slo.
|
||
|
||
Result: pes_per_replica = max(A, B); total = pes_per_replica × replicas.
|
||
Binding axis is whichever grew the count the most.
|
||
"""
|
||
n = total_active_params(model)
|
||
b_elem = model.bytes_per_elem
|
||
weight_bytes = n * b_elem
|
||
hbm_pe = int(machine.pe_hbm_gb * 1e9)
|
||
kv_bpt = kv_bytes_per_token(model)
|
||
|
||
# Axis C: throughput
|
||
b_at_slo = max_batch_within_slo(machine, model, avg_ctx, tpot_slo_s)
|
||
users_per_replica = max(1, min(n_users, b_at_slo)) if b_at_slo > 0 else 1
|
||
n_replicas = (n_users + users_per_replica - 1) // users_per_replica
|
||
|
||
# Axis A: bare weights
|
||
pes_a = (weight_bytes + hbm_pe - 1) // hbm_pe
|
||
|
||
# Axis B: weights + KV load for this replica's users
|
||
kv_per_replica = users_per_replica * avg_ctx * kv_bpt
|
||
pes_b = (weight_bytes + kv_per_replica + hbm_pe - 1) // hbm_pe
|
||
|
||
pes_per_replica = max(pes_a, pes_b)
|
||
total = pes_per_replica * n_replicas
|
||
|
||
if n_replicas > 1:
|
||
binding = "throughput"
|
||
elif pes_b > pes_a:
|
||
binding = "kv"
|
||
else:
|
||
binding = "capacity"
|
||
|
||
return GpuSizingResult(
|
||
n_users=n_users,
|
||
avg_ctx_tokens=avg_ctx,
|
||
tpot_slo_s=tpot_slo_s,
|
||
pes_axis_a_capacity=int(pes_a),
|
||
pes_axis_b_kv=int(pes_b),
|
||
pes_per_replica=int(pes_per_replica),
|
||
b_at_slo=b_at_slo,
|
||
users_per_replica=int(users_per_replica),
|
||
n_replicas=int(n_replicas),
|
||
total_pes=int(total),
|
||
binding_axis=binding,
|
||
)
|
||
|
||
|
||
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
|