Every fv-ml1 link on the Homepage dashboard was broken. Measured against the
live dashboard API before the fix: 16 entries pointing at the dead 10.250.50.54
and zero at the live 10.251.50.54, covering gen, M.O.G.-SEC, Scriberr, Embed,
Rerank, Reward, Coder, Dockge and six dormant seats.
The miss was structural, not careless. fv-ml1-rename-sweep.sh works from an
allowlist assembled from files that mention the HOST, and a homepage.href label
mentions only an IP -- so every stack whose sole stale reference was a label
fell outside it. The allowlist now covers those 24 files, and records how to
derive the list next time (grep the old address, subtract history) rather than
enumerating from memory.
History is still untouched, and the exclusions are now written down with the
reason each one keeps the old address: recorded benchmark results, whose
base_url is part of a measurement's provenance; the one LiteLLM comment
preserving a retired hand-test endpoint; and the cutover runbooks, where the old
address is the subject matter.
Two bugs found while applying it, both fixed here:
- deploy-stack.sh rejected any stack name containing a dot, so qwen3.5-122b,
qwopus3.5-122b and mistral-medium-3.5 could not be deployed by the script at
all. The check exists to stop path traversal, which means rejecting ".." and
"/" -- not every dot. Traversal is now rejected explicitly and tested.
- stacks/scriberr/.env.example allowed CORS only from the dead IP and from
scriberr.ana.internal, which no longer resolves; the box is at the fv site
and DNS already carries scriberr.fv.internal. The live .env had both stale
origins, i.e. an allowlist with nothing reachable in it.
Host side, applied separately: canonical pushed for the 16 stacks whose only
difference from the host was this renumber, and an in-place address-only fix for
the nine whose host copy has genuinely drifted or has no canonical copy, so that
drift survives for a deliberate reconciliation instead of being clobbered. Every
compose.yaml on fv-ml1 now reads 10.251.50.54. The labels themselves only take
effect at container creation, so the running containers still need recreating.
129 lines
5.8 KiB
YAML
129 lines
5.8 KiB
YAML
# qwen36-vl — Qwen3.6-35B-A3B vision-language MoE (official NVFP4) on fv-ml1.
|
|
#
|
|
# Replaces the qwen35-vl stack (Qwen3.5-9B) 2026-06-14. Co-located on GPU 1 with
|
|
# the granite summarizer + embed/rerank/reward trio. Serves on :8007.
|
|
#
|
|
# WHY NVFP4 now (swapped FROM FP8 2026-06-15): the nvidia ModelOpt NVFP4 MoE that
|
|
# was BROKEN on vLLM 0.19.1/0.22.0 (#44081, lm_head.input_scale) loads clean on
|
|
# 0.23.0 — the ModelOpt lm_head fix landed. So we cut FP8→NVFP4: ~20.4 GiB weights
|
|
# vs FP8's ~34 GiB (~40% lighter, ~13 GB reclaimed on GPU 1), faster single-stream
|
|
# on Blackwell's FP4 tensor cores, and the freed room funds fp16 KV + a granite
|
|
# context restore (see the rebalance note below). The NVFP4 checkpoint preserves
|
|
# the vision tower (ModelOpt leaves it high-precision) — VALIDATED by comfy-dev's
|
|
# real anatomy-judge A/B on 16 prod images: PASS, holds the load-bearing
|
|
# discrimination (gross-deformity reject + clean-pass), only shuffles already-
|
|
# unreliable sub-ceiling borderline-hand calls. brokkr's text/speed arm: parity
|
|
# except a minor multi-step chained-numeric-reasoning slip (W4A4 tell) — doesn't
|
|
# bite the vision-judge role; flag for any gateway consumer doing chained math.
|
|
# Requires vLLM >= 0.23.0 (pinned by digest in .env). NO --quantization flag
|
|
# (vLLM auto-detects the checkpoint's NVFP4).
|
|
#
|
|
# NAMING: served ONLY as its TRUE name `qwen3.6-35b-a3b`. A model is never aliased
|
|
# under a prior model's name — a caller asking for `qwen3.5-9b-fp8` (a 9B dense)
|
|
# must NOT be silently handed this 35B-A3B MoE; that's a downstream-confusion
|
|
# footgun. The legacy `qwen3.5-9b-fp8` name is RETIRED. Consumers (Arbo's vision
|
|
# hero-judge, stacks/arbo v0.11.3+) migrate to `qwen3.6-35b-a3b` — they 404 on the
|
|
# old name until they repoint, which is the correct loud signal (notified 2026-06-14).
|
|
#
|
|
# GPU-1 REBALANCE (2026-06-15, pinned): the NVFP4 swap freed ~13 GB, redistributed —
|
|
# qwen36 NVFP4: util 0.46→0.32 (~31 GB: 20.4 GB weights + fp16 KV + graph).
|
|
# fp16 KV (we DROPPED --kv-cache-dtype fp8) — the freed room buys back full-
|
|
# precision KV; hybrid attn (10/40 full-attn) keeps even fp16 KV affordable.
|
|
# granite: RESTORED 0.24→0.34, max-len 65536→131072 (gives back the context
|
|
# sacrificed for FP8 qwen — the FP8-vs-maxed-granite tradeoff is now undone).
|
|
# trio (embed/rerank/reward) unchanged at floor.
|
|
# Total GPU-1 util ~0.82 → ~17 GB headroom (was a tight ~5 GB).
|
|
#
|
|
# THINKING TOGGLE: this is ONE hybrid checkpoint (not separate Instruct/Thinking
|
|
# downloads) with a Qwen3-style per-request `enable_thinking` switch. The chat
|
|
# template defaults thinking ON (`<think>\n`); passing
|
|
# `chat_template_kwargs={"enable_thinking":false}` emits the empty
|
|
# `<think>\n\n</think>\n\n` block (no reasoning). We run --reasoning-parser qwen3
|
|
# (model-matched — its vLLM docstring describes THIS checkpoint) so ONE endpoint
|
|
# serves BOTH modes cleanly: thinking-ON splits <think>…</think> into
|
|
# reasoning_content; thinking-OFF routes everything to content. The gateway
|
|
# selects the mode per model_name (stacks/litellm/conf/config.yaml):
|
|
# qwen3.6-35b-a3b → enable_thinking:false (non-thinking DEFAULT)
|
|
# qwen3.6-35b-a3b-thinking → enable_thinking:true (opt-in reasoning)
|
|
#
|
|
# All tunables live in .env — edit that, not this file.
|
|
|
|
name: qwen36-vl
|
|
|
|
services:
|
|
vllm-qwen36:
|
|
image: ${QWEN_IMAGE}
|
|
container_name: ${QWEN_CONTAINER_NAME}
|
|
restart: unless-stopped
|
|
ipc: host
|
|
ports:
|
|
- "${QWEN_PORT}:8000"
|
|
volumes:
|
|
- /tank/aimodels/huggingface:/hfcache
|
|
environment:
|
|
- HF_HOME=/hfcache
|
|
- HF_HUB_CACHE=/hfcache/hub
|
|
- HUGGING_FACE_HUB_TOKEN=${HF_TOKEN:-}
|
|
- VLLM_API_KEY=${API_KEY:-}
|
|
command:
|
|
- ${QWEN_MODEL}
|
|
# Pre-quantized NVFP4 (ModelOpt) checkpoint → NO --quantization (vLLM auto-
|
|
# detects; the vision tower is left high-precision by the producer).
|
|
- --served-model-name
|
|
- qwen3.6-35b-a3b
|
|
- --host
|
|
- 0.0.0.0
|
|
- --port
|
|
- "8000"
|
|
- --gpu-memory-utilization
|
|
- ${QWEN_GPU_MEM_UTIL}
|
|
- --max-model-len
|
|
- ${QWEN_MAX_MODEL_LEN}
|
|
# Cap concurrency: vLLM warms the sampler with max_num_seqs dummy requests,
|
|
# and this model's 248K vocab makes that warmup tensor huge — the default
|
|
# 1024 OOMs on a shared GPU even though weights+KV fit. 32 is ample for a
|
|
# vision endpoint (the summarizer carries the concurrency, not this).
|
|
- --max-num-seqs
|
|
- ${QWEN_MAX_NUM_SEQS}
|
|
# fp16 KV (no --kv-cache-dtype): the NVFP4 swap freed enough room to run
|
|
# full-precision KV — better than the fp8 KV the FP8 build needed to fit.
|
|
- --trust-remote-code
|
|
- --dtype
|
|
- auto
|
|
- --enable-prefix-caching
|
|
# Model-matched reasoning parser for the hybrid thinking toggle (see header).
|
|
# Splits <think>…</think> into reasoning_content when thinking is ON; routes
|
|
# all output to content when the empty think-block signals thinking OFF — so
|
|
# this single :8007 endpoint serves both the non-thinking default and the
|
|
# qwen3.6-35b-a3b-thinking gateway variant.
|
|
- --reasoning-parser
|
|
- qwen3
|
|
deploy:
|
|
resources:
|
|
reservations:
|
|
devices:
|
|
- driver: nvidia
|
|
device_ids:
|
|
- "${QWEN_GPU_ID}"
|
|
capabilities:
|
|
- gpu
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 300s
|
|
networks:
|
|
- tnet
|
|
labels:
|
|
- homepage.group=AI Systems
|
|
- homepage.name=Qwen3.6-35B-A3B VL (NVFP4)
|
|
- homepage.icon=mdi-image-search
|
|
- homepage.description=Qwen3.6-35B-A3B vision-language MoE (NVFP4) via vLLM (fv-ml1)
|
|
- homepage.href=http://10.251.50.54:${QWEN_PORT}/docs
|
|
|
|
networks:
|
|
tnet:
|
|
name: traefik-net
|
|
external: true
|