madengine — Codebase Wiki

AI/ML model automation & benchmarking platform for local Docker, bare-metal conda, SLURM (and Spur), Kubernetes, and llm-d. A Typer-based CLI that discovers model cards, builds Docker images (or conda environments), runs them on a compute target, and writes structured performance results.

Entry point: src/madengine/cli/app.py::cli_main → console script madengine registered in pyproject.toml.

Reflects v2.4.0 + unreleased (develop) Python ≥ 3.8 7 commands Docker · bare-metal · SLURM/Spur · K8s · llm-d Typer + Rich + Hydra ROCm & CUDA Jinja2 templates

Version is derived from git tags at build time (versioningit); run madengine --version for the installed one. Where this page and the code disagree, the code wins — each section names the file it was derived from.

Overview

What madengine does

  1. Discover — finds model cards from models.json or dynamic get_models_json.py scripts and resolves tags (discover).
  2. Build — builds a Docker image per card (or adopts a pre-built one, or prepares a conda prefix for bare-metal) and writes build_manifest.json (build).
  3. Run — reads the manifest, infers the target from the context, runs the workload, and writes perf.csv (run, targets).
  4. Report & operate — HTML/email/TraceLens reports, MongoDB upload, orphaned-container cleanup, and a static model-picker page (report, cleanup / setup-page).

Build and run share one configuration spine — the additional_context dict — supplied as --additional-context, --additional-context-file, or a Hydra-composed --config (see additional_context and --config).

Execution targets at a glance

Selected byTargetSection
(nothing below)Local DockerPlatforms
"platform": "bare_metal"Local conda/mamba prefix, no containerPlatforms
"slurm" keySLURM sbatch (or Spur with scheduler: spur)SLURM
"k8s" / "kubernetes" keyKubernetes JobKubernetes
"llm_d" keyllm-d benchmark (K8s Job)llm-d

Exact precedence and edge cases (including slurm_multi) are in Deployment target inference.

Where to start

Using madengine

Quick start → run options → config recipes → exit codes for CI.

Writing a model card

Model definition → manifest → profiling tools → reporting.

Changing madengine

Layers → data flows → module reference → tests → contributing.

Quick start

Run these from a MAD-style project directory (one that contains models.json and/or scripts/). Every flag and key below is documented in the CLI and configuration sections.

# 1. Install (from the madengine repo checkout)
pip install -e .

# 2. List the cards a tag selects
madengine discover --tags dummy

# 3. Build and run in one command
madengine run --tags dummy \
  --additional-context '{"gpu_vendor":"AMD","guest_os":"UBUNTU"}'

# 4. Or build first (optionally push), then run from the manifest
madengine build --tags dummy --registry registry.example.com/ml
madengine run --manifest-file build_manifest.json \
  --additional-context '{"docker_gpus":"0,1,2,3"}'

No slurm, k8s, kubernetes or llm_d key and no platform → local Docker via ContainerRunner. gpu_vendor / guest_os default to AMD / UBUNTU when omitted.

madengine run --tags my_model --live-output \
  --additional-context '{
    "platform": "bare_metal",
    "conda": {"conda_bin": "/path/to/micromamba"}
  }'

Runs inside a conda/mamba/micromamba prefix on this machine; cannot be combined with slurm, k8s, kubernetes or llm_d. See Platforms.

madengine build --tags llama3 --registry registry.example.com/ml

# Multi-node torchrun via sbatch
madengine run --manifest-file build_manifest.json \
  --additional-context '{
    "slurm": {"partition":"gpu","nodes":4,"gpus_per_node":8,"time":"24:00:00"},
    "distributed": {"launcher":"torchrun","nnodes":4,"nproc_per_node":8}
  }'

# A card that drives its own per-node containers (slurm_multi):
# skip the local build; "auto" reads DOCKER_IMAGE_NAME from the card's env_vars
madengine build --tags pyt_sglang_disagg --use-image auto

A slurm key selects SlurmDeployment; add "scheduler": "spur" inside it for Spur. Launcher names are validated up front (unknown names exit 4). See SLURM, slurm_multi, launchers.

madengine run --tags llama3 \
  --additional-context '{"k8s":{"namespace":"ml-team","gpu_count":4}}'

A k8s (or kubernetes) key selects KubernetesDeployment; preset defaults are merged under your keys. The kubernetes Python client is installed with madengine. See Kubernetes for the sub-key table, storage layouts and secrets.

# Attach mode: benchmark an llm-d stack that already exists
madengine run --tags dummy_llm_d --additional-context '{
  "k8s":   {"namespace":"llm-d-bench","gpu_count":0},
  "llm_d": {
    "endpoint_url":"http://llm-d-inference-gateway.llm-d-bench.svc.cluster.local:80",
    "model":{"name":"Qwen/Qwen3-32B"}
  }
}'

An llm_d key wins over k8s. Without endpoint_url, madengine runs in managed mode and installs and removes helm releases itself. See llm-d.

# Context file (JSON object)
madengine run --tags llama3 --additional-context-file my_run.json

# Hydra / YAML frontend (same keys as the context, plus config groups)
madengine run --config my_job.yaml
madengine run --config my_job.yaml --config distributed.nnodes=4
madengine run --config my_job.yaml --config +env=nccl_debug

--additional-context is parsed as JSON first, then as a Python dict literal. --additional-context-file is merged under it (the inline string wins). --config cannot be combined with either. See CLI overview and --config.

Install & dev

Setup

git clone https://github.com/ROCm/madengine.git
cd madengine
python3 -m venv venv && source venv/bin/activate

# Editable install: ALL runtime and dev dependencies come with it
pip install -e .

# Optional: host-side TraceLens analysis (see "madengine report tracelens")
pip install -e '.[tracelens]'

pre-commit install
madengine --version

Run madengine from the root of a MAD-style model repository (the directory holding models.json and scripts/), or point MODEL_DIR at it (see Environment variables).

Requirements

  • Python ≥ 3.8 (requires-python; Black targets py38–py311, mypy checks as 3.8).
  • Docker with GPU access (ROCm: /dev/kfd + /dev/dri; NVIDIA: container toolkit) for the default container platform.
  • conda / mamba / micromamba instead of Docker for "platform": "bare_metal" (see Platforms).
  • Per target: SLURM (or Spur) CLI on the submit host; kubectl and a kubeconfig for Kubernetes; helm for managed llm-d stacks.

Dependencies and extras

Everything is declared in pyproject.toml under [project] dependencies, including the Kubernetes client, Hydra, MongoDB, and the dev toolchain. There is exactly one optional extra:

InstallAdds
pip install -e .pandas, typer, click, rich, tqdm, typing-extensions, jinja2, pyyaml, hydra-core, omegaconf, kubernetes, pymongo, paramiko, sqlalchemy, GitPython, jsondiff, toml, plus pytest (-cov, -xdist, -timeout, -mock, -asyncio), black, flake8, mypy, isort, pre-commit
pip install -e '.[tracelens]'TraceLens pinned to a git commit. Kept optional because it pins protobuf>=6.31 and xprof, which can conflict with other packages in a shared environment. Needed only for madengine report tracelens; the in-container tracelens tool installs into its own venv instead.
No [dev], [kubernetes] or [all] extras. They were removed when those dependencies moved into the base list; pip install -e ".[dev]" only produces a pip warning about an unknown extra. The wheel ships scripts/** and configs/** (Hydra groups for --config) via [tool.hatch.build.targets.wheel] artifacts.

Versioning

The version is not stored in the source tree. versioningit derives it from git tags matching v<version> (default tag v2.0.0 when none exists). Untagged commits become <base>.post<distance>+g<sha>; a dirty tree adds a build date (<base>+d<YYYYMMDD> on a tag, …+g<sha>.d<YYYYMMDD> after it). madengine --version and madengine.__version__ read the installed package metadata, so an uninstalled checkout reports unknown / dev.

Test

pytest                           # testpaths: tests/unit, integration, e2e
pytest tests/unit -v             # fast, mocked
pytest tests/unit/test_slurm_multi.py -v
pytest -k llm_d                  # name filter
pytest -m "not slow"             # skip slow tests
pytest -m "amd or cpu"           # hardware markers
pytest --cov=src/madengine --cov-report=html

addopts = -v --tb=short -ra --strict-markers -W default, so an unregistered marker is a collection error. Markers are applied per test, not per directory: -m unit selects only tests tagged unit; use a path to run a whole directory. See Test layout.

Quality

black src/ tests/
isort src/ tests/
flake8 src/ tests/
mypy src/madengine
pre-commit run --all-files

The pre-commit hooks are black, isort, flake8, mypy (src/madengine/ only), bandit, and file hygiene checks. See Contributing.

Layered architecture

Each layer calls only the layers below it. Layers are color-coded throughout this page.

LayerPathResponsibilities
CLI src/madengine/cli/, config/, configs/, setup_page/ Typer app with 7 commands, argument validation, Rich output, exit-code mapping, signal-handler install. The --config Hydra frontend (config/ + YAML groups in configs/) translates into additional_context. setup_page/ renders the model-picker HTML.
Orchestration src/madengine/orchestration/ BuildOrchestrator (discover → build → manifest) and RunOrchestrator (manifest → target inference → dispatch), GPU-arch/vendor image filtering, and the Docker-vs-bare-metal platform decision.
Deployment src/madengine/deployment/ DeploymentFactory + template-method BaseDeployment; SLURM, Spur, Kubernetes, and llm-d backends; presets, Jinja2 templates, layered config, K8s slurm_multi shims, monitoring and result collection.
Execution src/madengine/execution/ Local ContainerRunner (docker run, tools, log scanning, timeouts), BareMetalRunner + conda env management, DockerBuilder, run-result reporting.
Core src/madengine/core/ Context merging & GPU detection, shell Console, Docker wrapper, error hierarchy, registry auth, timeouts, data providers, image digests, and the container lifecycle (heartbeat watchdog, signal-handler teardown).
Utils src/madengine/utils/ Model discovery, GPU tool abstraction (AMD/NVIDIA), ROCm path resolution, GPU memory gate, config parsing, session tracking.
Reporting src/madengine/reporting/, database/ perf.csv / perf_entry.csv / perf_super.* writers, HTML/email/TraceLens reports, MongoDB upload.

Packaged assets live beside the code: src/madengine/scripts/ (pre/post scripts, tools, SLURM/K8s helper scripts) is copied into a project's scripts/common/ for a run and removed afterwards. See Module reference.

Architecture diagram

madengine architecture CLI on top of orchestration, which dispatches to five execution targets: local Docker, bare-metal conda, SLURM or Spur, Kubernetes, and llm-d. Core and utils support them; reporting and database sit at the bottom. CLI · Typer + Rich · --config (Hydra) discover · build · run · report · database · cleanup · setup-page ExitCode: 0 OK · 1 FAILURE · 2 BUILD · 3 RUN · 4 INVALID_ARGS · 5 GPU_WEDGED Orchestration BuildOrchestrator DiscoverModels → DockerBuilder / conda → build_manifest.json RunOrchestrator load manifest → merge context → infer target → dispatch image_filtering · platform GPU arch / vendor docker vs bare_metal Execution & deployment targets (inferred from context keys) default → local · platform=bare_metal · "slurm" (+scheduler: spur) · "k8s" / "kubernetes" · "llm_d" (checked before k8s) ContainerRunner local docker run BareMetalRunner conda / mamba prefix Slurm / SpurDeployment sbatch · job array KubernetesDeployment Jobs · PVCs · slurm_multi shim LlmdDeployment helm stack · benchmark Job Core Context · Console · Docker · errors · auth · lifecycle · data providers Utils DiscoverModels · GPU tools · rocm_path_resolver · gpu_memory_gate Reporting perf.csv · perf_entry.csv · perf_super · csv_to_html · csv_to_email · TraceLens Database MongoDB upload · MongoDBConfig.from_env()

Key data flows

Derived from orchestration/build_orchestrator.py and orchestration/run_orchestrator.py. Both orchestrators receive a single merged additional_context dict from the CLI (see additional_context).

Build flow (BuildOrchestrator.execute)

  1. Constructor: merge context sources, fill build defaults (gpu_vendor=AMD, guest_os=UBUNTU when absent), run ConfigLoader.load_config() (infers target, rejects conflicting blocks such as k8s + slurm or llm_d + slurm, applies presets). The keys the user really set under slurm are remembered separately from preset defaults.
  2. Context(build_only_mode=True): no GPU or NUMA detection. MAD_SYSTEM_GPU_ARCHITECTURE is auto-detected only when the build is part of madengine run --tags (detect_local_gpu_arch=True) and the user did not supply it.
  3. Dispatch on mode, in this order:
    • platform: bare_metal → create conda prefixes, no Docker (see Execution platforms).
    • --use-image (or auto) → write a manifest for an existing image, no build.
    • --build-on-compute → build and push from a SLURM compute node (needs --registry).
    • Otherwise the normal Docker build below.
    --use-image together with --build-on-compute raises ConfigurationError.
  4. Normal build: DiscoverModels.run() runs once and is reused. Cards whose distributed.launcher is a self-managed launcher (slurm_multi) need an image source: with no --registry, a single env_vars.DOCKER_IMAGE_NAME on the cards becomes an implicit --use-image (angle-bracket placeholders are rejected); otherwise ConfigurationError.
  5. Warn when a selected Dockerfile declares ARG MAD_SYSTEM_GPU_ARCHITECTURE with no default and no value was resolved.
  6. DockerBuilder.build_all_models() builds every matching Dockerfile per card, optionally pushes, and never aborts on a single failure (see Docker build).
  7. Manifest steps, always executed even after partial failure: export_build_manifest() → save summary → save deployment_config → merge the first built card's distributed / slurm fields into deployment_config (user-set values win).
  8. Raise BuildError only when all builds failed. A partial build exits successfully with a warning.

Run flow (RunOrchestrator.execute)

  1. Start a SessionTracker: records how many rows perf.csv already has (marker file .madengine_session_start beside it) so the CLI can show only this run's rows.
  2. Manifest source, in priority order: MAD_CONTAINER_IMAGE in context (needs --tags; validates or pulls the image, writes a synthetic manifest with local_image_mode); else an existing --manifest-file; else build first (--tags). Neither tags nor a manifest → ConfigurationError.
  3. _load_and_merge_manifest(): runtime context overrides the stored deployment_config for deploy slurm k8s kubernetes llm_d platform conda distributed vllm env_vars debug; tools, pre_scripts, post_scripts, encapsulate_script, require_pinned_image are written into manifest context. A runtime slurm block replaces the stored one wholesale and resets the explicit-key provenance.
  4. Stored deployment_config keys the runtime did not set are copied into the working context.
  5. Target inference (see Deployment targets): platform: bare_metal → llm_d → k8s/kubernetes → slurm (spur when slurm.scheduler or deploy is spur) → a self-managed launcher on the first built card implies slurm → otherwise local. A legacy deployment_config.target is the final fallback.
  6. Dispatch:
    TargetExecutor
    local / dockerContainerRunner.run_models_from_manifest()
    bare_metalBareMetalRunner.run_models_from_manifest()
    slurm, spur, k8s, llm-dDeploymentFactory.create(DeploymentConfig).execute()
  7. Always, in a finally-style path: _cleanup_model_dir_copies() removes the madengine-provided files under scripts/common/.

Local Docker path (_execute_local)

  1. Runtime Context (full system + GPU detection), optional data.json loaded as Data, node info table.
  2. Restore host-level fields from the manifest context without overriding runtime-detected values: docker_mounts, docker_build_arg, docker_gpus, gpu_vendor, guest_os, docker_env_vars; tools / scripts always come from the manifest unless the runtime context supplies its own.
  3. Filter built_images: GPU vendor/architecture compatibility (orchestration/image_filtering.py; an image without gpu_vendor is treated as compatible), then skip_gpu_arch from each card (unless --disable-skip-gpu-arch). Skipped cards are written to perf.csv with status SKIPPED. The whole step is best effort: any exception, including the ExecutionError raised when no image is compatible, is caught, printed as a warning, and the run proceeds with all images.
  4. _copy_scripts(): honours an external MODEL_DIR (merges its docker/ and scripts/ into the cwd) and copies pre_scripts, post_scripts, tools, entrypoints, tools.json, test_echo.sh from the madengine package into scripts/common/. The MAD project's own scripts/ and docker/ are never deleted.
  5. --wait-for-gpu-memory gate (local only), then ContainerRunner runs each manifest entry. Distributed targets print a warning and ignore --keep-alive, --keep-model-dir, --skip-model-run and --wait-for-gpu-memory.
  6. For a full workflow the build and run logs are merged into one *.live.log.

Distributed path (_execute_distributed)

  1. For target slurm only, skip_gpu_arch is decided before submission (a filtered per-run copy of the manifest is submitted; the build manifest is left intact). Skipped cards appear as SKIPPED rows. An empty manifest returns a FAILURE row instead of exiting 0. Spur, Kubernetes and llm-d do not apply this pre-filter.
  2. Manifest context.docker_env_vars are handed to the deployment layer (runtime keys win), because SLURM/K8s never enter the local restore step.
  3. Session baseline is measured on the working perf.csv (what the deployment appends to), and this run's rows are exported to --output afterwards when it names another file.
  4. DeploymentConfig carries timeout (resolved against the default) and cli_timeout (verbatim, so the inner madengine run on the node still lets the model card outrank the default); monitor and cleanup_on_failure (both default true) are read from context.
  5. If the scheduler fails before reporting per-model metrics, every manifest model not already reported is added to failed_runs, so the CLI exit code is not 0 for a failed job.

Per-backend job flow (sbatch template, array jobs on Spur, Indexed Jobs on K8s, helm for llm-d): see SLURM, Kubernetes, llm-d.

Runtime precedence cheat-sheet

SettingOrder, highest first
Run timeoutexplicit --timeout > model card timeout > 7200 s (core/timeout.py). Non-positive resolved value = no timeout.
GPU counttop-level runtime key (gpus_per_node, gpu_count, n_gpus, num_gpus, ngpus) > slurm.gpus_per_node / k8s.gpu_count > card n_gpus > 1 (utils/gpu_config.py)
Distributed launcher (local Docker)distributed.launcher in context > card distributed.launcher > MAD_LAUNCHER_TYPE > MAD_LAUNCHER
Docker env passed to the containerruntime-detected docker_env_vars are kept over manifest copies; additional_context.docker_env_vars then overwrite per key
Log error-scan settingscontext > model card > built-in defaults
Deployment configruntime --additional-context > manifest deployment_config > preset defaults

CLI overview

Entry point: src/madengine/cli/app.py::cli_main, installed as the madengine console script. The Typer app registers seven top-level commands; report is a sub-app with its own sub-commands.

CommandSourcePurpose
discovercli/commands/discover.pyList / select model cards by tag; print JSON.
buildcli/commands/build.pyBuild (or adopt) Docker images, or prepare conda envs for bare-metal; write build_manifest.json.
runcli/commands/run.pyRun from a manifest, or build + run when no manifest exists; write perf.csv.
report (sub-app)cli/commands/report.pyto-html, to-email, tracelens, tracelens-compare.
databasecli/commands/database.pyUpload a CSV/JSON results file to MongoDB.
cleanupcli/commands/cleanup.pyRemove benchmark containers orphaned by interrupted runs.
setup-pagecli/commands/setup_page.pyGenerate a self-contained HTML "setup picker" from the repo's models.json.

Global behavior

  • madengine --version prints the installed package version (importlib.metadata; unknown if not installed). With no sub-command the help is shown. Shell completion is disabled (add_completion=False).
  • cli_main() first calls core.lifecycle.install_signal_handlers() so SIGINT/SIGTERM/SIGHUP tear down benchmark containers (see container lifecycle). A KeyboardInterrupt or an unhandled exception that escapes a command exits 1.
  • A Rich traceback handler is installed with show_locals=True. An unhandled crash can therefore print local variable values; do not rely on the console log being free of credentials.

Context sources

build and run accept the same configuration spine through three mutually exclusive-in-part inputs:

InputFormatRules
--additional-context, -cString; parsed as JSON first, then as a Python dict literal (ast.literal_eval)Must be an object at top level. Empty / {} counts as "not given".
--additional-context-file, -fJSON file (object)Loaded first; the -c string is deep-merged over it (CLI wins key by key, nested dicts merge rather than replace).
--config (repeatable)YAML file and/or Hydra overridesCannot be combined with either input above (exit 4). See --config frontend.

Validation applied to the merged context

Implemented in cli/validators.py::finalize_additional_context_dict; any failure exits 4 (INVALID_ARGS). build always validates; run validates only when a non-empty context was supplied.

  1. Missing gpu_vendor / guest_os are defaulted to AMD / UBUNTU (core/additional_context_defaults.py) and a banner says so.
  2. Structure checks: docker_build_arg, docker_env_vars, docker_mounts, env_vars, k8s, slurm, kubernetes, llm_d, distributed, vllm, deployment_config, deploy must be objects; tools and pre_scripts arrays; docker_gpus and MAD_CONTAINER_IMAGE strings; timeout a number; debug a boolean; log_error_patterns a non-empty string array. Build-arg values are coerced to strings.
  3. gpu_vendor must be AMD or NVIDIA, guest_os must be UBUNTU or CENTOS (case-insensitive, normalized to upper case).
  4. Launcher: every distributed.launcher / launcher.type is validated and rewritten to its canonical spelling. An unknown name is rejected up front rather than running as a single process. A bare-string top-level launcher is rejected. The per-target rule (slurm_multi is not valid on llm-d) is enforced when the deployment is constructed. See Launchers.
  5. Platform: platform is a string; omitted means Docker, "bare_metal" selects conda. Unknown values, the retired top-level bare_metal object, and a conda block on a Docker run are rejected. See Platforms.

Common option letters

ShortLongNotes
-t--tagsRepeatable; each value may itself be comma-separated (-t a -t b = -t a,b).
-r--registrybuild, run.
-c / -f--additional-context / --additional-context-filebuild, run. On database, -f is --file and -c is --collection.
-m--manifest-output (build) / --manifest-file (run)Same letter, different meaning.
-s, -l, -o, -v--summary-output, --live-output, --output, --verbose--live-output and most other bool flags have no --no-… form.

How failures become exit codes

Each command wraps its body and maps exception classes from core/errors.py to an exit code. The mapping is not identical across commands:

Conditionbuildrun
Mutually exclusive flags, bad context, ConfigurationError44
BuildError, any failed build or failed registry push (including partial success)22 (BuildError only)
ExecutionError, any failed model run, GPU-memory gate refusaln/a3
DiscoveryError (no models matched), missing file, permission error, Ctrl-C, other exceptions11

CLI — discover

Lists model cards and validates tag selection without building or running anything. Must be run from the MAD project root: discovery reads ./models.json (required; FileNotFoundError otherwise) and walks ./scripts/.

OptionShortDefaultDescription
--tags TEXT-tnoneTags / model names; repeatable and comma-separable. Omitted: print the list of all model names.
--full-foffWith no --tags: print the complete model card of every model (static and dynamic) as JSON. With --tags, the selected cards are printed either way.
--json-joffPlain JSON on stdout only: no panels, no status lines; errors go to stderr. Without --tags/--full it prints a JSON array of model names. Use this when piping.
--verbose-voffDebug logging; Rich traceback on failure.

Tag syntax

FormExampleSelects
Tag, model name, or last path segment of a name--tags llama3Any card whose name, final /-separated name segment, or tags entry equals the value.
Several--tags a,b / -t a -t bUnion. A tag that matches nothing aborts with No models found corresponding to the given tag (exit 1).
Everything--tags allEvery discovered model.
Scoped--tags MAD-private/inferenceExactly one / and no :. Restricts to cards named MAD-private/… (from scripts/MAD-private/) and matches on tag, exact name, directory prefix, or any path component. scope/all selects the whole scope.
Per-run script args--tags dummy3:batch=512:fp16Text after the first : is split on :; each = becomes a space and the pieces are appended to the card's args as --batch 512 --fp16.
A single-slash name such as MAD/foo is always parsed as scope/tag, never as a plain name. To select by full name, use the scope form (MAD/foo matches the exact name) or the last-segment form (foo).

Discovery sources

  1. Root ./models.json — required.
  2. scripts/**/models.json — found by a recursive walk, so nested submodule layouts (scripts/MAD/…) work.
  3. scripts/**/get_models_json.py — dynamic; must define list_models() returning CustomModel objects (utils/discover_models.py).

A directory containing both models.json and get_models_json.py is an error. If MODEL_DIR points elsewhere than the current directory, discovery first copies docker/, scripts/, models.json, credential.json and data.json from it into the working directory.

CLI — build

Builds Docker images for the selected models (or creates conda environments when platform is bare_metal) and writes build_manifest.json. Context is always validated; gpu_vendor / guest_os default to AMD / UBUNTU when absent.

OptionShortDefaultDescription
--tags-tnoneModel selection (same syntax as discover). Effectively required: with no tags and no batch manifest, discovery selects nothing and the build ends with No models discovered (exit 1). Use --tags all for everything.
--batch-manifest FILEnoneJSON list of {"model_name", "build_new", "registry"?, "registry_image"?}. Models with build_new: true are built; the rest get synthetic manifest entries so a later run sees the full set.
--target-archs-anoneGPU architectures to build for (gfx90a,gfx942); repeatable / comma-separated. Omitted: one image using MAD_SYSTEM_GPU_ARCHITECTURE from context or the detected GPU.
--registry-rnonePush built images here. A failed push is a build failure (exit 2).
--use-image [NAME|auto]noneSkip the Docker build and record a pre-built image in the manifest. auto reads env_vars.DOCKER_IMAGE_NAME from the model card. The image name is also written into each model's manifest env_vars.DOCKER_IMAGE_NAME.
--build-on-computeoffBuild on a SLURM compute node and push to the registry; the run phase pulls in parallel on all nodes.
--additional-context-c{}See context sources.
--additional-context-file-fnoneJSON file; -c is deep-merged over it.
--confignoneYAML / Hydra overrides; see --config frontend. Fills tags, registry, target archs, manifest path, live output, summary path and docker.clean_cache when the matching CLI flag was left at its default.
--clean-docker-cacheoffRebuild without the Docker layer cache.
--manifest-output-mbuild_manifest.jsonManifest path.
--summary-output-snoneWrite the build summary JSON here.
--live-output-loffStream build output.
--verbose-voffDebug logging.
Rejected combinations (exit 4):
  • --batch-manifest with --tags
  • --use-image with --registry
  • --use-image with --build-on-compute
  • --build-on-compute without --registry
  • --config with --additional-context or --additional-context-file

Self-managed launchers need an image source

For a card whose launcher is slurm_multi, the build requires one of: --registry, --use-image, --build-on-compute --registry, or a single env_vars.DOCKER_IMAGE_NAME on the card(s). In the last case, with no registry given, it is used as an implicit --use-image. Otherwise the build raises ConfigurationError (exit 4). An angle-bracket placeholder such as <your-image> in DOCKER_IMAGE_NAME is rejected.

Exit status

0 only if every build succeeded and every push succeeded. Any failure, including a partial one, exits 2; images that did build remain in the manifest.

CLI — run

Runs models and writes perf.csv. Mode is chosen by the manifest:

  • --manifest-file names an existing file → execution only (--tags is not used to select models).
  • Otherwise → full workflow: build, then run. A named-but-missing manifest only prints a warning. With neither a manifest nor --tags (and no MAD_CONTAINER_IMAGE) the orchestrator raises ConfigurationError (exit 4).
  • MAD_CONTAINER_IMAGE in context skips the build and synthesizes a manifest for the given image; --tags is then required.
OptionShortDefaultDescription
--tags-tnoneModels to build + run (full workflow). Same syntax as discover.
--manifest-file-m"" (unset)Existing build manifest. No implicit default: if omitted, build_manifest.json is not picked up automatically.
--registry-rnoneRegistry URL (push in the build phase; image resolution at run).
--timeout INT-1Seconds per model. See timeout resolution.
--additional-context / -c, --additional-context-file / -f, --configsee overviewSame context sources as build. With --config, model.tags, model.timeout, model.manifest_file, model.skip_run, build.registry, output, summary_output, data_config, live_output, require_pinned_image, docker.keep_alive, and docker.clean_cache are used when the matching flag is at its default.
--keep-aliveoffLeave the container running after the model finishes (also opts the container out of the heartbeat dead-man's switch).
--keep-model-diroffKeep the model directory copy. Docker-only; ignored for bare metal.
--clean-docker-cacheoffRebuild without cache (full-workflow build phase only).
--skip-model-runoffStart the container and run pre_scripts, but skip the model script. Pair with --keep-alive for a ready-to-exec container. Does not delete a stale multiple_results file.
--require-pinned-imageoffPull registry images by the digest recorded in the manifest, not by tag; fails at once if an image has no digest. Same as context key require_pinned_image.
--manifest-outputbuild_manifest.jsonManifest path written by the build phase of a full workflow.
--summary-output-snoneWrite the run (or combined workflow) summary JSON.
--live-output-loffStream container output.
--output-operf.csvPerformance CSV path.
--ignore-deprecatedoffAccepted and stored on the args object, but nothing in src/madengine reads it; it currently has no effect.
--data-configdata.jsonData-provider config. Only loaded if the file exists.
--tools-config./scripts/common/tools.jsonProfiling-tools definition file.
--sys-env-detailsonCollect rocEnvTool system details. Declared as a single flag with default True, so there is no --no-sys-env-details; turn it off with the gen_sys_env_details context key.
--force-mirror-local PATHnoneTakes a path, not a flag. Sets mirrorlocal to that path on every non-local provider in the data config (core/dataprovider.py). It does not change the deployment target.
--disable-skip-gpu-archoffIgnore each card's skip_gpu_arch.
--cleanup-perfoffIntended to delete intermediate perf_entry* files after the run. See the warning below.
--wait-for-gpu-memory [SECONDS]unsetLocal runs only. Before any container starts, require every GPU to report at most 512 MB in use. 0 checks once and fails if any GPU is over; a positive value polls until memory drops or the timeout elapses. A GPU that cannot be queried fails closed. Failure is an ExecutionError (exit 3). SLURM and Kubernetes warn and ignore it.
--verbose-voffDebug logging.
Known defect — --cleanup-perf: cli/commands/run.py imports madengine.utils.perf_cleanup, which does not exist in the source tree. The model runs and results are written, but the post-run cleanup raises ImportError, which the generic handler turns into exit 1 ("Run process failed"). Avoid the flag until the module is restored.

Timeout resolution

Single definition in core/timeout.py::resolve_run_timeout, shared by local, SLURM and Kubernetes runs. Precedence, lowest to highest: default (DEFAULT_RUN_TIMEOUT = 7200 s) < model card timeout < explicit --timeout.

--timeoutEffect
-1 (default)"Not passed": falls through to the card's timeout, else 7200 s.
0No timeout, overriding the card.
Positive integerThat many seconds, overriding the card (even 7200, which is distinguishable from omitting the flag).
Below -1Rejected, exit 4.

A card may itself set "timeout" to 0 or any non-positive value to mean "unbounded"; null in an old manifest is ignored. Non-positive resolved values are mapped to "no timeout" for subprocess by subprocess_timeout() (since subprocess would treat 0 as "expire immediately").

Exit status

0 when every model succeeded; 3 if any model failed; 2 when every build of a full workflow failed (BuildError); 4 for invalid arguments/configuration; 1 otherwise. A full workflow whose build only partly failed still runs the images that built; if those runs all pass it exits 1 ("Workflow failed for unknown reasons"), not 2. After the run, run prints the per-model results table and the perf.csv table (current-session rows highlighted via utils/session_tracker.py).

CLI — report & database

madengine report

Sub-app (report_app, no_args_is_help=True). All sub-commands exit 1 on failure.

Sub-commandOptionsBehavior
to-html--csv-file-path FILE required, -vConverts one CSV to an HTML table written next to it (same name, .html). The file must exist and end in .csv.
to-email--directory / --dir (default .), --output / -o (default run_results.html), -vConsolidates every CSV in a directory into one email-ready HTML file with a section per CSV. Prints a warning, not an error, if the directory has no CSVs.
tracelens--root/-r (.), --output-dir/-o (tracelens_output), --mode (auto|pytorch|rocprof|pftrace|collective), --python, --gpu-arch (e.g. MI300X), --world-size (0 = number of traces), --max-traces (0 = no cap), --discover-only, -vFinds trace artifacts a run left behind (rocprof_output/, torch_profiler_output/, slurm_results/, k8s_results/) and runs the matching TraceLens report: operator + roofline for torch.profiler traces, kernel summaries for rocprofv3 JSON, activity/API/memcpy reports for pftrace, collective reports across ranks. Writes tracelens_summary.csv to the output dir. Exit 4 for an invalid --mode; 1 if TraceLens is missing, the root is not a directory, or any report failed. Requires pip install 'madengine[tracelens]' (or --python pointing at an interpreter that has it).
tracelens-compareREPORTS... (two or more .xlsx files or per-sheet CSV directories), --output/-o (tracelens_comparison.xlsx), --names (repeat per report), --python, -vDiff workbook: the first report is the baseline and every metric gains _diff and _pct columns.
madengine report to-html --csv-file-path perf.csv
madengine report to-email --dir ./results -o summary.html
madengine report tracelens --discover-only
madengine report tracelens --root slurm_results --mode collective --world-size 8
madengine report tracelens-compare base.xlsx cand.xlsx --names before --names after -o diff.xlsx

madengine database

Uploads a CSV or JSON results file to MongoDB (database/mongodb.py); the format is auto-detected.

OptionShortDefaultDescription
--file-frequiredCSV or JSON file.
--database / --dbrequiredDatabase name.
--collection-crequiredCollection name.
--unique-key-kauto-detectedComma-separated deduplication fields.
--batch-size1000Bulk operation size.
--no-upsertoffInsert only; do not update existing documents.
--no-indexoffSkip automatic index creation.
--dry-runoffValidate without uploading.
--verbose-voffAlso prints up to 10 upload errors.

Connection comes from the environment (MongoDBConfig.from_env()):

VariableDefault
MONGO_HOSTlocalhost
MONGO_PORT27017
MONGO_USER, MONGO_PASSWORDempty (no auth unless both set)
MONGO_AUTH_SOURCEadmin
MONGO_TIMEOUT_MS5000

Exit 0 for a successful upload, or a partial one that inserted/updated at least one document; 1 otherwise (missing file, connection failure, nothing written). See also Database and Reporting.

CLI — cleanup & setup-page

madengine cleanup

Removes benchmark containers left behind by runs that died before they could clean up. Every container madengine starts carries the labels madengine.session and madengine.owner_pid (core/lifecycle.py); cleanup lists containers (all states) with the session label and acts on those whose owner PID is no longer alive. It is safe to call from an if: always() CI step or from cron while other benchmarks are running, because containers with a live owner are skipped.

OptionDefaultDescription
--alloffAlso remove containers whose owning madengine process is still running.
--dry-runoffPrint what would be removed; remove nothing (exit 0).
ExitCondition
0Nothing found, nothing stale, dry run, or every stale container removed.
1docker ps timed out or failed (Docker unreachable), or a container could not be removed and was not wedged.
5 GPU_WEDGEDA container could not be removed and there is evidence of a wedge (host PIDs in uninterruptible sleep, or the daemon still reports the container as live after repeated removal). The host needs a reboot; a gpu_wedged.json marker is written to the state dir (~/.madengine, override MADENGINE_STATE_DIR) so CI can pull the runner out of rotation.

Removal escalates docker stop → kill → rm -f → direct SIGKILL of the container's host PIDs. See Container lifecycle for the heartbeat watchdog and signal handlers.

madengine setup-page

Generates a single, fully self-contained HTML page (no external assets, so it can be published to GitHub Pages) from the current directory's models.json. The page lists models and tags plus the madengine context schema (setup_page/schema.py::CONTEXT_SCHEMA: general, deployment target, container, distributed, Kubernetes, SLURM, tools & scripts, logging, run flags) and shows the exact madengine run command to copy. Only values that differ from defaults and match the selected deployment target are emitted.

OptionShortDefaultDescription
--output-oindex.htmlOutput path (parent directories are created).
--titlemadengine Setup PickerPage heading (HTML-escaped).
--repo-urlnoneModel repository URL, used for the clone instructions (.git suffix and trailing slash are stripped).
--install-cmdpip install git+https://github.com/ROCm/madengine.gitInstall command shown in step 1.
--verbose-voffDebug logging.

Model collection uses DiscoverModels (so scripts/**/models.json and get_models_json.py models appear), falling back to the root models.json alone if full discovery fails. Missing models.json exits 1. The schema is data-driven: adding a context key to the page is a one-entry change in schema.py, validated by validate_schema() at render time.

--config — YAML / Hydra frontend

An alternative to --additional-context for build and run. It composes a config with Hydra's Compose API, validates it, and translates it into the same additional_context dict the rest of the pipeline already consumes, so behavior is identical to the JSON form. Code: config/loader.py (HydraConfigLoader), config/schema.py (ConfigValidator), config/translator.py (ConfigTranslator), package data in src/madengine/configs/ (shipped in the wheel).

Argument forms

--config is repeatable; each value is one of:

FormExampleMeaning
YAML file--config my_job.yamlA .yaml/.yml argument without = and not starting with +. At most one file ("Only one YAML config file allowed").
Group selection--config scheduler=slurmSwap a default group. Only platform, scheduler, hardware, launcher are default groups.
Append group--config +env=nccl_debugAdd a preset from an optional group: +profile, +env, +tools, +data, +build. One preset file per group per invocation.
Value override--config distributed.nnodes=4Dotted key=value; also +env_vars.KEY=val to add a new key.

Precedence (later wins)

  1. Package defaults: configs/config.yaml with default groups platform=docker, scheduler=local, hardware=amd, launcher=none.
  2. The user YAML file, merged over the defaults.
  3. Group selections and +group appends. Only values that differ from the package defaults are overlaid, so unrelated keys in your YAML are not reset. Corollary: selecting a group's default (--config scheduler=local) is a no-op.
  4. key=value overrides.

Dicts merge key by key; lists are replaced (+tools=… replaces a YAML tools list, while +env=… adds to env_vars). Explicit CLI flags win over config-derived values wherever the flag was changed from its default.

Groups shipped in configs/

GroupKindFiles
platformdefaultdocker, bare_metal, podman, singularity. Each writes platform: {type: …}; only docker passes validation (see below).
schedulerdefaultlocal (empty), slurm, k8s
hardwaredefaultamd, nvidia, cpu
launcherdefaultnone (distributed.enabled: false), torchrun, deepspeed, megatron-lm, megatron (alias of megatron-lm), torchtitan, primus, vllm, sglang, sglang_disagg, slurm_multi, native
profileappendmi300x_single, mi300x_8gpu, mi250x_4gpu, h100_8gpu, a100_8gpu (GPU type/memory, per-node GPU count, nproc_per_node, AMD HSA/queue env vars)
envappendinfiniband, miopen_defaults, nccl_debug, nccl_tuned
toolsappendrocprofv3_lightweight, rocprofv3_comprehensive, rocm_trace_lite, power_profiler, vram_profiler
dataappendlocal, nas, s3, minio
buildappenddefault, ci (docker.clean_cache: true), multi_arch (gfx90a, gfx942)

Group files do not cover every deployment target: there is no group for spur or llm_d, and llm_d / conda are not accepted top-level YAML keys (see validation below). Use --additional-context for those.

Job YAML shape

A job file uses the same top-level keys as the JSON context (slurm, k8s, distributed, env_vars, tools, …), not a Hydra defaults: list. Reference templates and runnable demos live in examples/configs/templates/ and examples/configs/demo/{local,slurm,k8s}/. Defaults provided by config.yaml:

KeyDefaultUsed for
model.tags / model.timeout / model.manifest_file / model.skip_run / model.container_image[] / null / null / false / nullCLI metadata. container_image becomes context MAD_CONTAINER_IMAGE.
build.registry / build.target_archs / build.manifest_outputnull / [] / build_manifest.jsonCLI metadata for build.
docker.build_args, env_vars, mounts, gpus, cpus, additional_run_optionsempty / nullTranslated to docker_build_arg, docker_env_vars, docker_mounts, docker_gpus, docker_cpus, additional_docker_run_options. docker.keep_alive / docker.clean_cache become docker_keep_alive / docker_clean_cache only when true.
log_error.pattern_scan / benign_patterns / patternstrue / [] / []log_error_pattern_scan, log_error_benign_patterns, log_error_patterns (empty lists omitted).
output, summary_output, data_config, live_outputperf.csv, null, data.json, falseCLI metadata.
env_vars, tools, pre_scripts, post_scripts, encapsulate_script, debugempty / falsePassed through unchanged (empty values dropped).

Validation and restrictions

  • Unknown top-level keys are errors (Unknown config key: '…'). Allowed: defaults, platform, scheduler, hardware, launcher, model, docker, build, env_vars, debug, live_output, log_error, tools, pre_scripts, post_scripts, encapsulate_script, data_config, output, summary_output, gpu_vendor, guest_os, runtime, slurm, k8s, kubernetes, distributed, vllm, sglang_disagg, shared_data, timeout, gpu_type, gpu_memory_gb, gpus_per_node, data, require_pinned_image.
  • slurm together with k8s/kubernetes is rejected.
  • distributed.enabled: true requires distributed.launcher; distributed.nnodes must be a positive integer.
  • Platform: the object form platform: {type: …} accepts only docker; bare_metal, podman and singularity are rejected as "not yet supported", so the platform=bare_metal group fails validation. A top-level platform value is CLI metadata and is not forwarded into additional_context, and conda is not an accepted key, so bare metal cannot be selected through --config: pass "platform": "bare_metal" (and any conda block) with --additional-context.
  • Any validation problem is raised as ConfigurationError listing every error (exit 4).

Launcher handling

  • The launcher top-level key as a string is the Hydra group selector and is dropped from the context. As an object (type, master_port, …) it is kept as launcher for Kubernetes.
  • distributed.launcher is canonicalized by the same function the engine uses elsewhere (sglang_disagg → sglang-disagg, slurm-multi → slurm_multi). The group launcher=megatron yields megatron-lm; the bare string megatron is not a valid launcher name.
  • distributed.enabled is passed through as written; the translator never sets it. Every launcher group writes enabled: true, and the default launcher=none writes enabled: false. Validation requires a launcher when it is true. Kubernetes profile selection treats either enabled: true or a launcher name as distributed.
  • launcher=native (and docker) is a perf.csv reporting value, not a launcher. The translator forces distributed.enabled: false and launcher: null so a Kubernetes profile cannot inject torchrun. Do not copy that group into JSON.
Gotchas: --config needs hydra-core / omegaconf (declared dependencies). runtime from a hardware group is kept as metadata only and is not forwarded to additional_context. The groups disagree on the port key: torchrun writes both port and master_port, sglang / sglang_disagg write only port, the rest only master_port. SLURM reads port first and falls back to master_port; Kubernetes prefers master_port (including launcher.master_port) and falls back to port. A disagreeing pair is not rejected. The sglang_disagg group puts its split under a top-level sglang_disagg key, which the SLURM and Kubernetes launchers do not read; set distributed.sglang_disagg.prefill_nodes / decode_nodes to change the split. Likewise the vllm group's top-level vllm block is only stored in deployment_config.

Examples

# Job file from the repo, scaled up and with NCCL debug env added
madengine run \
  --config examples/configs/demo/slurm/multi-node-torchrun.yaml \
  --config slurm.nodes=4 \
  --config distributed.nnodes=4 \
  --config +env=nccl_debug

# Local single GPU straight from a demo file
madengine run --config examples/configs/demo/local/single-gpu.yaml
# No YAML: compose from groups, then tweak values
madengine run \
  --config scheduler=slurm \
  --config launcher=torchrun \
  --config hardware=amd \
  --config +profile=mi300x_8gpu \
  --config distributed.nnodes=2 \
  --config 'model.tags=[dummy_torchrun]'

Exit codes CI contract

Defined in src/madengine/cli/constants.py::ExitCode (an IntEnum). Use them in CI instead of scraping logs.

CodeNameMeaning / where raised
0SUCCESSOperation succeeded (for run: every model passed; for cleanup: nothing left behind).
1FAILUREGeneral failure: no model matched a tag, missing file, permission error, Docker unreachable (cleanup), any report/database/setup-page error, Ctrl-C, unexpected exception.
2BUILD_FAILUREBuildError; one or more images failed to build or push (partial success included).
3RUN_FAILUREOne or more model runs failed, an ExecutionError occurred, or the --wait-for-gpu-memory gate refused to start.
4INVALID_ARGSArgument or configuration rejected: mutually exclusive flags, --timeout < -1, malformed/invalid context or launcher or platform, --config validation errors, bad batch manifest, ConfigurationError, invalid tracelens --mode.
5GPU_WEDGEDOnly from madengine cleanup: a container could not be removed and the GPU is considered wedged. The host needs a reboot; take the runner out of rotation.
When piping through tee in a shell or Jenkins step, enable set -o pipefail (or bash -o pipefail) so madengine run … 2>&1 | tee madengine.log still propagates the exit code.

additional_context — configuration spine

How it is parsed

InputParsing
--additional-context '<string>'CLI validator (cli/validators.py) tries json.loads first, then ast.literal_eval. Both {"a": true} and {'a': True} are accepted. The top level must be an object; anything else exits with code 4.
--additional-context-file FILEStrict JSON object (json.load).
Both givenFile first, then the string deep-merged on top key by key (CLI wins). A file with slurm.partition plus a string with slurm.nodes yields both.
--configHydra frontend builds the same dict; mutually exclusive with the two flags above. See config frontend.
Inside the orchestratorsThe merged dict is passed to Context as repr(dict), which parses it with ast.literal_eval. Values must therefore be literals (strings, numbers, booleans, lists, dicts, None).
Quoting tip: prefer JSON in single quotes ('{"docker_gpus": "0,1"}') or a file. Python-literal strings such as "{'k': True}" also work, but unquoted shell expansion of True/False inside bare words does not.

What Context adds

core/context.py merges user values over detected ones; user-provided values are never overridden by detection.

Build-only mode (madengine build)

  • No GPU/NUMA detection (so CPU-only build nodes work). Detects ctx_test and host_os when absent, best effort.
  • gpu_vendor / guest_os default to AMD / UBUNTU (core/additional_context_defaults.py) so Dockerfile context filtering has values.
  • Reads every MAD_SECRETS* environment variable into docker_build_arg and docker_env_vars.

Runtime mode (madengine run)

  • Detects ctx_test, host_os, numa_balancing, gpu_vendor (NVIDIA via nvidia-smi, AMD via amd-smi then rocm-smi), rocm_path (see ROCm path).
  • Fills docker_env_vars: MAD_GPU_VENDOR, MAD_SYSTEM_NGPUS, MAD_SYSTEM_GPU_ARCHITECTURE, MAD_SYSTEM_HIP_VERSION, MAD_SYSTEM_GPU_PRODUCT_NAME (only if not already set); mirrors the architecture into docker_build_arg.
  • Defaults docker_gpus to "0-<N-1>" and computes gpu_renderDs (GPU index → /dev/dri/renderD*).
  • Detection failure raises RuntimeError on the runtime node.
Secrets: MAD_SECRETS* values are dropped (not masked) from the manifest context by manifest_safe_context(); at run time Context reads them from the environment again.

Key reference

Top-level keys read anywhere under src/madengine. Scheduler sub-keys are documented in their own sections.

KeyTypeSubsystemDescription & example
gpu_vendorstringCore"AMD" or "NVIDIA". Overrides detection. Defaults to AMD for builds only; a run on a node where detection fails raises instead of guessing. Also filters *.amd.Dockerfile / *.nvidia.Dockerfile variants.
guest_osstringCoreContainer OS: "UBUNTU" (default) or "CENTOS". Used for Dockerfile # CONTEXT matching, exported as MAD_GUEST_OS, and passed to rocEnvTool for package installs in rocenv_mode: "full".
host_os, ctx_test, numa_balancingstringCoreNormally detected. Supply host_os explicitly on build nodes where detection is not possible. A # CONTEXT header in a Dockerfile can match any key present in the context.
MAD_ROCM_PATHstringUtilTop-level override of the host ROCm root (priority 1). See ROCm path.
platformstringOrch"docker" (default when omitted) or "bare_metal". Any other string, or a non-string, is rejected. See Execution platforms.
condadictExecBare-metal settings: conda_bin, conda_env, env_root, reuse_env, cleanup_env, plus per-invocation overrides of the card fields (environment_file, requirements_file, python_version, setup_script, rocm) and gpu_vendor / gpu_arch. Rejected unless platform is bare_metal. The old top-level bare_metal key is rejected.
docker_env_varsdictExecVariables passed to docker run as --env K=V (values shell-quoted). Names must match [A-Za-z_][A-Za-z0-9_]*. Special: ROCM_PATH sets the in-container ROCm root explicitly; MAD_MULTI_NODE_RUNNER pre-empts the generated launcher command.
docker_build_argdictExecExtra --build-arg K=V flags for docker build. Takes precedence over the same name in a card's docker_build_arg. Setting MAD_SYSTEM_GPU_ARCHITECTURE here disables --target-archs.
docker_mountsdictExecExtra volume mounts as {"<container path>": "<host path>"}, e.g. {"/data": "/mnt/shared/data"} becomes -v /mnt/shared/data:/data. A target already mounted through the card's additional_docker_run_options is skipped to avoid Docker's duplicate-mount error.
docker_gpusstringExecGPU indices the runner may hand to containers: comma list and ranges, e.g. "0,1,2,3" or "0-3". Default "0-<N-1>". A card asking for more GPUs than listed raises.
docker_cpusstringExecPassed as --cpuset-cpus (spaces removed), e.g. "0-15".
MAD_CONTAINER_IMAGEstringOrchSkip the build and run every --tags model in this image. A synthetic manifest (local_image_mode) is written; the image is inspected and pulled if missing. Requires --tags.
require_pinned_imageboolOrchSame as --require-pinned-image: pull registry images by the digest recorded at push time (repo@sha256:…), fail instead of falling back to a tag. Persisted into the manifest context so nested SLURM runs inherit it.
toolslistExecProfiling/tracing tools, e.g. [{"name": "rocprofv3_compute"}]; per-tool cmd and env_vars may override scripts/common/tools.json. Runtime value replaces the manifest's. See Profiling tools.
pre_scripts, post_scriptslistExecEntries {"path": "...", "args": "...", "timeout": 600} copied into the model directory and run with bash before/after the model script. timeout is per script (default 600, 0 = none). After a failed model, post-scripts still run best-effort. A non-empty pre_scripts keeps CAP_SYS_ADMIN on the container.
encapsulate_scriptstringExecCommand prefix prepended to the model invocation (e.g. a profiler wrapper). Tool cmd values are prepended to it.
rocenv_modestringExec"lite" (default) or "full"; unknown values fall back to lite with a warning.
gen_sys_env_detailsboolExecfalse disables the rocEnvTool pre-script. Otherwise follows --sys-env-details (default on).
log_error_pattern_scanboolExecDefault true. false skips post-run log substring scanning. Accepts 0/1, "true"/"false", "yes"/"no", "on"/"off". Context overrides the card.
log_error_patternslist[str]ExecA non-empty list replaces the built-in pattern list (see Result status).
log_error_benign_patternslist[str]ExecLiteral substrings; a log line containing one is never counted as an error. Card and context lists are concatenated.
model_argsstringExecReplaces the card's args string for the run.
skip_perf_collectionboolExecRun succeeds without a local performance line; collection is deferred to an aggregator (multi-node SLURM). Normally set by the deployment layer.
datadictCoreData-provider definitions merged over data.json (--data-config), keyed by data name then provider type (custom, local, minio, nas, aws).
env_varsdictDeployEnvironment for the deployment layers (SLURM script, K8s Job), bare-metal runs and self-managed launchers; merged over card env_vars. Not injected into a plain local Docker container — use docker_env_vars there.
distributeddictDeploylauncher, nnodes, nproc_per_node, backend, port/master_port, disaggregated blocks. See Per-launcher configuration.
launcherdictDeployOlder shape {"type": "..."}; still read for the launcher recorded on result rows. Prefer distributed.launcher.
slurmdictDeploySelects SLURM (scheduler: "spur" selects Spur). See SLURM.
k8s / kubernetesdictDeploySelects Kubernetes. See Kubernetes.
llm_ddictDeploySelects llm-d; checked before k8s. See llm-d.
deploystringDeployOptional explicit target. Only "spur" changes behavior beyond inference; ConfigLoader still validates it against the blocks present.
monitor, cleanup_on_failureboolDeployDefault true each: poll the job until it ends; cancel/clean up when it failed.
debugboolDeployRead by the Kubernetes deployment for extra diagnostics; also stored in deployment_config.
docker_keep_alive, docker_clean_cacheboolCLIOnly honoured when produced by --config; the CLI promotes them to --keep-alive / --clean-docker-cache. The runners read the flags, not these keys.
live_output, session_start_row, deployment_config, _explicit_slurm_keys, vllminternalOrchSet by the orchestrators and handed to the deployment layer (vllm is only persisted into deployment_config). Do not set by hand.

Model definition — models.json

Cards come from root models.json, scripts/<dir>/models.json, or scripts/<dir>/get_models_json.py (returns CustomModel objects). The CustomModel dataclass in utils/discover_models.py lists the typed fields; static JSON cards may carry additional keys, which are copied verbatim into the manifest built_models entry.

{
  "name": "pyt_llama3_train",
  "dockerfile": "docker/pyt_llama3",
  "dockercontext": "",
  "scripts": "scripts/llama3/run.sh",
  "url": "",
  "cred": "",
  "owner": "ml-team@example.com",
  "data": "",
  "n_gpus": "8",
  "timeout": 14400,
  "training_precision": "bf16",
  "tags": ["pyt", "training"],
  "args": "--batch-size 4",
  "multiple_results": "",
  "skip_gpu_arch": "gfx908,gfx1100",
  "docker_build_arg": {"VLLM_REF": "v0.9.0"},
  "additional_docker_run_options": "--shm-size 64g",
  "env_vars": {"MY_FLAG": "1"},
  "distributed": {"launcher": "torchrun", "nnodes": 2, "nproc_per_node": 8},
  "slurm": {"partition": "gpu", "nodes": 2, "time": "04:00:00"}
}

Field reference

FieldDefault (CustomModel)Behavior
namerequiredUnique id. / is replaced by _ in image, log and env-file names. A scope/ prefix marks the scripts/<scope>/ directory the card came from (used by scoped tags).
dockerfile""Prefix, not a file name: docker/foo matches docker/foo.Dockerfile, docker/foo.<os>.<vendor>.Dockerfile and same-card context variants. See Docker build.
dockercontext""Docker build context. Default ./docker; Dockerfiles whose name contains primus default to . (repo root).
scripts""*.sh or *.slurm → bash; *.py → python3; anything else is a directory whose run.sh is run. The script's directory is copied into the model working directory (run_directory/, or the repo name when url is set); an existing file of the same name fails the run instead of being overwritten.
args""Appended to the script command. Extra tag segments (--tags name:key=val) are appended here by discovery. Replaced by context model_args.
url""Repository cloned inside the container (with submodules) into a directory named after the repo.
cred""Key into credential.json. Each field becomes <cred>_<FIELD> (upper-case) as a build arg and as a run env var; also used for git clone authentication. Missing key raises.
data""Comma list of data names resolved through data.json / context data.
n_gpus"-1"GPUs for the run. "-1" = every GPU allowed by docker_gpus. On bare metal and Docker it becomes a positive MAD_RUNTIME_NGPUS. Overridden by deployment-level GPU counts (see precedence).
timeout7200Seconds. Non-positive = no timeout. Outranked by an explicit --timeout. JSON null (older manifests) is treated as unset. Note a card produced by get_models_json.py always carries the dataclass default 7200.
training_precision, owner""training_precision is a perf.csv column; owner is carried in the manifest only.
tags[]Selection tags for --tags.
multiple_results""CSV the script writes with one result row per case; each row is appended to perf.csv (see Reporting). It is exported as MAD_OUTPUT_CSV. A leftover file from an earlier run is deleted before a normal run (not with --skip-model-run) so an appending script cannot re-report old rows.
skip_gpu_arch""Comma-separated architectures (or a list). The card is skipped (SKIPPED row) when the runtime architecture matches; NVIDIA names are normalised ("NVIDIA A100-SXM4-40GB" → A100). Enforced for local/bare-metal runs and for the slurm target before submission. Disable with --disable-skip-gpu-arch.
docker_build_arg—Per-card build args (must be an object). Context docker_build_arg, multi-arch and credential args win on conflict.
additional_docker_run_options—Free-form string appended to docker run. Mount targets in it suppress duplicate -v entries from context/data.
env_vars—Environment for deployment layers, bare-metal and self-managed runs. DOCKER_IMAGE_NAME here is the registry image that slurm_multi nodes pull; the builder writes it after a successful push.
distributed, slurm—Card-level defaults copied into the manifest deployment_config at build time (first built card only; user-set values win). A self-managed launcher on the card makes the run target SLURM even without a slurm block.
log_error_pattern_scan, log_error_patterns, log_error_benign_patterns—Per-card log-scan settings; context overrides them.
conda_env, environment_file, requirements_file, python_version, setup_script, rocm"" (rocm not on the dataclass)Bare-metal dependency recipe; the conda block in context overrides each. See Execution platforms.
k8s, llm_d blocks and llm-d model identity—Consumed by the Kubernetes / llm-d deployment code, not by the orchestrators. See Kubernetes and llm-d.
Dockerfile context headers: a line # CONTEXT {'gpu_vendor': 'AMD', 'guest_os': 'UBUNTU'} within the first five lines of a Dockerfile restricts it to matching contexts. Keys not present in the context never exclude a file.

Build manifest — build_manifest.json

Written by madengine build (or by a full run --tags), consumed by madengine run --manifest-file. The run phase rewrites it (filtering, merged context, docker_run_cmd).

{
  "built_images": {
    "ci-pyt_llama3_train_pyt_llama3.ubuntu.amd": {
      "model": "pyt_llama3_train",
      "docker_image": "ci-pyt_llama3_train_pyt_llama3.ubuntu.amd",
      "dockerfile": "docker/pyt_llama3.ubuntu.amd.Dockerfile",
      "base_docker": "rocm/pytorch:latest",
      "docker_sha": "sha256:<base image digest>",
      "build_duration": 183.4,
      "build_command": "docker build --network=host -t ... (secrets redacted)",
      "log_file": "pyt_llama3_train_pyt_llama3.ubuntu.amd.build.live.log",
      "gpu_vendor": "AMD",
      "gpu_architecture": "gfx942",
      "image_size_bytes": 23456789012,
      "registry": "registry.example.com/ml",
      "registry_image": "registry.example.com/ml/ci-pyt_llama3_train_pyt_llama3.ubuntu.amd",
      "image_digest": "sha256:<pushed digest>"
    }
  },
  "built_models": {
    "ci-pyt_llama3_train_pyt_llama3.ubuntu.amd": {
      "name": "pyt_llama3_train", "scripts": "scripts/llama3/run.sh", "n_gpus": "8",
      "env_vars": {"DOCKER_IMAGE_NAME": "registry.example.com/ml/ci-pyt_llama3_train_pyt_llama3.ubuntu.amd"}
    }
  },
  "context": {
    "docker_env_vars": {}, "docker_mounts": {}, "docker_build_arg": {},
    "gpu_vendor": "AMD", "guest_os": "UBUNTU", "docker_gpus": ""
  },
  "credentials_required": [],
  "summary": {
    "successful_builds": [], "failed_builds": [], "total_build_time": 183.4,
    "successful_pushes": [], "failed_pushes": []
  },
  "deployment_config": {
    "target": "slurm",
    "slurm": {"partition": "gpu", "nodes": 2},
    "distributed": {"launcher": "torchrun", "nnodes": 2, "nproc_per_node": 8},
    "env_vars": {}, "debug": false,
    "_explicit_slurm_keys": ["nodes", "partition"]
  },
  "docker_run_cmd": "docker run -t -d ... (written after a local run)"
}

Structure

KeyWritten byNotes
built_images, built_modelsbuildJoined by key. Normal builds key both by the local image tag ci-<name>_<dockerfile basename> (plus _<arch> with --target-archs). --use-image, --build-on-compute and bare-metal builds key both by model name.
built_images[*].registry_image, image_digestbuild (push)Digest comes from the docker push output, else docker image inspect. If a push failed: push_failed and push_error are set and a top-level push_failures list is added.
built_images[*].gpu_vendor / gpu_architecturebuildUsed by the run-time image filter. Vendor is inferred from .amd./.nvidia. in the file name, else from the FROM content.
built_models[*]buildThe card, plus env_vars.DOCKER_IMAGE_NAME after a registry push. On --use-image and compute-node builds DOCKER_IMAGE_NAME is the image used; entries carry prebuilt: true or built_on_compute: true.
contextbuilddocker_env_vars, docker_mounts, docker_build_arg (without MAD_SECRETS*), gpu_vendor, guest_os, docker_gpus, plus tools, pre_scripts, post_scripts, encapsulate_script when set. The run phase may add require_pinned_image.
deployment_configbuild, then run mergetarget (local, slurm, spur, k8s, llm-d, bare_metal), platform, conda, slurm, k8s, kubernetes, llm_d, distributed, vllm, env_vars (user-supplied only, without preset defaults and MIOPEN_USER_DB_PATH), debug. Omitted entirely for a plain local build with no extra context. _explicit_slurm_keys records which slurm.* keys the user or card really set.
summarybuildsuccessful_builds, failed_builds, total_build_time, successful_pushes, failed_pushes. Entries are build-info dicts for Docker builds and model names for pre-built manifests.
credentials_requiredbuildDistinct cred keys of built cards.
docker_run_cmdlocal runThe docker run line of the first container started. Best effort; never fails the run.
local_image_mode, local_image_nameMAD_CONTAINER_IMAGE runSynthetic manifest; entries are keyed local-<model> with build_status: SKIPPED.
bare_metal: truebare-metal buildEntries have docker_image: "", conda_env, and the card's dependency fields; context is the full runtime context.
Merge precedence at run time: keys in the runtime --additional-context replace the same top-level keys of the stored deployment_config (a runtime slurm or distributed object replaces the whole stored object); keys the runtime did not set are inherited from the manifest. The merged result is written back to the manifest file. For the local path, runtime-detected docker_env_vars/docker_build_arg/docker_mounts win over manifest copies.
Distributed GPU filtering happens on the node: for SLURM, the local-path image/architecture filter does not run in the submitting process. The job script writes a node-local manifest with deployment_config.target="docker" and re-enters madengine run on each node, which filters there (skip_gpu_arch is additionally pre-filtered at submission for the slurm target).

Execution platforms

The platform context key chooses where the workload's dependencies live; the scheduler blocks (slurm, k8s, llm_d) choose where it runs. Omitted platform means Docker.

Local Docker ContainerRunner

  1. Resolve the image: local_image entries (build-on-compute, MAD_CONTAINER_IMAGE, --use-image) are used as-is (and, on SLURM, may be rebuilt from the manifest Dockerfile or loaded from a shared tar); registry_image entries are pulled (docker pull, 1800 s default, MAD_DOCKER_PULL_TIMEOUT_SECS); plain tags run directly. A failed pull falls back to the local tag unless require_pinned_image is set.
  2. Build docker run options (below), mount the working directory at /myworkspace, start the container detached under the heartbeat watchdog (Container lifecycle).
  3. Inside: clone url if present, copy the script directory into the model directory, run pre-scripts, [encapsulate] bash run.sh <args> (timeout from the resolved value), post-scripts.
  4. Parse results, write perf.csv, perf_super.csv/json, stop and remove the container (unless --keep-alive).

Default run options

GPU vendorOptions
AMD--network host -u root --group-add video --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --security-opt apparmor=unconfined --ipc=host, --device=/dev/kfd, one --device=/dev/dri/renderD* per GPU
NVIDIA-u root --cap-add=SYS_PTRACE --cap-add SYS_NICE, same security-opt/network/ipc, --gpus '"device=..."'

Least-privilege rules

ConditionAdded
Ordinary runNo CAP_SYS_ADMIN, no /dev/fuse
Data comes from a nas provider without mirrorlocal (SSHFS mount in the container)--cap-add SYS_ADMIN --device /dev/fuse
Context pre_scripts is non-empty, or a tools entry whose name starts with rocprof or perf--cap-add SYS_ADMIN (no /dev/fuse). The built-in rocEnvTool pre-script does not trigger this.
Card needs FUSE for another reasonPut --device /dev/fuse --cap-add SYS_ADMIN in the card's additional_docker_run_options

Bare metal (conda) BareMetalRunner

No Docker build, no container. One conda prefix per model replaces the image; single-node and local only.

madengine run --tags my_model --live-output \
  --additional-context '{
    "platform": "bare_metal",
    "conda": {"conda_bin": "/path/to/micromamba"}
  }'

conda block

KeyDefaultMeaning
conda_binfirst of micromamba, mamba, conda on PATHExecutable. madengine never downloads a conda implementation.
conda_envcard conda_env, else mad_<name> (/ → _)Prefix name; flattened to one path segment.
env_root~/.cache/madengine/envsParent of prefixes. A same-named env in the user's own conda root is never reused.
reuse_envtrueKeep an existing prefix while its dependency stamp (hash of environment file, requirements file, setup script and ROCm wheel inputs, stored in ~/.cache/madengine/envhash/) still matches. A changed input forces create/update, pip, ROCm install and setup_script together.
cleanup_envfalseRemove the prefix after the run.
environment_file, requirements_file, python_version, setup_script, rocmcard valuesPer-invocation override. Vendor variants *.amd.* / *.nvidia.* are preferred when present. rocm: {enabled, index_url ("auto"), packages, torch} pip-installs ROCm (and torch) from the per-architecture index.

Behavior

  • Build validates the GPU installation, then creates/updates each prefix and writes a manifest with docker_image: "" and bare_metal: true. A run without a manifest builds first.
  • Run wraps the script in conda run -p <prefix> (plus --no-capture-output for conda/mamba, not micromamba) and does not wrap it in torchrun/srun; the script is its own launcher.
  • GPUs: no --device; HIP_VISIBLE_DEVICES/ROCR_VISIBLE_DEVICES (AMD) or CUDA_VISIBLE_DEVICES (NVIDIA) are exported. n_gpus: "-1" keeps an existing mask and becomes a positive MAD_RUNTIME_NGPUS, because torchrun --nproc_per_node rejects -1.
  • --keep-alive and --keep-model-dir are ignored.
  • Result rows record platform=bare_metal, deployment_type=local, empty docker_image, and the workload launcher (launcher.type > distributed.launcher > card > MAD_LAUNCHER_TYPE/MAD_LAUNCHER; native when none or not a workload launcher).

Rejected combinations

  • Top-level bare_metal key (retired).
  • conda block while platform is docker/omitted.
  • platform: bare_metal with slurm, k8s, kubernetes or llm_d.
  • Any other platform string (podman, singularity are not supported).

Self-managed launchers (local)

A card whose launcher is self-managed (slurm_multi) is not run in a madengine container: ContainerRunner._run_self_managed() executes the script directly on the host with cwd set to the script directory. Env vars from the card and from additional_context.env_vars are applied; only their names are logged. See slurm_multi.

Local launcher command (MAD_MULTI_NODE_RUNNER)

LauncherValue exported to the container
torchrun, megatron-lm, torchtitantorchrun --standalone --nproc_per_node=<MAD_RUNTIME_NGPUS>
deepspeeddeepspeed --num_gpus=<N>
vllm, sglang, sglang-disagg, primusempty string (self-managing; set so scripts under set -u do not fail). A local sglang-disagg run is not rejected, but it is one node and gets no prefill/decode topology.
none / unrecognisedtorchrun --standalone --nproc_per_node=N (unrecognised names warn)

Launcher validation (what names are accepted at the config boundary) is documented in Launcher matrix. A pre-set docker_env_vars.MAD_MULTI_NODE_RUNNER is never overwritten.

Result status and log scanning

  1. Performance is extracted from the log with the pattern performance: <number> <metric> (fallback: HuggingFace train_samples_per_second), or from the multiple_results CSV.
  2. Unless log_error_pattern_scan is false, the log is scanned for substrings: OutOfMemoryError, HIP out of memory, CUDA out of memory, RuntimeError:, AssertionError:, ValueError:, SystemExit, failed (exitcode:, Traceback (most recent call last), FAILED, Exception:, ImportError:, ModuleNotFoundError: (replaceable via log_error_patterns). Known-benign lines (metrics-agent noise, FutureWarning, rocprof/rpd tracer lines, plus log_error_benign_patterns) are ignored.
  3. Status: valid performance → SUCCESS even if an error pattern matched (model output can legitimately contain ValueError:); error pattern and no performance → FAILURE; worker nodes (MAD_COLLECT_METRICS=false) and skip_perf_collection runs are not failed for lacking local performance; otherwise no performance → FAILURE. A timeout, a setup exception or a non-zero script exit records a FAILURE row so the results table stays complete.

Data providers

core/dataprovider.py: data.json (plus context data) maps a data name to provider blocks. Providers are tried in the order custom, local, minio, nas, aws. --force-mirror-local <path> sets mirrorlocal to that path on every non-local provider so data is rsynced instead of SSHFS-mounted (keeping the container unprivileged). Selected provider name, type, size and download time are recorded on the result row.

Container lifecycle & cancellation

Implemented in core/lifecycle.py and core/docker.py. Problem solved: a cancelled CI job kills madengine's process tree, but the detached benchmark container survives, keeps /dev/kfd open and blocks the next job on the GPU. Three layers, most to least robust:

LayerMechanismSurvives SIGKILL of madengine?
1. Dead man's switchContainer PID 1 is a POSIX sh loop watching /myworkspace/.madengine_heartbeat.<container>. A host thread rewrites it every MADENGINE_HEARTBEAT_INTERVAL s. The container exits when the file is missing or older than MADENGINE_HEARTBEAT_STALE_AFTER s. One file per container, so closing one never stops another started from the same directory.Yes
2. Signal handlersInstalled by cli_main() for SIGINT/SIGTERM/SIGHUP: drop heartbeats, reap registered containers within a 6 s budget, run registered cancel hooks, os._exit(130). A second signal exits immediately.No
3. reap() / madengine cleanupBounded escalation shared by the handlers, normal teardown and the cleanup command.Manual / cron

Teardown escalation

  1. An already-exited container just gets docker rm -f. A live one gets docker stop -t 1 (10 s budget) → docker kill -s KILL (10 s) → docker rm -f (15 s); each step runs only if the container is still present.
  2. If the daemon cannot remove it: read the container's host PIDs from the cgroup (works with a hung dockerd), SIGKILL them, retry docker rm -f.
  3. Every docker call has a hard deadline and its own session, because a wedged GPU blocks the daemon too. An overall deadline caps the whole escalation.
  4. docker inspect failure means unknown state, not "gone".

GPU wedged

Processes in uninterruptible sleep (D state) inside the GPU driver cannot be killed from userspace. A wedge is reported only on evidence: D-state host PIDs, or a container still in a live state after stop, kill and rm. An unresponsive daemon alone is not enough.

  • Marker gpu_wedged.json (container, stuck PIDs, session, hostname, timestamp) is written to the state dir.
  • madengine cleanup exits 5 (ExitCode.GPU_WEDGED) so CI can pull the runner out of rotation; the host needs a reboot.

Labels, registry and tuning

ItemDetails
Container labelsmadengine.session=<id>, madengine.owner_pid=<pid> on every container, so cleanup can find orphans even if the owner died before recording anything.
Container registry<state dir>/containers-<pid>.json lists containers owned by a process (register()/unregister()); keep_alive entries are skipped by teardown.
Cancel hooksregister_cancel_hook(): a submitted deployment (SLURM, Spur, Kubernetes) registers its cleanup(), an in-place SLURM step has its process group signalled, and a managed llm-d stack is uninstalled. Hooks run before os._exit (which does not unwind finally blocks), newest first, and a failing hook does not skip the others.
MADENGINE_HEARTBEAT_INTERVALDefault 15 s. Non-integer or non-positive values fall back to the default; if not smaller than the stale threshold it is reduced to a third of it.
MADENGINE_HEARTBEAT_STALE_AFTERDefault 120 s (same fallback rule).
MADENGINE_STATE_DIRDefault ~/.madengine; holds the registry and gpu_wedged.json. An unwritable directory never fails a benchmark.
MADENGINE_SESSION_IDDefault: random 12-hex id per process. Set to group containers from cooperating processes.
--keep-aliveOpts out of the watchdog (container runs cat), is not registered for teardown and is left running; the log prints docker exec/stop/rm hints. Ignored on distributed targets.
Images without a POSIX shell: if the container does not stay up under the watchdog, madengine falls back to the old cat PID 1 with a warning. That container is unwatched and can outlive a cancelled run. If the daemon is unresponsive or the container is already live in that state, madengine refuses to start an unwatched container instead.

Docker build

Implemented in execution/docker_builder.py (DockerBuilder); orchestrated by BuildOrchestrator (see flows).

Image selection & naming

  • Candidates are ls <card.dockerfile>.*, kept only if they are this card's own variants: <prefix>.Dockerfile, <prefix>.<os>.<vendor>.Dockerfile, or with a same-card noctx/ctx<token> segment. A sibling card whose Dockerfile name merely starts with this prefix is not built.
  • Survivors are filtered by the Dockerfile's # CONTEXT {...} header against the context (gpu_vendor, guest_os, ...).
  • Local tag: ci-<name, lowercased, / → _>_<Dockerfile basename without .Dockerfile>. With --target-archs a _<arch> suffix is appended.
  • Log file: <name>_<dockerfile>.build.live.log in the cwd.

Registry names

  • --registry docker.io|dockerhub: <repository>:<local tag>, repository from credential.json / MAD_DOCKERHUB_REPO (placeholder repositories are ignored).
  • Any other registry: <registry>/<repository>:<tag> when credential.json has a repository for it, else <registry>/<local tag>.
  • A batch-build entry's registry_image / registry overrides both.

The docker build command

docker build [--no-cache] --network=host \
  [--build-context tools=docker/common] \
  -t <tag> --pull -f <dockerfile> \
  <--build-arg ...> <context>
  • --build-context tools=docker/common is emitted only when docker/common/ exists relative to the cwd, so Dockerfiles can COPY --from=tools .... Projects without that directory get no flag and do not fail.
  • Build args, lowest to highest precedence: card docker_build_arg, then --target-archs arch and cred args, then context docker_build_arg (which also carries MAD_SECRETS* from the environment and the detected MAD_SYSTEM_GPU_ARCHITECTURE). All values are shell-quoted.
  • Context: dockercontext, else ./docker; . for Dockerfiles with primus in the name.
  • build_command in the manifest has MAD_SECRETS* and token-shaped values masked.
  • If the base image's registry is not authenticated, a login is attempted first; an existing ambient docker login is left alone. A registry-denial in the log produces an actionable hint.

GPU architecture handling

SituationResult
docker_build_arg.MAD_SYSTEM_GPU_ARCHITECTURE setSingle-architecture flow for every card; --target-archs is ignored (with an info message).
run --tags on a local GPU node, nothing setArchitecture auto-detected and injected as a build arg.
--target-archs gfx90a,gfx942 (comma list)One image per architecture, each with --build-arg MAD_SYSTEM_GPU_ARCHITECTURE=<arch> and a _<arch> suffix; gpu_architecture is recorded in the manifest and used by the run-time filter.
Dockerfile has ARG MAD_SYSTEM_GPU_ARCHITECTURE without a default, nothing suppliedWarning listing the cards and Dockerfiles, with the exact --additional-context to pass.

Push, digest and manifest

  • docker tag + docker push (no timeout). A push failure marks the entry push_failed and does not fail the build; it is listed in push_failures and summary.failed_pushes.
  • The pushed digest is parsed from docker push output (fallback docker image inspect RepoDigests) and stored as image_digest (core/image_digest.py). It is only used with --require-pinned-image: the run pulls repo@sha256:... and refuses a manifest without a digest rather than falling back to the tag.
  • After a successful push env_vars.DOCKER_IMAGE_NAME on the built model is set to the registry image so slurm_multi nodes can pull in parallel.
  • image_size_bytes is recorded so the SLURM node health check can compare it with free disk on each candidate node.
  • The run side labels locally rebuilt images with mad.build_fingerprint (Dockerfile hash + build args) and rebuilds when a reused tag no longer matches; MAD_DOCKER_BUILDS may point at a shared directory for docker save tars that other SLURM nodes docker load.

Alternative build modes

ModeBehavior
--use-image <image>No build. One manifest entry per model, all pointing at the image; local_image: true, prebuilt: true, env_vars.DOCKER_IMAGE_NAME set.
--use-image autoTakes env_vars.DOCKER_IMAGE_NAME from the first card that has one (warns if cards disagree); error if none; placeholder values (<...>) rejected.
--build-on-compute --registry RBuild and push from one SLURM compute node in a single job. --registry is normalised (a missing host gets docker.io/; spaces or @ rejected). SLURM settings come from the first card plus context (user-set keys win). The job reuses slurm.account/slurm.qos, requests GPUs when slurm.build_gpus_per_node is set, and passes the same --build-arg set as a local build. Manifest entries are marked built_on_compute; run nodes pull in parallel.
Batch build (--batch-manifest)Per-model registry / registry_image overrides are applied when pushing and recorded per image.
platform: bare_metalNo Docker; see Execution platforms.

Deployment target inference

There is no required deploy field. RunOrchestrator._infer_deployment_target() inspects the merged context (runtime --additional-context over the manifest's deployment_config) and picks the first rule that matches.

Precedence

#Context conditionTargetExecutor
1"platform": "bare_metal"bare_metalexecution/bare_metal_runner.py via RunOrchestrator._execute_bare_metal() (see Platforms)
2llm_d key presentllm-dLlmdDeployment (see llm-d)
3k8s or kubernetes key presentk8sKubernetesDeployment (see Kubernetes)
4slurm key present and slurm.scheduler (or top-level deploy) is "spur"spurSpurDeployment (see SLURM)
5slurm key presentslurmSlurmDeployment
6No scheduler key, but the effective launcher is slurm_multi (distributed.launcher, else the model card's distributed.launcher)slurmSlurmDeployment — a self-managed card is a SLURM job by construction
7None of the abovelocalContainerRunner (local Docker)

Why the order matters

  • llm-d before k8s: an llm-d config also carries a k8s block, which sizes the benchmark-client Job.
  • k8s before slurm_multi promotion: a slurm_multi card with a k8s block runs on Kubernetes (see slurm_multi on Kubernetes); with neither block it is promoted to SLURM.
  • Manifest fallback: if inference yields local, deployment_config.target from the manifest is consulted. Scheduler blocks saved at build time are also merged into the run context first, unless the runtime context already has that key.

Platform rules (orchestration/platform.py)

  • platform is a string: "docker" (default when omitted) or "bare_metal". Anything else raises ConfigurationError.
  • The retired top-level bare_metal key is rejected.
  • A conda block requires platform: "bare_metal".
  • bare_metal cannot be combined with slurm, k8s, kubernetes or llm_d.

Build-time validation

BuildOrchestrator runs ConfigLoader.load_config(), which calls infer_and_validate_deploy_type() and applies the SLURM or K8s presets. It raises (surfaced as ConfigurationError) when:

  • k8s/kubernetes and slurm are both present;
  • llm_d and slurm are both present;
  • an explicit deploy value contradicts the blocks: k8s/kubernetes without a k8s block, slurm/spur without a slurm block, spur with a slurm.scheduler other than spur, or local with a scheduler block;
  • slurm.scheduler is anything other than slurm or spur.

Only k8s, slurm and spur get preset defaults here. An llm_d config with a k8s block gets the K8s presets; the llm-d defaults are applied later by LlmdDeployment itself.

Factory registration

deployment/factory.py registers deployments at import time. DeploymentFactory.create() raises ValueError listing the registered names for an unknown target.

Target stringClassNotes
slurmSlurmDeploymentAlways registered.
spurSpurDeploymentAlways registered.
k8s, kubernetesKubernetesDeploymentRegistered only if the kubernetes package imports; otherwise a UserWarning is issued and the target is unavailable.
llm-d, llm_dLlmdDeploymentRegistered in its own try/except, so a failure never takes SLURM or K8s down.

local, docker and bare_metal are not factory targets: RunOrchestrator runs them directly.

Launcher validity per target

Every launcher in the context, the manifest deployment_config and each model card is checked when a BaseDeployment is constructed (_validate_launchers()), so a bad value surfaces as INVALID_ARGS rather than a failed job. Unknown spellings raise ConfigurationError; they do not fall back to a single process. ensure_launcher_allowed() (deployment/common.py) has a single per-target rule:

LauncherAllowed onRejected on
slurm_multislurm, spur, k8s, and local (promoted to SLURM when no scheduler block)llm-d
Every other launcher (torchrun, deepspeed, megatron-lm, torchtitan, primus, vllm, sglang, sglang-disagg)every target—

sglang-disagg node minimums (2 on SLURM/Spur, 3 on Kubernetes) are checked when the launcher command is generated, before submission; see Per-launcher configuration.

Canonical names and aliases are listed in Launchers.

Gotchas
  • --force-mirror-local is not a target override. It is a data-provider flag (force_mirrorlocal) that forces local mirroring of a dataset; the SLURM template also forwards it as --force-mirror-local <shared_data>. There is no CLI flag that forces local execution when a scheduler block is present: remove the block.
  • The kubernetes spelling selects the K8s target, but its sub-keys are not read: presets always create a k8s block, which wins. Put sub-keys under k8s.
  • --keep-alive, --keep-model-dir, --skip-model-run and --wait-for-gpu-memory have no effect on distributed targets; a warning lists the ignored flags.
  • For SLURM, skip_gpu_arch is applied before submission, for every launcher including slurm_multi; skipped cards appear as SKIPPED rows.

SLURM & Spur deployment

Implemented by SlurmDeployment in deployment/slurm.py (node preflight in slurm_node_selector.py) and its subclass SpurDeployment in spur.py. madengine drives the scheduler through its local CLI (sbatch, squeue, sacct, scontrol, srun, scancel): run it on a login node; no SSH and no Python SLURM library are involved. The target is chosen by the presence of a slurm key (see Deployment target inference). The sbatch script is rendered from deployment/templates/slurm/job.sh.j2.

madengine must be on PATH of the compute nodes. For templated launchers prepare() runs madengine --version before submission, and the job script re-adds $HOME/.local/bin and the directory the console script was resolved from at submit time (a site may default sbatch to --export=NONE or module load may rewrite PATH). Activate the virtual environment before madengine run.

Selecting and validating the target

ConfigResult
"slurm": {...}SlurmDeployment (registered as slurm)
"slurm": {"scheduler": "spur"} or "deploy": "spur" (with a slurm block)SpurDeployment (registered as spur)
slurm.scheduler other than "", slurm, spurValueError: unknown scheduler
slurm together with k8s/kubernetesValueError: conflicting deployment configuration
slurm together with llm_dValueError: llm-d is Kubernetes-native
"deploy" disagrees with the block present (e.g. "deploy":"slurm" without slurm; "deploy":"local" with slurm; "deploy":"spur" with scheduler: "slurm")ValueError
No slurm block but distributed.launcher is slurm_multiPromoted to a SLURM deployment at run time (see slurm_multi)

Checks live in ConfigLoader.infer_and_validate_deploy_type() (deployment/config_loader.py). Launcher validation runs in BaseDeployment.__init__ so a bad launcher exits with INVALID_ARGS before anything is submitted; see Launcher matrix. validate() requires sbatch, squeue, scontrol (Spur: sbatch, squeue, sacct) and a working sinfo -h.

Preset merge order

ConfigLoader.load_slurm_config() deep-merges three layers (last wins). Nested dicts merge key by key; lists and scalars are replaced; env_vars merges per variable.

  1. presets/slurm/defaults.json
  2. One profile, chosen by the merged slurm.nodes: profiles/multi-node.json when nodes > 1, otherwise profiles/single-node.json
  3. Your --additional-context / --additional-context-file / --config result

The loader also records _preset_env_keys: the env_vars keys that only a preset supplied. The self-managed slurm_multi path drops those keys (the card's own script owns its environment); a key you set yourself is always kept.

presets/slurm/defaults.json and profiles/ contents
PresetKeys
defaults.jsongpu_vendor: AMD, guest_os: UBUNTU, debug: false; slurm: partition: amd-rccl, nodes: 1, gpus_per_node: 8, time: 24:00:00, output_dir: ./slurm_results, exclusive: true, modules: []; distributed: backend: nccl, port: 29500; env_vars: OMP_NUM_THREADS=8, MIOPEN_FIND_MODE=1, MIOPEN_USER_DB_PATH=/tmp/.miopen
profiles/single-node.jsonslurm.nodes: 1, gpus_per_node: 8, time: 12:00:00; env_vars.NCCL_DEBUG=WARN
profiles/multi-node.jsonslurm.nodes: 2, gpus_per_node: 8, time: 24:00:00; distributed.backend: nccl, port: 29500; env_vars: NCCL_DEBUG=WARN, NCCL_DEBUG_SUBSYS=INIT,NET, NCCL_IB_DISABLE=1, NCCL_SOCKET_IFNAME=eth0, TORCH_NCCL_HIGH_PRIORITY=1, GPU_MAX_HW_QUEUES=2, TORCH_NCCL_ASYNC_ERROR_HANDLING=1, NCCL_TIMEOUT=600, HSA_ENABLE_SDMA=0, HSA_FORCE_FINE_GRAIN_PCIE=1, RCCL_ENABLE_HIPGRAPH=0
The multi-node profile disables InfiniBand (NCCL_IB_DISABLE=1, NCCL_SOCKET_IFNAME=eth0). On an IB fabric set env_vars.NCCL_IB_DISABLE and env_vars.NCCL_SOCKET_IFNAME (or slurm.network_interface) yourself. The partition default amd-rccl is a site-specific name; override it on other clusters.

slurm sub-keys

Every key below is read in code. Defaults are the effective value after presets.

KeyDefaultEffect
partitionamd-rccl#SBATCH --partition; also used by the preflight probe and the --build-on-compute build job (required there).
nodes1#SBATCH --nodes. Reconciled with distributed.nnodes: if nodes was not set explicitly, nnodes sizes the allocation; if both are set and differ, slurm.nodes wins with a warning; a non-numeric or < 1 nnodes is ignored with a warning. A card that declares only distributed.nnodes is therefore honoured.
gpus_per_node8#SBATCH --gpus-per-node and GPUS_PER_NODE. The value written is resolved by resolve_runtime_gpus (runtime override, then deployment config, then the card's n_gpus).
time24:00:00 (single-node profile: 12:00:00)#SBATCH --time.
exclusivetrueAdds #SBATCH --exclusive.
account, qosunset#SBATCH --account / --qos. Also applied to the --build-on-compute build job and honoured by the slurm_multi wrapper.
reservationunset#SBATCH --reservation; forwarded to the health-check and cleanup srun calls.
nodelistunset#SBATCH --nodelist (comma list, whitespace stripped). Setting it skips the node health preflight. A bracketed list is expanded before the GPU-architecture probe.
exclude, constraintunset#SBATCH --exclude / --constraint. The preflight appends unhealthy nodes to exclude.
modules[]Emitted as module load <name> lines before anything else runs (also in the slurm_multi wrapper).
output_dir./slurm_resultsWhere sbatch logs, the generated madengine_<model>.sh, task scripts and per-node collection directories go. Must be on a shared filesystem for multi-node and Spur.
network_interfaceunsetExports NCCL_SOCKET_IFNAME and GLOO_SOCKET_IFNAME. If unset and NCCL_SOCKET_IFNAME is empty in the node environment, the script picks ib0 when an ib* link exists, else the default-route interface.
shared_workspaceunsetExplicit shared workspace the job script cds into instead of auto-detecting. Multi-node task scripts still copy the project to a node-local workspace.
results_dirunsetslurm_multi only: directory searched for perf*.csv when the card declares no multiple_results.
skip_gpus_directivefalseOmit --gpus-per-node for clusters without GPU GRES (sbatch would reject it).
gpu_archunsetTarget GPU architecture used to enforce a card's skip_gpu_arch. If unset madengine runs one srun rocminfo on the partition; if that fails the restriction is not enforced and a warning is printed.
enable_node_checktrueRun the node health preflight (see below). Ignored on Spur.
node_check_timeout120Seconds each probe srun may wait. This is a queue wait, so raise it on busy clusters.
verbose_node_checkfalsePer-node progress output.
auto_cleanup_nodesfalseClean dirty nodes in place instead of only excluding them.
allow_submit_without_clean_nodesfalseMulti-node: submit even when fewer clean nodes than nodes were found.
build_gpus_per_nodeunsetBuild job only (--build-on-compute): adds #SBATCH --gpus-per-node. The build otherwise requests no GPUs; set it for schedulers that refuse a GPU-less job (Spur).
schedulerslurmslurm or spur.
rendezvous_timeout900Spur only: seconds a non-zero rank waits for rank 0 (and for peer IP files).

Related top-level keys read by the SLURM path

KeyEffect
distributed.launcher, nnodes, nproc_per_node, backend, port / master_portSee Per-launcher configuration. nnodes defaults to the resolved node count, nproc_per_node to the resolved GPUs per node, backend is exported as DISTRIBUTED_BACKEND. port wins; master_port is used when port is omitted; default 29500.
env_varsExported in the job script (values double-quoted). MIOPEN_USER_DB_PATH is not exported globally; it is set per node and rank.
toolsWith nnodes > 1, configure_multi_node_profiling() checks for rocprofv3 and swaps in MPI-aware tool variants; without it profiling is dropped with a warning.
shared_dataForwarded to the in-job madengine run as --force-mirror-local <value>.
live_outputStreams node output while the job runs; also adds --live-output to the in-job madengine run.
require_pinned_imageslurm_multi: pins DOCKER_IMAGE_NAME to the digest recorded at build time.

What the job script does (templated launchers)

  1. Header. #SBATCH --job-name=madengine-<model> (a / in the card name becomes _), --output/--error=<output_dir>/madengine-<model>_%j_%t.{out,err}, --nodes N --ntasks N --ntasks-per-node=1, --gpus-per-node, --time, then the optional directives from the table above.
  2. Distributed environment. MASTER_ADDR is the first host of SLURM_NODELIST; MASTER_PORT, WORLD_SIZE (= SLURM_NTASKS), LOCAL_RANK, NNODES, GPUS_PER_NODE. GPU visibility: HIP_VISIBLE_DEVICES, ROCR_VISIBLE_DEVICES and CUDA_VISIBLE_DEVICES for ordinary launchers; for the Ray-based vllm, sglang and sglang-disagg only the one matching the detected vendor is set, and RAY_EXPERIMENTAL_NOSET_HIP_VISIBLE_DEVICES is unset.
  3. Single node. Uses the submission directory when it is on nfs*, lustre, gpfs, ceph, beegfs or panfs (judged from the df filesystem-type column only); otherwise a job-scoped local workspace under $SLURM_TMPDIR or /tmp. It writes build_manifest_local.json with deployment_config.target="docker" and the slurm/k8s blocks removed, exports MAD_MULTI_NODE_RUNNER="torchrun --standalone --nproc_per_node=<gpus>", and runs madengine run --manifest-file build_manifest_local.json --timeout <cli value> locally on the node.
  4. Multi node. Writes <output_dir>/madengine_task_<job tag>.sh and runs srun bash <task script>. Each task sets NODE_RANK=$SLURM_PROCID, emits the launcher's environment (MAD_MULTI_NODE_RUNNER etc.), rsyncs the project (excluding .git, caches, virtualenvs, old logs) into a node-local workspace madengine_job_<tag>_node_<rank>, builds the same local manifest, sets MIOPEN_USER_DB_PATH=/tmp/.miopen/node_<rank>_rank_<local> and TORCH_ELASTIC_RDZV_TIMEOUT=3600, and calls madengine run with output redirected to madengine-<model>_<tag>_node_<rank>.{out,err}. Only rank 0 collects performance (MAD_COLLECT_METRICS=true); the others set MAD_SKIP_PERF_COLLECTION=true.
  5. Artifact collection. On success each node copies its logs, *_env.csv, *.log, multi_results*.csv, results*, gpu_info_*.csv, library_trace.csv, the card's multiple_results file, and the profiler directories rocprof_output, rpd_output, rocm_trace_lite_output, torch_profiler_output, tracelens_output to <output_dir>/<model>/<job tag>/node_<rank>/. On failure only the two logs are copied.
  6. Workspace hygiene. A node's workspace is deleted only after a successful run and a complete artifact copy. Workspaces of the same user older than 7 days whose job is no longer in squeue are reclaimed by later jobs.

Environment set by the job script

VariableValue
MAD_DEPLOYMENT_TYPEslurm (also on Spur)
MAD_SLURM_JOB_ID$SLURM_JOB_ID
MAD_COLLECT_JOB_ID${SLURM_ARRAY_JOB_ID:-$SLURM_JOB_ID}; the id collect_results() keys on
MAD_NODE_RANK, MAD_TOTAL_NODESNode rank ($SLURM_NODEID) and node count
MAD_IN_SLURM_JOB1; makes the in-job ContainerRunner remove the cached registry image before pulling (fresh-pull policy)
MAD_LAUNCHER_TYPEResolved launcher name; read back by the in-job ContainerRunner
MASTER_ADDR, MASTER_PORT, WORLD_SIZE, NNODES, GPUS_PER_NODE, LOCAL_RANKDistributed rendezvous (see above)
NODE_RANK, RANKPer-task, multi-node only ($SLURM_PROCID); single node uses 0
DISTRIBUTED_BACKENDdistributed.backend (default nccl)
NCCL_SOCKET_IFNAME, GLOO_SOCKET_IFNAMEslurm.network_interface or auto-detected
TORCH_ELASTIC_RDZV_TIMEOUT3600 (multi-node)
MIOPEN_USER_DB_PATH/tmp/.miopen/node_${SLURM_PROCID}_rank_${LOCAL_RANK:-0} (per task)
HIP_VISIBLE_DEVICES / ROCR_VISIBLE_DEVICES / CUDA_VISIBLE_DEVICES0..gpus_per_node-1; see the Ray note above

Node health preflight

deploy() runs SlurmNodeSelector before sbatch when enable_node_check is true, slurm.nodelist is unset, the scheduler is not Spur, and the run is not in place. It lists idle candidate nodes in the partition (honouring exclude and constraint, at most 100 candidates) and probes them one by one with srun --nodelist=<node> --overlap until enough are clean.

HealthMeaning
CLEANUsable.
DIRTYA GPU with more than 50 GB VRAM in use (read from amdgpu sysfs), or, without sysfs, stale ray::/RayWorkerWrapper/raylet/vllm processes; or Docker's data root has less free space than the largest image to pull and the image is not already present.
UNREACHABLE / UNKNOWNThe probe could not run or its output could not be read.
  • Unhealthy nodes are appended to the job's --exclude. With enough clean nodes the job is pinned to them via --nodelist.
  • Multi-node gate: fewer clean nodes than nodes fails the run ("Not enough clean nodes") unless allow_submit_without_clean_nodes is true. Single-node runs are never gated.
  • Inconclusive checks stand down: if every examined node was unreachable/unknown the check is disregarded (no gate, no pin); measured dirty nodes stay excluded.
  • auto_cleanup_nodes stops only containers labelled madengine.session and kills Ray, vLLM and SGLang processes on a dirty node.
  • Probe jobs are named madengine_nodecheck_<pid>_<time> and always cancelled afterwards.

Admins can additionally install scripts/slurm/epilog.sh as a SLURM epilog to clear stray GPU processes after every job (setup notes in scripts/slurm/SLURM_EPILOG_SETUP.md).

Submission, monitoring and results

  • Submit: sbatch <output_dir>/madengine_<model>.sh. Transient Raft-leader errors ("not the raft leader", "no leader elected", "service is currently unavailable") are retried up to 6 times, 5 s apart.
  • Monitor: BaseDeployment polls every 30 s. squeue -j states RUNNING, PENDING, CONFIGURING, COMPLETING mean keep waiting; COMPLETED is success; any other state (FAILED, CANCELLED, TIMEOUT, NODE_FAIL, ...) is failure. When the job has left the queue the final state comes from sacct -j <id> -n -X -o State, retried 3 times 5 s apart; -X is dropped if the scheduler rejects it. No row at all gives status UNKNOWN, never an assumed success.
  • Results are always collected, even for failed jobs, so failed runs are recorded in perf.csv. The login-node process parses the collected per-node logs and multiple_results CSVs under <output_dir>/<model>/<job id>/node_<rank>/, builds one run record and writes it through the same reporting pipeline as local runs (perf.csv, perf_super.csv/json), so a single cumulative file serves local and distributed runs.
  • Cancel (Ctrl-C, SIGTERM, CI cancel): the lifecycle handler runs cleanup(), which issues scancel <job id>. Inside an allocation the allocation itself is never cancelled; the in-place process group is signalled instead (see below).
  • skip_gpu_arch: for non-Spur SLURM runs the orchestrator drops cards whose skip_gpu_arch names the nodes' architecture before submission (a card on the wrong architecture is reported SKIPPED, not submitted). The filtered manifest is a unique per-run copy <manifest stem>.skip_gpu_arch.<pid>.<uuid>.json beside the build manifest; the build manifest is left untouched, and a failure to write the copy aborts instead of falling back to the original. --disable-skip-gpu-arch turns this off.

Running inside an existing allocation (salloc)

When SLURM_JOB_ID is set madengine detects the allocation and sizes it with scontrol show job (falling back to SLURM_JOB_NUM_NODES, SLURM_NNODES, then the node list). Requesting more nodes than the allocation holds fails; fewer is allowed and prints a note.

CaseBehaviour
Templated launcher, nodes > 1Runs in place: bash <script> synchronously, and the task script is started with srun -N <nodes> -n <nodes> --nodelist "$STEP_NODELIST" so only the requested part of the allocation is used. Rendezvous host and WORLD_SIZE come from that step's hosts (SLURM_STEP_NODELIST).
Templated launcher, single nodeStill submits its own sbatch: the single-node branch calls madengine run inline, which in a shell would run on the login node.
slurm_multiAlways in place (see slurm_multi).
  • Every in-place run gets a discriminator suffix _<YYYYmmddHHMMSS>_<6 hex> appended to the job tag, so several runs in one allocation never share paths. The suffix appears in node workspaces (madengine_job_<job id><suffix>_node_<rank> under $SLURM_TMPDIR or /tmp), logs, collection directories and the DeepSpeed hostfile /tmp/deepspeed_hostfile_<job id><suffix>.txt.
  • The result has skip_monitoring=true (nothing to poll). A non-zero exit is still a failed run, but results are collected so rows from cells that passed are not lost.
  • The timeout is resolved like the job's own madengine would (--timeout, then the card, then the default), so a card allowing longer runs is not cut off.
  • On timeout or cancel the step's process group gets SIGTERM, then SIGKILL (30 s grace on the normal timeout path; immediately from the signal handler). The allocation is left alone.

Spur scheduler ("scheduler": "spur")

Spur (Crusoe) exposes SLURM-compatible CLI shims but differs in ways that break the stock multi-node flow: srun cannot fan out tasks across nodes (SLURM_PROCID is empty), scontrol show hostname[s] is unsupported, and the Raft-based control plane makes squeue/sacct eventually consistent. SpurDeployment reuses the SLURM template, presets and orchestration, and swaps the fan-out mechanism:

AspectStock SLURMSpur
Multi-node fan-outOne job, srun bash task_scriptJob array --array=0-(N-1) of single-node tasks (--nodes=1 --ntasks=1); SLURM_ARRAY_TASK_ID is the node rank and is copied to SLURM_PROCID, SLURM_NODEID, NODE_RANK
Log names%j_%t%A_%a (array id, rank) so results are found by the id sbatch returned
MASTER_ADDRFirst host of the node listShared-filesystem rendezvous under <output_dir>/spur_rendezvous/<array job id>/: rank 0 writes master_addr; peers poll up to slurm.rendezvous_timeout (default 900 s) and fail fast with a diagnostic and a non-zero done_rank<N> marker
Peer list (sglang-disagg)Expanded from the job node listEvery rank publishes ip_rank<N>; the list is built in rank order, waiting up to the rendezvous timeout
SLURM_JOB_IDJob idPinned to SLURM_ARRAY_JOB_ID so ports and shared directories match across tasks
Completion detectionsqueue then sacctPer-rank done_rank<N> exit-code files; squeue (%i %j %T, whitespace-parsed) is only a liveness guard. Fails when ranks stop reporting and no tasks are queued (4 empty polls after tasks were seen), when squeue is unreachable for 20 polls (UNKNOWN), or when no task ever appears in 20 polls
Node health preflightYesSkipped (its srun -w runs on the head node and the pinned multi-node --nodelist would deadlock a one-node-per-task array)
Image pull (slurm_multi)srun on all nodesLocal docker pull in each array task
Required CLIssbatch squeue scontrolsbatch squeue sacct
Submission retriesTransient Raft-leader errors retried (both)
The rendezvous directory must be visible from every node (it lives under slurm.output_dir), and array tasks are not gang-scheduled: raise slurm.rendezvous_timeout when the queue wait between the first and last task can exceed 900 s. skip_gpu_arch pre-filtering applies to stock SLURM only. See the Spur recipe in Config recipes.

Layered model configuration (mad-config.yaml)

Implemented in deployment/layered_config.py; consumed on the slurm_multi path (_prepare_slurm_multi_script). A card may carry one file that holds what travels with a model: how it is served and what is measured. It resolves to a plain environment dict, so no workload script has to change. Run-level and site-level settings stay in --config groups or cluster.sh; see docs/distributed-config.md.

ItemBehaviour
File locationenv_vars.MAD_CONFIG on the card (relative to the card's script directory), else mad-config.yaml next to the script. No file means no effect: absence is never an error.
SchemaYAML map with version: 1 (other versions raise LayeredConfigError), model.env (model-level variables), and benchmark:, a list of entries each with env and an optional kind. An entry without kind is a default for all benchmarks.
Precedence (low to high)model.env → benchmark entries (defaults, then the selected kind) → the card's env_vars → submit-time env_vars (the user's additional_context.env_vars and anything --config composed). Values are stringified.
Benchmark selectionBENCHMARK_SCRIPT, taken from the file's model.env, then a kind-less benchmark entry, then the card's env_vars, then the submit-time override; the winner selects which kind block is merged.
Refused (raises)A site: block (no layer would apply it) and a model.serve block (no launcher receives those flags). Refusing beats silently ignoring a setting that looks applied.
WarningA distributed card whose args contains sbatch-style flags (-N, -n, --nodes, --ntasks, --ntasks-per-node, --gres): madengine passes args to the model script, not to sbatch. Set node count via distributed.nnodes / slurm.nodes.

Kubernetes deployment

Implemented by KubernetesDeployment in deployment/kubernetes.py, composed from mixins. It renders Jinja2 manifests, applies them with the Kubernetes Python client, streams logs, and reads results back through pod logs, kubectl and a results volume. The kubernetes package ships as a core dependency; if it fails to import, the target is unregistered (see Target inference).

Lifecycle (BaseDeployment.execute())

  1. validate — cluster reachable, namespace exists, at least one node advertises gpu_resource_name. madengine never creates a namespace. The GPU-node check blocks CPU-only models too (llm-d attach mode skips it).
  2. prepare — renders ConfigMap, Job and (multi-node) headless Service.
  3. deploy — deletes this job's leftovers, creates the results PVC, Secrets, ConfigMap, Service, then the Job.
  4. monitor — polls the Job; with --live-output it streams pod logs. A Job is finished only on the Complete condition (or succeeded >= completions); Failed condition means failure. A non-zero succeeded count alone is not completion.
  5. collect results — always runs, even after failure, so failed runs are recorded.
  6. cleanup — on deploy failure or Ctrl-C/cancel only: deletes the Job, ConfigMap and Service. After a successful run the Job and its PVC stay unless ttl_seconds_after_finished is set; the next run of the same model replaces them.

Source files

FileConcern
kubernetes.pyKubernetesDeployment: kube target resolution, validate/prepare/deploy/monitor/cleanup, leftover deletion.
k8s_template_context.pyBuilds the Jinja2 context: resources, launcher command, env, tools, data provider, ConfigMap size preflight (about 1 MiB cap).
kubernetes_launcher_mixin.pyGenerates the per-launcher command strings (torchrun, DeepSpeed, TorchTitan, vLLM, SGLang, disaggregated, Megatron-LM). There is one Job template; launchers differ by the generated command, not the template.
k8s_scripts.pyLoads common scripts, tool wrappers and Primus overlays for the ConfigMap; rocEnvTool wiring (rocenv_mode, guest_os).
k8s_pvc.pyResults/data PVCs, storage-class and ReadWriteMany detection, layout selection.
k8s_secrets.pyRegistry-pull and runtime Secrets from credential.json; k8s.secrets strategies.
k8s_names.pyName sanitizers and the 63-character Job-name rule.
k8s_results.pyLog/PVC/artifact collection, per-node aggregation, log-CSV recovery, llm-d result stamping, collector_pod_name().
k8s_slurm_multi.py + k8s_shim/slurm_multi on Kubernetes.
templates/kubernetes/job.yaml.j2, configmap.yaml.j2, service.yaml.j2, pvc.yaml.j2, pvc-data.yaml.j2.

Preset merge order

ConfigLoader.load_k8s_config() deep-merges (later wins; dicts merge, lists and scalars replace, env_vars merge per key):

  1. presets/k8s/defaults.json
  2. presets/k8s/gpu-vendors/amd.json or nvidia.json, chosen by gpu_vendor (default AMD); sets k8s.gpu_resource_name to amd.com/gpu or nvidia.com/gpu. The NVIDIA preset also sets NCCL env vars.
  3. presets/k8s/gpu-vendors/amd-multi-gpu.json — AMD only, when the run is multi-GPU or multi-node: NCCL/RCCL env vars (NCCL_IB_DISABLE=1, NCCL_SOCKET_IFNAME=eth0, GPU_MAX_HW_QUEUES=2, HSA_ENABLE_SDMA=0, …).
  4. One profile, chosen from the merged defaults and user config:
    ProfileSelected whenSets
    multi-node.jsondistributed.nnodes > 1memory 64Gi/128Gi, cpu 16/32, distributed torchrun defaults, NCCL_* env
    multi-gpu.jsongpu_count > 1, or a distributed launcher/enabled, and single nodememory 64Gi/128Gi, cpu 16/32, distributed.launcher=torchrun unless you set one
    single-gpu.jsongpu_count is 1 (the default when unset) and no distributed blockgpu_count=1, memory 16Gi/32Gi, cpu 8/16
    nonee.g. gpu_count: 0 without a distributed blockcode fallbacks: memory 128Gi/256Gi, cpu 32/64
  5. User configuration (runtime context merged over the manifest's deployment_config).

The loader records which k8s and env_vars keys came only from a preset (_preset_k8s_keys, _preset_env_keys), so self-managed slurm_multi cards are not sized or configured by preset guesses.

k8s sub-keys

Only keys read by the code are listed. Defaults come from presets/k8s/defaults.json unless noted.

KeyDefaultMeaning
Placement and access
namespace"default"Must already exist.
kubeconfigunsetFile to use, else $KUBECONFIG (may list several files), else ~/.kube/config, else in-cluster service account. The Python client and every kubectl call use this one file.
contextfile's current contextKube context. It must exist in the kubeconfig or the run refuses rather than fall back to another cluster.
gpu_countprofile-dependent (1 when unset and not distributed)GPUs per pod, requested as gpu_resource_name. Resolved by resolve_runtime_gpus: top-level runtime gpu_count/gpus_per_node/n_gpus in the context, then slurm.gpus_per_node / k8s.gpu_count, then the card's n_gpus, then 1.
gpu_resource_nameamd.com/gpu / nvidia.com/gpu by vendorExtended resource requested and checked in validate().
node_selector{}Map rendered as nodeSelector.
tolerations[]List of {key, operator, value, effect}.
output_dir./k8s_manifestsWhere rendered manifests are written when debug: true (also llm-d values).
Pod and Job
memory, memory_limit, cpu, cpu_limitprofile values; fallback 128Gi/256Gi/"32"/"64"Container requests and limits.
image_pull_policy"Always"
backoff_limit3Job backoffLimit (pods use restartPolicy: Never).
ttl_seconds_after_finishednullAuto-delete finished Jobs.
allow_privileged_profilingnullnull means on when any tools are configured. On adds SYS_PTRACE and an unconfined seccomp profile; it does not make the pod privileged.
Storage (see layouts below)
results_layout, data_layout"auto"auto | shared | per_pod.
nfs_storage_class"nfs-banff"Site-specific default; set it for your cluster. Used for multi-node results and as a data-class fallback.
local_path_storage_class"local-path"ReadWriteOnce class for single-node results and per-pod volumes.
storage_class"local-path"Last-resort fallback for every class below.
multi_node_results_storage_class, single_node_results_storage_classunsetOverride the results class (fallbacks: nfs_storage_class / local_path_storage_class, then storage_class).
data_storage_class"nfs-banff"Class for madengine-shared-data (fallbacks: nfs_storage_class, storage_class).
results_storage_size, data_storage_size10Gi, 100Gi
results_access_modeunsetOverrides the results claim's access mode (default ReadWriteMany for multi-node, else ReadWriteOnce). Lets several pods share one node on a cluster with only RWO storage.
data_pvcunsetName of an existing claim to mount at /data. With a data provider and no value, madengine-shared-data is created or reused.
recreate_shared_data_pvcfalseDelete and recreate madengine-shared-data (back up first).
Secrets
secrets.strategy"from_local_credentials"from_local_credentials | existing | omit.
secrets.image_pull_secret_names["dockerhub-rocm"]Existing Secrets referenced as imagePullSecrets; madengine does not create them.
secrets.runtime_secret_namenullExisting Secret with credential.json; used with existing or omit.
slurm_multi only
volumes, extra_resources, security_context, shm_size, include_script_dirs, host_network, pod_annotations—See slurm_multi on Kubernetes.
Not config keys: host_ipc is derived (true whenever nnodes > 1); a k8s.host_ipc value, including the one in the multi-node profile, is not read. The /dev/shm volume is a fixed 16Gi memory-backed emptyDir on the standard path. HSA_NO_SCRATCH_RECLAIM=1 is added to the pod environment unless you set it.

Results and data volumes

LayoutWhat each pod gets
sharedOne claim <job>-results (label madengine-pvc), mounted at /results by every pod. ReadWriteOnce for one pod, ReadWriteMany for several (unless results_access_mode).
per_podA ReadWriteOnce generic ephemeral volume per pod (class local_path_storage_class). No shared claim is created; the Job completes only when every indexed pod has finished, and performance CSVs are recovered from pod logs.
auto (default)One pod: shared. Several pods: shared if the multi-node results StorageClass can serve ReadWriteMany, else per_pod.

ReadWriteMany support is detected by reading the StorageClass: a provisioner from a known shared-filesystem family (NFS, CephFS, EFS, Azure Files, Filestore) or an existing ReadWriteMany PersistentVolume of that class. A missing or forbidden class counts as unusable. data_layout: auto works the same for madengine-shared-data: an existing RWX claim is reused, an existing non-RWX claim is left unmounted and each pod gets a local volume. Data providers always mount at /data.

Secrets and image pulls

  • With from_local_credentials and a credential.json in the working directory, deploy creates <job>-runtime (mounted at /workspace/credential.json) and, if it holds usable Docker Hub credentials, <job>-registry-pull. Both carry app=madengine. Without the file, no Secret is created and an empty {} credential payload goes into the ConfigMap.
  • There is no top-level secrets context key that turns arbitrary values into a Kubernetes Secret. Put credentials in credential.json, or pre-create a Secret and point k8s.secrets at it.
  • The pod must be able to pull the image. A local ci-… tag or a placeholder such as <your-image> is not pullable: build with --registry or pass a registry reference with --use-image, or the run fails with a ConfigurationError naming the push error. require_pinned_image in the context makes pods pull by digest.

Names and multi-node

  • Job name is madengine-<sanitized model>, capped at 63 characters (Batch Jobs require the job-name label to equal metadata.name); long names are shortened with a hash. The headless Service, pod subdomain and container name share one DNS-label form of it. The ConfigMap is <job>-config.
  • nnodes > 1 makes an Indexed Job (completions=parallelism=nnodes) with a headless Service and pod subdomain so <job>-0 resolves. sglang-disagg always gets the Service and needs at least 3 nodes (pod 0 is a dedicated proxy).
  • vLLM and SGLang are per-replica launchers: each node reports its own metric and results are aggregated across pods.
  • Launcher shape is resolved from launcher.type / launcher.nnodes (older form), then distributed in the run context, then the manifest's deployment_config.distributed, field by field. A runtime distributed block wins over the manifest, and n_gpus: -1 is not used as a process count.
  • The model script directory is bundled recursively (text files only) into the ConfigMap; a payload beyond about 1 MiB is rejected before submission.

Leftovers, cancel and results

  • Names are deterministic, so a rerun replaces its own Job, Secrets, ConfigMap, Service, collector pod and results PVC. Everything madengine creates is labelled app=madengine; a same-named object without that label stops the deploy instead of being deleted (older unlabelled Secrets and collector pods are accepted). Use another namespace in that case.
  • Results land under ./k8s_results/<job>/<pod>/: pod.log, a pvc/ mirror, and log_csv/ files rebuilt from MADENGINE_PERF_CSV_BEGIN/END blocks the pod printed when no volume copy survived.
  • The card's timeout (or --timeout) is applied inside the pod with timeout; 0 means unbounded.
  • If the Kubernetes API is unreachable during cleanup, delete by hand: kubectl delete job,configmap,service -l app=madengine -n <ns> (scope with --context if you use several clusters).

Examples

# Minimal: 4 GPUs, one pod (presets supply everything else)
madengine run --manifest-file build_manifest.json \
  --additional-context '{"k8s": {"namespace": "ml-team", "gpu_count": 4}}'

# Two nodes x two GPUs (examples/k8s-configs/minimal/torchrun-multi-node-minimal.json)
{
  "gpu_vendor": "AMD", "guest_os": "UBUNTU",
  "k8s": {"gpu_count": 2},
  "distributed": {"launcher": "torchrun", "nnodes": 2, "nproc_per_node": 2}
}

More shapes are in examples/k8s-configs/ (minimal/ and basic/: torchrun, DeepSpeed, Megatron-LM, TorchTitan, Primus, vLLM, SGLang, SGLang disaggregated, data provider). Some set host_ipc, which is not read.

slurm_multi on Kubernetes

A slurm_multi card ships its own SLURM batch script that fans out with srun and starts one docker run per node. On Kubernetes madengine runs that script unchanged: one pod per node (an Indexed Job with a headless Service), the script on pod 0, and stand-ins for srun, docker and scontrol first on PATH. Code: deployment/k8s_slurm_multi.py and deployment/k8s_shim/. Select it by giving a card (or distributed.launcher) the slurm_multi launcher together with a k8s block.

How it runs

  1. prepare() builds a ConfigMap holding a tarball of the card's script directory, any include_script_dirs, and the shim; a headless Service (publishNotReadyAddresses, port 29711); and an Indexed Job.
  2. Every pod unpacks the bundle and runs mad_k8s_node.sh, which exports SLURM_JOB_ID, SLURM_JOB_NODELIST, SLURM_NNODES, SLURM_PROCID, SLURM_NODEID and related variables.
  3. Pods 1..N-1 serve tasks from a small TCP agent. Pod 0 starts its own agent, waits for all peers, then runs the card script from the script's directory with the card's args.
  4. The shim's srun sends a task to each named pod's agent and returns the highest exit status. docker run runs the command in that pod: the pod is the container.
  5. Pod 0 publishes /results/<job>-0/perf.csv and exit_code, then tells the peers to shut down.

Sizing

WhatResolved from (first set wins)
Pods (nnodes)distributed.nnodes in the run context (only when its launcher is unset or slurm_multi), the card's distributed.nnodes, the card's slurm.nodes, else 1
GPUs per poda k8s.gpu_count you set (preset-derived values are ignored), the card's slurm.gpus_per_node, the card's n_gpus; otherwise an error
Imageregistry_image, docker_image, the card's docker_image, env_vars.DOCKER_IMAGE_NAME; used as given
CPU / memoryonly if you set k8s.cpu / k8s.memory
Deadlineresolved run timeout becomes activeDeadlineSeconds (omitted when unbounded)

Pod-spec keys (all under k8s)

KeyDefaultEffect
gpu_countcard's slurm.gpus_per_nodeGPUs per pod.
volumesnoneList of mounts, each with host_path (+ optional host_path_type), pvc, or nfs: {server, path}; plus mount_path (defaults to host_path), name, read_only. For model weights the script reads.
extra_resourcesnoneOther device-plugin resources added to requests and limits (for example RDMA devices).
security_contextnoneContainer securityContext (for example IPC_LOCK).
shm_size64GiMemory-backed /dev/shm size.
include_script_dirssibling common/ directory of the script, if presentExtra directories shipped with the card's, so ${SCRIPT_DIR}/../common resolves.
host_networkfalseUse the node's NICs. Adds required pod anti-affinity so no two host-network slurm_multi pods (from any run) share a node, because the shim agent and the card bind fixed ports.
pod_annotationsnoneFor example a Multus k8s.v1.cni.cncf.io/networks entry for a per-pod RDMA interface.
node_selector, tolerations, ttl_seconds_after_finished, namespace, image_pull_policy, results_access_modeas in KubernetesHonoured on this path. backoff_limit is fixed at 0: rerunning one pod of a multi-node run is not a retry of the run.

Shim coverage

Only the forms the MAD launchers use are implemented. Anything else exits with a message naming the unsupported form, so a launcher change that needs more fails loudly instead of running wrongly.

CommandSupportedAccepted and ignoredNot supported
srun-N/--nodes, -n/--ntasks, -w/--nodelist, --ntasks-per-node, flags --overlap --exclusive -l -u -Q-t, -J, --export, -c, --mpi, --gpus, --gpus-per-node, --gres, --jobidMore than one task per node; any other option.
dockerrun (-e, --env-file, -v, --name, -w, --entrypoint, -d), stop, rm, ps, pull, image inspect ({{.Id}} only)Host-level options such as --device, --network, --ipc, --privileged, --shm-size, --ulimit, --cap-add, --rm: they are the pod spec's job.Starting an image other than the pod's own (exit 125); run without a command or --entrypoint; other subcommands.
scontrolshow hostnames, show topology—Everything else.

Environment, results and limits

  • Environment: the pod gets the card's env_vars plus the run's env_vars, minus keys only a preset supplied (for example the AMD multi-GPU preset's NCCL_IB_DISABLE), DOCKER_IMAGE_NAME, and LOG_PATH defaulting to /results/logs (shared by all pods). Internal MAD_K8S_* variables carry node count, name prefix, run id and the card script.
  • Results: pod 0 publishes the file named by the card's multiple_results (found under the script directory or LOG_PATH), else $LOG_PATH/$SLURM_JOB_ID/perf.csv, else ./perf.csv. madengine reads exactly that file from ./k8s_results/<job>/run-<id>/, splits rows into successful and failed by status, and appends them to ./perf.csv. No published CSV is a failed run, never an empty success.
  • Names: pod hostnames are <job>-<index> and the card addresses nodes by them, so dots in card names become dashes and long names are shortened with a hash to keep them valid DNS labels.
  • Storage: this path always uses the shared <job>-results claim, so multi-node needs a ReadWriteMany class (or a results_access_mode override); results_layout: per_pod is not used.
  • Image requirements: the card's image needs bash and python3. The scripts bundle is capped near 900 KiB of base64 so the ConfigMap stays under 1 MiB; narrow include_script_dirs if it is exceeded.
  • The image reference is used as resolved; unlike the standard path it is not checked for being node-pullable.
{
  "k8s": {
    "namespace": "ml-team",
    "gpu_count": 8,
    "volumes": [{"host_path": "/data/models", "mount_path": "/models", "read_only": true}],
    "shm_size": "128Gi"
  },
  "distributed": {"launcher": "slurm_multi", "nnodes": 3}
}

slurm_multi launcher on SLURM

What it is

An escape hatch for workloads that orchestrate their own per-node Docker containers with srun (for example SGLang or vLLM disaggregated serving: proxy, prefill and decode). madengine does not use job.sh.j2 and does not start a container: it generates a small wrapper that runs the card's own .slurm/.sh script directly on the head node, on bare metal, so the script can call srun/scontrol itself without nesting.

It is not a peer of the templated launchers: madengine builds no launcher command for it.

On Kubernetes the same card runs as an Indexed Job; see slurm_multi on Kubernetes.

How to select it

{
  "slurm": {"partition": "gpu", "nodes": 3, "gpus_per_node": 8, "time": "02:00:00"},
  "distributed": {"launcher": "slurm_multi"}
}

Accepted spellings: slurm_multi and the alias slurm-multi (normalised). The launcher may come from the card's distributed block or the run context; the deployment config wins over the card. A card that names the launcher but has no slurm block is still treated as a SLURM deployment. It is rejected on llm-d.

Build modes

Compute nodes pull the image, so a registry reference is required for multi-node runs. madengine build resolves it in this order:

ModeInvocationBehaviour
Explicit image--use-image registry.io/img:tagNo Docker build; the manifest records that image for every selected card. Mutually exclusive with --registry and --build-on-compute.
From the card--use-image autoReads env_vars.DOCKER_IMAGE_NAME from the discovered cards. If cards disagree the first is used with a warning; none found is a ConfigurationError.
Implicitno flags, no --registryIf every slurm_multi card declares the same DOCKER_IMAGE_NAME it is used as if passed via --use-image. Otherwise: ConfigurationError "slurm_multi launcher requires --registry or --use-image" with the options listed.
Build and push locally--registry docker.io/myorgNormal build, push; nodes pull in parallel at run time.
Build on compute--build-on-compute --registry <host/path>One sbatch --wait job (or srun -N1 --ntasks=1 inside an allocation) builds, tags and pushes every selected card. Needs slurm.partition; uses slurm.reservation, slurm.time, and the run job's slurm.account and slurm.qos; requests GPUs only if slurm.build_gpus_per_node is set. Receives the same --build-arg set as a local build (docker_build_arg from the context, then the card). Credentials come from MAD_DOCKERHUB_USER/MAD_DOCKERHUB_PASSWORD or credential.json. Manifest entries carry built_on_compute: true.
Placeholders are rejected. Cards ship DOCKER_IMAGE_NAME as an angle-bracket marker such as <supply-your-image>. A value that is empty, starts with < or ends with > raises ConfigurationError at build time (suggestions: pass --use-image, use --registry, or fix the card) instead of becoming a docker pull failure on every node. At submit time a ci-* local tag on a multi-node run is rejected too: the other nodes cannot pull an image that exists only on the build machine (usually a failed push).

The generated wrapper

  1. Header: --job-name=madengine-<model>, --nodes/--ntasks = resolved node count, --export=ALL, --gpus-per-node (unless skip_gpus_directive), --time, --exclusive (default), and reservation, nodelist, qos, account when set. slurm.nodes is sized from distributed.nnodes when not set explicitly; an explicit slurm.nodes wins with a warning; invalid nnodes is rejected.
  2. unset SLURM_EXCLUSIVE (otherwise every srun inside re-parses it as a step request and fails with "Invalid --exclusive specification"), then module load for slurm.modules.
  3. Environment exports, lowest to highest precedence: layered config (mad-config.yaml), manifest context.docker_env_vars, the card's env_vars, then additional_context.env_vars minus keys only a preset supplied (so the multi-node preset's NCCL_IB_DISABLE=1, NCCL_SOCKET_IFNAME=eth0, HSA_ENABLE_SDMA=0 do not override the card's own fabric detection). Also: xP/yD from distributed.sglang_disagg (prefill_nodes/decode_nodes) when the script has not set them, and MAD_TOOLS=<names> when tools are configured and rocprofv3 is available (madengine cannot wrap a script it does not control; the script decides). Values are shell-quoted.
  4. Node addressing (stock SLURM): MAD_NODE_IPS (rank-ordered, loopback addresses rejected), IPADDRS, SGLANG_NODE_IPS and MAD_NODE_RANK, each only if not already set. Spur builds the equivalent through its rendezvous directory.
  5. Image staging for registry images: one srun --ntasks=$SLURM_NNODES --ntasks-per-node=1 step that logs in using MAD_DOCKERHUB_USER/MAD_DOCKERHUB_PASSWORD (or MAD_DOCKER_USER/MAD_DOCKER_TOKEN; only the names are written to the script, values arrive through --export=ALL), skips nodes that already hold the image, and otherwise pulls. Spur pulls locally in each array task. DOCKER_IMAGE_NAME comes from the registry reference recorded by the push, else an already-resolved card value, else the card's docker_image. With require_pinned_image it is pinned to the build digest.
  6. Run: cd into the script's directory, delete a stale multiple_results file (unless it is one of madengine's own cumulative reports), then bash <script> <args> with set +e so the exit code is always captured. The script name and args are shell-quoted. A madengine_<model>_<job id>.complete marker (exit_code=, timestamp=) is written to slurm.output_dir, and the wrapper exits with the script's exit code.

Execution paths

ContextBehaviour
No SLURM_JOB_IDsbatch of the wrapper. The node health preflight runs as for other launchers (unless nodelist is set or the scheduler is Spur), then monitoring via squeue/sacct (Spur: marker files).
SLURM_JOB_ID set (inside salloc)The wrapper runs with bash, synchronously and in place, for any node count. skip_monitoring=true. The allocation must hold at least nodes nodes. A non-zero exit still collects results (the run stays failed). In-place paths use the raw SLURM_JOB_ID because the card's script builds its own paths from it.

Results aggregation

The card's script writes the full perf schema itself, so it is read directly rather than merged with common run info (re-deriving status from performance would turn a legitimate zero-score FAILURE into SUCCESS). The first source found wins:

  1. The card's multiple_results, searched in the script's directory, <output_dir>/<job id>, <output_dir> and the current directory. A file older than this submission (30 s clock-skew slack) is ignored as stale.
  2. perf*.csv in slurm.results_dir.
  3. /shared_inference/$USER/<job id>/perf.csv (or .../model_blog_logs/<job id>/perf.csv), slurm_output/perf_csv/*<job id>*.csv, then ./perf.csv, retried for about 30 s to ride out NFS propagation.

Rows are then copied (or appended without the header) into ./perf.csv so madengine report to-html and the run summary find them. If the source is ./perf.csv nothing is appended, so rows are never duplicated.

Local execution of a self-managed card

If a self-managed launcher reaches ContainerRunner, it runs the script directly on the host (no Docker): bash <script> (or python3 for .py), working directory = the script's directory, model env_vars and context env_vars injected, and only the keys logged, never the values. The script must be a .sh, .slurm or .py file, or a directory containing run.sh.

llm-d deployment

Benchmarks an llm-d stack (vLLM model servers behind a Gateway API Inference Extension InferencePool). The benchmark client is an ordinary single-pod Kubernetes Job, so LlmdDeployment subclasses KubernetesDeployment and reuses ConfigMap bundling, log streaming, result harvesting and the perf.csv path. Selected by an llm_d key (see Target inference); it cannot be combined with slurm. Code: deployment/llm_d.py, deployment/llm_d_stack.py, deployment/presets/llm-d/defaults.json.

Attach mode safe default

Set llm_d.endpoint_url. madengine runs the client Job against a stack it did not create and never installs or uninstalls anything. Validation checks cluster access and the namespace only; no GPU node is required. dry_run has nothing to render here and fails.

{
  "k8s": {"namespace": "llm-d-bench", "gpu_count": 0},
  "llm_d": {
    "endpoint_url": "http://llm-d-inference-gateway.llm-d-bench.svc.cluster.local:80",
    "model": {"name": "Qwen/Qwen3-32B"}
  }
}

Matches examples/llm-d-configs/01-attach-existing-stack.json.

Managed mode

Leave endpoint_url null. madengine installs three pinned helm releases, waits for readiness, reads the endpoint from the live Gateway, benchmarks, then uninstalls. Requirements, checked in validate():

  • helm on PATH;
  • every llm_d.charts.<name>.version set (shipped as null on purpose, so benchmark numbers are reproducible);
  • llm_d.model.uri, or hf_repo to derive it; model.size when the URI is hf://;
  • the Gateway API CRDs (gateways, httproutes) and an inferencepools CRD in either API group; a GatewayClass named by llm_d.gateway;
  • an existing namespace (never created or deleted), and nodes advertising the GPU resource.

llm_d.model.name is required in both modes. It is the model string sent in requests and recorded in results.

llm_d keys

KeyDefaultMeaning
endpoint_urlnullSet for attach mode.
release_prefix"madengine"Stem of the release names; combined with the card name and kept within 40 characters so <prefix>-modelservice fits helm's 53-character limit.
gateway"agentgateway"GatewayClass name. Known providers (istio, kgateway, agentgateway, gke-l7-regional-external-managed) also set the infra chart provider.
dry_runfalseManaged only: write values and helm template output to k8s.output_dir (llm-d-<component>-values.yaml, llm-d-<component>-manifests.yaml); install nothing, run no Job, touch no cluster. Needs helm and reachable charts.
model.namenullRequired. Becomes MAD_LLM_D_MODEL.
model.urinullArtifact the servers load: hf://org/repo, pvc://<pvc>/<path> or pvc+hf://<pvc>/<path>. Wins over hf_repo.
model.hf_reponullShorthand: uri becomes hf://<hf_repo> (re-downloaded on every standup).
model.cache_pvc, cache_timeout, cache_job_imagenull, 7200, python:3.11-slimWith hf_repo: madengine downloads the repo once onto that PVC in a Job before standup and points the chart at pvc+hf://<cache_pvc>/hf_hub_cache/<hf_repo>. madengine-shared-data is created if missing; any other name must already exist. A failed or timed-out download Job is left for kubectl logs.
model.hf_token_secretnullName of an existing Secret with key HF_TOKEN. Used by model servers, the cache Job and the client; the token never enters a values file or helm command line.
model.sizenullModel volume size; required for hf:// URIs because the chart's own default is unusable (set well above the on-disk size).
prefill / decodereplicas 1, tensor_parallel 1, gpu_count 1Role sizing. prefill.replicas: 0 means aggregated serving: no prefill role and no routing sidecar. image defaults to the selected card's built image; madengine warns, because a client-only image cannot serve. Set it explicitly otherwise.
charts.infra|gaie|modelservice.refllm-d-infra, inferencepool and llm-d-modelservice OCI refsOCI reference or a helm repo alias (helm repo add it first).
charts.*.versionnullMust be pinned in managed mode.
standup_timeout1800Seconds for each helm upgrade --install --wait.
readiness_timeout1800Seconds for the second stage that polls the model-server Deployments (<prefix>-modelservice-*) until every replica is Ready, because helm can return before weights are loaded. Best-effort: if no Deployments are listed or listing is forbidden it relies on helm's gate.
teardowntrueManaged only. false leaves the releases running, also after a failed standup.
extra_values{}Deep-merged last into the chart values; keyed by component (infra, gaie, modelservice) or a bare dict applied to modelservice.
generate_sys_env_details (top-level)falseThe client pod has no GPU, so ROCm environment capture is off by default.

k8s.gpu_count defaults to 0 under llm-d (the client is CPU-only so it does not compete with the stack it measures); an explicit value wins. With gpu_count: 0 and no distributed block no sizing profile applies, so the client pod requests the fallback 128Gi memory and 32 CPUs; set k8s.memory and k8s.cpu explicitly.

Managed flow

  1. Model preparation: ensure madengine-shared-data if the URI names it; run the cache-download Job when cache_pvc is set.
  2. Values: madengine generates chart values for what it models (model, replicas, tensor parallelism, GPU count and accelerator type, auth secret, gateway class, service port, image pull secrets from k8s.secrets.image_pull_secret_names). When prefill.replicas > 0 it also generates the llm-d endpoint-picker configuration, a pinned disaggregation sidecar and NIXL KV-transfer settings with ROCm UCX transports (rocm_copy,rocm_ipc,tcp,self,sm). Non-portable layout (anti-affinity, ClusterIP gateways) belongs in extra_values.
  3. Install three releases in order — <prefix>-infra (Gateway), <prefix>-gaie (InferencePool and endpoint picker), <prefix>-modelservice (prefill/decode servers) — with helm upgrade --install, so a rerun converges. Each release is recorded before helm runs so an interrupted install is still torn down.
  4. Readiness (second stage above), then endpoint discovery: the Gateway labelled app.kubernetes.io/instance=<prefix>-infra (or the only Gateway in the namespace); its status.addresses and an HTTP listener are preferred. Several candidate Gateways, or none with an address, is an error that tells you to set endpoint_url.
  5. Benchmark Job runs with the MAD_LLM_D_* environment below.
  6. Teardown runs on success, on failure, on a failed standup, and on cancel (registered as a signal-handler hook). Releases are uninstalled newest first with --ignore-not-found; a failed uninstall prints the manual helm uninstall command and never masks the benchmark result. teardown: false keeps them.

Client environment contract

VariableValue
MAD_LLM_D_ENDPOINTAttach endpoint_url, or the address resolved from the Gateway.
MAD_LLM_D_MODELllm_d.model.name
MAD_LLM_D_NAMESPACENamespace of the Job.
MAD_LLM_D_PREFILL_REPLICAS, MAD_LLM_D_DECODE_REPLICASReplica counts from the config.
MAD_LLM_D_TPdecode.tensor_parallel
MAD_LLM_D_RELEASE_PREFIXManaged mode only: the helm release stem, for correlating with helm list.
MAD_OUTPUT_CSVThe card's multiple_results file name, if set.
HF_TOKENFrom a Secret reference only: a host MAD_SECRETS_HFTOKEN or HF_TOKEN is stored in a job Secret (<job>-hf-token, removed in cleanup), else model.hf_token_secret is referenced. Never written into the manifest.

Model identity and reporting

  • The selected card's --model_repo (in its args) fills llm_d.model.hf_repo and model.name when unset. A different explicit value is a ConfigurationError, so a run cannot serve one model and record another. Cards without --model_repo need an explicit model.name.
  • Only the first model matched by --tags is benchmarked (the stack serves that model and image); a warning lists the rest.
  • Result rows record deployment_type=llm-d, launcher=vllm (for aggregated and disaggregated serving alike), and serving_mode=aggregated|disaggregated (disaggregated when prefill replicas run). n_gpus, nnodes and gpus_per_node describe the serving pods (read from live pods, falling back to the role config), not the client. The Model column stays the MAD card name.
  • When the card sets multiple_results, that CSV is used in preference to performance: log lines.

Managed example

{
  "k8s": {"namespace": "llm-d-bench", "gpu_count": 0, "gpu_resource_name": "amd.com/gpu"},
  "llm_d": {
    "model": {"hf_repo": "Qwen/Qwen3-0.6B", "name": "Qwen/Qwen3-0.6B", "size": "5Gi"},
    "prefill": {"replicas": 0},
    "decode": {"replicas": 1, "tensor_parallel": 1, "gpu_count": 1, "image": "rocm/vllm:latest"},
    "charts": {
      "infra": {"ref": "llm-d-infra/llm-d-infra", "version": "v1.4.0"},
      "gaie": {"version": "v1.5.0"},
      "modelservice": {"ref": "llm-d-modelservice/llm-d-modelservice", "version": "v0.4.16"}
    }
  }
}

From examples/llm-d-configs/05-managed-qwen3-0.6b-amd.json (aggregated serving, a cheap end-to-end smoke test). The llm-d-infra/… aliases need helm repo add first; see the file's comments. Run 02-managed-dry-run.json first to inspect generated values. Other examples: 03 disaggregated with a model cache PVC, 04 aggregated with teardown: false and extra_values.

madengine run --tags dummy_llm_d \
  --additional-context "$(cat examples/llm-d-configs/05-managed-qwen3-0.6b-amd.json)"
MI250 / AMD path: deploy/llm-d/mi250/ holds the hand-applied helm values that served Qwen3-0.6B on an MI250 cluster: one GCD per pod, no InfiniBand (not an RDMA/MoRI/WideEP path), UCX limited to rocm_copy,rocm_ipc,tcp,self,sm because cuda_copy is absent from the ROCm image. Aggregated serving there needs the InferencePool chart installed without the prefill/decode endpoint-picker override. Its README records a measured aggregated-versus-disaggregated comparison for that cluster.

Launcher matrix

distributed.launcher accepts exactly the names below. Validation (validate_launcher() in deployment/common.py) is case-insensitive and trims whitespace, and runs at every config boundary: CLI validators, BaseDeployment.__init__ (context, manifest deployment_config, and each card), for SLURM, Spur, Kubernetes and llm-d. A second check, ensure_launcher_allowed(), rejects a launcher on a backend that has no arm for it; today that is only slurm_multi on llm-d.

Unknown launchers fail; they no longer run as one process. Anything not listed raises ConfigurationError (exit code 4) naming the source (additional_context.distributed.launcher, a manifest path, a card) and a close-match hint. That includes the old spelling megatron and the reporting sentinels docker and native; omit the key to mean "no launcher". The Hydra group launcher=native is the exception: the translator clears distributed.launcher for it. Hydra's launcher=megatron group is an alias file that still writes megatron-lm.
Canonical nameAccepted aliasesLocal DockerKubernetesSLURM / SpurTypeNotes
torchrun-YesYesYesTrainDefault on SLURM when no launcher is configured.
megatron-lm-YesYesYesTraintorchrun plus Megatron parallelism env vars.
torchtitan-YesYesYesTraintorchrun plus TorchTitan parallelism env vars.
deepspeed-YesYesYesTrainDynamic hostfile from the node list.
primus-YesYesYesTrainMegatron-LM / TorchTitan / MaxText through Primus' run_pretrain.sh; madengine exports PRIMUS_* and no runner.
vllm-YesYesYesInferSelf-managing; per-replica metrics (each node reports its own).
sglang-YesYesYesInferSelf-managing; per-replica metrics.
sglang-disaggsglang_disaggRuns (one node, no topology)YesYesInferMin 2 nodes on SLURM/Spur (co-located or dedicated proxy), 3 on Kubernetes (dedicated proxy).
slurm_multislurm-multiSelf-managedIndexed JobYesMetaRuns the card's own script; no template. Not valid on llm-d. See SLURM and Kubernetes.

What is and is not checked

  • Rejected: any name not in the table (exit 4), and slurm_multi on llm-d (ensure_launcher_allowed(), when LlmdDeployment is constructed).
  • Checked when the job is generated, before submission: sglang-disagg node counts and an explicit prefill/decode split (see the disagg tab under Per-launcher configuration).
  • Warning only: slurm.nodes differing from distributed.nnodes (the explicit slurm.nodes wins), and GPU-count sources that disagree (utils/gpu_config.py).
  • Not cross-checked: two copies of one setting, such as launcher.type vs distributed.launcher or port vs master_port. Each backend reads them in its own order (see the next section), so set only one.
  • llm-d is not a launcher: scale lives in llm_d.prefill / llm_d.decode (see llm-d).

Per-launcher configuration

Common keys under distributed: launcher, nnodes (default: the resolved node count), nproc_per_node (default: resolved GPUs per node), backend (exported as DISTRIBUTED_BACKEND, default nccl), and the rendezvous port as port or master_port (default 29500; port wins on SLURM, master_port on Kubernetes, so set only one). The runtime context overrides the manifest's saved deployment_config, and a card's distributed.launcher is used only when the deployment config names none (resolve_launcher_from_sources).

Single-node SLURM jobs do not emit the launcher block. The template renders the launcher command (MAD_MULTI_NODE_RUNNER and framework variables) only in the per-node task script used when slurm.nodes > 1. A single-node job exports MAD_MULTI_NODE_RUNNER="torchrun --standalone --nproc_per_node=<gpus>" and MAD_LAUNCHER_TYPE, then runs madengine run locally, which resolves the launcher itself (see below). The "1 node" values in the tables describe what the generator produces for nnodes == 1.

Local Docker resolution

ContainerRunner sets MAD_MULTI_NODE_RUNNER unless it is already in docker_env_vars. The launcher is the first non-empty of: additional_context.distributed.launcher, the card's distributed.launcher, MAD_LAUNCHER_TYPE, MAD_LAUNCHER; default torchrun. The GPU count is MAD_RUNTIME_NGPUS.

LauncherMAD_MULTI_NODE_RUNNER
torchrun, megatron-lm, torchtitantorchrun --standalone --nproc_per_node=N
deepspeeddeepspeed --num_gpus=N
vllm, sglang, sglang-disagg, primusempty string (so scripts under set -u do not fail)

Local runs are always one node. An unrecognised name on this path prints a warning and falls back to torchrun, but names normally never get that far because they are validated first.

What reaches the container on SLURM

The job shell exports the launcher's variables, but ContainerRunner forwards into docker run only an allowlist (SLURM_PASSTHROUGH_ENV_VARS in execution/container_runner.py): MASTER_ADDR, MASTER_PORT, WORLD_SIZE, RANK, NODE_RANK, NNODES, NPROC_PER_NODE, MAD_MULTI_NODE_RUNNER, MAD_COLLECT_METRICS, NCCL_SOCKET_IFNAME, GLOO_SOCKET_IFNAME, NCCL_DEBUG, NCCL_IB_DISABLE, NCCL_NET_GDR_LEVEL, PRIMUS_CONFIG_PATH, PRIMUS_CLI_EXTRA, TORCH_ELASTIC_RDZV_TIMEOUT, the three *_VISIBLE_DEVICES, and the SGLANG_DISAGG_*, SGLANG_NODE_IPS, SGLANG_NODE_RANK, SGLANG_TP_SIZE disaggregation variables.

Framework variables such as TENSOR_MODEL_PARALLEL_SIZE, TORCHTITAN_*, VLLM_TENSOR_PARALLEL_SIZE and SGLANG_TENSOR_PARALLEL_SIZE are not on it. A script inside the container that needs them should receive them through docker_env_vars.

Inside a SLURM job the runner also removes the cached registry image (docker rmi -f) before pulling, to avoid corrupted cached layers.

Default launcher on SLURM. The job exports MAD_MULTI_NODE_RUNNER and the model script prepends it to its entry point.

  • nnodes == 1: torchrun --standalone --nproc_per_node=<N>
  • nnodes > 1: torchrun --nnodes=<nnodes> --nproc_per_node=<N> --node_rank=${NODE_RANK} --master_addr=${MASTER_ADDR} --master_port=<port> (node rank from SLURM_PROCID; Spur: the array index)
{
  "slurm": {"partition": "gpu", "nodes": 4, "gpus_per_node": 8, "time": "24:00:00"},
  "distributed": {"launcher": "torchrun", "nnodes": 4, "nproc_per_node": 8, "backend": "nccl", "port": 29500},
  "env_vars": {"NCCL_DEBUG": "WARN", "TORCH_NCCL_ASYNC_ERROR_HANDLING": "1"}
}

Kubernetes: node rank is ${JOB_COMPLETION_INDEX} and MASTER_ADDR is pod 0's headless-service DNS name; see Kubernetes deployment.

Canonical name megatron-lm (the bare string megatron is rejected; Hydra's launcher=megatron group writes megatron-lm). Uses torchrun and exports Megatron parallelism variables from the launcher block:

Variable1 node> 1 node
TENSOR_MODEL_PARALLEL_SIZEmin(nproc_per_node, 8)nproc_per_node
PIPELINE_MODEL_PARALLEL_SIZE1nnodes
CONTEXT_PARALLEL_SIZE11
MAD_MULTI_NODE_RUNNERsame torchrun commands as the torchrun tab
On multi-node SLURM these are exported by the per-node task script, which runs after env_vars are exported, so a same-named env_vars entry is overridden. Express a different TP/PP split in the model's own arguments, or pass values to the container with docker_env_vars.
{
  "slurm": {"partition": "gpu", "nodes": 8, "gpus_per_node": 8, "time": "48:00:00"},
  "distributed": {"launcher": "megatron-lm", "nnodes": 8, "nproc_per_node": 8}
}

torchrun launch plus TorchTitan variables.

Variable1 node> 1 node
TORCHTITAN_TENSOR_PARALLEL_SIZEnproc_per_nodenproc_per_node
TORCHTITAN_PIPELINE_PARALLEL_SIZE1nnodes
TORCHTITAN_FSDP_ENABLEDnot set1
TORCHTITAN_CONTEXT_PARALLEL_SIZEnot set1
{
  "slurm": {"partition": "gpu", "nodes": 4, "gpus_per_node": 8, "time": "24:00:00"},
  "distributed": {"launcher": "torchtitan", "nnodes": 4, "nproc_per_node": 8}
}
  • nnodes == 1: deepspeed --num_gpus=<N>
  • nnodes > 1: each task writes /tmp/deepspeed_hostfile_<job tag>.txt with one <host> slots=<N> line per node (hosts expanded scheduler-portably from SLURM_JOB_NODELIST, or SLURM_STEP_NODELIST in place), and exports deepspeed --hostfile=<file> --master_addr=${MASTER_ADDR} --master_port=<port>. The job tag includes the in-place suffix when running inside an allocation.
{
  "slurm": {"partition": "gpu", "nodes": 8, "gpus_per_node": 8, "time": "48:00:00", "reservation": "ml-priority"},
  "distributed": {"launcher": "deepspeed", "nnodes": 8, "nproc_per_node": 8, "backend": "nccl"}
}

vLLM manages its own processes: no torchrun and no MAD_MULTI_NODE_RUNNER. Multi-node is data parallel: every node runs an independent serve with tensor parallelism only inside that node; there is no cross-node Ray cluster.

Variable1 node> 1 node
VLLM_TENSOR_PARALLEL_SIZEnproc_per_nodenproc_per_node (per node)
VLLM_PIPELINE_PARALLEL_SIZE11
VLLM_DISTRIBUTED_BACKENDautonone

The metric each node reports is its own replica's, not an aggregate (vllm and sglang are the per-replica launchers).

AMD plus Ray: for vllm, sglang and sglang-disagg the SLURM job sets only the visibility variable matching the detected vendor (HIP_VISIBLE_DEVICES on AMD, CUDA_VISIBLE_DEVICES on NVIDIA), unsets the others, and unsets RAY_EXPERIMENTAL_NOSET_HIP_VISIBLE_DEVICES (set by the rocm/vllm image), which would make Ray ignore GPU visibility.
{
  "slurm": {"partition": "gpu", "nodes": 2, "gpus_per_node": 8, "time": "12:00:00"},
  "distributed": {"launcher": "vllm", "nnodes": 2, "nproc_per_node": 8}
}

Kubernetes uses its own env block for vLLM (for example VLLM_DISTRIBUTED_BACKEND=ray per pod with VLLM_HOST_IP); see Kubernetes deployment.

Same model as vLLM: self-managing, data parallel across nodes, no MAD_MULTI_NODE_RUNNER. Exports SGLANG_TENSOR_PARALLEL_SIZE=<nproc_per_node> and SGLANG_PIPELINE_PARALLEL_SIZE=1 for one or many nodes. Same Ray GPU-visibility handling as vLLM.

{
  "slurm": {"partition": "gpu", "nodes": 2, "gpus_per_node": 8, "time": "06:00:00"},
  "distributed": {"launcher": "sglang", "nnodes": 2, "nproc_per_node": 8}
}

Disaggregated prefill/decode serving with SGLang. madengine only publishes the topology; the model script (run.sh) assigns roles per rank and starts the proxy, prefill and decode servers. The alias sglang_disagg is accepted and normalised.

AspectBehaviour
Split configdistributed.sglang_disagg with prefill_nodes and decode_nodes. A top-level sglang_disagg object (what the Hydra sglang_disagg group writes) is not read by the launchers.
Exported (SLURM)SGLANG_DISAGG_MODE=enabled, SGLANG_DISAGG_PREFILL_NODES, SGLANG_DISAGG_DECODE_NODES, SGLANG_DISAGG_TOTAL_NODES, SGLANG_TP_SIZE, MASTER_PORT, SGLANG_NODE_IPS, SGLANG_NODE_RANK
Node rankfirst set of SLURM_PROCID, NODE_RANK, PMI_RANK, SLURM_NODEID, else 0 (Spur leaves SLURM_PROCID empty)
KubernetesPod 0 runs the proxy, pods 1..prefill the prefill servers and the rest the decode servers; server URLs use the headless-Service DNS names.

Topology rules

BackendMinimum nodesProxyValid explicit split
SLURM, Spur2Co-located on the first prefill node, or a dedicated nodeprefill + decode == nnodes (co-located) or == nnodes - 1 (dedicated)
Kubernetes3Dedicatedprefill + decode + 1 == nnodes

Both counts must be at least 1. Without an explicit split, one node is set aside for the proxy and the rest are divided about 40/60: prefill = max(1, (nnodes-1)*2 // 5), decode = nnodes - 1 - prefill; the two-node co-located case is 1 prefill and 1 decode. A violation raises ValueError while the launcher command is generated, so the run fails before anything is submitted.

The peer list SLURM_NODE_IPS is resolved in rank order with getent ahostsv4, skipping 127.* addresses (Ubuntu maps the local hostname to 127.0.1.1); the local node falls back to its NCCL_SOCKET_IFNAME address. An unresolvable peer or an empty list fails the job instead of corrupting the peer list.

{
  "slurm": {"partition": "gpu", "nodes": 5, "gpus_per_node": 8, "time": "04:00:00"},
  "distributed": {
    "launcher": "sglang-disagg", "nnodes": 5, "nproc_per_node": 8,
    "sglang_disagg": {"prefill_nodes": 2, "decode_nodes": 2}
  }
}

Primus (Megatron-LM, TorchTitan, JAX/MaxText) runs through the model script, which calls Primus' run_pretrain.sh; madengine sets no launcher command (MAD_MULTI_NODE_RUNNER is empty locally). Configuration is under distributed.primus, merged from the manifest's deployment_config and the runtime context (runtime wins). The SLURM and Kubernetes launchers export the keys below; a local Docker run exports none of them, so pass them with docker_env_vars or rely on the script's defaults.

KeyMeaning
config_pathPrimus experiment YAML, exported as PRIMUS_CONFIG_PATH. Defaults: exp_pretrain.yaml on SLURM, examples/torchtitan/configs/MI300X/qwen3_1.7B-pretrain.yaml on Kubernetes
cli_extraExtra CLI arguments, exported as PRIMUS_CLI_EXTRA
backendExported as BACKEND. When omitted it is inferred from a model named primus_pretrain/<token>_... (maxtext becomes MaxText; torchtitan, megatron, megatron_bridge, moe_package map to themselves)
{
  "distributed": {
    "launcher": "primus",
    "primus": {"config_path": "examples/megatron/configs/MI300X/deepseek_v2_lite-BF16-pretrain.yaml"}
  }
}

SLURM: the job script supplies NNODES, NODE_RANK, MASTER_ADDR and MASTER_PORT. Kubernetes: the launcher also exports PRIMUS_ROOT (default /workspace/Primus), NNODES, NODE_RANK=${JOB_COMPLETION_INDEX}, GPUS_PER_NODE and pod 0's DNS name as MASTER_ADDR, and bundles scripts/Primus/examples/… into the ConfigMap because the Job's emptyDir hides the image's /workspace.

slurm_multi has no launcher command or environment block of its own: the card's script owns launching. See slurm_multi on SLURM and on Kubernetes.

Config recipes

Working shapes for common scenarios. Partition names, registries and tags are placeholders: replace them with your own. Every key shown is read by the code; more complete files live in examples/slurm-configs/, examples/k8s-configs/, examples/profiling-configs/ and examples/configs/. --additional-context-file and --additional-context can be combined (the inline string is deep-merged over the file); --config cannot be combined with either.

Local Docker, one AMD GPU

madengine run --tags llama3 \
  --additional-context '{"gpu_vendor": "AMD", "guest_os": "UBUNTU", "docker_gpus": "0"}'

docker_gpus is a comma list with ranges ("0,2-4,7"); the default is every detected GPU (0-(N-1)).

Local Docker, four GPUs with a torchrun-style launcher

madengine run --tags megatron-llama3 \
  --additional-context '{
    "gpu_vendor": "AMD",
    "guest_os": "UBUNTU",
    "docker_gpus": "0,1,2,3",
    "distributed": {"launcher": "megatron-lm"}
  }'

Local runs are one node: MAD_MULTI_NODE_RUNNER becomes torchrun --standalone --nproc_per_node=<runtime GPUs>. See Per-launcher configuration.

SLURM, one node, eight GPUs

cat > slurm-single.json <<'EOF'
{
  "slurm": {"partition": "gpu", "nodes": 1, "gpus_per_node": 8, "time": "12:00:00", "exclusive": true},
  "distributed": {"launcher": "torchrun", "nnodes": 1, "nproc_per_node": 8}
}
EOF
madengine build --tags llama3 --registry registry.example.com/ml
madengine run --manifest-file build_manifest.json \
  --additional-context-file slurm-single.json

Run from a shared filesystem on a login node with madengine on PATH. If the working directory is on NFS/Lustre/GPFS the job uses it directly; otherwise files are copied to node-local scratch.

SLURM, four nodes, DeepSpeed, InfiniBand, reservation

cat > slurm-multi.json <<'EOF'
{
  "slurm": {
    "partition": "gpu", "nodes": 4, "gpus_per_node": 8, "time": "24:00:00",
    "exclusive": true, "reservation": "ml-training-q1", "network_interface": "ib0"
  },
  "distributed": {"launcher": "deepspeed", "nnodes": 4, "nproc_per_node": 8, "backend": "nccl"},
  "env_vars": {"NCCL_IB_DISABLE": "0", "NCCL_DEBUG": "WARN", "HSA_ENABLE_SDMA": "0"}
}
EOF
madengine run --manifest-file build_manifest.json \
  --additional-context-file slurm-multi.json

The multi-node preset sets NCCL_IB_DISABLE=1 and NCCL_SOCKET_IFNAME=eth0; the entries above and network_interface override them. Add "nodelist": "node[01-04]" to pin nodes and skip the health preflight.

Inside an existing salloc

salloc -N 2 --gpus-per-node=8 --exclusive
# inside the allocation shell, on the login node:
madengine run --manifest-file build_manifest.json \
  --additional-context '{
    "slurm": {"partition": "gpu", "nodes": 2, "gpus_per_node": 8},
    "distributed": {"launcher": "torchrun", "nnodes": 2, "nproc_per_node": 8}
  }'

With nodes > 1 the templated script runs in place via srun on the allocated nodes (no nested sbatch), blocks until done, and never cancels your allocation. A single-node run still submits its own sbatch.

Spur, two nodes

{
  "slurm": {
    "scheduler": "spur",
    "partition": "gpu", "nodes": 2, "gpus_per_node": 8, "time": "04:00:00",
    "rendezvous_timeout": 1800
  },
  "distributed": {"launcher": "torchrun", "nnodes": 2, "nproc_per_node": 8}
}

Run from a shared filesystem: rank 0 publishes MASTER_ADDR under <output_dir>/spur_rendezvous/ for the other array tasks. For --build-on-compute on Spur add "build_gpus_per_node": 1 (or more) so the build job carries a GPU request. See Spur.

Kubernetes, one pod, four GPUs

madengine run --tags llama3-infer \
  --additional-context '{
    "k8s": {"namespace": "ml-team", "gpu_count": 4, "memory": "128Gi", "cpu": "32"}
  }'

Defaults come from presets/k8s/; see Kubernetes deployment for the full key list. The k8s key (or kubernetes) selects the target; k8s and slurm in one config is an error.

Kubernetes, two nodes, vLLM replicas

{
  "k8s": {
    "namespace": "ml-team", "gpu_count": 4,
    "memory": "256Gi", "memory_limit": "384Gi", "cpu": "64", "cpu_limit": "96"
  },
  "distributed": {"launcher": "vllm", "nnodes": 2, "nproc_per_node": 4, "master_port": 29500},
  "env_vars": {"PYTORCH_CUDA_ALLOC_CONF": "expandable_segments:True"}
}

Each pod runs an independent replica; hostIPC is enabled automatically for nnodes > 1. sglang-disagg needs at least 3 pods on Kubernetes.

SGLang disaggregated serving on SLURM (templated launcher)

{
  "slurm": {"partition": "gpu", "nodes": 3, "gpus_per_node": 8, "time": "04:00:00"},
  "distributed": {
    "launcher": "sglang-disagg", "nnodes": 3, "nproc_per_node": 8,
    "sglang_disagg": {"prefill_nodes": 1, "decode_nodes": 1}
  }
}

Three nodes with prefill + decode == nnodes - 1 means a dedicated proxy node. With prefill_nodes/decode_nodes summing to nnodes the proxy is co-located on the first prefill node; the minimum is 2 nodes on SLURM and 3 on Kubernetes. Omit sglang_disagg for the default split.

Self-managed multi-node (slurm_multi)

# Build modes: pick one
madengine build --tags pyt_sglang_disagg --use-image registry.example.com/sglang:latest
madengine build --tags pyt_sglang_disagg --use-image auto          # DOCKER_IMAGE_NAME from the card
madengine build --tags pyt_sglang_disagg --registry registry.example.com/ml --build-on-compute

madengine run --manifest-file build_manifest.json \
  --additional-context '{
    "slurm": {"partition": "gpu", "nodes": 3, "gpus_per_node": 8, "time": "02:00:00"},
    "distributed": {"launcher": "slurm_multi"}
  }'

Export MAD_DOCKERHUB_USER and MAD_DOCKERHUB_PASSWORD for private images; the nodes log in and pull in parallel. See slurm_multi.

Local run with ROCm profiling

madengine run --tags llama3 \
  --additional-context '{
    "gpu_vendor": "AMD",
    "tools": [
      {"name": "rocprofv3_compute"},
      {"name": "gpu_info_power_profiler", "env_vars": {"POWER_DEVICE": "all", "POWER_SAMPLING_RATE": "0.1"}}
    ]
  }'

Tools stack in list order. On SLURM with nnodes > 1, madengine checks for rocprofv3 and swaps in the MPI-aware variants, or drops profiling with a warning. Tool names and outputs: Profiling & tracing tools.

Profiling & tracing tools

Tools are declared in additional_context.tools as a list of objects and resolved against scripts/common/tools.json (override with madengine run --tools-config FILE). Each definition bundles pre_scripts, a command wrapper (cmd), env_vars and post_scripts. Implementation: execution/container_runner.py::apply_tools(); helper scripts live in scripts/common/{pre_scripts,post_scripts,tools}/.

{
  "tools": [
    {"name": "rocprofv3_compute"},
    {"name": "gpu_info_power_profiler",
     "env_vars": {"POWER_SAMPLING_RATE": "0.5"}},
    {"name": "tracelens", "env_vars": {"TRACELENS_MODE": "rocprof"}}
  ]
}

How tools are applied

AspectBehavior
Per-entry overridesAn entry may add cmd (replaces the definition's wrapper) and env_vars (merged key by key over the definition's). Nothing else is read from the entry.
Order of effectsEach tool prepends its pre_scripts (so a later tool's pre-script runs first), appends its post_scripts (tool order), and prepends its cmd to encapsulate_script (the first tool ends up innermost, wrapping the model command most closely). List a profiler before tracelens so the traces exist when the analysis post-script runs.
Duplicate wrappersIdentical cmd strings are emitted once (the five library tracers share get_library_trace.py), but every tool's env_vars still apply.
Where scripts runInside the container, in the model directory: each script is copied there (cp -vLR) and run as bash <script> <args>. Pre/post script entries are {"path": "...", "args": "...", "timeout": N}; timeout is seconds, 0 means none, default 600.
Failed modelPost-scripts still run on a non-zero model exit, best-effort: one failing collector does not skip the rest, and the run stays failed.
User scriptspre_scripts, post_scripts and encapsulate_script in the context use the same shapes and are combined with the tool-provided ones (tool pre-scripts run before yours; the rocEnvTool step is appended last).
Unknown tool nameLocal Docker raises KeyError at run time. Kubernetes silently skips names missing from tools.json.
slurm_multimadengine cannot wrap a script it does not control, so tools are not applied. After the same rocprofv3 availability check it exports MAD_TOOLS=<comma-separated names> for the card's own script to honour.
Bare metalContext pre_scripts, post_scripts and encapsulate_script run inside the conda prefix. tools and the rocEnvTool step are not applied there.
rocprof* and CAP_SYS_ADMINA tool whose name starts with rocprof or perf makes the local container keep CAP_SYS_ADMIN (without /dev/fuse); ordinary runs no longer request it.
Pick one primary GPU profiler. Do not wrap the same workload with both a rocprof/rocprofv3* tool and rocm_trace_lite. This is documented guidance (docs/profiling.md); the code does not reject the combination.

Tool catalog

tools.json defines every entry below. All rocprof-family tools except rocprofv3 go through tools/rocprof_wrapper.sh, which uses legacy rocprof on ROCm < 7.0 and prefers rocprofv3 on ≥ 7.0. Always keep the -- separator between profiler options and the command.

GPU kernel / API profilers → rocprof_output/ (rpd_output/ for rpd)

ToolCollectsNotes
rpdROCm Profile Data trace (trace.rpd → trace.json)Pre-script installs build deps (apt or yum), clones and builds rocmProfileData; needs network and root or sudo. Output dir rpd_output/.
rocprof--runtime-traceLegacy name; use rocprofv3_* on ROCm ≥ 7.0.
rocprof_hip_only--hip-trace
rocprof_sys--sys-trace
rocprofv3rocprofv3 --runtime-trace -- (direct, no wrapper)Multi-node runs upgrade a plain rocprof entry to this.
rocprofv3_computeHIP + kernel trace, compute_bound.txt counters, pftrace
rocprofv3_memory+ memory copy/allocation trace, memory_bound.txt counters, pftrace
rocprofv3_communicationHIP, HSA, kernel, memcopy, RCCL, scratch-memory trace, pftraceSets RCCL_DEBUG=INFO.
rocprofv3_fullEverything above + marker/runtime trace, full_profile.txt counters, --statsHigh overhead, use short runs. Sets RCCL_DEBUG=INFO, HSA_ENABLE_SDMA=0.
rocprofv3_lightweightHIP + kernel trace as JSONLowest overhead of the family.
rocprofv3_agentRuntime trace, kernel trace, stats as CSV (rocprofv3_output_* prefix)Post-script consolidates CSVs to canonical rocprofv3_output_*.csv names.
rocprofv3_agent_counterAs rocprofv3_agent + instruction_mix.txt countersPost-script also writes instruction_histogram.json from counter/domain-stats CSVs.
rocprofv3_perfettoHIP, kernel, memcopy as pftraceOpen in ui.perfetto.dev; also a TraceLens input.
rocprofv3_api_overheadHIP, HSA, marker, runtime trace + stats as JSON
rocprofv3_pc_samplingPC sampling (time unit, stochastic, interval 1000), pftraceBeta feature in rocprofv3; needs supporting hardware/ROCm.

Counter files: scripts/common/tools/counters/{compute_bound,memory_bound,communication_bound,full_profile,instruction_mix}.txt.

Other tracers and monitors

ToolCollectsOutputNotes
rocm_trace_liteHSA dispatch timeline via rtl trace, RTL_MODE=literocm_trace_lite_output/trace.db (SQLite)Low overhead; skips some dispatches that already carry a completion signal. See below.
rocm_trace_lite_defaultSame, RTL_MODE=defaultsameBroader coverage, higher overhead.
rocblas_trace, hipblaslt_trace, miopen_trace, tensile_trace, rccl_traceLibrary call logs parsed into a config table (sets ROCBLAS_TRACE=1 etc.)library_trace.csv (OUTPUT_FILE)Wrapper get_library_trace.py; TRACE_MODE may list several modes comma-separated. rccl_trace matters for multi-node communication profiling.
gpu_info_power_profilerPower over timegpu_info_power_profiler_output.csvEnv: POWER_DEVICE (all), POWER_SAMPLING_RATE (0.1), POWER_MODE, POWER_DUAL_GCD (false), POWER_OUTPUT_FILE.
gpu_info_vram_profilerVRAM usage over timegpu_info_vram_profiler_output.csvSame shape with VRAM_* variables.
therock_checkRuns detect_therock.sh as a pre-scriptConsole reportTells a TheRock install (pip/tarball) from an apt ROCm.
test_tools_A, test_tools_BEcho wrappers—Test fixtures for the stacking logic; not for real runs.

rocm-trace-lite

RTL ships as GitHub release wheels, not on PyPI. The rocm_trace_lite pre-script (pre_scripts/trace.sh) installs the pinned v0.3.7 linux_x86_64 wheel by default and needs outbound HTTPS to github.com unless the package is baked into the image.

VariableEffect
ROCM_TRACE_LITE_WHEEL_URLInstall this wheel URL instead (wins over everything; kept out of set -x logs because it may embed credentials).
ROCM_TRACE_LITE_FOLLOW_LATEST=1Resolve the latest release wheel via the GitHub API; falls back to the pinned wheel if the lookup fails.
ROCM_TRACE_LITE_GIT_REFGit ref used to rebuild librtl.so natively. Default: the tag of the installed package, else v0.3.7.
ROCM_TRACE_LITE_SKIP_NATIVE_BUILD=1Keep the wheel's librtl.so on images without /opt/rocm.
RTL_MODElite or default; passed as rtl trace --mode when the installed CLI supports it (set by the two tool names).
RTL_WRAPPER_OUTPUT_DIR, RTL_WRAPPER_TRACE_DBOutput directory (default rocm_trace_lite_output) and merged DB path (default <dir>/trace.db).
RTL_WRAPPER_ALLOW_EMPTY=1Exit 0 even when the trace has no kernel dispatches (default: non-zero).
RTL_WRAPPER_KEEP_REGISTER=1Do not set HSA_TOOLS_DISABLE_REGISTER=1 (the wrapper sets it so TheRock's rocprofiler-register does not swallow HSA_TOOLS_LIB).
TheRock images: with no /opt/rocm/include/hsa/hsa.h, the prebuilt library records zero GPU ops, so the pre-script rebuilds librtl.so against the image's HSA headers (hooking hsa_amd_queue_create, which HIP 7.15 uses) and replaces the wheel copy that rtl trace loads. The wrapper deletes stale trace*.db before the run, exports HSA_TOOLS_LIB and RTL_OUTPUT=<dir>/trace_%p.db so every torchrun worker writes its own database, then merges them into trace.db.

torch.profiler on unmodified workloads (dynolog) and TraceLens

torch_profiler_dynolog

Starts the dynolog daemon (--enable_ipc_monitor, port DYNOLOG_PORT, default 1778) and a background trigger that sends dyno gputrace once the workload's PyTorch processes register. A site hook (tools/madengine_kineto_hook.py) imports torch.profiler for the run so optimizer.step() iteration counting works without editing the model. Output: torch_profiler_output/libkineto_trace_<pid>.json per rank.

  • Needs PyTorch ≥ 1.13, x86_64 Debian/Ubuntu image, and HTTPS to fetch the dynolog .deb (DYNOLOG_DEB_URL selects a mirror; baking dynolog and dyno into the image skips the download).
  • Inference-only workloads have no optimizer: set TORCH_PROFILE_ITERATIONS=0 to use duration capture (TORCH_PROFILE_DURATION_MS).

tracelens family

Pre-script installs TraceLens into an isolated venv (TRACELENS_VENV, default /opt/madengine-tracelens-venv; pinned commit, override with TRACELENS_GIT_REF). Post-script post_scripts/tracelens.sh (timeout 7200 s) runs tools/tracelens_analyze.py over whatever traces are in the working directory and writes tracelens_output/ (.xlsx reports, tracelens_summary.csv, tracelens_summary.json). Analysis failure only warns; it never fails the model run.

Variants fix TRACELENS_MODE: tracelens (auto), tracelens_pytorch, tracelens_rocprof, tracelens_pftrace, tracelens_collective. Optional env: TRACELENS_GPU_ARCH, TRACELENS_WORLD_SIZE, TRACELENS_MAX_TRACES.

torch_profiler_dynolog variableDefaultPurpose
TORCH_PROFILE_WARMUP_S60Delay before the first trace request.
TORCH_PROFILE_ITERATIONS5Iterations to capture; 0 switches to duration capture.
TORCH_PROFILE_RETRY_INTERVAL_S / TORCH_PROFILE_MAX_ATTEMPTS15 / 40Retry cadence while no PyTorch process has registered, and the attempt cap.
TORCH_PROFILE_PROCESS_LIMIT64Ranks to trace (dyno's own default of 3 would drop most ranks).
TORCH_PROFILE_RECORD_SHAPES, _WITH_STACKS, _WITH_MODULES1Shapes are required for TraceLens per-operator analysis.
TORCH_PROFILE_OUTPUT_DIR, TORCH_PROFILE_LOG_FILEtorch_profiler_output, libkineto_trace.jsonWhere Kineto writes (copied out of the container's own disk by the stop script).
KINETO_USE_DAEMON, KINETO_DAEMON_INIT_DELAY_S1, 3Register PyTorch/Kineto with the daemon.

To analyze traces after the fact on the host, use madengine report tracelens and tracelens-compare (see report). Example contexts: examples/profiling-configs/ (torch_profiler_tracelens.json, tracelens_rocprofv3.json, rocm_trace_lite*.json, rocprofv3_*.json).

Output collection

TargetWhat is collected
Local DockerPost-scripts copy <tool>_output/ to /myworkspace/, i.e. the host working directory.
SLURMThe job script copies rocprof_output, rpd_output, rocm_trace_lite_output, torch_profiler_output, tracelens_output per node into the results directory.
KubernetesThe pod copies rocprof_output results plus rocm_trace_lite_output, rpd_output, torch_profiler_output, tracelens_output into its /results/<pod>/ directory; the collector then fetches those directories and the GPU-monitor CSVs.
Multi-node profiling policyIf rocprofv3 is on the submit host, plain rocprof is upgraded to rocprofv3 and every node profiles. If it is missing, rocprof-family entries are dropped; other tools (RTL, library tracers) are kept; if nothing remains, profiling is disabled with installation guidance (configure_multi_node_profiling in deployment/common.py).

rocEnvTool (system environment capture)

Unless disabled, pre_scripts/run_rocenv_tool.sh runs before the model as <model>_env <mode> <GUEST_OS>. It is controlled by the CLI flag --sys-env-details (default on) and the context key gen_sys_env_details (the context wins). If the script is not reachable it is skipped with a message rather than failing the run.

rocenv_modeCollects
lite (default)The env_tags.json subset: ROCm info, GPU topology, driver version. Unknown values fall back to lite with a warning.
fullEverything in lite plus hardware information, BIOS, dmesg GPU/DRM logs and amdgpu modinfo. Best-effort installs lshw, dmidecode, kmod when missing, using guest_os (UBUNTU via apt; CENTOS via microdnf/dnf/yum). Any other guest OS skips the install.

ROCm & GPU utilities

Host-side GPU discovery lives in utils/ (rocm_path_resolver, therock_markers, rocm_tool_manager, nvidia_tool_manager, gpu_tool_factory, gpu_validator, gpu_config, gpu_memory_gate) and is driven by core/context.py. Two independent ROCm roots are resolved: the host's and the container's.

Host root (GPU detection, validation, build)

Resolved once by resolve_host_rocm_path() and handed to the tool managers:

  1. Top-level MAD_ROCM_PATH key in additional_context (a context key, not an env var).
  2. If env MAD_AUTO_ROCM_PATH=0: stop scanning; use env ROCM_PATH, else /opt/rocm.
  3. Auto-detect, first valid root wins:
    1. /opt/rocm
    2. Versioned /opt/rocm-* (sorted)
    3. rocm-sdk path --root (TheRock pip) when rocm-sdk is on PATH
    4. Env ROCM_PATH, ROCM_HOME, HIP_PATH (up to four parent directories are tried)
    5. /usr/local/rocm, ~/rocm, ~/therock
    6. Root inferred from rocminfo, amd-smi, rocm-smi on PATH (<root>/bin/<tool>)
  4. Nothing found: env ROCM_PATH, else /opt/rocm.

A directory counts as a ROCm root if it has TheRock markers (share/therock/therock_manifest.json or dist_info.json), bin/rocminfo, both SMI tools, an SMI tool with .info/version, or .info/version alone.

In-container root (AMD Docker runs)

Resolved per model by finalize_container_rocm_path() at run time; the host path is not mirrored into the container.

  1. docker_env_vars.ROCM_PATH in additional_context (the override key is ROCM_PATH, not MAD_ROCM_PATH).
  2. ROCM_PATH, else ROCM_HOME, from the image's OCI config (docker image inspect).
  3. In-image probe (docker run --rm --entrypoint /bin/sh): rocm-sdk path --root, /opt/rocm, /opt/rocm-*, any other /opt/*, /usr/local/rocm.
  4. /opt/rocm with a warning.

A value from an earlier model in the same run is cleared unless the user supplied it, so every image is resolved afresh. The run-phase table prints host and container ROCm root, install type (apt / therock / unknown) and version side by side.

Tool selection and GPU discovery

ConcernBehaviorSource
Vendor detectiongpu_vendor in context wins. Otherwise /usr/bin/nvidia-smi present → NVIDIA; rocm-smi or amd-smi under the ROCm root (or /usr/local/bin/amd-smi) → AMD; else unknown and an error with install hints.gpu_validator.detect_gpu_vendor
Tool managerOne cached manager per (vendor, ROCm root): ROCmToolManager or NvidiaToolManager.gpu_tool_factory
ROCm versionhipconfig --version, then .info/version, then rocminfo. Threshold 6.4.1: ≥ uses amd-smi (falls back to rocm-smi with a warning); below uses rocm-smi; unknown version tries amd-smi first.rocm_tool_manager
GPU countCounts only data rows: amd-smi list --csv rows starting with an integer GPU id, or rocm-smi --showid --csv rows starting with cardN. Group-membership warnings printed before the table are ignored (they once inflated 8 GPUs to 17).ROCmToolManager.get_gpu_count
GPU architectureFirst gfx* token from rocminfo.get_gpu_architecture
gpu_renderDsMaps GPU index to /dev/dri/renderD<N>. Reads drm_render_minor from the KFD topology (if readable). ROCm < 6.4.1: joins KFD unique_id with rocm-smi unique ids. ≥ 6.4.1: amd-smi list -e --json node_id joined to KFD nodes, or, when KFD is not readable (restricted HPC hosts), the render field from amd-smi directly. The output may carry warnings before the JSON; it is parsed tolerantly.Context.get_gpu_renderD_nodes
ValidationROCmValidator checks ROCm root, hipconfig, rocminfo, SMI tools, ROCm version, GPU accessibility, KFD (/dev/kfd, topology) and returns a GPUValidationResult with issues, warnings and suggestions; NVIDIAValidator is the CUDA counterpart. GPUInstallationError carries the formatted report.gpu_validator
GPU count per runPrecedence: runtime context > deployment config (slurm.gpus_per_node / k8s.gpu_count) > model card n_gpus > 1. Aliases: gpus_per_node, gpu_count, n_gpus, num_gpus, ngpus. Disagreeing sources emit a UserWarning.utils/gpu_config.py

--wait-for-gpu-memory

utils/gpu_memory_gate.py blocks a local run until every GPU reports at most 512 MB in use. It runs after the image build and before the first container starts, to absorb slow driver release from a previous job.

ValueBehavior
unsetNo check.
0Check once; fail immediately if any GPU is over the threshold.
N > 0Poll once per second until clear or N seconds of wall-clock time pass (a slow SMI query counts against the budget); then fail.
negativeRejected by the CLI (min=0).
  • Fails closed: if memory cannot be queried (amd-smi metric --mem-usage --json, then rocm-smi --showmeminfo vram --json; nvidia-smi on NVIDIA) or returns no devices, the run is refused with an ExecutionError.
  • The error names each busy GPU and its used MB, and hints at a container left up by --keep-alive.
  • On SLURM and Kubernetes the flag is ignored with a warning, as are --keep-alive, --keep-model-dir and --skip-model-run.

Environment variables

Three groups: variables read by madengine on the host, variables set by madengine for containers, jobs and pods, and the variables the SLURM job script exports. Context keys such as MAD_ROCM_PATH and MAD_CONTAINER_IMAGE live in additional_context, not in the process environment; see additional_context.

Read by madengine

VariableModulePurpose / default
ROCM_PATHrocm_path_resolver, core/constantsHost ROCm root fallback (see resolution order).
ROCM_HOME, HIP_PATHrocm_path_resolverAdditional auto-detect hints for the host root.
MAD_AUTO_ROCM_PATHrocm_path_resolver0 disables the filesystem scan (default 1).
MODEL_DIRcore/constants, discover_models, run_orchestratorModel repository root. Default ..
MAD_SETUP_MODEL_DIRcore/constantstrue copies $MODEL_DIR/* into the working directory at import when it differs from the cwd; skipped for --version/--help.
MAD_VERBOSE_CONFIGcore/constantstrue logs how credential/config values were resolved.
MAD_SECRETS*core/contextAny variable whose name contains MAD_SECRETS is copied into docker_build_arg and docker_env_vars. Values are redacted in logged commands and stripped from the manifest context.
MAD_DOCKERHUB_USER, MAD_DOCKERHUB_PASSWORD, MAD_DOCKERHUB_REPOcore/auth, build_orchestratorDocker Hub credentials; user and password must both be set to override credential.json. Placeholder values such as your-docker-username are ignored.
MAD_SKIP_DOCKER_LOGINcore/auth1 always uses the existing docker login and never logs in.
DOCKER_CONFIGcore/authDocker config dir inspected for ambient credentials (default ~/.docker).
MAD_DOCKER_PULL_TIMEOUT_SECScontainer_runnerdocker pull timeout. Default 1800; a blank, non-integer or non-positive value falls back with a warning.
MAD_DOCKER_BUILDScontainer_runnerShared directory where multi-node runs stage a docker save tar so workers docker load instead of rebuilding.
MAD_BARRIER_TOKENcontainer_runnerToken for the TCP image-sync barrier between nodes. Default JOB<SLURM_JOB_ID>.
MAD_DEPLOYMENT_TYPEcontainer_runnerReported deployment_type; default local. Set by the SLURM script and K8s pod.
MAD_LAUNCHER_TYPE, MAD_LAUNCHERcontainer_runner, run_reportingLauncher fallback after context and model card (the SLURM script exports the former).
MAD_IN_SLURM_JOBcontainer_runner1 inside a madengine SLURM job (default 0); with MAD_DEPLOYMENT_TYPE=slurm it removes the cached registry image before pulling.
MAD_COLLECT_METRICScontainer_runner, run_reportingfalse marks a worker node that reports no metric and still counts as success. Default true.
MASTER_ADDR, MASTER_PORT, WORLD_SIZE, RANK, NODE_RANK, NNODES, NPROC_PER_NODE, GPUS_PER_NODEcontainer_runnerDistributed rendezvous values forwarded into the container; defaults 127.0.0.1 / 29500 for the first two.
SLURM_JOB_ID, SLURM_NNODES, SLURM_JOB_NUM_NODES, SLURM_NODELIST, SLURM_PROCID, USERslurm, spur, build_orchestrator, container_runnerDetect an existing allocation (in-place runs, build-on-compute), node counts, rank and per-user paths.
MIOPEN_USER_DB_PATHcontainer_runnerTaken from the shell environment into docker_env_vars (a value set by the SLURM script is per node/rank).
KUBECONFIGdeployment/kubernetesKubeconfig when k8s.kubeconfig is unset; then ~/.kube/config.
HF_TOKEN, MAD_SECRETS_HFTOKENdeployment/llm_dHugging Face token for managed llm-d model downloads.
MONGO_HOST, MONGO_PORT, MONGO_USER, MONGO_PASSWORD, MONGO_AUTH_SOURCE, MONGO_TIMEOUT_MSdatabase/mongodbSee Database. Defaults localhost, 27017, empty, empty, admin, 5000.
NAS_NODES, MAD_AWS_S3, MAD_MINIO, PUBLIC_GITHUB_ROCM_KEYcore/constantsJSON strings. Precedence: env > credential.json > built-in default. Invalid JSON falls back to defaults with an error log.
MADENGINE_STATE_DIRcore/lifecycleState directory for session records, heartbeats and gpu_wedged.json. Default ~/.madengine.
MADENGINE_SESSION_IDcore/lifecycleSession id labelled on containers (madengine.session); random 12 hex chars if unset.
MADENGINE_HEARTBEAT_INTERVAL, MADENGINE_HEARTBEAT_STALE_AFTERcore/lifecycleContainer dead-man's-switch timing in seconds. Defaults 15 and 120; non-integer or non-positive values fall back.
BUILD_NUMBER, pipelineutils/run_detailsCI build number (default 0) and pipeline name, stamped into build_number / pipeline columns.
TRACELENS_VENVreporting/tracelens_reportInterpreter venv for host-side TraceLens (also read by the in-container scripts).
ROCM_TRACE_LITE_WHEEL_URL, ROCM_TRACE_LITE_FOLLOW_LATEST, ROCM_TRACE_LITE_GIT_REF, ROCM_TRACE_LITE_SKIP_NATIVE_BUILD, RTL_WRAPPER_*, RTL_MODEscripts/common (in container)See rocm-trace-lite.
DYNOLOG_DEB_URL, DYNOLOG_PORT, TORCH_PROFILE_*, TRACELENS_*, POWER_*, VRAM_*, TRACE_MODE, OUTPUT_FILEscripts/common (in container)Tool tuning knobs; set through a tool entry's env_vars. See Profiling tools.

Set by madengine in containers, pods and shims

VariableSet byValue / purpose
MAD_GPU_VENDORcore/contextAMD or NVIDIA.
MAD_SYSTEM_NGPUScore/contextGPUs detected on the host.
MAD_SYSTEM_GPU_ARCHITECTUREcore/contextArchitecture string (gfx90a, ...); also a build arg, and the gpu_architecture perf column.
MAD_SYSTEM_HIP_VERSION, MAD_SYSTEM_GPU_PRODUCT_NAMEcore/contextHIP version and product name (AMD).
MAD_GUEST_OScontainer_runnerguest_os from the context (default UBUNTU).
MAD_RUNTIME_NGPUScontainer_runnerGPUs allocated to this run (a real count on bare metal even for n_gpus: -1).
MAD_MULTI_NODE_RUNNERcontainer_runner, SLURM script, K8s launcher mixinLauncher command prefix, e.g. torchrun --standalone --nproc_per_node=8 on local/single-node.
MAD_MODEL_NAME, MAD_OUTPUT_CSVcontainer_runnerCard name; the multiple_results file name.
ROCM_PATHcontainer_runnerResolved in-container ROCm root (AMD).
JENKINS_BUILD_NUMBERcontainer_runnerFrom BUILD_NUMBER (default 0).
MAD_DATAHOME, MAD_DATANAME, MAD_DATA_PROVIDER_TYPE, MAD_DATA_SIZE, MAD_DATA_DOWNLOAD_DURATIONcore/dataprovider, container_runner, K8s data scriptsWhere a data provider mounted or downloaded the dataset, and its size and timing.
MAD_WORKLOAD, MAD_WORKLOAD_ARGS, MAD_WORKLOAD_CMDentrypoints/run_workload.shWorkload name and args exported by the agnostic entrypoint; MAD_WORKLOAD_CMD is an explicit command override read by it.
MAD_TOOLSdeployment/slurm (slurm_multi)Comma-separated tool names for a self-managed script to honour.
MAD_NODE_IPSdeployment/slurm (slurm_multi)Rank-ordered node IPs; also seeds IPADDRS and SGLANG_NODE_IPS when unset.
MAD_REGISTRY_PASSWORDcore/authPassed only to the docker login --password-stdin subprocess; never on a command line.
MAD_LLM_D_ENDPOINT, _MODEL, _NAMESPACE, _PREFILL_REPLICAS, _DECODE_REPLICAS, _TP, _RELEASE_PREFIXdeployment/llm_dThe contract the llm-d card's run.sh reads; the release prefix is set only for managed stacks.
MAD_K8S_JOB, MAD_K8S_NAMESPACE, MAD_K8S_POD_NAME, POD_INDEXK8s job.yaml.j2Pod identity and index inside the Indexed Job.
MAD_K8S_NNODES, _NODE_PREFIX, _JOB_ID, _IMAGE, _SCRIPT, _ARGS, _AGENT_PORT, _MULTIPLE_RESULTSdeployment/k8s_slurm_multiInputs to the in-pod srun/docker/scontrol stand-ins for slurm_multi cards.
MAD_K8S_AGENT_BIND, _AGENT_PORT, _CONNECT_TIMEOUT, _STATE, _WORKSPACE, _SELFk8s_shim/mad_k8s.pyShim tuning; defaults 0.0.0.0, 29711, 600, /tmp/mad-k8s, /workspace.
MADENGINE_PERF_CSV_BEGIN / _ENDK8s job templateLog markers that bracket the pod's perf CSV so the collector can recover it from logs when the volume is gone.
RAY_EXPERIMENTAL_NOSET_HIP_VISIBLE_DEVICEScontainer_runner, SLURM scriptCleared when HIP_VISIBLE_DEVICES is active so Ray-based AMD images honour GPU visibility.

Exported by the SLURM job script (job.sh.j2)

VariableValue
MAD_DEPLOYMENT_TYPEslurm
MAD_IN_SLURM_JOB1
MAD_SLURM_JOB_ID$SLURM_JOB_ID
MAD_COLLECT_JOB_ID$SLURM_ARRAY_JOB_ID if set (Spur job arrays), else $SLURM_JOB_ID. SLURM_JOB_ID itself is pinned to the array id so every task shares the readiness directory.
MAD_NODE_RANK, MAD_TOTAL_NODES$SLURM_NODEID; node count
MAD_LAUNCHER_TYPEResolved launcher (falls back to torchrun)
MAD_MULTI_NODE_RUNNERSingle node: torchrun --standalone --nproc_per_node=<gpus>; multi-node: built by the launcher dispatch
MAD_COLLECT_METRICStrue on SLURM_PROCID=0, false elsewhere
MAD_SKIP_PERF_COLLECTIONfalse on rank 0, true elsewhere
MAD_CLI_COMMANDmadengine (after the CLI is located; version and path are logged)
MASTER_ADDRFirst host of the job (or step) node list, expanded without scontrol on Spur
MASTER_PORTdistributed.port (else distributed.master_port), default 29500
WORLD_SIZE, NNODES, GPUS_PER_NODESLURM_NTASKS or node count; node count; GPUs per node
RANK, NODE_RANK, LOCAL_RANKPer task: $SLURM_PROCID exported inside the srun task script; node rank; $SLURM_LOCALID (or 0)
HIP_VISIBLE_DEVICES, ROCR_VISIBLE_DEVICES, CUDA_VISIBLE_DEVICESGPU index list. Ray-based launchers (vllm, sglang, sglang-disagg) set only HIP_VISIBLE_DEVICES on AMD or only CUDA_VISIBLE_DEVICES on NVIDIA and unset the others
NCCL_SOCKET_IFNAME, GLOO_SOCKET_IFNAMEslurm.network_interface when set
TORCH_ELASTIC_RDZV_TIMEOUT3600
MIOPEN_USER_DB_PATH/tmp/.miopen/node_${SLURM_PROCID}_rank_${LOCAL_RANK:-0}
SLURM_SUBMIT_DIRDefaulted to the manifest's directory when the scheduler does not set it (Spur)

The SLURM script forwards a fixed passthrough list into the container (rendezvous variables, NCCL_*/GLOO_*, GPU visibility, Primus config, SGLANG_DISAGG_* topology and SGLang node IPs/rank). A launcher variable missing from SLURM_PASSTHROUGH_ENV_VARS in container_runner.py never reaches the model script.

Error types

Defined in src/madengine/core/errors.py. All subclass MADEngineError(Exception), which carries message, category (ErrorCategory), context (ErrorContext: operation, phase, component, model_name, node_id, file_path, additional_info), cause, recoverable and suggestions. ErrorHandler renders a Rich panel (category icon, context, cause, suggestions, "may be recoverable") and logs the same data; create_error_context() builds contexts.

ClassCategoryRecoverableTypical causes
ValidationErrorVALIDATIONyesInvalid CLI args, malformed context values.
NetworkErrorCONNECTIONyesRegistry connectivity, pull failures, database connection.
AuthenticationErrorAUTHENTICATIONyesRegistry login failure, bad credential format.
ExecutionErrorRUNTIMEnoRun-phase failures: no compatible images, GPU memory gate, scheduler errors.
BuildErrorBUILDnoAll builds failed, compute-node build timeout, bare-metal env setup failed everywhere.
DiscoveryErrorDISCOVERYyesNo models matched the tags, unreadable models.json.
OrchestrationErrorORCHESTRATIONnoWorkflow-level failures.
RunnerErrorRUNNERyesRunner-internal failures.
ConfigurationErrorCONFIGURATIONyesConflicting blocks (k8s + slurm, bare_metal + scheduler), slurm_multi without an image source, invalid --registry, unknown launcher, placeholder image, --use-image + --build-on-compute.
DeploymentTimeoutErrorTIMEOUTyesJob exceeded its wall-time budget.

There is no RuntimeError alias; the standard RuntimeError is still raised directly by runner internals (e.g. too many GPUs requested, credentials key missing) and by Context when runtime detection fails. For exit codes see Exit codes.

Credentials & registry login

core/auth.py is the single implementation used by the builder and the runner.

TopicBehavior
Sourcescredential.json in the cwd (must be a JSON object, otherwise a ConfigurationError is reported), then environment MAD_DOCKERHUB_USER + MAD_DOCKERHUB_PASSWORD (+ optional MAD_DOCKERHUB_REPO) which replace the dockerhub entry.
Placeholders ignoredTemplate values such as your-docker-username / your-dockerhub-password-or-token and placeholder repositories (your-org/your-repo) count as not configured. A real value that merely starts with your- (e.g. your-company) still logs in.
Ambient loginIf no usable explicit credentials exist and ${DOCKER_CONFIG:-~/.docker}/config.json already authenticates the registry, that login is reused (never overridden). If explicit credentials are rejected but an ambient login exists, it is used instead of failing the push. MAD_SKIP_DOCKER_LOGIN=1 always defers to the ambient login.
Secret handlingdocker login --username U --password-stdin with the password passed through the MAD_REGISTRY_PASSWORD environment variable of the child process, never on the command line. Console.sh(secret=True) suppresses command echo; redact_secrets() masks MAD_SECRETS*-style assignments and token patterns in logs and in the manifest's build_command.
Failure modelogin_to_registry(raise_on_failure=False) (base-image login during a build, run-phase login) logs and continues so public images still work; push login raises.

Reporting & result files

Writers live in reporting/; row construction is shared by the Docker runner (create_run_details_dict), the bare-metal runner (execution/run_reporting.py) and the SLURM/K8s collectors.

Files a run writes

FileScopeFormat
perf.csvCumulative; rows are appended (path set by --output for local runs)31-column CSV, below
perf_entry.csv, perf_entry.jsonLatest model only, overwritten each modelSame columns (multi-result runs write all rows)
perf_super.jsonCumulative supersetList of records: the perf columns plus configs (parsed config file named in args) and multi_results (extra per-row metrics)
perf_entry_super.json, perf_entry_super.csvLatest entries of this runGenerated from perf_super.json
perf_super.csvCumulativeperf_super.json as CSV; configs and multi_results serialized as JSON strings
.madengine_session_startNext to perf.csvRow count when the run began, so the end-of-run table highlights this session's rows
SLURM and Kubernetes: the deployment layer always appends to ./perf.csv and perf_super.* in the working directory and measures the session offset on that file. When --output names a different file, this session's rows are then appended to it (columns it lacks are dropped, its column order is kept; nothing is copied when it resolves to the same file).

perf.csv columns (31)

Defined once in PERF_CSV_HEADER (reporting/update_perf_csv.py); it must match ContainerRunner.ensure_perf_csv_exists() and the deployment writers.

GroupColumns
Identitymodel, pipeline, build_number, machine_name, git_commit, tags (list flattened to comma-separated), args
Scalen_gpus (total = nnodes × gpus_per_node), nnodes, gpus_per_node, gpu_architecture
How it ranplatform (docker or bare_metal), deployment_type (local, slurm, kubernetes, llm-d), launcher, serving_mode (llm-d only: aggregated or disaggregated; blank otherwise), training_precision, additional_docker_run_options
Imagedocker_file, base_docker, docker_sha, docker_image, build_duration
Resultperformance, metric, relative_change, status, test_duration
Datadataname, data_provider_type, data_size, data_download_duration

Extra columns from a multiple_results CSV are appended after these. Older rows with missing columns are padded with empty strings.

How a row is produced

StepRule
Metric extractionLog line matching performance: <number>[ unit] <metric> (PERFORMANCE_LOG_PATTERN in deployment/base.py, shared by all paths). Fallback: HuggingFace train_samples_per_second, reported with metric samples_per_second.
StatusSUCCESS needs a metric and no error pattern in the log. A worker node (MAD_COLLECT_METRICS=false) or a run with skip_perf_collection succeeds without a metric. Otherwise FAILURE. SKIPPED is written for --skip-model-run and for models skipped by skip_gpu_arch. Failures before the container starts (for example a failed pull) still get a FAILURE row.
multiple_resultsThe card's CSV (looked up in the cwd, then the model directory) must have model, performance, metric columns. Each row becomes a record named <card>_<row model>, SUCCESS if performance is non-null else FAILURE. The CSV is read only when the run succeeded; a failed run writes a single FAILURE row instead. A normal run deletes a leftover results file before starting so an appending script cannot report the previous run; --skip-model-run leaves it.
platform axisFilled by apply_result_axes if the writer did not set it. A legacy bare-metal row that stored bare_metal in deployment_type and conda in launcher is rewritten to platform=bare_metal, deployment_type=local, launcher=native.
launcherOrder: context distributed.launcher, model card distributed.launcher, MAD_LAUNCHER_TYPE/MAD_LAUNCHER. If still empty: docker (local), native (Kubernetes), MAD_LAUNCHER_TYPE or torchrun (SLURM). Bare metal records the card's workload launcher, or native; conda is never a launcher.
llm-d rowsdeployment_type=llm-d, launcher=vllm, serving_mode = disaggregated when prefill replicas run, else aggregated. The model column stays the MAD card name.

Converting and publishing

CommandModuleNotes
madengine report to-html --csv-file-path perf.csvcsv_to_html.pyWrites <name>.html next to the CSV (DataFrame.to_html).
madengine report to-email --directory DIR --output run_results.htmlcsv_to_email.pyOne <h2> section per CSV found under the directory, concatenated into a single HTML.
madengine report tracelens, tracelens-comparetracelens_report.pyDiscover trace artifacts under --root (default .) and run the packaged analyzer with a TraceLens interpreter (--python, TRACELENS_VENV, or the current one). Modes: auto, pytorch, rocprof, pftrace, collective; --discover-only lists without analyzing; default output tracelens_output.
madengine database ...database/mongodb.pySee Database.

Full option lists: CLI — report.

MongoDB upload

madengine database loads a CSV or JSON file and bulk-upserts it into a collection. Implementation: cli/commands/database.py → database/mongodb.py (MongoDBConfig, UploadOptions, UploadResult, MongoDBUploader).

# Latest superset entries, keyed automatically
madengine database -f perf_entry_super.json --db benchmarks -c perf_super

# CSV with an explicit dedup key; validate first
madengine database -f perf.csv --db benchmarks -c runs -k model,build_number --dry-run

Options

OptionDefault
--file, -f requiredCSV or JSON; format from extension, else sniffed ({/[ = JSON)
--database, --db required—
--collection, -c required—
--unique-key, -kComma-separated fields; auto-detected when omitted
--batch-size1000
--no-upsertInsert only (default is upsert)
--no-indexSkip index creation
--dry-runParse, transform and report without writing
--verbose, -vShow the first 10 errors

Connection (environment)

VariableDefault
MONGO_HOSTlocalhost
MONGO_PORT27017
MONGO_USER, MONGO_PASSWORDempty; credentials are used only when both are set
MONGO_AUTH_SOURCEadmin
MONGO_TIMEOUT_MS5000 (server selection timeout)

Behavior

  • Types: CSV values keep native numeric types; cells that look like JSON objects/arrays (configs, multi_results) are parsed into nested documents; NaN becomes null.
  • Metadata: each document gains _meta_uploaded_at (UTC) and, if absent, created_date.
  • Unique key: if not given, the first of model, name, id, timestamp, date, pipeline whose values are unique in the first 100 rows; failing that, model (plus timestamp when present). Upsert matches on those fields with $set. A document missing every key field is silently skipped in upsert mode. --no-upsert uses plain insert instead.
  • Indexes: single-field indexes on the key fields, or on whichever of model, timestamp, date, status, pipeline exist; plus a non-unique compound index when there are several key fields.
  • Exit codes: 0 on success; a partial upload exits 0 if anything was written, else 1; a missing file or total failure exits 1.
  • MONGO_PASSWORD is embedded in the connection URI (mongodb://user:pass@host:port/authSource); use a least-privilege account and keep it out of shell history.

Module reference

Paths are relative to src/madengine/ unless they start with tests/, examples/, docs/ or deploy/.

LayerPathContents
CLIcli/__init__.pyRe-exports app, cli_main, ExitCode, defaults, and helper functions for backward compatibility.
CLIcli/app.pyTyper app: registers 7 commands (build, run, discover, database, cleanup, setup-page, report sub-app); --version; cli_main() installs lifecycle signal handlers and maps uncaught errors to exit 1.
CLIcli/constants.pyExitCode enum (0-5), VALID_GPU_VENDORS, VALID_GUEST_OS, DEFAULT_MANIFEST_FILE, DEFAULT_PERF_OUTPUT, DEFAULT_DATA_CONFIG, DEFAULT_TOOLS_CONFIG, DEFAULT_TIMEOUT=-1.
CLIcli/utils.pysetup_logging(), split_comma_separated_tags(), create_args_namespace(), save_summary_with_feedback(), display_results_table() (per-node breakdown for runs), display_performance_table().
CLIcli/validators.pyContext parsing and validation: merge_additional_context_from_sources(), finalize_additional_context_dict() (defaults, structure, vendor/OS, launcher, platform), validate_additional_context(), process_batch_manifest(), process_batch_manifest_entries().
CLIcli/commands/__init__.pyExports the command functions and report_app.
CLIcli/commands/build.pybuild: batch manifest, --use-image, --build-on-compute, --config handling, mutual-exclusion checks, drives BuildOrchestrator, maps results to exit 0/2/4/1.
CLIcli/commands/run.pyrun: execution-only vs full workflow, timeout sentinel, --wait-for-gpu-memory, --require-pinned-image, --config handling, drives RunOrchestrator, prints result and perf tables.
CLIcli/commands/discover.pydiscover: --tags, --full, --json; wraps DiscoverModels.
CLIcli/commands/report.pyreport_app with to-html, to-email, tracelens, tracelens-compare.
CLIcli/commands/database.pydatabase: MongoDB upload via upload_file_to_mongodb, MongoDBConfig.from_env(), UploadOptions.
CLIcli/commands/cleanup.pycleanup: lists containers labelled madengine.session, skips live owners, reaps via lifecycle.reap(); exit 0/1/5.
CLIcli/commands/setup_page.pysetup-page: calls generate_setup_page().
CLIconfig/__init__.pyload_config(config_args): compose, validate, translate; returns (additional_context, metadata).
CLIconfig/loader.pyHydraConfigLoader: Hydra Compose API, user-YAML / group / value override ordering (_overlay_changed() applies only values that differ from the package defaults).
CLIconfig/schema.pyConfigValidator, KNOWN_TOP_LEVEL_KEYS, SUPPORTED_PLATFORMS.
CLIconfig/translator.pyConfigTranslator.to_additional_context(): YAML keys to context keys (docker.*, log_error.*), launcher canonicalization, reporting-sentinel handling, CLI metadata split.
CLIconfigs/config.yamlHydra root: default groups (platform=docker, scheduler=local, hardware=amd, launcher=none) and base values for model, docker, build, log_error, outputs.
CLIconfigs/{platform,scheduler,hardware,launcher}/Default groups: platforms (docker, bare_metal, podman, singularity), schedulers (local, slurm, k8s), hardware (amd, nvidia, cpu), launchers (torchrun, deepspeed, megatron-lm, megatron, torchtitan, primus, vllm, sglang, sglang_disagg, slurm_multi, native, none).
CLIconfigs/{profile,env,tools,data,build}/Append-only presets selected with +group=name: GPU profiles, NCCL/MIOpen env sets, profiling tool stacks, data providers, build presets.
CLIsetup_page/__init__.pyExports CONTEXT_SCHEMA, SECTIONS, WORKLOADS, collect_models, render_setup_page, generate_setup_page.
CLIsetup_page/generator.pycollect_models(), render_setup_page() (Jinja2, autoescape on, JSON payload embedded), generate_setup_page(), DEFAULT_INSTALL_CMD.
CLIsetup_page/schema.pyData-driven CONTEXT_SCHEMA (key, type, choices, default, deploy_scope, optional CLI flag), SECTIONS, WORKLOADS, validate_schema().
CLIsetup_page/templates/setup_page.html.j2Self-contained HTML/JS template for the setup picker (no external assets).
Orchorchestration/__init__.pyExports BuildOrchestrator and RunOrchestrator.
Orchorchestration/build_orchestrator.pyBuildOrchestrator.execute(): context merge + ConfigLoader, dispatch to Docker build / --use-image / --build-on-compute / bare-metal build, slurm_multi image gate, manifest writing (_save_build_summary, _save_deployment_config, _merge_model_config_into_manifest).
Orchorchestration/run_orchestrator.pyRunOrchestrator.execute(): manifest load/merge, MAD_CONTAINER_IMAGE synthetic manifest, _infer_deployment_target(), local / bare-metal / distributed dispatch, skip_gpu_arch pre-filter for SLURM, _copy_scripts() / _cleanup_model_dir_copies(), GPU-memory gate, session-row export.
Orchorchestration/image_filtering.pyPure filters: filter_images_by_gpu_compatibility() (vendor/architecture) and filter_images_by_skip_gpu_arch() (card skip_gpu_arch, NVIDIA name normalisation).
Orchorchestration/platform.pyresolve_context_platform(): validates platform (docker/bare_metal), rejects the retired bare_metal key, stray conda blocks and scheduler combinations.
Execexecution/__init__.pyExports ContainerRunner.
Execexecution/container_runner.pyContainerRunner: local docker run (AMD/NVIDIA options, least-privilege rules, env/mount/tool injection), image pull/rebuild/tar-cache logic, run_container(), _run_self_managed(), _generate_local_launcher_command(), run_models_from_manifest(), docker_run_cmd write-back.
Execexecution/container_runner_helpers.pyLog error-pattern config and scan (DEFAULT_LOG_ERROR_PATTERNS, resolve_log_error_scan_config()), resolve_run_status(), container-name and run-log path helpers; re-exports resolve_run_timeout.
Execexecution/docker_builder.pyDockerBuilder: Dockerfile discovery (_is_prefix_variant, # CONTEXT filter), build_image(), multi-arch builds, registry naming and push_image() with digest capture, export_build_manifest().
Execexecution/dockerfile_utils.pyDockerfile GPU-architecture variable parsing and normalisation; dockerfile_requires_explicit_mad_arch_build_arg().
Execexecution/run_reporting.pyBackend-independent result handling: extract_performance_from_log(), log_has_errors(), determine_status(), resolve_workload_launcher(), write_perf_records() (perf.csv + perf_super).
Execexecution/bare_metal_runner.pyBareMetalRunner: runs cards inside a conda prefix (conda run -p), GPU visibility env, MAD_RUNTIME_NGPUS, pre/post scripts, cleanup_env, bare-metal run-detail rows.
Execexecution/conda_env.pyCondaEnvManager: conda/mamba/micromamba detection, prefix management under env_root, dependency-hash stamps, optional ROCm/torch pip wheels, vendor-specific file variants.
Corecore/__init__.pyEmpty package marker.
Corecore/additional_context_defaults.pyDEFAULT_GPU_VENDOR="AMD", DEFAULT_GUEST_OS="UBUNTU", apply_build_context_defaults().
Corecore/auth.pyload_credentials(), login_to_registry() (--password-stdin, ambient-login reuse, placeholder detection), resolve_dockerhub_repository(), explain_registry_denial().
Corecore/console.pyConsole.sh() shell executor (timeout, live output, secret=True), redact_secrets(), manifest_safe_context().
Corecore/constants.pyEnv-driven constants: MODEL_DIR, MAD_VERBOSE_CONFIG, MAD_SETUP_MODEL_DIR, NAS_NODES, MAD_MINIO, PUBLIC_GITHUB_ROCM_KEY, legacy get_rocm_path().
Corecore/context.pyContext: parses additional context, build-only vs runtime init, GPU/OS/NUMA detection, MAD_SECRETS* propagation, renderD mapping, filter() for Dockerfile # CONTEXT headers.
Corecore/dataprovider.pyData + providers (custom, local, minio, nas, aws), DataProviderFactory, mirror-local handling.
Corecore/docker.pyDocker container wrapper: heartbeat-watched docker run -t -d, session labels, sh() via docker exec, context-manager teardown.
Corecore/errors.pyError hierarchy (MADEngineError + 10 subclasses), ErrorCategory, ErrorContext, ErrorHandler Rich panels, create_error_context().
Corecore/image_digest.pyPush-digest parsing and resolve_pinned_image() for --require-pinned-image.
Corecore/lifecycle.pyContainer registry and labels, heartbeat thread and watchdog script, reap() escalation, wedged-GPU marker, signal handlers, cancel hooks.
Corecore/timeout.pyDEFAULT_RUN_TIMEOUT=7200, resolve_run_timeout(), subprocess_timeout(), Timeout (SIGALRM) context manager.
Depdeployment/__init__.pyPublic exports: BaseDeployment, DeploymentConfig, DeploymentResult, DeploymentStatus, DeploymentFactory.
Depdeployment/base.pyBaseDeployment (Template Method: validate, prepare, deploy, monitor every 30 s, collect_results, cleanup), DeploymentConfig (incl. cli_timeout), DeploymentResult (incl. skip_monitoring), DeploymentStatus, create_jinja_env(), PERFORMANCE_LOG_PATTERN. Validates and canonicalises every launcher in __init__; registers a cancel hook that calls cleanup().
Depdeployment/common.pyShared SLURM/K8s helpers: VALID_LAUNCHERS, validate_launcher(), ensure_launcher_allowed(), canonicalize_distributed_launcher(), resolve_launcher_from_sources(), resolve_node_count(), resolve_distributed_launch(), is_self_managed_launcher(), is_per_replica_launcher(), launcher_for_reporting(), is_cumulative_report(), multi-node rocprofv3 profiling helpers (configure_multi_node_profiling()).
Depdeployment/config_loader.pyConfigLoader: deep_merge(), load_slurm_config(), load_k8s_config(), load_llmd_config(), infer_and_validate_deploy_type() (slurm / spur / k8s / local, conflict checks), _preset_env_keys provenance, apply_deployment_config().
Depdeployment/factory.pyDeploymentFactory.register()/create(); registers slurm, spur, k8s/kubernetes (UserWarning if the kubernetes package is missing) and llm-d/llm_d (own try/except so it cannot break the others).
Depdeployment/layered_config.pyPer-model mad-config.yaml front end for the slurm_multi path: find_config(), load_config(), resolve_env(), resolve_for_model(), validate_card(), LayeredConfigError; refuses site: and model.serve.
Depdeployment/primus_backend.pyPrimus helpers: merged_primus_config() (manifest + runtime distributed.primus), infer_primus_backend_from_model_name(), infer_primus_examples_overlay_subdirs() (used by the K8s ConfigMap bundler).
Depdeployment/slurm.pySlurmDeployment: sbatch script generation (prepare(), _prepare_template_context(), per-launcher _generate_*_command()), _prepare_slurm_multi_script(), node preflight in deploy(), in-allocation execution (_run_inside_existing_allocation()), monitor() (squeue/sacct), collect_results(), _collect_slurm_multi_results(), cleanup() (scancel).
Depdeployment/slurm_node_selector.pySlurmNodeSelector: select_nodes(), check_node_health() (VRAM, stale Ray/vLLM, Docker free space), cleanup_node(), probe_gpu_arch(), cancel_health_check_jobs(); NodeHealth, NodeStatus; reservation forwarded to every srun.
Depdeployment/spur.pySpurDeployment(SlurmDeployment): job-array fan-out, render_rendezvous_block() shared-filesystem rendezvous, _sglang_node_ips_script(), marker-file monitor(), DEFAULT_RENDEZVOUS_TIMEOUT=900.
Depdeployment/presets/slurm/defaults.json plus profiles/single-node.json and profiles/multi-node.json; selected by slurm.nodes.
Depdeployment/templates/slurm/job.sh.j2The sbatch template for templated launchers: single-node direct run, multi-node per-node task script via srun, Spur array and rendezvous branches, in-place (in_place) branches, per-node workspaces and artifact collection.
Scriptsscripts/slurm/epilog.sh and SLURM_EPILOG_SETUP.md: optional admin-installed SLURM epilog that clears stray Ray/vLLM processes and containers after a job.
Depdeployment/kubernetes.pyKubernetesDeployment: kube target resolution (_resolve_kube_target, kubectl_cmd), validate/prepare/deploy/monitor/cleanup, label-guarded leftover deletion (_delete_if_ours), Job completion helpers (job_finished_successfully). Composes the mixins below.
Depdeployment/k8s_template_context.pyKubernetesTemplateContextMixin: builds the Jinja2 context (resources, launcher command, env, tools, data provider, ConfigMap size check); _k8s_pullable_image().
Depdeployment/kubernetes_launcher_mixin.pyKubernetesLauncherMixin: per-launcher command generators (torchrun, DeepSpeed, TorchTitan, vLLM, SGLang, SGLang disaggregated, Megatron-LM, Primus).
Depdeployment/k8s_scripts.pyKubernetesScriptsMixin: common/tool script loading for the ConfigMap, rocEnvTool wiring (gather_system_env_details), Primus example overlay, K8s tools config.
Depdeployment/k8s_pvc.pyKubernetesPVCMixin: results and shared-data PVCs, RWX detection (provisioner_supports_rwx), resolve_results_layout / resolve_data_layout.
Depdeployment/k8s_secrets.pyRegistry-pull and runtime Secrets from credential.json; k8s.secrets strategies (from_local_credentials, existing, omit); imagePullSecrets resolution; ConfigMap payload estimate.
Depdeployment/k8s_names.pysanitize_k8s_object_name, sanitize_k8s_container_name, sanitize_k8s_label_value; 63-character Job-name cap; pod_job_name_label_selector.
Depdeployment/k8s_results.pyKubernetesResultsMixin: collect_results() (logs, PVC mirror, artifacts), per-node aggregation, materialize_perf_csvs_from_logs, collector_pod_name(), llm-d result stamping (llm_d_serving_mode).
Depdeployment/k8s_slurm_multi.pyKubernetesSlurmMultiMixin: slurm_multi card as an Indexed Job (bundle ConfigMap, headless Service, pod spec from k8s.*), results from pod 0's perf.csv; is_slurm_multi().
Depdeployment/k8s_shim/mad_k8s.py (stand-in srun/docker/scontrol and per-pod TCP task agent) and mad_k8s_node.sh (per-pod entry point that sets SLURM_* and runs the card script on pod 0). Shipped inside the scripts ConfigMap.
Depdeployment/llm_d.pyLlmdDeployment(KubernetesDeployment): attach and managed modes, model identity from --model_repo, model-cache Job, CRD/GatewayClass checks, readiness polling, Gateway endpoint discovery, teardown, MAD_LLM_D_* env.
Depdeployment/llm_d_stack.pyLlmdStack: generates chart values for the infra, gaie and modelservice releases and drives helm upgrade --install, template and uninstall.
Depdeployment/presets/k8s/defaults.json, gpu-vendors/{amd,nvidia,amd-multi-gpu}.json, profiles/{single-gpu,multi-gpu,multi-node}.json.
Depdeployment/presets/llm-d/defaults.jsonllm-d defaults (llm_d block; chart refs, timeouts, teardown). Chart versions intentionally null.
Depdeployment/templates/kubernetes/job.yaml.j2, configmap.yaml.j2, service.yaml.j2, pvc.yaml.j2, pvc-data.yaml.j2.
Scriptsscripts/k8s/tools.json (K8s tool and data-provider definitions), data/download_{aws,local,minio,nas}.sh data-provider scripts, wrappers/run_profiler.sh and run_rocenv.sh.
Repodeploy/llm-d/mi250/Hand-applied helm values for an MI250 llm-d stack (aggregated and disaggregated) with a README of measured results; outside the Python package.
Repoexamples/k8s-configs/minimal/ and basic/ example --additional-context files (including Primus and SGLang disaggregated) plus a README.
Repoexamples/llm-d-configs/Six llm-d contexts: attach, managed dry run, disaggregated, aggregated keep-stack, Qwen3-0.6B smoke tests for AMD and NVIDIA.
Package__init__.pyPackage docstring and __version__ read from installed metadata (dev when uninstalled).
Repreporting/__init__.pyRe-exports PERF_CSV_HEADER, update_perf_csv, flatten_tags, update_perf_super_json, update_perf_super_csv, convert_super_json_to_csv.
Repreporting/update_perf_csv.pyPERF_CSV_HEADER (31 columns), update_perf_csv() for single, multiple and exception results, apply_result_axes() (fills platform, normalizes legacy bare-metal rows), UpdatePerfCsv; writes perf.csv, perf_entry.csv, perf_entry.json.
Repreporting/update_perf_super.pyCumulative perf_super.json (adds configs / multi_results), update_perf_super_json(), generate_perf_entry_super_json(), convert_super_json_to_csv(), update_perf_super_csv().
Repreporting/csv_to_html.pyconvert_csv_to_html() and ConvertCsvToHtml behind madengine report to-html.
Repreporting/csv_to_email.pyfind_csv_files(), convert_directory_csvs_to_html() and ConvertCsvToEmail behind madengine report to-email (one section per CSV).
Repreporting/tracelens_report.pyHost-side TraceLens: resolve_python(), check_tracelens_available(), discover_traces(), generate_tracelens_reports(), compare_tracelens_reports(); drives the packaged tracelens_analyze.py.
DBdatabase/__init__.pyRe-exports MongoDBHandler, MongoDBConfig, UploadOptions, UploadResult, upload_file_to_mongodb, upload_csv_to_mongodb.
DBdatabase/mongodb.pyMongoDBConfig.from_env(), JSON/CSV loaders with type inference, DocumentTransformer (metadata, normalization, unique-key inference), MongoDBUploader (indexes, bulk upsert), upload_file_to_mongodb(), legacy MongoDBHandler.
Utilutils/__init__.pyPackage marker with docstring.
Utilutils/discover_models.pyCustomModel dataclass and DiscoverModels: root, per-directory and dynamic (get_models_json.py) discovery, scoped vs unscoped tag selection, MODEL_DIR setup.
Utilutils/config_parser.pyConfigParser: finds and loads the config file named in a model's args (CSV, JSON, YAML) across MAD, MAD-private and MAD-internal script layouts; match_config_to_result(); get_config_parser().
Utilutils/gpu_tool_manager.pyBaseGPUToolManager ABC: shell execution, tool availability, result cache, logging helpers.
Utilutils/rocm_tool_manager.pyROCmToolManager: version-aware amd-smi / rocm-smi selection (threshold 6.4.1), GPU count, product name, architecture, memory usage, list_gpus_json(); parse_amd_smi_mem_usage(), parse_rocm_smi_mem_usage().
Utilutils/nvidia_tool_manager.pyNvidiaToolManager: nvidia-smi / nvcc wrappers for CUDA and driver version, GPU count, architecture, memory usage.
Utilutils/gpu_tool_factory.pyget_gpu_tool_manager(vendor, rocm_path): cached per (vendor, ROCm root), auto-detects vendor; clear_manager_cache(), get_cached_managers().
Utilutils/gpu_validator.pyGPUVendor, GPUValidationResult, ROCmValidator, NVIDIAValidator, detect_gpu_vendor(), validate_gpu_installation(), GPUInstallationError.
Utilutils/gpu_config.pyGPUConfigResolver and resolve_runtime_gpus(): runtime > deployment > model card > default GPU count with mismatch warnings.
Utilutils/gpu_memory_gate.pycheck_gpu_memory_free() behind --wait-for-gpu-memory (512 MB threshold, 1 s poll, fail-closed).
Utilutils/rocm_path_resolver.pyRocmPathResolver, resolve_host_rocm_path(), finalize_container_rocm_path() (OCI env, in-image probe, default), get_rocm_path_legacy().
Utilutils/therock_markers.pyTheRock share/therock marker paths and is_therock_tree().
Utilutils/session_tracker.pySessionTracker: records the perf.csv row count at run start in .madengine_session_start to highlight this run's rows.
Utilutils/run_details.pyget_pipeline(), get_build_number() (from pipeline / BUILD_NUMBER), flatten_tags_in_place().
Utilutils/log_formatting.pyDataFrame formatting for logs: format_dataframe_for_log(), format_dataframe_rich(), print_dataframe_beautiful().
Utilutils/ops.pyPythonicTee (stdout tee to log file) and file_print().
Utilutils/path_utils.pyget_madengine_root() (package directory) and scripts_base_dir_from().
Scriptsscripts/common/tools.jsonTool catalog consumed by apply_tools() and the K8s ConfigMap builder: pre/post scripts, command wrapper and env per tool.
Scriptsscripts/common/pre_scripts/trace.sh (rpd, rocm_trace_lite, dynolog, tracelens installers), dynolog_start.sh, gpu_info_*_start.sh, gpu_info_pre.sh, run_rocenv_tool.sh, miopen_build_latest.sh, pre_test.sh, and rocEnvTool/ (rocenv_tool.py, csv_parser.py, console.py, env_tags.json; lite and full modes).
Scriptsscripts/common/post_scripts/trace.sh (collects <tool>_output/ to the workspace), dynolog_stop.sh, tracelens.sh, gpu_info_*_stop.sh, gpu_info_post.sh, post_test.sh.
Scriptsscripts/common/tools/Wrappers and helpers: rocprof_wrapper.sh, rtl_trace_wrapper.sh, get_library_trace.py, gpu_info_profiler.py, dynolog_trigger.sh, madengine_kineto_hook.py, tracelens_analyze.py, rocprof_counter_csv_to_instruction_histogram.py, amd_smi_utils.py, rocm_smi_utils.py, pynvml_utils.py, therock_detector.py, detect_therock.sh.
Scriptsscripts/common/tools/counters/rocprofv3 counter lists: compute_bound, memory_bound, communication_bound, full_profile, instruction_mix (.txt).
Scriptsscripts/common/entrypoints/run_workload.shModel-agnostic workload dispatcher used as an encapsulate_script; exports MAD_WORKLOAD / MAD_WORKLOAD_ARGS.
Scriptsscripts/k8s/, scripts/slurm/K8s: data-provider download scripts (download_{local,nas,aws,minio}.sh), profiler/rocenv wrappers, K8s tools.json. SLURM: epilog.sh GPU cleanup and its setup guide.
Repotests/ (repo root)unit/, integration/, e2e/, fixtures/ (dummy MAD repo), conftest.py, scripts/k8s_validate_manifests.sh.
Repoexamples/ (repo root)Ready-to-use contexts: build-manifest/, configs/ (Hydra templates), k8s-configs/, slurm-configs/, llm-d-configs/, profiling-configs/.
Repodocs/ (repo root)Markdown guides (installation, usage, cli-reference, configuration, deployment, launchers, profiling, llm-d, bare-metal, batch-build, distributed-config, contributing), presentation/ and this wiki (wiki/index.html).
Repodeploy/ (repo root)Deployment assets outside the Python package; currently llm-d/mi250/ (MI250 llm-d stack values and notes).

Test layout

unit/

Fast and mocked; conftest.py gives every test an empty HOME and no $KUBECONFIG so results do not depend on the runner's cluster. Areas: CLI and validators, Hydra config (test_hydra_config_loader, test_config_*), SLURM/Spur (test_slurm_*, test_spur), Kubernetes (test_k8s*, test_k8s_shim, test_k8s_slurm_multi), llm-d (test_llm_d*), bare metal, lifecycle/cleanup, ROCm path, reporting, TraceLens, setup page, shell quoting.

integration/

Real Docker, system or chart calls: test_docker_integration, test_container_execution, test_container_cancellation, test_gpu_management, test_orchestrator_workflows, test_platform_integration, test_profiling_tools_config, test_tracelens_tools_config, test_llm_d_charts, test_console_integration.

e2e/

Full workflows against the dummy models: test_build_workflows, test_run_workflows, test_profiling_workflows, test_data_workflows, test_execution_features, test_scripting_workflows, test_tracelens_workflows, plus the dynolog and tracelens dummy pipelines (skipped unless their prerequisites exist).

Fixtures

  • tests/fixtures/dummy/ is a miniature MAD repository: models.json (cards such as dummy, dummy_prof, dummy_torchrun, dummy_vllm, dummy_sglang_disagg, dummy_llm_d, dummy_bare_metal, dummy_therock), docker/ Dockerfiles, scripts/, data.json and a placeholder credential.json (template values are ignored by the login logic).
  • tests/fixtures/dummy_tracelens/, tests/fixtures/configs/, and tests/fixtures/utils.py (cached GPU/vendor probes, skip helpers).
  • tests/conftest.py: mock AMD/NVIDIA/CPU contexts, multi_platform_context (parametrized), sample manifests and build summaries, temp working dirs, and the assert_build_manifest_valid / assert_perf_csv_valid helpers.
  • tests/scripts/k8s_validate_manifests.sh validates rendered Kubernetes manifests.

Markers (registered in pyproject.toml)

MarkerMeaning
unit, integration, e2eTest tier
slowSkip with -m "not slow"
gpu, amd, nvidia, cpuHardware or vendor specific
requires_docker, requires_modelsNeeds a Docker daemon or model fixtures

Markers are opt-in per test (most e2e tests use skipif guards on detected hardware instead). Run a tier by path, not by marker. There is no test workflow in .github/workflows/ (it only holds the setup-page publisher), so run pytest tests/unit and the pre-commit hooks locally before opening a PR.

Contributing & code style

Style rules

  • Formatting: Black, line length 88, targets py38–py311.
  • Imports: isort profile = "black", known_first_party = ["madengine"].
  • Lint: flake8 with --max-line-length=88 --extend-ignore=E203,E501,W503.
  • Types: mypy (python 3.8, check_untyped_defs, no_implicit_optional, warn_unreachable, strict_equality); the hook only checks src/madengine/ and excludes scripts/. Type hints are expected on public functions.
  • Security scan: bandit via pre-commit, configured in pyproject.toml (excludes tests; skips B101).
  • Hygiene hooks: trailing whitespace, EOF newline, YAML/JSON/TOML validity, files > 1000 KB, merge-conflict markers, stray debug statements, LF line endings.
  • Docstrings: Google style.
  • Commits: conventional prefixes feat:, fix:, docs:, test:, refactor:, style:, perf:, chore:. Add a CHANGELOG.md entry for user-visible changes.
  • Reviews: .github/CODEOWNERS assigns default owners.

Security rules (as implemented)

  • Quote shell interpolations. shlex.quote() is used wherever user-controlled values (image names, paths, container names, build args, registry hosts) are interpolated into shell strings, across core/docker.py, docker_builder, container_runner, conda_env, slurm, llm_d_stack and the orchestrators. New code that builds a command string must do the same.
  • Registry passwords never reach a command line. login_to_registry() passes the password in the MAD_REGISTRY_PASSWORD environment variable of the subprocess and pipes it to docker login --password-stdin, run with secret=True.
  • Credential files are validated. credential.json must be a JSON object, otherwise loading fails with a ConfigurationError. Blank and shipped-template values (your-docker-username) are treated as unset so a placeholder never overrides a working ambient docker login.
  • Logs and manifests are scrubbed. Console masks MAD_SECRETS*=... assignments and common token shapes (hf_, sk-, ghp_, xox*-) with ***REDACTED***; manifest_safe_context() removes MAD_SECRETS_* values from the context written to build_manifest.json. Log environment keys, never values.
  • Least privilege containers. Ordinary runs do not request CAP_SYS_ADMIN or /dev/fuse. Both are added only for SSHFS-mounted NAS data; user pre-scripts and rocprof/perf tools keep CAP_SYS_ADMIN without /dev/fuse.
  • Kubernetes cleanup is label-scoped. A rerun deletes only objects labelled app=madengine, so a shared namespace is not swept.

Workflow and PR checklist: docs/contributing.md. Pre-commit usage: .pre-commit-setup.md.

Changelog highlights

A reading aid, not the record: CHANGELOG.md is authoritative and lists every fix with its issue/PR number. Behavior changes are marked.

Unreleased (develop after 2.4.0)

Added

  • Bare-metal conda execution — "platform": "bare_metal" runs inside a conda/mamba/micromamba prefix; build prepares the environment, run enters it with conda run -p. See Platforms.
  • slurm_multi on Kubernetes — a card's SLURM launcher runs unchanged across one pod per node, with stand-ins for srun, docker and scontrol. See slurm_multi on K8s.
  • Spur through the SLURM backend — node-list expansion and final-state lookup work without scontrol show hostnames / sacct -X.

Changed / fixed

  • rocm-trace-lite defaults to the pinned v0.3.7 wheel; Spur sglang-disagg peer list is built from per-rank files.
  • Least-privilege Docker defaults — CAP_SYS_ADMIN and /dev/fuse are no longer requested for ordinary runs (see Platforms).
  • --build-on-compute now forwards slurm.account/qos, GPU requests and --build-args; failed in-allocation runs still collect results.
  • Kubernetes: one kubeconfig/context for the client and kubectl; reruns delete only objects labelled app=madengine.
  • GPU counting ignores amd-smi warning lines; placeholder Docker Hub credentials are ignored.
2.4.0 — 2026-10-18
  • Container lifecycle — in-container heartbeat watchdog, signal-handler teardown, madengine cleanup, and GPU_WEDGED (exit 5). See Container lifecycle.
  • llm-d deployment target (attach and managed modes). See llm-d.
  • Spur (Crusoe) scheduler support via "slurm": {"scheduler": "spur"}.
  • --wait-for-gpu-memory (local runs), madengine setup-page, layered model configuration.
  • SLURM multi-node runs inside an existing allocation run in place with srun; stale multiple_results files are removed before a run.
2.3.0 — 2026-10-09
  • --config Hydra/YAML frontend for build and run. See --config.
  • TraceLens reports (madengine report tracelens, tracelens-compare) and on-demand PyTorch traces (torch_profiler_dynolog). See Profiling tools.
  • docker_run_cmd recorded on build_manifest.json; multi-node Kubernetes without ReadWriteMany storage.
  • Behavior change: unknown launcher names now fail with ConfigurationError (exit 4) instead of running as one process. See Launchers.
2.2.x — 2026-08-12 to 2026-09-24
  • --require-pinned-image digest enforcement for run; per-card docker_build_arg; reuse of an existing docker login; slurm.skip_gpus_directive.
  • Behavior change: --timeout precedence restored to flag > model card > 7200 s default; 0 disables. See run.
  • madengine discover --full and --json; Kubernetes now enforces the card/flag timeout; failed K8s models keep their results.
2.1.x — 2026-05-28 to 2026-07-15
  • slurm_multi launcher on SLURM, --use-image / --build-on-compute, conditional --build-context tools=, local MAD_MULTI_NODE_RUNNER.
2.0.x — 2026-04-09 to 2026-05-26
  • Complete rewrite: unified madengine CLI (legacy mad-* removed), orchestration/deployment/execution layering, Factory + Template Method deployments, presets + Jinja2 templates, structured errors and fixed exit codes, ROCm path resolution and TheRock support, K8s decomposition into focused modules, rocEnvTool full mode.