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>
279 lines
12 KiB
Bash
Executable File
279 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
#
|
||
# Switch between club-3090 compose variants.
|
||
#
|
||
# Brings down whatever's currently running, brings up the new variant,
|
||
# and (optionally) waits for the server to report ready on /v1/models.
|
||
# Stateless — re-run any time you want a different config.
|
||
#
|
||
# Usage:
|
||
# bash scripts/switch.sh <variant> # switch + tail until ready
|
||
# bash scripts/switch.sh <variant> --no-wait # switch and return immediately
|
||
# bash scripts/switch.sh --list # show all variants
|
||
# bash scripts/switch.sh --down # just bring down whatever's up
|
||
#
|
||
# Variant names (engine/file, file is the docker-compose.<file>.yml stem):
|
||
#
|
||
# Single-card vLLM:
|
||
# vllm/default 48K + TQ3 + MTP + vision + tools (recommended)
|
||
# vllm/long-vision 198K + TQ3 + vision (cliff-safe; Cliff 2 single-prompt >50K still applies)
|
||
# vllm/long-text 180K + TQ3 + MTP + text-only (Balanced MTP — 60K single-prompt closed via v7.69 + #35975)
|
||
# vllm/long-text-no-mtp 200K + TQ3 + no MTP + text-only (Max-context — same Cliff 2 closure, more KV pool, slower decode)
|
||
# vllm/bounded-thinking 180K + TQ3 + structured-CoT FSM in reasoning (recommended grammar: DeepSeek scratchpad — 87.4% combined HE+/LCB v6)
|
||
# vllm/tools-text 75K + fp8 + MTP + text-only (IDE agents — Cline / Cursor)
|
||
# vllm/minimal 32K + fp8 (no Genesis, no spec-decode, simplest)
|
||
#
|
||
# Dual-card vLLM (TP=2):
|
||
# vllm/dual 262K + fp8 + 2 streams + vision (recommended dual)
|
||
# vllm/dual4 262K + fp8 + 4 streams + vision (4× 3090 PCIe baseline)
|
||
# vllm/dual4-dflash 262K + FP16 + DFlash N=5 + 2 streams + vision (4× 3090 code)
|
||
# vllm/dual-turbo 262K + TQ3 + 4 streams + vision (multi-tenant)
|
||
# vllm/dual-dflash 185K + FP16 + DFlash N=5 + vision (peak code TPS)
|
||
# vllm/dual-dflash-noviz 200K + FP16 + DFlash N=5 + no vision (peak code, max ctx)
|
||
# vllm/dual-nvlink 262K + fp8 + 2 streams + vision (REQUIRES NVLink bridge — community/experimental)
|
||
# vllm/dual-nvlink-turbo 262K + TQ3 + 4 streams + vision (REQUIRES NVLink bridge — community/experimental)
|
||
# vllm/dual-nvlink-dflash 185K + FP16 + DFlash N=5 + vision (REQUIRES NVLink bridge — community/experimental)
|
||
# vllm/dual-nvlink-dflash-noviz 188K + FP16 + DFlash N=5 + no vision (REQUIRES NVLink bridge — community/experimental)
|
||
# vllm/gemma-mtp Gemma-4-31B + Google MTP drafter (32K, bf16 KV, vision — community/experimental, pre-merge)
|
||
#
|
||
# Single-card llama.cpp:
|
||
# llamacpp/default Q3_K_XL + 262K + q4_0 KV + vision (max ctx, no cliffs)
|
||
# llamacpp/concurrent Q3_K_XL + 192K pool + 4 parallel slots + vision
|
||
#
|
||
# Env overrides (rarely needed):
|
||
# COMPOSE_BIN Default: "docker compose" (set to e.g. "podman compose" if needed)
|
||
# READY_URL Default: http://localhost:8020/v1/models
|
||
# READY_TIMEOUT Default: 600 (seconds — longer for cold cudagraph capture)
|
||
|
||
set -euo pipefail
|
||
|
||
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
COMPOSE_BIN="${COMPOSE_BIN:-docker compose}"
|
||
READY_TIMEOUT="${READY_TIMEOUT:-600}"
|
||
|
||
# Load .env if present, so PORT / MODEL_DIR / etc. flow through to docker
|
||
# compose AND to the ready-URL probe below.
|
||
if [[ -f "${ROOT_DIR}/.env" ]]; then
|
||
set -a
|
||
# shellcheck disable=SC1091
|
||
source "${ROOT_DIR}/.env"
|
||
set +a
|
||
fi
|
||
|
||
# Per-variant default port (matches each compose's "${PORT:-XXXX}:8000"
|
||
# fallback). Used when neither $PORT nor $READY_URL is set explicitly.
|
||
declare -A VARIANT_DEFAULT_PORT=(
|
||
[vllm/default]=8020
|
||
[vllm/long-vision]=8020
|
||
[vllm/long-text]=8020
|
||
[vllm/long-text-no-mtp]=8021
|
||
[vllm/bounded-thinking]=8020
|
||
[vllm/tools-text]=8020
|
||
[vllm/minimal]=8020
|
||
[vllm/dual]=8010
|
||
[vllm/dual4]=8015
|
||
[vllm/dual4-dflash]=8016
|
||
[vllm/dual-turbo]=8011
|
||
[vllm/dual-dflash]=8012
|
||
[vllm/dual-dflash-noviz]=8013
|
||
[vllm/dual-nvlink]=8014
|
||
[vllm/dual-nvlink-turbo]=8017
|
||
[vllm/dual-nvlink-dflash]=8018
|
||
[vllm/dual-nvlink-dflash-noviz]=8019
|
||
[vllm/gemma-mtp]=8030
|
||
[vllm/gemma-mtp-tp1]=8031
|
||
[vllm/gemma-dflash]=8032
|
||
[llamacpp/default]=8020
|
||
[llamacpp/concurrent]=8020
|
||
)
|
||
|
||
# variant -> "engine|compose_dir|file" (file relative to compose_dir)
|
||
declare -A VARIANTS=(
|
||
[vllm/default]="vllm|models/qwen3.6-27b/vllm/compose|single/docker-compose.yml"
|
||
[vllm/long-vision]="vllm|models/qwen3.6-27b/vllm/compose|single/long-vision.yml"
|
||
[vllm/long-text]="vllm|models/qwen3.6-27b/vllm/compose|single/long-text.yml"
|
||
[vllm/long-text-no-mtp]="vllm|models/qwen3.6-27b/vllm/compose|single/long-text-no-mtp.yml"
|
||
[vllm/bounded-thinking]="vllm|models/qwen3.6-27b/vllm/compose|single/bounded-thinking.yml"
|
||
[vllm/tools-text]="vllm|models/qwen3.6-27b/vllm/compose|single/tools-text.yml"
|
||
[vllm/minimal]="vllm|models/qwen3.6-27b/vllm/compose|single/minimal.yml"
|
||
[vllm/dual]="vllm|models/qwen3.6-27b/vllm/compose|dual/docker-compose.yml"
|
||
[vllm/dual4]="vllm|models/qwen3.6-27b/vllm/compose|multi4/docker-compose.yml"
|
||
[vllm/dual4-dflash]="vllm|models/qwen3.6-27b/vllm/compose|multi4/dflash.yml"
|
||
[vllm/dual-turbo]="vllm|models/qwen3.6-27b/vllm/compose|dual/turbo.yml"
|
||
[vllm/dual-dflash]="vllm|models/qwen3.6-27b/vllm/compose|dual/dflash.yml"
|
||
[vllm/dual-dflash-noviz]="vllm|models/qwen3.6-27b/vllm/compose|dual/dflash-noviz.yml"
|
||
[vllm/dual-nvlink]="vllm|models/qwen3.6-27b/vllm/compose|dual/nvlink.yml"
|
||
[vllm/dual-nvlink-turbo]="vllm|models/qwen3.6-27b/vllm/compose|dual/nvlink-turbo.yml"
|
||
[vllm/dual-nvlink-dflash]="vllm|models/qwen3.6-27b/vllm/compose|dual/nvlink-dflash.yml"
|
||
[vllm/dual-nvlink-dflash-noviz]="vllm|models/qwen3.6-27b/vllm/compose|dual/nvlink-dflash-noviz.yml"
|
||
[vllm/gemma-mtp]="vllm|models/gemma-4-31b/vllm/compose|dual/docker-compose.yml"
|
||
[vllm/gemma-mtp-tp1]="vllm|models/gemma-4-31b/vllm/compose|single/docker-compose.yml"
|
||
[vllm/gemma-dflash]="vllm|models/gemma-4-31b/vllm/compose|dual/dflash.yml"
|
||
[llamacpp/default]="llamacpp|models/qwen3.6-27b/llama-cpp/compose|single/docker-compose.yml"
|
||
[llamacpp/concurrent]="llamacpp|models/qwen3.6-27b/llama-cpp/compose|single/concurrent.yml"
|
||
)
|
||
|
||
# Container name patterns we'll bring down — covers all current composes.
|
||
RUNNING_PATTERN="^(vllm-qwen36-27b|llama-cpp-qwen36-27b|vllm-qwen36-27b-bounded-thinking|vllm-qwen36-27b-long-text-no-mtp|vllm-gemma-4-31b)"
|
||
|
||
usage() {
|
||
sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'
|
||
exit 0
|
||
}
|
||
|
||
list_variants() {
|
||
echo "Available variants:"
|
||
for v in "${!VARIANTS[@]}"; do
|
||
IFS='|' read -r eng dir file <<< "${VARIANTS[$v]}"
|
||
echo " ${v} → ${dir}/${file}"
|
||
done | sort
|
||
exit 0
|
||
}
|
||
|
||
down_running() {
|
||
local running
|
||
running=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -E "$RUNNING_PATTERN" || true)
|
||
if [[ -z "$running" ]]; then
|
||
echo "[switch] no club-3090 container running"
|
||
return
|
||
fi
|
||
for c in $running; do
|
||
echo "[switch] bringing down: ${c}"
|
||
# find the compose dir from the container's labels — fallback to direct stop
|
||
local lbl_dir lbl_file
|
||
lbl_dir=$(docker inspect --format '{{ index .Config.Labels "com.docker.compose.project.working_dir"}}' "$c" 2>/dev/null || true)
|
||
lbl_file=$(docker inspect --format '{{ index .Config.Labels "com.docker.compose.project.config_files"}}' "$c" 2>/dev/null || true)
|
||
if [[ -n "$lbl_dir" && -n "$lbl_file" ]]; then
|
||
(cd "$lbl_dir" && ${COMPOSE_BIN} -f "$lbl_file" down) || docker stop "$c" >/dev/null
|
||
else
|
||
docker stop "$c" >/dev/null
|
||
fi
|
||
done
|
||
}
|
||
|
||
up_variant() {
|
||
local v="$1"
|
||
if [[ -z "${VARIANTS[$v]:-}" ]]; then
|
||
echo "ERROR: unknown variant '${v}'." >&2
|
||
echo "Run: bash scripts/switch.sh --list" >&2
|
||
exit 1
|
||
fi
|
||
IFS='|' read -r eng dir file <<< "${VARIANTS[$v]}"
|
||
local full_dir="${ROOT_DIR}/${dir}"
|
||
if [[ ! -f "${full_dir}/${file}" ]]; then
|
||
echo "ERROR: compose file missing at ${full_dir}/${file}" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# Pre-up sanity:
|
||
# - genesis_pin: warn if on-disk Genesis tree differs from GENESIS_PIN in setup.sh
|
||
# - repo_drift: warn if local HEAD is behind origin/master
|
||
# - compose_deps: HARD error if compose mounts a model dir that doesn't exist on host
|
||
# (catches the "you didn't WITH_DFLASH_DRAFT=1 then tried dual-dflash-noviz" case;
|
||
# see club-3090#37 — this is the canonical fix raphael / snoby asked for)
|
||
# - kv_format_hint: soft warn if VRAM class needs --kv-cache-dtype override (#47)
|
||
if [[ -f "${ROOT_DIR}/scripts/preflight.sh" ]]; then
|
||
# shellcheck source=preflight.sh
|
||
source "${ROOT_DIR}/scripts/preflight.sh"
|
||
preflight_genesis_pin "${ROOT_DIR}" || true
|
||
preflight_repo_drift "${ROOT_DIR}" || true
|
||
preflight_compose_deps "${full_dir}/${file}" || exit 1
|
||
preflight_kv_format_hint "${full_dir}/${file}" || true
|
||
fi
|
||
|
||
echo "[switch] bringing up: ${v} (${dir}/${file})"
|
||
(cd "${full_dir}" && ${COMPOSE_BIN} -f "${file}" up -d)
|
||
}
|
||
|
||
resolve_ready_url() {
|
||
# Precedence: $READY_URL (full override) → $PORT (port only, host=localhost)
|
||
# → per-variant default port from VARIANT_DEFAULT_PORT.
|
||
local variant="$1"
|
||
if [[ -n "${READY_URL:-}" ]]; then
|
||
return 0
|
||
fi
|
||
local port="${PORT:-${VARIANT_DEFAULT_PORT[$variant]:-8020}}"
|
||
READY_URL="http://localhost:${port}/v1/models"
|
||
}
|
||
|
||
wait_ready() {
|
||
# Find the container we just brought up so we can detect crashes mid-boot
|
||
# AND surface stage progress markers from its logs while we wait.
|
||
local container
|
||
container=$(docker ps --format '{{.Names}}' 2>/dev/null \
|
||
| grep -E '^(vllm-qwen36-27b|llama-cpp-qwen36-27b|vllm-gemma-4-31b)' | head -1)
|
||
|
||
if [[ -z "$container" ]]; then
|
||
# Compose started but no container is up — almost always a syntax error
|
||
# or env-var issue caught before vLLM even started.
|
||
echo "[switch] ERROR: no container running after 'compose up' — boot failed before vLLM started." >&2
|
||
echo "[switch] Run 'docker compose -f <file> logs' for the compose-level error." >&2
|
||
exit 1
|
||
fi
|
||
|
||
echo "[switch] waiting for ${READY_URL} (container=${container}, timeout ${READY_TIMEOUT}s)..."
|
||
local elapsed=0 step=4 last_marker=""
|
||
until curl -sf -o /dev/null --max-time 3 "${READY_URL}"; do
|
||
# CRASH DETECTION: if the container died, dump tail and exit fast — don't
|
||
# silently burn through the full timeout on a dead server.
|
||
local state
|
||
state=$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null || echo missing)
|
||
if [[ "$state" != "true" ]]; then
|
||
local exit_code
|
||
exit_code=$(docker inspect -f '{{.State.ExitCode}}' "$container" 2>/dev/null || echo "?")
|
||
echo "[switch] ERROR: container '${container}' is no longer running (state=${state}, exit=${exit_code})." >&2
|
||
echo "[switch] Last 30 log lines:" >&2
|
||
docker logs --tail 30 "$container" 2>&1 | sed 's/^/[switch] | /' >&2
|
||
echo "[switch] Full logs: docker logs ${container}" >&2
|
||
exit 1
|
||
fi
|
||
|
||
sleep $step
|
||
elapsed=$((elapsed + step))
|
||
|
||
# PROGRESS SIGNAL: surface boot-stage markers so users see WHAT vLLM is
|
||
# doing, not just that it's "still waiting". The grep is selective — one
|
||
# line per phase transition, not raw log streaming.
|
||
local marker
|
||
marker=$(docker logs --tail 50 "$container" 2>&1 | grep -oE \
|
||
'Genesis Results: .* applied|Resolved architecture: \w+|Loading weights|Compilation finished|Memory profiling|Capturing CUDA graphs|Application startup complete' \
|
||
| tail -1 || true)
|
||
if [[ -n "$marker" && "$marker" != "$last_marker" ]]; then
|
||
echo "[switch] ${elapsed}s — ${marker}"
|
||
last_marker="$marker"
|
||
elif [[ $((elapsed % 30)) -eq 0 ]]; then
|
||
echo "[switch] ${elapsed}s elapsed, still waiting..."
|
||
fi
|
||
|
||
if [[ $elapsed -ge $READY_TIMEOUT ]]; then
|
||
echo "[switch] timeout — server not ready after ${READY_TIMEOUT}s" >&2
|
||
echo "[switch] tail logs: docker logs --tail 100 ${container}" >&2
|
||
exit 1
|
||
fi
|
||
done
|
||
echo "[switch] ✓ ready (${elapsed}s)"
|
||
}
|
||
|
||
# --- arg parsing ---
|
||
WAIT=1
|
||
case "${1:-}" in
|
||
-h|--help|"") usage ;;
|
||
--list) list_variants ;;
|
||
--down) down_running; exit 0 ;;
|
||
esac
|
||
|
||
VARIANT="$1"
|
||
shift || true
|
||
for arg in "$@"; do
|
||
case "$arg" in
|
||
--no-wait) WAIT=0 ;;
|
||
*) echo "Unknown flag: $arg"; exit 1 ;;
|
||
esac
|
||
done
|
||
|
||
resolve_ready_url "${VARIANT}"
|
||
down_running
|
||
up_variant "${VARIANT}"
|
||
[[ $WAIT -eq 1 ]] && wait_ready
|
||
echo "[switch] done. Try: curl -s ${READY_URL%/v1/models}/v1/models | jq ."
|