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.
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
- Discover — finds model cards from
models.jsonor dynamicget_models_json.pyscripts and resolves tags (discover). - 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). - Run — reads the manifest, infers the target from the context, runs the workload, and writes
perf.csv(run, targets). - 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 by | Target | Section |
|---|---|---|
| (nothing below) | Local Docker | Platforms |
"platform": "bare_metal" | Local conda/mamba prefix, no container | Platforms |
"slurm" key | SLURM sbatch (or Spur with scheduler: spur) | SLURM |
"k8s" / "kubernetes" key | Kubernetes Job | Kubernetes |
"llm_d" key | llm-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
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;
kubectland a kubeconfig for Kubernetes;helmfor 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:
| Install | Adds |
|---|---|
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. |
[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.
| Layer | Path | Responsibilities |
|---|---|---|
| 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
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)
- Constructor: merge context sources, fill build defaults (
gpu_vendor=AMD,guest_os=UBUNTUwhen absent), runConfigLoader.load_config()(infers target, rejects conflicting blocks such ask8s+slurmorllm_d+slurm, applies presets). The keys the user really set underslurmare remembered separately from preset defaults. Context(build_only_mode=True): no GPU or NUMA detection.MAD_SYSTEM_GPU_ARCHITECTUREis auto-detected only when the build is part ofmadengine run --tags(detect_local_gpu_arch=True) and the user did not supply it.- Dispatch on mode, in this order:
platform: bare_metal→ create conda prefixes, no Docker (see Execution platforms).--use-image(orauto) → 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-imagetogether with--build-on-computeraisesConfigurationError. - Normal build:
DiscoverModels.run()runs once and is reused. Cards whosedistributed.launcheris a self-managed launcher (slurm_multi) need an image source: with no--registry, a singleenv_vars.DOCKER_IMAGE_NAMEon the cards becomes an implicit--use-image(angle-bracket placeholders are rejected); otherwiseConfigurationError. - Warn when a selected Dockerfile declares
ARG MAD_SYSTEM_GPU_ARCHITECTUREwith no default and no value was resolved. DockerBuilder.build_all_models()builds every matching Dockerfile per card, optionally pushes, and never aborts on a single failure (see Docker build).- Manifest steps, always executed even after partial failure:
export_build_manifest()→ savesummary→ savedeployment_config→ merge the first built card'sdistributed/slurmfields intodeployment_config(user-set values win). - Raise
BuildErroronly when all builds failed. A partial build exits successfully with a warning.
Run flow (RunOrchestrator.execute)
- Start a
SessionTracker: records how many rowsperf.csvalready has (marker file.madengine_session_startbeside it) so the CLI can show only this run's rows. - Manifest source, in priority order:
MAD_CONTAINER_IMAGEin context (needs--tags; validates or pulls the image, writes a synthetic manifest withlocal_image_mode); else an existing--manifest-file; else build first (--tags). Neither tags nor a manifest →ConfigurationError. _load_and_merge_manifest(): runtime context overrides the storeddeployment_configfordeploy slurm k8s kubernetes llm_d platform conda distributed vllm env_vars debug;tools,pre_scripts,post_scripts,encapsulate_script,require_pinned_imageare written into manifestcontext. A runtimeslurmblock replaces the stored one wholesale and resets the explicit-key provenance.- Stored
deployment_configkeys the runtime did not set are copied into the working context. - Target inference (see Deployment targets):
platform: bare_metal→llm_d→k8s/kubernetes→slurm(spurwhenslurm.schedulerordeployisspur) → a self-managed launcher on the first built card impliesslurm→ otherwiselocal. A legacydeployment_config.targetis the final fallback. - Dispatch:
Target Executor local/dockerContainerRunner.run_models_from_manifest()bare_metalBareMetalRunner.run_models_from_manifest()slurm,spur,k8s,llm-dDeploymentFactory.create(DeploymentConfig).execute() - Always, in a
finally-style path:_cleanup_model_dir_copies()removes the madengine-provided files underscripts/common/.
Local Docker path (_execute_local)
- Runtime
Context(full system + GPU detection), optionaldata.jsonloaded asData, node info table. - Restore host-level fields from the manifest
contextwithout 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. - Filter
built_images: GPU vendor/architecture compatibility (orchestration/image_filtering.py; an image withoutgpu_vendoris treated as compatible), thenskip_gpu_archfrom each card (unless--disable-skip-gpu-arch). Skipped cards are written toperf.csvwith statusSKIPPED. The whole step is best effort: any exception, including theExecutionErrorraised when no image is compatible, is caught, printed as a warning, and the run proceeds with all images. _copy_scripts(): honours an externalMODEL_DIR(merges itsdocker/andscripts/into the cwd) and copiespre_scripts,post_scripts,tools,entrypoints,tools.json,test_echo.shfrom the madengine package intoscripts/common/. The MAD project's ownscripts/anddocker/are never deleted.--wait-for-gpu-memorygate (local only), thenContainerRunnerruns each manifest entry. Distributed targets print a warning and ignore--keep-alive,--keep-model-dir,--skip-model-runand--wait-for-gpu-memory.- For a full workflow the build and run logs are merged into one
*.live.log.
Distributed path (_execute_distributed)
- For target
slurmonly,skip_gpu_archis decided before submission (a filtered per-run copy of the manifest is submitted; the build manifest is left intact). Skipped cards appear asSKIPPEDrows. An empty manifest returns aFAILURErow instead of exiting 0. Spur, Kubernetes and llm-d do not apply this pre-filter. - Manifest
context.docker_env_varsare handed to the deployment layer (runtime keys win), because SLURM/K8s never enter the local restore step. - Session baseline is measured on the working
perf.csv(what the deployment appends to), and this run's rows are exported to--outputafterwards when it names another file. DeploymentConfigcarriestimeout(resolved against the default) andcli_timeout(verbatim, so the innermadengine runon the node still lets the model card outrank the default);monitorandcleanup_on_failure(both defaulttrue) are read from context.- 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
| Setting | Order, highest first |
|---|---|
| Run timeout | explicit --timeout > model card timeout > 7200 s (core/timeout.py). Non-positive resolved value = no timeout. |
| GPU count | top-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 container | runtime-detected docker_env_vars are kept over manifest copies; additional_context.docker_env_vars then overwrite per key |
| Log error-scan settings | context > model card > built-in defaults |
| Deployment config | runtime --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.
| Command | Source | Purpose |
|---|---|---|
discover | cli/commands/discover.py | List / select model cards by tag; print JSON. |
build | cli/commands/build.py | Build (or adopt) Docker images, or prepare conda envs for bare-metal; write build_manifest.json. |
run | cli/commands/run.py | Run from a manifest, or build + run when no manifest exists; write perf.csv. |
report (sub-app) | cli/commands/report.py | to-html, to-email, tracelens, tracelens-compare. |
database | cli/commands/database.py | Upload a CSV/JSON results file to MongoDB. |
cleanup | cli/commands/cleanup.py | Remove benchmark containers orphaned by interrupted runs. |
setup-page | cli/commands/setup_page.py | Generate a self-contained HTML "setup picker" from the repo's models.json. |
Global behavior
madengine --versionprints the installed package version (importlib.metadata;unknownif not installed). With no sub-command the help is shown. Shell completion is disabled (add_completion=False).cli_main()first callscore.lifecycle.install_signal_handlers()soSIGINT/SIGTERM/SIGHUPtear down benchmark containers (see container lifecycle). AKeyboardInterruptor an unhandled exception that escapes a command exits1.- 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:
| Input | Format | Rules |
|---|---|---|
--additional-context, -c | String; 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, -f | JSON 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 overrides | Cannot 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.
- Missing
gpu_vendor/guest_osare defaulted toAMD/UBUNTU(core/additional_context_defaults.py) and a banner says so. - Structure checks:
docker_build_arg,docker_env_vars,docker_mounts,env_vars,k8s,slurm,kubernetes,llm_d,distributed,vllm,deployment_config,deploymust be objects;toolsandpre_scriptsarrays;docker_gpusandMAD_CONTAINER_IMAGEstrings;timeouta number;debuga boolean;log_error_patternsa non-empty string array. Build-arg values are coerced to strings. gpu_vendormust beAMDorNVIDIA,guest_osmust beUBUNTUorCENTOS(case-insensitive, normalized to upper case).- Launcher: every
distributed.launcher/launcher.typeis 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-levellauncheris rejected. The per-target rule (slurm_multiis not valid on llm-d) is enforced when the deployment is constructed. See Launchers. - Platform:
platformis a string; omitted means Docker,"bare_metal"selects conda. Unknown values, the retired top-levelbare_metalobject, and acondablock on a Docker run are rejected. See Platforms.
Common option letters
| Short | Long | Notes |
|---|---|---|
-t | --tags | Repeatable; each value may itself be comma-separated (-t a -t b = -t a,b). |
-r | --registry | build, run. |
-c / -f | --additional-context / --additional-context-file | build, 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:
| Condition | build | run |
|---|---|---|
Mutually exclusive flags, bad context, ConfigurationError | 4 | 4 |
BuildError, any failed build or failed registry push (including partial success) | 2 | 2 (BuildError only) |
ExecutionError, any failed model run, GPU-memory gate refusal | n/a | 3 |
DiscoveryError (no models matched), missing file, permission error, Ctrl-C, other exceptions | 1 | 1 |
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/.
| Option | Short | Default | Description |
|---|---|---|---|
--tags TEXT | -t | none | Tags / model names; repeatable and comma-separable. Omitted: print the list of all model names. |
--full | -f | off | With 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 | -j | off | Plain 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 | -v | off | Debug logging; Rich traceback on failure. |
Tag syntax
| Form | Example | Selects |
|---|---|---|
| Tag, model name, or last path segment of a name | --tags llama3 | Any card whose name, final /-separated name segment, or tags entry equals the value. |
| Several | --tags a,b / -t a -t b | Union. A tag that matches nothing aborts with No models found corresponding to the given tag (exit 1). |
| Everything | --tags all | Every discovered model. |
| Scoped | --tags MAD-private/inference | Exactly 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:fp16 | Text after the first : is split on :; each = becomes a space and the pieces are appended to the card's args as --batch 512 --fp16. |
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
- Root
./models.json— required. scripts/**/models.json— found by a recursive walk, so nested submodule layouts (scripts/MAD/…) work.scripts/**/get_models_json.py— dynamic; must definelist_models()returningCustomModelobjects (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.
| Option | Short | Default | Description |
|---|---|---|---|
--tags | -t | none | Model 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 FILE | none | JSON 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 | -a | none | GPU architectures to build for (gfx90a,gfx942); repeatable / comma-separated. Omitted: one image using MAD_SYSTEM_GPU_ARCHITECTURE from context or the detected GPU. |
--registry | -r | none | Push built images here. A failed push is a build failure (exit 2). |
--use-image [NAME|auto] | none | Skip 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-compute | off | Build 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 | -f | none | JSON file; -c is deep-merged over it. |
--config | none | YAML / 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-cache | off | Rebuild without the Docker layer cache. | |
--manifest-output | -m | build_manifest.json | Manifest path. |
--summary-output | -s | none | Write the build summary JSON here. |
--live-output | -l | off | Stream build output. |
--verbose | -v | off | Debug logging. |
4):
--batch-manifestwith--tags--use-imagewith--registry--use-imagewith--build-on-compute--build-on-computewithout--registry--configwith--additional-contextor--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-filenames an existing file → execution only (--tagsis 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 noMAD_CONTAINER_IMAGE) the orchestrator raisesConfigurationError(exit4). MAD_CONTAINER_IMAGEin context skips the build and synthesizes a manifest for the given image;--tagsis then required.
| Option | Short | Default | Description |
|---|---|---|---|
--tags | -t | none | Models 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 | -r | none | Registry URL (push in the build phase; image resolution at run). |
--timeout INT | -1 | Seconds per model. See timeout resolution. | |
--additional-context / -c, --additional-context-file / -f, --config | see overview | Same 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-alive | off | Leave the container running after the model finishes (also opts the container out of the heartbeat dead-man's switch). | |
--keep-model-dir | off | Keep the model directory copy. Docker-only; ignored for bare metal. | |
--clean-docker-cache | off | Rebuild without cache (full-workflow build phase only). | |
--skip-model-run | off | Start 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-image | off | Pull 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-output | build_manifest.json | Manifest path written by the build phase of a full workflow. | |
--summary-output | -s | none | Write the run (or combined workflow) summary JSON. |
--live-output | -l | off | Stream container output. |
--output | -o | perf.csv | Performance CSV path. |
--ignore-deprecated | off | Accepted and stored on the args object, but nothing in src/madengine reads it; it currently has no effect. | |
--data-config | data.json | Data-provider config. Only loaded if the file exists. | |
--tools-config | ./scripts/common/tools.json | Profiling-tools definition file. | |
--sys-env-details | on | Collect 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 PATH | none | Takes 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-arch | off | Ignore each card's skip_gpu_arch. | |
--cleanup-perf | off | Intended to delete intermediate perf_entry* files after the run. See the warning below. | |
--wait-for-gpu-memory [SECONDS] | unset | Local 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 | -v | off | Debug logging. |
--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.
--timeout | Effect |
|---|---|
-1 (default) | "Not passed": falls through to the card's timeout, else 7200 s. |
0 | No timeout, overriding the card. |
| Positive integer | That many seconds, overriding the card (even 7200, which is distinguishable from omitting the flag). |
Below -1 | Rejected, 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-command | Options | Behavior |
|---|---|---|
to-html | --csv-file-path FILE required, -v | Converts 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), -v | Consolidates 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, -v | Finds 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-compare | REPORTS... (two or more .xlsx files or per-sheet CSV directories), --output/-o (tracelens_comparison.xlsx), --names (repeat per report), --python, -v | Diff 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.
| Option | Short | Default | Description |
|---|---|---|---|
--file | -f | required | CSV or JSON file. |
--database / --db | required | Database name. | |
--collection | -c | required | Collection name. |
--unique-key | -k | auto-detected | Comma-separated deduplication fields. |
--batch-size | 1000 | Bulk operation size. | |
--no-upsert | off | Insert only; do not update existing documents. | |
--no-index | off | Skip automatic index creation. | |
--dry-run | off | Validate without uploading. | |
--verbose | -v | off | Also prints up to 10 upload errors. |
Connection comes from the environment (MongoDBConfig.from_env()):
| Variable | Default |
|---|---|
MONGO_HOST | localhost |
MONGO_PORT | 27017 |
MONGO_USER, MONGO_PASSWORD | empty (no auth unless both set) |
MONGO_AUTH_SOURCE | admin |
MONGO_TIMEOUT_MS | 5000 |
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.
| Option | Default | Description |
|---|---|---|
--all | off | Also remove containers whose owning madengine process is still running. |
--dry-run | off | Print what would be removed; remove nothing (exit 0). |
| Exit | Condition |
|---|---|
0 | Nothing found, nothing stale, dry run, or every stale container removed. |
1 | docker ps timed out or failed (Docker unreachable), or a container could not be removed and was not wedged. |
5 GPU_WEDGED | A 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.
| Option | Short | Default | Description |
|---|---|---|---|
--output | -o | index.html | Output path (parent directories are created). |
--title | madengine Setup Picker | Page heading (HTML-escaped). | |
--repo-url | none | Model repository URL, used for the clone instructions (.git suffix and trailing slash are stripped). | |
--install-cmd | pip install git+https://github.com/ROCm/madengine.git | Install command shown in step 1. | |
--verbose | -v | off | Debug 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:
| Form | Example | Meaning |
|---|---|---|
| YAML file | --config my_job.yaml | A .yaml/.yml argument without = and not starting with +. At most one file ("Only one YAML config file allowed"). |
| Group selection | --config scheduler=slurm | Swap a default group. Only platform, scheduler, hardware, launcher are default groups. |
| Append group | --config +env=nccl_debug | Add a preset from an optional group: +profile, +env, +tools, +data, +build. One preset file per group per invocation. |
| Value override | --config distributed.nnodes=4 | Dotted key=value; also +env_vars.KEY=val to add a new key. |
Precedence (later wins)
- Package defaults: configs/config.yaml with default groups
platform=docker,scheduler=local,hardware=amd,launcher=none. - The user YAML file, merged over the defaults.
- Group selections and
+groupappends. 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. key=valueoverrides.
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/
| Group | Kind | Files |
|---|---|---|
platform | default | docker, bare_metal, podman, singularity. Each writes platform: {type: …}; only docker passes validation (see below). |
scheduler | default | local (empty), slurm, k8s |
hardware | default | amd, nvidia, cpu |
launcher | default | none (distributed.enabled: false), torchrun, deepspeed, megatron-lm, megatron (alias of megatron-lm), torchtitan, primus, vllm, sglang, sglang_disagg, slurm_multi, native |
profile | append | mi300x_single, mi300x_8gpu, mi250x_4gpu, h100_8gpu, a100_8gpu (GPU type/memory, per-node GPU count, nproc_per_node, AMD HSA/queue env vars) |
env | append | infiniband, miopen_defaults, nccl_debug, nccl_tuned |
tools | append | rocprofv3_lightweight, rocprofv3_comprehensive, rocm_trace_lite, power_profiler, vram_profiler |
data | append | local, nas, s3, minio |
build | append | default, 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:
| Key | Default | Used for |
|---|---|---|
model.tags / model.timeout / model.manifest_file / model.skip_run / model.container_image | [] / null / null / false / null | CLI metadata. container_image becomes context MAD_CONTAINER_IMAGE. |
build.registry / build.target_archs / build.manifest_output | null / [] / build_manifest.json | CLI metadata for build. |
docker.build_args, env_vars, mounts, gpus, cpus, additional_run_options | empty / null | Translated 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 / patterns | true / [] / [] | log_error_pattern_scan, log_error_benign_patterns, log_error_patterns (empty lists omitted). |
output, summary_output, data_config, live_output | perf.csv, null, data.json, false | CLI metadata. |
env_vars, tools, pre_scripts, post_scripts, encapsulate_script, debug | empty / false | Passed 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. slurmtogether withk8s/kubernetesis rejected.distributed.enabled: truerequiresdistributed.launcher;distributed.nnodesmust be a positive integer.- Platform: the object form
platform: {type: …}accepts onlydocker;bare_metal,podmanandsingularityare rejected as "not yet supported", so theplatform=bare_metalgroup fails validation. A top-levelplatformvalue is CLI metadata and is not forwarded intoadditional_context, andcondais not an accepted key, so bare metal cannot be selected through--config: pass"platform": "bare_metal"(and anycondablock) with--additional-context. - Any validation problem is raised as
ConfigurationErrorlisting every error (exit4).
Launcher handling
- The
launchertop-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 aslauncherfor Kubernetes. distributed.launcheris canonicalized by the same function the engine uses elsewhere (sglang_disagg→sglang-disagg,slurm-multi→slurm_multi). The grouplauncher=megatronyieldsmegatron-lm; the bare stringmegatronis not a valid launcher name.distributed.enabledis passed through as written; the translator never sets it. Every launcher group writesenabled: true, and the defaultlauncher=nonewritesenabled: false. Validation requires a launcher when it istrue. Kubernetes profile selection treats eitherenabled: trueor a launcher name as distributed.launcher=native(anddocker) is aperf.csvreporting value, not a launcher. The translator forcesdistributed.enabled: falseandlauncher: nullso a Kubernetes profile cannot injecttorchrun. Do not copy that group into JSON.
--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.
| Code | Name | Meaning / where raised |
|---|---|---|
0 | SUCCESS | Operation succeeded (for run: every model passed; for cleanup: nothing left behind). |
1 | FAILURE | General failure: no model matched a tag, missing file, permission error, Docker unreachable (cleanup), any report/database/setup-page error, Ctrl-C, unexpected exception. |
2 | BUILD_FAILURE | BuildError; one or more images failed to build or push (partial success included). |
3 | RUN_FAILURE | One or more model runs failed, an ExecutionError occurred, or the --wait-for-gpu-memory gate refused to start. |
4 | INVALID_ARGS | Argument 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. |
5 | GPU_WEDGED | Only 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. |
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
| Input | Parsing |
|---|---|
--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 FILE | Strict JSON object (json.load). |
| Both given | File 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. |
--config | Hydra frontend builds the same dict; mutually exclusive with the two flags above. See config frontend. |
| Inside the orchestrators | The 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). |
'{"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_testandhost_oswhen absent, best effort. gpu_vendor/guest_osdefault toAMD/UBUNTU(core/additional_context_defaults.py) so Dockerfile context filtering has values.- Reads every
MAD_SECRETS*environment variable intodocker_build_arganddocker_env_vars.
Runtime mode (madengine run)
- Detects
ctx_test,host_os,numa_balancing,gpu_vendor(NVIDIA vianvidia-smi, AMD viaamd-smithenrocm-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 intodocker_build_arg. - Defaults
docker_gpusto"0-<N-1>"and computesgpu_renderDs(GPU index →/dev/dri/renderD*). - Detection failure raises
RuntimeErroron the runtime node.
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.
| Key | Type | Subsystem | Description & example |
|---|---|---|---|
gpu_vendor | string | Core | "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_os | string | Core | Container 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_balancing | string | Core | Normally 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_PATH | string | Util | Top-level override of the host ROCm root (priority 1). See ROCm path. |
platform | string | Orch | "docker" (default when omitted) or "bare_metal". Any other string, or a non-string, is rejected. See Execution platforms. |
conda | dict | Exec | Bare-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_vars | dict | Exec | Variables 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_arg | dict | Exec | Extra --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_mounts | dict | Exec | Extra 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_gpus | string | Exec | GPU 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_cpus | string | Exec | Passed as --cpuset-cpus (spaces removed), e.g. "0-15". |
MAD_CONTAINER_IMAGE | string | Orch | Skip 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_image | bool | Orch | Same 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. |
tools | list | Exec | Profiling/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_scripts | list | Exec | Entries {"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_script | string | Exec | Command prefix prepended to the model invocation (e.g. a profiler wrapper). Tool cmd values are prepended to it. |
rocenv_mode | string | Exec | "lite" (default) or "full"; unknown values fall back to lite with a warning. |
gen_sys_env_details | bool | Exec | false disables the rocEnvTool pre-script. Otherwise follows --sys-env-details (default on). |
log_error_pattern_scan | bool | Exec | Default true. false skips post-run log substring scanning. Accepts 0/1, "true"/"false", "yes"/"no", "on"/"off". Context overrides the card. |
log_error_patterns | list[str] | Exec | A non-empty list replaces the built-in pattern list (see Result status). |
log_error_benign_patterns | list[str] | Exec | Literal substrings; a log line containing one is never counted as an error. Card and context lists are concatenated. |
model_args | string | Exec | Replaces the card's args string for the run. |
skip_perf_collection | bool | Exec | Run succeeds without a local performance line; collection is deferred to an aggregator (multi-node SLURM). Normally set by the deployment layer. |
data | dict | Core | Data-provider definitions merged over data.json (--data-config), keyed by data name then provider type (custom, local, minio, nas, aws). |
env_vars | dict | Deploy | Environment 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. |
distributed | dict | Deploy | launcher, nnodes, nproc_per_node, backend, port/master_port, disaggregated blocks. See Per-launcher configuration. |
launcher | dict | Deploy | Older shape {"type": "..."}; still read for the launcher recorded on result rows. Prefer distributed.launcher. |
slurm | dict | Deploy | Selects SLURM (scheduler: "spur" selects Spur). See SLURM. |
k8s / kubernetes | dict | Deploy | Selects Kubernetes. See Kubernetes. |
llm_d | dict | Deploy | Selects llm-d; checked before k8s. See llm-d. |
deploy | string | Deploy | Optional explicit target. Only "spur" changes behavior beyond inference; ConfigLoader still validates it against the blocks present. |
monitor, cleanup_on_failure | bool | Deploy | Default true each: poll the job until it ends; cancel/clean up when it failed. |
debug | bool | Deploy | Read by the Kubernetes deployment for extra diagnostics; also stored in deployment_config. |
docker_keep_alive, docker_clean_cache | bool | CLI | Only 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, vllm | internal | Orch | Set 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
| Field | Default (CustomModel) | Behavior |
|---|---|---|
name | required | Unique 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). |
timeout | 7200 | Seconds. 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. |
# 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
| Key | Written by | Notes |
|---|---|---|
built_images, built_models | build | Joined 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_digest | build (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_architecture | build | Used by the run-time image filter. Vendor is inferred from .amd./.nvidia. in the file name, else from the FROM content. |
built_models[*] | build | The 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. |
context | build | docker_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_config | build, then run merge | target (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. |
summary | build | successful_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_required | build | Distinct cred keys of built cards. |
docker_run_cmd | local run | The docker run line of the first container started. Best effort; never fails the run. |
local_image_mode, local_image_name | MAD_CONTAINER_IMAGE run | Synthetic manifest; entries are keyed local-<model> with build_status: SKIPPED. |
bare_metal: true | bare-metal build | Entries have docker_image: "", conda_env, and the card's dependency fields; context is the full runtime context. |
--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.
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
- Resolve the image:
local_imageentries (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_imageentries 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 unlessrequire_pinned_imageis set. - Build
docker runoptions (below), mount the working directory at/myworkspace, start the container detached under the heartbeat watchdog (Container lifecycle). - Inside: clone
urlif present, copy the script directory into the model directory, run pre-scripts,[encapsulate] bash run.sh <args>(timeout from the resolved value), post-scripts. - Parse results, write
perf.csv,perf_super.csv/json, stop and remove the container (unless--keep-alive).
Default run options
| GPU vendor | Options |
|---|---|
| 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
| Condition | Added |
|---|---|
| Ordinary run | No 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 reason | Put --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
| Key | Default | Meaning |
|---|---|---|
conda_bin | first of micromamba, mamba, conda on PATH | Executable. madengine never downloads a conda implementation. |
conda_env | card conda_env, else mad_<name> (/ → _) | Prefix name; flattened to one path segment. |
env_root | ~/.cache/madengine/envs | Parent of prefixes. A same-named env in the user's own conda root is never reused. |
reuse_env | true | Keep 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_env | false | Remove the prefix after the run. |
environment_file, requirements_file, python_version, setup_script, rocm | card values | Per-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: ""andbare_metal: true. A run without a manifest builds first. - Run wraps the script in
conda run -p <prefix>(plus--no-capture-outputfor conda/mamba, not micromamba) and does not wrap it intorchrun/srun; the script is its own launcher. - GPUs: no
--device;HIP_VISIBLE_DEVICES/ROCR_VISIBLE_DEVICES(AMD) orCUDA_VISIBLE_DEVICES(NVIDIA) are exported.n_gpus: "-1"keeps an existing mask and becomes a positiveMAD_RUNTIME_NGPUS, becausetorchrun --nproc_per_noderejects-1. --keep-aliveand--keep-model-dirare ignored.- Result rows record
platform=bare_metal,deployment_type=local, emptydocker_image, and the workload launcher (launcher.type>distributed.launcher> card >MAD_LAUNCHER_TYPE/MAD_LAUNCHER;nativewhen none or not a workload launcher).
Rejected combinations
- Top-level
bare_metalkey (retired). condablock whileplatformisdocker/omitted.platform: bare_metalwithslurm,k8s,kubernetesorllm_d.- Any other
platformstring (podman,singularityare 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)
| Launcher | Value exported to the container |
|---|---|
torchrun, megatron-lm, torchtitan | torchrun --standalone --nproc_per_node=<MAD_RUNTIME_NGPUS> |
deepspeed | deepspeed --num_gpus=<N> |
vllm, sglang, sglang-disagg, primus | empty 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 / unrecognised | torchrun --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
- Performance is extracted from the log with the pattern
performance: <number> <metric>(fallback: HuggingFacetrain_samples_per_second), or from themultiple_resultsCSV. - Unless
log_error_pattern_scanis 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 vialog_error_patterns). Known-benign lines (metrics-agent noise,FutureWarning, rocprof/rpd tracer lines, pluslog_error_benign_patterns) are ignored. - Status: valid performance →
SUCCESSeven if an error pattern matched (model output can legitimately containValueError:); error pattern and no performance →FAILURE; worker nodes (MAD_COLLECT_METRICS=false) andskip_perf_collectionruns are not failed for lacking local performance; otherwise no performance →FAILURE. A timeout, a setup exception or a non-zero script exit records aFAILURErow 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:
| Layer | Mechanism | Survives SIGKILL of madengine? |
|---|---|---|
| 1. Dead man's switch | Container 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 handlers | Installed 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 cleanup | Bounded escalation shared by the handlers, normal teardown and the cleanup command. | Manual / cron |
Teardown escalation
- An already-exited container just gets
docker rm -f. A live one getsdocker 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. - If the daemon cannot remove it: read the container's host PIDs from the cgroup (works with a hung dockerd),
SIGKILLthem, retrydocker rm -f. - 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.
docker inspectfailure 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 cleanupexits5(ExitCode.GPU_WEDGED) so CI can pull the runner out of rotation; the host needs a reboot.
Labels, registry and tuning
| Item | Details |
|---|---|
| Container labels | madengine.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 hooks | register_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_INTERVAL | Default 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_AFTER | Default 120 s (same fallback rule). |
MADENGINE_STATE_DIR | Default ~/.madengine; holds the registry and gpu_wedged.json. An unwritable directory never fails a benchmark. |
MADENGINE_SESSION_ID | Default: random 12-hex id per process. Set to group containers from cooperating processes. |
--keep-alive | Opts 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. |
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-cardnoctx/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-archsa_<arch>suffix is appended. - Log file:
<name>_<dockerfile>.build.live.login the cwd.
Registry names
--registry docker.io|dockerhub:<repository>:<local tag>, repository fromcredential.json/MAD_DOCKERHUB_REPO(placeholder repositories are ignored).- Any other registry:
<registry>/<repository>:<tag>whencredential.jsonhas arepositoryfor it, else<registry>/<local tag>. - A batch-build entry's
registry_image/registryoverrides 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/commonis emitted only whendocker/common/exists relative to the cwd, so Dockerfiles canCOPY --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-archsarch andcredargs, then contextdocker_build_arg(which also carriesMAD_SECRETS*from the environment and the detectedMAD_SYSTEM_GPU_ARCHITECTURE). All values are shell-quoted. - Context:
dockercontext, else./docker;.for Dockerfiles withprimusin the name. build_commandin the manifest hasMAD_SECRETS*and token-shaped values masked.- If the base image's registry is not authenticated, a login is attempted first; an existing ambient
docker loginis left alone. A registry-denial in the log produces an actionable hint.
GPU architecture handling
| Situation | Result |
|---|---|
docker_build_arg.MAD_SYSTEM_GPU_ARCHITECTURE set | Single-architecture flow for every card; --target-archs is ignored (with an info message). |
run --tags on a local GPU node, nothing set | Architecture 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 supplied | Warning 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 entrypush_failedand does not fail the build; it is listed inpush_failuresandsummary.failed_pushes.- The pushed digest is parsed from
docker pushoutput (fallbackdocker image inspectRepoDigests) and stored asimage_digest(core/image_digest.py). It is only used with--require-pinned-image: the run pullsrepo@sha256:...and refuses a manifest without a digest rather than falling back to the tag. - After a successful push
env_vars.DOCKER_IMAGE_NAMEon the built model is set to the registry image soslurm_multinodes can pull in parallel. image_size_bytesis 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_BUILDSmay point at a shared directory fordocker savetars that other SLURM nodesdocker load.
Alternative build modes
| Mode | Behavior |
|---|---|
--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 auto | Takes 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 R | Build 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_metal | No 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 condition | Target | Executor |
|---|---|---|---|
| 1 | "platform": "bare_metal" | bare_metal | execution/bare_metal_runner.py via RunOrchestrator._execute_bare_metal() (see Platforms) |
| 2 | llm_d key present | llm-d | LlmdDeployment (see llm-d) |
| 3 | k8s or kubernetes key present | k8s | KubernetesDeployment (see Kubernetes) |
| 4 | slurm key present and slurm.scheduler (or top-level deploy) is "spur" | spur | SpurDeployment (see SLURM) |
| 5 | slurm key present | slurm | SlurmDeployment |
| 6 | No scheduler key, but the effective launcher is slurm_multi (distributed.launcher, else the model card's distributed.launcher) | slurm | SlurmDeployment — a self-managed card is a SLURM job by construction |
| 7 | None of the above | local | ContainerRunner (local Docker) |
Why the order matters
- llm-d before k8s: an llm-d config also carries a
k8sblock, which sizes the benchmark-client Job. - k8s before slurm_multi promotion: a
slurm_multicard with ak8sblock runs on Kubernetes (see slurm_multi on Kubernetes); with neither block it is promoted to SLURM. - Manifest fallback: if inference yields
local,deployment_config.targetfrom 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)
platformis a string:"docker"(default when omitted) or"bare_metal". Anything else raisesConfigurationError.- The retired top-level
bare_metalkey is rejected. - A
condablock requiresplatform: "bare_metal". bare_metalcannot be combined withslurm,k8s,kubernetesorllm_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/kubernetesandslurmare both present;llm_dandslurmare both present;- an explicit
deployvalue contradicts the blocks:k8s/kuberneteswithout ak8sblock,slurm/spurwithout aslurmblock,spurwith aslurm.schedulerother thanspur, orlocalwith a scheduler block; slurm.scheduleris anything other thanslurmorspur.
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 string | Class | Notes |
|---|---|---|
slurm | SlurmDeployment | Always registered. |
spur | SpurDeployment | Always registered. |
k8s, kubernetes | KubernetesDeployment | Registered only if the kubernetes package imports; otherwise a UserWarning is issued and the target is unavailable. |
llm-d, llm_d | LlmdDeployment | Registered 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:
| Launcher | Allowed on | Rejected on |
|---|---|---|
slurm_multi | slurm, 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.
--force-mirror-localis 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
kubernetesspelling selects the K8s target, but its sub-keys are not read: presets always create ak8sblock, which wins. Put sub-keys underk8s. --keep-alive,--keep-model-dir,--skip-model-runand--wait-for-gpu-memoryhave no effect on distributed targets; a warning lists the ignored flags.- For SLURM,
skip_gpu_archis applied before submission, for every launcher includingslurm_multi; skipped cards appear asSKIPPEDrows.
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.
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
| Config | Result |
|---|---|
"slurm": {...} | SlurmDeployment (registered as slurm) |
"slurm": {"scheduler": "spur"} or "deploy": "spur" (with a slurm block) | SpurDeployment (registered as spur) |
slurm.scheduler other than "", slurm, spur | ValueError: unknown scheduler |
slurm together with k8s/kubernetes | ValueError: conflicting deployment configuration |
slurm together with llm_d | ValueError: 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_multi | Promoted 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.
- presets/slurm/defaults.json
- One profile, chosen by the merged
slurm.nodes: profiles/multi-node.json whennodes > 1, otherwise profiles/single-node.json - Your
--additional-context/--additional-context-file/--configresult
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
| Preset | Keys |
|---|---|
defaults.json | gpu_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.json | slurm.nodes: 1, gpus_per_node: 8, time: 12:00:00; env_vars.NCCL_DEBUG=WARN |
profiles/multi-node.json | slurm.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 |
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.
| Key | Default | Effect |
|---|---|---|
partition | amd-rccl | #SBATCH --partition; also used by the preflight probe and the --build-on-compute build job (required there). |
nodes | 1 | #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_node | 8 | #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). |
time | 24:00:00 (single-node profile: 12:00:00) | #SBATCH --time. |
exclusive | true | Adds #SBATCH --exclusive. |
account, qos | unset | #SBATCH --account / --qos. Also applied to the --build-on-compute build job and honoured by the slurm_multi wrapper. |
reservation | unset | #SBATCH --reservation; forwarded to the health-check and cleanup srun calls. |
nodelist | unset | #SBATCH --nodelist (comma list, whitespace stripped). Setting it skips the node health preflight. A bracketed list is expanded before the GPU-architecture probe. |
exclude, constraint | unset | #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_results | Where 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_interface | unset | Exports 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_workspace | unset | Explicit 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_dir | unset | slurm_multi only: directory searched for perf*.csv when the card declares no multiple_results. |
skip_gpus_directive | false | Omit --gpus-per-node for clusters without GPU GRES (sbatch would reject it). |
gpu_arch | unset | Target 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_check | true | Run the node health preflight (see below). Ignored on Spur. |
node_check_timeout | 120 | Seconds each probe srun may wait. This is a queue wait, so raise it on busy clusters. |
verbose_node_check | false | Per-node progress output. |
auto_cleanup_nodes | false | Clean dirty nodes in place instead of only excluding them. |
allow_submit_without_clean_nodes | false | Multi-node: submit even when fewer clean nodes than nodes were found. |
build_gpus_per_node | unset | Build 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). |
scheduler | slurm | slurm or spur. |
rendezvous_timeout | 900 | Spur only: seconds a non-zero rank waits for rank 0 (and for peer IP files). |
Related top-level keys read by the SLURM path
| Key | Effect |
|---|---|
distributed.launcher, nnodes, nproc_per_node, backend, port / master_port | See 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_vars | Exported in the job script (values double-quoted). MIOPEN_USER_DB_PATH is not exported globally; it is set per node and rank. |
tools | With 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_data | Forwarded to the in-job madengine run as --force-mirror-local <value>. |
live_output | Streams node output while the job runs; also adds --live-output to the in-job madengine run. |
require_pinned_image | slurm_multi: pins DOCKER_IMAGE_NAME to the digest recorded at build time. |
What the job script does (templated launchers)
- 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. - Distributed environment.
MASTER_ADDRis the first host ofSLURM_NODELIST;MASTER_PORT,WORLD_SIZE(=SLURM_NTASKS),LOCAL_RANK,NNODES,GPUS_PER_NODE. GPU visibility:HIP_VISIBLE_DEVICES,ROCR_VISIBLE_DEVICESandCUDA_VISIBLE_DEVICESfor ordinary launchers; for the Ray-basedvllm,sglangandsglang-disaggonly the one matching the detected vendor is set, andRAY_EXPERIMENTAL_NOSET_HIP_VISIBLE_DEVICESis unset. - Single node. Uses the submission directory when it is on
nfs*,lustre,gpfs,ceph,beegfsorpanfs(judged from thedffilesystem-type column only); otherwise a job-scoped local workspace under$SLURM_TMPDIRor/tmp. It writesbuild_manifest_local.jsonwithdeployment_config.target="docker"and theslurm/k8sblocks removed, exportsMAD_MULTI_NODE_RUNNER="torchrun --standalone --nproc_per_node=<gpus>", and runsmadengine run --manifest-file build_manifest_local.json --timeout <cli value>locally on the node. - Multi node. Writes
<output_dir>/madengine_task_<job tag>.shand runssrun bash <task script>. Each task setsNODE_RANK=$SLURM_PROCID, emits the launcher's environment (MAD_MULTI_NODE_RUNNERetc.),rsyncs the project (excluding.git, caches, virtualenvs, old logs) into a node-local workspacemadengine_job_<tag>_node_<rank>, builds the same local manifest, setsMIOPEN_USER_DB_PATH=/tmp/.miopen/node_<rank>_rank_<local>andTORCH_ELASTIC_RDZV_TIMEOUT=3600, and callsmadengine runwith output redirected tomadengine-<model>_<tag>_node_<rank>.{out,err}. Only rank 0 collects performance (MAD_COLLECT_METRICS=true); the others setMAD_SKIP_PERF_COLLECTION=true. - Artifact collection. On success each node copies its logs,
*_env.csv,*.log,multi_results*.csv,results*,gpu_info_*.csv,library_trace.csv, the card'smultiple_resultsfile, and the profiler directoriesrocprof_output,rpd_output,rocm_trace_lite_output,torch_profiler_output,tracelens_outputto<output_dir>/<model>/<job tag>/node_<rank>/. On failure only the two logs are copied. - 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
squeueare reclaimed by later jobs.
Environment set by the job script
| Variable | Value |
|---|---|
MAD_DEPLOYMENT_TYPE | slurm (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_NODES | Node rank ($SLURM_NODEID) and node count |
MAD_IN_SLURM_JOB | 1; makes the in-job ContainerRunner remove the cached registry image before pulling (fresh-pull policy) |
MAD_LAUNCHER_TYPE | Resolved launcher name; read back by the in-job ContainerRunner |
MASTER_ADDR, MASTER_PORT, WORLD_SIZE, NNODES, GPUS_PER_NODE, LOCAL_RANK | Distributed rendezvous (see above) |
NODE_RANK, RANK | Per-task, multi-node only ($SLURM_PROCID); single node uses 0 |
DISTRIBUTED_BACKEND | distributed.backend (default nccl) |
NCCL_SOCKET_IFNAME, GLOO_SOCKET_IFNAME | slurm.network_interface or auto-detected |
TORCH_ELASTIC_RDZV_TIMEOUT | 3600 (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_DEVICES | 0..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.
| Health | Meaning |
|---|---|
CLEAN | Usable. |
DIRTY | A 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 / UNKNOWN | The 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
nodesfails the run ("Not enough clean nodes") unlessallow_submit_without_clean_nodesis 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_nodesstops only containers labelledmadengine.sessionand 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:
BaseDeploymentpolls every 30 s.squeue -jstatesRUNNING,PENDING,CONFIGURING,COMPLETINGmean keep waiting;COMPLETEDis success; any other state (FAILED,CANCELLED,TIMEOUT,NODE_FAIL, ...) is failure. When the job has left the queue the final state comes fromsacct -j <id> -n -X -o State, retried 3 times 5 s apart;-Xis dropped if the scheduler rejects it. No row at all gives statusUNKNOWN, 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 andmultiple_resultsCSVs 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 issuesscancel <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 whoseskip_gpu_archnames the nodes' architecture before submission (a card on the wrong architecture is reportedSKIPPED, not submitted). The filtered manifest is a unique per-run copy<manifest stem>.skip_gpu_arch.<pid>.<uuid>.jsonbeside 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-archturns 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.
| Case | Behaviour |
|---|---|
Templated launcher, nodes > 1 | Runs 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 node | Still submits its own sbatch: the single-node branch calls madengine run inline, which in a shell would run on the login node. |
slurm_multi | Always 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_TMPDIRor/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
madenginewould (--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:
| Aspect | Stock SLURM | Spur |
|---|---|---|
| Multi-node fan-out | One job, srun bash task_script | Job 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_ADDR | First host of the node list | Shared-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 list | Every rank publishes ip_rank<N>; the list is built in rank order, waiting up to the rendezvous timeout |
SLURM_JOB_ID | Job id | Pinned to SLURM_ARRAY_JOB_ID so ports and shared directories match across tasks |
| Completion detection | squeue then sacct | Per-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 preflight | Yes | Skipped (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 nodes | Local docker pull in each array task |
| Required CLIs | sbatch squeue scontrol | sbatch squeue sacct |
| Submission retries | Transient Raft-leader errors retried (both) | |
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.
| Item | Behaviour |
|---|---|
| File location | env_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. |
| Schema | YAML 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 selection | BENCHMARK_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. |
| Warning | A 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())
- 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). - prepare — renders ConfigMap, Job and (multi-node) headless Service.
- deploy — deletes this job's leftovers, creates the results PVC, Secrets, ConfigMap, Service, then the Job.
- monitor — polls the Job; with
--live-outputit streams pod logs. A Job is finished only on theCompletecondition (orsucceeded >= completions);Failedcondition means failure. A non-zerosucceededcount alone is not completion. - collect results — always runs, even after failure, so failed runs are recorded.
- 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_finishedis set; the next run of the same model replaces them.
Source files
| File | Concern |
|---|---|
| kubernetes.py | KubernetesDeployment: kube target resolution, validate/prepare/deploy/monitor/cleanup, leftover deletion. |
| k8s_template_context.py | Builds the Jinja2 context: resources, launcher command, env, tools, data provider, ConfigMap size preflight (about 1 MiB cap). |
| kubernetes_launcher_mixin.py | Generates 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.py | Loads common scripts, tool wrappers and Primus overlays for the ConfigMap; rocEnvTool wiring (rocenv_mode, guest_os). |
| k8s_pvc.py | Results/data PVCs, storage-class and ReadWriteMany detection, layout selection. |
| k8s_secrets.py | Registry-pull and runtime Secrets from credential.json; k8s.secrets strategies. |
| k8s_names.py | Name sanitizers and the 63-character Job-name rule. |
| k8s_results.py | Log/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):
- presets/k8s/defaults.json
- presets/k8s/gpu-vendors/amd.json or
nvidia.json, chosen bygpu_vendor(defaultAMD); setsk8s.gpu_resource_nametoamd.com/gpuornvidia.com/gpu. The NVIDIA preset also sets NCCL env vars. - 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, …). - One profile, chosen from the merged defaults and user config:
Profile Selected when Sets multi-node.jsondistributed.nnodes > 1memory 64Gi/128Gi, cpu 16/32, distributedtorchrun defaults,NCCL_*envmulti-gpu.jsongpu_count > 1, or adistributedlauncher/enabled, and single nodememory 64Gi/128Gi, cpu 16/32, distributed.launcher=torchrununless you set onesingle-gpu.jsongpu_countis 1 (the default when unset) and no distributed blockgpu_count=1, memory 16Gi/32Gi, cpu 8/16none e.g. gpu_count: 0without a distributed blockcode fallbacks: memory 128Gi/256Gi, cpu 32/64 - 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.
| Key | Default | Meaning |
|---|---|---|
| Placement and access | ||
namespace | "default" | Must already exist. |
kubeconfig | unset | File 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. |
context | file's current context | Kube context. It must exist in the kubeconfig or the run refuses rather than fall back to another cluster. |
gpu_count | profile-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_name | amd.com/gpu / nvidia.com/gpu by vendor | Extended resource requested and checked in validate(). |
node_selector | {} | Map rendered as nodeSelector. |
tolerations | [] | List of {key, operator, value, effect}. |
output_dir | ./k8s_manifests | Where rendered manifests are written when debug: true (also llm-d values). |
| Pod and Job | ||
memory, memory_limit, cpu, cpu_limit | profile values; fallback 128Gi/256Gi/"32"/"64" | Container requests and limits. |
image_pull_policy | "Always" | |
backoff_limit | 3 | Job backoffLimit (pods use restartPolicy: Never). |
ttl_seconds_after_finished | null | Auto-delete finished Jobs. |
allow_privileged_profiling | null | null 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_class | unset | Override 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_size | 10Gi, 100Gi | |
results_access_mode | unset | Overrides 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_pvc | unset | Name 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_pvc | false | Delete 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_name | null | Existing 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. |
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
| Layout | What each pod gets |
|---|---|
shared | One claim <job>-results (label madengine-pvc), mounted at /results by every pod. ReadWriteOnce for one pod, ReadWriteMany for several (unless results_access_mode). |
per_pod | A 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_credentialsand acredential.jsonin the working directory, deploy creates<job>-runtime(mounted at/workspace/credential.json) and, if it holds usable Docker Hub credentials,<job>-registry-pull. Both carryapp=madengine. Without the file, no Secret is created and an empty{}credential payload goes into the ConfigMap. - There is no top-level
secretscontext key that turns arbitrary values into a Kubernetes Secret. Put credentials incredential.json, or pre-create a Secret and pointk8s.secretsat 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--registryor pass a registry reference with--use-image, or the run fails with aConfigurationErrornaming the push error.require_pinned_imagein the context makes pods pull by digest.
Names and multi-node
- Job name is
madengine-<sanitized model>, capped at 63 characters (Batch Jobs require thejob-namelabel to equalmetadata.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 > 1makes an Indexed Job (completions=parallelism=nnodes) with a headless Service and pod subdomain so<job>-0resolves.sglang-disaggalways 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), thendistributedin the run context, then the manifest'sdeployment_config.distributed, field by field. A runtimedistributedblock wins over the manifest, andn_gpus: -1is 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, apvc/mirror, andlog_csv/files rebuilt fromMADENGINE_PERF_CSV_BEGIN/ENDblocks the pod printed when no volume copy survived. - The card's
timeout(or--timeout) is applied inside the pod withtimeout;0means unbounded. - If the Kubernetes API is unreachable during cleanup, delete by hand:
kubectl delete job,configmap,service -l app=madengine -n <ns>(scope with--contextif 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
prepare()builds a ConfigMap holding a tarball of the card's script directory, anyinclude_script_dirs, and the shim; a headless Service (publishNotReadyAddresses, port 29711); and an Indexed Job.- Every pod unpacks the bundle and runs
mad_k8s_node.sh, which exportsSLURM_JOB_ID,SLURM_JOB_NODELIST,SLURM_NNODES,SLURM_PROCID,SLURM_NODEIDand related variables. - 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. - The shim's
srunsends a task to each named pod's agent and returns the highest exit status.docker runruns the command in that pod: the pod is the container. - Pod 0 publishes
/results/<job>-0/perf.csvandexit_code, then tells the peers to shut down.
Sizing
| What | Resolved 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 pod | a 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 |
| Image | registry_image, docker_image, the card's docker_image, env_vars.DOCKER_IMAGE_NAME; used as given |
| CPU / memory | only if you set k8s.cpu / k8s.memory |
| Deadline | resolved run timeout becomes activeDeadlineSeconds (omitted when unbounded) |
Pod-spec keys (all under k8s)
| Key | Default | Effect |
|---|---|---|
gpu_count | card's slurm.gpus_per_node | GPUs per pod. |
volumes | none | List 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_resources | none | Other device-plugin resources added to requests and limits (for example RDMA devices). |
security_context | none | Container securityContext (for example IPC_LOCK). |
shm_size | 64Gi | Memory-backed /dev/shm size. |
include_script_dirs | sibling common/ directory of the script, if present | Extra directories shipped with the card's, so ${SCRIPT_DIR}/../common resolves. |
host_network | false | Use 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_annotations | none | For 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_mode | as in Kubernetes | Honoured 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.
| Command | Supported | Accepted and ignored | Not 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, --jobid | More than one task per node; any other option. |
docker | run (-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. |
scontrol | show hostnames, show topology | — | Everything else. |
Environment, results and limits
- Environment: the pod gets the card's
env_varsplus the run'senv_vars, minus keys only a preset supplied (for example the AMD multi-GPU preset'sNCCL_IB_DISABLE),DOCKER_IMAGE_NAME, andLOG_PATHdefaulting to/results/logs(shared by all pods). InternalMAD_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 orLOG_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 bystatus, 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>-resultsclaim, so multi-node needs a ReadWriteMany class (or aresults_access_modeoverride);results_layout: per_podis not used. - Image requirements: the card's image needs
bashandpython3. The scripts bundle is capped near 900 KiB of base64 so the ConfigMap stays under 1 MiB; narrowinclude_script_dirsif 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:
| Mode | Invocation | Behaviour |
|---|---|---|
| Explicit image | --use-image registry.io/img:tag | No Docker build; the manifest records that image for every selected card. Mutually exclusive with --registry and --build-on-compute. |
| From the card | --use-image auto | Reads env_vars.DOCKER_IMAGE_NAME from the discovered cards. If cards disagree the first is used with a warning; none found is a ConfigurationError. |
| Implicit | no flags, no --registry | If 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/myorg | Normal 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. |
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
- Header:
--job-name=madengine-<model>,--nodes/--ntasks= resolved node count,--export=ALL,--gpus-per-node(unlessskip_gpus_directive),--time,--exclusive(default), andreservation,nodelist,qos,accountwhen set.slurm.nodesis sized fromdistributed.nnodeswhen not set explicitly; an explicitslurm.nodeswins with a warning; invalidnnodesis rejected. unset SLURM_EXCLUSIVE(otherwise everysruninside re-parses it as a step request and fails with "Invalid --exclusive specification"), thenmodule loadforslurm.modules.- Environment exports, lowest to highest precedence: layered config (
mad-config.yaml), manifestcontext.docker_env_vars, the card'senv_vars, thenadditional_context.env_varsminus keys only a preset supplied (so the multi-node preset'sNCCL_IB_DISABLE=1,NCCL_SOCKET_IFNAME=eth0,HSA_ENABLE_SDMA=0do not override the card's own fabric detection). Also:xP/yDfromdistributed.sglang_disagg(prefill_nodes/decode_nodes) when the script has not set them, andMAD_TOOLS=<names>whentoolsare configured androcprofv3is available (madengine cannot wrap a script it does not control; the script decides). Values are shell-quoted. - Node addressing (stock SLURM):
MAD_NODE_IPS(rank-ordered, loopback addresses rejected),IPADDRS,SGLANG_NODE_IPSandMAD_NODE_RANK, each only if not already set. Spur builds the equivalent through its rendezvous directory. - Image staging for registry images: one
srun --ntasks=$SLURM_NNODES --ntasks-per-node=1step that logs in usingMAD_DOCKERHUB_USER/MAD_DOCKERHUB_PASSWORD(orMAD_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_NAMEcomes from the registry reference recorded by the push, else an already-resolved card value, else the card'sdocker_image. Withrequire_pinned_imageit is pinned to the build digest. - Run:
cdinto the script's directory, delete a stalemultiple_resultsfile (unless it is one of madengine's own cumulative reports), thenbash <script> <args>withset +eso the exit code is always captured. The script name and args are shell-quoted. Amadengine_<model>_<job id>.completemarker (exit_code=,timestamp=) is written toslurm.output_dir, and the wrapper exits with the script's exit code.
Execution paths
| Context | Behaviour |
|---|---|
No SLURM_JOB_ID | sbatch 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:
- 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. perf*.csvinslurm.results_dir./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():
helmonPATH;- every
llm_d.charts.<name>.versionset (shipped asnullon purpose, so benchmark numbers are reproducible); llm_d.model.uri, orhf_repoto derive it;model.sizewhen the URI ishf://;- the Gateway API CRDs (
gateways,httproutes) and aninferencepoolsCRD in either API group; aGatewayClassnamed byllm_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
| Key | Default | Meaning |
|---|---|---|
endpoint_url | null | Set 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_run | false | Managed 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.name | null | Required. Becomes MAD_LLM_D_MODEL. |
model.uri | null | Artifact the servers load: hf://org/repo, pvc://<pvc>/<path> or pvc+hf://<pvc>/<path>. Wins over hf_repo. |
model.hf_repo | null | Shorthand: uri becomes hf://<hf_repo> (re-downloaded on every standup). |
model.cache_pvc, cache_timeout, cache_job_image | null, 7200, python:3.11-slim | With 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_secret | null | Name 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.size | null | Model volume size; required for hf:// URIs because the chart's own default is unusable (set well above the on-disk size). |
prefill / decode | replicas 1, tensor_parallel 1, gpu_count 1 | Role 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.ref | llm-d-infra, inferencepool and llm-d-modelservice OCI refs | OCI reference or a helm repo alias (helm repo add it first). |
charts.*.version | null | Must be pinned in managed mode. |
standup_timeout | 1800 | Seconds for each helm upgrade --install --wait. |
readiness_timeout | 1800 | Seconds 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. |
teardown | true | Managed 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) | false | The 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
- Model preparation: ensure
madengine-shared-dataif the URI names it; run the cache-download Job whencache_pvcis set. - 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). Whenprefill.replicas > 0it 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 inextra_values. - Install three releases in order —
<prefix>-infra(Gateway),<prefix>-gaie(InferencePool and endpoint picker),<prefix>-modelservice(prefill/decode servers) — withhelm upgrade --install, so a rerun converges. Each release is recorded before helm runs so an interrupted install is still torn down. - Readiness (second stage above), then endpoint discovery: the Gateway labelled
app.kubernetes.io/instance=<prefix>-infra(or the only Gateway in the namespace); itsstatus.addressesand an HTTP listener are preferred. Several candidate Gateways, or none with an address, is an error that tells you to setendpoint_url. - Benchmark Job runs with the
MAD_LLM_D_*environment below. - 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 manualhelm uninstallcommand and never masks the benchmark result.teardown: falsekeeps them.
Client environment contract
| Variable | Value |
|---|---|
MAD_LLM_D_ENDPOINT | Attach endpoint_url, or the address resolved from the Gateway. |
MAD_LLM_D_MODEL | llm_d.model.name |
MAD_LLM_D_NAMESPACE | Namespace of the Job. |
MAD_LLM_D_PREFILL_REPLICAS, MAD_LLM_D_DECODE_REPLICAS | Replica counts from the config. |
MAD_LLM_D_TP | decode.tensor_parallel |
MAD_LLM_D_RELEASE_PREFIX | Managed mode only: the helm release stem, for correlating with helm list. |
MAD_OUTPUT_CSV | The card's multiple_results file name, if set. |
HF_TOKEN | From 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 itsargs) fillsllm_d.model.hf_repoandmodel.namewhen unset. A different explicit value is aConfigurationError, so a run cannot serve one model and record another. Cards without--model_reponeed an explicitmodel.name. - Only the first model matched by
--tagsis 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), andserving_mode=aggregated|disaggregated(disaggregatedwhen prefill replicas run).n_gpus,nnodesandgpus_per_nodedescribe the serving pods (read from live pods, falling back to the role config), not the client. TheModelcolumn stays the MAD card name. - When the card sets
multiple_results, that CSV is used in preference toperformance: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)"
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.
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 name | Accepted aliases | Local Docker | Kubernetes | SLURM / Spur | Type | Notes |
|---|---|---|---|---|---|---|
torchrun | - | Yes | Yes | Yes | Train | Default on SLURM when no launcher is configured. |
megatron-lm | - | Yes | Yes | Yes | Train | torchrun plus Megatron parallelism env vars. |
torchtitan | - | Yes | Yes | Yes | Train | torchrun plus TorchTitan parallelism env vars. |
deepspeed | - | Yes | Yes | Yes | Train | Dynamic hostfile from the node list. |
primus | - | Yes | Yes | Yes | Train | Megatron-LM / TorchTitan / MaxText through Primus' run_pretrain.sh; madengine exports PRIMUS_* and no runner. |
vllm | - | Yes | Yes | Yes | Infer | Self-managing; per-replica metrics (each node reports its own). |
sglang | - | Yes | Yes | Yes | Infer | Self-managing; per-replica metrics. |
sglang-disagg | sglang_disagg | Runs (one node, no topology) | Yes | Yes | Infer | Min 2 nodes on SLURM/Spur (co-located or dedicated proxy), 3 on Kubernetes (dedicated proxy). |
slurm_multi | slurm-multi | Self-managed | Indexed Job | Yes | Meta | Runs 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), andslurm_multion llm-d (ensure_launcher_allowed(), whenLlmdDeploymentis constructed). - Checked when the job is generated, before submission:
sglang-disaggnode counts and an explicit prefill/decode split (see the disagg tab under Per-launcher configuration). - Warning only:
slurm.nodesdiffering fromdistributed.nnodes(the explicitslurm.nodeswins), and GPU-count sources that disagree (utils/gpu_config.py). - Not cross-checked: two copies of one setting, such as
launcher.typevsdistributed.launcherorportvsmaster_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).
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.
| Launcher | MAD_MULTI_NODE_RUNNER |
|---|---|
torchrun, megatron-lm, torchtitan | torchrun --standalone --nproc_per_node=N |
deepspeed | deepspeed --num_gpus=N |
vllm, sglang, sglang-disagg, primus | empty 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 fromSLURM_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:
| Variable | 1 node | > 1 node |
|---|---|---|
TENSOR_MODEL_PARALLEL_SIZE | min(nproc_per_node, 8) | nproc_per_node |
PIPELINE_MODEL_PARALLEL_SIZE | 1 | nnodes |
CONTEXT_PARALLEL_SIZE | 1 | 1 |
MAD_MULTI_NODE_RUNNER | same torchrun commands as the torchrun tab | |
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.
| Variable | 1 node | > 1 node |
|---|---|---|
TORCHTITAN_TENSOR_PARALLEL_SIZE | nproc_per_node | nproc_per_node |
TORCHTITAN_PIPELINE_PARALLEL_SIZE | 1 | nnodes |
TORCHTITAN_FSDP_ENABLED | not set | 1 |
TORCHTITAN_CONTEXT_PARALLEL_SIZE | not set | 1 |
{
"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>.txtwith one<host> slots=<N>line per node (hosts expanded scheduler-portably fromSLURM_JOB_NODELIST, orSLURM_STEP_NODELISTin place), and exportsdeepspeed --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.
| Variable | 1 node | > 1 node |
|---|---|---|
VLLM_TENSOR_PARALLEL_SIZE | nproc_per_node | nproc_per_node (per node) |
VLLM_PIPELINE_PARALLEL_SIZE | 1 | 1 |
VLLM_DISTRIBUTED_BACKEND | auto | none |
The metric each node reports is its own replica's, not an aggregate (vllm and sglang are the per-replica launchers).
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.
| Aspect | Behaviour |
|---|---|
| Split config | distributed.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 rank | first set of SLURM_PROCID, NODE_RANK, PMI_RANK, SLURM_NODEID, else 0 (Spur leaves SLURM_PROCID empty) |
| Kubernetes | Pod 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
| Backend | Minimum nodes | Proxy | Valid explicit split |
|---|---|---|---|
| SLURM, Spur | 2 | Co-located on the first prefill node, or a dedicated node | prefill + decode == nnodes (co-located) or == nnodes - 1 (dedicated) |
| Kubernetes | 3 | Dedicated | prefill + 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.
| Key | Meaning |
|---|---|
config_path | Primus 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_extra | Extra CLI arguments, exported as PRIMUS_CLI_EXTRA |
backend | Exported 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
| Aspect | Behavior |
|---|---|
| Per-entry overrides | An 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 effects | Each 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 wrappers | Identical cmd strings are emitted once (the five library tracers share get_library_trace.py), but every tool's env_vars still apply. |
| Where scripts run | Inside 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 model | Post-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 scripts | pre_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 name | Local Docker raises KeyError at run time. Kubernetes silently skips names missing from tools.json. |
slurm_multi | madengine 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 metal | Context 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_ADMIN | A 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. |
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)
| Tool | Collects | Notes |
|---|---|---|
rpd | ROCm 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-trace | Legacy name; use rocprofv3_* on ROCm ≥ 7.0. |
rocprof_hip_only | --hip-trace | |
rocprof_sys | --sys-trace | |
rocprofv3 | rocprofv3 --runtime-trace -- (direct, no wrapper) | Multi-node runs upgrade a plain rocprof entry to this. |
rocprofv3_compute | HIP + kernel trace, compute_bound.txt counters, pftrace | |
rocprofv3_memory | + memory copy/allocation trace, memory_bound.txt counters, pftrace | |
rocprofv3_communication | HIP, HSA, kernel, memcopy, RCCL, scratch-memory trace, pftrace | Sets RCCL_DEBUG=INFO. |
rocprofv3_full | Everything above + marker/runtime trace, full_profile.txt counters, --stats | High overhead, use short runs. Sets RCCL_DEBUG=INFO, HSA_ENABLE_SDMA=0. |
rocprofv3_lightweight | HIP + kernel trace as JSON | Lowest overhead of the family. |
rocprofv3_agent | Runtime trace, kernel trace, stats as CSV (rocprofv3_output_* prefix) | Post-script consolidates CSVs to canonical rocprofv3_output_*.csv names. |
rocprofv3_agent_counter | As rocprofv3_agent + instruction_mix.txt counters | Post-script also writes instruction_histogram.json from counter/domain-stats CSVs. |
rocprofv3_perfetto | HIP, kernel, memcopy as pftrace | Open in ui.perfetto.dev; also a TraceLens input. |
rocprofv3_api_overhead | HIP, HSA, marker, runtime trace + stats as JSON | |
rocprofv3_pc_sampling | PC sampling (time unit, stochastic, interval 1000), pftrace | Beta 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
| Tool | Collects | Output | Notes |
|---|---|---|---|
rocm_trace_lite | HSA dispatch timeline via rtl trace, RTL_MODE=lite | rocm_trace_lite_output/trace.db (SQLite) | Low overhead; skips some dispatches that already carry a completion signal. See below. |
rocm_trace_lite_default | Same, RTL_MODE=default | same | Broader coverage, higher overhead. |
rocblas_trace, hipblaslt_trace, miopen_trace, tensile_trace, rccl_trace | Library 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_profiler | Power over time | gpu_info_power_profiler_output.csv | Env: POWER_DEVICE (all), POWER_SAMPLING_RATE (0.1), POWER_MODE, POWER_DUAL_GCD (false), POWER_OUTPUT_FILE. |
gpu_info_vram_profiler | VRAM usage over time | gpu_info_vram_profiler_output.csv | Same shape with VRAM_* variables. |
therock_check | Runs detect_therock.sh as a pre-script | Console report | Tells a TheRock install (pip/tarball) from an apt ROCm. |
test_tools_A, test_tools_B | Echo 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.
| Variable | Effect |
|---|---|
ROCM_TRACE_LITE_WHEEL_URL | Install this wheel URL instead (wins over everything; kept out of set -x logs because it may embed credentials). |
ROCM_TRACE_LITE_FOLLOW_LATEST=1 | Resolve the latest release wheel via the GitHub API; falls back to the pinned wheel if the lookup fails. |
ROCM_TRACE_LITE_GIT_REF | Git ref used to rebuild librtl.so natively. Default: the tag of the installed package, else v0.3.7. |
ROCM_TRACE_LITE_SKIP_NATIVE_BUILD=1 | Keep the wheel's librtl.so on images without /opt/rocm. |
RTL_MODE | lite 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_DB | Output directory (default rocm_trace_lite_output) and merged DB path (default <dir>/trace.db). |
RTL_WRAPPER_ALLOW_EMPTY=1 | Exit 0 even when the trace has no kernel dispatches (default: non-zero). |
RTL_WRAPPER_KEEP_REGISTER=1 | Do not set HSA_TOOLS_DISABLE_REGISTER=1 (the wrapper sets it so TheRock's rocprofiler-register does not swallow HSA_TOOLS_LIB). |
/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_URLselects a mirror; bakingdynologanddynointo the image skips the download). - Inference-only workloads have no optimizer: set
TORCH_PROFILE_ITERATIONS=0to 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 variable | Default | Purpose |
|---|---|---|
TORCH_PROFILE_WARMUP_S | 60 | Delay before the first trace request. |
TORCH_PROFILE_ITERATIONS | 5 | Iterations to capture; 0 switches to duration capture. |
TORCH_PROFILE_RETRY_INTERVAL_S / TORCH_PROFILE_MAX_ATTEMPTS | 15 / 40 | Retry cadence while no PyTorch process has registered, and the attempt cap. |
TORCH_PROFILE_PROCESS_LIMIT | 64 | Ranks to trace (dyno's own default of 3 would drop most ranks). |
TORCH_PROFILE_RECORD_SHAPES, _WITH_STACKS, _WITH_MODULES | 1 | Shapes are required for TraceLens per-operator analysis. |
TORCH_PROFILE_OUTPUT_DIR, TORCH_PROFILE_LOG_FILE | torch_profiler_output, libkineto_trace.json | Where Kineto writes (copied out of the container's own disk by the stop script). |
KINETO_USE_DAEMON, KINETO_DAEMON_INIT_DELAY_S | 1, 3 | Register 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
| Target | What is collected |
|---|---|
| Local Docker | Post-scripts copy <tool>_output/ to /myworkspace/, i.e. the host working directory. |
| SLURM | The job script copies rocprof_output, rpd_output, rocm_trace_lite_output, torch_profiler_output, tracelens_output per node into the results directory. |
| Kubernetes | The 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 policy | If 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_mode | Collects |
|---|---|
lite (default) | The env_tags.json subset: ROCm info, GPU topology, driver version. Unknown values fall back to lite with a warning. |
full | Everything 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:
- Top-level
MAD_ROCM_PATHkey inadditional_context(a context key, not an env var). - If env
MAD_AUTO_ROCM_PATH=0: stop scanning; use envROCM_PATH, else/opt/rocm. - Auto-detect, first valid root wins:
/opt/rocm- Versioned
/opt/rocm-*(sorted) rocm-sdk path --root(TheRock pip) whenrocm-sdkis onPATH- Env
ROCM_PATH,ROCM_HOME,HIP_PATH(up to four parent directories are tried) /usr/local/rocm,~/rocm,~/therock- Root inferred from
rocminfo,amd-smi,rocm-smionPATH(<root>/bin/<tool>)
- 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.
docker_env_vars.ROCM_PATHinadditional_context(the override key isROCM_PATH, notMAD_ROCM_PATH).ROCM_PATH, elseROCM_HOME, from the image's OCI config (docker image inspect).- In-image probe (
docker run --rm --entrypoint /bin/sh):rocm-sdk path --root,/opt/rocm,/opt/rocm-*, any other/opt/*,/usr/local/rocm. /opt/rocmwith 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
| Concern | Behavior | Source |
|---|---|---|
| Vendor detection | gpu_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 manager | One cached manager per (vendor, ROCm root): ROCmToolManager or NvidiaToolManager. | gpu_tool_factory |
| ROCm version | hipconfig --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 count | Counts 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 architecture | First gfx* token from rocminfo. | get_gpu_architecture |
gpu_renderDs | Maps 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 |
| Validation | ROCmValidator 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 run | Precedence: 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.
| Value | Behavior |
|---|---|
| unset | No check. |
0 | Check once; fail immediately if any GPU is over the threshold. |
N > 0 | Poll once per second until clear or N seconds of wall-clock time pass (a slow SMI query counts against the budget); then fail. |
| negative | Rejected by the CLI (min=0). |
- Fails closed: if memory cannot be queried (
amd-smi metric --mem-usage --json, thenrocm-smi --showmeminfo vram --json;nvidia-smion NVIDIA) or returns no devices, the run is refused with anExecutionError. - 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-dirand--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
| Variable | Module | Purpose / default |
|---|---|---|
ROCM_PATH | rocm_path_resolver, core/constants | Host ROCm root fallback (see resolution order). |
ROCM_HOME, HIP_PATH | rocm_path_resolver | Additional auto-detect hints for the host root. |
MAD_AUTO_ROCM_PATH | rocm_path_resolver | 0 disables the filesystem scan (default 1). |
MODEL_DIR | core/constants, discover_models, run_orchestrator | Model repository root. Default .. |
MAD_SETUP_MODEL_DIR | core/constants | true copies $MODEL_DIR/* into the working directory at import when it differs from the cwd; skipped for --version/--help. |
MAD_VERBOSE_CONFIG | core/constants | true logs how credential/config values were resolved. |
MAD_SECRETS* | core/context | Any 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_REPO | core/auth, build_orchestrator | Docker 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_LOGIN | core/auth | 1 always uses the existing docker login and never logs in. |
DOCKER_CONFIG | core/auth | Docker config dir inspected for ambient credentials (default ~/.docker). |
MAD_DOCKER_PULL_TIMEOUT_SECS | container_runner | docker pull timeout. Default 1800; a blank, non-integer or non-positive value falls back with a warning. |
MAD_DOCKER_BUILDS | container_runner | Shared directory where multi-node runs stage a docker save tar so workers docker load instead of rebuilding. |
MAD_BARRIER_TOKEN | container_runner | Token for the TCP image-sync barrier between nodes. Default JOB<SLURM_JOB_ID>. |
MAD_DEPLOYMENT_TYPE | container_runner | Reported deployment_type; default local. Set by the SLURM script and K8s pod. |
MAD_LAUNCHER_TYPE, MAD_LAUNCHER | container_runner, run_reporting | Launcher fallback after context and model card (the SLURM script exports the former). |
MAD_IN_SLURM_JOB | container_runner | 1 inside a madengine SLURM job (default 0); with MAD_DEPLOYMENT_TYPE=slurm it removes the cached registry image before pulling. |
MAD_COLLECT_METRICS | container_runner, run_reporting | false 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_NODE | container_runner | Distributed 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, USER | slurm, spur, build_orchestrator, container_runner | Detect an existing allocation (in-place runs, build-on-compute), node counts, rank and per-user paths. |
MIOPEN_USER_DB_PATH | container_runner | Taken from the shell environment into docker_env_vars (a value set by the SLURM script is per node/rank). |
KUBECONFIG | deployment/kubernetes | Kubeconfig when k8s.kubeconfig is unset; then ~/.kube/config. |
HF_TOKEN, MAD_SECRETS_HFTOKEN | deployment/llm_d | Hugging Face token for managed llm-d model downloads. |
MONGO_HOST, MONGO_PORT, MONGO_USER, MONGO_PASSWORD, MONGO_AUTH_SOURCE, MONGO_TIMEOUT_MS | database/mongodb | See Database. Defaults localhost, 27017, empty, empty, admin, 5000. |
NAS_NODES, MAD_AWS_S3, MAD_MINIO, PUBLIC_GITHUB_ROCM_KEY | core/constants | JSON strings. Precedence: env > credential.json > built-in default. Invalid JSON falls back to defaults with an error log. |
MADENGINE_STATE_DIR | core/lifecycle | State directory for session records, heartbeats and gpu_wedged.json. Default ~/.madengine. |
MADENGINE_SESSION_ID | core/lifecycle | Session id labelled on containers (madengine.session); random 12 hex chars if unset. |
MADENGINE_HEARTBEAT_INTERVAL, MADENGINE_HEARTBEAT_STALE_AFTER | core/lifecycle | Container dead-man's-switch timing in seconds. Defaults 15 and 120; non-integer or non-positive values fall back. |
BUILD_NUMBER, pipeline | utils/run_details | CI build number (default 0) and pipeline name, stamped into build_number / pipeline columns. |
TRACELENS_VENV | reporting/tracelens_report | Interpreter 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_MODE | scripts/common (in container) | See rocm-trace-lite. |
DYNOLOG_DEB_URL, DYNOLOG_PORT, TORCH_PROFILE_*, TRACELENS_*, POWER_*, VRAM_*, TRACE_MODE, OUTPUT_FILE | scripts/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
| Variable | Set by | Value / purpose |
|---|---|---|
MAD_GPU_VENDOR | core/context | AMD or NVIDIA. |
MAD_SYSTEM_NGPUS | core/context | GPUs detected on the host. |
MAD_SYSTEM_GPU_ARCHITECTURE | core/context | Architecture string (gfx90a, ...); also a build arg, and the gpu_architecture perf column. |
MAD_SYSTEM_HIP_VERSION, MAD_SYSTEM_GPU_PRODUCT_NAME | core/context | HIP version and product name (AMD). |
MAD_GUEST_OS | container_runner | guest_os from the context (default UBUNTU). |
MAD_RUNTIME_NGPUS | container_runner | GPUs allocated to this run (a real count on bare metal even for n_gpus: -1). |
MAD_MULTI_NODE_RUNNER | container_runner, SLURM script, K8s launcher mixin | Launcher command prefix, e.g. torchrun --standalone --nproc_per_node=8 on local/single-node. |
MAD_MODEL_NAME, MAD_OUTPUT_CSV | container_runner | Card name; the multiple_results file name. |
ROCM_PATH | container_runner | Resolved in-container ROCm root (AMD). |
JENKINS_BUILD_NUMBER | container_runner | From BUILD_NUMBER (default 0). |
MAD_DATAHOME, MAD_DATANAME, MAD_DATA_PROVIDER_TYPE, MAD_DATA_SIZE, MAD_DATA_DOWNLOAD_DURATION | core/dataprovider, container_runner, K8s data scripts | Where a data provider mounted or downloaded the dataset, and its size and timing. |
MAD_WORKLOAD, MAD_WORKLOAD_ARGS, MAD_WORKLOAD_CMD | entrypoints/run_workload.sh | Workload name and args exported by the agnostic entrypoint; MAD_WORKLOAD_CMD is an explicit command override read by it. |
MAD_TOOLS | deployment/slurm (slurm_multi) | Comma-separated tool names for a self-managed script to honour. |
MAD_NODE_IPS | deployment/slurm (slurm_multi) | Rank-ordered node IPs; also seeds IPADDRS and SGLANG_NODE_IPS when unset. |
MAD_REGISTRY_PASSWORD | core/auth | Passed 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_PREFIX | deployment/llm_d | The 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_INDEX | K8s job.yaml.j2 | Pod identity and index inside the Indexed Job. |
MAD_K8S_NNODES, _NODE_PREFIX, _JOB_ID, _IMAGE, _SCRIPT, _ARGS, _AGENT_PORT, _MULTIPLE_RESULTS | deployment/k8s_slurm_multi | Inputs to the in-pod srun/docker/scontrol stand-ins for slurm_multi cards. |
MAD_K8S_AGENT_BIND, _AGENT_PORT, _CONNECT_TIMEOUT, _STATE, _WORKSPACE, _SELF | k8s_shim/mad_k8s.py | Shim tuning; defaults 0.0.0.0, 29711, 600, /tmp/mad-k8s, /workspace. |
MADENGINE_PERF_CSV_BEGIN / _END | K8s job template | Log 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_DEVICES | container_runner, SLURM script | Cleared when HIP_VISIBLE_DEVICES is active so Ray-based AMD images honour GPU visibility. |
Exported by the SLURM job script (job.sh.j2)
| Variable | Value |
|---|---|
MAD_DEPLOYMENT_TYPE | slurm |
MAD_IN_SLURM_JOB | 1 |
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_TYPE | Resolved launcher (falls back to torchrun) |
MAD_MULTI_NODE_RUNNER | Single node: torchrun --standalone --nproc_per_node=<gpus>; multi-node: built by the launcher dispatch |
MAD_COLLECT_METRICS | true on SLURM_PROCID=0, false elsewhere |
MAD_SKIP_PERF_COLLECTION | false on rank 0, true elsewhere |
MAD_CLI_COMMAND | madengine (after the CLI is located; version and path are logged) |
MASTER_ADDR | First host of the job (or step) node list, expanded without scontrol on Spur |
MASTER_PORT | distributed.port (else distributed.master_port), default 29500 |
WORLD_SIZE, NNODES, GPUS_PER_NODE | SLURM_NTASKS or node count; node count; GPUs per node |
RANK, NODE_RANK, LOCAL_RANK | Per task: $SLURM_PROCID exported inside the srun task script; node rank; $SLURM_LOCALID (or 0) |
HIP_VISIBLE_DEVICES, ROCR_VISIBLE_DEVICES, CUDA_VISIBLE_DEVICES | GPU 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_IFNAME | slurm.network_interface when set |
TORCH_ELASTIC_RDZV_TIMEOUT | 3600 |
MIOPEN_USER_DB_PATH | /tmp/.miopen/node_${SLURM_PROCID}_rank_${LOCAL_RANK:-0} |
SLURM_SUBMIT_DIR | Defaulted 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.
| Class | Category | Recoverable | Typical causes |
|---|---|---|---|
ValidationError | VALIDATION | yes | Invalid CLI args, malformed context values. |
NetworkError | CONNECTION | yes | Registry connectivity, pull failures, database connection. |
AuthenticationError | AUTHENTICATION | yes | Registry login failure, bad credential format. |
ExecutionError | RUNTIME | no | Run-phase failures: no compatible images, GPU memory gate, scheduler errors. |
BuildError | BUILD | no | All builds failed, compute-node build timeout, bare-metal env setup failed everywhere. |
DiscoveryError | DISCOVERY | yes | No models matched the tags, unreadable models.json. |
OrchestrationError | ORCHESTRATION | no | Workflow-level failures. |
RunnerError | RUNNER | yes | Runner-internal failures. |
ConfigurationError | CONFIGURATION | yes | Conflicting blocks (k8s + slurm, bare_metal + scheduler), slurm_multi without an image source, invalid --registry, unknown launcher, placeholder image, --use-image + --build-on-compute. |
DeploymentTimeoutError | TIMEOUT | yes | Job 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.
| Topic | Behavior |
|---|---|
| Sources | credential.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 ignored | Template 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 login | If 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 handling | docker 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 mode | login_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
| File | Scope | Format |
|---|---|---|
perf.csv | Cumulative; rows are appended (path set by --output for local runs) | 31-column CSV, below |
perf_entry.csv, perf_entry.json | Latest model only, overwritten each model | Same columns (multi-result runs write all rows) |
perf_super.json | Cumulative superset | List 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.csv | Latest entries of this run | Generated from perf_super.json |
perf_super.csv | Cumulative | perf_super.json as CSV; configs and multi_results serialized as JSON strings |
.madengine_session_start | Next to perf.csv | Row count when the run began, so the end-of-run table highlights this session's rows |
./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.
| Group | Columns |
|---|---|
| Identity | model, pipeline, build_number, machine_name, git_commit, tags (list flattened to comma-separated), args |
| Scale | n_gpus (total = nnodes × gpus_per_node), nnodes, gpus_per_node, gpu_architecture |
| How it ran | platform (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 |
| Image | docker_file, base_docker, docker_sha, docker_image, build_duration |
| Result | performance, metric, relative_change, status, test_duration |
| Data | dataname, 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
| Step | Rule |
|---|---|
| Metric extraction | Log 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. |
| Status | SUCCESS 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_results | The 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 axis | Filled 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. |
launcher | Order: 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 rows | deployment_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
| Command | Module | Notes |
|---|---|---|
madengine report to-html --csv-file-path perf.csv | csv_to_html.py | Writes <name>.html next to the CSV (DataFrame.to_html). |
madengine report to-email --directory DIR --output run_results.html | csv_to_email.py | One <h2> section per CSV found under the directory, concatenated into a single HTML. |
madengine report tracelens, tracelens-compare | tracelens_report.py | Discover 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.py | See 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
| Option | Default |
|---|---|
--file, -f required | CSV or JSON; format from extension, else sniffed ({/[ = JSON) |
--database, --db required | — |
--collection, -c required | — |
--unique-key, -k | Comma-separated fields; auto-detected when omitted |
--batch-size | 1000 |
--no-upsert | Insert only (default is upsert) |
--no-index | Skip index creation |
--dry-run | Parse, transform and report without writing |
--verbose, -v | Show the first 10 errors |
Connection (environment)
| Variable | Default |
|---|---|
MONGO_HOST | localhost |
MONGO_PORT | 27017 |
MONGO_USER, MONGO_PASSWORD | empty; credentials are used only when both are set |
MONGO_AUTH_SOURCE | admin |
MONGO_TIMEOUT_MS | 5000 (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 becomesnull. - 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, pipelinewhose values are unique in the first 100 rows; failing that,model(plustimestampwhen present). Upsert matches on those fields with$set. A document missing every key field is silently skipped in upsert mode.--no-upsertuses plain insert instead. - Indexes: single-field indexes on the key fields, or on whichever of
model, timestamp, date, status, pipelineexist; plus a non-unique compound index when there are several key fields. - Exit codes:
0on success; a partial upload exits0if anything was written, else1; a missing file or total failure exits1. MONGO_PASSWORDis 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/.
| Layer | Path | Contents |
|---|---|---|
| CLI | cli/__init__.py | Re-exports app, cli_main, ExitCode, defaults, and helper functions for backward compatibility. |
| CLI | cli/app.py | Typer 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. |
| CLI | cli/constants.py | ExitCode enum (0-5), VALID_GPU_VENDORS, VALID_GUEST_OS, DEFAULT_MANIFEST_FILE, DEFAULT_PERF_OUTPUT, DEFAULT_DATA_CONFIG, DEFAULT_TOOLS_CONFIG, DEFAULT_TIMEOUT=-1. |
| CLI | cli/utils.py | setup_logging(), split_comma_separated_tags(), create_args_namespace(), save_summary_with_feedback(), display_results_table() (per-node breakdown for runs), display_performance_table(). |
| CLI | cli/validators.py | Context 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(). |
| CLI | cli/commands/__init__.py | Exports the command functions and report_app. |
| CLI | cli/commands/build.py | build: batch manifest, --use-image, --build-on-compute, --config handling, mutual-exclusion checks, drives BuildOrchestrator, maps results to exit 0/2/4/1. |
| CLI | cli/commands/run.py | run: execution-only vs full workflow, timeout sentinel, --wait-for-gpu-memory, --require-pinned-image, --config handling, drives RunOrchestrator, prints result and perf tables. |
| CLI | cli/commands/discover.py | discover: --tags, --full, --json; wraps DiscoverModels. |
| CLI | cli/commands/report.py | report_app with to-html, to-email, tracelens, tracelens-compare. |
| CLI | cli/commands/database.py | database: MongoDB upload via upload_file_to_mongodb, MongoDBConfig.from_env(), UploadOptions. |
| CLI | cli/commands/cleanup.py | cleanup: lists containers labelled madengine.session, skips live owners, reaps via lifecycle.reap(); exit 0/1/5. |
| CLI | cli/commands/setup_page.py | setup-page: calls generate_setup_page(). |
| CLI | config/__init__.py | load_config(config_args): compose, validate, translate; returns (additional_context, metadata). |
| CLI | config/loader.py | HydraConfigLoader: Hydra Compose API, user-YAML / group / value override ordering (_overlay_changed() applies only values that differ from the package defaults). |
| CLI | config/schema.py | ConfigValidator, KNOWN_TOP_LEVEL_KEYS, SUPPORTED_PLATFORMS. |
| CLI | config/translator.py | ConfigTranslator.to_additional_context(): YAML keys to context keys (docker.*, log_error.*), launcher canonicalization, reporting-sentinel handling, CLI metadata split. |
| CLI | configs/config.yaml | Hydra root: default groups (platform=docker, scheduler=local, hardware=amd, launcher=none) and base values for model, docker, build, log_error, outputs. |
| CLI | configs/{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). |
| CLI | configs/{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. |
| CLI | setup_page/__init__.py | Exports CONTEXT_SCHEMA, SECTIONS, WORKLOADS, collect_models, render_setup_page, generate_setup_page. |
| CLI | setup_page/generator.py | collect_models(), render_setup_page() (Jinja2, autoescape on, JSON payload embedded), generate_setup_page(), DEFAULT_INSTALL_CMD. |
| CLI | setup_page/schema.py | Data-driven CONTEXT_SCHEMA (key, type, choices, default, deploy_scope, optional CLI flag), SECTIONS, WORKLOADS, validate_schema(). |
| CLI | setup_page/templates/setup_page.html.j2 | Self-contained HTML/JS template for the setup picker (no external assets). |
| Orch | orchestration/__init__.py | Exports BuildOrchestrator and RunOrchestrator. |
| Orch | orchestration/build_orchestrator.py | BuildOrchestrator.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). |
| Orch | orchestration/run_orchestrator.py | RunOrchestrator.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. |
| Orch | orchestration/image_filtering.py | Pure filters: filter_images_by_gpu_compatibility() (vendor/architecture) and filter_images_by_skip_gpu_arch() (card skip_gpu_arch, NVIDIA name normalisation). |
| Orch | orchestration/platform.py | resolve_context_platform(): validates platform (docker/bare_metal), rejects the retired bare_metal key, stray conda blocks and scheduler combinations. |
| Exec | execution/__init__.py | Exports ContainerRunner. |
| Exec | execution/container_runner.py | ContainerRunner: 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. |
| Exec | execution/container_runner_helpers.py | Log 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. |
| Exec | execution/docker_builder.py | DockerBuilder: Dockerfile discovery (_is_prefix_variant, # CONTEXT filter), build_image(), multi-arch builds, registry naming and push_image() with digest capture, export_build_manifest(). |
| Exec | execution/dockerfile_utils.py | Dockerfile GPU-architecture variable parsing and normalisation; dockerfile_requires_explicit_mad_arch_build_arg(). |
| Exec | execution/run_reporting.py | Backend-independent result handling: extract_performance_from_log(), log_has_errors(), determine_status(), resolve_workload_launcher(), write_perf_records() (perf.csv + perf_super). |
| Exec | execution/bare_metal_runner.py | BareMetalRunner: 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. |
| Exec | execution/conda_env.py | CondaEnvManager: conda/mamba/micromamba detection, prefix management under env_root, dependency-hash stamps, optional ROCm/torch pip wheels, vendor-specific file variants. |
| Core | core/__init__.py | Empty package marker. |
| Core | core/additional_context_defaults.py | DEFAULT_GPU_VENDOR="AMD", DEFAULT_GUEST_OS="UBUNTU", apply_build_context_defaults(). |
| Core | core/auth.py | load_credentials(), login_to_registry() (--password-stdin, ambient-login reuse, placeholder detection), resolve_dockerhub_repository(), explain_registry_denial(). |
| Core | core/console.py | Console.sh() shell executor (timeout, live output, secret=True), redact_secrets(), manifest_safe_context(). |
| Core | core/constants.py | Env-driven constants: MODEL_DIR, MAD_VERBOSE_CONFIG, MAD_SETUP_MODEL_DIR, NAS_NODES, MAD_MINIO, PUBLIC_GITHUB_ROCM_KEY, legacy get_rocm_path(). |
| Core | core/context.py | Context: parses additional context, build-only vs runtime init, GPU/OS/NUMA detection, MAD_SECRETS* propagation, renderD mapping, filter() for Dockerfile # CONTEXT headers. |
| Core | core/dataprovider.py | Data + providers (custom, local, minio, nas, aws), DataProviderFactory, mirror-local handling. |
| Core | core/docker.py | Docker container wrapper: heartbeat-watched docker run -t -d, session labels, sh() via docker exec, context-manager teardown. |
| Core | core/errors.py | Error hierarchy (MADEngineError + 10 subclasses), ErrorCategory, ErrorContext, ErrorHandler Rich panels, create_error_context(). |
| Core | core/image_digest.py | Push-digest parsing and resolve_pinned_image() for --require-pinned-image. |
| Core | core/lifecycle.py | Container registry and labels, heartbeat thread and watchdog script, reap() escalation, wedged-GPU marker, signal handlers, cancel hooks. |
| Core | core/timeout.py | DEFAULT_RUN_TIMEOUT=7200, resolve_run_timeout(), subprocess_timeout(), Timeout (SIGALRM) context manager. |
| Dep | deployment/__init__.py | Public exports: BaseDeployment, DeploymentConfig, DeploymentResult, DeploymentStatus, DeploymentFactory. |
| Dep | deployment/base.py | BaseDeployment (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(). |
| Dep | deployment/common.py | Shared 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()). |
| Dep | deployment/config_loader.py | ConfigLoader: 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(). |
| Dep | deployment/factory.py | DeploymentFactory.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). |
| Dep | deployment/layered_config.py | Per-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. |
| Dep | deployment/primus_backend.py | Primus 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). |
| Dep | deployment/slurm.py | SlurmDeployment: 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). |
| Dep | deployment/slurm_node_selector.py | SlurmNodeSelector: 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. |
| Dep | deployment/spur.py | SpurDeployment(SlurmDeployment): job-array fan-out, render_rendezvous_block() shared-filesystem rendezvous, _sglang_node_ips_script(), marker-file monitor(), DEFAULT_RENDEZVOUS_TIMEOUT=900. |
| Dep | deployment/presets/slurm/ | defaults.json plus profiles/single-node.json and profiles/multi-node.json; selected by slurm.nodes. |
| Dep | deployment/templates/slurm/job.sh.j2 | The 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. |
| Scripts | scripts/slurm/ | epilog.sh and SLURM_EPILOG_SETUP.md: optional admin-installed SLURM epilog that clears stray Ray/vLLM processes and containers after a job. |
| Dep | deployment/kubernetes.py | KubernetesDeployment: 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. |
| Dep | deployment/k8s_template_context.py | KubernetesTemplateContextMixin: builds the Jinja2 context (resources, launcher command, env, tools, data provider, ConfigMap size check); _k8s_pullable_image(). |
| Dep | deployment/kubernetes_launcher_mixin.py | KubernetesLauncherMixin: per-launcher command generators (torchrun, DeepSpeed, TorchTitan, vLLM, SGLang, SGLang disaggregated, Megatron-LM, Primus). |
| Dep | deployment/k8s_scripts.py | KubernetesScriptsMixin: common/tool script loading for the ConfigMap, rocEnvTool wiring (gather_system_env_details), Primus example overlay, K8s tools config. |
| Dep | deployment/k8s_pvc.py | KubernetesPVCMixin: results and shared-data PVCs, RWX detection (provisioner_supports_rwx), resolve_results_layout / resolve_data_layout. |
| Dep | deployment/k8s_secrets.py | Registry-pull and runtime Secrets from credential.json; k8s.secrets strategies (from_local_credentials, existing, omit); imagePullSecrets resolution; ConfigMap payload estimate. |
| Dep | deployment/k8s_names.py | sanitize_k8s_object_name, sanitize_k8s_container_name, sanitize_k8s_label_value; 63-character Job-name cap; pod_job_name_label_selector. |
| Dep | deployment/k8s_results.py | KubernetesResultsMixin: 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). |
| Dep | deployment/k8s_slurm_multi.py | KubernetesSlurmMultiMixin: 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(). |
| Dep | deployment/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. |
| Dep | deployment/llm_d.py | LlmdDeployment(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. |
| Dep | deployment/llm_d_stack.py | LlmdStack: generates chart values for the infra, gaie and modelservice releases and drives helm upgrade --install, template and uninstall. |
| Dep | deployment/presets/k8s/ | defaults.json, gpu-vendors/{amd,nvidia,amd-multi-gpu}.json, profiles/{single-gpu,multi-gpu,multi-node}.json. |
| Dep | deployment/presets/llm-d/defaults.json | llm-d defaults (llm_d block; chart refs, timeouts, teardown). Chart versions intentionally null. |
| Dep | deployment/templates/kubernetes/ | job.yaml.j2, configmap.yaml.j2, service.yaml.j2, pvc.yaml.j2, pvc-data.yaml.j2. |
| Scripts | scripts/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. |
| Repo | deploy/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. |
| Repo | examples/k8s-configs/ | minimal/ and basic/ example --additional-context files (including Primus and SGLang disaggregated) plus a README. |
| Repo | examples/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__.py | Package docstring and __version__ read from installed metadata (dev when uninstalled). |
| Rep | reporting/__init__.py | Re-exports PERF_CSV_HEADER, update_perf_csv, flatten_tags, update_perf_super_json, update_perf_super_csv, convert_super_json_to_csv. |
| Rep | reporting/update_perf_csv.py | PERF_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. |
| Rep | reporting/update_perf_super.py | Cumulative 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(). |
| Rep | reporting/csv_to_html.py | convert_csv_to_html() and ConvertCsvToHtml behind madengine report to-html. |
| Rep | reporting/csv_to_email.py | find_csv_files(), convert_directory_csvs_to_html() and ConvertCsvToEmail behind madengine report to-email (one section per CSV). |
| Rep | reporting/tracelens_report.py | Host-side TraceLens: resolve_python(), check_tracelens_available(), discover_traces(), generate_tracelens_reports(), compare_tracelens_reports(); drives the packaged tracelens_analyze.py. |
| DB | database/__init__.py | Re-exports MongoDBHandler, MongoDBConfig, UploadOptions, UploadResult, upload_file_to_mongodb, upload_csv_to_mongodb. |
| DB | database/mongodb.py | MongoDBConfig.from_env(), JSON/CSV loaders with type inference, DocumentTransformer (metadata, normalization, unique-key inference), MongoDBUploader (indexes, bulk upsert), upload_file_to_mongodb(), legacy MongoDBHandler. |
| Util | utils/__init__.py | Package marker with docstring. |
| Util | utils/discover_models.py | CustomModel dataclass and DiscoverModels: root, per-directory and dynamic (get_models_json.py) discovery, scoped vs unscoped tag selection, MODEL_DIR setup. |
| Util | utils/config_parser.py | ConfigParser: 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(). |
| Util | utils/gpu_tool_manager.py | BaseGPUToolManager ABC: shell execution, tool availability, result cache, logging helpers. |
| Util | utils/rocm_tool_manager.py | ROCmToolManager: 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(). |
| Util | utils/nvidia_tool_manager.py | NvidiaToolManager: nvidia-smi / nvcc wrappers for CUDA and driver version, GPU count, architecture, memory usage. |
| Util | utils/gpu_tool_factory.py | get_gpu_tool_manager(vendor, rocm_path): cached per (vendor, ROCm root), auto-detects vendor; clear_manager_cache(), get_cached_managers(). |
| Util | utils/gpu_validator.py | GPUVendor, GPUValidationResult, ROCmValidator, NVIDIAValidator, detect_gpu_vendor(), validate_gpu_installation(), GPUInstallationError. |
| Util | utils/gpu_config.py | GPUConfigResolver and resolve_runtime_gpus(): runtime > deployment > model card > default GPU count with mismatch warnings. |
| Util | utils/gpu_memory_gate.py | check_gpu_memory_free() behind --wait-for-gpu-memory (512 MB threshold, 1 s poll, fail-closed). |
| Util | utils/rocm_path_resolver.py | RocmPathResolver, resolve_host_rocm_path(), finalize_container_rocm_path() (OCI env, in-image probe, default), get_rocm_path_legacy(). |
| Util | utils/therock_markers.py | TheRock share/therock marker paths and is_therock_tree(). |
| Util | utils/session_tracker.py | SessionTracker: records the perf.csv row count at run start in .madengine_session_start to highlight this run's rows. |
| Util | utils/run_details.py | get_pipeline(), get_build_number() (from pipeline / BUILD_NUMBER), flatten_tags_in_place(). |
| Util | utils/log_formatting.py | DataFrame formatting for logs: format_dataframe_for_log(), format_dataframe_rich(), print_dataframe_beautiful(). |
| Util | utils/ops.py | PythonicTee (stdout tee to log file) and file_print(). |
| Util | utils/path_utils.py | get_madengine_root() (package directory) and scripts_base_dir_from(). |
| Scripts | scripts/common/tools.json | Tool catalog consumed by apply_tools() and the K8s ConfigMap builder: pre/post scripts, command wrapper and env per tool. |
| Scripts | scripts/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). |
| Scripts | scripts/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. |
| Scripts | scripts/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. |
| Scripts | scripts/common/tools/counters/ | rocprofv3 counter lists: compute_bound, memory_bound, communication_bound, full_profile, instruction_mix (.txt). |
| Scripts | scripts/common/entrypoints/run_workload.sh | Model-agnostic workload dispatcher used as an encapsulate_script; exports MAD_WORKLOAD / MAD_WORKLOAD_ARGS. |
| Scripts | scripts/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. |
| Repo | tests/ (repo root) | unit/, integration/, e2e/, fixtures/ (dummy MAD repo), conftest.py, scripts/k8s_validate_manifests.sh. |
| Repo | examples/ (repo root) | Ready-to-use contexts: build-manifest/, configs/ (Hydra templates), k8s-configs/, slurm-configs/, llm-d-configs/, profiling-configs/. |
| Repo | docs/ (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). |
| Repo | deploy/ (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 asdummy,dummy_prof,dummy_torchrun,dummy_vllm,dummy_sglang_disagg,dummy_llm_d,dummy_bare_metal,dummy_therock),docker/Dockerfiles,scripts/,data.jsonand a placeholdercredential.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 theassert_build_manifest_valid/assert_perf_csv_validhelpers. - tests/scripts/k8s_validate_manifests.sh validates rendered Kubernetes manifests.
Markers (registered in pyproject.toml)
| Marker | Meaning |
|---|---|
unit, integration, e2e | Test tier |
slow | Skip with -m "not slow" |
gpu, amd, nvidia, cpu | Hardware or vendor specific |
requires_docker, requires_models | Needs 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 checkssrc/madengine/and excludesscripts/. Type hints are expected on public functions. - Security scan: bandit via pre-commit, configured in
pyproject.toml(excludestests; skipsB101). - Hygiene hooks: trailing whitespace, EOF newline, YAML/JSON/TOML validity, files > 1000 KB, merge-conflict markers, stray
debugstatements, 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, acrosscore/docker.py,docker_builder,container_runner,conda_env,slurm,llm_d_stackand 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 theMAD_REGISTRY_PASSWORDenvironment variable of the subprocess and pipes it todocker login --password-stdin, run withsecret=True. - Credential files are validated.
credential.jsonmust be a JSON object, otherwise loading fails with aConfigurationError. Blank and shipped-template values (your-docker-username) are treated as unset so a placeholder never overrides a working ambientdocker login. - Logs and manifests are scrubbed.
ConsolemasksMAD_SECRETS*=...assignments and common token shapes (hf_,sk-,ghp_,xox*-) with***REDACTED***;manifest_safe_context()removesMAD_SECRETS_*values from the context written tobuild_manifest.json. Log environment keys, never values. - Least privilege containers. Ordinary runs do not request
CAP_SYS_ADMINor/dev/fuse. Both are added only for SSHFS-mounted NAS data; user pre-scripts androcprof/perftools keepCAP_SYS_ADMINwithout/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 withconda run -p. See Platforms. slurm_multion Kubernetes — a card's SLURM launcher runs unchanged across one pod per node, with stand-ins forsrun,dockerandscontrol. 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.7wheel; Spursglang-disaggpeer list is built from per-rank files. - Least-privilege Docker defaults —
CAP_SYS_ADMINand/dev/fuseare no longer requested for ordinary runs (see Platforms). --build-on-computenow forwardsslurm.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 labelledapp=madengine. - GPU counting ignores
amd-smiwarning lines; placeholder Docker Hub credentials are ignored.
2.4.0 — 2026-10-18
- Container lifecycle — in-container heartbeat watchdog, signal-handler teardown,
madengine cleanup, andGPU_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; stalemultiple_resultsfiles are removed before a run.
2.3.0 — 2026-10-09
--configHydra/YAML frontend forbuildandrun. See --config.- TraceLens reports (
madengine report tracelens,tracelens-compare) and on-demand PyTorch traces (torch_profiler_dynolog). See Profiling tools. docker_run_cmdrecorded onbuild_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-imagedigest enforcement forrun; per-carddocker_build_arg; reuse of an existingdocker login;slurm.skip_gpus_directive.- Behavior change:
--timeoutprecedence restored to flag > model card > 7200 s default;0disables. See run. madengine discover --fulland--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_multilauncher on SLURM,--use-image/--build-on-compute, conditional--build-context tools=, localMAD_MULTI_NODE_RUNNER.
2.0.x — 2026-04-09 to 2026-05-26
- Complete rewrite: unified
madengineCLI (legacymad-*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, rocEnvToolfullmode.