Žiadny popis

Krasimir Kazakov 8ed9b64020 initial files 5 dní pred
.gitignore 8ed9b64020 initial files 5 dní pred
README.md 8ed9b64020 initial files 5 dní pred
benchmark.sh 8ed9b64020 initial files 5 dní pred
docker-compose.yaml 8ed9b64020 initial files 5 dní pred
download-model.sh 8ed9b64020 initial files 5 dní pred
proxy-passthrough.py 8ed9b64020 initial files 5 dní pred

README.md

vLLM backend — Huihui Qwen3.8-27B-abliterated (AWQ W4A16)

Self-contained vLLM version of the parent coding-stack-huihui-qwen3.8-27b project. Instead of the Q8 GGUF it serves the pre-quantized INT4 checkpoint shawnw3i/Huihui-Qwen3.8-27B-abliterated-AWQ-MTP (auto-round AWQ, group_size 64, 18.65 GiB on disk) as the model foundation, behind the same OpenAI-compatible proxy that injects a SearXNG web_search tool.

This is the fast backend: vLLM's paged KV cache, chunked prefill and Marlin INT4 kernels are what make the 250k-token context practical on one 48 GB card. It is also the least flexible one — one sequence at a time, no vision, no FP8 on this GPU.

Deliberately not included: the llama.cpp and Ollama compose files, the LibreChat/MongoDB/Meilisearch stack, GGUF/mmproj handling, and the parent's shared container names/API key.

                   ┌─────────────── web_search ──────────────► SearXNG :8889 (external)
                   │
Client ──► :11433 proxy (Flask) ──► :18080 vLLM (OpenAI API)
                   ▲
                   └── :18080 is also published directly, bypassing the proxy

Containers: coding-stack-vllm (vllm/vllm-openai:nightly) and coding-stack-proxy (python:3.11-slim). Port 11433 and the proxy container name clash with the parent and Ollama stacks — stop those first if you switch back and forth.

Files

File Purpose
docker-compose.yaml vLLM server + web_search proxy (the whole stack)
proxy-passthrough.py OpenAI facade on :11433; injects and executes web_search
download-model.sh Pulls the AWQ checkpoint into the HF cache vLLM reads
benchmark.sh TTFT + decode tok/s from a streaming request
.gitignore Keeps caches and weights out of git

There is no Modelfile and no import step: vLLM loads the HF checkpoint directly, and the chat template comes from the checkpoint's chat_template.jinja.

Requirements

  • Docker + Docker Compose v2, NVIDIA driver + Container Toolkit
  • One whole 48 GB GPU (RTX A6000 here): 18.65 GiB weights + ~15.5 GiB KV at 253952 context + CUDA graphs/activations, at --gpu-memory-utilization 0.93
  • Compute capability ≥ 8.9 only for FP8 — the A6000 is 8.6, so weights and KV stay FP16 and INT4 comes from AWQ + Marlin kernels. Don't add --kv-cache-dtype fp8 (or an FP8 checkpoint) on this card: it won't run.
  • ~62 GB RAM is comfortable; vLLM reported 55 GiB available while loading 18.65 GiB
  • Image: vllm/vllm-openai:nightly (~29 GB, 0.27.2rc1 at the time of writing, 2 months old in this host's cache). It is a moving tag — re-pull deliberately.
  • Optional: SearXNG at host.docker.internal:8889 for web_search

The checkpoint is already in this host's cache: /storage-models/vllm/hub/models--shawnw3i--Huihui-Qwen3.8-27B-abliterated-AWQ-MTP (7 shards + model_extra_tensors.safetensors, size-verified in the parent project). download-model.sh is a no-op when it is there.

Quick start

cd vllm
./download-model.sh          # ~19 GB; no-op if the HF cache already has it
docker compose up -d         # first start: load + compile + CUDA graph capture (minutes)
docker compose ps            # wait for (healthy) — start_period is 900 s
./benchmark.sh               # TTFT + tok/s

Cold start is slow by design: vLLM compiles the model's kernels (torch.compile, Triton GDN kernels) and captures CUDA graphs before it reports healthy. Expect several minutes on first boot; subsequent boots reuse the compile cache in the image's /root/.cache only if you persist it (this stack does not — add a volume if you care).

Verify

# Direct to vLLM (OpenAI-compatible)
curl http://localhost:18080/v1/models
curl http://localhost:18080/v1/chat/completions -H "Authorization: Bearer x" \
  -d '{"model":"foundation","messages":[{"role":"user","content":"Hello"}],"max_tokens":64}'

# Through the proxy (+ web_search injected)
curl http://localhost:11433/v1/chat/completions -H "Authorization: Bearer x" \
  -d '{"model":"foundation","messages":[{"role":"user","content":"Hello"}],"max_tokens":64}'

# Streaming (proxy fast-path: SSE passed straight through)
curl -N http://localhost:11433/v1/chat/completions -H "Authorization: Bearer x" \
  -d '{"model":"foundation","messages":[{"role":"user","content":"Hello"}],"stream":true,"max_tokens":64}'

curl http://localhost:18080/health

vLLM is started without --api-key, so any Authorization value works (or none). Add --api-key and BACKEND_API_KEY to the proxy if the published port must be closed.

Endpoints

Port Service Notes
11433 proxy (Flask) OpenAI /v1/*; injects web_search; everything else is transparent
18080 vLLM OpenAI /v1/*, /health, /metrics; no auth

Model name: foundation (--served-model-name), regardless of the HF repo name.

Configuration

vLLM flags (docker-compose.yaml)

Flag Value Why
--max-model-len 253952 fits 18.65 GiB weights + ~15.5 GiB f16 KV on 48 GB; native max is 262144
--gpu-memory-utilization 0.93 leaves ~3 GiB for the desktop/other processes
--max-num-seqs 1 one 250k sequence consumes the whole KV pool
--max-num-batched-tokens 16384 chunked prefill granularity for long prompts
--enable-chunked-prefill / --enable-prefix-caching on keeps a 250k-token agent prompt responsive and cached across turns
--reasoning-parser qwen3 splits the reasoning trace out of content
--tool-call-parser qwen3_coder required for tool calls (the proxy's web_search)
--enable-auto-tool-choice on without it vLLM rejects requests that carry tools
--language-model-only on skips the vision tower (see Limitations)
--dtype float16 the checkpoint's dtype; bf16 would double nothing but adds no benefit here
--trust-remote-code on auto-round checkpoint ships custom config

The VLLM_* environment variables in the compose file are the parent project's tuning for this exact card (Marlin atomic add, FlashInfer sampler, graph estimation, spawn workers); they are not required for correctness.

Changing context length

KV is f16 on the 16 full-attention layers only (the 48 GDN layers keep a small recurrent state), so 64 KiB/token:

Context KV (f16) Weights Approx. total
32k ~2.0 GiB 18.65 GiB ~22 GiB
128k ~8.2 GiB 18.65 GiB ~29 GiB
253952 (default) ~15.5 GiB 18.65 GiB ~37 GiB
262144 (native max) ~16 GiB 18.65 GiB ~38 GiB

Lower --max-model-len (and drop VLLM_ALLOW_LONG_MAX_MODEL_LEN if you go below the model's 262144) if you need to share the GPU. Going up is not possible without a second card or a smaller/quantized KV cache — and FP8 KV needs CC ≥ 8.9.

Thinking / reasoning

--reasoning-parser qwen3 moves the trace out of content. The model keeps Qwen3.8's default reasoning level, so it thinks by default and reasoning tokens count against max_tokens — give it 2048–4096 for real questions. Verified on this build (vLLM 0.27.2rc1): the trace arrives in the message's reasoning field (streaming: delta.reasoning), the answer in content, and the split is reported in usage.completion_tokens_details.reasoning_tokens. (Older vLLM releases used reasoning_content; the proxy re-streams either.)

Thinking controls pass through chat_template_kwargs (verified):

# no reasoning at all
curl localhost:18080/v1/chat/completions -H 'Content-Type: application/json' \
  -d '{"model":"foundation","messages":[{"role":"user","content":"Hi"}],
       "chat_template_kwargs":{"enable_thinking":false}}'

# shorter trace (named level from the Qwen3.8 template)
  ... "chat_template_kwargs":{"reasoning_effort":"low"}

With default settings a 300- and even a 1000-token reply came back as pure reasoning (finish_reason: "length", empty content) — this checkpoint thinks a lot. Set a high max_tokens or lower the effort.

The proxy does not disable thinking and does not touch the reasoning field — it passes completions through unchanged.

Tools and web_search

--enable-auto-tool-choice --tool-call-parser qwen3_coder is what makes the proxy's injected web_search tool usable. Verified: a get_weather request came back as tool_calls: [{"function":{"name":"get_weather","arguments":"{\"city\": \"Sofia\"}"}}] with finish_reason: "tool_calls". Behaviour is the same as the other backends:

  • Non-streaming through :11433: the proxy runs the search loop server-side (MAX_SEARCH_ITER, default 10) against SearXNG and returns only the final answer. The client's own tool calls are returned untouched, unless the model mixes them into a search turn (then they are stubbed — inherited edge case).
  • Streaming: passed through natively; if the model calls web_search, the client gets the tool_call and must execute it.
  • SEARXNG_URL / SEARXNG_RESULTS tune the backend (defaults http://host.docker.internal:8889, 5). extra_hosts is what resolves that hostname.
  • When SearXNG returns nothing, this checkpoint keeps retrying with new queries and the loop ends with an explicit error instead of an answer — verified during testing (SearXNG had all engines rate-limited). The error now reports the counts: Exceeded max search iterations (2 searches, 2 empty) — SearXNG returned no usable results. Lower MAX_SEARCH_ITER (env on the proxy) if you prefer to fail fast.

Pointing a client at it

base_url = http://localhost:11433/v1   # or http://localhost:18080/v1 to skip web_search
model    = foundation
api_key  = anything

Use the proxy for coding agents so web_search works, and set max_tokens high enough for the reasoning trace. Direct vLLM (:18080) is also OpenAI-compatible, including native json_schema structured output — the vLLM build does not need the grammar stripping that llama.cpp did (dropped from this proxy).

Limitations

  • No vision. --language-model-only drops the visual tower and vLLM logs running in text-only mode. Remove the flag and lower --max-model-len if you need images; the processor configs are in the checkpoint.
  • One sequence at a time. --max-num-seqs 1: a second concurrent request queues.
  • No FP8. A6000 is compute capability 8.6; FP8 weights and FP8 KV need 8.9+.
  • No auth out of the box (--api-key omitted).
  • Nightly image. vllm/vllm-openai:nightly moves; v0.18.0 (also cached on this host) predates qwen3_5 support. Pin the digest if you need reproducibility.
  • INT4 is below the Q8 GGUF in fidelity. AWQ keeps mtp, lm_head, the visual merger and some linear_attn.in_proj_a/b tensors in fp16, but the bulk of the weights are 4-bit; the llama.cpp/Ollama stacks with UD-DW-Q8_K_L are the near-lossless option.

Performance

Measured with ./benchmark.sh on this RTX A6000 with the GPU otherwise idle (only ~0.3 GiB of desktop usage), default settings, 253952 context:

Metric Value
decode (300 tokens) 38.5 tok/s
decode (1000 tokens) 38.3 tok/s
TTFT (92-token prompt, warm) 0.10 s
prefill (9,062-token prompt) ~1.8k tok/s (5.9 s wall, including 32 decoded tokens)
cold start → healthy ~4 min (06:52:28 → 06:56:41, kernels compiled + graphs captured)
VRAM at steady state 42.9 GiB: 17.7 GiB weights/non-torch, 2.74 GiB peak activations, 0.04 GiB CUDA graphs, 23.6 GiB KV pool

Comparison: llama.cpp with the Q8 GGUF measures 9.95 tok/s on the same card, so vLLM INT4 is ~3.9× the decode rate (the parent project hoped for 2–3×). The trade is fidelity: 4-bit AWQ vs near-lossless Q8.

The KV pool is 379,388 tokens (65 KiB/token f16) — --max-model-len 253952 only uses ~67% of it, so the context could be raised to the native 262144; the rest is headroom for one long prompt, not a second sequence (--max-num-seqs 1).

Two implementation details that affect speed on this card (from the vLLM log):

  • Using MarlinLinearKernel for AutoAWQMarlinLinearMethod — the INT4 path is Marlin.
  • Falling back to the Triton GDN decode path — the fused CUDA GDN kernel requires a BF16 model with K=V=128; this FP16 checkpoint uses the Triton kernel instead.

Troubleshooting

Problem Fix
ValueError: No available memory for the cache blocks / OOM at startup lower --max-model-len (e.g. 131072) or --gpu-memory-utilization; free the GPU first (nvidia-smi)
Container stuck in health: starting it is compiling/capturing graphs — watch docker compose logs -f vllm; allow up to 15 min on first boot
model ... not found / 404 ./download-model.sh; confirm HF_HOME=/models and the cache path under /storage-models/vllm/hub
AWQ kernel / quant errors try --quantization compressed-tensors, or add --enforce-eager (slower but skips CUDA graphs)
Requests queue behind each other expected with --max-num-seqs 1; raise it only if you lower --max-model-len
Tool calls rejected / not parsed the parsers are mandatory: --enable-auto-tool-choice --tool-call-parser qwen3_coder
reasoning empty check --reasoning-parser qwen3 is present; it is what extracts the trace
Empty content, finish_reason: "length" thinking ate the budget — raise max_tokens or pass chat_template_kwargs: {"enable_thinking": false}
Exceeded max search iterations (N searches, M empty) SearXNG returned nothing; check its engines, or lower MAX_SEARCH_ITER on the proxy
Proxy returns 502 docker compose logs vllm — usually a failed load; also confirm BACKEND_URL=http://vllm:8000
web_search → Search failed SearXNG on host.docker.internal:8889 is down (external to this stack)
Startup warning about multimodal limits harmless: it is the text-only mode you asked for

Reset

docker compose down                      # keeps the HF cache and the image
# nuclear: delete the checkpoint cache (19 GB) and re-download with ./download-model.sh
sudo rm -rf /storage-models/vllm/hub/models--shawnw3i--Huihui-Qwen3.8-27B-abliterated-AWQ-MTP