aorta

Triage recipes

Renamed (issue #248): aorta triage and aorta probe are now the single command aorta sweep. Use aorta sweep run everywhere this guide says aorta triage run, and aorta sweep run ... -- <cmd> for the subprocess (former aorta probe) flow. The old commands still work as deprecated aliases that print a notice and delegate to the same engine.

A triage recipe is the authoritative description of an aorta sweep run --mode matrix invocation: which (mitigation x environment) cells to run, per-cell trial / step counts, the ticket the matrix belongs to, and the speed-confound detection config.

Recipes are the primary interface. The --mode matrix flag shim is kept as an escape hatch for ad-hoc one-shots; internally it constructs an in-memory Recipe and reuses the same execution path.

Before you run

Commands in this repository assume the repository root unless stated otherwise. For installation and host/container ownership, see Where commands run and the selected workload’s guide.

Layout

Recipes are grouped by workload:

Folder Contents
training/ DDP / FSDP training smoke recipes.
inference/ Offline / continuous inference smoke recipes.
llm-determinism/ Bit-exact double-run determinism probe.
race/ RCCL race / SDC reproducers (incl. AINIC/Pollara GDR).
hrx/ HRX launch-probe and perf (GEMM / triad) recipes.
probe/ Subprocess-flow templates for wrapping opaque launch commands.
emulated/ Variants that run under the mirage GPU emulator (no real GPU).

Quick reference

schema_version: 1                    # required; loader rejects unknown versions
ticket: EXAMPLE-001                  # optional; drives output dir grouping
workload: training                   # required; resolved via aorta.workloads entry-point group
trials: 8                            # required; per-cell trial count
steps: 5000                          # required; per-cell step count
trial_isolation: auto                # optional: auto | in_process | process
save_logs: false                     # optional; when true, dispatcher writes per-trial stdout/stderr files
extra_env:                           # optional; applies to every cell
  GLOBAL_DEBUG_FLAG: "1"             # values must be strings

confound:
  threshold: 1.15                    # default; > 1.15 -> "speed (+N%)" flag
  baseline_cell: baseline-local      # optional; defaults to the first "baseline-*" cell
                                     # or the first cell with mitigations: [none]

cells:
  - name: baseline-local
    mitigations: [none]
    environment: local

  - name: tf32_off-local
    mitigations: [tf32_off]
    environment: local
    collect: []                      # optional per-cell override; disables collection here

  - name: stack-tf32-xnack-local     # mitigation stacking (env vars unioned in list order)
    mitigations: [tf32_off, xnack]
    environment: local
    trials: 16                       # optional per-cell override
    steps: 8000                      # optional per-cell override

  - name: custom-env-override        # one-off env var override for this cell only
    mitigations: [tf32_off]
    environment: local
    extra_env:
      MY_DEBUG_FLAG: "1"

Schema rules (full detail)

Trial isolation and environment timing

trial_isolation is a top-level triage field:

Use normal extra_env in process-isolated recipes even for import-time native settings. For example, race requires process isolation, so its AINIC cells can vary NCCL_NET_GDR_LEVEL, NCCL_GDR_FLUSH_DISABLE, NCCL_PROTO, and GPU_MAX_HW_QUEUES safely. In an in-process workload, values cached during module/shared-library import still need the launcher environment or an explicit trial_isolation: process recipe.

Launcher identity (RANK, WORLD_SIZE, LOCAL_RANK, MASTER_ADDR, MASTER_PORT) is never a cell axis and is rejected from a process-isolated controlled overlay.

Environment.env, mitigations, and recipe/cell extra_env keep their existing precedence; process isolation changes when the resolved overlay becomes visible, not how it is merged. For distributed details, see README-running-recipes.md.

Every validation error reports a path like cells[2].mitigations so the failure is localisable without reading the loader source.

The complete environment-variable precedence is:

Environment.env
< mitigations
< recipe-level extra_env
< cell-level extra_env

Direct aorta run --extra-env occupies the same highest-precedence request layer as the recipe runner’s merged recipe/cell values. The dispatcher applies the controlled overlay to host workloads and injects the exact same mapping as config["_aorta_trial_env"] for self-isolating wrappers. It never adds unrelated ambient host variables, and the core dispatcher does not execute docker run. Docker-aware wrappers can use aorta.run.docker_env_flags; see docs/configuration.md.

Workloads

The workload: name is resolved through the aorta.workloads entry-point group at cell-execution time, not recipe-load time. A recipe naming an unregistered workload loads and validates fine; it fails per-cell when the cell runs. So “the recipe is valid” does not imply “the workload exists” – confirm registration with a dry-run (or pip install -e . after adding a new entry-point).

race

Wraps the in-tree RCCL race reproducer (aorta.race). launch_mode: distributed, min_world_size: 2. The communication pattern is selected by the mode config key, not by a separate workload name:

workload: race
workload_config:
  mode: fsdp            # default | ddp | fsdp
  warmup_iterations: 0
  verify_iterations: 50
  dtype: bfloat16       # bfloat16 | float16 | float32

workload_config accepts any field of aorta.race.config.ReproducerConfig (e.g. h2d_tensor_size, fsdp_shard_size, gemm_layers, simulate_compute, h2d_prefetch, same_stream_mode, stop_on_first_corruption, log_interval).

Because race is launch_mode: distributed, it MUST be launched under torchrun (a bare aorta sweep run starts one process, WORLD_SIZE=1, and is refused by launch-mode validation). Use the aorta console script as torchrun’s target – -m aorta is not a runnable module:

# validate only (no GPUs / no launcher):
aorta sweep run --recipe recipes/training/example-fsdp-smoke.yaml --dry-run

# single node, 2 ranks (bump --nproc_per_node to your GPU count):
torchrun --standalone --nproc_per_node=2 $(which aorta) sweep run \
  --recipe recipes/training/example-fsdp-smoke.yaml

# multi-node, 1 rank per host (AINIC/Pollara -- 1 process owns all GPUs on the node):
torchrun --nnodes=N --nproc_per_node=1 --rdzv-backend=c10d \
  --rdzv-endpoint=$MASTER_ADDR:29500 $(which aorta) sweep run \
  --recipe recipes/training/example-fsdp-smoke.yaml

# multi-node, 1 rank per GPU (IB / NVLink / generic fabric):
torchrun --nnodes=N --nproc_per_node=8 --rdzv-backend=c10d \
  --rdzv-endpoint=$MASTER_ADDR:29500 $(which aorta) sweep run \
  --recipe recipes/training/example-fsdp-smoke.yaml

Each rank runs the full sweep run; the ranks find each other in dist.init_process_group(). The long-lived matrix parent on every rank starts a fresh worker for each Race trial; matching workers initialize the trial process group, synchronize their result, tear it down, and exit before the next trial starts. Results are written by rank 0 only, so multi-rank launches do not clobber each other. llm_determinism uses the same worker model only when its recipe requests trial_isolation: process.

[!IMPORTANT] Unknown workload_config keys are dropped with a WARNING, not errors. A key that isn’t a ReproducerConfig field is silently ignored at runtime – which for a stress recipe means a lever you thought you set never applied (a false green). Watch the run log for race: ignoring unknown workload_config key ... and fix the recipe. Note verify_iterations defaults to 10000 and simulate_compute to True; cap these for smoke runs or a trial takes hours.

Race smoke recipes

Recipe Purpose Fabric
recipes/race/race_smoke.yaml Fabric-agnostic sanity check (1 trial, 5 iters, model_dim=512). Works on NVLink, IB, AINIC, or SHM. Any
recipes/race/ainic-smoke.yaml Same but forces NCCL_NET_GDR_LEVEL=SYS + dmabuf + GDR read to validate the AINIC/Pollara GDR path specifically. AINIC only

Confirmed working on a single node with 8 GPUs (one rank per GPU):

# single node, 8 GPUs:
torchrun --standalone --nproc_per_node=8 $(which aorta) sweep run \
  --recipe recipes/race/race_smoke.yaml

A green run proves: compute_type=transformer, layers_verified > 0, layer_checksum_mismatches == 0, passed=true. Use recipes/race/ainic-gdr-flush-sdc.yaml for the full SDC triage matrix.

Output layout

<output-dir>/
  <ticket or _no_ticket_>/
    <workload>/
      <timestamp>[-N]/                          # e.g. 2026-04-28T14-12-03; -2, -3 ... on same-second collisions
        matrix.md
        matrix.json
        recipe.resolved.yaml                    # post-resolution snapshot
        host_env.json                           # collect_env() once per run
        environments/<env-name>/env.json        # once per unique environment (see "Environment snapshots")
        inline_environments.sidecar.json        # only when inline docker is used
        sidecars/<basename>                     # one copy per --mitigations-file
        cells/<cell-name>/<workload>/trial_*.json

matrix.json per-cell shape (the canonical machine-readable record):

Note on the trailing <workload>/ directory inside each cell. B1’s runner (aorta.run.run_trials) appends /<workload> to the output directory it was given. B2 honours that contract: each cell is told to write to cells/<cell-name>/, and B1 ends up writing cells/<cell-name>/<workload>/trial_N.json. matrix.json records the real paths; a future B1 follow-up can drop this level of nesting via a skip_workload_subdir kwarg on RunRequest.

Environment snapshots

Every run records the environment each cell ran in under environments/<env-name>/env.json, once per unique environment:

Wrapper contract for in-container snapshots

For isolated envs the dispatcher injects a reserved config["_aorta_env_probe"] dict with two keys:

A self-isolating wrapper that invokes docker run opts in by reading the reserved config key and running the probe as the first step inside the container. There are no AORTA_PROBE_SRC / AORTA_ENV_OUT environment variables – the paths arrive only through config["_aorta_env_probe"], and the wrapper is responsible for turning them into bind-mounts on the docker run it builds:

# inside the wrapper's run(), before building argv:
probe = self.config.get("_aorta_env_probe")   # None for non-isolated envs
mounts, probe_prefix = [], ""
if probe is not None:
    src, out = probe["src"], probe["out"]      # host paths from the runner
    out_dir = os.path.dirname(out)
    mounts = ["-v", f"{src}:/opt/aorta_src:ro", "-v", f"{out_dir}:/aorta_out"]
    probe_prefix = (
        "PYTHONPATH=/opt/aorta_src python -m aorta.instrumentation._probe_main "
        f"/aorta_out/{os.path.basename(out)} && "
    )

argv = [
    "docker", "run", *mounts, image,
    "bash", "-c", f"{probe_prefix}{workload_cmd}",
]

aorta.instrumentation._probe_main imports only stdlib plus the collect_env() module (no Click), so it runs under a bare container Python with no pip install. It writes exactly the same env.json shape as aorta env probe -o, so a promoted in-container snapshot is indistinguishable on disk from a local-env one.

Probing is per unique environment, not per cell: the first cell of each isolated env requests a probe, and subsequent cells reusing that env keep requesting one until a valid snapshot is captured (so a cell whose container fails to start doesn’t permanently lose the snapshot). Once captured, later cells stop requesting it. An env that never produces a valid snapshot gets the placeholder at the end of the run.

Re-running a past matrix

Every run writes recipe.resolved.yaml alongside the matrix. The file is a strict, schema-valid recipe – you can pass it back to aorta sweep run --recipe ... directly. Inline-docker cells are re-emitted in the { docker: <ref> } shorthand so the same _inline_<hash> is re-derived without needing to ship a sidecar JSON next to the file.

For runs that used --mitigations-file, the resolved YAML still references those mitigation / environment names by name. The runner snapshots each operator-supplied sidecar into <run_dir>/sidecars/<basename> so the run directory is self-contained for replay. The runner also prints the exact rerun command on stdout when sidecars are involved, e.g.:

cd <run_dir> && aorta sweep run --recipe recipe.resolved.yaml \
  --mitigations-file sidecars/foo.json

The per-cell mitigation env-var bundles AS APPLIED, plus the resolved Environment descriptor for each cell, live in matrix.json (under each cell’s resolved_env_vars and resolved_environment keys) – not in recipe.resolved.yaml – so the rerun artifact stays loadable while audit data is still preserved next to the run.

[!NOTE] “Reproducing the same matrix” is up to the registries available at rerun time. If a sidecar mitigation’s env-var bundle drifts between runs the rerun will use the new bundle (snapshotting the file doesn’t pin its contents once the operator edits it later). Inline-docker cells are immune (the docker ref is in the recipe text itself), but registry drift on named entries is currently not pinned. Compare each run’s matrix.json::cells[*].resolved_env_vars to detect drift.

Flag mode (escape hatch)

Example flag-mode CLI:

aorta sweep run --mode matrix \
  --workload training \
  --mitigation-axis none,tf32_off,xnack \
  --environment-axis local \
  --trials 2 --steps 100 \
  --ticket EXAMPLE-151

Inline docker still works in flag mode via the image: prefix on the axis, e.g. --environment-axis local,image:rocm/pytorch:nightly. Each comma-separated item is parsed independently; bare names go through the registry, image:<ref> maps to the same { docker: <ref> } shorthand as recipe mode. The value remains workload metadata; a Docker-aware plugin must consume it.

Probe handout templates (issue #188 Phase 3)

Generic mode: probe recipes for customer handouts. Each template includes a redaction: block consumed by aorta bundle when packaging artifacts for sharing.

Templates live under recipes/probe/.

Template Typical trailing argv
recipes/probe/probe-template-torchrun.yaml torchrun --nproc_per_node=N train.py ...
recipes/probe/probe-template-buck2.yaml buck2 run //path:target -- ...
recipes/probe/probe-template-bash.yaml bash launch.sh ...

Dry-run smoke:

aorta sweep run --recipe recipes/probe/probe-template-bash.yaml --dry-run -- echo hi

See docs/probe/handout-templates.md for per-template walkthroughs.