Views
No views yet
⚡ Built for 2× RTX 2080 Ti (Turing / SM75) + NVLink
A 40B reasoning model at ~42–44 tok/s with working MTP speculative decoding on a pair of 2019 gaming cards — faster than the same model's MTP GGUF in llama.cpp (~35 tok/s) on the same hardware. If you have old Turing cards gathering dust, this is for you.
| Component | Detail |
|---|---|
| GPUs | 2× NVIDIA RTX 2080 Ti, 22 GB VRAM-modded each (44 GB total), TU102, compute capability 7.5 |
| Interconnect | NVLink bridge (NV2) — used by NCCL for the tensor-parallel all-reduce |
| PCIe | Gen3, x8/x8 (bifurcated riser setup) — NVLink carries the TP traffic, so PCIe lanes are not the bottleneck |
| Host | Proxmox LXC container with GPU passthrough, 97 GB RAM |
| Engine | SM75 community build of vLLM (weicj/vLLM-2080ti-definitive, v0.1.dev1) |
| Runtime | CUDA 12.8 · PyTorch 2.11.0+cu128 · Python 3.11 |
| Parallelism | --tensor-parallel-size 2, single-stream benchmarks (--max-num-seqs 1) |
version: gemm (AutoAWQ export layout), served with vLLM's awq_marlin kernel. Produced with Intel AutoRound 0.14.2 in RTN mode (iters=0, no gradient rounding optimization — see Honesty notes). bits=4, group_size=128, sym=False (asymmetric, runtime zero-points).model-mtphead.safetensors). The 40B expansion preserved the 27B's width — hidden 5120, intermediate 17408, 24 attention heads / 4 KV heads, head_dim 256, vocab 248320 are identical — so the donor head drops in unchanged. Same transplant idea as PiehSoft's inject_mtp_40b.py, done in the safetensors/HF path. The head was not fine-tuned on the 40B.| Group | Precision | Size (disk) |
|---|---|---|
MLP gate/up/down (96 blocks), attn o_proj, GDN out_proj — 380 Linear layers | INT4 AWQ g128 | 14.8 GB |
GDN linear_attn.in_proj_{qkv,z,a,b} | BF16 | 12.2 GB |
self_attn.{q,k,v}_proj (24 attention blocks) | BF16 | 3.5 GB |
lm_head / embed_tokens | BF16 | 2.5 / 2.5 GB |
| MTP head (transplanted) | BF16 | 0.85 GB |
| Layer 0 (entire block) + norms | BF16 | 0.6 GB |
| Mode | Context | Decode | Notes |
|---|---|---|---|
| MTP, safe sync (recommended) | 12k | ~42–44 tok/s | lossless: greedy output is token-identical to no-MTP |
| No MTP | 24k | ~24 tok/s | more KV headroom |
| Long context (weights → RAM) | up to 131k | ~3.2 tok/s | --cpu-offload-gb 8; PCIe-bound |
ignore_eos, decode rate computed as a delta between a long and a short run (excludes prompt/TTFT). TTFT on short prompts ≈ 0.4 s.-sm tensor, NVLink) does ~35 tok/s on the same cards. This vLLM path is ~20% faster single-stream, and unlike llama.cpp it batches concurrent requests.gpu_memory_utilization 0.94 → ~27k max without MTP; the safe-sync MTP path reserves ~1 GiB more → ~12k. KV-cache fp8 does not engage on SM75; vLLM's --kv-offloading-size is a prefix-cache offload and does not extend a single request's context.VLLM_ALLOW_MAMBA_SPEC_FULL_CUDAGRAPH=1 with MTP on this model. Full-CUDA-graphing the GatedDeltaNet (linear-attention) state update during speculation replays stale recurrent state and silently corrupts generation — repetition loops, hallucinated prompts, empty outputs — while short trivial prompts can still look fine, which makes it easy to miss. Keep the default 0, and use VLLM_SM75_SPEC_SYNC_MODE=safe (not nosync, which corrupts the same way). Symptom → cause was verified by A/B on identical prompts.--disable-custom-all-reduce is required on this 2× 2080 Ti setup: the custom all-reduce kernel fails with CUDA graphs (custom_all_reduce.cuh: invalid argument). NCCL over NVLink takes over the TP all-reduce; cost is ~2 tok/s.1VLLM_SM75_SPEC_SYNC_MODE=safe VLLM_ALLOW_MAMBA_SPEC_FULL_CUDAGRAPH=0 \
2python -m vllm.entrypoints.openai.api_server \
3 --model <this-repo> --served-model-name qwen40b-deckard \
4 --dtype half --tensor-parallel-size 2 --disable-custom-all-reduce \
5 --quantization awq_marlin --max-model-len 12288 \
6 --gpu-memory-utilization 0.94 --max-num-seqs 1 \
7 --language-model-only --skip-mm-profiling \
8 --mamba-cache-mode align --enable-prefix-caching \
9 --reasoning-parser qwen3 \
10 --enable-auto-tool-choice --tool-call-parser qwen3_xml \
11 --speculative-config '{"method":"mtp","num_speculative_tokens":3}' \
12 --compilation-config '{"cudagraph_mode":"FULL_AND_PIECEWISE","cudagraph_capture_sizes":[4],"max_cudagraph_capture_size":4}'num_speculative_tokens+1 (here 4), or the engine aborts with "No valid cudagraph sizes".--speculative-config, use capture size 1 and --max-model-len 24576.--cpu-offload-gb 8 --max-model-len 131072.--language-model-only); no vision weights are shipped.reasoning_content + content (hence --reasoning-parser qwen3). Give it generous max_tokens.bits=4, group_size=128, sym=False, iters=0, format="auto_awq"), keeping the BF16 list above via layer_config={name:{"bits":16}}. Two patches are needed for this VL-architecture checkpoint: force the LLM (not MLLM) calibration path, and fix the checkpoint-name reverse mapping so tensors export as model.language_model.* (matching how vLLM loads the composite arch).mtp.* tensors from the 27B donor into a separate safetensors shard and register them in model.safetensors.index.json. No renaming needed — mtp.* lives top-level.config.json + quantization_config {quant_method: awq, bits: 4, group_size: 128, version: gemm, zero_point: true, modules_to_not_convert: [...]} (see this repo's config for the exact list). mtp_num_hidden_layers: 1 is already present in the base config.iters=200) would likely be marginally more accurate. May follow later.inject_mtp_40b.py) and acceptance write-up for the GGUF path.