Replace the purged `vllm-nightly-clean` pin (nightly-bf610c2f, now 404 on Docker Hub — so the launcher injected a dead image for every slug on it) with a v0.22.0 two-engine design: - `vllm-stable`: OVERLAY-FREE v0.22.0 — the broad successor to vllm-nightly-clean. Serves Qwen3-Next (hybrid 27b + MoE 35B-A3B), generic `dense` transformers, and uncurated derived-emission bases. Migrates vllm/qwen-35b-a3b-dual, vllm/dual, vllm/minimal (live-validated on stock v0.22.0: TP=2 + MTP + tool-call clean; marlin-pad confirmed unnecessary). Added as the first loads:true arch pin on the qwen + dense rows. - `vllm-gemma-stable`: KEPT as the OVERLAY-CARRYING v0.22.0 engine (#40391 per-head INT8 KV + #42006 tool-parser) — the Gemma 4 path. Unchanged. - `vllm-pip-baseline`: the renamed pip `dense` lower-bound (frees the `vllm-stable` name for the docker engine above). Why two engines and not one: `vendored_overlays` is LOAD-BEARING, not documentation. `derived_emittable` (CONTRACT-5) refuses any engine whose `vendored_overlays != []` as a derived-emission base, and `diagnose-profile` expects the Gemma overlay declared on its engine. A single engine cannot be both overlay-free (for derived/Qwen bases) and overlay-carrying (for Gemma provenance) — so the two stay split. Compose owns patch APPLICATION; engine owns provenance/compatibility. (An attempt to fold both into one engine broke 4 tests via exactly these two consumers; the split is the validated shape.) Also adds scripts/tests/test-compose-image-drift.sh: asserts every fixed `${VLLM_IMAGE:-<tag>}` compose default equals its engine's `install.spec` (catches bump-the-engine-forget-the-literal drift), and a docs/ADDING_MODELS.md "Coherence rules" section codifying the compose/engine/patch separation. 42/42 gate green; resolver emits v0.22.0 for all migrated Qwen + Gemma slugs. The remaining experimental/preview slugs still on vllm-nightly-clean migrate in follow-up as each is validated, then that engine is deprecated. Refs #254. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
19 KiB
Compose Generator — scripts/generate-compose.sh (v0.8.0, #141)
Operator/user guide for the v0.8.0 compose generator. It emits a
minimal-reproduction docker-compose for one in-scope profile by
replaying the whole shipped service definition that profile was captured
from, wiring (or deliberately omitting) the patches that profile depends on,
and refusing — loudly and cleanly — anything outside its scope.
This is the [D] substrate of the v0.8.x design. Its job is the narrow one
described by the v0.8.x headline scope:
evaluate any safetensors HF repo; pull only vLLM-loadable supported ones, and only when the gates pass (or an explicit override is accepted)
The generator itself implements the generation slice of that: it does not download, derive, or VRAM-gate (those are other v0.8.x phases). It takes an already-resolved in-scope profile and produces the compose that reproduces it.
For the contributor contract governing the patch metadata this generator
consumes, see docs/PATCH_POLICY.md. For the underlying
patch/arch/profile data model see
docs/PATCH_ATTRIBUTION.md.
1. Mission — reproduce + flag, NEVER repair
The single locked rule that explains every other behaviour:
The generator reproduces a known config and flags what is missing. It never repairs anything.
Concretely, the shipped code guarantees:
- It never rewrites the engine image — the captured
image: ${VLLM_IMAGE:-vllm/vllm-openai:nightly-${VLLM_NIGHTLY_SHA}}expression is passed through verbatim (see §5). - It never wires a patch whose drift-guard failed — a failed capability-scoped guard omits the patch (DEGRADED), a failed foundational guard refuses the whole compose. It never edits the patch to make it apply.
- It never blind-passes
--trust-remote-code— a governed security slot is stripped from the captured body and only ever emitted if the trc gate explicitly permits, which it never does in-scope (see §6). - It synthesizes nothing — every non-insertion-point line, including all
constants and param-slot
${VAR:-default}expressions, reproduces byte-for-byte from the captured shipped compose (see §4).
A generated compose that won't boot is a true signal that something in the captured config drifted, not a bug to be silently patched around. Repair is a separate, out-of-scope loop.
2. Scope and the two clean-refuse classes
The generator handles non-Genesis, vLLM-only profiles. Anything else is a clean refusal (stderr message + non-zero exit), never a partial or best-effort emit.
Scope is decided by two gates that run first, before any patch or template work:
| # | Gate | Discriminator | Refuse message (verbatim shape) | Exit |
|---|---|---|---|---|
| 1 | Engine type | engine.type != "vllm" |
engine <id> type='<t>' != vllm; the #141 generator is non-Genesis vLLM only -> refuse (out of scope) |
2 |
| 2 | Genesis-equipped | profile genesis_equipped: true |
profile <p> is genesis_equipped:true (<evidence>); Genesis-flag generation is permanently out of scope -> refuse |
2 |
genesis_equipped discriminator
genesis_equipped is captured per profile in
scripts/lib/profiles/profile_runtime.yml using the locked v0.8.x §6
discriminator: a profile is genesis_equipped: true iff its shipped compose
contains a _genesis / GENESIS_PIN / GENESIS_ENABLE token OR its
kv_format starts with turboquant. (It is not keyed off an engine's
required_genesis flag.) Observed examples:
$ scripts/generate-compose.sh --profile vllm/tools-text
[generate-compose] REFUSE: profile vllm/tools-text is genesis_equipped:true
(compose contains GENESIS_* / _genesis token); Genesis-flag generation is
permanently out of scope -> refuse
$ scripts/generate-compose.sh --profile vllm/dual-tq3-mtp
[generate-compose] REFUSE: profile vllm/dual-tq3-mtp is genesis_equipped:true
(kv_format 'turboquant_3bit_nc' starts with 'turboquant'); Genesis-flag
generation is permanently out of scope -> refuse
$ scripts/generate-compose.sh --profile llamacpp/default
[generate-compose] REFUSE: engine llama-cpp-local type='llama.cpp' != vllm;
the #141 generator is non-Genesis vLLM only -> refuse (out of scope)
Genesis-flag compose generation is permanently out of scope (a locked v0.8.x decision), not a deferred feature.
3. Input — --profile is authoritative
scripts/generate-compose.sh --profile <COMPOSE_REGISTRY key> [--out FILE] [--accept-degraded]
--profile is the only authoritative input. It must be a key in
scripts/lib/profiles/compose_registry.py (COMPOSE_REGISTRY). An unknown
profile is a usage error:
$ scripts/generate-compose.sh --profile vllm/does-not-exist
[generate-compose] REFUSE: unknown profile 'vllm/does-not-exist' (not in COMPOSE_REGISTRY) # exit 64
Convenience tuple (discovery aid only — NOT authoritative)
--model / --engine / --kv / --tp form a convenience tuple that
lists candidate --profile values and exits non-zero (4). It never
generates — it is a discovery aid so you can find the authoritative profile
name:
$ scripts/generate-compose.sh --model gemma-4-31b --engine vllm-stable
convenience tuple matched these profiles (re-run with an authoritative --profile):
--profile vllm/gemma-int8-mtp
--profile vllm/gemma-bf16-mtp
--profile vllm/gemma-mtp-tp1
# exit 4
No args at all → usage error (exit 64).
Output
--out FILE writes the compose there (parent dirs created) and prints a
one-line summary to stderr; without --out the compose goes to stdout. The
3-category provenance header (§7) is always part of the emitted file.
Capacity values are the reference profile's — NOT fit-adapted to your hardware
The generated compose's capacity knobs — --max-model-len, --gpu-memory-utilization, --max-num-seqs, and the KV cache dtype — are copied verbatim from the captured reference profile (the shipped compose it reproduces). The generator does not run a fit solve, does not size context to your GPU's VRAM, and does not down-cast the KV dtype for capacity. So:
- On a card smaller than the reference target, the emitted
--max-model-lenmay not boot — it is not shrunk for you. - On a card larger than the reference target, you are leaving context/throughput on the table — it is not grown for you.
- For a derived (non-curated) model the KV dtype is whatever the model ships (often
bf16); it is not optimised toward a denser hardware-legal format.
This is deliberate (Mission §1 — reproduce + flag, NEVER repair): the generated file is a known-safe starting point, not a hardware-tuned config. For the actual fit on your hardware, run scripts/pull.sh <slug> --profile-like <key> --recommend (or tools/kv-calc.py --solve-max-ctx ...) and tune the emitted ${MAX_MODEL_LEN} (it is intentionally an env-overridable default) accordingly. An opt-in capacity optimiser is planned for a later release; until then, right-sizing is a deliberate user step.
Running a generated compose — it is NOT relocatable
Per the whole-service-template model (§4), the generator copies the shipped
service definition verbatim — including its bind-mount sources, which are
repo-relative (e.g. ../../patches/<patch>/..., ../../cache/..., and the
${MODEL_DIR:-../../../../../models-cache} default). The generator deliberately
does not rewrite these (it synthesizes nothing). They resolve correctly only
when Docker Compose's project directory is the shipped compose's own directory.
Consequence: a generated file run from an arbitrary location (e.g.
docker compose -f /somewhere/else/out.yml up) has dangling bind-mount
sources. Docker silently creates empty directories at the missing source
paths, so e.g. a mounted chat-template file becomes a directory and vLLM aborts
at boot with ... looks like a file path, but it failed to be opened ... Is a directory.
Run a generated compose with --project-directory anchored to the shipped
compose's directory (the value of compose_service_template.source in
profile_runtime.yml), and set the same MODEL_DIR / VLLM_IMAGE the repo
launcher sets:
docker compose \
--project-directory models/<model>/vllm/compose/<topology> \
-f <generated-file> up -d
Equivalently, write --out into that shipped compose directory and run it
there. This is consistent with §10 (coexistence with the pre-baked tree): the
generated artifact is a drop-in for that location, not a portable standalone.
4. The whole-service-template model
The capture unit is the entire shipped service definition, recorded per
profile in profile_runtime.yml under compose_service_template. The
generator loads the file named at compose_service_template.source and
classifies every token into exactly one of three classes:
| Class | What it is | What the generator does |
|---|---|---|
| param-slot | ${TP}, ${MAX_MODEL_LEN}, ${GPU_MEMORY_UTILIZATION}, ${KV_CACHE_DTYPE}, --max-num-seqs, --speculative-config, ports, NVLink env |
Reproduced as the shipped ${VAR:-default} expression (the registry carries the resolved value; the env-default mechanism does the substitution at runtime). --speculative-config is omitted entirely when the profile has no drafter. |
| governed-slot | --trust-remote-code (and any future security/policy flag) |
Captured but flagged governed. Emitted only when the gate state explicitly permits — which never happens in-scope (§6). |
| constant | The image: expression, shm_size, ipc, deploy.resources, model-cache volume, base environment/entrypoint, --model, --served-model-name, --quantization, --dtype, --tool-call-parser, --chat-template, --reasoning-parser, --default-chat-template-kwargs, --enable-prefix-caching, --enable-chunked-prefill, --enforce-eager, … |
Reproduced verbatim, byte-for-byte. |
The only transformation the generator applies to the body is at two named insertion points:
volumes:— overlay / sidecar bind-mount lines.entrypoint:— sidecar / install-script invoke lines.
A selected+wired patch keeps its mount/invoke lines; a selected-but-omitted patch (delivery-gap or failed-guard) has its lines stripped. Everything else is reproduced exactly — the generator synthesizes nothing. For a golden profile the compose-keyed selection reproduces precisely the patch set the maintainer shipped, so the generated service body is byte-identical to the shipped file outside the insertion points (the golden-parity invariant, §9).
5. The image expression is preserved, never rewritten
The shipped composes use
image: ${VLLM_IMAGE:-vllm/vllm-openai:nightly-${VLLM_NIGHTLY_SHA}}. This is
a captured constant and is passed through verbatim. The engine pin
flows through the existing VLLM_NIGHTLY_SHA / VLLM_IMAGE env mechanism —
the generator does not rewrite the image line.
The engine pin is validated (§8), not substituted. Validation confirms the captured config is internally consistent; it does not edit the artifact.
6. --trust-remote-code is a governed slot, never emitted in-scope
--trust-remote-code is governed under the locked v0.8.x §88 security model.
It is treated as a governed-slot, not a constant:
- Gate (step 5): the arch's
requires_trust_remote_codeis read fromarch_patches.yml. If it istrueorunverified, the generator refuses (security refusal, exit2) — trc acknowledgement is the pull-gate's job, not the generator's. - Suppression: when generation proceeds (arch trc = evidence-cited
false),trc_emitis provablyFalse, and the generator strips any--trust-remote-codetoken from the captured body before emitting.
The combination guarantees an in-scope generated compose never
blind-passes --trust-remote-code. Every golden triple is asserted to emit
no --trust-remote-code and report meta.trc_emitted == False. The header
states this explicitly:
# NOTE: --trust-remote-code is a GOVERNED slot (locked §88); it is
# NOT emitted for any in-scope profile (arch trc gate = false).
Any future security/policy flag joins the governed-slot set and gets the same treatment.
7. Patch selection, delivery gaps, drift guards (§4.1 graded drift)
Patch selection is compose-keyed only: patch P is selected for profile
X iff X ∈ P.load_bearing_when[].composes. Every other patch is
EXCLUDED (not load-bearing for this compose) — listed in header category
[3], emitted nowhere.
For each selected patch the generator classifies it, in this order:
- Delivery-gap first. If the patch declares a
delivery_gaps[]entry covering this profile, it is selected-but-undelivered: its wiring is omitted, a header WARNING records the gap'sissuestring, and the drift-guard is skipped entirely. This is the structural acknowledgement of a known coverage boundary (the #145 class — seedocs/PATCH_POLICY.md). - Drift-guard (graded, §4.1). For a will-be-wired patch the generator
records its
drift_guard. The guard is a runtime import/boot/behavioral probe that cannot run at generation time (no engine container here); the contract is that the shipped, maintainer-tested state is "applies cleanly" (= drift-guard-tested, a locked decision), so the patch is wired and the guard is surfaced for the boot leg / operator to re-run. When a guard does fail (the test harness drives this deterministically viaCLUB3090_FORCE_GUARD_FAIL; there is no other way to force a runtime probe failure from a unit test), the gap-before-guard ordering and the §4.1 grade decide the outcome:- capability-scoped fail → patch OMITTED, compose flagged
DEGRADED. The run requires
--accept-degradedto proceed; without it the generator refuses with exit3. A failed patch is never wired. - foundational fail (or
on_fail: hard-refuse) → hard-refuse (exit2) — even with--accept-degraded. Weights/boot depend on it; there is nothing safe to emit.
- capability-scoped fail → patch OMITTED, compose flagged
DEGRADED. The run requires
Observed degraded path (forced fail, no ack):
$ scripts/generate-compose.sh --profile vllm/gemma-int8-mtp # CLUB3090_FORCE_GUARD_FAIL=gemma-vllm-gemma4-tool-parser-fixes
[generate-compose] REFUSE: DEGRADED: capability-scoped patch(es)
[gemma-vllm-gemma4-tool-parser-fixes] omitted after a failed drift-guard; re-run with
--accept-degraded to proceed # exit 3
With --accept-degraded the same case proceeds: the patch is in
degraded_omitted (never wired), its mount lines are physically absent
from the body, and the header carries a WARNING: DEGRADED block.
The generator never repairs — capability-scoped failure degrades, foundational failure refuses; neither edits the patch.
8. Engine-pin <id>@<sha> validation
Step 3 validates the engine pin (it does not rewrite the image, §5). The
pin is derived as <engine_id>@<sha> from the engine's install.spec (the
concrete image tag), and must match an entry in the resolved arch row's
engine_pin[] with loads: true:
<id>(no sha) or<id>@<sha>forms are both accepted.- A pin that matches the engine but is
loads: false→ refuse, surfacing the pin'sreason. - No
loads: truematch for the engine → refuse.
The arch row is resolved via arch_model_xref (E.model ∈ model_slugs), and
two further validations gate generation: E.tp must be in the arch's
valid_tp.tp_divisors, and E.kv_format must be in the engine's
supported_kv_formats. All are clean refusals (exit 2) on failure. The
validated pin is recorded in the header:
# engine-pin (validated, image NOT rewritten): vllm-nightly-clean@bf610c2f...
The kv_format → --kv-cache-dtype map (non-Genesis only) is exercised on
every run and unit-tested:
kv_format |
emitted --kv-cache-dtype |
|---|---|
bf16, fp16 |
(no arg) |
fp8_e5m2 |
fp8_e5m2 |
fp8_e4m3 |
fp8 |
int8_per_token_head |
auto+PTH |
q4_0 |
q4_0 |
k8v4 |
k8v4 |
9. The 3-category header and the 5-triple golden-parity guarantee
Every generated compose carries a provenance header above the top-level
services: key (so the patch-attribution service_body() parser discards
it — header text can never create a false-positive reachability hit). The
header has three categories:
- selected + WIRED — drift-guard-tested, wired at
volumes:/entrypoint:. - selected + UNDELIVERED — delivery-gap or failed-guard; wiring omitted, with the reason.
- EXCLUDED — not load-bearing for this compose.
Plus the validated engine pin, the mission line, an optional WARNING: DEGRADED block, and the governed-trc note.
Golden-parity guarantee. scripts/tests/test-generate-compose.sh asserts
five golden triples, all verified genesis_equipped: false, spanning every
in-scope engine class:
| Profile | Engine | Notes |
|---|---|---|
vllm/minimal |
vllm-nightly-clean |
tp1, fp8, drafter=None; surfaces the qwen3coder delivery-gap as undelivered |
vllm/dual |
vllm-nightly-clean |
tp2, fp8, MTP drafter |
vllm/gemma-bf16-mtp |
vllm-stable |
gemma, bf16 |
vllm/gemma-int8-mtp |
vllm-stable |
int8-PTH, multi-file overlay |
vllm/gemma-mtp-tp1 |
vllm-stable |
single-card fp8 risk path |
Per triple the test asserts: the semantic diff vs the shipped compose is
confined to the two patch insertion points (image expression + every constant
reproduce verbatim); selected+wired ⊆ shipped; wired patches pass reaches()
on the generated compose (actual wiring, not header text);
selected-but-undelivered patches are not reachable; the 3-category header
is present; and no --trust-remote-code is emitted. The refusal/degraded
matrix (genesis, llama.cpp, foundational hard-refuse, capability-scoped
degrade ± ack, convenience tuple) is asserted alongside. At least one triple
is booted on-rig.
10. Coexistence with the pre-baked compose tree
The generator does not replace or delete the existing hand-maintained
models/<model>/<engine>/compose/... tree. The shipped composes remain the
ground truth — they are exactly what the generator captures from and
reproduces. Operators who want the curated, maintainer-tested file keep using
it directly; the generator is for deriving a minimal reproduction of an
in-scope profile (e.g. to bisect drift, or as the [D] substrate of the
v0.8.x pull flow). There is no big-bang tree deletion in v0.8.0.
11. Exit codes
| Code | Meaning |
|---|---|
0 |
compose emitted (possibly DEGRADED with --accept-degraded) |
2 |
clean scope / validation / foundational refusal |
3 |
capability-scoped DEGRADED, --accept-degraded absent |
4 |
convenience tuple matched profiles (not authoritative — re-run with --profile) |
64 |
argv / lookup misuse (no --profile, unknown profile) |