File size: 11,421 Bytes
2941b49 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 | # Recipe
Everything needed to serve, tune, and benchmark this model — in order. Each step
says what to run, what you should see, and what to do when it goes wrong.
**Time:** ~20 minutes to a working server, plus ~1 hour for the full benchmark suite.
---
## What you need
| | Minimum used here |
|---|---|
| GPUs | 2× NVIDIA RTX PRO 6000 Blackwell (96 GB, SM120) |
| VRAM | ~193 GB total across both cards |
| Disk | ~400 GB for the checkpoint |
| Software | vLLM with DeepSeek-V4 sparse-MLA support, CUDA 13.x |
Less VRAM? Lower `--max-model-len` from 262144 — that is the main memory lever,
since the KV dtype cannot be reduced on this architecture (see Step 6).
---
## Step 1 — Serve the model
```bash
vllm serve /path/to/model \
--served-model-name dsv4-hybrid-vision dsv4-hybrid \
--host 127.0.0.1 --port 8000 \
--tensor-parallel-size 2 \
--tokenizer-mode deepseek_v4 \
--kv-cache-dtype fp8_ds_mla \
--block-size 256 \
--max-model-len 262144 \
--max-num-seqs 16 \
--max-num-batched-tokens 8192 \
--gpu-memory-utilization 0.968 \
--speculative-config '{"method":"dspark","model":"/path/to/model","num_speculative_tokens":5}' \
--compilation-config '{"cudagraph_mode":"FULL_DECODE_ONLY","max_cudagraph_capture_size":8}' \
--limit-mm-per-prompt '{"vision_chunk":8}' \
--enable-auto-tool-choice \
--tool-call-parser deepseek_v4 \
--generation-config vllm \
--default-chat-template-kwargs '{"thinking": false, "enable_thinking": false}' \
--override-generation-config '{"max_new_tokens": 1024, "temperature": 0.6, "top_p": 0.95}'
```
**Expect:** ~3 minutes to load. Success looks like:
```
GPU KV cache size: 263,310 tokens
Application startup complete
```
**Check it:**
```bash
curl -s http://127.0.0.1:8000/v1/models | python3 -m json.tool
```
### Why these flags
| Flag | Reason |
|---|---|
| `--tokenizer-mode deepseek_v4` | Required. This checkpoint ships **no** `chat_template`; turn formatting is built in Python by the `deepseek_v4` tokenizer, which never evaluates Jinja. |
| `--kv-cache-dtype fp8_ds_mla` | The only working option on SM120 (Step 6). |
| `--max-num-seqs 16` | 4 capped throughput at 227 tok/s with 3.3 s tail latency; 16 gives 486 tok/s at 1.74 s. KV cache usage stayed under 1%, so the cap was a scheduler limit, not memory. |
| `--override-generation-config` | With `--generation-config vllm`, the checkpoint's own `generation_config.json` is **ignored** and you inherit vLLM's `temperature=1.0`. This sets sane defaults (Step 3). |
| `--default-chat-template-kwargs` | Pins chain-of-thought off so a stray client kwarg cannot enable it server-wide. |
---
## Step 2 — Warm up the JIT kernels
vLLM compiles Triton/TileLang kernels the first time it sees each tensor shape,
**inside whichever request happens to arrive**. Its own log says so:
```
WARNING [jit_monitor] Triton kernel JIT compilation during inference:
_compute_prefill_metadata_kernel. This causes a latency spike;
consider extending warmup to cover this shape/config.
```
Measured: **12 events in the first 14 minutes**, each ~6 s against an 0.086 s
median. Issue one request per shape class right after startup instead:
```bash
#!/usr/bin/env bash
# warmup.sh -- run once after the server is ready
URL=http://127.0.0.1:8000/v1/chat/completions
warm() { # $1 = filler word count, $2 = max_tokens, $3 = label
local filler; filler=$(python3 -c "print('word '*$1)")
curl -s -m 600 "$URL" -H 'Content-Type: application/json' \
-d "$(python3 -c "
import json
print(json.dumps({'model':'dsv4-hybrid-vision','max_tokens':$2,
'messages':[{'role':'user','content':'Reply ok. '+'''$filler'''}]}))")" -o /dev/null
echo " warmed $3"
}
warm 5 16 tiny
warm 100 16 short
warm 800 16 medium
warm 3000 16 long
warm 10000 16 xlong
warm 20 256 decode-deep
```
Also send one 448 px image and one tool-calling request — those are separate code
paths with their own kernels.
**Expect:** ~2 s once the cache is populated; longer the first time. Compiled
kernels persist in vLLM's JIT cache directory, so mount it on durable storage to
keep the benefit across restarts.
**Verify it worked:** every `jit_monitor` line in the server log should fall
inside your warmup window, not during later user traffic.
---
## Step 3 — Fix verbose output
**Symptom:** ask for one bash one-liner, get five variants plus a bullet-point
explainer — 347 tokens for a one-line question.
**Cause, in two parts:**
1. **No system prompt.** The checkpoint ships no chat template, so nothing steers
response length.
2. **Sampling.** `--generation-config vllm` ignores the checkpoint's
`generation_config.json`, leaving `temperature=1.0`, `top_p=1.0`.
**Fix part 1** — sampling, already in the Step 1 command:
```
--override-generation-config '{"max_new_tokens": 1024, "temperature": 0.6, "top_p": 0.95}'
```
**Fix part 2** — a default system prompt. vLLM has **no flag** for this, and the
usual `--chat-template` trick does not apply because `--tokenizer-mode deepseek_v4`
never evaluates Jinja. Send one per request:
```python
SYSTEM = """Be concise and direct. Answer what was asked, then stop.
- Give ONE best answer, not a menu of alternatives. Compare options only if asked.
- No preamble, no restating the question, no closing summary or recap.
- Do not explain self-evident code. Note only genuinely surprising behaviour.
- Prefer a sentence over a paragraph, a paragraph over a list.
- Match length to the question: a one-line question gets a one-line answer."""
messages = [{"role": "system", "content": SYSTEM}] + user_messages
```
For a fleet, put a small proxy in front that injects this when the caller supplies
no system message of their own.
**Result:**
| Prompt | Before | After |
|---|---|---|
| "bash one-liner, 10 largest files under /var/log" | 347 tokens, 5 variants | **29 tokens**, 1 command |
| "What port does SSH use?" | 54 tokens | **3 tokens** (`22.`) |
---
## Step 4 — Cap image resolution at 448 px
**Do this before sending any image.** Above ~450 px the model drifts toward
answering "yes" to everything and invents text that is not in the picture.
```python
from PIL import Image
import base64, io
def encode_image(path, max_side=448):
im = Image.open(path).convert("RGB")
im.thumbnail((max_side, max_side)) # preserves aspect ratio
buf = io.BytesIO(); im.save(buf, "PNG")
return base64.b64encode(buf.getvalue()).decode()
```
**Measured effect** (balanced yes/no questions, equal true-yes and true-no):
| Image | Full resolution | Capped at 448 px |
|---|---|---|
| Desert landscape | 5/7 | **7/7** |
| Mountain at sunset | 5/7 | **6/7** |
It also cuts prompt tokens **12×** — a 4736×2656 photo drops from ~3,100 to ~260.
---
## Step 5 — Benchmark it
Install the harness:
```bash
pip install lm-eval==0.4.12 transformers langdetect immutabledict
```
### Multiple choice (loglikelihood — needs `/v1/completions`)
```bash
lm_eval --model local-completions \
--model_args "model=dsv4-hybrid,base_url=http://127.0.0.1:8000/v1/completions,\
num_concurrent=16,max_retries=3,tokenized_requests=False,\
tokenizer=/path/to/model,tokenizer_backend=huggingface" \
--tasks arc_challenge,hellaswag,winogrande,piqa,openbookqa,truthfulqa_mc2 \
--limit 300 --batch_size 16 --output_path eval_mc
```
### Generative (chat endpoint)
```bash
lm_eval --model local-chat-completions \
--model_args "model=dsv4-hybrid,base_url=http://127.0.0.1:8000/v1/chat/completions,\
num_concurrent=16,max_retries=3,tokenized_requests=False,\
tokenizer=/path/to/model,tokenizer_backend=huggingface" \
--tasks gsm8k,ifeval --limit 200 --apply_chat_template --output_path eval_gen
```
### Code — use the custom script
```bash
python3 benchmarks/humaneval_chat.py
```
**Do not trust `lm_eval --tasks humaneval` on a chat endpoint.** It reports
**0.0** because the task expects a raw completion while the model returns prose
plus a ```` ```python ```` fence. The script above extracts the fenced code and
runs the official tests: **78.0% pass@1**.
### Gotchas
| Error | Fix |
|---|---|
| `got multiple values for keyword argument 'batch_size'` | `batch_size` is a CLI flag, not a `--model_args` key. |
| `No module named 'transformers'` | Install into the **same** environment as `lm_eval`. |
| `dsv4-hybrid is not a local folder...` | Pass `tokenizer=/path/to/model` — it defaults to resolving the served name on the Hub. |
| `No module named 'langdetect'` | `pip install langdetect immutabledict` (IFEval). |
| HumanEval scores 0.0 | Expected on a chat endpoint. Use `humaneval_chat.py`. |
---
## Step 6 — Do not bother with nvfp4 KV cache
`nvfp4_ds_mla` would cut the KV record from 656 to 432 B/token (~1.52× more
cache). Both the vLLM CLI and the b12x backend advertise it. **It does not work on
SM120** — the engine fails at load:
```
AssertionError: DeepseekV4 fp8_ds_mla layout only supports fp8 kv-cache,
got nvfp4_ds_mla
```
Three independent blockers, each verified:
| Layer | Blocker |
|---|---|
| vLLM Python | Every DSv4 attention class reachable on SM120 sets `use_fp8_ds_mla_layout = True`, which asserts `dtype.startswith("fp8")` |
| FlashInfer API | Only `trtllm_batch_decode_sparse_mla_dsv4` exists; no nvfp4 variant |
| CUDA kernel | Prebuilt cubin, documented as **584 B/token**, BF16 or FP8 E4M3 only |
Patching only the Python assertion would feed 432-byte records to a kernel reading
584-byte records: **silent numerical corruption**, not a clean error. Do not do it.
Of the 17 dtypes the CLI advertises, six are accepted (`fp8`, `fp8_ds_mla`,
`fp8_e4m3`, `fp8_e5m2`, `fp8_inc`, `fp8_per_token_head`) and **all six resolve to
the same 656 B record** — none changes capacity. This needs upstream support.
---
## Step 7 — Verify before you ship
Quick checks that catch the common regressions:
```bash
# 1. Terse output (expect ~3 tokens: "22.")
curl -s http://127.0.0.1:8000/v1/chat/completions -H 'Content-Type: application/json' \
-d '{"model":"dsv4-hybrid","messages":[{"role":"user","content":"What port does SSH use?"}]}' \
| python3 -c "import json,sys;d=json.load(sys.stdin);print(d['usage']['completion_tokens'],'tok:',d['choices'][0]['message']['content'])"
# 2. Tool calling (expect finish_reason: tool_calls)
curl -s http://127.0.0.1:8000/v1/chat/completions -H 'Content-Type: application/json' -d '{
"model":"dsv4-hybrid","messages":[{"role":"user","content":"Weather in Paris?"}],
"tools":[{"type":"function","function":{"name":"get_weather","description":"Get weather",
"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}]}' \
| python3 -c "import json,sys;d=json.load(sys.stdin);print(d['choices'][0]['finish_reason'])"
# 3. Streaming (expect several chunks then [DONE])
curl -sN http://127.0.0.1:8000/v1/chat/completions -H 'Content-Type: application/json' \
-d '{"model":"dsv4-hybrid","stream":true,"messages":[{"role":"user","content":"Count 1 to 5."}]}' \
| grep -c '^data:'
```
**Benchmark tip:** when comparing runs, give every prompt a **unique prefix**.
vLLM's prefix cache is on by default (~86% hit rate in normal use); re-sending an
identical prompt returns in 0.11 s instead of 2.68 s. Measuring that reports
impossible numbers — 276,000 tok/s prefill in one early run here. A nonce in the
prompt prefix invalidates the cached sequence and restores honest timings.
|