Files
Vijay Janapa Reddi 5b17578797 docs(mlsysim): P10a — propagate core→engine + infra→infrastructure rename to docs/tutorial/paper
Repoint mlsysim.core.<engine-mod> -> mlsysim.engine.<mod>, the
from-mlsysim.core-import-calibration form, and mlsysim.infra -> mlsysim.infrastructure
across the deferred consumers: docs prose + tutorials, tutorial slides/cheatsheet/
exercises, paper.tex, README, cli/DESIGN.md, and the mlsysim_constants audit README.
Also fix two docstrings inside the moved engine modules (calibration.py, pipeline.py)
that still named the old core paths. Update the quartodoc config (sections list) to
the new module paths and add the engine package.

NOTE: the generated quartodoc API stubs under mlsysim/docs/api/ (core.*.qmd,
infra*.qmd) still carry the old paths — they regenerate from the updated config via
`quartodoc build` (toolchain not installed in this worktree), so they are left
untouched here rather than hand-edited. Run `quartodoc build` to refresh them.

482 passed; import clean.
2026-05-29 18:35:38 -04:00

697 lines
24 KiB
Markdown

# MLSys·im Tutorial: Hands-On Exercises
Eight exercises aligned with the eight tutorial parts.
Each exercise is self-contained and takes 5-10 minutes.
---
## Exercise 1 — The Roofline Transition (Part 1: Single-Node Performance)
**Learning Objective:** Identify the batch size where a CNN workload transitions
from memory-bound to compute-bound on a datacenter GPU.
### Setup
```python
import mlsysim
from mlsysim import Engine, Hardware, Models
model = Models.Vision.ResNet50
hw = Hardware.Cloud.A100
```
### Task
Sweep `batch_size` from 1 to 512 (powers of 2). For each, call
`Engine.solve()` and record the `bottleneck` field along with the
compute and memory latency components. Find the smallest batch size
where the compute term overtakes the memory term.
```python
for bs in [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]:
p = Engine.solve(model, hw, batch_size=bs, precision="fp16", efficiency=0.5)
print(f"bs={bs:>4d} bottleneck={p.bottleneck:<10s} "
f"T_compute={p.latency_compute.to('ms'):~P.3f} "
f"T_memory={p.latency_memory.to('ms'):~P.3f} "
f"throughput={p.throughput:~P.1f}")
```
### Question
At what batch size does ResNet-50's compute time overtake its memory time
on the A100? Compare `latency_compute` vs. `latency_memory` to find the
exact crossover point.
### Hint
Watch for when `latency_compute > latency_memory`. The crossover is the
Roofline ridge point. Note that the reported `bottleneck` field depends
on the overall arithmetic intensity calculation, so inspect both latency
components directly.
<details>
<summary><strong>Expected Answer</strong></summary>
ResNet-50 on the A100 at FP16 with efficiency=0.5 is already compute-bound
at very small batch sizes because of its high FLOP count (~8 GFLOPs per
image) relative to its small weight footprint (~51 MB at FP16). The compute
term scales linearly with batch size while the memory term grows more slowly
(weights loaded once, only activation traffic scales). For CNN workloads
like ResNet-50, the transition may already be at batch_size=1, unlike
Transformer decode which is memory-bound at batch_size=1. Compare with
`Models.Language.Llama3_8B` to see a memory-bound regime.
</details>
### Discussion
Why does this transition point matter for production deployment? Compare
ResNet-50 (compute-bound even at batch_size=1) with Llama-3-8B (memory-bound
at batch_size=1). What hardware characteristic should you optimize for
in each case -- peak FLOPS or memory bandwidth?
---
## Exercise 2 — LLM Serving Capacity (Part 2: Serving and Inference)
**Learning Objective:** Determine how many concurrent LLM requests fit in
GPU memory, accounting for both model weights and KV-cache.
### Setup
```python
from mlsysim import ServingModel, Hardware, Models
serving = ServingModel()
model = Models.Language.Llama3_8B
hw = Hardware.Cloud.H100
```
### Task
Sweep `batch_size` from 1 to 128 at `seq_len=4096`. Find the maximum
batch size where the serving result is still `feasible`.
```python
for bs in [1, 2, 4, 8, 16, 32, 64, 128]:
r = serving.solve(model, hw, seq_len=4096, batch_size=bs, precision="fp16")
print(f"bs={bs:>4d} feasible={r.feasible} "
f"mem={r.total_memory_required:~P.1f} "
f"kv_cache={r.kv_cache_size:~P.1f} "
f"TTFT={r.ttft:~P.1f} ITL={r.itl:~P.2f}")
```
### Question
How many concurrent Llama-3-8B requests can a single H100 (80 GB) serve
at 4K context in FP16? What is the dominant memory consumer at that
maximum batch size -- weights or KV-cache?
### Hint
H100 has 80 GB HBM3. Llama-3-8B at FP16 is ~16 GB of weights.
Each request's KV-cache at 4096 tokens depends on the model's
layer count, head count, and head dimension.
<details>
<summary><strong>Expected Answer</strong></summary>
The maximum concurrent batch size is approximately **128 requests** on
the H100 (80 GiB = ~85.9 GB). At FP16, Llama-3-8B weights consume ~16 GB.
Each request's KV-cache at 4096 tokens is ~0.5 GB, so after reserving
~16 GB for weights, the remaining ~70 GB fits roughly 128 KV-cache slots.
At maximum capacity, **KV-cache dominates** memory usage (~69 GB KV vs.
~16 GB weights). At bs=160, total memory exceeds capacity and becomes
infeasible.
</details>
### Discussion
What happens if you switch from FP16 to INT8 quantization? The weights
halve, but the KV-cache also shrinks. Which effect matters more for
concurrent serving capacity?
---
## Exercise 3 — Quantization Trade-offs (Part 3: Compression and Efficiency)
**Learning Objective:** Quantify the memory savings and inference speedup
from INT4 quantization, and understand the accuracy trade-off.
### Setup
```python
from mlsysim import Engine, Hardware, Models
from mlsysim.engine.solver import CompressionModel
compress = CompressionModel()
model = Models.Language.Llama3_8B
hw = Hardware.Cloud.H100
```
### Task
Compare the FP16 baseline with INT4 quantization. Run the compression
model and then run `Engine.solve()` at both precisions.
```python
# Compression analysis
result = compress.solve(model, hw, method="quantization", target_bitwidth=4)
print(f"Compression ratio: {result.compression_ratio:.1f}x")
print(f"Original size: {result.original_size_gb:~P.2f}")
print(f"Compressed size: {result.compressed_size_gb:~P.2f}")
print(f"Memory savings: {result.memory_savings_pct:.1f}%")
print(f"Inference speedup: {result.inference_speedup:.2f}x")
print(f"Accuracy delta: {result.estimated_accuracy_delta:.2f}%")
# Roofline comparison
fp16 = Engine.solve(model, hw, batch_size=1, precision="fp16", efficiency=0.5)
int4 = Engine.solve(model, hw, batch_size=1, precision="int4", efficiency=0.5)
print(f"\nFP16 latency: {fp16.latency:~P.2f} bottleneck: {fp16.bottleneck}")
print(f"INT4 latency: {int4.latency:~P.2f} bottleneck: {int4.bottleneck}")
```
### Question
What is the memory savings from FP32 baseline to INT4 for Llama-3-8B?
What is the estimated accuracy degradation? Is the speedup closer to
8x or 4x, and why?
### Hint
The `CompressionModel` measures compression ratio from the **FP32 baseline**
(32-bit), so INT4 gives an 8x ratio on paper (32/4). But inference speedup
depends on whether the workload is compute-bound or memory-bound. At
batch_size=1, LLM inference is memory-bound, so the speedup tracks with
the reduction in bytes moved, not FLOPS saved.
<details>
<summary><strong>Expected Answer</strong></summary>
- **Memory savings:** ~87.5% (8x compression from FP32 to INT4 baseline),
reducing the model from ~32 GB (FP32) to ~4 GB (INT4).
- **Accuracy delta:** Approximately 2-5% degradation (conservative estimate
from the Gholami et al. survey).
- **Inference speedup:** At batch_size=1, the workload is memory-bound, so
the speedup is roughly **proportional to the bytes reduction**. Compared to
FP16 inference (the practical baseline), the speedup from INT4 is ~4x in
memory traffic. The exact speedup depends on whether the hardware has
native INT4 execution units (B200 does, H100 does not).
</details>
### Discussion
When would you choose INT8 over INT4? Consider a deployment where you
need <1% accuracy loss but also need to fit the model on a single GPU.
What is the optimal compression point?
---
## Exercise 4 — Parallelism Strategy Search (Part 4: Distributed Training)
**Learning Objective:** Find the optimal 3D parallelism configuration
(TP x PP x DP) for training a 70B model on a 64-GPU cluster.
### Setup
```python
from mlsysim import Models, Systems
from mlsysim.engine.solver import ParallelismOptimizer
optimizer = ParallelismOptimizer()
model = Models.Language.Llama3_70B
fleet = Systems.Clusters.Research_256 # 256 H100s
```
### Task
Since we want 64 GPUs, build a custom fleet. Then run the optimizer.
```python
from mlsysim.systems.types import Fleet
from mlsysim.systems.registry import Nodes, Fabrics
fleet_64 = Fleet(
name="64-GPU H100 Cluster",
node=Nodes.DGX_H100,
count=8, # 8 nodes x 8 GPUs = 64
fabric=Fabrics.InfiniBand_NDR
)
result = optimizer.solve(
model, fleet_64,
batch_size=512,
precision="fp16",
efficiency=0.4,
overlap_comm=True
)
print(f"Best config: {result.best_config}")
print(f"Best MFU: {result.best_mfu:.3f}")
print(f"Best step time: {result.best_step_time:~P.1f}")
print(f"Configs explored: {result.total_searched}")
# Print top candidates
for c in result.top_candidates[:10]:
cfg = c['config']
print(f" TP={cfg['tp']:>2d} PP={cfg['pp']:>2d} DP={cfg['dp']:>2d} "
f"MFU={c['mfu']:.3f}")
```
### Question
What is the optimal TP x PP x DP split for Llama-3-70B on 64 H100s?
Why does the optimizer prefer TP=8 (one full node) for tensor parallelism?
### Hint
TP communication happens over NVLink (900 GB/s within a DGX H100 node).
PP communication is point-to-point (small volume). DP communication is
AllReduce over InfiniBand (across nodes). The optimizer balances memory
fit (TP+PP must shard the 70B model enough to fit) against communication
overhead.
<details>
<summary><strong>Expected Answer</strong></summary>
The optimizer typically finds **TP=8, PP=2, DP=4** or a nearby configuration.
- **TP=8** keeps tensor parallelism within a single DGX node (8 GPUs connected
by NVLink at 900 GB/s), minimizing communication latency for the 2 AllReduce
operations per layer.
- **PP=2** splits the 80 layers across 2 pipeline stages, reducing per-GPU
memory to fit in 80 GB HBM.
- **DP=4** provides data parallelism across 4 groups, allowing a reasonable
local batch size of 128 per DP rank.
The MFU is typically 0.30-0.45, reflecting the pipeline bubble and
communication overheads.
</details>
### Discussion
What happens if you double the cluster to 128 GPUs? Does MFU go up or
down? Which parallelism dimension should absorb the extra GPUs?
---
## Exercise 5 — Carbon Geography (Part 5: Sustainability)
**Learning Objective:** Quantify how datacenter location affects the
carbon footprint of a long training run.
### Setup
```python
from mlsysim import SustainabilityModel, Infrastructure, Systems
sustain = SustainabilityModel()
fleet = Systems.Clusters.Research_256 # 256 H100s
```
### Task
Compare a 30-day training run in Virginia (US Average grid) versus
Quebec (hydroelectric).
```python
# Virginia (US Average)
r_va = sustain.solve(fleet, duration_days=30, datacenter=Infrastructure.Grids.US_Avg, mfu=0.4)
print(f"=== Virginia (US Average Grid) ===")
print(f"Energy: {r_va.total_energy_kwh:,.0f} kWh")
print(f"Carbon: {r_va.carbon_footprint_kg:,.0f} kg CO2")
print(f"Water: {r_va.water_usage_liters:,.0f} liters")
# Quebec (Hydro)
r_qc = sustain.solve(fleet, duration_days=30, datacenter=Infrastructure.Grids.Quebec, mfu=0.4)
print(f"\n=== Quebec (Hydroelectric) ===")
print(f"Energy: {r_qc.total_energy_kwh:,.0f} kWh")
print(f"Carbon: {r_qc.carbon_footprint_kg:,.0f} kg CO2")
print(f"Water: {r_qc.water_usage_liters:,.0f} liters")
print(f"\n=== Comparison ===")
print(f"Carbon reduction: {(1 - r_qc.carbon_footprint_kg / r_va.carbon_footprint_kg) * 100:.1f}%")
print(f"Water reduction: {(1 - r_qc.water_usage_liters / r_va.water_usage_liters) * 100:.1f}%")
```
### Question
How much carbon (in kg CO2) does moving from Virginia to Quebec save for
a 30-day training run on 256 H100s? What fraction of the total energy
is consumed by cooling and power delivery (not compute)?
### Hint
Quebec's carbon intensity is ~1.2 gCO2/kWh (hydro) vs. US average
~390 gCO2/kWh (mixed grid). The PUE overhead is the ratio of total
facility energy to IT energy -- a PUE of 1.1 means 10% overhead.
<details>
<summary><strong>Expected Answer</strong></summary>
- **Energy:** Both regions consume similar total energy (~180,000-220,000 kWh
depending on PUE), since the GPUs draw the same power regardless of location.
- **Carbon:** Virginia produces roughly **80,000-90,000 kg CO2** while Quebec
produces approximately **200-300 kg CO2** -- a **~99% reduction**.
- **PUE overhead:** Quebec's liquid-cooled facility (PUE ~1.05) wastes ~5% on
infrastructure vs. US average air-cooled (PUE ~1.1-1.2) wasting 10-20%.
The carbon savings come entirely from the grid's energy source, not from
using less power. This is why datacenter location is the single highest-leverage
sustainability decision.
</details>
### Discussion
If you also factor in cost (electricity price per kWh), does Quebec remain
the optimal choice? Use `EconomicsModel` to find out. What about the
network latency penalty if your team is in California?
---
## Exercise 6 — Pareto-Optimal Serving (Part 6: Economics and Fleet Design)
**Learning Objective:** Given a fixed budget, find the serving configuration
that maximizes throughput while meeting latency SLAs.
### Setup
```python
from mlsysim import (
EconomicsModel, ServingModel, Hardware, Models,
Systems, Infrastructure
)
from mlsysim.systems.types import Fleet, Node, NetworkFabric
from mlsysim.systems.registry import Nodes, Fabrics
serving = ServingModel()
econ = EconomicsModel()
model = Models.Language.Llama3_8B
```
### Task
Compare three hardware options for serving Llama-3-8B at 4K context,
each scaled to fit within a $1M annual budget.
```python
configs = [
("A100 cluster", Hardware.Cloud.A100, Nodes.DGX_A100, 20), # ~20 nodes
("H100 cluster", Hardware.Cloud.H100, Nodes.DGX_H100, 8), # ~8 nodes
("B200 cluster", Hardware.Cloud.B200, Nodes.DGX_B200, 4), # ~4 nodes
]
for name, hw, node_template, n_nodes in configs:
fleet = Fleet(
name=name, node=node_template, count=n_nodes,
fabric=Fabrics.InfiniBand_NDR, region=Infrastructure.Grids.US_Avg
)
# Economics: 365-day TCO
tco = econ.solve(fleet, duration_days=365, mfu=0.3)
# Serving: per-GPU capacity
r = serving.solve(model, hw, seq_len=4096, batch_size=32, precision="fp16")
total_gpus = fleet.total_accelerators
print(f"\n=== {name} ({total_gpus} GPUs) ===")
print(f" TCO: ${tco.tco_usd:,.0f}")
print(f" TTFT: {r.ttft:~P.1f}")
print(f" ITL: {r.itl:~P.2f}")
print(f" Feasible: {r.feasible}")
print(f" Per-GPU mem: {r.total_memory_required:~P.1f}")
```
### Question
With a $1M annual budget, which hardware generation provides the best
cost-per-request for Llama-3-8B serving? Is it always the newest GPU?
### Hint
Newer GPUs have higher unit cost but also higher bandwidth and FLOPS.
The Pareto-optimal choice depends on whether serving is memory-bound
(bandwidth matters) or compute-bound (FLOPS matter), and how many
GPUs you can afford.
<details>
<summary><strong>Expected Answer</strong></summary>
The answer depends on the specific unit costs in the registry, but the
general finding is:
- **A100:** Cheapest per-unit, so you get the most GPUs, but each has lower
bandwidth (2 TB/s vs. 3.35 TB/s for H100). Good for throughput-oriented
workloads where you can batch aggressively.
- **H100:** Best balance of cost and performance for LLM serving. Higher
bandwidth directly reduces ITL (inter-token latency) in the memory-bound
decode phase.
- **B200:** Highest per-unit cost but offers FP8/INT4 support and highest
bandwidth (~8 TB/s). Most cost-effective only if you can use lower precision.
The Pareto frontier typically shows **H100 as the sweet spot** for FP16
serving, with B200 winning if INT4/FP8 quantization is acceptable.
</details>
### Discussion
How does the analysis change if you add a latency SLA (e.g., ITL < 20ms)?
Does the Pareto-optimal choice shift when you constrain latency instead
of just minimizing cost?
---
## Exercise 7 — TinyML SLA Feasibility (Part 7: Edge and TinyML)
**Learning Objective:** Determine whether a keyword-spotting CNN can meet
a real-time SLA on a microcontroller.
### Setup
```python
from mlsysim import Engine, Models, ureg
from mlsysim.hardware.types import HardwareNode, ComputeCore, MemoryHierarchy
model = Models.Tiny.DS_CNN
# Construct the nRF52840 (Cortex-M4F @ 64 MHz) -- MLPerf Tiny reference platform
hw = HardwareNode(
name="Nordic nRF52840 (Cortex-M4F)",
release_year=2018,
compute=ComputeCore(
peak_flops=0.000064 * ureg.TFLOPs / ureg.s,
precision_flops={"int8": 0.000128 * ureg.TOPS},
),
memory=MemoryHierarchy(
capacity=1 * ureg.MB,
bandwidth=0.064 * ureg.GB / ureg.s,
sram_capacity=256 * ureg.KiB,
sram_bandwidth=0.256 * ureg.GB / ureg.s,
flash_capacity=1 * ureg.MB,
flash_bandwidth=0.064 * ureg.GB / ureg.s,
),
tdp=0.015 * ureg.W,
dispatch_tax=0.5 * ureg.ms,
)
```
### Task
Check if DS-CNN keyword spotting meets a 30ms latency SLA on the
nRF52840. Then explore what happens at different precisions and
efficiency levels.
```python
# Baseline: INT8 (native on Cortex-M4F)
p_int8 = Engine.solve(model, hw, batch_size=1, precision="int8", efficiency=0.3)
print(f"=== DS-CNN on nRF52840 (INT8) ===")
print(f"Latency: {p_int8.latency:~P.2f}")
print(f"Bottleneck: {p_int8.bottleneck}")
print(f"Memory: {p_int8.memory_footprint:~P.2f}")
print(f"Feasible: {p_int8.feasible}")
print(f"Energy: {p_int8.energy:~P.4f}")
print(f"Meets 30ms: {p_int8.latency.to('ms').magnitude < 30}")
# Compare: FP16 (no hardware support -- uses FP32 path)
p_fp16 = Engine.solve(model, hw, batch_size=1, precision="fp16", efficiency=0.1)
print(f"\n=== DS-CNN on nRF52840 (FP16 emulated) ===")
print(f"Latency: {p_fp16.latency:~P.2f}")
print(f"Meets 30ms: {p_fp16.latency.to('ms').magnitude < 30}")
# Sweep efficiency to find the minimum required
for eff in [0.1, 0.2, 0.3, 0.4, 0.5]:
p = Engine.solve(model, hw, batch_size=1, precision="int8", efficiency=eff)
print(f"eff={eff:.1f} latency={p.latency:~P.2f} meets_30ms={p.latency.to('ms').magnitude < 30}")
```
### Question
Can DS-CNN meet a 30ms SLA on the nRF52840? If not, what is the binding
constraint, and what hardware or algorithmic change would close the gap?
### Hint
The nRF52840 has ~128 MOPS at INT8 and ~64 MFLOPS at FP32. DS-CNN
has ~6M FLOPs. Do the back-of-envelope math: 6M / (128M * efficiency).
Is this compute-bound or memory-bound?
<details>
<summary><strong>Expected Answer</strong></summary>
- **INT8 at efficiency=0.3:** DS-CNN inference takes approximately
**500ms** on the nRF52840 -- far exceeding the 30ms SLA. The workload is
**compute-bound**: 6M FLOPs / (128 MOPS * 0.3) ~ 156ms of raw compute,
plus framework overhead (dispatch tax, layer tax) pushes it past 500ms.
- **FP16 emulated:** Even slower (~1500ms+) because the Cortex-M4F has no
native FP16 support and falls back to the FP32 path at 64 MFLOPS.
- **Energy:** At 15 mW TDP over ~500ms, a single inference consumes roughly
**8 millijoules** -- acceptable for battery operation, but the latency
is the problem, not energy.
- **The gap:** To meet 30ms, you would need either a faster MCU (e.g.,
Cortex-M7 at 480 MHz with DSP extensions) or a smaller model (fewer FLOPs).
Even at efficiency=1.0, the raw compute time is 6M/128M = 47ms -- still
above 30ms. The nRF52840 simply cannot meet this SLA for DS-CNN.
</details>
### Discussion
This result surprises many students: a "tiny" 26K-parameter model still
cannot meet a 30ms SLA on a microcontroller. What does this teach about
the relationship between model size and latency? Try the ESP32-S3
(`Hardware.Tiny.ESP32_S3`) which has ~20x the compute throughput.
Does it meet 30ms? What if you needed to run MobileNetV2 -- try
`Engine.solve(Models.Vision.MobileNetV2, hw, ...)` and observe
the `feasible` field for the memory wall.
---
## Exercise 8 — Capstone: Fleet Design Under Constraints (Parts 1-7)
**Learning Objective:** Design a complete serving fleet for Llama-3-70B
at 1000 QPS, within a $5M annual budget, deployed across two regions.
### Setup
```python
from mlsysim import (
ServingModel, EconomicsModel, SustainabilityModel,
Hardware, Models, Infrastructure
)
from mlsysim.engine.solver import CompressionModel
from mlsysim.systems.types import Fleet
from mlsysim.systems.registry import Nodes, Fabrics
model = Models.Language.Llama3_70B
serving = ServingModel()
econ = EconomicsModel()
sustain = SustainabilityModel()
compress = CompressionModel()
```
### Task
Design a fleet that meets ALL constraints simultaneously:
- **Throughput:** 1000 QPS (queries per second) total across two regions
- **Latency:** ITL < 50ms per token
- **Budget:** < $5M annual TCO
- **Carbon:** < 500 tonnes CO2/year
- **Regions:** US East + Quebec (for redundancy)
**Step 1:** Determine per-GPU serving capacity.
```python
# How many QPS per H100 at FP16?
r = serving.solve(model, Hardware.Cloud.H100, seq_len=2048, batch_size=1, precision="fp16")
print(f"Single H100: TTFT={r.ttft:~P.1f} ITL={r.itl:~P.1f} feasible={r.feasible}")
# Try with INT4 compression
c = compress.solve(model, Hardware.Cloud.H100, method="quantization", target_bitwidth=4)
print(f"INT4 compression: {c.compression_ratio:.1f}x accuracy_delta={c.accuracy_delta:.1f}%")
```
**Step 2:** Calculate fleet size needed for 1000 QPS.
```python
# Estimate: each H100 can decode ~X tokens/sec for this model
# tokens_per_sec_per_gpu = 1000 / itl_in_seconds
itl_sec = r.itl.to("s").magnitude
tokens_per_sec = 1.0 / itl_sec if itl_sec > 0 else 0
print(f"Tokens/sec per GPU: {tokens_per_sec:.1f}")
print(f"GPUs needed for 1000 QPS: ~{1000 / max(tokens_per_sec, 1):.0f}")
```
**Step 3:** Check budget and carbon for each region split.
```python
for us_pct in [0.3, 0.5, 0.7]:
qc_pct = 1.0 - us_pct
# (Adapt node count based on your QPS calculation above)
n_total = 100 # placeholder -- replace with your calculation
n_us = int(n_total * us_pct)
n_qc = n_total - n_us
fleet_us = Fleet(name="US East", node=Nodes.DGX_H100, count=max(1,n_us),
fabric=Fabrics.InfiniBand_NDR, region=Infrastructure.Grids.US_Avg)
fleet_qc = Fleet(name="Quebec", node=Nodes.DGX_H100, count=max(1,n_qc),
fabric=Fabrics.InfiniBand_NDR, region=Infrastructure.Grids.Quebec)
tco_us = econ.solve(fleet_us, duration_days=365, mfu=0.3)
tco_qc = econ.solve(fleet_qc, duration_days=365, mfu=0.3)
co2_us = sustain.solve(fleet_us, duration_days=365, mfu=0.3)
co2_qc = sustain.solve(fleet_qc, duration_days=365, mfu=0.3)
total_tco = tco_us.tco_usd + tco_qc.tco_usd
total_co2 = (co2_us.carbon_footprint_kg + co2_qc.carbon_footprint_kg) / 1000 # tonnes
print(f"Split {us_pct:.0%} US / {qc_pct:.0%} QC: "
f"TCO=${total_tco:,.0f} CO2={total_co2:.0f}t")
```
### Question
What is your recommended fleet configuration? How many total GPUs,
what regional split, and does INT4 quantization change your answer?
### Hint
Llama-3-70B at FP16 requires ~140 GB -- it does not fit on a single
H100 (80 GB). You must either use tensor parallelism (2+ GPUs per
inference instance) or quantize to INT4 (~35 GB, fits on one H100).
This fundamentally changes the fleet size calculation.
<details>
<summary><strong>Expected Answer</strong></summary>
This is an open-ended design exercise. A strong answer includes:
1. **Precision choice:** INT4 quantization (35 GB) fits on a single H100,
halving the GPU requirement vs. FP16 (which needs TP=2 minimum).
The 2-5% accuracy trade-off is usually acceptable for serving.
2. **Fleet size:** With INT4 and typical ITL of ~30-50ms per token, each
H100 can handle roughly 20-30 QPS. For 1000 QPS total, you need
approximately 35-50 GPUs (5-7 DGX H100 nodes).
3. **Regional split:** Placing 70% of capacity in Quebec dramatically
reduces carbon (hydro grid at ~1.2 gCO2/kWh vs. US average at
~390 gCO2/kWh) while keeping 30% in US East for latency-sensitive
users. Total CO2 stays well under 500 tonnes/year.
4. **Budget check:** 5-7 DGX H100 nodes at ~$200-300K each, plus
electricity and networking, fits within the $5M annual budget.
The key insight is that **compression is not just an optimization -- it is
an architectural decision** that changes the fleet design by 2x.
</details>
### Discussion
What are the failure modes of this design? What happens when a full
DGX node goes down in the Quebec region? How does the ReliabilityModel
inform your redundancy strategy? Should you over-provision by N+1 or N+2?