DeepSeek v4 Flash EXL3 on one DGX Spark
320
stars
4
commits
Python
primary language
Aug 24, 2026
updated
by Mia'a AI Lab
Single-node launcher for DeepSeek V4 Flash 0731 (EXL3/ExLlamaV3) with DSpark speculative decoding on one NVIDIA DGX Spark (GB10, SM121, 128 GiB unified memory).
Serves the 0xSero/deepseek-v4-flash-0731-spark build (3.0 bpw EXL3) via the sparkinfer (formerly b12x) kernel stack — a complete, self-contained Docker recipe tuned for speed and KV-cache headroom on a single device.
⚙️ Defaults changed (2026-08-21): the launcher
start.shserves the deep-context NVFP4 config —KV_RECORD=stock432(native 432-byte records),GPU_MEMORY_UTILIZATION=0.94,MAX_MODEL_LEN=384000,MAX_NUM_SEQS=1, DSpark K5 healthy → 439,622-token KV pool (a 370,104-token context stress-tested with exact needle recall — see Stress test). The NVFP4 dual-cache prefill bugs behind the old either/or are fixed — full story in the internal postmortem (kept local, not in this repo).
MODE=dspark, fixed K5, K64 draft).nvfp4_ds_mla compressed KV cache with the B12X_MLA_SPARSE attention/MoE backend.[6,12,24]) so concurrent decode stays on captured graphs instead of falling back to eager../hf-hub (no remote machine involved). An optional LAN mode reuses one copy of the ~107 GB across machines via an SSHFS share (REMOTE_HOST — opt-in).| Metric | Value |
|---|---|
Decode tok/s (structured) — start.sh, 384k context | 44–47 tok/s |
| KV cache pool | 439,622 tokens (boot-dependent — see Stress test) |
| Stress test — 320k & 370k context | exact needle recall, 0 preemptions — see Stress test |
Two needle-in-a-haystack runs on 2026-08-21, both on fresh boots of the deep-context config (MAX_NUM_SEQS=1, KV_RECORD=stock432, util 0.94), each at ~96% of that boot's MAX_MODEL_LEN.
Method (both runs): a single large user prompt of random-word filler, generated so prefix caching cannot deduplicate it (cached_tokens: 0 — every token fresh KV); a secret passphrase planted at token ~20; a recall question at the very end; temperature 0, thinking disabled via chat_template_kwargs, max_completion_tokens 128.
| Run 1 | Run 2 | |
|---|---|---|
MAX_MODEL_LEN | 334,000 | 384,000 (now the default) |
| KV pool (that boot) | 402,334 tokens (1.20×) | 439,622 tokens (1.14×, cold boot) |
| Prompt size | 320,037 tokens (1.33 MB) | 370,104 tokens (1.54 MB) |
| Share of pool | ~80% | ~84% |
| Needle recall from token ~20 | ✅ exact — XQ-7741-BLUE | ✅ exact — ZK-9931-AMBER |
finish_reason | stop (clean, not truncated) | stop (clean, not truncated) |
| Preemptions | 0 | 0 |
| End-to-end | 517 s — prefill ~630 tok/s effective | 594 s — prefill ~625 tok/s effective |
| Server after | healthy, /health 200, KV usage back to 0% | healthy, /health 200, KV usage back to 0% |
Prefill throughput decays with depth — ~1,024 tok/s at the start of a request, ~350–614 tok/s past 300k accumulated — so a full-length 384k prefill takes ~10 minutes end-to-end.
These runs are the deepest exercise to date of the NVFP4 dual-cache prefill path fixed in image-patch/sparkinfer/ — the pre-fix kernel NaN'd on any prompt ≥ 7 tokens; these tests pushed 320k and 370k tokens through it with exact recall.
The pool is a leftover-derived number — (util budget) − weights − profiled activation peak − non-torch overhead — and it moves between boots. Observed on this host: 337,841 (2-seq boot) → 402,334 → 430,909 → 440,461 (334k/1-seq) → 439,622 (384k/1-seq, cold). Two effects dominate:
MAX_NUM_SEQS. Single-sequence boots route more of the budget into the global cache that defines the reported pool (337,841 at 2 seqs → 402k+ at 1 seq from comparable bytes).The worst boot observed still clears MAX_MODEL_LEN with ≥ 1.14× headroom; a truly bad boot trips the boot-time KV check and stops cleanly (restart: on-failure:1).
sudo systemctl disable --now earlyoom). The server intentionally holds ~94% of the 128 GiB unified memory, so a user-space OOM killer can't tell a healthy server from a leak — and may kill it mid-serve.curl, and ~110+ GiB free local disk. sshfs + fuse3 are only needed for the optional LAN sharing mode (auto-installed with sudo when missing; requires user_allow_other in /etc/fuse.conf).REMOTE_HOST/REMOTE_USER/REMOTE_SHARE_DIR to reuse a single copy across machines.) No HuggingFace login required; the repo and image are public. If you do hit HF rate limits or need a private repo, set the optional HF_TOKEN in start.sh (or via env / .env)../start.sh # start: deep-context NVFP4 (384k, DSpark) — writes compose.yml
./start.sh --no-wait # start without waiting
First boot is intentionally long: pulls the image, downloads ~107 GB of weights locally (into ./hf-hub), coalesces TP4→TP1 losslessly, builds the K64 draft, and captures CUDA graphs. It is marked healthy only when the OpenAI-compatible endpoint responds.
ℹ️ No compose file is shipped in this repo —
compose.ymlis generated bystart.shon the first run and rewritten on every launch. It is gitignored, since the real config lives in the launcher. To produce it without starting anything:./start.sh compose-gen. Do not hand-edit it.
Everything downloads on this machine; no remote host is involved in the default path:
./start.sh auto-downloads on first boot into the local HF
cache root ./hf-hub and coalesces into ./data/tp1. The losslessly
coalesced serving checkpoint, the K64 draft, and the runtime caches all
live on this machine../download.sh performs the same
download+coalesce+verify standalone (also fully local). Once
./data/tp1/rank-sliced-tp1-manifest.json exists, boot skips the
download/coalesce step entirely, so a network-free runtime follows — the
manifest (not an env knob) is the gate.REMOTE_HOST (+ REMOTE_USER /
REMOTE_SHARE_DIR / MIA_MOUNT / HF_CACHE) to reuse a single weight
copy across machines instead of downloading locally — the spark3
arrangement, preserved as an opt-in mode on main and unchanged on the
mia-shared-setup branch.The only hard requirement is free host RAM ≥ 114.3 GiB at launch (0.94 ×
121.63 GiB; this UMA machine shares the 121.63 GiB between GPU and host) —
stop an old container first and check free -h before launching. If a boot
ever fails (KV check or otherwise), it stops after one failure (restart: on-failure:1 — it can never death-spiral the host); lower MAX_MODEL_LEN a
notch and retry once, don't launch repeatedly while the host is loaded.
Concurrency is a single env override away: MAX_NUM_SEQS=4 ./start.sh. Note
that the pool itself shrinks at higher sequence counts (the hybrid cache split
shifts — a 2-seq boot observed ~337k total, ≈169k per slot), so depth trades
against concurrency. The rest of the launcher is the deep-context config
described in this README.
curl -sS http://127.0.0.1:8888/v1/chat/completions \
-H 'Content-Type: application/json' -d '{
"model": "deepseek-v4-flash-0731",
"messages": [{"role":"user","content":"Write a correct Python function that returns the first n Fibonacci numbers."}],
"temperature": 0, "max_completion_tokens": 256 }'
Served model name: deepseek-v4-flash-0731. API: http://127.0.0.1:8888/v1 (OpenAI-compatible chat/responses endpoints, DSpark spec decoding active).
| Path | Purpose |
|---|---|
start.sh | Launcher (deep-context NVFP4, 384k/1-seq, DSpark) — all tunables live here; regenerates compose.yml (do not edit that file directly) |
compose.yml | Generated by start.sh; pinned image + mounts + runtime env |
image-patch/ | Read-only bind-mount overrides (coalescer + kernel backports + optional ABLATE hook) |
files/ | Bundled refusal direction for the optional ABLATE path (MIT-licensed; see files/README.md) |
data/ | Serving checkpoint (tp1/), K64 draft, caches (on local disk) |
cache/ | Runtime JIT/kernel caches (CuTeDSL, TileLang, TRITON, vLLM) |
| Command | Action |
|---|---|
./start.sh | start (deep-context config) + wait for /health |
./start.sh --no-wait | start without waiting |
./start.sh mount / unmount | manage the optional SSHFS share (no-op in local mode) |
./start.sh logs / ps / status | inspect runtime |
./start.sh stop / restart / down | lifecycle (preserves data/ + caches) |
./start.sh pull | pull the pinned image now |
./start.sh selftest-ablate | manual ABLATE overlay probe (no GPU, no serve; not on the boot path) |
./start.sh help | usage |
start.sh)| Variable | Default | Notes |
|---|---|---|
MAX_MODEL_LEN | 384000 | ~13% under the worst-observed cold-boot pool (439,622 tokens); lower it if a boot ever fails the KV check |
MAX_NUM_SEQS | 1 | start.sh default (single deep-context request; raise for concurrency — the pool shrinks with seq count via the hybrid cache split, e.g. 2 seqs ≈ 337k total) |
MAX_NUM_BATCHED_TOKENS | 8224 | prefill budget and b12x MLA workspace size (locked after warmup — do not lower; see below) |
GPU_MEMORY_UTILIZATION | 0.94 | max this host boots at (see KV section) |
VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS | 0 | removes the profiler's ~0.68 GiB graph over-reservation (real usage is 0.07 GiB) → grows KV |
LONG_PREFILL_TOKEN_THRESHOLD | 1024 | caps prefill chunks; prevents decode starvation (0=off) |
MAX_NUM_PARTIAL_PREFILLS | 0 | wired but currently a no-op on this fork |
MAX_CUDAGRAPH_CAPTURE_SIZE | 24 | = seqs×(k+1); 0=image default |
CUDAGRAPH_CAPTURE_SIZES | 6,12,24 | explicit capture list; 0=image default |
HF_TOKEN | (empty) | optional HF token — normally not needed (repo + image are public); set it to avoid rate limits or reach a private repo. Also honored from the environment or a local .env (the set-in-file knob is in start.sh) |
SERVED_MODEL_NAME | deepseek-v4-flash-0731 | id clients send as model |
MODE | dspark | fixed K5 DSpark draft |
DSPARK_* | — | draft size/experts knobs |
VERIFY_MODEL_CHECKSUMS | 1 | 0 skips SAP-256 inventory |
DEFAULT_CHAT_TEMPLATE_KWARGS_THINKING | true | server-side default thinking (client kwargs override) |
DEFAULT_CHAT_TEMPLATE_KWARGS_EFFORT | max | server-side default effort (override to high/low/false) |
ABLATE | 0 | opt-in refusal-direction ablation (details) — 1 projects a published refusal direction out of the attention output stream at runtime. No weight edits, no measurable perf cost, fully inert while off |
REMOTE_HOST / REMOTE_USER / REMOTE_SHARE_DIR | (empty) / mia / /home/mia/shared | LAN-share mode, opt-in — set REMOTE_HOST to reuse one weight copy across machines; defaults target the spark3 shared folder (10.0.0.1 / mia / /home/mia/shared) |
HF_CACHE | ./hf-hub (local) or $MIA_MOUNT (remote mode) | where weights download — always resolved absolute |
SERVING_PORT | 8888 | OpenAI-compatible port |
SERVING_HOST | 0.0.0.0 | bind address (passed to vLLM as HOST). 0.0.0.0 = reachable from the LAN, 127.0.0.1 = local-only, or pin one interface (192.168.1.50, ::). No auth sits in front of this port — widen it only on a trusted network |
Every tunable is an environment variable override: GPU_MEMORY_UTILIZATION=0.94 ./start.sh.
MAX_NUM_BATCHED_TOKENS is load-bearing twice. vLLM sizes the profiled activation peak (and therefore leftover KV) from it, and warmup captures the largest b12x compressed-MLA scratch, then lock_workspace() freezes that size. Lowering it to recover KV can still boot and even match prefill on a cold prompt, then crash later — typically a warm prefix-cache turn that attends a large width immediately:
AssertionError: Workspace is locked but allocation from 'b12x.py:…:_run_compressed_mla'
requires … MB, current size is … MB. Workspace growth is not allowed after locking.
Leave it at 8224. Do not treat it as a free memory knob. (Reported in #4; a 2048 trial recovered ~3.9 GiB of profiled peak but then hit the locked-workspace assertion under real traffic.)
Two record layouts are switchable with KV_RECORD on start.sh:
stock432 (default, fixed 2026-08-20): native 432-byte NVFP4 records → 439,622-token pool at util 0.94 on the 384k/1-seq config (370,104-token context stress-tested, DSpark acceptance 0.65–0.92 / 0.44 / 0.29–0.46 / 0.19–0.42 / 0.12). The dual-cache prefill path had four NVFP4 bugs (fixed via image-patch/sparkinfer/ bind mounts) — see the internal postmortem (kept local, not in this repo).padded: 584-byte FP8-compat records (stock semantics, ~270k pool at 256k) — the fallback.This host reports only ~114.5 GiB of 121.63 as free (the unified-memory display/desktop holds ~7 GiB), so GPU_MEMORY_UTILIZATION above ~0.940 fails to boot — the vendor's recipe value 0.9465 does not start here. The numbers below are the historical sweep of the 584-byte FP8-compat padded layout (KV_RECORD=padded); the default 432-byte NVFP4 layout (KV_RECORD=stock432) reaches ~440k tokens (439,622 observed cold on the 384k/1-seq config; see the intro). The padded-layout ceiling on this hardware:
| Config | KV pool | Notes |
|---|---|---|
| 0.93 (stock) | ~142k tokens | initial |
| 0.936 + est=0 | ~165k tokens | graph reservation reclaimed |
| 0.940 + est=0 (sweep winner) | ~181k tokens | validated with a 130k prefill, no OOM |
For more KV, options are structural (smaller weights / lower bpw, or a 2-node TP2 stack).
ABLATE, off by default)ABLATE=1 enables a runtime abliteration path: a unit-norm 4096-dim refusal direction is projected out of the attention output (wo_b) stream of decoder layers 10–42 on every forward pass — the runtime equivalent of the weight-space edit W ← W − λ·v(vᵀW) used by abliterated releases of this model family. No weights are modified: the projection is one 4096-dim matvec per layer (no measurable throughput/KV impact), fully reversible, and completely inert while ABLATE=0 — default serving stays byte-identical to stock.
Why a runtime projection instead of editing the EXL3 weights: this family's mHC residual pathway re-normalizes low-rank weight perturbations (LoRA-style abliteration is a documented dead end), and the proven edit targets the wo_b output space — which is exactly what the hook acts on. Editing the quantized trellis tensors in place would require dequant → edit → requant; projecting the output stream is algebraically identical and leaves the checkpoint untouched.
Thanks — the bundled files/direction_r1.pt is from drowzeys/keys-DeepSeekV4-Flash-GA-0731-Dspark-Abliterated-Anchored-Tensors. Thank you for publishing the refusal direction.
Setup — a default direction ships with this repo at files/direction_r1.pt (MIT-licensed; attribution in files/README.md). The first ABLATE=1 boot auto-provisions it into ./data/files/ (container path /models/files/). To use your own direction instead, drop a unit-norm 4096-dim .pt at ./data/files/direction_r1.pt or point DSV4_ABLATE_FILE at it — a pre-existing data/files/ file is never overwritten. Then:
./start.sh # applies ABLATE as set in start.sh (0 or 1)
ABLATE=1 ./start.sh # same, env override for this boot
Flip ABLATE between 0 and 1, then run ./start.sh again (not restart). Each flag change is a slow boot: start.sh wipes the inductor / AOT compile cache and force-recreates the container so the overlay actually changes. Expect the same kind of wait as a first boot (graph recapture), not a quick restart. Unchanged ABLATE on a later ./start.sh does not wipe.
| Knob | Default | Notes |
|---|---|---|
ABLATE | 0 | master switch. 1 enables (auto-provisions the bundled direction into data/files/); anything other than 0/1 aborts with an error; ABLATE=1 with no direction file anywhere also aborts (no silent stock boot) |
DSV4_ABLATE_LAMBDA | 3.5 | projection strength. drowzeys validated 3.5; lovesenko ships 2.5 (more conservative). λ ≥ 4 risks long-CoT reasoning-loop degeneration |
DSV4_ABLATE_LAYERS | 10-42 | inclusive target-layer range. Layers 0–9 stay stock (chat/tool protocol, drowzeys' hybrid range); the DSpark draft (layers 43+) is never touched — speculation only needs the draft to propose, verification is against the target |
DSV4_ABLATE_FILE | (auto) | explicit in-container path override; resolves to /models/files/direction_r1.pt when ABLATE=1 |
Implementation notes:
image-patch/vllm/models/deepseek_v4/nvidia/model.py — the image's file plus an additive, env-gated hook. Bind-mounted read-only only when ABLATE=1; stock boots (ABLATE=0, the default) use the image's model.py unchanged. Plain tensor math — torch.compile- and CUDA-graph-capturable../start.sh selftest-ablate, not on the boot path; no compose.yml / ./data needed): stock image model.py has no hook; overlay with DSV4_ABLATE_FILE unset is inert; overlay with FILE set checks env parse, unit-norm load, layer gating (incl. draft exclusion), fp32 projection identity y·v = (1−λ)·x·v, disabled-layer passthrough, and that a missing FILE raises.files/direction_r1.pt is byte-identical (sha256 6e4d8a8f…) to drowzeys' published results/refusal_direction_r1.pt, redistributed under that repo's MIT license with the notice preserved in files/README.md. The abliterated weights releases (HF) stay access-gated and are not redistributed here — only the 18 KB direction vector is.DSV4_ABLATE_LAYERS=0-42 first, then raise λ while watching capability.image-patch/vllm/models/deepseek_v4/nvidia/model.py from the new image — same rule as any whole-file override here.Responsible use: this removes safety refusals the upstream model was trained to have. Useful for red-teaming, evaluation, research and over-refusal workarounds — and it removes guardrails you must then supply yourself. The upstream DeepSeek license carries over to any derived serving.
Who built EXL3: @turboderp_ and @BrandonMusicKy are the core people behind EXL3. The EXL3 path used here with vLLM is based on turboderp's ExLlama work and was adapted to this customized trellis quantization format by Brandon and other members of the RTX 6K Discord. See Credits.
This build ships EXL3 3.0 bpw weights (MCG codebook / Trellis TR3 tier) on a REAP-pruned K216 checkpoint that retains 216 of 256 experts per MoE scope. Size: ~99.5 GiB. Non-routed tensors (attention, embeddings, output head, mHC, compressor, indexer) stay FP8/BF16. Two independent things are happening, and it's worth keeping them separate:
| EXL3 bpw | Typical GGUF quality equivalent | Notes |
|---|---|---|
| 2.0–2.5 | IQ2_M / IQ3_XXS territory (usable) | EXL3 stays more coherent |
| 3.0 | IQ4_XS / Q4_K_S, often feels like Q4–Q5 | Strongest advantage zone for EXL3 |
| 4.0 | Q4_K_M / Q4_K_L (sometimes better) | Early tests: EXL3 4.0 ≈ EXL2 5.0 / GGUF Q4_K |
| 5.0+ | Q5_K_M / Q6_K | Closer to parity |
At matched bits-per-weight, EXL3 and GGUF I-quants are roughly similar, but EXL3 pulls ahead at low bpw because of better error distribution and codebook design.
Treat EXL3 3.0 bpw ≈ Q4_K_M–Q5_K range in GGUF quality, often closer to Q5 in practice for this model — noticeably better than a standard GGUF Q3 at similar size, in line with EXL3's reputation for punching above its bit rate. A user who tested this exact Spark recipe reported it feels about Q5 GGUF quality (previous Spark recipes used much lower-quality Q2/Q3 GGUF). Exact perceived quality still depends on the task — coding/agentic workloads were part of the calibration here.
The server exposes an OpenAI-compatible API on http://127.0.0.1:8888/v1. It binds 0.0.0.0 by default (the container runs with network_mode: host), so from another machine on the LAN use the Spark's own address — http://<spark-ip>:8888/v1. There is no authentication in front of the port: keep it on a trusted network, or run SERVING_HOST=127.0.0.1 ./start.sh for local-only and reach it over an SSH tunnel. Recommended settings for any client:
| Setting | Value | Notes |
|---|---|---|
| Base URL | http://127.0.0.1:8888/v1 | |
| Model id | deepseek-v4-flash-0731 | sent as model |
| Context window | up to 384000 (start.sh default) | actual ceiling is the KV pool: 439,622 tokens (boot-dependent) |
| Max output tokens | e.g. 32768 | anything ≤ MAX_MODEL_LEN is accepted |
| Tokenizer | DSV4 (deepseek_v4) | enabled server-side |
| Reasoning | thinking ON, effort max by default | this is the server-side default; send chat_template_kwargs to override per request (thinking false, or reasoning_effort low/high/max) |
| Tool calling | supported (deepseek_v4 parser, auto tool choice) |
~/.pi/agent/models.json)The pi coding agent can target this server directly. Model config (this exact entry is already installed at ~/.pi/agent/models.json):
"deepseek-v4-flash-spark-local": {
"baseUrl": "http://127.0.0.1:8888/v1",
"apiKey": "dummy",
"api": "openai-completions",
"authHeader": false,
"auth": "none",
"models": [
{
"id": "deepseek-v4-flash-0731",
"name": "DeepSeek V4 Flash 0731 Spark · DSpark · 384k (local Spark)",
"reasoning": true,
"input": ["text"],
"contextWindow": 384000,
"maxTokens": 32768,
"thinkingLevelMap": {
"minimal": null, "low": null, "medium": null,
"high": "high", "max": "max"
},
"compat": {
"supportsDeveloperRole": false,
"supportsReasoningEffort": true,
"requiresReasoningContentOnAssistantMessages": true,
"maxTokensField": "max_tokens",
"thinkingFormat": "deepseek"
}
}
]
}
Then select deepseek-v4-flash-0731 in pi.
Two fixes from local-inference-lab/b12x (the current maintainer repo of the sparkinfer/b12x kernel stack) are applied onto the image's pinned kernel tree (272a84bd) as read-only bind-mounts in image-patch/sparkinfer/:
The 0xSero image is pinned and no newer build (with newer kernel commits) is published yet, which is why these are backported locally. Remove the image-patch/sparkinfer/ mounts from start.sh to return to stock kernels.
EXL3 — @turboderp_ and @BrandonMusicKy are the core people behind EXL3. The EXL3 path used here with vLLM is based on the work of turboderp (ExLlama) and was adapted to this customized trellis quantization format by Brandon and other members of the RTX 6K Discord. Format source: turboderp-org/exllamav3.
0xSero/deepseek-v4-flash-0731-spark (REAP-K216, EXL3 3.0 bpw, Trellis) and the upstream 0xSero/DeepSeek-V4-Flash-0731-EXL3-3.0bpw. That is the full extent of the 0xSero attribution — the hosted repo this recipe pulls weights from, not EXL3 itself.ghcr.io/0xsero/deepseek-v4-flash-0731-spark-sparkinfer (NVIDIA vLLM 26.02 base)local-inference-lab/b12x (sparkinfer / formerly b12x)MiaAI-Lab/DeepSeek-v4-Flash-DSpark-2x-DGX-Spark (2-node TP2 recipe; our speed/KV work derives from its methodology)files/direction_r1.pt): thank you drowzeys/keys-DeepSeekV4-Flash-GA-0731-Dspark-Abliterated-Anchored-TensorsThe packaging/orchestration glue in this repository is licensed under the MIT License. The runtime image, model weights, and upstream libraries are covered by their own licenses.
Python
91.9%
Shell
8.1%
DeepSeek v4 Flash EXL3 on one DGX Spark
320
stars
4
commits
Python
primary language
Aug 24, 2026
updated
by Mia'a AI Lab
Single-node launcher for DeepSeek V4 Flash 0731 (EXL3/ExLlamaV3) with DSpark speculative decoding on one NVIDIA DGX Spark (GB10, SM121, 128 GiB unified memory).
Serves the 0xSero/deepseek-v4-flash-0731-spark build (3.0 bpw EXL3) via the sparkinfer (formerly b12x) kernel stack — a complete, self-contained Docker recipe tuned for speed and KV-cache headroom on a single device.
⚙️ Defaults changed (2026-08-21): the launcher
start.shserves the deep-context NVFP4 config —KV_RECORD=stock432(native 432-byte records),GPU_MEMORY_UTILIZATION=0.94,MAX_MODEL_LEN=384000,MAX_NUM_SEQS=1, DSpark K5 healthy → 439,622-token KV pool (a 370,104-token context stress-tested with exact needle recall — see Stress test). The NVFP4 dual-cache prefill bugs behind the old either/or are fixed — full story in the internal postmortem (kept local, not in this repo).
MODE=dspark, fixed K5, K64 draft).nvfp4_ds_mla compressed KV cache with the B12X_MLA_SPARSE attention/MoE backend.[6,12,24]) so concurrent decode stays on captured graphs instead of falling back to eager../hf-hub (no remote machine involved). An optional LAN mode reuses one copy of the ~107 GB across machines via an SSHFS share (REMOTE_HOST — opt-in).| Metric | Value |
|---|---|
Decode tok/s (structured) — start.sh, 384k context | 44–47 tok/s |
| KV cache pool | 439,622 tokens (boot-dependent — see Stress test) |
| Stress test — 320k & 370k context | exact needle recall, 0 preemptions — see Stress test |
Two needle-in-a-haystack runs on 2026-08-21, both on fresh boots of the deep-context config (MAX_NUM_SEQS=1, KV_RECORD=stock432, util 0.94), each at ~96% of that boot's MAX_MODEL_LEN.
Method (both runs): a single large user prompt of random-word filler, generated so prefix caching cannot deduplicate it (cached_tokens: 0 — every token fresh KV); a secret passphrase planted at token ~20; a recall question at the very end; temperature 0, thinking disabled via chat_template_kwargs, max_completion_tokens 128.
| Run 1 | Run 2 | |
|---|---|---|
MAX_MODEL_LEN | 334,000 | 384,000 (now the default) |
| KV pool (that boot) | 402,334 tokens (1.20×) | 439,622 tokens (1.14×, cold boot) |
| Prompt size | 320,037 tokens (1.33 MB) | 370,104 tokens (1.54 MB) |
| Share of pool | ~80% | ~84% |
| Needle recall from token ~20 | ✅ exact — XQ-7741-BLUE | ✅ exact — ZK-9931-AMBER |
finish_reason | stop (clean, not truncated) | stop (clean, not truncated) |
| Preemptions | 0 | 0 |
| End-to-end | 517 s — prefill ~630 tok/s effective | 594 s — prefill ~625 tok/s effective |
| Server after | healthy, /health 200, KV usage back to 0% | healthy, /health 200, KV usage back to 0% |
Prefill throughput decays with depth — ~1,024 tok/s at the start of a request, ~350–614 tok/s past 300k accumulated — so a full-length 384k prefill takes ~10 minutes end-to-end.
These runs are the deepest exercise to date of the NVFP4 dual-cache prefill path fixed in image-patch/sparkinfer/ — the pre-fix kernel NaN'd on any prompt ≥ 7 tokens; these tests pushed 320k and 370k tokens through it with exact recall.
The pool is a leftover-derived number — (util budget) − weights − profiled activation peak − non-torch overhead — and it moves between boots. Observed on this host: 337,841 (2-seq boot) → 402,334 → 430,909 → 440,461 (334k/1-seq) → 439,622 (384k/1-seq, cold). Two effects dominate:
MAX_NUM_SEQS. Single-sequence boots route more of the budget into the global cache that defines the reported pool (337,841 at 2 seqs → 402k+ at 1 seq from comparable bytes).The worst boot observed still clears MAX_MODEL_LEN with ≥ 1.14× headroom; a truly bad boot trips the boot-time KV check and stops cleanly (restart: on-failure:1).
sudo systemctl disable --now earlyoom). The server intentionally holds ~94% of the 128 GiB unified memory, so a user-space OOM killer can't tell a healthy server from a leak — and may kill it mid-serve.curl, and ~110+ GiB free local disk. sshfs + fuse3 are only needed for the optional LAN sharing mode (auto-installed with sudo when missing; requires user_allow_other in /etc/fuse.conf).REMOTE_HOST/REMOTE_USER/REMOTE_SHARE_DIR to reuse a single copy across machines.) No HuggingFace login required; the repo and image are public. If you do hit HF rate limits or need a private repo, set the optional HF_TOKEN in start.sh (or via env / .env)../start.sh # start: deep-context NVFP4 (384k, DSpark) — writes compose.yml
./start.sh --no-wait # start without waiting
First boot is intentionally long: pulls the image, downloads ~107 GB of weights locally (into ./hf-hub), coalesces TP4→TP1 losslessly, builds the K64 draft, and captures CUDA graphs. It is marked healthy only when the OpenAI-compatible endpoint responds.
ℹ️ No compose file is shipped in this repo —
compose.ymlis generated bystart.shon the first run and rewritten on every launch. It is gitignored, since the real config lives in the launcher. To produce it without starting anything:./start.sh compose-gen. Do not hand-edit it.
Everything downloads on this machine; no remote host is involved in the default path:
./start.sh auto-downloads on first boot into the local HF
cache root ./hf-hub and coalesces into ./data/tp1. The losslessly
coalesced serving checkpoint, the K64 draft, and the runtime caches all
live on this machine../download.sh performs the same
download+coalesce+verify standalone (also fully local). Once
./data/tp1/rank-sliced-tp1-manifest.json exists, boot skips the
download/coalesce step entirely, so a network-free runtime follows — the
manifest (not an env knob) is the gate.REMOTE_HOST (+ REMOTE_USER /
REMOTE_SHARE_DIR / MIA_MOUNT / HF_CACHE) to reuse a single weight
copy across machines instead of downloading locally — the spark3
arrangement, preserved as an opt-in mode on main and unchanged on the
mia-shared-setup branch.The only hard requirement is free host RAM ≥ 114.3 GiB at launch (0.94 ×
121.63 GiB; this UMA machine shares the 121.63 GiB between GPU and host) —
stop an old container first and check free -h before launching. If a boot
ever fails (KV check or otherwise), it stops after one failure (restart: on-failure:1 — it can never death-spiral the host); lower MAX_MODEL_LEN a
notch and retry once, don't launch repeatedly while the host is loaded.
Concurrency is a single env override away: MAX_NUM_SEQS=4 ./start.sh. Note
that the pool itself shrinks at higher sequence counts (the hybrid cache split
shifts — a 2-seq boot observed ~337k total, ≈169k per slot), so depth trades
against concurrency. The rest of the launcher is the deep-context config
described in this README.
curl -sS http://127.0.0.1:8888/v1/chat/completions \
-H 'Content-Type: application/json' -d '{
"model": "deepseek-v4-flash-0731",
"messages": [{"role":"user","content":"Write a correct Python function that returns the first n Fibonacci numbers."}],
"temperature": 0, "max_completion_tokens": 256 }'
Served model name: deepseek-v4-flash-0731. API: http://127.0.0.1:8888/v1 (OpenAI-compatible chat/responses endpoints, DSpark spec decoding active).
| Path | Purpose |
|---|---|
start.sh | Launcher (deep-context NVFP4, 384k/1-seq, DSpark) — all tunables live here; regenerates compose.yml (do not edit that file directly) |
compose.yml | Generated by start.sh; pinned image + mounts + runtime env |
image-patch/ | Read-only bind-mount overrides (coalescer + kernel backports + optional ABLATE hook) |
files/ | Bundled refusal direction for the optional ABLATE path (MIT-licensed; see files/README.md) |
data/ | Serving checkpoint (tp1/), K64 draft, caches (on local disk) |
cache/ | Runtime JIT/kernel caches (CuTeDSL, TileLang, TRITON, vLLM) |
| Command | Action |
|---|---|
./start.sh | start (deep-context config) + wait for /health |
./start.sh --no-wait | start without waiting |
./start.sh mount / unmount | manage the optional SSHFS share (no-op in local mode) |
./start.sh logs / ps / status | inspect runtime |
./start.sh stop / restart / down | lifecycle (preserves data/ + caches) |
./start.sh pull | pull the pinned image now |
./start.sh selftest-ablate | manual ABLATE overlay probe (no GPU, no serve; not on the boot path) |
./start.sh help | usage |
start.sh)| Variable | Default | Notes |
|---|---|---|
MAX_MODEL_LEN | 384000 | ~13% under the worst-observed cold-boot pool (439,622 tokens); lower it if a boot ever fails the KV check |
MAX_NUM_SEQS | 1 | start.sh default (single deep-context request; raise for concurrency — the pool shrinks with seq count via the hybrid cache split, e.g. 2 seqs ≈ 337k total) |
MAX_NUM_BATCHED_TOKENS | 8224 | prefill budget and b12x MLA workspace size (locked after warmup — do not lower; see below) |
GPU_MEMORY_UTILIZATION | 0.94 | max this host boots at (see KV section) |
VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS | 0 | removes the profiler's ~0.68 GiB graph over-reservation (real usage is 0.07 GiB) → grows KV |
LONG_PREFILL_TOKEN_THRESHOLD | 1024 | caps prefill chunks; prevents decode starvation (0=off) |
MAX_NUM_PARTIAL_PREFILLS | 0 | wired but currently a no-op on this fork |
MAX_CUDAGRAPH_CAPTURE_SIZE | 24 | = seqs×(k+1); 0=image default |
CUDAGRAPH_CAPTURE_SIZES | 6,12,24 | explicit capture list; 0=image default |
HF_TOKEN | (empty) | optional HF token — normally not needed (repo + image are public); set it to avoid rate limits or reach a private repo. Also honored from the environment or a local .env (the set-in-file knob is in start.sh) |
SERVED_MODEL_NAME | deepseek-v4-flash-0731 | id clients send as model |
MODE | dspark | fixed K5 DSpark draft |
DSPARK_* | — | draft size/experts knobs |
VERIFY_MODEL_CHECKSUMS | 1 | 0 skips SAP-256 inventory |
DEFAULT_CHAT_TEMPLATE_KWARGS_THINKING | true | server-side default thinking (client kwargs override) |
DEFAULT_CHAT_TEMPLATE_KWARGS_EFFORT | max | server-side default effort (override to high/low/false) |
ABLATE | 0 | opt-in refusal-direction ablation (details) — 1 projects a published refusal direction out of the attention output stream at runtime. No weight edits, no measurable perf cost, fully inert while off |
REMOTE_HOST / REMOTE_USER / REMOTE_SHARE_DIR | (empty) / mia / /home/mia/shared | LAN-share mode, opt-in — set REMOTE_HOST to reuse one weight copy across machines; defaults target the spark3 shared folder (10.0.0.1 / mia / /home/mia/shared) |
HF_CACHE | ./hf-hub (local) or $MIA_MOUNT (remote mode) | where weights download — always resolved absolute |
SERVING_PORT | 8888 | OpenAI-compatible port |
SERVING_HOST | 0.0.0.0 | bind address (passed to vLLM as HOST). 0.0.0.0 = reachable from the LAN, 127.0.0.1 = local-only, or pin one interface (192.168.1.50, ::). No auth sits in front of this port — widen it only on a trusted network |
Every tunable is an environment variable override: GPU_MEMORY_UTILIZATION=0.94 ./start.sh.
MAX_NUM_BATCHED_TOKENS is load-bearing twice. vLLM sizes the profiled activation peak (and therefore leftover KV) from it, and warmup captures the largest b12x compressed-MLA scratch, then lock_workspace() freezes that size. Lowering it to recover KV can still boot and even match prefill on a cold prompt, then crash later — typically a warm prefix-cache turn that attends a large width immediately:
AssertionError: Workspace is locked but allocation from 'b12x.py:…:_run_compressed_mla'
requires … MB, current size is … MB. Workspace growth is not allowed after locking.
Leave it at 8224. Do not treat it as a free memory knob. (Reported in #4; a 2048 trial recovered ~3.9 GiB of profiled peak but then hit the locked-workspace assertion under real traffic.)
Two record layouts are switchable with KV_RECORD on start.sh:
stock432 (default, fixed 2026-08-20): native 432-byte NVFP4 records → 439,622-token pool at util 0.94 on the 384k/1-seq config (370,104-token context stress-tested, DSpark acceptance 0.65–0.92 / 0.44 / 0.29–0.46 / 0.19–0.42 / 0.12). The dual-cache prefill path had four NVFP4 bugs (fixed via image-patch/sparkinfer/ bind mounts) — see the internal postmortem (kept local, not in this repo).padded: 584-byte FP8-compat records (stock semantics, ~270k pool at 256k) — the fallback.This host reports only ~114.5 GiB of 121.63 as free (the unified-memory display/desktop holds ~7 GiB), so GPU_MEMORY_UTILIZATION above ~0.940 fails to boot — the vendor's recipe value 0.9465 does not start here. The numbers below are the historical sweep of the 584-byte FP8-compat padded layout (KV_RECORD=padded); the default 432-byte NVFP4 layout (KV_RECORD=stock432) reaches ~440k tokens (439,622 observed cold on the 384k/1-seq config; see the intro). The padded-layout ceiling on this hardware:
| Config | KV pool | Notes |
|---|---|---|
| 0.93 (stock) | ~142k tokens | initial |
| 0.936 + est=0 | ~165k tokens | graph reservation reclaimed |
| 0.940 + est=0 (sweep winner) | ~181k tokens | validated with a 130k prefill, no OOM |
For more KV, options are structural (smaller weights / lower bpw, or a 2-node TP2 stack).
ABLATE, off by default)ABLATE=1 enables a runtime abliteration path: a unit-norm 4096-dim refusal direction is projected out of the attention output (wo_b) stream of decoder layers 10–42 on every forward pass — the runtime equivalent of the weight-space edit W ← W − λ·v(vᵀW) used by abliterated releases of this model family. No weights are modified: the projection is one 4096-dim matvec per layer (no measurable throughput/KV impact), fully reversible, and completely inert while ABLATE=0 — default serving stays byte-identical to stock.
Why a runtime projection instead of editing the EXL3 weights: this family's mHC residual pathway re-normalizes low-rank weight perturbations (LoRA-style abliteration is a documented dead end), and the proven edit targets the wo_b output space — which is exactly what the hook acts on. Editing the quantized trellis tensors in place would require dequant → edit → requant; projecting the output stream is algebraically identical and leaves the checkpoint untouched.
Thanks — the bundled files/direction_r1.pt is from drowzeys/keys-DeepSeekV4-Flash-GA-0731-Dspark-Abliterated-Anchored-Tensors. Thank you for publishing the refusal direction.
Setup — a default direction ships with this repo at files/direction_r1.pt (MIT-licensed; attribution in files/README.md). The first ABLATE=1 boot auto-provisions it into ./data/files/ (container path /models/files/). To use your own direction instead, drop a unit-norm 4096-dim .pt at ./data/files/direction_r1.pt or point DSV4_ABLATE_FILE at it — a pre-existing data/files/ file is never overwritten. Then:
./start.sh # applies ABLATE as set in start.sh (0 or 1)
ABLATE=1 ./start.sh # same, env override for this boot
Flip ABLATE between 0 and 1, then run ./start.sh again (not restart). Each flag change is a slow boot: start.sh wipes the inductor / AOT compile cache and force-recreates the container so the overlay actually changes. Expect the same kind of wait as a first boot (graph recapture), not a quick restart. Unchanged ABLATE on a later ./start.sh does not wipe.
| Knob | Default | Notes |
|---|---|---|
ABLATE | 0 | master switch. 1 enables (auto-provisions the bundled direction into data/files/); anything other than 0/1 aborts with an error; ABLATE=1 with no direction file anywhere also aborts (no silent stock boot) |
DSV4_ABLATE_LAMBDA | 3.5 | projection strength. drowzeys validated 3.5; lovesenko ships 2.5 (more conservative). λ ≥ 4 risks long-CoT reasoning-loop degeneration |
DSV4_ABLATE_LAYERS | 10-42 | inclusive target-layer range. Layers 0–9 stay stock (chat/tool protocol, drowzeys' hybrid range); the DSpark draft (layers 43+) is never touched — speculation only needs the draft to propose, verification is against the target |
DSV4_ABLATE_FILE | (auto) | explicit in-container path override; resolves to /models/files/direction_r1.pt when ABLATE=1 |
Implementation notes:
image-patch/vllm/models/deepseek_v4/nvidia/model.py — the image's file plus an additive, env-gated hook. Bind-mounted read-only only when ABLATE=1; stock boots (ABLATE=0, the default) use the image's model.py unchanged. Plain tensor math — torch.compile- and CUDA-graph-capturable../start.sh selftest-ablate, not on the boot path; no compose.yml / ./data needed): stock image model.py has no hook; overlay with DSV4_ABLATE_FILE unset is inert; overlay with FILE set checks env parse, unit-norm load, layer gating (incl. draft exclusion), fp32 projection identity y·v = (1−λ)·x·v, disabled-layer passthrough, and that a missing FILE raises.files/direction_r1.pt is byte-identical (sha256 6e4d8a8f…) to drowzeys' published results/refusal_direction_r1.pt, redistributed under that repo's MIT license with the notice preserved in files/README.md. The abliterated weights releases (HF) stay access-gated and are not redistributed here — only the 18 KB direction vector is.DSV4_ABLATE_LAYERS=0-42 first, then raise λ while watching capability.image-patch/vllm/models/deepseek_v4/nvidia/model.py from the new image — same rule as any whole-file override here.Responsible use: this removes safety refusals the upstream model was trained to have. Useful for red-teaming, evaluation, research and over-refusal workarounds — and it removes guardrails you must then supply yourself. The upstream DeepSeek license carries over to any derived serving.
Who built EXL3: @turboderp_ and @BrandonMusicKy are the core people behind EXL3. The EXL3 path used here with vLLM is based on turboderp's ExLlama work and was adapted to this customized trellis quantization format by Brandon and other members of the RTX 6K Discord. See Credits.
This build ships EXL3 3.0 bpw weights (MCG codebook / Trellis TR3 tier) on a REAP-pruned K216 checkpoint that retains 216 of 256 experts per MoE scope. Size: ~99.5 GiB. Non-routed tensors (attention, embeddings, output head, mHC, compressor, indexer) stay FP8/BF16. Two independent things are happening, and it's worth keeping them separate:
| EXL3 bpw | Typical GGUF quality equivalent | Notes |
|---|---|---|
| 2.0–2.5 | IQ2_M / IQ3_XXS territory (usable) | EXL3 stays more coherent |
| 3.0 | IQ4_XS / Q4_K_S, often feels like Q4–Q5 | Strongest advantage zone for EXL3 |
| 4.0 | Q4_K_M / Q4_K_L (sometimes better) | Early tests: EXL3 4.0 ≈ EXL2 5.0 / GGUF Q4_K |
| 5.0+ | Q5_K_M / Q6_K | Closer to parity |
At matched bits-per-weight, EXL3 and GGUF I-quants are roughly similar, but EXL3 pulls ahead at low bpw because of better error distribution and codebook design.
Treat EXL3 3.0 bpw ≈ Q4_K_M–Q5_K range in GGUF quality, often closer to Q5 in practice for this model — noticeably better than a standard GGUF Q3 at similar size, in line with EXL3's reputation for punching above its bit rate. A user who tested this exact Spark recipe reported it feels about Q5 GGUF quality (previous Spark recipes used much lower-quality Q2/Q3 GGUF). Exact perceived quality still depends on the task — coding/agentic workloads were part of the calibration here.
The server exposes an OpenAI-compatible API on http://127.0.0.1:8888/v1. It binds 0.0.0.0 by default (the container runs with network_mode: host), so from another machine on the LAN use the Spark's own address — http://<spark-ip>:8888/v1. There is no authentication in front of the port: keep it on a trusted network, or run SERVING_HOST=127.0.0.1 ./start.sh for local-only and reach it over an SSH tunnel. Recommended settings for any client:
| Setting | Value | Notes |
|---|---|---|
| Base URL | http://127.0.0.1:8888/v1 | |
| Model id | deepseek-v4-flash-0731 | sent as model |
| Context window | up to 384000 (start.sh default) | actual ceiling is the KV pool: 439,622 tokens (boot-dependent) |
| Max output tokens | e.g. 32768 | anything ≤ MAX_MODEL_LEN is accepted |
| Tokenizer | DSV4 (deepseek_v4) | enabled server-side |
| Reasoning | thinking ON, effort max by default | this is the server-side default; send chat_template_kwargs to override per request (thinking false, or reasoning_effort low/high/max) |
| Tool calling | supported (deepseek_v4 parser, auto tool choice) |
~/.pi/agent/models.json)The pi coding agent can target this server directly. Model config (this exact entry is already installed at ~/.pi/agent/models.json):
"deepseek-v4-flash-spark-local": {
"baseUrl": "http://127.0.0.1:8888/v1",
"apiKey": "dummy",
"api": "openai-completions",
"authHeader": false,
"auth": "none",
"models": [
{
"id": "deepseek-v4-flash-0731",
"name": "DeepSeek V4 Flash 0731 Spark · DSpark · 384k (local Spark)",
"reasoning": true,
"input": ["text"],
"contextWindow": 384000,
"maxTokens": 32768,
"thinkingLevelMap": {
"minimal": null, "low": null, "medium": null,
"high": "high", "max": "max"
},
"compat": {
"supportsDeveloperRole": false,
"supportsReasoningEffort": true,
"requiresReasoningContentOnAssistantMessages": true,
"maxTokensField": "max_tokens",
"thinkingFormat": "deepseek"
}
}
]
}
Then select deepseek-v4-flash-0731 in pi.
Two fixes from local-inference-lab/b12x (the current maintainer repo of the sparkinfer/b12x kernel stack) are applied onto the image's pinned kernel tree (272a84bd) as read-only bind-mounts in image-patch/sparkinfer/:
The 0xSero image is pinned and no newer build (with newer kernel commits) is published yet, which is why these are backported locally. Remove the image-patch/sparkinfer/ mounts from start.sh to return to stock kernels.
EXL3 — @turboderp_ and @BrandonMusicKy are the core people behind EXL3. The EXL3 path used here with vLLM is based on the work of turboderp (ExLlama) and was adapted to this customized trellis quantization format by Brandon and other members of the RTX 6K Discord. Format source: turboderp-org/exllamav3.
0xSero/deepseek-v4-flash-0731-spark (REAP-K216, EXL3 3.0 bpw, Trellis) and the upstream 0xSero/DeepSeek-V4-Flash-0731-EXL3-3.0bpw. That is the full extent of the 0xSero attribution — the hosted repo this recipe pulls weights from, not EXL3 itself.ghcr.io/0xsero/deepseek-v4-flash-0731-spark-sparkinfer (NVIDIA vLLM 26.02 base)local-inference-lab/b12x (sparkinfer / formerly b12x)MiaAI-Lab/DeepSeek-v4-Flash-DSpark-2x-DGX-Spark (2-node TP2 recipe; our speed/KV work derives from its methodology)files/direction_r1.pt): thank you drowzeys/keys-DeepSeekV4-Flash-GA-0731-Dspark-Abliterated-Anchored-TensorsThe packaging/orchestration glue in this repository is licensed under the MIT License. The runtime image, model weights, and upstream libraries are covered by their own licenses.
Python
91.9%
Shell
8.1%