TurboQuant v1.9.0 — DeepSeek-V4-Flash FP4 on 2× DGX Spark: Tensor-Parallel + Multi-Slot + MTP
Latest🆕 v1.9.0 — DeepSeek-V4-Flash FP4 on 2× DGX Spark: Tensor-Parallel + Multi-Slot + MTP
The full serving stack on native FP4, all at once: 2-node tensor parallelism across two DGX Sparks, concurrent multi-slot batching, and MTP self-speculative decoding — on the FP4/FP8-native checkpoint with no requantization.
Environment: 2× NVIDIA DGX Spark (GB10, 128GB) over RoCE · DeepSeek-V4-Flash-FP4-FP8-native (nsparks lineage: F8_E4M3 dense + MXFP4 experts, 146GB) · ctx=8192.
| Config (single stream) | gen t/s |
|---|---|
| FP4 PLAIN (2-node TP) | ~15 |
| FP4 + MTP + verify-reuse | 16.6 (beats no-MTP) |
Concurrent multi-slot requests batch on top of this for ~2× aggregate throughput.
What landed:
- A new
GGML_TYPE_F8_E4M3_B128quant type, end to end. Full machinery for the FP8 dense weights: the block struct (128× E4M3 values + one E8M0 power-of-two block scale), a bit-exact CPU codec (E4M3FN decode with subnormals/NaN/±448 saturation + E8M02^(e−127)scale), CUDA dequant (to_fp16for the cuBLAS GEMM path) and a CUDA MMVQ GEMV kernel. MMVQ defaults to a__shared__-staged 256-entry LUT (bit-identical to the scalar dequant); anint8 dp4aapproximation is available but env-gated OFF (GGML_CUDA_F8_APPROX_DP4A, lossy). The CPU↔CUDA↔LUT decode tables were verified identical across all 256 codes. To free the type slot,GGML_TYPE_TBQ3_0was relocated42 → 65(with the range-guards that depend on it fixed — see audit below). - FP4/FP8-native loads with no requantization, via a load-time adapter. The 146GB
nsparkscheckpoint (F8_E4M3 dense + MXFP4 experts,ftype 41) is served as-is. A loader adapter — gated strictly onftype 41so upstream models keep every key required — translates ~21 nsparks tensor names to ourdeepseek4schema (compressor→compress,attn_kv→attn_kv_latent,output_hc→hc_head, dropped.weightsuffixes) and fills the metadata keys nsparks omits (output_lora_rank,n_hc,hc_sinkhorn, the 43-entrycompress_ratios,compress_rope_freq_base, …) with the architecture's defaults. - MTP baked into the FP4 file (not a side-shard). The standalone MTP head (arch
deepseek4_mtp_support,mtp.0.*) has notok_embd/output, so it can't load as a draft on its own.turboquant/ds4_fp4_bake_mtp.pystreams (via mmap — no 150GB in RAM) one combined GGUF = every FP4 tensor byte-identical + the MTP head renamed toblk.43.nextn.*+nextn_predict_layers=1, keepingfile_type 41so the FP4 adapter still applies. Then--spec-type draft-mtpconsumes it directly. DSV4_VERIFY_REUSEis the unlock — an honest reversal from v1.8.0. In v1.8.0 this verify-graph-reuse infra shipped but OFF because graph rebuild overlapped the async GPU and it gained ~0 t/s. On the FP4 + MTP stack the situation inverts: MTP first measured 2.27 t/s withgraphs reused = 0— every speculative round rebuilds the full DSV4 graph (~1 s/round), and that rebuild is now the bottleneck. TurningDSV4_VERIFY_REUSEon restores reuse (0 → 100+/req) → 16.6 t/s, beating the no-MTP baseline (~15). It is perplexity-sensitive (the 256-padded view perturbs flash-attn fp-accumulation order), so it stays gated and was verified lossless here (greedyFrance→Paris/수도→서울, plus a Hangul-jamo-decomposition multi-turn repro, stable across both boxes).- 2-node TP + multi-slot + MTP, composed. SPMD tensor parallelism across two Sparks over RoCE (mirrored output, NCCL all-reduce), the multi-slot decode-batching path (
DSV4_MULTISLOT, concurrent requests → ~2× aggregate), and MTP all run together. The enabling fix is a graph-scoped metadata rebuild in the meta-backend so the multi-slot + MTP verify graph no longer corrupts across TP splits. - Long-context graph-arena crash fixed. The DSV4 chunk compressor built
O(context)graph objects on long multi-turn prefills, exhausting the metadata arena (the"괭"-after-N-turns crash).DSV4_BATCHED_COMPRESSORrewrites the chunk compressor to a fixedO(1)op count (carry-in / bulk-pool / carry-out, numerically equal to the unrolled recurrence); sched-context decoupling (max_splitscap, 21GB → ~1.3GB metadata) and-ub 256keep every arena bounded. - Pre-push code audit (4 parallel deep reviews + manual verify). Caught and fixed two critical latent bugs before release: (1) the new batched compressor's carry-out read a negative tensor offset when the carry-in block was the only completed block (
n_full==0, e.g. a non-alignedratio==128prefill chunk); (2) theTBQ3_0 42→65relocation silently broke three>=TBQ3_0 && <=TBQP4_4range-guards (65..61= always false) that gate the GB10 SoC-freeze protections for TBQ KV caches — restored to==TBQ3_0 || (TBQ4_0..TBQP4_4). Plus a per-layer-per-tokenstderrdebug flood removed from the launch scripts. F8 codec / sched cap / loader alias / bake tool audited clean.
Performance characterization
Single-stream decode is ~16.6 t/s (MTP+reuse) vs ~15 plain — FP4 by itself is not a decode-speed lever over Q4 (decode is memory-bound GEMV; native FP4 tensor cores help GEMM/prefill, not per-token GEMV), so the win here is MTP made viable by graph reuse, not the quant. The structural ceiling is the same one v1.8.0 documented: DSV4 decode is LPDDR-bandwidth-bound (matmuls ~52%, data-shuffle ~19%, attention ~2.3%), and MoE speculation is capped by distinct-expert byte growth — so MTP's edge over plain is modest by design, and the real result is that the full TP + multi-slot + MTP stack runs stably and losslessly on the native FP4 checkpoint at parity-or-better throughput. Multi-slot does not batch a single MTP stream (pure_decode=0, recurrent-state rollback ⇒ split_seq); its ~2× is aggregate, under concurrent load.
Usage
# 1) Bake the MTP head into the FP4 checkpoint → one combined GGUF (~5 min, no requant)
python3 turboquant/ds4_fp4_bake_mtp.py \
DeepSeek-V4-Flash-FP4-FP8-native.gguf \
DeepSeek-V4-Flash-MTP-Q4K-Q8_0-F32.gguf \
DeepSeek-V4-Flash-FP4-FP8-native-MTP.gguf # add --dry-run to validate metadata only
# 2) Mirror the combined GGUF to the second Spark (4-NIC parallel copy; edit INTERFACES for your cluster)
python3 turboquant/scp_dgx_spark.py ~/Models/DeepSeek-V4-Flash-GGUF/FP4
# 3) Serve the 2-node TP + multi-slot + MTP stack
bash fp4ctl.sh start # start | stop | restart | status (pid-only kill, GPU-reclaim wait)
# raw two-box launch (master on box A 10.0.1.1, slave on box B 10.0.1.2):
# bash run_tp_MASTER_DSV4-FP4-MTP.sh # serves http://0.0.0.0:8080
# bash run_tp_SLAVE_DSV4-FP4-MTP.sh # follower, no HTTPKey env baked into the launch scripts: DSV4_MULTISLOT=1 (concurrent slot batching), DSV4_VERIFY_REUSE=1 (MTP verify-graph reuse — the speed key), DSV4_BATCHED_COMPRESSOR=1 (long-context safety), with --spec-type draft-mtp --spec-draft-n-max 2 --spec-draft-p-min 0.0. Master and slave must run identical env (SPMD).
🛠️ Development effort. This was not a weekend project. The FP4 + 2-node TP + multi-slot + MTP stack landed in a ~5-day near-continuous sprint (Jun 18–22 2026, ~46 commits), the tail end of a ~2-week DeepSeek-V4-Flash marathon (~140 DSV4-related commits, near-daily Jun 9–22). One developer, two DGX Sparks, very little sleep — the type port, the loader adapter, the bake pipeline, the long-context crash hunt, the graphs-reused-0 → verify-reuse breakthrough, and a full pre-push audit, all by hand.