feat(build): honor per-model docker_build_arg from models.json - #175
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the Docker build path so each model can define its own build-time docker_build_arg block in models.json, while still allowing invocation-wide --additional-context and multi-arch/credential build args to take precedence. This makes per-model build pins reproducible and avoids forcing callers to repeat model-specific build arguments on every run.
Changes:
- Merge per-model
docker_build_argfrommodel_infointo the build args used byDockerBuilder.build_image(), with explicit precedence rules (context + caller-supplied args win). - Make
DockerBuilder.get_build_arg()resilient to contexts that don’t include adocker_build_argkey. - Add integration tests and documentation describing per-model
docker_build_argsupport.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/madengine/execution/docker_builder.py |
Merges model-card build args into per-build arguments and avoids KeyError when context lacks docker_build_arg. |
tests/integration/test_docker_integration.py |
Adds integration tests verifying model-card build args are applied and that context/multi-arch args override them. |
docs/configuration.md |
Documents docker_build_arg support within models.json entries and precedence with --additional-context. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Build args could only come from --additional-context, which is global to the invocation, so a model had no way to declare its own build-time pins (e.g. VLLM_REPO/VLLM_REF) in-repo: every caller had to repeat them on the command line, and two models needing different values could not be built together. build_image now merges a model card's docker_build_arg into that model's own build. Context, credential and multi-arch args win on conflict, matching the precedence _pick_context_over_model() already applies to run-time keys, so an operator can still redirect a build without editing models.json. Also stop indexing ctx["docker_build_arg"] directly in get_build_arg(): a non-empty run_build_arg combined with a context lacking that key raised KeyError.
171fa5e to
58172c2
Compare
A malformed docker_build_arg (string or list) previously surfaced as an AttributeError from deep inside the build path. Fail early with a message naming the model and the offending type instead.
|
Thanks for taking a look! Both points are addressed. Full test suite — no regressions. Ran unit + integration on this branch and on unmodified
The delta is exactly the 4 tests this PR adds; every pre-existing test still passes. (The one skip is the pre-existing Copilot comment. Fair catch — a The behavior itself is unchanged: context and multi-arch/credential args still win over the model card, matching |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/madengine/execution/docker_builder.py:255
card_build_arg = model_info.get("docker_build_arg") or {}treats falsy non-dict values (e.g.,[],0,false) as if the key were absent, so malformeddocker_build_argcan slip through without raising the intended RuntimeError. Consider explicitly checking for key presence /Noneand raising for any non-dict type.
# Per-model build args declared in the model card (models.json). Lets a model
# pin build-time sources (e.g. VLLM_REPO/VLLM_REF) in-repo instead of requiring
# every caller to pass --additional-context. Context and multi-arch/cred args
# win on conflict, matching _pick_context_over_model() for run-time keys.
card_build_arg = model_info.get("docker_build_arg") or {}
if not isinstance(card_build_arg, dict):
raise RuntimeError(
f"docker_build_arg for model {model_info['name']} must be a JSON object "
f"mapping build-arg names to values, got {type(card_build_arg).__name__}"
)
Testing summaryEvery check below was run on this branch and on unmodified 1. Unit + integration suites — no regressions
Tests added in
2. Real
|
| Scenario | Result |
|---|---|
| Card pins, zero CLI flags | both args land on the docker build command and are baked into the image environment |
| Same, read from inside a running container | values match the card, as does a file written at build time from the ARG |
Control: identical card on develop |
image still carries the Dockerfile defaults — the card is ignored without this change |
--additional-context override |
the overridden key wins, the other card key is retained (per-key merge) |
--additional-context-file override |
same result via file |
| Two models, one invocation, different pins | each image gets its own values, no cross-contamination |
Card declares an arg no ARG consumes |
build still succeeds; the extra arg is passed and harmlessly ignored |
Malformed docker_build_arg (a string) |
targeted RuntimeError; no AttributeError in any log |
| Pin actually consumed | a Dockerfile cloning from the card's repo at the card's ref fetched and checked out the exact pinned 40-character SHA |
Real multi-arch build (--target-archs gfx942) |
a card deliberately setting MAD_SYSTEM_GPU_ARCHITECTURE=CARD_TRIES_TO_WIN did not shadow it: the image reports gfx942, and the card's other arg still applied |
| An existing model recipe with its real, unmodified Dockerfile | both pins on the build command with no build-arg flags on the CLI; the develop control passed neither |
Existing behavior explicitly confirmed unchanged: --additional-context and --additional-context-file precedence, multi-arch architecture injection, credential build args, and models that declare no docker_build_arg at all.
3. Copilot review comment — addressed
Copilot's one comment, on docker_builder.py:255, was a fair catch: a non-object docker_build_arg would have surfaced as a bare AttributeError from inside the build path, hard to trace back to the model card.
Fixed in e85cade — build_image() validates before iterating:
card_build_arg = model_info.get("docker_build_arg") or {}
if not isinstance(card_build_arg, dict):
raise RuntimeError(
f"docker_build_arg for model {model_info['name']} must be a JSON object "
f"mapping build-arg names to values, got {type(card_build_arg).__name__}"
)Covered by test_malformed_model_card_docker_build_arg_raises, and confirmed on hardware: a card carrying "docker_build_arg": "VLLM_REF=..." now fails with docker_build_arg for model <name> must be a JSON object mapping build-arg names to values, got str, with no AttributeError anywhere in the logs.
This reverts commit 6c966a8. PR #178 targeted main by mistake; it should have gone to develop. Because the head branch was based on develop while main was five PRs behind, the squash merge pulled unreleased develop work into main along with the feature: #166, #168, #163, #161 and #175. Reverting restores main to ec4de0b exactly. The feature is being re-opened against develop; no revert-of-this-revert is needed, since the squash commit shares no SHAs with develop's history and a later develop -> main merge applies cleanly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two PRs merged to develop after the 2.2.0 section was written had no changelog entry: per-model docker_build_arg support (#175) and pinned image digest enforcement (#180). Adds Added entries for both features and Fixed entries for the bugs bundled in them: the 60s Console.sh timeout on docker push, the invalid container name derived from repo@sha256 references, the local-tag fallback that defeated pinning on pull failure, and the get_build_arg KeyError on a context missing docker_build_arg. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Problem
Docker build args can only reach a build through
--additional-context/--additional-context-file. That dictionary is global to the invocation, so:DockerBuilder.get_build_arg()reads onlycontext.ctx["docker_build_arg"], andbuild_image()never consultsmodel_info, so adocker_build_argblock in amodels.jsonentry is silently ignored today.Change
build_image()now merges a model card'sdocker_build_arginto that model's own build args.Precedence is context-wins, matching what
_pick_context_over_model()already does for run-time keys (log_error_patterns, node counts, and friends): a key present in--additional-contextoverrides the card, so an operator can still redirect a build without editingmodels.json. Credential args and the multi-archadditional_build_argsalso win, so a card entry cannot shadowMAD_SYSTEM_GPU_ARCHITECTUREduring a--target-archsbuild.Also stops indexing
ctx["docker_build_arg"]directly inget_build_arg(). It survives today only becauseContext.__init__always seeds that key; a non-emptyrun_build_argwith a context lacking it raisedKeyError.No schema change is needed — unknown keys already pass through
DiscoverModelsuntouched.{ "name": "my_model", "dockerfile": "docker/my_model", "tags": ["my_model"], "docker_build_arg": { "VLLM_REPO": "https://github.com/myorg/vllm.git", "VLLM_REF": "d723eb305eb78d1bda0ed357b2b54cc29487221f" } }Motivation
A MAD recipe pins its vLLM fork and commit in a per-model Dockerfile. Reproducing that image today means either editing the Dockerfile or remembering two
--build-argvalues on every rebuild. Neither survives an invocation the recipe author does not control (CI, another team). With this change the pins live next to the model that needs them.Test plan
New tests in
tests/integration/test_docker_integration.py:docker_build_argreaches the docker build command--additional-contextoverrides the card for the same keyMAD_SYSTEM_GPU_ARCHITECTUREVerified end to end with real
docker buildruns, two models in one invocation with different pins and no CLI flags:Both values confirmed baked into the resulting images, and re-running with
-f ctx.jsoncollapsed both to the context value.Full suite: 741 passed, 1 skipped (the skip requires a non-AMD GPU).