Every gemma-4-31b vLLM compose pinned a purged Docker Hub nightly (e47c98ef / bf610c2f), un-bootable for fresh users (#250, #167 class). Prune the 9-variant set to 3 and repoint survivors onto the immutable stable release tag vllm/vllm-openai:v0.21.0 (never :latest). Survivors: vllm/gemma-mtp (bf16 dual, default), vllm/gemma-int8 (int8-PTH dual), vllm/gemma-mtp-tp1 (fp8 single). Dropped: gemma-dflash, gemma-dflash-int8, gemma-int8-tq3, gemma-bf16, gemma-awq; the 262K path folds into gemma-int8 via a CTX env override. Gemma is decoupled from the shared Qwen nightly profiles via a new vllm-gemma-stable engine profile; Qwen nightly profiles are untouched. Diagnose decoupling: the pruned gemma dflash patch dirs doubled as the diagnose tool's cross-model disk-source proxies for two Qwen overlays (vllm-pr41703-dflash, vllm-pr42102-dflash-kv-quant). Those capabilities are image-baked in the pinned nightly, not mountable files, so mark them image_baked and have diagnose skip the disk-source check for image-baked overlays. Runtime-neutral (no Qwen compose mounts them). switch.sh launcher: handle the new VLLM_IMAGE engine-pin export (the twin loop in launch.sh had it; switch.sh did not -> "unexpected engine pin export"), and guard the VLLM_NIGHTLY_SHA echo for the image-only stable profile (was unbound under set -u). Validated on 2x RTX 3090 (Ampere, v0.21.0): full scripts/tests/*.sh suite 35/0; live boots of gemma-mtp (bf16) and gemma-int8 serve coherent output, qwen vllm/dual regression-clean. gemma-mtp-tp1 (fp8_e4m3) is correctly SM-gated (required_sm=9.0) -- confirmed Ampere-incompatible (Triton "fp8e4nv not supported on sm_86"), a documented 32 GB+/Hopper variant; beellama remains the single-card gemma path on Ampere. Refs #451, #250, #167. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
90 lines
3.2 KiB
Bash
Executable File
90 lines
3.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
cd "$ROOT_DIR"
|
|
export PYTHONPATH="$ROOT_DIR${PYTHONPATH:+:$PYTHONPATH}"
|
|
|
|
python3 - <<'PY'
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
root = Path.cwd().resolve()
|
|
compose_files = sorted(Path("models").glob("*/*/compose/*/*/*.yml"))
|
|
failures: list[str] = []
|
|
|
|
ENV_DEFAULT = re.compile(r"^\$\{[^}:]+:-(.+)\}$")
|
|
|
|
def host_source(source: str) -> str | None:
|
|
match = ENV_DEFAULT.match(source)
|
|
if match:
|
|
return match.group(1)
|
|
if source.startswith("${"):
|
|
return None
|
|
if source.startswith("/") or source.startswith("~"):
|
|
return None
|
|
if not source.startswith("."):
|
|
return None
|
|
return source
|
|
|
|
def split_volume(value: str) -> tuple[str, str | None]:
|
|
# Compose short-form host paths in this repo do not contain ':' except as
|
|
# the source/target separator. Preserve the first two fields and ignore ro/rw.
|
|
parts = value.split(":")
|
|
if len(parts) < 2:
|
|
return value, None
|
|
return parts[0], parts[1]
|
|
|
|
def check(cond: bool, msg: str) -> None:
|
|
if cond:
|
|
print(f"PASS: {msg}")
|
|
else:
|
|
print(f"FAIL: {msg}")
|
|
failures.append(msg)
|
|
|
|
for compose in compose_files:
|
|
data = yaml.safe_load(compose.read_text()) or {}
|
|
base = compose.parent.resolve()
|
|
services = data.get("services", {}) or {}
|
|
for service_name, service in sorted(services.items()):
|
|
for volume in service.get("volumes", []) or []:
|
|
if isinstance(volume, str):
|
|
raw_source, target = split_volume(volume)
|
|
source = host_source(raw_source)
|
|
elif isinstance(volume, dict):
|
|
raw_source = str(volume.get("source", ""))
|
|
target = str(volume.get("target", ""))
|
|
source = host_source(raw_source)
|
|
else:
|
|
continue
|
|
if source is None:
|
|
continue
|
|
raw_path = base / source
|
|
lexical = Path(os.path.abspath(raw_path))
|
|
resolved = raw_path.resolve()
|
|
label = f"{compose}:{service_name}:{target or raw_source}"
|
|
check(str(lexical).startswith(str(root)), f"{label}: resolves inside repo")
|
|
if "models-cache" in resolved.parts:
|
|
check(resolved == root / "models-cache" or root / "models-cache" in resolved.parents, f"{label}: models-cache points at repo root")
|
|
elif "cache" in resolved.parts:
|
|
cache_idx = resolved.parts.index("cache")
|
|
cache_root = Path(*resolved.parts[:cache_idx + 1])
|
|
check(cache_root.exists(), f"{label}: cache root exists")
|
|
elif "patches" in resolved.parts and "genesis" in resolved.parts:
|
|
patches_idx = resolved.parts.index("patches")
|
|
patches_root = Path(*resolved.parts[:patches_idx + 1])
|
|
check(patches_root.exists(), f"{label}: patches root exists")
|
|
else:
|
|
check(resolved.exists(), f"{label}: source exists")
|
|
|
|
if failures:
|
|
raise SystemExit(f"{len(failures)} mount-resolution checks failed")
|
|
PY
|
|
|
|
echo "test-compose-mounts-resolve: ok"
|