Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
efad7b5
perf: defer DDP progress readback until training completes
cfregly Sep 8, 2026
30446ff
feat: add explicit execution placement and destination write audits
cfregly Sep 8, 2026
ce4681d
fix: freeze independent KV and Ozaki arithmetic requirements
cfregly Sep 8, 2026
0de2f31
fix: keep minimal Nsight captures to the requested five metrics
cfregly Sep 8, 2026
f69a6c0
perf: reuse serving engines and remove redundant 1P1D barriers
cfregly Sep 8, 2026
09207e5
perf: amortize fixed-weight pipeline fill and drain across repeats
cfregly Sep 8, 2026
aea503d
feat: expose matched TE precision batch sizes for throughput sweeps
cfregly Sep 8, 2026
d71c4ca
fix: apply serving profile arguments and close reused engines
cfregly Sep 8, 2026
d36ef3d
fix: honor explicit Nsight Systems warmup and capture counts
cfregly Sep 8, 2026
2aae639
fix: expose the declared Ozaki comparison budget on its reference
cfregly Sep 8, 2026
06a58a7
fix: reject empty execution audits and preserve qualification scope
cfregly Sep 8, 2026
ad47f66
docs: explain matched batch controls and explicit execution audits
cfregly Sep 8, 2026
1530151
docs: clarify profile loop budgets and Ozaki comparison envelope
cfregly Sep 8, 2026
f10a5b7
fix: derive KV pairwise tolerance from the independent reference budgets
cfregly Sep 8, 2026
8a75407
docs: record B200 follow-through gains, accuracy gates and profiler l…
cfregly Sep 8, 2026
73ab9ef
ci: leave enough time for complete CPU validation on hosted runners
cfregly Sep 8, 2026
7db240c
test: preserve profiler range checks for contiguous pipeline schedules
cfregly Sep 8, 2026
e582b90
Use portable paths in validation documentation
cfregly Sep 8, 2026
4b1cfb8
Refresh FP8 documentation regression after measured sweep
cfregly Sep 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/benchmark-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ defaults:
jobs:
validate:
runs-on: ubuntu-latest
timeout-minutes: 30
# Full CPU validation can approach 30 minutes on slower hosted runners.
# Keep the complete suite and leave time for the final audits and artifacts.
timeout-minutes: 35

steps:
- name: Checkout repository
Expand Down
78 changes: 65 additions & 13 deletions code/ch04/optimized_pipeline_parallel_1f1b.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import argparse
import os
import time
from collections.abc import Callable, Sequence
from typing import Optional

import torch
Expand Down Expand Up @@ -135,6 +136,54 @@ def _run_rank_stage_inplace(
return _run_stage_inplace(stages[rank], x)


def _run_contiguous_1f1b_iterations(
*,
rank: int,
world_size: int,
micro_batches_per_iteration: int,
iteration_count: int,
get_rank0_microbatch: Callable[[int], torch.Tensor],
recv_forward_buffers: Sequence[torch.Tensor],
recv_backward_buffers: Sequence[torch.Tensor],
forward_step: Callable[[torch.Tensor], torch.Tensor],
backward_step: Callable[[torch.Tensor, torch.Tensor], torch.Tensor],
activation_slots: list[Optional[tuple[int, torch.Tensor]]],
capture: Optional[PipelineIterationCapture] = None,
) -> None:
"""Amortize boundaries across fixed-state iterations with no intervening update.

This is valid for this benchmark's fixed weights and repeated input batch. A
training loop that updates parameters between iterations must drain before
each update instead of using this helper across that boundary.
"""

if iteration_count <= 0:
raise ValueError("iteration_count must be positive")
if micro_batches_per_iteration <= 0:
raise ValueError("micro_batches_per_iteration must be positive")
if micro_batches_per_iteration < world_size:
raise ValueError("Each logical iteration needs at least one microbatch per stage")
scheduled_micro_batches = micro_batches_per_iteration * iteration_count

def get_repeated_rank0_microbatch(index: int) -> torch.Tensor:
return get_rank0_microbatch(index % micro_batches_per_iteration)

run_1f1b_iteration(
rank=rank,
world_size=world_size,
num_micro_batches=scheduled_micro_batches,
get_rank0_microbatch=get_repeated_rank0_microbatch,
# Only references are repeated. The same fixed tensor slots are safe to
# receive into after their prior microbatch has completed its stage.
recv_forward_buffers=list(recv_forward_buffers) * iteration_count,
recv_backward_buffers=list(recv_backward_buffers) * iteration_count,
forward_step=forward_step,
backward_step=backward_step,
activation_slots=activation_slots,
capture=capture,
)


def _run_worker(
iters: int,
warmup: int,
Expand Down Expand Up @@ -209,11 +258,15 @@ def _get_rank0_microbatch(micro_idx: int) -> torch.Tensor:
warmup_steps, 1
)

def _run_iteration(capture: Optional[PipelineIterationCapture] = None) -> None:
run_1f1b_iteration(
def _run_contiguous_iterations(
iteration_count: int,
capture: Optional[PipelineIterationCapture] = None,
) -> None:
_run_contiguous_1f1b_iterations(
rank=rank,
world_size=world_size,
num_micro_batches=num_micro_batches,
micro_batches_per_iteration=num_micro_batches,
iteration_count=iteration_count,
get_rank0_microbatch=_get_rank0_microbatch,
recv_forward_buffers=recv_micro_batches,
recv_backward_buffers=recv_grads,
Expand All @@ -226,22 +279,21 @@ def _run_iteration(capture: Optional[PipelineIterationCapture] = None) -> None:
result_requested = pipeline_child_result_requested()
captured_iteration: Optional[PipelineIterationCapture] = None
with torch.inference_mode():
for _ in range(max(warmup, 0)):
_run_iteration()
warmup_iterations = max(warmup, 0)
if warmup_iterations:
_run_contiguous_iterations(warmup_iterations)
torch.cuda.synchronize(device)

with nvtx_range(PROFILE_NVTX_RANGE, enable=True):
start = time.perf_counter()
measured_iterations = max(iters, 1)
for iteration in range(max(iters, 1)):
capture = (
PipelineIterationCapture.create(num_micro_batches)
if result_requested and iteration == measured_iterations - 1
else None
if result_requested:
captured_iteration = PipelineIterationCapture.create(
num_micro_batches,
first_microbatch_index=(measured_iterations - 1)
* num_micro_batches,
)
_run_iteration(capture)
if capture is not None:
captured_iteration = capture
_run_contiguous_iterations(measured_iterations, captured_iteration)
torch.cuda.synchronize(device)
elapsed = time.perf_counter() - start

Expand Down
18 changes: 15 additions & 3 deletions code/ch04/pipeline_parallel_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,29 @@

@dataclass
class PipelineIterationCapture:
"""References to one measured iteration, indexed by original microbatch."""
"""References to a contiguous microbatch window from a measured schedule."""

forward_inputs: list[torch.Tensor | None]
backward_inputs: list[torch.Tensor | None]
backward_outputs: list[torch.Tensor | None]
first_microbatch_index: int = 0

@classmethod
def create(cls, num_micro_batches: int) -> PipelineIterationCapture:
def create(
cls,
num_micro_batches: int,
*,
first_microbatch_index: int = 0,
) -> PipelineIterationCapture:
if num_micro_batches <= 0:
raise ValueError("num_micro_batches must be positive")
if first_microbatch_index < 0:
raise ValueError("first_microbatch_index must be non-negative")
return cls(
forward_inputs=[None] * num_micro_batches,
backward_inputs=[None] * num_micro_batches,
backward_outputs=[None] * num_micro_batches,
first_microbatch_index=first_microbatch_index,
)

def concatenate(self) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
Expand Down Expand Up @@ -114,7 +123,10 @@ def _record(
tensor: torch.Tensor,
) -> None:
if capture is not None:
getattr(capture, collection)[microbatch_index] = tensor
capture_index = microbatch_index - capture.first_microbatch_index
values = getattr(capture, collection)
if 0 <= capture_index < len(values):
values[capture_index] = tensor


def _exchange_neighbor(
Expand Down
32 changes: 31 additions & 1 deletion code/ch13/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,15 @@ Representative validated results from `artifacts/runs/20260303_163946__bench__pr

This chapter is one of the easiest places to fool yourself with framework overhead. That is why the benchmark contract and side-by-side baseline/optimized structure matter here more than almost anywhere else.

The prior `precisionfp8_te` number compared eager FP16 with CUDA-graph-replayed FP8, so it is retired. The pair now runs both sides eagerly to isolate Transformer Engine FP8; publish a new speed result only after a fresh B200 correctness and timing run.
The prior `precisionfp8_te` number compared eager FP16 with CUDA-graph-replayed FP8, so it is retired. The pair now runs both sides eagerly to isolate Transformer Engine FP8. A fresh direct-B200 sweep with Transformer Engine 2.18 measured the full training update after five setup and ten warmup updates:

| Matched batch | Eager FP16 median | Eager TE FP8 median | FP16 / FP8 | Disposition |
| ---: | ---: | ---: | ---: | --- |
| 256 | `0.4786 ms` | `0.6662 ms` | `0.7183x` | no measured speedup |
| 1,024 | `0.5483 ms` | `0.6644 ms` | `0.8252x` | no measured speedup |
| 4,096 | `1.2303 ms` | `0.9616 ms` | `1.2795x` | candidate speedup for this workload |

The 24 observations cover two seeds, two repeats, both arms, and all three batches. Every observation compared the actual prediction and all 67,121,152 post-step parameters and passed the frozen output policy. Batch 256 remains the default, so the batch-4,096 result establishes a workload-specific crossover rather than a general default-workload speedup. Whole-call cost is retained separately from the CUDA-event update timing.

The TE 2.18 pair now verifies its captured prediction and every post-step parameter with a calibrated per-output policy. Previously, all outputs inherited the global `(rtol=0.5, atol=5.0)` threshold. B200 calibration of the unchanged batch-256, hidden-4096 training step selected these stricter budgets across seed 44 and fixed holdouts 45, 1044, and 1045:

Expand All @@ -39,6 +47,22 @@ The TE 2.18 pair now verifies its captured prediction and every post-step parame

The frozen map passed all holdouts and independently rejected zeroed and localized corrupted copies of all five outputs. These bounds apply to this TE 2.18 workload and establish numerical verification only; they do not establish a speedup.

## Matched Batch Controls
`precisionfp8_te` keeps batch size 256 as its default workload. Omitting a target override preserves that default for both the eager FP16 baseline and eager Transformer Engine FP8 candidate:

```bash
python -m cli.aisp bench run --targets ch13:precisionfp8_te --profile deep_dive --single-gpu
```

To test optional larger matched controls, pass one pair-wide override through the harness. This sends the requested batch to both arms and updates their workload metadata consistently:

```bash
python -m cli.aisp bench run --targets ch13:precisionfp8_te --profile deep_dive --single-gpu --target-extra-arg 'ch13:precisionfp8_te=--batch-size 1024'
python -m cli.aisp bench run --targets ch13:precisionfp8_te --profile deep_dive --single-gpu --target-extra-arg 'ch13:precisionfp8_te=--batch-size 4096'
```

Compare results only when both arms report the same requested batch size and workload signature. Fresh B200 checks pass at 256, 1,024, and 4,096 with the frozen policy. The first two batches are slower under FP8; the observed 1.2795x result applies only to the matched batch-4,096 workload and does not change the default.

## Profiler Evidence
Use deep-dive runs when you want to see whether the gain came from framework overhead reduction, memory behavior, or the lower-precision path itself:

Expand All @@ -53,6 +77,8 @@ Those targets cover three different PyTorch optimization stories:
- `autograd_standard`: framework/compile overhead
- `precisionfp8_te`: lower-precision execution with real library support

Matched batch-4,096 Nsys captures pass for both `precisionfp8_te` arms. The FP8 trace shows shorter main GEMMs alongside quantization and scale-update work. Each trace contains one profiled update after setup and one profiler warmup, so trace durations are diagnostic; the repeated sweep above remains the timing authority.

The torchao FP8 recipe demos (`precisionfp8`, `precisionfp8_rowwise`, `precisionfp8_rowwise_gw_hp`) remain useful implementation references, but they are treated as informational examples rather than canonical speed-claim surfaces.

## Repro Commands
Expand All @@ -61,6 +87,8 @@ python -m ch13.compare
python -m cli.aisp bench list-targets --chapter ch13
python -m cli.aisp bench run --targets ch13 --profile minimal
python -m cli.aisp bench run --targets ch13:precisionfp8_te --profile deep_dive --single-gpu
python -m cli.aisp bench run --targets ch13:precisionfp8_te --profile deep_dive --single-gpu --target-extra-arg 'ch13:precisionfp8_te=--batch-size 1024'
python -m cli.aisp bench run --targets ch13:precisionfp8_te --profile deep_dive --single-gpu --target-extra-arg 'ch13:precisionfp8_te=--batch-size 4096'
```

## Learning Goals
Expand Down Expand Up @@ -97,10 +125,12 @@ python -m cli.aisp bench run --targets ch13 --profile minimal
## Validation Checklist
- `python -m ch13.compare --examples training_standard` shows optimized training runs producing higher goodput with identical metrics.
- `python -m cli.aisp bench run --targets ch13:precisionfp8_te --profile minimal` confirms Transformer Engine calibration plus NVFP8 execution with max error tolerances enforced.
- Matched B200 `precisionfp8_te` checks at batches 256, 1,024, and 4,096 pass the full-output policy; only batch 4,096 shows a measured candidate speedup (`1.2795x`) for this exact workload.
- `python -m ch13.memory_profiling --dump` and the optimized variant demonstrate allocator fragmentation dropping after applying the recommended knobs, with memory reduction treated as the primary benchmark outcome.

## Notes
- `custom_allocator.py` contains a standalone torch allocator shim that can be re-used in other chapters when debugging fragmentation.
- `compiled_autograd.py` doubles as a tutorial on partial graph capture; the README here references it directly.
- `precisionfp8_te` defaults to batch 256. Larger `--batch-size` values are explicit pair-wide workload overrides; the B200 crossover appeared at batch 4,096 and does not change the default.
- `torchao_quantization_compiled`, `kv_cache_naive_flash_blockwise`, `precisionfp8`, `precisionfp8_rowwise`, and `precisionfp8_rowwise_gw_hp` remain informational variants.
- `kv_cache_naive` and `memory_profiling` are memory-goal benchmarks; they are expected to reduce memory pressure even when the timed path is not faster.
31 changes: 30 additions & 1 deletion code/ch13/baseline_precisionfp8_te.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
import torch.nn as nn

from ch13.te_runtime_common import (
TE_PRECISION_DEFAULT_BATCH_SIZE,
ensure_te_runtime_initialized,
get_te_precision_output_tolerances,
parse_te_precision_batch_size,
)
from core.benchmark.verification_mixin import VerificationPayloadMixin
from core.harness.benchmark_harness import (
Expand Down Expand Up @@ -73,8 +75,9 @@ def __init__(self):
self.targets: Optional[torch.Tensor] = None
self.optimizer: Optional[torch.optim.Optimizer] = None
self.criterion: Optional[nn.Module] = None
self.batch_size = 256
self.batch_size = TE_PRECISION_DEFAULT_BATCH_SIZE
self.hidden_dim = 4096
self._target_override_error: str | None = None
tokens = self.batch_size * self.hidden_dim
self._workload = WorkloadMetadata(
requests_per_iteration=1.0,
Expand All @@ -92,7 +95,33 @@ def __init__(self):
self._verify_input: Optional[torch.Tensor] = None
self._verify_target: Optional[torch.Tensor] = None

def _set_batch_size(self, batch_size: int) -> None:
self.batch_size = batch_size
tokens = self.batch_size * self.hidden_dim
self._workload = WorkloadMetadata(
requests_per_iteration=1.0,
tokens_per_iteration=float(tokens),
)
self.register_workload_metadata(
requests_per_iteration=1.0,
tokens_per_iteration=float(tokens),
)

def apply_target_overrides(self, argv: list[str]) -> None:
"""Apply ``aisp bench --target-extra-arg`` batch configuration."""
try:
batch_size = parse_te_precision_batch_size(argv, default=self.batch_size)
except ValueError as exc:
# The harness logs and suppresses override-hook exceptions. Retain
# the error so setup still rejects an invalid requested workload.
self._target_override_error = str(exc)
raise
self._target_override_error = None
self._set_batch_size(batch_size)

def setup(self) -> None:
if self._target_override_error is not None:
raise ValueError(f"Invalid target override: {self._target_override_error}")
_load_te_linear()
model = TEFP16MLP(hidden_dim=self.hidden_dim).to(self.device).train().half()
self.model = model
Expand Down
31 changes: 30 additions & 1 deletion code/ch13/optimized_precisionfp8_te.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
from torch.optim import Optimizer

from ch13.te_runtime_common import (
TE_PRECISION_DEFAULT_BATCH_SIZE,
ensure_te_runtime_initialized,
get_te_precision_output_tolerances,
parse_te_precision_batch_size,
)
from core.benchmark.verification_mixin import VerificationPayloadMixin
from core.harness.benchmark_harness import (
Expand Down Expand Up @@ -83,8 +85,9 @@ def __init__(self):
self.optimizer: Optional[Optimizer] = None
self.criterion: Optional[nn.Module] = None
self.fp8_recipe: Optional[object] = None
self.batch_size = 256
self.batch_size = TE_PRECISION_DEFAULT_BATCH_SIZE
self.hidden_dim = 4096
self._target_override_error: str | None = None
self.compute_dtype = torch.float16
self.input_pool: List[torch.Tensor] = []
self.target_pool: List[torch.Tensor] = []
Expand All @@ -105,7 +108,33 @@ def __init__(self):
tokens_per_iteration=float(tokens),
)

def _set_batch_size(self, batch_size: int) -> None:
self.batch_size = batch_size
tokens = self.batch_size * self.hidden_dim
self._workload = WorkloadMetadata(
requests_per_iteration=1.0,
tokens_per_iteration=float(tokens),
)
self.register_workload_metadata(
requests_per_iteration=1.0,
tokens_per_iteration=float(tokens),
)

def apply_target_overrides(self, argv: list[str]) -> None:
"""Apply ``aisp bench --target-extra-arg`` batch configuration."""
try:
batch_size = parse_te_precision_batch_size(argv, default=self.batch_size)
except ValueError as exc:
# The harness logs and suppresses override-hook exceptions. Retain
# the error so setup still rejects an invalid requested workload.
self._target_override_error = str(exc)
raise
self._target_override_error = None
self._set_batch_size(batch_size)

def setup(self) -> None:
if self._target_override_error is not None:
raise ValueError(f"Invalid target override: {self._target_override_error}")
_, _, te_recipe_module = _load_transformer_engine()
# Transformer Engine 2.x defaults to HYBRID format (E4M3 forward,
# E5M2 backward); keep a long amax history and the standard max policy.
Expand Down
25 changes: 25 additions & 0 deletions code/ch13/te_runtime_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

from __future__ import annotations

import argparse
import ctypes
from collections.abc import Iterable
from functools import lru_cache
from pathlib import Path

Expand All @@ -25,6 +27,29 @@
"parameter.fc2.bias": (0.001, 0.00005),
}

TE_PRECISION_DEFAULT_BATCH_SIZE = 256


def parse_te_precision_batch_size(
argv: Iterable[str],
*,
default: int = TE_PRECISION_DEFAULT_BATCH_SIZE,
) -> int:
"""Parse the shared batch override used by both TE precision arms."""
parser = argparse.ArgumentParser(
add_help=False,
allow_abbrev=False,
exit_on_error=False,
)
parser.add_argument("--batch-size", type=int, default=default)
try:
args, _ = parser.parse_known_args(list(argv))
except (argparse.ArgumentError, SystemExit) as exc:
raise ValueError("--batch-size must be a positive integer") from exc
if args.batch_size <= 0:
raise ValueError("--batch-size must be a positive integer")
return int(args.batch_size)


def get_te_precision_output_tolerances() -> dict[str, tuple[float, float]]:
"""Return the calibrated full-output policy for the TE2.18 precision pair."""
Expand Down
Loading