Files
vllm-frontdoor/NOTES-2026-08-13.md
bing 80eef4ce6a Initial commit: router front-door vLLM stack
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>
2026-08-17 10:17:42 +00:00

211 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 14617771538048.
> - 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.