Compose files now live under `<model>/<engine>/compose/<topology>/<file>.yml`, with topology as a folder rather than a filename prefix. Solves all 7 inconsistencies surfaced in the post-rename audit (single-card composes without `single-` prefix, unsuffixed `docker-compose.yml` ambiguity, fine-tunes encoding model name in filename, etc.) by making the directory hierarchy enforce the convention. Layout: models/<model>/<engine>/compose/<topology>/<feature>.yml Where: - <model>: qwen3.6-27b, gemma-4-31b - <engine>: vllm, llama-cpp, sglang - <topology>: single, dual, multi3, multi4, multi8 - <feature>: docker-compose.yml (default) | turbo.yml | dflash.yml | etc. Each topology subdir has a `docker-compose.yml` for the recommended starter — bare `cd <topology> && docker compose up` works because docker compose finds that filename automatically. Variants drop the `docker-compose.` prefix since they're invoked via `-f` flag. 27 compose file moves total: - 18 Qwen vLLM composes redistributed across single/dual/multi4 - 2 Qwen llama-cpp composes into single/ - 6 Gemma vLLM composes redistributed across single/dual - 1 untracked qwopus-bf16mtp moved to dual/ Inside each compose: relative paths to `../patches/` and `../cache/` bumped to `../../patches/` / `../../cache/`, and `../../../../models-cache` to `../../../../../models-cache` (one extra `..` for the new depth). Reference updates across 148 files (BENCHMARKS, all docs, CHANGELOGs, sibling-table cross-references in compose headers, scripts, patch READMEs, .github issue templates, tools/residency-instrument). scripts/switch.sh VARIANTS map updated; tags themselves unchanged (`vllm/dual` → `dual/docker-compose.yml`, `vllm/dual4` → `multi4/docker-compose.yml`, `vllm/gemma-mtp` → `gemma-4-31b/.../dual/docker-compose.yml`, etc.). AGENTS.md "Compose layout" section rewritten to describe the new hierarchy, with concrete examples and the fine-tune exception (`dual/carnice-bf16mtp.yml` carries the fine-tune name as a filename prefix until the fine-tune graduates to its own model directory). All switch.sh paths verified to resolve to actual files post-move. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13 KiB
AGENTS.md
Guidance for AI coding agents (Claude Code, Cursor, Copilot, Continue, etc.) working in this repo. Focused — only conventions an agent wouldn't infer from the code itself.
Read first
Before making non-trivial changes:
README.md— what the repo is, two-routes framing, repo layoutdocs/SINGLE_CARD.md/docs/DUAL_CARD.md— pick-by-workload guidance + the cliffs they referencedocs/UPSTREAM.md— every upstream issue / PR we depend on or have filed (see "Upstream issues" section below for why this matters)models/qwen3.6-27b/INTERNALS.md— DFlash forensics, AutoRound rationale, Marlin pad fork, MTP headCONTRIBUTING.md— what kinds of PRs land cleanly + benchmark + verify protocol
Hardware truths
- 2× RTX 3090 Ampere SM 8.6, PCIe-only, no NVLink (and we won't add it).
- Custom all-reduce must be disabled in vLLM/SGLang configs (PCIe topology breaks NVLink-assumed paths).
- No native FP8 compute; FP8 KV is a storage optimization only.
- Speculative decoding using EAGLE / DFlash is blocked on Qwen3-Next family (DeltaNet rollback). MTP works. See
docs/UPSTREAM.md— vllm#39931.
Upstream issues — single source of truth
docs/UPSTREAM.md tracks every upstream issue / PR we depend on, have filed, or use as context. Before filing a new upstream issue or referencing an existing one in code/docs, check this file. When status changes (issue closed, PR merged, pin bumped), update the row.
This rule exists because we previously had upstream links scattered across CHANGELOG, INTERNALS, FAQ, and per-compose comment headers — and they drifted. The tracker file is the canonical place; cross-link to it from anywhere else.
When filing a fresh upstream issue from this work:
- Add the row to
docs/UPSTREAM.mdfirst - Link the issue back to
noonghunna/club-3090in the body (helps maintainers see affected user surface) - If the upstream eventually merges + propagates, update the row to ✅ Resolved and bump the relevant pin (Genesis commit, vLLM nightly, etc.) in the same commit / PR
Conventions on this repo
Bench protocol
3 warm + 5 measured runs. Canonical prompts: 800-word essay (narrative, max_tokens=1000) + quicksort code (max_tokens=800). temperature=0.6, top_p=0.95, top_k=20. Capture both wall-time TPS and engine-internal gen throughput from logs. Always capture per-card peak VRAM alongside TPS.
Genesis opt-in env vars
Genesis ships ~50 env-gated patches. Some are targeted bugfixes (P64 streaming, PN8 memory savings, P3/P5/P6 KV); others are behavioral mitigations that silently rewrite the request (P68 = tool_choice → required, P69 = inject "must use tool" reminder). Behavioral mitigations need a streaming + large-prompt repro before shipping default-on. We learned this the hard way on 2026-04-29 — see docs/UPSTREAM.md → Genesis #9 row + the club-3090 #2 thread.
If you're considering enabling a new Genesis env var by default in a shipped compose:
- Read the patch's header docstring in
vllm/_genesis/wiring/. Does it modifyrequest.tool_choice,request.messages, or rewrite output? - If yes (behavioral): run a streaming repro with prompt > the patch's threshold + a casual user message ("hi") +
tool_choice: auto. Iffinish_reason=stopwith empty content, don't ship default-on. - Pure bugfixes (no behavioral override) are fine to ship default-on once they pass
verify-full.sh.
CHANGELOG
CHANGELOG.md(cross-cutting) andmodels/<name>/CHANGELOG.md(per-model) are append-only history. Don't rewrite past entries even when a finding is superseded — add a new entry. The historical trail is load-bearing for "why did we do X."- Old entries can reference files / patches that no longer exist. That's fine — leave them.
Compose variants
- Each variant ships with a header table comparing it against sibling composes in the same directory. Update both directions when adding/removing variants.
- Keep the variant set lean. Three composes that overlap badly (similar TPS + similar context + same KV) is worse than two with a clean differentiation. Removed
fast-chat.yml2026-04-29 for exactly this reason.
Compose layout — <model>/<engine>/compose/<topology>/<feature>.yml
The directory hierarchy encodes (model, engine, topology); the filename encodes the feature stack. No level repeats information from another.
| Path level | Encodes | Examples |
|---|---|---|
models/<model>/ |
Model | qwen3.6-27b · gemma-4-31b |
models/<model>/<engine>/ |
Inference engine | vllm · llama-cpp · sglang |
compose/<topology>/ |
Hardware topology | single · dual · multi3 · multi4 · multi8 |
<feature>.yml (filename) |
Feature stack | docker-compose.yml (recommended default) · turbo.yml · dflash.yml · nvlink-dflash-noviz.yml |
Topology rule: single (TP=1) and dual (TP=2) have no count ambiguity. multi<N> requires the count because N varies (3 / 4 / 5 / 6 / 8). Aligns with docs/SINGLE_CARD.md / DUAL_CARD.md / MULTI_CARD.md doc framing.
Default-per-topology rule: each topology subdirectory has a docker-compose.yml — the recommended starter. Bare cd <topology> && docker compose up works because docker compose finds docker-compose.yml automatically. Variants drop the docker-compose. prefix because they're explicitly invoked with -f.
Feature suffix order (when stacking): interconnect → drafter → KV → vision modifier. Examples:
dual/turbo.yml— TP=2 + TurboQuant KVdual/dflash.yml— TP=2 + DFlash drafterdual/nvlink-dflash-noviz.yml— TP=2 + NVLink + DFlash + no visiondual/int8.yml— TP=2 + INT8 PTH KVdual/awq.yml— TP=2 + AWQ-4bit weightsmulti4/dflash.yml— TP=4 + DFlash
Concrete examples (post-2026-05-09 restructure):
models/qwen3.6-27b/vllm/compose/single/docker-compose.yml— Qwen single-card defaultmodels/qwen3.6-27b/vllm/compose/single/long-text.yml— Qwen single-card text-only long-ctx variantmodels/qwen3.6-27b/vllm/compose/dual/docker-compose.yml— Qwen dual default (fp8 + MTP)models/qwen3.6-27b/vllm/compose/dual/turbo.yml— Qwen dual + TQ3models/qwen3.6-27b/vllm/compose/multi4/dflash.yml— Qwen 4-card + DFlashmodels/gemma-4-31b/vllm/compose/dual/docker-compose.yml— Gemma dual default (bf16 + MTP)models/gemma-4-31b/vllm/compose/dual/int8.yml— Gemma dual + INT8 PTH KV
Filename collisions across topology dirs (e.g. dual/dflash.yml vs multi4/dflash.yml) are fine — the path disambiguates. Registry tags in scripts/switch.sh decouple from filesystem paths; rename only the file path in the VARIANTS map and keep tags backward-compatible.
Fine-tune exception: model-specific fine-tunes that share the canonical model's compose directory keep the fine-tune name as a filename prefix (e.g. dual/carnice-bf16mtp.yml, dual/qwopus-bf16mtp.yml under models/qwen3.6-27b/). Long-term those fine-tunes can graduate to their own model directories (models/carnice-v2-27b/, models/qwopus3.6-27b/) when their variant set warrants it.
Profile schema header (every compose, every time)
Every compose starts with a Profile (at-a-glance) block declaring the (Model, Topology, Drafter, KV, Vision, Max-ctx, Genesis) tuple in structured form. Free-form description follows below the schema, not in place of it.
# ===========================================================================
# Profile (at-a-glance):
# Model: <name + quant — e.g. "Qwen3.6-27B (Lorbus AutoRound INT4)">
# Topology: <e.g. "Dual 3090 PCIe (TP=2, no NVLink)">
# Drafter: <none | MTP n=N | DFlash n=N | ngram K=N>
# KV: <fp8_e5m2 | bf16 | int8_per_token_head | turboquant_3bit_nc>
# Vision: <yes | no>
# Max ctx: <e.g. 262K>
# Genesis: <none | v7.72.2 | N/A — Genesis is Qwen3-Next-specific>
# Status: <optional — only if not production: ⚠️ PREVIEW / BOOT-OOM / etc.>
# Best for: <one short phrase — what workload this serves; ⭐ for canonical>
# ---------------------------------------------------------------------------
# (existing free-form description continues below)
This rule applies to shipped composes AND local-only test composes — apply the convention even before deciding whether to ship; it avoids a rename later if the experiment graduates.
When testing a new model, create the directory hierarchy from the start: models/<new-model>/<engine>/compose/<topology>/docker-compose.yml. The hierarchy enforces the convention; filenames encode only the feature stack within that topology. When the model isn't Qwen3-Next, write Genesis: N/A — Genesis is Qwen3-Next-specific in the profile schema so readers don't expect Genesis-style perf folds where they don't apply.
Where do experimental / unvalidated composes live?
Same directory as shipped composes, but kept untracked until validation passes. Don't create a separate experimental/ subdirectory — the relative paths to ../patches/... and ../cache/... are calibrated to the compose dir, and promoting an experiment from a sub-folder would require re-pathing every mount.
Workflow:
- Author the compose in
models/<model>/<engine>/compose/<topology>/<feature>.yml(ordocker-compose.ymlfor the topology default) with the standard profile schema header. MarkStatus: ⚠️ EXPERIMENTAL(or⚠️ PREVIEWif quality issues are known) so readers know it's not validated. - Don't
git adduntil validation passes. The file shows up ingit statusas??— that's the signal.git ls-tree -r HEADlists only shipped composes; the gap between that andls compose/*.ymltells you what's pending validation. - Validation gates before promoting:
verify-full.sh8/8,verify-stress.sh7/7 (or documented failures with rationale),bench.sh(numbers added to BENCHMARKS.md),soak-test.sh SOAK_MODE=continuous(catches Cliff 2b). - Promote: drop the
Status: ⚠️ EXPERIMENTALline from the profile schema,git add, commit. Cross-rig validation can come later via thenumbers-from-your-rigissue template.
For entirely new models under validation (e.g. "let's try MiniMax-M2.7"): keep the whole models/<new-model>/ directory untracked until at least one compose validates. Avoid pushing models/<new-model>/README.md etc. before there's a working compose to back it up — empty model directories on master signal capability we don't actually have.
References (orphan composes / patches sitting in this state as of 2026-05-09):
models/gemma-4-31b/vllm/compose/dual-awq.yml— local-only AWQ-4bit weights variant, Status: ⚠️ EXPERIMENTALmodels/gemma-4-31b/vllm/compose/dual-dflash-int8.yml— Status: ⚠️ EXPERIMENTAL (boots only with vLLM PR #42102 vendored mounts)models/qwen3.6-27b/vllm/compose/qwopus-bf16mtp.yml— Status: ⚠️ PREVIEW (41× line repetition + NIAH drop, not production)
Documentation
- Don't create new docs proactively. Most non-obvious things belong in
INTERNALS.md,FAQ.md,SINGLE_CARD.md, orDUAL_CARD.md. New top-level files only when there's a recurring search miss. - Charts: source
.svg+ exported.pngat retina resolution (≥1500px wide). Markdown embeds use.png(clicking opens a viewable image; SVG opens raw XML). Re-generate withpython3 tools/charts/gen-perf.pyandgen-vram.pyafter editing data. - For any change that adds a footnote to "this depends on upstream X" — the answer is to link the row in
docs/UPSTREAM.md, not to inline-cite the upstream URL.
Tests
verify-full.sh— fast functional smoke (~1–2 min). Runs on every compose change.verify-stress.sh— boundary cases (longctx ladder + tool-prefill OOM ~5–10 min). Runs on cliff-related changes.bench.sh— canonical TPS bench. Run when you change anything that could move TPS (compose flags, Genesis env vars, vLLM pin).
Commits
- New commit per logical change. Don't amend published commits.
- Commit messages: subject ≤72 chars, imperative ("Disable P68/P69..." not "Disabled..."), optional body for "why."
- Don't push without local verify-full + verify-stress passing for the affected compose.
Hooks / verification
- Pre-flight checks live in
scripts/preflight.shand run fromsetup.sh+launch.sh. They check docker / GPU / disk before any heavy work. If a check fails, print an actionableFix:hint, never a cryptic mid-run crash.
Things to NOT do
- Don't add NVLink suggestions. The user has explicitly declined.
- Don't recommend EAGLE / DFlash spec-decode on Qwen3-Next single-card. It's blocked by DeltaNet rollback (see
docs/UPSTREAM.md→ vllm#39931). MTP works. - Don't enable Genesis behavioral patches (P68/P69 class) by default. They override user intent. If a user wants them, they can flip the env var.
- Don't claim a TPS number you didn't measure. "Should be ~80" labeled as estimate is fine; "is 80" needs a bench.
- Don't compress historical CHANGELOG entries. Append-only.
- Don't scatter upstream issue links across multiple docs. Link to the row in
docs/UPSTREAM.mdinstead.