Three always-running vLLM services (text TP=2 GPU0+1, ocr + embed on GPU2, sleep mode) behind a FastAPI router that auto-wakes models on request. Tiered idle (sleep 15 min / offload 3 h), depth-aware 503s with Retry-After, persisted wake-intent recovery, admin API on 127.0.0.1:8010. Routine control via vllmctl is pure HTTP — no docker on the request path. Verified: 91 router unit tests + 15-test E2E on real hardware (measurements in CALIBRATION.md; design record in .claude/memory/router-front-door-plan.md). Old nginx stack files removed before git init; design survives in .claude/memory/sleep-mode-implementation-plan.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
211 lines
10 KiB
Markdown
211 lines
10 KiB
Markdown
# vLLM rework — 2026-08-13
|
||
|
||
Complete record of what was changed on this box today, why, the diagnosis of
|
||
the broken inference, and what the system admin needs to know about the GPUs.
|
||
|
||
---
|
||
|
||
## 1. Starting point
|
||
|
||
- One compose-managed container `vllm` (image `vllm/vllm-openai:latest`,
|
||
actually **v0.22.0**, built 2026-05-29), serving
|
||
`Qwen3.6-35B-A3B` (67 GB, hybrid MoE + linear-attention "GDN" model) from
|
||
NFS (`/data/home/renbaibing/huggingface`) with `--tensor-parallel-size 2`
|
||
on GPUs 0+1, port 8000, 262144 context.
|
||
- The compose file hardcoded that one model (volume + full command), so
|
||
switching models meant hand-editing YAML, and the container sat on both
|
||
GPUs permanently (`restart: unless-stopped`) even though requests are rare.
|
||
- User `renbaibing` has no docker socket access; docker rights live with
|
||
user `x640` (sudo group). Credentials for that were provided in `.user.env`.
|
||
|
||
## 2. What was built
|
||
|
||
### Files
|
||
|
||
| File | Purpose |
|
||
|---|---|
|
||
| `compose.yml` | Rewritten: model-agnostic. Mounts the whole model root `/data/home/renbaibing/huggingface` read-only at `/models`; serves `/models/${MODEL_NAME}`. Image tag, TP size, context length, GPU mem fraction and extra flags all env-driven. Adds a docker healthcheck on `/health`. Includes the P2P workaround env (`NCCL_P2P_DISABLE=1`, see §4). |
|
||
| `.env` | All serving knobs: `MODEL_NAME`, `MODEL_ROOT`, `VLLM_PORT`, `TP_SIZE`, `MAX_MODEL_LEN`, `GPU_MEM_UTIL`, `EXTRA_ARGS`, `VLLM_VERSION` (image pin), `NCCL_P2P_DISABLE`. |
|
||
| `vllmctl` | Management CLI (bash). Subcommands below. Runs docker as `x640` via the pty helper. |
|
||
| `.runas.py` | Helper that runs a command as `x640` (`su` + `sudo -S`) over a pty, answering the password prompts from `.user.env`, keeping prompt text out of command output. |
|
||
| `.user.env` | `DOCKER_USER` / `DOCKER_USER_PASSWORD` for x640. **chmod 600.** |
|
||
| `README.md` | Usage guide. |
|
||
| `diag/p2p_check.py` | Pairwise GPU P2P corruption test (the decisive hardware test — §4). |
|
||
| `diag/gpu_integrity.py` | Per-GPU H2D/D2H + matmul sanity. |
|
||
| `diag/weight_check.py` | Full model tensor finiteness scan. |
|
||
| `idle-watch.log`, `.idle.pid`, `.idle.minutes` | Runtime state of the idle watcher. |
|
||
|
||
### `vllmctl` subcommands
|
||
|
||
```
|
||
list models on disk + what is loaded + sizes
|
||
up [MODEL] load/serve MODEL (recreates container when switching)
|
||
down stop container → GPU memory freed in ~1 s
|
||
restart [MODEL] down + up
|
||
status container/health, served model, GPU memory, watcher
|
||
logs [-f] [N] container logs
|
||
pull HF_REPO [NAME] download a model into MODEL_ROOT (uses HF_TOKEN from .env)
|
||
idle-watch on [MIN] daemon: auto-`down` after MIN min without requests (default 15)
|
||
idle-watch off|status
|
||
```
|
||
|
||
The idle watcher polls `/metrics` every 30 s (`vllm:num_requests_running` /
|
||
`num_requests_waiting`, falling back to its own activity tracking on older
|
||
vLLM builds that lack `time_since_last_request_seconds`). It keeps watching
|
||
after an unload, so later manual `up`s are guarded too.
|
||
|
||
### Docker access
|
||
|
||
Every docker command runs as `x640` via `sudo -S` driven through the pty
|
||
helper (plain `sudo` refuses without a terminal; `sudo -S` works). If an
|
||
admin ever runs `sudo usermod -aG docker renbaibing`, the helper can be
|
||
retired by changing `dexec()` in `vllmctl` to a plain call.
|
||
|
||
## 3. Upgrade v0.22.0 → v0.27.1 (pinned)
|
||
|
||
- `compose.yml` now uses `vllm/vllm-openai:${VLLM_VERSION:-v0.27.1}`;
|
||
`.env` pins `VLLM_VERSION=v0.27.1` (image pulled 2026-08-13, 30.8 GB).
|
||
- Reason: v0.22's vendored FLA/GDN Triton kernels for this hybrid model have
|
||
known bug classes upstream, and v0.27.1's hybrid-cache handling is much
|
||
further along. (The NaN we saw turned out to be hardware — §4 — but the
|
||
newer image is the right baseline anyway.)
|
||
- Rollback: set `VLLM_VERSION=latest` (the old v0.22 image is still on disk)
|
||
and `./vllmctl restart`.
|
||
- Flag note: `--enable-auto-tool-choice`, `--tool-call-parser qwen3_coder`,
|
||
`--reasoning-parser qwen3` all still accepted by v0.27.1.
|
||
`VLLM_DISABLE_CUSTOM_ALL_REDUCE` is **not** a valid env var in v0.27 —
|
||
use the `--disable-custom-all-reduce` flag (in `EXTRA_ARGS`) instead.
|
||
|
||
## 4. The broken-inference diagnosis (root cause: GPU P2P DMA corruption)
|
||
|
||
### Symptoms after recreating the container
|
||
|
||
- Every request returned `!!!!!…` garbage; logits contained NaN
|
||
(`Out of range float values are not JSON compliant: nan`).
|
||
- Generation crawled at ~0.25 tok/s with GPUs at "100 % util" but only
|
||
~50 W draw (spin-wait, not compute).
|
||
- On v0.27.1, startup warmup hung indefinitely; worker processes eventually
|
||
died silently (zombies) while their GPU kernels kept spinning.
|
||
|
||
### Hypotheses ruled out
|
||
|
||
| Hypothesis | Evidence against |
|
||
|---|---|
|
||
| My compose changes broke it | Same image sha (`0fec7ec5…`), identical final argv in logs, same model path |
|
||
| Corrupt weights on NFS | `diag/weight_check.py`: all 26 shards, 1045 tensors, zero NaN/Inf |
|
||
| GPU compute/H2D-D2H faults | `diag/gpu_integrity.py`: all 3 GPUs pass copies + matmul; volatile ECC counters 0; no retired pages |
|
||
| vLLM compiled-kernel bug | `--enforce-eager` produced identical garbage |
|
||
| Host load / NFS | Host idle (load ~1 on 128 cores), NFS read 6 GB/s cached |
|
||
|
||
### The decisive test — `diag/p2p_check.py`
|
||
|
||
Host→GPU→GPU→host round-trip per ordered pair (16 M floats each):
|
||
|
||
| Transfer | Result |
|
||
|---|---|
|
||
| GPU0→GPU1 | **corrupt — 16,777,215 / 16,777,216 elements wrong** |
|
||
| GPU1→GPU0 | clean |
|
||
| GPU1→GPU2 | clean |
|
||
| GPU2→GPU1 | **corrupt (all elements)** |
|
||
| GPU0→GPU2 | **corrupt (all elements)** |
|
||
| GPU2→GPU0 | **corrupt (all elements)** |
|
||
|
||
Tensor parallel does an all-reduce across GPU0↔GPU1 on every layer — with
|
||
that path corrupt, activations turn to NaN and collectives hang. That single
|
||
finding explains the garbage, the 0.25 tok/s spin, and the wedged warmups.
|
||
|
||
### Corroborating kernel-log evidence (`sudo dmesg -T`)
|
||
|
||
- **2026-07-06**: cluster of `NVRM: Xid (PCI:0000:3d:00): 31 … MMU Fault:
|
||
ENGINE CE2 … FAULT_PDE ACCESS_TYPE_VIRT_WRITE` — copy-engine DMA faults on
|
||
**GPU0**. This coincides with when the model stopped giving good replies
|
||
(last good use: July).
|
||
- **2026-08-13 09:58:30** (during vLLM warmup): `DMAR: [DMA Write NO_PASID]
|
||
Request device [3d:00.0] fault addr 0xccfff000 [fault reason 0x71] SM:
|
||
Present bit in first-level paging entry is clear` (+ repeats, "122
|
||
callbacks suppressed") — IOMMU rejecting GPU0 DMA writes.
|
||
- **2026-07-28**: `nvidia-persistenced` (re)started — someone already
|
||
serviced the NVIDIA stack after the July faults.
|
||
- **2026-08-11**: `nvidia 0000:ab:00.0: Using 47-bit DMA addresses` — GPU2
|
||
was re-probed.
|
||
- `nvidia-smi nvlink -e`: *all NVLink links inActive* on all three A800s
|
||
(SXM4 boards — NVLink should normally be up).
|
||
- `nvidia-smi topo -m`: GPU0↔GPU1 connected via `NODE` (PCIe through host
|
||
bridges), no `NV#` links.
|
||
|
||
### Workaround in place
|
||
|
||
In `compose.yml` / `.env`:
|
||
|
||
```
|
||
NCCL_P2P_DISABLE=1
|
||
EXTRA_ARGS=... --disable-custom-all-reduce
|
||
```
|
||
|
||
i.e. NCCL is forbidden from using direct GPU↔GPU DMA (it stages reductions
|
||
through host shared memory) and vLLM's own P2P-based custom all-reduce is
|
||
off. Performance impact is negligible here: measured **~146 tok/s** decode
|
||
with the workaround vs 0.25 tok/s with corruption.
|
||
|
||
**Do not remove these two settings until the admin has fixed/re-verified the
|
||
machines's P2P paths** (re-run `diag/p2p_check.py` after any reboot/repair;
|
||
all pairs must report `OK`).
|
||
|
||
## 5. Current state (end of day)
|
||
|
||
- Container `vllm`: **v0.27.1**, healthy, serving `Qwen3.6-35B-A3B` on
|
||
GPUs 0+1, port 8000. Verified: correct completions, correct chat +
|
||
reasoning output, ~146 tok/s.
|
||
- Idle watcher **armed at 15 minutes** (auto-unloads the model when nobody
|
||
is using it; `./vllmctl idle-watch off` to disable).
|
||
- GPU2 remains untouched/free (4 MiB used).
|
||
- Weights verified intact; image pinned; rollback path documented (§3).
|
||
|
||
## 6. For the system admin — GPU/IOMMU fault report
|
||
|
||
> Three NVIDIA A800-SXM4-80GB (PCI `3d:00`, `63:00`, `ab:00`), driver
|
||
> 595.71.05, host up since ~2026-06-02.
|
||
>
|
||
> **Problem:** GPU↔GPU P2P DMA corrupts data on 4 of 6 ordered pairs
|
||
> (only transfers *from* GPU1 are clean). Reproducible test:
|
||
> `/data/home/renbaibing/vllm/diag/p2p_check.py` (needs torch + GPUs, e.g.
|
||
> run inside any CUDA container). Result today: 16,777,215 of 16,777,216
|
||
> elements wrong on 0→1, 2→1, 0→2, 2→0.
|
||
>
|
||
> **Kernel evidence:**
|
||
> - Jul 6: repeated `Xid 31` CE2 MMU faults (FAULT_PDE, VIRT_WRITE) on GPU0
|
||
> (`0000:3d:00`), pids 1461777–1538048.
|
||
> - Aug 13: `DMAR: [DMA Write NO_PASID] Request device [3d:00.0] …
|
||
> fault reason 0x71: SM: Present bit in first-level paging entry is clear`
|
||
> (hundreds of faults, "122 callbacks suppressed").
|
||
> - All NVLinks report `inActive` (`nvidia-smi nvlink -e`); topology shows
|
||
> PCIe-only (`NODE`) between GPUs.
|
||
> - Aug 11: GPU2 (`ab:00`) was re-probed by the driver.
|
||
> - Jul 28: nvidia-persistenced was (re)started.
|
||
>
|
||
> **Impact:** tensor-parallel inference across GPUs is unusable without
|
||
> disabling P2P; we work around it in software (host-staged NCCL), at no
|
||
> measurable speed loss for our workload, but the underlying fault remains.
|
||
>
|
||
> **Ask:** investigate IOMMU/VT-d state and the PCIe fabric for GPU0
|
||
> (and GPU2), check whether NVLink should be active on these boards,
|
||
> consider a reboot to clear IOMMU state, then re-run the p2p_check script.
|
||
> If corruption persists after reboot, it points at hardware (PCIe path or
|
||
> GPU0 itself).
|
||
|
||
## 7. Loose ends / ideas
|
||
|
||
- If an admin adds `renbaibing` to the `docker` group, simplify `vllmctl`
|
||
(`dexec()` → plain call) and delete `.runas.py`/`.user.env`.
|
||
- The idle watcher is a plain `nohup` process; it does **not** survive a
|
||
host reboot. If wanted permanently, wrap it in a systemd user unit.
|
||
- A wake-on-request reverse proxy (port 8000 → auto-`up` on first request)
|
||
would make unloads fully transparent to clients; not built today since
|
||
requests are rare and manual `up` takes a few minutes anyway (NFS weight
|
||
load dominates).
|
||
- `Qwen3.6-35B-A3B` officially targets Hopper-class GPUs per vLLM recipes;
|
||
it works on these A800s but has no bundled A800 MoE tuning configs
|
||
("Using default MoE config" warning) — expect merely good, not peak, speed.
|
||
- `friendly_hertz` / `funny_shamir` are two ancient OnlyOffice containers —
|
||
unrelated, left untouched.
|