vLLM publishes an OFFICIAL `vllm/vllm-openai:gemma` image (pushed 2026-06-10, a stock build of the dgemma branch commit 74b5964f) with DiffusionGemma baked in — `DiffusionGemmaForBlockDiffusion` registers natively, transformers 5.10.2. So we pin that image (BY DIGEST, purge-resistant) and drop the bespoke sideload from PR #358 (stock nightly + 123-file branch overlay + install_script). 3 fixes are NOT upstream (vLLM tests H100/B200 + TP=1) so they're not in :gemma — the compose now bind-mounts them (site_package_overlay) from the new lean dir models/diffusiongemma-26b-a4b/vllm/patches/gemma-image-fixes/: - marlin.py + marlin_utils_fp8.py — sm_86 fp8 Marlin sub-tile-K pad. :gemma clean dies in warmup ("Invalid thread config ... num_bits=8 ... max_shared_mem=101376", K=352/1056) without it. - diffusion_gemma.py — TP-vocab soft-embed + dtype fix (TP=2; their recipe is TP=1). Changes: - base.yml: image -> :gemma@sha256:9c719fc0...; default `vllm serve` entrypoint + 3 file mounts (was: overlay-dir mount + install_script bash entrypoint). Status 🧪 experimental (was upstream-gated; supersedes PR #359 too). - engine vllm-diffusion-gemma: install.spec -> :gemma@digest; vendored_overlays -> the 3 fixes (delivery site_package_overlay). - registry status -> experimental; note rewritten. - patches.yml: dgemma-gemma-image-fixes (site_package_overlay, 3 overlay_files). - diagnose_profile_cli OVERLAY_PATH_HINTS + docs/UPSTREAM.md #45163 row. - DELETE the 123-file dgemma-overlay/ + the regeneration Dockerfile. Validated live on 2x RTX 3090 (2026-06-11): :gemma clean dies on the Marlin wall; :gemma + 3 mounts (via `docker compose -f base.yml up`) boots, serves coherent output, 262K, ~177/180 TPS typical / ~1100 peak, 23.1 GB/card. verify-full: gen + tool-call + reasoning + output-quality pass (streaming-SSE "1 chunk" is the expected block-diffusion artifact). Full gate green (test-compose-registry-disk local-only red = untracked nex-n2-mini WIP; CI-clean validated: disk 46 / reg 45). Co-Authored-By: Claude Opus 4.8 <[email protected]>
483 lines
17 KiB
Python
483 lines
17 KiB
Python
#!/usr/bin/env python3
|
|
"""Per-compose and free-form profile diagnostics for club-3090."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
if str(REPO_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
|
|
os.environ.setdefault("CLUB3090_LOG_LEVEL", "ERROR")
|
|
|
|
from scripts.lib.profiles.compat import ( # noqa: E402
|
|
FitsResult,
|
|
ProfileError,
|
|
calibration_status,
|
|
fits,
|
|
from_compose_name,
|
|
load_profiles,
|
|
to_compose_name,
|
|
)
|
|
from scripts.lib.profiles.compose_registry import COMPOSE_REGISTRY # noqa: E402
|
|
from scripts.lib.profiles.launch_compat import resolve_engine_pin # noqa: E402
|
|
|
|
|
|
class DiagnoseProfileError(Exception):
|
|
"""User-facing diagnose-profile failure."""
|
|
|
|
|
|
COMPOSE_ALIASES = {
|
|
"gemma-dual-int8": "vllm/gemma-int8-mtp",
|
|
"gemma-dual-int8-262k": "vllm/gemma-int8-mtp",
|
|
"gemma-dual": "vllm/gemma-bf16-mtp",
|
|
}
|
|
|
|
OVERLAY_PATH_HINTS = {
|
|
"vllm-pr40361-marlin-pad": ["models/qwen3.6-27b/vllm/patches/vllm-marlin-pad"],
|
|
"vllm-pr35936-qwen3coder-tool-parser": [
|
|
"models/qwen3.6-27b/vllm/patches/vllm-pr35936-required-fallback"
|
|
],
|
|
"vllm-pr40391-rebased": [
|
|
"models/gemma-4-31b/vllm/patches/vllm-pr40391-v0.22.0"
|
|
],
|
|
"vllm-gemma4-tool-parser-fixes": [
|
|
"models/gemma-4-31b/vllm/patches/vllm-pr42006-v0.22.0"
|
|
],
|
|
"vllm-pr41800-truncate-prompt-tokens": [
|
|
"models/gemma-4-31b/vllm/patches/vllm-pr41800-truncate-prompt-tokens",
|
|
"models/qwen3.6-27b/vllm/patches/vllm-pr41800-truncate-prompt-tokens",
|
|
],
|
|
"dgemma-gemma-image-fixes": [
|
|
"models/diffusiongemma-26b-a4b/vllm/patches/gemma-image-fixes"
|
|
],
|
|
}
|
|
|
|
|
|
def marker(ok: bool) -> str:
|
|
return "✓" if ok else "✗"
|
|
|
|
|
|
def print_available_composes() -> None:
|
|
print(" available composes:")
|
|
for name in sorted(COMPOSE_REGISTRY):
|
|
print(f" - {name}")
|
|
if COMPOSE_ALIASES:
|
|
print(" aliases:")
|
|
for alias, target in sorted(COMPOSE_ALIASES.items()):
|
|
print(f" - {alias} -> {target}")
|
|
|
|
|
|
def normalize_compose_name(name: str) -> str:
|
|
return COMPOSE_ALIASES.get(name, name)
|
|
|
|
|
|
def setup_genesis_pin() -> str | None:
|
|
path = REPO_ROOT / "scripts/setup.sh"
|
|
if not path.exists():
|
|
return None
|
|
match = re.search(r'^GENESIS_PIN="\$\{GENESIS_PIN:-(.+?)\}"', path.read_text(encoding="utf-8"), re.M)
|
|
return match.group(1) if match else None
|
|
|
|
|
|
def hardware_for_entry(entry: dict[str, Any], profiles) -> tuple[list, bool, str]:
|
|
world_size = int(entry["tp"]) * int(entry.get("pp", 1))
|
|
required_sm = float(entry.get("required_sm") or profiles.engines[entry["engine"]].min_sm)
|
|
requires_nvlink = bool(entry.get("requires_nvlink", False))
|
|
|
|
if required_sm >= 9.0:
|
|
hardware_id = "rtx-5090"
|
|
else:
|
|
hardware_id = "rtx-3090"
|
|
if world_size <= 0:
|
|
world_size = 1
|
|
hardware = [profiles.hardware[hardware_id] for _ in range(world_size)]
|
|
if requires_nvlink and world_size == 2:
|
|
scenario = f"{world_size}x-{hardware_id}-nvlink"
|
|
nvlink_active = True
|
|
else:
|
|
scenario = f"{world_size}x-{hardware_id}-pcie" if world_size > 1 else f"1x-{hardware_id}"
|
|
nvlink_active = False
|
|
return hardware, nvlink_active, scenario
|
|
|
|
|
|
def hardware_for_combo(args: argparse.Namespace, profiles) -> tuple[list, bool, str]:
|
|
count = max(int(args.tp) * int(args.pp), 1)
|
|
hardware_id = args.hardware
|
|
hardware = [profiles.hardware[hardware_id] for _ in range(count)]
|
|
scenario = f"{count}x-{hardware_id}-pcie" if count > 1 else f"1x-{hardware_id}"
|
|
return hardware, bool(args.nvlink_active), scenario
|
|
|
|
|
|
def entry_objects(entry: dict[str, Any], profiles):
|
|
drafter = profiles.drafters[entry["drafter"]] if entry.get("drafter") else None
|
|
return (
|
|
profiles.models[entry["model"]],
|
|
profiles.workloads[entry["workload"]],
|
|
profiles.engines[entry["engine"]],
|
|
drafter,
|
|
)
|
|
|
|
|
|
def fits_for_entry(entry: dict[str, Any], profiles, hardware: list, nvlink_active: bool, *, project_vram: bool) -> FitsResult:
|
|
model, workload, engine, drafter = entry_objects(entry, profiles)
|
|
return fits(
|
|
hardware=hardware,
|
|
model=model,
|
|
workload=workload,
|
|
engine=engine,
|
|
drafter=drafter,
|
|
tp=int(entry["tp"]),
|
|
pp=int(entry.get("pp", 1)),
|
|
kv_format=entry["kv_format"],
|
|
max_ctx=int(entry["max_ctx"]),
|
|
max_num_seqs=int(entry["max_num_seqs"]),
|
|
mem_util=entry.get("mem_util"),
|
|
weights_variant=entry["weights_variant"],
|
|
nvlink_active=nvlink_active,
|
|
requires_nvlink=bool(entry.get("requires_nvlink", False)),
|
|
required_engine_features=list(entry.get("required_engine_features", [])),
|
|
required_sm=entry.get("required_sm"),
|
|
project_vram=project_vram,
|
|
)
|
|
|
|
|
|
def fits_for_combo(args: argparse.Namespace, profiles, hardware: list, nvlink_active: bool, *, project_vram: bool) -> FitsResult:
|
|
model = profiles.models[args.model]
|
|
workload = profiles.workloads[args.workload]
|
|
engine = profiles.engines[args.engine]
|
|
drafter = profiles.drafters[args.drafter] if args.drafter else None
|
|
return fits(
|
|
hardware=hardware,
|
|
model=model,
|
|
workload=workload,
|
|
engine=engine,
|
|
drafter=drafter,
|
|
tp=args.tp,
|
|
pp=args.pp,
|
|
kv_format=args.kv_format,
|
|
max_ctx=args.max_ctx,
|
|
max_num_seqs=args.max_num_seqs,
|
|
mem_util=args.mem_util,
|
|
weights_variant=args.weights_variant,
|
|
nvlink_active=nvlink_active,
|
|
required_engine_features=list(args.required_feature or []),
|
|
project_vram=project_vram,
|
|
)
|
|
|
|
|
|
def constraints_line(result: FitsResult) -> str:
|
|
diag = result.diagnostics
|
|
passed = len(diag.get("constraints_passed", []))
|
|
failed = len(diag.get("constraints_failed", []))
|
|
skipped = len(diag.get("constraints_skipped", []))
|
|
total = passed + failed + skipped
|
|
return f"valid={str(result.valid).lower()}; constraints passed: {passed}/{total}; elapsed {diag.get('elapsed_ms')} ms"
|
|
|
|
|
|
def print_reasons(result: FitsResult) -> None:
|
|
for reason in result.reasons:
|
|
print(f" - {reason}")
|
|
|
|
|
|
def print_notes(result: FitsResult) -> None:
|
|
for note in result.notes:
|
|
print(f" note: {note}")
|
|
|
|
|
|
def print_kv_projection(result: FitsResult) -> bool:
|
|
kv = result.kv_projection
|
|
if not kv:
|
|
skipped = result.diagnostics.get("constraints_skipped", [])
|
|
if "C12" in skipped:
|
|
c12_notes = [note for note in result.notes if "KV projection" in note]
|
|
note = c12_notes[-1] if c12_notes else "KV projection skipped"
|
|
print(f" ⊘ {note}")
|
|
return True
|
|
print(" ⊘ no kv-calc projection available")
|
|
return True
|
|
verdict = kv.get("verdict", "UNKNOWN")
|
|
total = kv.get("total_gb")
|
|
budget = kv.get("budget_gb")
|
|
pct = kv.get("pct_of_vram")
|
|
if verdict == "FAIL":
|
|
print(f" ⊘ predicted total {total} GB/card ({pct}% budget), verdict FAIL")
|
|
return False
|
|
print(f" ✓ predicted total {total} GB/card ({pct}% budget), verdict {verdict}; budget {budget} GB")
|
|
return True
|
|
|
|
|
|
def newest_calibration_summary(profiles, model_id: str) -> str | None:
|
|
cal = profiles.calibration.get(model_id)
|
|
if not cal:
|
|
return None
|
|
rows = [row for row in cal.rows if row.get("status") == "active"]
|
|
if not rows:
|
|
return None
|
|
rows.sort(key=lambda row: str(row.get("date", "")), reverse=True)
|
|
row = rows[0]
|
|
date = row.get("date", "unknown-date")
|
|
compose = row.get("compose", "unknown-compose")
|
|
return f"most recent calibration row {date} ({compose})"
|
|
|
|
|
|
def print_calibration(profiles, compose_name: str | None, entry: dict[str, Any] | None, hardware: list) -> None:
|
|
if not compose_name or not entry:
|
|
print(" ⊘ free-form combo; no exact compose calibration row")
|
|
return
|
|
status, row = calibration_status(profiles, compose_name, hardware, max_ctx=entry.get("max_ctx"))
|
|
if row:
|
|
print(f" ✓ {status}; {row.get('source', 'calibration row present')}")
|
|
return
|
|
engine_pin = entry.get("engine", "unknown-engine")
|
|
summary = newest_calibration_summary(profiles, entry["model"])
|
|
if summary:
|
|
print(f" ⊘ engine_pin={engine_pin}; no exact row for this scenario; {summary}")
|
|
print(" action: re-bench when convenient, or accept extrapolation")
|
|
else:
|
|
print(f" ⊘ engine_pin={engine_pin}; no calibration rows for model {entry['model']}")
|
|
|
|
|
|
def overlay_exists(overlay_id: str) -> tuple[bool, str | None]:
|
|
for rel in OVERLAY_PATH_HINTS.get(overlay_id, []):
|
|
path = REPO_ROOT / rel
|
|
if path.exists():
|
|
return True, rel
|
|
patches_root = REPO_ROOT / "models"
|
|
needle = overlay_id.lower().replace("vllm-", "").replace("pr", "")
|
|
for path in patches_root.glob("*/vllm/patches/*"):
|
|
if needle and needle in path.name.lower():
|
|
return True, str(path.relative_to(REPO_ROOT))
|
|
return False, None
|
|
|
|
|
|
def print_overlays(engine) -> bool:
|
|
ok = True
|
|
required = list(engine.required_overlays or [])
|
|
if required:
|
|
print(f" ⊘ required_overlays: {required}")
|
|
else:
|
|
print(" ✓ required_overlays: []")
|
|
|
|
overlays = list(engine.vendored_overlays or [])
|
|
if not overlays:
|
|
print(" ✓ vendored_overlays: []")
|
|
for overlay in overlays:
|
|
overlay_id = overlay.get("id") if isinstance(overlay, dict) else str(overlay)
|
|
if isinstance(overlay, dict) and overlay.get("image_baked"):
|
|
# Capability provided by the pinned engine image (not a mountable
|
|
# file overlay), so there is no on-disk source to verify.
|
|
print(f" ✓ {overlay_id}: image-baked (provided by pinned engine image)")
|
|
continue
|
|
exists, rel = overlay_exists(overlay_id)
|
|
if exists:
|
|
print(f" ✓ {overlay_id}: {rel}")
|
|
else:
|
|
print(f" ✗ {overlay_id}: source path not found")
|
|
ok = False
|
|
|
|
if engine.required_genesis:
|
|
setup_pin = setup_genesis_pin()
|
|
suffix = f" (setup.sh GENESIS_PIN={setup_pin})" if setup_pin else ""
|
|
print(f" ✓ Genesis pin: {engine.genesis_pin or 'unspecified'}{suffix}")
|
|
else:
|
|
print(" ✓ Genesis: not required")
|
|
return ok
|
|
|
|
|
|
def resolve_free_form_compose(args: argparse.Namespace, profiles, result: FitsResult, nvlink_active: bool) -> str | None:
|
|
if not result.valid:
|
|
return None
|
|
model = profiles.models[args.model]
|
|
workload = profiles.workloads[args.workload]
|
|
engine = profiles.engines[args.engine]
|
|
drafter = profiles.drafters[args.drafter] if args.drafter else None
|
|
weights = result.weights_variant or args.weights_variant or model.default_weight_variant
|
|
return to_compose_name(
|
|
model,
|
|
engine,
|
|
drafter,
|
|
result.recommended_kv_format or args.kv_format,
|
|
args.tp,
|
|
args.pp,
|
|
workload=workload,
|
|
weights_variant=weights,
|
|
nvlink_active=nvlink_active,
|
|
max_ctx=args.max_ctx,
|
|
max_num_seqs=args.max_num_seqs,
|
|
)
|
|
|
|
|
|
def command_diagnose(args: argparse.Namespace) -> int:
|
|
compose_name = normalize_compose_name(args.compose) if args.compose else None
|
|
free_form = compose_name is None
|
|
|
|
try:
|
|
profiles = load_profiles()
|
|
except ProfileError as exc:
|
|
target = args.compose or "free-form combo"
|
|
print(f"Profile triage: {target}")
|
|
print("=" * (16 + len(target)))
|
|
print("[1/6] Compose registry entry exists")
|
|
print(" ⊘ free-form combo" if free_form else " ✓ compose argument provided")
|
|
print("[2/6] Cross-references resolve")
|
|
print(f" ✗ {exc}")
|
|
print("")
|
|
print("Triage summary: RED")
|
|
return 2
|
|
|
|
target = compose_name or "free-form combo"
|
|
print(f"Profile triage: {target}")
|
|
print("=" * (16 + len(target)))
|
|
|
|
entry = None
|
|
hardware = []
|
|
nvlink_active = False
|
|
scenario = "unknown"
|
|
print("[1/6] Compose registry entry exists")
|
|
if compose_name:
|
|
if compose_name not in COMPOSE_REGISTRY:
|
|
print(f" ✗ {compose_name} not found")
|
|
print_available_composes()
|
|
print("")
|
|
print("Triage summary: RED")
|
|
return 3
|
|
entry = COMPOSE_REGISTRY[compose_name]
|
|
print(
|
|
f" ✓ {compose_name} found "
|
|
f"(model={entry['model']}, workload={entry['workload']}, engine={entry['engine']})"
|
|
)
|
|
else:
|
|
missing = [name for name in ("model", "engine") if not getattr(args, name)]
|
|
if missing:
|
|
print(f" ✗ free-form combo missing required flag(s): {', '.join('--' + name for name in missing)}")
|
|
print("")
|
|
print("Triage summary: RED")
|
|
return 3
|
|
print(" ⊘ free-form combo; compose registry lookup skipped")
|
|
|
|
print("")
|
|
print("[2/6] Cross-references resolve")
|
|
try:
|
|
if entry:
|
|
model, workload, engine, drafter = entry_objects(entry, profiles)
|
|
hardware, nvlink_active, scenario = hardware_for_entry(entry, profiles)
|
|
refs = ["model", "workload", "engine"] + (["drafter"] if drafter else [])
|
|
else:
|
|
model = profiles.models[args.model]
|
|
workload = profiles.workloads[args.workload]
|
|
engine = profiles.engines[args.engine]
|
|
drafter = profiles.drafters[args.drafter] if args.drafter else None
|
|
hardware, nvlink_active, scenario = hardware_for_combo(args, profiles)
|
|
refs = ["model", "workload", "engine"] + (["drafter"] if drafter else [])
|
|
print(f" ✓ all referenced profiles exist ({', '.join(refs)}, hardware-default={scenario})")
|
|
except KeyError as exc:
|
|
print(f" ✗ missing referenced profile: {exc}")
|
|
print("")
|
|
print("Triage summary: RED")
|
|
return 2
|
|
|
|
print("")
|
|
print(f"[3/6] fits() on canonical {scenario} scenario")
|
|
if entry:
|
|
fast_result = from_compose_name(compose_name, hardware, nvlink_active, profiles, project_vram=False)
|
|
else:
|
|
fast_result = fits_for_combo(args, profiles, hardware, nvlink_active, project_vram=False)
|
|
print(f" {marker(fast_result.valid)} {constraints_line(fast_result)}")
|
|
print_notes(fast_result)
|
|
if not fast_result.valid:
|
|
print_reasons(fast_result)
|
|
|
|
print("")
|
|
print("[4/6] kv-calc projection")
|
|
if entry:
|
|
full_result = fits_for_entry(entry, profiles, hardware, nvlink_active, project_vram=True)
|
|
else:
|
|
full_result = fits_for_combo(args, profiles, hardware, nvlink_active, project_vram=True)
|
|
kv_ok = print_kv_projection(full_result)
|
|
for note in full_result.notes:
|
|
if note.startswith("kv-calc:"):
|
|
print(f" note: {note}")
|
|
|
|
resolved_compose = compose_name
|
|
if free_form:
|
|
resolved_compose = resolve_free_form_compose(args, profiles, fast_result, nvlink_active)
|
|
if resolved_compose:
|
|
entry = COMPOSE_REGISTRY[resolved_compose]
|
|
|
|
print("")
|
|
print("[5/6] Calibration freshness")
|
|
print_calibration(profiles, resolved_compose, entry, hardware)
|
|
|
|
print("")
|
|
print("[6/6] Vendored overlays applied")
|
|
overlays_ok = print_overlays(engine)
|
|
try:
|
|
if engine.type == "vllm":
|
|
pins = resolve_engine_pin(profiles, engine.id)
|
|
for key, value in pins.items():
|
|
print(f" ✓ {key} resolves: {value}")
|
|
except ProfileError as exc:
|
|
print(f" ✗ {exc}")
|
|
overlays_ok = False
|
|
|
|
print("")
|
|
if not fast_result.valid or not overlays_ok:
|
|
print("Triage summary: RED")
|
|
return 2
|
|
if not kv_ok:
|
|
print("Triage summary: YELLOW")
|
|
return 1
|
|
print("Triage summary: GREEN")
|
|
return 0
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(description="club-3090 per-compose profile diagnostics")
|
|
parser.add_argument("compose", nargs="?", help="Compose registry name, e.g. vllm/dual")
|
|
parser.add_argument("--model")
|
|
parser.add_argument("--workload", default="long-ctx-single")
|
|
parser.add_argument("--engine")
|
|
parser.add_argument("--drafter", default="")
|
|
parser.add_argument("--kv-format", default="fp8_e5m2")
|
|
parser.add_argument("--weights-variant")
|
|
parser.add_argument("--tp", type=int, default=1)
|
|
parser.add_argument("--pp", type=int, default=1)
|
|
parser.add_argument("--max-ctx", type=int)
|
|
parser.add_argument("--max-num-seqs", type=int)
|
|
parser.add_argument("--mem-util", type=float)
|
|
parser.add_argument("--hardware", default="rtx-3090")
|
|
parser.add_argument("--nvlink-active", action="store_true")
|
|
parser.add_argument("--required-feature", action="append")
|
|
return parser
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
raw_argv = list(sys.argv[1:] if argv is None else argv)
|
|
parser = build_parser()
|
|
try:
|
|
args = parser.parse_args(raw_argv)
|
|
except SystemExit as exc:
|
|
return 3 if int(exc.code) != 0 else 0
|
|
if args.compose and any(token.startswith("--") for token in raw_argv):
|
|
print("[diagnose-profile] ERROR: pass either a compose name or free-form flags, not both", file=sys.stderr)
|
|
return 3
|
|
if not args.compose and not (args.model or args.engine):
|
|
parser.print_usage(sys.stderr)
|
|
return 3
|
|
try:
|
|
return command_diagnose(args)
|
|
except DiagnoseProfileError as exc:
|
|
print(f"[diagnose-profile] ERROR: {exc}", file=sys.stderr)
|
|
return 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|