Skip to main content

Full-Stack Audit: LLaMA-70B Training

One model, six domains, twelve walls: a complete systems analysis in 60 seconds.

capstone
advanced
Compose 6+ solvers across all six taxonomy domains to produce a holistic training analysis across memory capacity, compute, communication, reliability, and sustainability walls.

The Question

What does a complete systems analysis look like? No single solver captures the full picture. Training a 71B-parameter model (Llama-3.1-70B) on 512 NVIDIA H100 GPUs involves compute walls, memory walls, communication overhead, checkpoint I/O, energy costs, and carbon emissions, all simultaneously. This tutorial traces all six taxonomy domains and exercises 12 of the 22 systems walls through a single workload.

NotePrerequisites

Complete Tutorial 0: Hello, Roofline, Tutorial 1: The Memory Wall, Tutorial 6: Scaling to 1000 GPUs, and Tutorial 9: Sensitivity Analysis. This tutorial assumes familiarity with roofline analysis, distributed training, and binding constraint identification.

NoteLearning Objectives
  • Compose six solver families across all taxonomy domains into a holistic analysis
  • Identify which of the 22 systems walls bind for a real training workload
  • Quantify the hidden costs: checkpoint overhead, carbon, water, and TCO
  • Produce a summary table mapping domain -> solver -> binding wall
TipSolver Quick Reference

This capstone uses solvers from all six domains. For readers who arrived via an accelerated learning path, the table below summarizes what each solver does:

Solver Domain What It Computes
SingleNodeModel Node Roofline bottleneck, latency, throughput
DataModel Data Whether the data pipeline can sustain GPU demand
ScalingModel Algorithm Compute-optimal training budget (Chinchilla)
DistributedModel Fleet Communication overhead and scaling efficiency
ReliabilityModel Fleet Cluster MTBF and optimal checkpoint intervals
EconomicsModel Ops CapEx, OpEx, and total cost of ownership (TCO)
SustainabilityModel Ops Energy, carbon footprint, and water usage
SensitivitySolver Analysis Partial derivatives identifying the binding constraint
SynthesisSolver Analysis Minimum hardware specs from a latency target
TipBackground: The Six Taxonomy Domains

The MLSys wall taxonomy organizes 22 systems walls into six domains:

Domain Walls What It Covers
Node 1–3 Compute, memory capacity, memory bandwidth
Data 8–10 Storage throughput, data pipeline stalls
Algorithm 11–13 Scaling laws, compute-optimal training
Fleet 14–16 Communication, synchronization, reliability
Ops 17–20 TCO, energy, carbon, water, safety
Analysis 21–22 Sensitivity, inverse synthesis

No single solver spans all six. The insight emerges from composition.


1. Setup: Build the Fleet

We construct a 512-GPU training cluster: 64 DGX H100 nodes, 8 GPUs per node, NVLink intra-node (900 GB/s), InfiniBand NDR inter-node (400 Gbps), powered by Quebec (Hydro)’s hydroelectric grid (20 g CO2/kWh).

import mlsysim
from mlsysim.systems.types import Fleet, Node, NetworkFabric
from mlsysim.core.units import Q_
from mlsysim.systems.types import Fleet, Node, NetworkFabric
from mlsysim.infrastructure.registry import Grids
from mlsysim.core.units import Q_

model = mlsysim.Models.Language.Llama3_70B
h100 = mlsysim.Hardware.Cloud.H100
ib_ndr = mlsysim.Systems.Fabrics.InfiniBand_NDR

# Build the DGX H100 node: 8 GPUs connected by NVLink 4.0
node = Node(
    name="DGX H100",
    accelerator=h100,
    accelerators_per_node=8,
    intra_node_bw=h100.nvlink.bandwidth
)

# Build the cluster fabric: InfiniBand NDR
fabric = NetworkFabric(
    name="InfiniBand NDR",
    topology="fat-tree",
    bandwidth=ib_ndr.bandwidth
)

# Build the fleet: 64 nodes = 512 GPUs, Quebec grid
fleet = Fleet(
    name="Training Cluster",
    node=node,
    count=64,
    fabric=fabric,
    region=Grids.Quebec
)

from mlsysim.show import table, info, banner

info("Fleet Configuration",
     Model=f"{model.name} ({model.parameters.to('Bparam'):.1f~})",
     Fleet=f"{fleet.count} nodes x {node.accelerators_per_node} GPUs = {fleet.total_accelerators} GPUs",
     Intra_node=f"NVLink 4.0 ({h100.nvlink.bandwidth.to('GB/s'):.0f~})",
     Inter_node=f"IB NDR ({ib_ndr.bandwidth.to('Gbps'):.0f~})",
     Region=Grids.Quebec.name)
── Fleet Configuration ─────────────────────
Model:       Llama-3.1-70B (70.6 Bparam)
Fleet:       64 nodes x 8 GPUs = 512 GPUs
Intra node:  NVLink 4.0 (900 GB / s)
Inter node:  IB NDR (400 Gbps)
Region:      Quebec (Hydro)

2. Node (Walls 1–3): Single-GPU Roofline

The first step evaluates an unsharded Llama-3.1-70B forward pass on a single NVIDIA H100 GPU.

from mlsysim.solvers import SingleNodeModel

node_solver = SingleNodeModel()
node_result = node_solver.solve(
    model=model, hardware=h100,
    batch_size=4, precision="fp16"
)

banner("Domain: Node (Walls 1-3)")
info(Feasible=node_result.feasible,
     Required_memory=node_result.memory_footprint.to('GiB'),
     Available_memory=h100.memory.capacity.to('GiB'),
     Bottleneck=node_result.bottleneck,
     Per_GPU_latency=node_result.latency.to('ms'),
     Throughput=f"{node_result.throughput.to('1/s').magnitude:.1f} samples/s")

=== Domain: Node (Walls 1-3) ===
Feasible:          False
Required memory:   131.5 GiB
Available memory:  80 GiB
Bottleneck:        Memory
Per GPU latency:   3,089.6 ms
Throughput:        1.3 samples/s

At 70.6B parameters, Llama-3.1-70B weights alone require 141.2 GB in FP16, exceeding a single NVIDIA H100’s 80 GiB (85.9 GB) HBM capacity (feasible = False). Because the unsharded model spills across PCIe (64 GB / s), SingleNodeModel reports Memory-bound at 3089.6 ms per step (1.3 samples/s).

Wall 2 (Memory Capacity) and Wall 3 (Memory Bandwidth) immediately rule out unsharded single-GPU execution and force intra-node tensor parallelism (TP=8 in Section 5). The next question is whether the storage and preprocessing pipeline can keep up with 512 GPUs demanding training tokens.


3. Data (Walls 8–10): Whether the Pipeline Can Keep Up

The storage and preprocessing pipeline must deliver tokenized batches at least as fast as the cluster consumes them. If it cannot, the GPUs stall.

from mlsysim.solvers import DataModel

# Estimate data demand per step: 4 samples/GPU * 512 GPUs * 2048 tokens * 2 bytes ≈ 8 MB/step
data_demand = Q_("8 MB/s")

data_solver = DataModel()
data_result = data_solver.solve(
    workload_data_rate=data_demand,
    hardware=h100
)

banner("Domain: Data (Walls 8-10)")
info(Data_demand=data_result.demand_bw,
     Data_supply=data_result.supply_bw,
     Utilization=f"{data_result.utilization:.2%}",
     Stalled=data_result.is_stalled,
     Bottleneck=data_result.bottleneck)

=== Domain: Data (Walls 8-10) ===
Data demand:  8.00e-03 GB/s
Data supply:  7 GB/s
Utilization:  0.11%
Stalled:      False
Bottleneck:   Storage

At 8 MB / s demand against 7.0 GB / s storage bandwidth, storage utilization is only 0.11% (is_stalled = False). For text-based training, the data pipeline is rarely the bottleneck because tokenized text is compact; for image or video training, this wall can dominate.

The remaining question is whether the 30-day compute budget on 512 GPUs is allocated optimally according to scaling laws.


4. Algorithm (Walls 11–13): Compute-Optimal Budget

The test of whether this training budget is compute-optimal is the Chinchilla scaling law, which prescribes \(D = 20P\) (tokens = \(20\times\) parameters) for optimal allocation.

from mlsysim.solvers import ScalingModel

# MFU (Model FLOP Utilization): the fraction of peak hardware FLOP/s that goes
# to useful model computation (excluding communication, idle time, overhead).
# MFU = 0.4 means 40% of theoretical peak.
gpu_flops = h100.compute.peak_flops.to("flop/s").magnitude
total_flops = fleet.total_accelerators * gpu_flops * 30 * 86400 * 0.4
compute_budget = Q_(total_flops, "flop")

scaling_solver = ScalingModel()
scaling_result = scaling_solver.solve(
    compute_budget=compute_budget,
    target_model_size=model.parameters
)

banner("Domain: Algorithm (Walls 11-13)")
info(Compute_budget=compute_budget.to('EFLOP'),
     Optimal_tokens=f"{scaling_result.optimal_tokens.magnitude:.2e}",
     Tokens_per_parameter=f"{scaling_result.tokens_per_parameter:.1f}",
     Chinchilla_ratio=f"{'OVER' if scaling_result.tokens_per_parameter > 20 else 'UNDER'}-trained")

=== Domain: Algorithm (Walls 11-13) ===
Compute budget:        525,002.3 EFLOP
Optimal tokens:        1.24e+12
Tokens per parameter:  17.6
Chinchilla ratio:      UNDER-trained

With 512 GPUs at 989 TFLOP/s peak FP16 and 40% MFU over 30 days, the compute budget reaches 525002 EFLOP, funding 1.24T tokens (17.6 tokens per parameter, slightly below the Chinchilla 20 tokens/parameter ratio). If the tokens-per-parameter ratio is significantly above or below 20, the training budget is not optimally allocated: over-training wastes compute, while under-training wastes model capacity.


5. Fleet (Walls 14–16): Communication and Reliability

Sharding Llama-3.1-70B with TP=8 within each DGX H100 node (PP=1, DP=64 across the 64 nodes) divides the weights across 8 GPUs per replica. The distributed solver models both the replica’s 2,048-token forward-backward pass and the AllReduce communication overhead, while the reliability solver computes cluster MTBF and optimal checkpoint intervals.

from mlsysim.solvers import DistributedModel, ReliabilityModel

# 3D parallelism: TP=8 (within node), PP=1, DP=64
dist_solver = DistributedModel()
dist_result = dist_solver.solve(
    model=model, fleet=fleet,
    batch_size=2048, precision="fp16",
    tp_size=8, pp_size=1,
    overlap_comm=True, seq_len=2048
)

banner("Domain: Fleet (Walls 14-16)")
info(Replica_bottleneck=dist_result.node_profile.bottleneck,
     Replica_compute_latency=dist_result.node_profile.latency.to('ms'),
     Scaling_efficiency=f"{dist_result.scaling_efficiency:.2%}",
     Step_latency=dist_result.step_latency_total.to('ms'),
     DP_comm_latency=dist_result.dp_communication_latency.to('ms'),
     TP_comm_latency=dist_result.tp_communication_latency.to('ms'),
     Bubble_fraction=f"{dist_result.bubble_fraction:.2%}")

=== Domain: Fleet (Walls 14-16) ===
Replica bottleneck:       Compute
Replica compute latency:  7,019.9 ms
Scaling efficiency:       90.06%
Step latency:             7,794.6 ms
DP comm latency:          695.6 ms
TP comm latency:          670.3 ms
Bubble fraction:          0.00%
# Reliability: 30-day training job
rel_solver = ReliabilityModel()
rel_result = rel_solver.solve(
    fleet=fleet,
    job_duration_hours=30*24,
    checkpoint_time_s=120
)

info(Fleet_MTBF=rel_result.fleet_mtbf.to('hour'),
     Failure_probability=f"{rel_result.failure_probability:.2%}",
     Expected_failures=f"{rel_result.expected_failures:.1f}",
     Optimal_ckpt_interval=rel_result.optimal_checkpoint_interval.to('minute'))
Fleet MTBF:             83.71 h
Failure probability:    99.98%
Expected failures:      8.6
Optimal ckpt interval:  141.7 min

With TP=8 across each node, the 2,048-token forward-backward pass is Compute-bound (7019.9 ms replica compute latency), and overlapping DP AllReduce (695.6 ms unoverlapped) with backward compute plus intra-node TP AllReduce (670.3 ms) yields a 7794.6 ms step time and 90.06% scaling efficiency.

At 512 GPUs, however, the cluster MTBF shrinks to 83.7 h, with 8.6 expected failures over the 30-day run (99.98% failure probability) and an optimal Young-Daly checkpoint interval of 141.7 min. Checkpoint overhead and rollback recovery become a non-trivial fraction of wall-clock time—the hidden cost that single-node analysis misses entirely.


6. Ops (Walls 17–20): TCO, Energy, Carbon, Water

The economics solver combines CapEx, OpEx, and sustainability into a single financial model.

from mlsysim.solvers import EconomicsModel, SustainabilityModel

# 30-day training run
econ_solver = EconomicsModel()
econ_result = econ_solver.solve(
    fleet=fleet,
    duration_days=30,
    grid=Grids.Quebec,
    mfu=0.4
)

banner("Domain: Ops (Walls 17-20)")
info(CapEx=f"${econ_result.capex_usd:,.0f}",
     OpEx_energy=f"${econ_result.opex_energy_usd:,.0f}",
     OpEx_maintenance=f"${econ_result.opex_maintenance_usd:,.0f}",
     Total_TCO=f"${econ_result.tco_usd:,.0f}")

=== Domain: Ops (Walls 17-20) ===
CapEx:             $350,685
OpEx energy:       $19,038
OpEx maintenance:  $52,603
Total TCO:         $422,325
sust_solver = SustainabilityModel()
sust_result = sust_solver.solve(
    fleet=fleet,
    duration_days=30,
    datacenter=Grids.Quebec,
    mfu=0.4
)

info(IT_Energy=sust_result.it_energy_kwh.to('MWh'),
     Total_Energy_PUE=sust_result.total_energy_kwh.to('MWh'),
     Carbon_footprint=f"{sust_result.carbon_footprint_kg:.0f} kg CO2",
     Water_usage=f"{sust_result.water_usage_liters:.0f} liters",
     PUE=sust_result.pue,
     Region=sust_result.region_name)
IT Energy:         149.7 MWh
Total Energy PUE:  158.6 MWh
Carbon footprint:  3173 kg CO2
Water usage:       0 liters
PUE:               1.06
Region:            Quebec (Hydro)

For the 30-day run, EconomicsModel reports $422,325 in total TCO ($350,685 amortized CapEx, $19,038 energy OpEx, and $52,603 maintenance OpEx). Quebec (Hydro)’s hydroelectric grid (20 g CO2/kWh, PUE 1.06) keeps total emissions to 3,173 kg CO2 (3.2 tonnes across 158.6 MWh of facility energy). The same run in Poland (820 g CO2/kWh, PUE 1.58) produces 193,910 kg CO2 (193.9 tonnes, 61× higher), making infrastructure geography a first-class engineering variable.


7. Analysis (Walls 21–22): Sensitivity and Synthesis

Finally, run SensitivitySolver on a single NVIDIA H100 and SynthesisSolver for a 200 ms step latency target.

from mlsysim.solvers import SensitivitySolver, SynthesisSolver

# Sensitivity: check the single-GPU binding constraint for unsharded Llama-3.1-70B
sens_solver = SensitivitySolver()
sens_result = sens_solver.solve(
    model=model, hardware=h100, precision="fp16"
)

banner("Domain: Analysis (Walls 21-22)")
info(Binding_constraint=sens_result.binding_constraint)

sens_rows = [[param, f"{val:+.4f}"] for param, val in sens_result.sensitivities.items()]
table(["Parameter", "Sensitivity"], sens_rows)

=== Domain: Analysis (Walls 21-22) ===
Binding constraint:  memory_capacity
Parameter         Sensitivity
─────────────────────────────
peak_flops            +0.0000
memory_bandwidth      +0.0000
memory_capacity       +0.0000
# Synthesis: minimum hardware specs for a 200 ms single-step target
synth_solver = SynthesisSolver()
synth_result = synth_solver.solve(
    model=model,
    target_latency=Q_("200 ms"),   # per-GPU step target
    precision="fp16"
)

info("Synthesis (200ms step target)",
     Required_BW=synth_result.required_bw.to('TB/s'),
     Required_FLOPS=synth_result.required_flops.to('TFLOPs/s'),
     Required_memory=synth_result.required_memory.to('GB'))
── Synthesis (200ms step target) ───────────
Required BW:      0.706 TB/s
Required FLOPS:   1.41 TFLOPs/s
Required memory:  141.2 GB

On an unsharded single NVIDIA H100, SensitivitySolver identifies memory_capacity as the binding constraint because the 141.2 GB FP16 weight footprint exceeds 80 GiB (85.9 GB), pinning all small local perturbations at PCIe offload bandwidth until model parallelism (TP=8) brings the per-GPU footprint inside HBM.


8. Summary Table: The Complete Picture

We have now traced a single workload through all six domains. Each solver answered one question in isolation. The systems engineer’s job, however, is synthesis: seeing the complete picture at once. The table below is that picture, and its most important property is that no single row captures the full story.

mtbf_hours = rel_result.fleet_mtbf.to('hour').magnitude
summary_rows = [
    ["Node",      "SingleNodeModel",      f"Bottleneck: {node_result.bottleneck}",              "Wall 2/3: Memory"],
    ["Data",      "DataModel",            f"Util: {data_result.utilization:.2%}",               "Not binding"],
    ["Algorithm", "ScalingModel",         f"Tok/param: {scaling_result.tokens_per_parameter:.1f}","Wall 11"],
    ["Fleet",     "DistributedModel",     f"Efficiency: {dist_result.scaling_efficiency:.0%}",   "Wall 14: Comm"],
    ["Fleet",     "ReliabilityModel",     f"MTBF: {mtbf_hours:.0f}h",                           "Wall 16: Ckpt"],
    ["Ops",       "EconomicsModel",       f"TCO: ${econ_result.tco_usd:,.0f}",                  "Wall 19: Cost"],
    ["Ops",       "SustainabilityModel",  f"CO2: {sust_result.carbon_footprint_kg:.0f} kg",     "Wall 18: Carbon"],
    ["Analysis",  "SensitivitySolver",    f"Binding: {sens_result.binding_constraint}",         "Wall 21"],
]

table(["Domain", "Solver", "Key Metric", "Binding Wall"], summary_rows, "<<>>")
Domain     Solver                             Key Metric      Binding Wall
──────────────────────────────────────────────────────────────────────────
Node       SingleNodeModel            Bottleneck: Memory  Wall 2/3: Memory
Data       DataModel                         Util: 0.11%       Not binding
Algorithm  ScalingModel                  Tok/param: 17.6           Wall 11
Fleet      DistributedModel              Efficiency: 90%     Wall 14: Comm
Fleet      ReliabilityModel                    MTBF: 84h     Wall 16: Ckpt
Ops        EconomicsModel                  TCO: $422,325     Wall 19: Cost
Ops        SustainabilityModel              CO2: 3173 kg   Wall 18: Carbon
Analysis   SensitivitySolver    Binding: memory_capacity           Wall 21
ImportantKey Insight

No single solver captures the full picture; the systems view emerges from composition. This end-to-end trace exercises 12 of 22 walls through a single model. At the single-GPU level, memory capacity (141.2 GB vs. 80 GiB) forces tensor parallelism (TP=8), where each 8-GPU replica is compute-bound (7020 ms per step at 90% scaling efficiency). The hidden costs only appear at fleet scale: a 83.7 h cluster MTBF forces checkpoints every 142 min, and infrastructure geography (Quebec (Hydro) vs. Poland (Coal)) changes grid carbon intensity by 41× and PUE-adjusted carbon emissions by 61× (as Tutorial 7 demonstrated). A complete systems analysis is not one solver run but the composition of all six domains.


Next Steps

Back to top