SinclairSchneider's picture
Update README.md
09f6e2a verified
|
Raw
History Blame Contribute Delete
12.6 kB
---
# license: set this to match the original repository before publishing
pipeline_tag: text-generation
base_model: canada-quant/DeepSeek-V4-Flash-W4A16-FP8-MTP
tags:
- deepseek
- deepseek-v4
- w4a16
- fp8
- mtp
- speculative-decoding
- sm89
- ada
- rtx-4090
- rtx-6000-ada
- vllm
---
# DeepSeek-V4-Flash-W4A16-FP8-MTP — Ada (SM89) Serving Edition
This repository is a **byte-identical mirror** of
[canada-quant/DeepSeek-V4-Flash-W4A16-FP8-MTP](https://huggingface.co/canada-quant/DeepSeek-V4-Flash-W4A16-FP8-MTP)
— all credit for the quantization and the checkpoint itself goes to
**canada-quant**. The mirror exists so that the complete, verified recipe for
serving this model on **SM89 (Ada) GPUs** — RTX 4090, RTX 6000 Ada — can live
next to the weights: the setup script, the one required source patch, a
systemd unit, and this document.
The Ada adaptation — four nights of debugging across two CUDA toolchains,
three attention backends, and one 870-commit upstream regression — was worked
out with the help of **Claude (Anthropic)**.
## What this checkpoint is
DeepSeek-V4-Flash quantized to **W4A16 INT4 experts + FP8 attention**
(compressed-tensors) with a **BF16 MTP head** for speculative decoding.
~159 GB on disk, DeepSeek sparse attention (Lightning Indexer / MLA),
**262,144-token context**. It loads at roughly 38 GiB per GPU under
tensor-parallel 4, which is exactly why 4× 48 GB Ada cards are such an
attractive target — and exactly why it is painful that the default serving
stacks refuse to run there.
> **262k is the checkpoint's context ceiling, not the recommended serving
> length on 48 GB Ada.** After weights + activations + CUDA-graph pools, only
> ~1 GiB/GPU is left for KV cache, so a full 262k context fits only at high
> `--gpu-memory-utilization` and leaves almost no headroom. For concurrent or
> agentic workloads the generated defaults cap `--max-model-len` lower (98304)
> to keep KV *and* JIT headroom — see [Serving on 48 GB Ada](#serving-on-48-gb-ada-memory-concurrency-and-context) below.
## Measured performance
4× RTX 6000 Ada (48 GB, PCIe-only, TP=4), `fp8_ds_mla` KV cache,
262,144 max context, measured 2026-07-21:
| Configuration | Generation throughput | Notes |
|---|---|---|
| Plain decoding | ~80 tok/s single-stream | Triton sparse-MLA path |
| **MTP, `num_speculative_tokens=1`** | **115–133 tok/s sustained** | draft acceptance 96.6–100%, mean acceptance length ~1.97–2.00 / 2.00 |
The BF16 MTP head drafts nearly perfectly on this checkpoint; a ~1.5–1.65×
speedup from a single speculative token is about as good as MTP-k=1 gets.
These figures are **single-stream throughput at the maximum-context operating
point** (262k, high utilization, `--max-num-batched-tokens 2048`). That is the
right point to *quote* peak tok/s, but not the right point to *run* a
concurrent or agentic workload on 48 GB cards — see the next section for why,
and for the defaults the setup script actually generates.
## Why Ada needs special handling
The stock DeepSeek-V4 serving paths gate on newer architectures at three
independent layers: FlashMLA's sparse kernels (SM90+), DeepGEMM's FP8
paged-MQA-logits kernels (arch 9/10/12 only — no SM89 device code exists),
and FlashInfer's sparse-MLA route. The path that actually works on SM89 is
the **portable Triton sparse-MLA implementation** (upstream vLLM
[PR #41834](https://github.com/vllm-project/vllm/pull/41834)) extended to
Ada in a community fork.
## The exact working stack
Everything below is automated by
[`setup_deepseek_v4_sm89.sh`](./setup_deepseek_v4_sm89.sh) in this repo.
- **vLLM fork**: [yhfgyyf/vllm-deepseek-v4-sm89](https://github.com/yhfgyyf/vllm-deepseek-v4-sm89),
release
[`v0.23.1rc1.dev145-g8c631d45e-cu130-sm89`](https://github.com/yhfgyyf/vllm-deepseek-v4-sm89/releases/tag/v0.23.1rc1.dev145-g8c631d45e-cu130-sm89)
(July 1). **The newer `dev1018` release does not work on SM89** — an
upstream sync regressed the fork's SM89 indexer-logits fallbacks
(`sm12x_deep_gemm_fallbacks`), so its decode path dead-ends in DeepGEMM's
architecture assert. Install the `dev145` wheel specifically.
- **FlashInfer**: `flashinfer_python 0.6.14+sm89` (shipped as an asset of the
fork's `dev1018` release) + `flashinfer-cubin==0.6.13`. Idle on the Triton
path, but part of the validated environment.
- **torch 2.11.0+cu130**, with the **CUDA 13.0 toolkit pinned** at runtime
(`CUDA_HOME`/`CUDA_PATH`/`PATH`). Mixed toolchains (an apt CUDA 12 `nvcc`
shadowing 13.x headers) break JIT builds with walls of one-argument
`__cudaLaunch` errors.
- **transformers pinned to `5.8.1`, installed last.** Newer releases reject
this checkpoint's config: their `ALLOWED_LAYER_TYPES` validation no longer
accepts the `hash_moe` entries in `mlp_layer_types`. Installing the vLLM
wheel afterwards would let pip drag a newer transformers back in, so the
pin must be the final install step.
- **deep_gemm must NOT be installed.** The fork binds DeepGEMM's mqa-logits
kernels on *presence*, not capability — if the package is importable, SM89
decode crashes in `csrc/apis/attention.hpp` ("Unsupported architecture").
Absent, the capability-gated Triton fallbacks take over.
- **Launch essentials**: `VLLM_TRITON_MLA_SPARSE=1` (the master switch for
the SM89 Triton path — load-bearing), **no `--attention-backend` flag**
(forcing the FlashInfer backend crashes on an SWA-cache shape mismatch),
`--kv-cache-dtype fp8_ds_mla --block-size 256`, and the `deepseek_v4`
tokenizer / reasoning-parser / tool-call-parser modes.
- **MTP**: `--speculative-config '{"method":"mtp","num_speculative_tokens":1}'`.
`--max-num-batched-tokens` is `2048` at the peak-throughput point above; the
generated `serve.sh` raises it to `4096` for the agentic-serving defaults
(which enables chunked prefill — see the memory section, as its buffers come
out of the KV pool).
## The one required source patch (MTP only)
The fork's fused o-projection (`nvidia/ops/o_proj.py`,
`deep_gemm_fp8_o_proj`) unconditionally reads FP8 block scales
(`weight_scale` / `weight_scale_inv`) off `wo_a`. The FP8 target model sails
through; the checkpoint's **BF16 MTP drafter head** has no scales, so
enabling speculative decoding crashes with
`AttributeError: 'ColumnParallelLinear' object has no attribute 'weight_scale'`.
[`patch_dev145_bf16_oproj.py`](./patch_dev145_bf16_oproj.py) inserts a
guarded fallback that fires only for scale-less modules: inverse RoPE
replicated in plain torch (interleaved even/odd pairs on the last `rope_dim`
dims of each head, rotation by −θ, mirroring the fork's own
`_fused_inv_rope_fp8_quant_per_head` kernel), a grouped BF16 einsum against
`wo_a.weight` viewed `[n_groups, o_lora_rank, heads_per_group·head_dim]`,
then `wo_b`. The FP8 path is untouched down to the byte, and the drafter is
a single tiny layer, so the unfused fallback costs nothing measurable. The
patcher is idempotent, backs up the original, and refuses to apply against
any file that doesn't match the `dev145` layout.
## Serving on 48 GB Ada: memory, concurrency, and context
On these cards **KV cache is the thin slice** — weights, activations, and
CUDA-graph pools consume almost the whole 48 GB, leaving on the order of
~1 GiB/GPU for KV. Two failure modes sit on opposite sides of a narrow tuning
window, and the generated defaults are the corner that clears both:
- **JIT-OOM (too little headroom).** The Triton sparse-MLA path compiles a
kernel specialization *per prompt shape*, lazily, and loading each freshly
compiled cubin (`cuModuleLoadData`) needs driver-level free VRAM *outside*
the torch pool. At high utilization a never-before-seen prompt shape
arriving mid-run can find no room to load its kernel and crash a worker
(observed in `_tf32_hc_prenorm_gemm`), taking the server down. Two things
defuse this: the generated `warmup.sh` compiles the shape space at startup
while memory is free, and the systemd unit uses `Restart=always` (a JIT-OOM
can exit *cleanly* from systemd's view, so `on-failure` is not enough).
- **KV-fit refusal (too little KV).** vLLM refuses to boot if a single full
`--max-model-len` sequence won't fit the KV pool. At the headroom-safe
utilizations below, full 262k does **not** fit; `--max-model-len 98304` does,
with room for the KV a long agentic trajectory actually uses.
**Concurrency: use `--max-num-seqs 1` for agentic workloads.** With several
30–100k-token trajectories in flight, they cannot co-reside in the KV pool, so
vLLM preempts and *re-prefills* evicted sequences from scratch — prefix-cache
hit rate collapses below 40%, generation throughput drops to single digits
while prefill spikes, and wall-clock blows up roughly 10×. A single resident
stream (prefix hit ~85–91%) decoding continuously is dramatically faster in
practice. One stream that never evicts beats four that thrash.
**Utilization.** `0.97` maximizes the KV pool and works for a *watched* run
backed by warmup + auto-restart, but leaves only ~0.5–0.8 GiB/GPU free under a
long trajectory. For **unattended or shared** operation prefer
`--gpu-memory-utilization 0.95`: it restores ~2.4 GiB of headroom at identical
seq=1 throughput. The setup script exposes both as parameters; pick per how
closely you'll be watching.
**Smaller Ada cards (RTX 4090, 24 GB).** This recipe is validated on 4× 48 GB
(RTX 6000 Ada). A 159 GB checkpoint under TP=4 needs ~38 GiB/GPU for weights
alone, so it does **not** fit four 24 GB 4090s — you would need more cards
(e.g. TP=8) and an even tighter KV budget, and none of the memory figures here
transfer directly. The `rtx-4090` tag reflects the SM89 architecture the stack
targets, not a claim that this checkpoint fits on 4090-class VRAM.
## Quickstart
```bash
# 1. Build the environment (venv, pinned wheels, patch, verification,
# generated serve.sh / warmup.sh / canary.sh / systemd unit):
./setup_deepseek_v4_sm89.sh ~/deepseek-v4-serve SinclairSchneider/DeepSeek-V4-Flash-W4A16-FP8-MTP-Ada
# (the original repo id works identically:
# ./setup_deepseek_v4_sm89.sh ~/deepseek-v4-serve canada-quant/DeepSeek-V4-Flash-W4A16-FP8-MTP)
# 2. Fetch the weights once (~159 GB):
~/deepseek-v4-serve/.venv/bin/huggingface-cli download SinclairSchneider/DeepSeek-V4-Flash-W4A16-FP8-MTP-Ada
# 3. Serve (OpenAI-compatible API on :8002):
~/deepseek-v4-serve/serve.sh
# 4. Smoke test:
~/deepseek-v4-serve/canary.sh
```
Expect ~80 s of weight loading plus ~5 s for the MTP drafter, then a Triton
shape-warmup pass (`warmup.sh`, also generated) that compiles the kernel
specializations up front, then steady-state throughput. For unattended
operation the setup script generates a systemd unit
(see [`deepseek-v4-flash.service`](./deepseek-v4-flash.service)) that runs the
warmup automatically after every start (`ExecStartPost`) and uses
`Restart=always` so a mid-run kernel-load OOM self-heals rather than leaving a
dead port.
## Gotchas the scripts already handle
- pip rejects wheels whose filenames were changed — release assets are kept
under their canonical names, and `+` in GitHub asset URLs is sent as `%2B`.
- Stale `VLLM_TRITON_MLA_SPARSE_*` exports in shell profiles are silently
consumed by `dev145` and can corrupt cache geometry — `serve.sh` scrubs
them with `env -u`, and systemd's clean service environment makes the unit
immune by construction.
- `HF_HUB_OFFLINE=1` resolution follows `refs/main`; an interrupted online
metadata refresh can leave that ref pointing at a snapshot that was never
downloaded. `serve.sh` therefore resolves the model to a concrete snapshot
directory itself, trusting the ref only when its target exists on disk.
## Credits
- **[canada-quant](https://huggingface.co/canada-quant)** — the original
[DeepSeek-V4-Flash-W4A16-FP8-MTP](https://huggingface.co/canada-quant/DeepSeek-V4-Flash-W4A16-FP8-MTP)
quantization this repository mirrors. All credit for the checkpoint.
- **[yhfgyyf](https://github.com/yhfgyyf)** — the
[vllm-deepseek-v4-sm89](https://github.com/yhfgyyf/vllm-deepseek-v4-sm89)
fork whose `dev145` release contains the working SM89 path, building on
vLLM's portable Triton sparse-MLA kernels
([PR #41834](https://github.com/vllm-project/vllm/pull/41834)).
- **DeepSeek** — the DeepSeek-V4-Flash model family.
- **[Claude](https://claude.com) (Anthropic)** — the Ada adaptation,
root-cause analysis, the BF16 o_proj patch, and these scripts were
developed with Claude's help.
The weights inherit the license of the original repository.