Files
dots/fl/.config/vllm/README.md
T

207 lines
14 KiB
Markdown
Raw 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 on fl (docker, ROCm) — side by side with the llama.cpp router, never at the same time
Added 2026-10-21 to answer one question: **how does vLLM compare with llama.cpp on this card, on the
same models?** It is a bench rig, not a second production endpoint. Nothing here was run on fl yet —
fl has no sshd, so every command below is for you to run there; the first run will find the rough edges.
- **Box**: the same fl as the router — RX 7600 XT 16 GB (gfx1102, RDNA3), 5600X, 48 GB RAM. The card
drives the display. llama.cpp runs the Arch Vulkan build; vLLM needs ROCm, which comes **inside the
docker image** — nothing ROCm is installed on the host.
- **Files**: `vllm.ini` (presets, same `key = --flag` convention as `../llamacpp/config.ini`),
`vllmctl.py` (start/stop with the VRAM guards), `bench-vs.py` (one table for both engines).
- **Port 11348**. Not in the router, not in any client list (opencode/pi/aichat/OWUI) on purpose.
## ⚠ The one rule: one engine resident at a time
vLLM pre-allocates `gpu-memory-utilization` × 16 GiB at startup and keeps it, idle or not. The router
loads models lazily on request. So while vLLM is up, **any client request to :11343** makes the router
load a model on top of it → overcommit → GTT spill → frozen PC (the same failure `--models-max 1`
guards against). Unloading resident models is not enough; the clients on the LAN are the problem.
`vllmctl.py up` therefore refuses while `llama.service` is active. The sequence is always:
```fish
~/.config/vllm/vllmctl.py up <preset> --stop-router # sudo systemctl stop llama.service, then start vLLM
# … use / bench vLLM …
~/.config/vllm/vllmctl.py down # stop container, wait for VRAM to drain
sudo systemctl start llama.service # router back for the LAN
```
The other order is the only one that should fail safely: vLLM compares its requested budget against
free VRAM at startup and aborts when a router model is resident ("free memory … is less than desired
GPU memory utilization" — reported by users, not something verified on this card). Everything else freezes.
## What "same models" means here, and the size surprise
llama.cpp runs GGUF; vLLM runs Hugging Face safetensors (bf16 / AWQ / GPTQ / compressed-tensors).
The GGUF files cannot be loaded natively, so the pairs below are **the same model at a vLLM-native
quant**, not the same files (same-file GGUF loading is phase 2, see the end).
Sizes from the HF API, 2026-10-21 — the "4-bit" vLLM checkpoints are **10–11.5 GB** because embeddings,
lm_head and the vision tower stay bf16. They are bigger than the Q6/Q8 GGUFs they pair with, so on
16 GB they are **clean-desktop only**, at `gpu-memory-utilization` ≈ 0.78 and 16k context:
| pair | llama.cpp preset (ledger) | vLLM preset (`vllm.ini`) | vLLM weights | fits? |
|---|---|---|---|---|
| smoke | — | `Qwen3-0.6B-smoke` = `Qwen/Qwen3-0.6B` | 1.5 GB | yes — proves docker+GPU+image |
| gemma-4-E4B | `gemma-4-E4B-it-UD-Q8_K_XL` — 57 t/s **with MTP**, 96k ctx, ~7 GB | ~~`google/gemma-4-E4B-it-qat-w4a16-ct`~~ | 11.5 GB | **✗ no** (2026-10-21): vLLM needs ~16 GiB before any KV — the bf16 per-layer embedding table (262144 × 42 × 256 × 2 B = 5.6 GB) is held twice. Tombstone in the ini. |
| Qwen3.5-9B | `Qwen3.5-9B-UD-Q6_K_XL` — 32 t/s **with MTP**, 32k ctx, 9 GB | `Qwen3.5-9B-w4a16` = `RedHatAI/Qwen3.5-9B-quantized.w4a16` | 11.0 GB | the last viable pair: 0.75 on a 2.1 GB desktop, 0.78 clean; prefill batch 512 |
**Cannot be compared on this card at all**: every 30B+ MoE (Coder-30B, GLM-4.7-Flash, Qwen3.6-35B,
Ornith, gemma-4-26B), Coder-Next 80B and Qwen3.8-27B — they only fit llama.cpp through `n-cpu-moe`
expert offload into RAM, which vLLM does not have (its MoE CPU-offload RFC died stale; the only
option is whole-weight `cpu_offload_gb`, which is slow). gpt-oss-20b (MXFP4, ~13 GB) is marginal and
its RDNA3 kernel status is unknown — not in the ini.
**Why the "docker should just work" expectation does not hold here**: of the three first-day failures,
one was the image (no gfx1102 kernels → the override, solved) and two were memory. vLLM keeps
everything in VRAM — bf16 embeddings, fp32 logits during profiling, CUDA-graph memory — on a card that
also drives the desktop. No image changes a model's memory need; the ROCm images on Docker Hub are the
right ones.
So the comparison is **not apples to apples** and the table says so: llama.cpp rows run Q8/Q6 with a
speculative MTP draft at a bigger context; vLLM rows run int4 without speculation at 16k. Read it as
"what each engine delivers on this card for this model", not as a kernel shoot-out.
## Setup on fl (once)
```fish
sudo pacman -S docker # ~20 GB free disk for the ROCm image + 25 GB for two models
sudo systemctl enable --now docker
sudo usermod -aG docker anon # then log out / in (or `newgrp docker`)
cd ~/.dots && ./install.sh # links ~/.config/vllm/* (new files are not live until re-stow)
~/.config/vllm/vllmctl.py pull # vllm/vllm-openai-rocm:v0.32.0 (ROCm 10.0; 11.6 GB download, ~25 GB on disk)
~/.config/vllm/vllmctl.py gpu-check # torch sees the card? prints device name + gfx arch
```
`gpu-check` must print `cuda available: True`, a `gfx1100` device (that is the override at work) and
`torch kernel: ok`. **The override is required** (found 2026-10-21): `v0.32.0` carries no gfx1102 code
objects, and a kernel launch without one aborts the process *silently* — the engine core vanishes, the
API server only says `Failed core proc(s): {}`, and even the `print`s before the abort are lost to
stdout buffering. `[global] env.HSA_OVERRIDE_GFX_VERSION = 11.0.0` presents the card as gfx1100 (RX 7900),
whose RDNA3 code runs on this chip. Nothing else on the host changes.
Then the smoke test (tiny model, 30 % of the card, proves the whole path before any big download —
✓ passed 2026-10-21: 154.9 t/s decode, 1462 t/s prefill, TTFT 0.02 s, 7.7 GB used incl. a 2.9 GB desktop):
```fish
~/.config/vllm/vllmctl.py up Qwen3-0.6B-smoke --stop-router
curl -s localhost:11348/v1/chat/completions -H 'content-type: application/json' \
-d '{"model":"Qwen3-0.6B-smoke","messages":[{"role":"user","content":"hi"}],"max_tokens":20}'
~/.config/vllm/vllmctl.py down; sudo systemctl start llama.service
```
First start of any model compiles Triton kernels: minutes, with `vllmctl.py up` echoing the log tail
every 30 s.
**Tags and updates.** `<tag>` is the part after the colon: `latest` = newest stable release (was
`v0.32.0` on 2026-10-18, ROCm 10.0), `nightly` = today's main branch (18 GB), `latest-rocm72` /
`nightly-rocm72` = the same on the older ROCm 7.2 stack (smaller, 12 GB), `…-base` = without vLLM
itself. The ini pins `v0.32.0` so a re-pull cannot silently change the engine under a bench run. To
update: pick the new `vX.Y.Z` on hub.docker.com/r/vllm/vllm-openai-rocm/tags, put it in `[global] image`,
`vllmctl.py pull`, then `vllmctl.py down` / `up` — the container is recreated from the image on every
`up`. Afterwards `docker image prune -f` drops the superseded layers (`docker images` shows what is kept).
`HF_TOKEN` is only needed for gated repos; none of the three presets is gated. If one ever is,
`hf auth login` on fl writes `~/.cache/huggingface/token`, which the mounted cache hands to the container.
## Running the benchmark
Two phases, because the engines cannot be resident together. Do both on a **clean desktop** (close the
browser; `vramx` must show ≤ ~1.5 GB used) — the vLLM presets leave ~2 GB free at 0.78 and an evening
desktop does not fit.
```fish
# phase 1 — vLLM (router stopped by --stop-router; first run also downloads the model)
~/.config/vllm/vllmctl.py up gemma-4-E4B-it-w4a16 --stop-router
~/.config/vllm/bench-vs.py -t vllm:gemma-4-E4B-it-w4a16 --ctx 8000 --concurrency 4
~/.config/vllm/vllmctl.py down
~/.config/vllm/vllmctl.py up Qwen3.5-9B-w4a16 # router is still stopped
~/.config/vllm/bench-vs.py -t vllm:Qwen3.5-9B-w4a16 --ctx 8000 --concurrency 4
~/.config/vllm/vllmctl.py down
# phase 2 — llama.cpp (vLLM down, router back)
sudo systemctl start llama.service
~/.config/vllm/bench-vs.py -t llamacpp:gemma-4-E4B-it-UD-Q8_K_XL -t llamacpp:Qwen3.5-9B-UD-Q6_K_XL --ctx 8000 --concurrency 4
```
Every row lands in `bench-vs-history.md` (append) and the latest run in `bench-vs-results.md`, same
folder. The guards: a `llamacpp` target is skipped while vLLM answers `/health`; a `vllm` target is
skipped while the router has a model loaded or `llama.service` is active (`--force` overrides both).
**How to read it**
- `decode t/s` is usage tokens over wall time after the first token, measured the same way on both
engines; llama.cpp's own `timings` number is in parentheses so rows match `../llamacpp/bench-results.md`.
- Thinking is **off** on both (`chat_template_kwargs.enable_thinking=false`) and a reasoning delta counts
as first token, so TTFT compares the engines, not the preamble lengths. `--think` reverts that.
- `spec` shows the MTP draft on the llama.cpp side; vLLM rows have none unless the preset sets
`speculative-config`. Expect the gemma/Qwen llama.cpp rows to carry a 1.5–2× draft bonus.
- `agg t/s @N` is continuous batching's home turf — and llama-server still won it (85 vs 61 at 4
streams): its default slot count on b11146 is evidently > 1, so no `parallel` key is needed.
- `--ctx 8000` adds decode at an 8k prompt — the number that matters for agent loops.
## Results
**2026-10-21, Qwen3.5-9B, same evening, same script, thinking off, 256 tok, 0.6 GB desktop:**
| engine · checkpoint | spec | decode t/s | prefill t/s | TTFT s | decode @8k prompt | 4 streams agg t/s | VRAM used |
|---|---|---|---|---|---|---|---|
| vLLM · RedHatAI w4a16 @0.80, 16k, batch 512 | — | 29.4 | **181** | **0.17** | 16.0 | 60.6 | 12.5 GB |
| llama.cpp · UD-Q6_K_XL, 32k | MTP draft | **61.3** | 114 | 0.27 | **51.1** | **85.4** | 10.9 GB |
What it says, for this card:
- **Decode**: llama.cpp wins 2× single-stream and 3× at an 8k prompt. Most of the 2× is the MTP draft
(the ledger's pre-MTP Q8 number was 17 t/s in July; a no-draft Q6 today is probably ~30, i.e. parity
with vLLM). The 8k gap is not MTP: vLLM's decode halves with context here (16.0 vs 29.4) while
llama.cpp loses 17 % — on RDNA3 vLLM's attention path is the Triton generic one, llama.cpp's is a
tuned Vulkan kernel.
- **Prefill**: vLLM +59 % even with its batch capped at 512 for memory. This is its real strength on
the box, and it shows in TTFT.
- **Concurrency**: the column that was supposed to be vLLM's — it batched 4 streams to 2× its single
(60.6), but llama-server served the same 4 at 85.4. Its default slot count is evidently > 1 on this
build, so the README's "llama.cpp serializes" caveat does not hold; it does not need a `parallel` key.
- **Memory**: vLLM holds 1.6 GB more for a model with 2 GB less context, and only runs on a ≤ 0.6 GB
desktop. 0.75 was 0.11 GiB short of KV room; 0.80 is the setting.
**Verdict**: no vLLM model earns a roster slot on fl; the bench rig stays, the router stays, llama-swap
is not needed. The one follow-up that could move the decode column is MTP in vLLM (the knob is in
the ini) — but it cannot touch the memory column or the MoE roster, so it is curiosity, not a decision.
## When it does not start
- **`No available memory for the cache blocks`** (seen 2026-10-21, gemma @0.73/8k on a 2.1 GB desktop):
the model loaded, but budget − weights − activation reservations left nothing for KV. What vLLM
reserved is in the log: `docker logs vllm 2>&1 | grep -iE 'profil|kv cache|weights take|peak'`.
Cheapest fixes first: text-only (`limit-mm-per-prompt` all zero — skips the image/audio encoder
warmup, now in both presets), smaller `max-num-batched-tokens` (2048, now in both presets), lower
`max-num-seqs`; then a cleaner desktop so the projection allows 0.78–0.82; last resort the smaller
GPTQ checkpoint. `--set key=val` tries any of these without editing the ini.
- **`ABORT: projected free …`**: the desktop is using more than the preset assumes. Close GPU apps or
take the suggested utilization it prints. `--force` exists; the freeze is real.
- **compressed-tensors / "no kernel" for int4 on ROCm**: both real presets use compressed-tensors
pack-quantized. If this image lacks a ROCm kernel for it, swap in the GPTQ alternative noted in the
ini (`Vishva007/gemma-4-E4B-it-W4A16-AutoRound-GPTQ`, 10.0 GB) — different kernel path.
- **Hangs / garbage output**: RDNA3 attention backend. Set `env.VLLM_ATTENTION_BACKEND = TRITON_ATTN`
explicitly; option names moved between versions — `vllmctl.py exec vllm serve --help` shows this image's.
- **GTT growing during a run** (`vramx`, the GTT line): the budget is leaking into system RAM — stop,
lower the utilization. Do not wait for the desktop to stutter.
- **Root-owned files in `~/.cache/huggingface`**: the container runs as root, so its downloads land
root-owned in anon's HF cache. Harmless for the rig; `sudo chown -R anon: ~/.cache/huggingface` if
the host-side `hf` tooling complains.
- **Container died**: `docker logs vllm` (the container is kept after a crash; `vllmctl.py up` shows
the tail and removes it on the next start).
## Phase 2 — if the numbers justify more
1. **Same-file GGUF in vLLM**: GGUF loading moved to an out-of-tree `vllm-gguf-plugin`; it is
"highly experimental and under-optimized" per vLLM's docs and needs a custom image layer
(`pip install vllm-gguf-plugin`), plus `--tokenizer <base repo>`. Untested on ROCm. It would make the
weights identical — and show vLLM's unoptimized path, so interpret carefully.
2. **MTP in vLLM**: `RedHatAI/Qwen3.5-9B-quantized.w4a16` ships `model_mtp.safetensors`; the
commented `speculative-config` in the ini is the knob (syntax per this image's `--help`).
3. **Permanent coexistence**: replace the router's `--models-preset` mode with llama-swap (AUR
`llama-swap-bin`), which spawns llama-server *or* the vLLM container per model id, one at a time,
with a `ttl` idle unload. Same host/port/model ids for every client. Only worth it if a vLLM model
earns a place in the daily roster.