Reproduces the Odyssey AMD Inference Pilot (April 2026) benchmark methodology
end-to-end and cross-validates the PyTorch numbers against ROCm Validation
Suite (RVS), rocm-bandwidth-test, and rccl-tests. AMD Instinct
MI355X is the reference target — every threshold and ceiling cited in the
PDF is captured against it — but the same toolchain runs unmodified on:
- AMD Instinct MI355X / MI325X / MI300X / MI250X
- AMD Radeon R9700
- NVIDIA B200 / H200 / H100 (SXM, PCIe) / L40S / A100 (80 / 40 GB) / V100
- CPU host (Linux, AMD or Intel) for CI / dev-laptop validation runs
setup.sh auto-detects the device, picks the matching PyTorch wheel
(ROCm / CUDA / CPU), and creates a per-device venv. The report renderer reads
the device profile out of configs/report_config.json so headlines, deltas,
and prose retitle themselves automatically — no MI355X-specific text appears
when the run lands on a different target.
The full methodology lives in TESTPLAN.md. The data-driven
report knobs (target registry, classification thresholds, glossary) live in
configs/report_config.json.
Wan2.2 (720p / real video DiT): this repo does not vendor upstream Wan
weights or generate.py, but you can run Wan-Video/Wan2.2
alongside the same hardware benchmark (ceilings + comm) and optionally add a
workload JSON that approximates token geometry. See
docs/WAN2.2.md for install links, Hugging Face checkpoints,
--size 1280*720 / --convert_model_dtype, and how bench04/bench05 relate
to the real Wan graph.
setup.sh one-shot env provisioner (CPU/CUDA/ROCm; auto-detects)
test.sh top-level test runner (registry of testcase × workload)
run.sh per-job runner; system probe, profiling, iterations, parsing
report.py thin shortcut → scripts/report.py (auto-picks latest benchmark)
benchmarks/
common/ timing, env capture, stats, IO, FLOP/byte accounting,
telemetry (SMI poller), topology (CCD/socket/NUMA helpers)
bench01_bf16_compute.py Family 1 — BF16 GEMM peak, size sweep, per-component GEMMs, BF16-vs-FP32 correctness
bench02_hbm_bandwidth.py Family 2 — HBM / DDR streaming microbenchmarks
bench03_dram_capacity.py Family 3 — practical allocatable memory (HBM on GPU, DDR on CPU); --measure-headroom for post-model residual capacity
bench04_workload_ops.py Family 4 — escher_14b_480p per-op decomposition (theory vs default vs optimized)
bench05_e2e_mfu.py Family 5 — eager vs torch.compile end-to-end MFU + per-chunk stability
bench12_multigpu_comm.py Family 6 — all-gather / reduce-scatter / all-reduce (torchrun)
bench06_fused.py Family 6 — compatibility shim to bench06_aiter_fused.py
bench06_aiter_fused.py Family 6 — AITER fused AG+MM / MM+RS availability + speedup probe
(probes upstream aiter.* first, then aiter.ops.triton.comms.fused.*,
finally falls through to the vendored AITER-conformant kernels in
benchmarks/aiter_kernels/)
aiter_kernels/ New AITER-style fused collective+GEMM kernels (AG+MM and MM+RS) —
SymmMem-compatible API, Iris-aware Triton kernels (with staged
fallback), experimental HipKittens/Iris CDNA4 native kernels,
per-arch JSON tile configs (gfx950/gfx942), pure-Torch reference,
op-tests. See docs/AITER_FUSED_KERNELS.md for user-facing usage /
tuning / troubleshooting and benchmarks/aiter_kernels/README.md for
the kernel-design review + upstream-to-aiter/ops/triton/comms/fused/
path. Dispatcher selection:
aiter.upstream → aiter.ops.triton.comms.fused →
benchmarks.aiter_kernels.hipkittens →
benchmarks.aiter_kernels.triton → torch.ops.symm_mem → pure-Torch fallback.
bench10_symm_fused.py Family 10 — torch SymmMem fused AG+MM / MM+RS probe + correctness gate
bench07_sustained.py Family 7 — sustained-throughput / thermal-drift probe with paired SMI telemetry
bench08_topology_bw.py Family 8 — all-pairs device-to-device (or inter-CCD/inter-socket) bandwidth matrix
bench09_numerical_stability.py Family 9 — full per-(dtype, K) GEMM error distribution vs FP32 reference
configs/
escher_14b_480p.json workload shape spec + source-pilot reference numbers
(peak TFLOPs, ICI BW, MFU targets) — single source of truth for "the PDF says X"
report_config.json data-driven report knobs: target registry (AMD/NVIDIA/CPU specs),
classification thresholds, status-pill mapping, glossary, project metadata
reference_video_models.json optional registry for video foundation models (ids,
optional ``huggingface_id``, optional ``workload_config``).
For ``testcase=benchmark``, ``REFERENCE_MODEL=<id>`` in ``run.sh``
selects ``workload_config`` when present, writes
``results/<out>/benchmark_meta.json`` (``reference_model``, metadata),
and lets ``scripts/report.py`` resolve the Hub repo for the cover +
**Model Description** section. List ids: ``python scripts/report.py
--list-reference-models`` (optional ``--reference-models-config``).
validation/
rvs/ ROCm Validation Suite configs (gst, mem, pebb) + runner
rocm_bw/ rocm-bandwidth-test runner (HBM ground truth)
rccl/ rccl-tests runner + parser (RCCL ground truth)
compare.py PyTorch vs RVS vs rocm-bandwidth-test vs rccl-tests
scripts/
run_benchmark.sh orchestrator (TESTPLAN §15 sequence)
strong_scaling.sh sweep WORLD ∈ {2,4,8} -> TP-3 strong-scaling table (§16.3)
strong_scaling_table.py aggregator: per-world busbw / efficiency / MFU table
across_run_variability.py inter-run CV%% on the headline metrics (feeds SC-8)
plot_results.py charts A2/A3/A6/A7/A8 from TESTPLAN §13
score_benchmark.py SC-1…SC-12 pass/fail scorecard
report.py data-driven report.md / report.html / report.pdf (cover +
Model Description may embed Hugging Face README when resolvable)
results/ per-benchmark output directories (override via $RESULTS_DIR)
# 1. One-time setup: auto-detects rocm / cuda / cpu, creates a venv at
# .microbenchmarks-<device>-venv/, installs the matching PyTorch wheel
# (and torchvision), probes optional backends (AITER, Iris, HipKittens,
# flash_attn, triton)
# on GPU hosts, and reports calibration drift against TESTPLAN §1.4.
./setup.sh
source .microbenchmarks-rocm-venv/bin/activate # or -cuda-venv / -cpu-venv
# Optional: build the experimental CDNA4 HipKittens/Iris fused backend.
HIPKITTENS_BUILD_FUSED=1 ./setup.sh
# 2. Run a full single-node 8-GPU benchmark
./test.sh -t benchmark
# 3. Inspect the latest benchmark (dirs are <model>-YYYYMMDD-HHMMSS; legacy
# runs may still be *-benchmark-*)
LATEST=$(ls -1td results/*/ 2>/dev/null | while read -r d; do
[[ -f "${d}env.json" ]] || continue
b=$(basename "$d")
[[ "$b" == *-benchmark-* || "$b" =~ -[0-9]{8}-[0-9]{6}$ ]] && echo "$d"
done | head -1)
cat $LATEST/summary.md
cat $LATEST/scorecard.md
cat $LATEST/validation.md
# 4. Open the auto-generated report (data-driven, mirrors the source PDF).
# `./report.py` with no args picks the most recent full benchmark directory
# and emits report.md, report.html and report.pdf side-by-side.
./report.py
xdg-open $LATEST/report.pdfThe report is regenerable on demand from the JSON outputs alone:
./report.py # auto-pick latest benchmark dir; emits md+html+pdf
./report.py --out results/<id>/ # explicit dir
./report.py --out results/<id>/ --format md # md only
./report.py --out results/<id>/ --format html # html only
./report.py --out results/<id>/ --format pdf # pdf only
./report.py --out results/<id>/ --no-pdf # md+html, skip pdf
./report.py --out results/<id>/ --no-embed # link plots/ instead of base64
./report.py --out results/<id>/ --target h100 # force the device profile
# (mi355x | mi325x | mi300x | mi250x |
# r9700 | b200 | h200 | h100 | l40s |
# a100 | v100, or any registry pattern)
./report.py --out results/<id>/ --report-config configs/report_config.jsonPDF generation prefers wkhtmltopdf (clean HTML→PDF), falls back to pandoc
with a LaTeX engine, and finally to a Markdown→PDF pandoc path. For full PDF
formatting—including the [AMD Confidential - Distribution Under NDA] running
header and page-numbered footers—a patched Qt engine is required. Install the
static wkhtmltox binary from the official releases rather than using the
unpatched apt install wkhtmltopdf.
The generated report.md includes Obsidian-compliant YAML frontmatter (author,
sensitivity, title) for easy knowledge base integration.
configs/report_config.json is the single tunable surface for the
report itself: the target-hardware registry (rated peaks, HBM size, vendor),
classification thresholds (e.g. meas_over_theory_at_hw_limit = 1.10,
meas_over_theory_tunable = 1.50, mfu_pdf_tolerance_pp = 5.0,
calibration_drift_pct = 5.0), status-pill CSS mapping, glossary, and
project metadata all live there. Re-tune any of those without touching code,
or pass --report-config <path> to swap in a benchmark-specific override.
Hugging Face on the report: scripts/report.py resolves a repo id from
the workload JSON (huggingface_id / huggingface_model_id), from
benchmark_meta.json + reference_video_models.json, or when the workload
name matches a registry id. 0. Cover can embed README.md (model
card) and adds a Hugging Face table row. Model Description (instrumented
DiT parameters + shapes + op mix) may prepend an Architecture from Hugging
Face model card subsection: filtered ## sections from that README (license
/ citation blocks skipped). Fetching the README needs outbound HTTPS when the
report is built; if it fails, the cover still links to the Hub.
Source-pilot reference values (peak TFLOPs, ICI ring BW, MFU targets
77 / 93 / 99 %) live next to the workload spec in the
source_pilot_reference block of configs/escher_14b_480p.json. bench05
reads MFU targets from there, and the report reads everything else from
there, so any line in the artifacts that says "the source PDF says X"
traces back to that one block.
setup.sh auto-detects the device (rocm/cuda/cpu) and creates a
correspondingly-named venv. To force a specific stack:
DEVICE=rocm TORCH_INDEX_URL=https://download.pytorch.org/whl/rocm7.2 ./setup.sh
DEVICE=cuda ./setup.sh # default torch+torchvision channel
DEVICE=cpu ./setup.sh # https://download.pytorch.org/whl/cpuEach family can also be run individually (substitute your benchmark id):
# Core 1..6 — wired into test.sh / run.sh / run_benchmark.sh
python -m benchmarks.bench01_bf16_compute --out results/$BENCHMARK_ID/
python -m benchmarks.bench02_hbm_bandwidth --out results/$BENCHMARK_ID/
python -m benchmarks.bench03_dram_capacity --out results/$BENCHMARK_ID/
python -m benchmarks.bench04_workload_ops --out results/$BENCHMARK_ID/ --config configs/escher_14b_480p.json
python -m benchmarks.bench05_e2e_mfu --out results/$BENCHMARK_ID/ --config configs/escher_14b_480p.json
torchrun --nproc_per_node=8 benchmarks/bench12_multigpu_comm.py --out results/$BENCHMARK_ID/
# Optional fused/sustained/topology/stability probes invoked directly when needed
torchrun --nproc_per_node=8 benchmarks/bench06_aiter_fused.py --out results/$BENCHMARK_ID/ # AITER AG+MM / MM+RS availability + speedup
torchrun --nproc_per_node=8 benchmarks/bench10_symm_fused.py --out results/$BENCHMARK_ID/ # torch SymmMem AG+MM / MM+RS probe + fallback correctness check
# Standalone correctness gate for the new AITER-style fused kernels (bench06's probe target):
torchrun --nproc_per_node=2 -m benchmarks.aiter_kernels.op_tests.test_fused_collective # asserts every available backend (aiter / local triton / symm_mem) matches the pure-Torch reference
python -m benchmarks.bench07_sustained --out results/$BENCHMARK_ID/ --duration 1800 # 30-min sustained throughput + telemetry
python -m benchmarks.bench08_topology_bw --out results/$BENCHMARK_ID/ # all-pairs D2D / inter-CCD BW matrix
python -m benchmarks.bench09_numerical_stability --out results/$BENCHMARK_ID/ # per-(dtype, K) GEMM error distributiontest.sh is the GuideLLM-style top-level entrypoint that wraps the same
scripts with named testcases and workload variants. It does not run the
benchmarks itself — it enumerates (testcase × workload) combinations and
delegates each to run.sh, which is the per-job runner. The two-script split
mirrors the Partition project layout:
| Script | Role |
|---|---|
test.sh |
Registry of testcases + workloads, argv parser, batch driver. Calls run.sh once per (testcase, workload) combination. |
run.sh |
Single-job runner. Probes the system (amd-smi partition mode, GPU count), materializes a derived config when shape/depth overrides are set, dispatches to the right bench0X / validator / report script, runs ITERATIONS iterations, parses each iteration's JSON outputs into a one-line RESULT: record, and at the end aggregates mean / median across iterations. |
./test.sh --list # registered testcases & workloads
./test.sh -t compute # only Family 1 (BF16 GEMM)
./test.sh -t e2e -w smoke # quick MFU sanity run
./test.sh -t benchmark -i 3 # full benchmark 3× (regression averaging)
./test.sh -a # every testcase × every workloadrun.sh can also be invoked directly when you already know the exact
parameters and don't want the test.sh registry layer:
./run.sh --testcase compute --iterations 5
./run.sh --testcase e2e --workload smoke --iterations 3
./run.sh --testcase multigpu --nproc 8 --iterations 3
./run.sh --testcase benchmark --benchmark-id $(date +%Y%m%d-%H%M%S)Each run.sh invocation tees its full console output to
results/_logs/<testcase>-<workload>-px<P>-nps<M>-g<GPUS>-tp<NPROC>.<ts>.log and
appends one RESULT: line per iteration to results/<out-id>/iterations.results.
When --iterations > 1 the aggregate block at the end prints
mean / median / values=(...) for each headline metric of the chosen testcase
(e.g. peak_tflops for compute, mfu_eager/mfu_compiled for e2e,
all_reduce_busbw_gb_s for multigpu).
run.sh ships with Phoronix-style profiling hooks that capture the host /
GPU state for every job. The behaviour mirrors what turbostat does for CPU
work, but uses the GPU vendor's SMI tool:
| Flag | What it does |
|---|---|
--prepare-sys |
Run kernel/VM tuning (numa_balancing=0, transparent hugepages always, vm.compact_memory=1, drop_caches=3, rocm-smi --setperflevel high). Requires sudo; failures are non-fatal so unprivileged hosts keep going. Off by default. |
--no-stat / STAT=off |
Disable the live telemetry poller. Default: on. |
--stat-interval N |
Poller interval in seconds. Default: 1. |
--numactl "<spec>" |
Wrap the per-iteration command with numactl <spec> (only honored for non-distributed testcases; torchrun jobs handle their own affinity). Example: --numactl "--physcpubind=0-15 --localalloc". |
The poller auto-selects in this order: amd-smi metric --watch, then a
rocm-smi --showpower --showtemp --showuse --showmemuse --showclocks --csv
loop, then nvidia-smi --query-gpu=..., then disabled. Output goes to a
sidecar file paired with the main log:
results/_logs/<testcase>-<workload>-px<P>-nps<M>-g<G>-tp<N>.<ts>.log # console
results/_logs/<testcase>-<workload>-px<P>-nps<M>-g<G>-tp<N>.<ts>.stat.log # GPU telemetry
A one-shot system fingerprint (uname, lscpu, rocminfo, rocm-smi,
amd-smi static, /sys knobs, relevant env vars, key pip packages) is
written to the main log at the start of every run, so a single .log is
enough to reproduce the hardware/software baseline of any reported number.
The benchmark is fully runnable on a CPU-only host. Every bench0X script
detects the device internally and produces real measured numbers under the
same JSON schema as the GPU path, so report.py and score_benchmark.py can
diff CPU and GPU runs against each other without special-casing. On a CPU
host:
./setup.shinstallstorch+torchvisionfrom the CPU wheel index and creates.microbenchmarks-cpu-venv/../test.sh -t benchmarkruns the core six families end-to-end:bench01..03measure CPU BF16 GEMM peak, system DDR bandwidth, andpsutil-bounded DRAM capacity.bench04analytically accounts every op and selectively measures sub---cpu-budget-gflopsops (default 5 GFLOP); heavy GEMMs / attention are intentionallyNaNto keep the run tractable.bench05auto-downscalesdepth/seq_image/seq_text, runs eager-only (notorch.compile), and suppresses sum-of-ops MFU when bench04's measurement coverage falls below 95%.bench06is the CPU multi-CCD / multi-socket path: gloo over loopback with one rank per Linuxdie_id(Infinity Fabric boundary) andos.sched_setaffinitypinning. World size is auto-set tomax(#CCDs, #sockets). Override withWORLD=N CPU_TOPOLOGY=ccd|socket|split|autoor./run.sh --testcase multigpu --cpu-topology socket.
- The opt-in probes (
bench06_aiter_fused,bench10_symm_fused,bench07_sustained,bench08_topology_bw,bench09_numerical_stability) also run on a CPU host: the fused-kernel probe always reportsnot available(which is itself the report-relevant data point), the sustained probe drives the CPU FFN forward instead of the DiT, and the topology probe falls back to a CCD/socket bandwidth matrix. Wire them in as needed; they are not part of the default-t benchmarkflow. scorecard.mdshows aHOST CPUrow at the top. The CPU host treats GPU-bound criteria asSKIPrather thanFAIL:SC-1drops the absolute rated-peak TFLOP/s target (no GPU to compare against) but still enforces thebest_sweep ≥ 0.9 × measured peakconsistency check.SC-2widens the bandwidth-spread threshold to 15 % to match DDR variance.SC-3(op taxonomy) is graded normally.SC-4SKIPs when bench05's MFU rows are incomplete (notorch.compileon CPU).SC-5(artifact set) is graded normally — the*.pdfand*.htmlare required outputs even on CPU.SC-6(BF16-vs-FP32 numerical correctness) is graded normally.SC-7..SC-9are opt-in:SKIPuntil the matching probe runs.SC-10(numerical-stability sweep) graded if bench09 ran.SC-11(post-model-load residual capacity) emitsWARN_CPUwhen the DiT does not fit in host RAM — informational, not a failure.SC-12(fused AG+MM / MM+RS) isSKIPon CPU. On GPU it grades toPASS/FAILrather thanSKIP: the dispatcher inbenchmarks/aiter_kernels/always resolves a vendored Triton backend on a CUDA host with triton, so the speedup ratio over the sequential reference can always be measured. Seedocs/AITER_FUSED_KERNELS.md.
- The report retitles itself "CPU host benchmark report", swaps
"HBM bandwidth" → "Memory bandwidth", compares DRAM against host RAM,
and renames the multi-GPU section to "Multi-CCD / Multi-Socket
Communication" with a topology-pinning table. No MI355X-specific prose
appears unless the run actually lands on an MI355X — the device profile
is looked up dynamically from
configs/report_config.json's target registry.
The benchmark's exit code is 0 on a CPU host: the per-SC details are
recorded in scorecard.{md,json} for inspection, but absolute thresholds
against the GPU target are not enforced as pass/fail.
Per-benchmark artifacts land under results/<benchmark-id>/ and the
per-iteration logs under results/_logs/. Override the root with
$RESULTS_DIR:
RESULTS_DIR=/data/odyssey-mi355x ./test.sh -t benchmark
./report.py # honors $RESULTS_DIR./report.py falls back to <repo>/runs/ when <repo>/results/ is
absent, so existing working trees from before the rename keep working
without manual migration.
External validators are shelled out, not vendored. Install separately:
| Tool | Provides | Install |
|---|---|---|
ROCm Validation Suite (rvs) |
BF16 peak (gst), memory integrity (mem), PCIe BW (pebb) |
shipped with ROCm |
rocm-bandwidth-test |
HBM D2D / H2D / D2H bandwidth ground truth | shipped with ROCm |
rccl-tests |
RCCL all-reduce / all-gather / reduce-scatter ground truth | https://github.com/ROCm/rccl-tests |
Cross-validation is run by validation/compare.py, which loads the JSON outputs from
each family and from each external tool, then writes results/<id>/validation.md
flagging any disagreement larger than the per-metric tolerance.
A benchmark signs off when all gating SCs (SC-1 … SC-6) in
TESTPLAN.md §1.2 hold AND the cross-validation report
shows no FAIL rows. SC-7 … SC-12 are opt-in / informational:
| SC | Role | When it must PASS |
|---|---|---|
| SC-1 | BF16 compute peak vs measured + rated | gating |
| SC-2 | HBM / memory plateau stability | gating |
| SC-3 | Roofline placement (compute vs memory bound) | gating |
| SC-4 | MFU ordering: compiled ≥ eager ≥ sum-of-ops (±5 pp) | gating |
| SC-5 | All required artifacts (incl. report.pdf) exist |
gating |
| SC-6 | BF16 vs FP32 numerical correctness | gating |
| SC-7 | Sustained throughput stable over bench07 window |
opt-in (skips if not run) |
| SC-8 | Cross-run CV %% on headline metrics ≤ threshold | opt-in (needs across_run_variability.py) |
| SC-9 | RVS / rocm-bandwidth-test / rccl-tests agreement |
opt-in (skips if external tools absent) |
| SC-10 | bench09 per-(dtype, K) error distribution within bound |
opt-in |
| SC-11 | bench03 --measure-headroom post-model-load residual |
opt-in (WARN_CPU on host) |
| SC-12 | Fused AG+MM / MM+RS kernels available + faster than unfused — sourced from upstream AITER, the canonical aiter.ops.triton.comms.fused.* path, the vendored benchmarks.aiter_kernels Triton backend, or torch.ops.symm_mem (in priority order) |
opt-in — SKIP only on CPU and on hosts with no triton + no SymmMem; otherwise grades PASS / FAIL |
The orchestrator exits non-zero only when a gating SC fails or
compare.py flags a FAIL, so this can run in CI as a regression gate
without forcing the opt-in probes to be wired in everywhere.