1x Spark: DeepSeek-V4-Flash-0731 @ 1,000 tok/s prefill, 59 tok/s multi-agent serving

v0.5.2 is out

All three fixes came straight from reports in this thread, so thank you all. What changed:

HTTP 500s at large -c (@jbourny, @kafej666). Found it. The serial fallback path sized its memory by the serverโ€™s -c flag instead of by your actual request, so on a big-context server a 26 token request could demand an 11 GiB allocation that had no chance of fitting next to the batch banks. Anything that rides the serial path hit this every time: the Anthropic style /v1/messages endpoint, non-streaming token id echo, and requests bounced off the continuous batch during memory pressure. The server now sizes that session to what the request needs and grows it when a longer conversation comes in. If even a minimal request cannot fit you get a clean retryable 503 instead of the 500. If you saw โ€œlazy session graph alloc failedโ€, this was you.

Silent slow decode at DS4_SERVER_COALESCE_MAX=1 (@GaelicThndr). Speculative decoding runs inside the continuous batch engine, and COALESCE_MAX=1 turns that engine off entirely at boot. Your speculation was armed but had nowhere to run, so everything decoded plain serial at roughly half speed with no indication. The boot log now says this loudly and tells you the fix: COALESCE_MAX of 2 or more keeps speculation, or pass --no-spec if you really want the single-lane shape.

Memory and GPU burned after interrupting requests (@emX0r). Two real bugs. A cancelled non-streaming request kept generating its full token budget for nobody, and a request cancelled during a long prompt ingest ran the whole ingest and then started generating. Both are fixed: the server now notices a closed connection at every stage and drops the work within one chunk. In my measurement an interrupt during a 45k token ingest used to waste the full 80 seconds plus a phantom generation, and now wastes about 8 seconds. For the slow creep you also described, please try v0.5.1 or later first, the grow-only cache pool from v0.5.0 was fixed there and your 0.5 tok/s crawl looks like that bug.

Upgrade is the usual installer run or a pull and rebuild. Everything ships default-on with env kill switches, and none of it changes decode output: our serving twins are byte-exact against v0.5.1.

Not intended, and thank you for the log. What you pasted is the aftermath: once a CUDA kernel hits an illegal memory access the whole context is poisoned, so every later allocation reports the same error. The two โ€œprompt startโ€ lines in the same second are just queued requests draining into the already dead context. The actual bug fired earlier, above your excerpt.

Two asks so I can pin it:

  1. Scroll up to the very first โ€œillegal memory accessโ€ line in that run and paste the 20 or so lines above it. That is where the real faulting operation is named.
  2. Your engine version (the git tag or commit you built, or the date you ran the installer) and driver version from nvidia-smi.

One likely lead: another user hit the same error class through Codex CLI and found it goes away with DS4_SESSION_LAZY_GRAPH=0. Codex style clients ride a code path that allocates a large session buffer mid-serving, and that allocation timing is my current suspect. If your client is Codex or anything using the Responses API, please try DS4_SESSION_LAZY_GRAPH=0 and tell me whether the crash disappears. That result alone narrows it a lot. Tracking this for v0.5.3.

Havenโ€™t upgraded to 0.5.2 yet. Iโ€™ll run an A/B with 0.5.0 vs 0.5.2test with BetterBench now.

Report card โ€” BetterBench perf sweep: DeepSeek-V4-Flash-0731 (IQ2XXS) + DSpark, Entrpi ds4 v0.5.0

BetterBench โ€” a serving-performance sweep with server-truth token counts
(stream_options.include_usage, no client-side tokenizer guessing). Three axes: single-stream latency per
category (TTFT, prompt-processing, inter-token latency, decode t/s), a concurrency sweep (1โ†’16 parallel), and a
prefill depth sweep (prompt-processing throughput at increasing input length). Quality is NOT measured here โ€” pair
with the tool-eval card 2026-08-02-0001.

What we ran on: GB10 (sm_121), driver 580.159.03, dedicated (idle box). Engine: Entrpi ds4 v0.5.0 (fork
commit d9c8587, built sm_121a). Model: DeepSeek-V4-Flash 0731 IQ2XXS (a ~2-bit imatrix quant of a 284B
fine-grained MoE) + the DSpark drafter (re-extracted for 0731), served via ds4-serve with DSpark speculative
decode, ctx 32768, full-residency (~104โ€“109 GiB) โ€” the same ship config the tool-eval card scored.

Provenance (locked): engine Entrpi/ds4@v0.5.0 (d9c8587, sm_121a); base antirez/deepseek-v4-gguf โ†’
โ€ฆIQ2XXSโ€ฆ-0731.gguf (81 GiB); drafter bleysg/DeepSeek-V4-Flash-DSpark-drafter-GGUF โ†’ DSpark-drafter-Q2K-Q8-0731.gguf
(6.5 GiB); harness BetterBench @365594755a (corpus v1.0, temp 0.7, 20 runs/cat, cold prefix cache).

Results

axis number
decode t/s (combined median) 29.4
TTFT p50 (combined) 620 ms
ITL 1%-low (combined, stutter floor) 8.3 t/s
concurrency aggregate 1 โ†’ 16 27.1 โ†’ 53.6 t/s
prefill (prompt-processing) @2k โ†’ @32k 979 โ†’ 975 t/s
DSpark acceptance (this workload) 0.765 median / 271 batches

All arms: 48/48 completed at every concurrency level; 64k prefill depth auto-skipped (exceeds ctx 32768).

How the headline combines (betterbench/report.py:_combined): the single-value decode t/s (and TTFT p50) is a
weighted mean of the per-category medians, weights code 0.30, reasoning 0.20, prose 0.15, json 0.15, file_edit 0.10, summarization 0.10, renormalized over the categories actually run. Per-category medians in
Detailed Results.

Read in 20 seconds

  • A 284B MoE runs at ~29 t/s decode on ONE GB10 โ€” via 2-bit (IQ2XXS) quant + DSpark speculative decode, full weights
    resident in unified memory. Thatโ€™s the headline: a planner-tier model that would not otherwise fit, serving at a usable
    single-stream rate.
  • Where it sits on our GB10 axis (decode t/s, base arms): gemma4-26b 50 โ€บ math/json-heavy DS4F ~29โ€“37 โ€บ 27b-mtp 28 โ€บ
    gemma4-12b 21 โ€บ 27b-base 12. DS4F decode โ‰ˆ 27b-with-MTP, but for a model ~10ร— the total parameters โ€” the DSpark
    win is what makes that trade land.
  • Category spread is wide (prose 21 โ†’ json 37): decode is content-dependent because DSpark acceptance varies by how
    predictable the continuation is โ€” structured/json output drafts well (37.4), free prose least (21.4).
  • Concurrency scales to ~2ร— aggregate (27โ†’54 t/s at 16-way) but per-request decode collapses (32โ†’3.8 t/s) and TTFT
    p99 blows out to ~10 s โ€” batch it for throughput, not for low-latency interactive fan-out.
  • DSpark acceptance 0.765 here vs 0.852 on tool-eval โ€” acceptance is workload-dependent (tool-use continuations are
    more predictable than open BetterBench prose); both are healthy, spec-decode is doing real work.

Reproduce (externally repeatable)

# 1) engine + weights + drafter, via the Spark installer (pins Entrpi/ds4@v0.5.0, builds sm_121, pulls 0731 + drafter)
curl -sSL https://raw.githubusercontent.com/entrpi/ds4-on-spark/main/install.sh | bash    # (omit --start to not auto-serve)
#    base: antirez/deepseek-v4-gguf  โ€ฆIQ2XXSโ€ฆ-0731.gguf   drafter: bleysg/DeepSeek-V4-Flash-DSpark-drafter-GGUF  DSpark-drafter-Q2K-Q8-0731.gguf
# 2) serve the DSpark ship config (env-var invocation via the launcher; raw --dspark flag is NOT valid in v0.5.0)
DS4_GGUF_DIR=<gguf dir> ds4-serve -c 32768 --host 0.0.0.0 --port 8000     # sets DS4_CONT_DSPARK=1 / DS4_DSPARK_MODEL=<drafter>
# 3) sweep it (server-truth token counts; --max-model-len skips the 64k prefill depth)
betterbench run --endpoint http://localhost:8000/v1 --model deepseek-v4-flash --max-model-len 32768 --out result.json
betterbench report result.json > report.md

What this card omits (BetterBench provides more)

  • Quality โ€” this is a pure perf sweep. Tool-use quality is the companion card 2026-08-02-0001 (hardmode 83; base โ‰ˆ88).
  • Deeper prefill (64k+) โ€” ctx was set to the ds4-serve default 32768 (not a memory limit), so BetterBench auto-skipped
    the @64k depth (prompt > --max-model-len). Context is nearly free here โ€” KV at 32k costs only ~1 GiB (MLA compressed KV) and
    we ran with ~10 GiB headroom โ€” so a 64k arm is a config change (ds4-serve -c 69632), not a mem-mode change. Only very deep
    (128kโ€“240k, entrpiโ€™s own claims) would pressure the full-residency weights budget and want --ssd-streaming.
  • Alternate mem modes โ€” full-residency only here. --ssd-streaming (experts from SSD, ~7โ€“17 GiB resident) trades decode
    speed for headroom; not swept in this card.
  • 99%-high ITL is a burst artifact (prefix-cache warm hits inside a run) โ€” read the median and 1%-low columns; the
    99%-high figures (up to 318k t/s) are not real sustained decode.

Notes and Caveats

  • Single run, harness-default sampling (temp 0.7). Perf, not quality โ€” no correctness assertion is made on any completion.
  • 2-bit quant. Decode figures are for IQ2XXS; a higher-precision quant would be larger (may not fit resident) and would
    change both memory footprint and DSpark acceptance.
  • DSpark acceptance is workload-dependent (0.765 here). The decode t/s therefore varies by content โ€” a fixed โ€œdecode t/sโ€
    for this model is a category-weighted average, not a constant.
  • Memory headroom: ran at ~10โ€“13 GiB free throughout, above the 4 GiB OOM-watchdog floor; box returned to 119 GiB free on
    clean teardown. The memory constraint is on batch width, not context: the engine clamped max_seq 32 โ†’ 20 at startup
    ([reduced from requested to fit memory]) โ€” so the real ceiling is ~20 concurrent sequences (our 16-way sweep sat under it,
    hence 48/48). Context length is cheap (KV ~1 GiB at 32k); concurrency capacity is the scarce resource in full-residency.

Detailed Results

Single-stream (batch = 1)

ITL columns are tokens/sec: 1% low = slowest tokens (stutter), median, 99% high = fastest (burst/cache artifact).
TTFT in ms; decode = per-run tok/s.

category runs TTFT p50 TTFT p99 PP t/s (med) ITL 1% low ITL median ITL 99% high decode t/s (med) ยฑIQR CV
chat 20 800.3 932.1 102.2 9.2 3060.8 318832.3 30.0 2.7 9.8%
code 20 570.9 822.5 109.0 8.1 2472.0 173810.8 28.8 5.2 14.2%
file_edit 20 757.2 1068.9 120.4 8.2 3148.9 272416.6 31.8 5.2 14.2%
json 20 639.1 702.6 109.7 6.8 3446.6 311736.1 37.4 4.1 7.6%
math 20 506.8 831.7 97.8 6.8 3541.0 179825.3 35.6 4.0 9.2%
prose 20 468.3 778.4 110.2 8.7 21.2 124516.1 21.4 1.2 12.5%
reasoning 20 612.1 748.9 100.8 8.8 2468.1 159615.3 26.2 6.1 20.7%
summarization 20 843.0 962.0 115.8 9.3 2815.3 307118.0 34.9 4.4 14.2%

Combined (weighted code 0.30, reasoning 0.20, prose 0.15, json 0.15, file_edit 0.10, summarization 0.10) โ€” decode t/s
median โ‰ˆ 29.4, ITL 1%-low โ‰ˆ 8.3 t/s, TTFT p50 โ‰ˆ 620 ms.

Concurrency sweep

level ok/req aggregate t/s TTFT p50 TTFT p99 per-req decode t/s (med)
1 48/48 27.1 635.5 1250.0 32.3
2 48/48 29.7 687.7 1398.8 15.3
4 48/48 43.3 732.6 2641.6 11.5
8 48/48 51.0 879.7 4718.8 6.9
16 48/48 53.6 1399.6 10421.5 3.8

Aggregate throughput ~doubles to 16-way; per-request decode and TTFT-p99 degrade steeply โ€” a throughput regime, not a
low-latency fan-out regime. Hard ceiling ~20 concurrent sequences in full-residency (engine clamped max_seq 32โ†’20 to
fit memory at startup) โ€” the 16-way sweep sits just under it, so all levels completed 48/48.

Prompt processing (prefill) sweep

Prefill throughput = prompt tokens รท TTFT, at increasing input depth (tiny decode, cold prefix cache). PP t/s columns:
1% low / median / 99% high.

target depth prompt tokens (med) TTFT p50 (ms) PP t/s 1% low PP t/s median PP t/s 99% high
2000 1506 1539.2 863.9 978.8 980.5
8000 5911 5597.1 1010.6 1056.2 1057.7
16000 11786 11116.0 1033.6 1060.2 1061.6
32000 23536 24149.3 967.7 974.6 991.5
64000 โ€” โ€” โ€” skipped โ€”

Prefill throughput is flat ~1000 t/s across depth (2kโ†’32k) โ€” no falloff, the MoE prefill is compute-bound and steady.
64000 skipped: input depth exceeds the modelโ€™s 32768-token context window.

Glossary

term expands to meaning
BetterBench โ€” serving-perf sweep with server-truth token counts (usage from the stream, not a client tokenizer).
TTFT time to first token latency from request to the first streamed token (ms); the โ€œhow long before it startsโ€ number.
ITL inter-token latency per-token spacing during decode, reported here as t/s (1/ITL). 1%-low = worst tokens (stutter), 99%-high = burst.
PP t/s prompt-processing tok/s prefill throughput = prompt tokens รท TTFT; how fast the model ingests input before generating.
decode t/s โ€” sustained generation rate (tokens/sec) once decoding; the headline single-stream throughput.
IQR inter-quartile range spread of the per-run decode distribution (Q3โˆ’Q1); a robust ยฑ that ignores outlier tails.
CV coefficient of variation std รท mean, as %; run-to-run stability (lower = more consistent decode).
combined โ€” category-weighted mean of the per-category medians (weights above), renormalized over categories run.
DSpark โ€” Entrpi ds4โ€™s lossless speculative decode โ€” an external drafter GGUF proposes tokens the target verifies (num_speculative=4).
accept rate โ€” fraction of drafted tokens accepted; workload-dependent (0.765 on this BetterBench prose vs 0.852 on tool-eval).
IQ2XXS โ€” a ~2-bit imatrix quantization of the weights โ€” very small footprint; the base of this run.
0731 โ€” the DeepSeek-V4-Flash-0731 checkpoint (no in-weights MTP head โ†’ DSpark drafter is the only speculation path).
full-residency โ€” all weights held in GB10 unified memory (~104โ€“109 GiB); the alternative is --ssd-streaming (experts from SSD).

Field report: serial lane (Anthropic /v1/messages) at high boot ctx, v0.5.2

GB10, driver 580.159.03, v0.5.2 (82d2a6f), 0731 IQ2XXS + DSpark drafter, served with -c 196608.

Symptom 1, capacity: Claude Code CLI (which speaks /v1/messages, so it rides the serial lane) started getting 503 โ€œno session graph fits beside the batch banksโ€ for every request, including a 389-token prompt (log: โ€œserial right-size: no graph fits (prompt=389 need_min=1413 boot -c 196608)โ€). /v1/stats showed requests_refused_deep_serial=22 out of 28 started. Batch-path requests were unaffected.

Root cause: the serial session graph has a ~7 GiB floor (the graph allocation is not proportional to small prompts). At boot, DS4_BATCH_FIT reserves DS4_BATCH_FIT_HEADROOM_MB (default 8192) and sizes the batch banks to fill the rest, which leaves ~8 GiB CUDA free, just under the serial floor plus its 1 GiB margin. At 196608 ctx the batch graph itself is larger, so nothing serial fit at all.

Fix: boot with DS4_BATCH_FIT_HEADROOM_MB=16384. The serial lane gets ~15 GiB and everything passes: small, 16k and 28k-token /v1/messages requests all 200 (the 27801-token one in ~28s). Trade-off: batch max_seq drops from 27 to 9 at 196608 (boot log: free=21.10 GiB, per_bank=572.5 MiB), which is fine for agent workloads. A smaller bump (12 GiB) lands around max_seq 20 if you need more batch concurrency.


Symptom 2, speed: while that worked, decode on the serial lane runs at tok/step 1.0, i.e. no speculative decode, ~15.6 tok/s sustained, versus ~28 tok/s on the batch path with DSpark. Speculative decode lives inside the continuous batch engine, so any /v1/messages client (Claude Code and friends) gets roughly half the advertised speed. Not a bug per se, but worth documenting, and spec on the serial lane would be a big win for agent-CLI users.

Thanks for the v0.5.2 fixes, the right-sizing turned the 500s into clean retryable 503s which made this easy to bisect.

Same here. Iโ€™ve been unable to get above 20 tok/sec decode at all while sending requests from Claude Code CLI in VSCode IDE. If spec decode can be added for this path, it would improve the experience very much! Thank you very much for the work on this project so far!

Edit: I changed to Codex CLI and still getting 18/19 tok/sec on average.

I let this run for about 12 hours repeatedly doing anywhere between 24-124k token inputs. Itโ€™s still running, but I have noticed that it has drastically slowed down at this point. Memory is sitting at about 116GB (started at about 112).

I started it with

 nohup ~/.local/bin/ds4-serve -c 300000 --host 0.0.0.0 --port 8188 > ~/ds4-serve.log  2>&1 &

Not sure if Iโ€™m on 0.5.2 or 0.5.1. Will try a rebuild.

Iโ€™m going to repeat an earlier observation, though: this 731 version of DSV4F behaves โ€œoddlyโ€, in my opinion. It isnโ€™t as intelligent as the mixed Q2/Q8 (or whatever it was; I am probably fudging it) quant behavior that I observed on base antirez. From pre-0731. Something is a little odd.

Might try reasoning effort max, but this model already takes way longer to think than Qwen 122B while outputting questionable results (ie not sure if better or worse to be honest) for my task.

On the bright side, I donโ€™t think Iโ€™ve observed much if any looping.

I completed the Betterbench A/B run with version 0.5.0 vs 0.5.2. Here is a short version of the report card.


Report card โ€” engine A/B: Entrpi ds4 v0.5.0 vs v0.5.2 (DS4-Flash-0731 DSpark, BetterBench)

BetterBench โ€” a serving-perf sweep with server-truth token counts (usage from the stream, no client
tokenizer). This is an engine A/B: the ONLY variable is the ds4 version. Held constant: weights (0731
IQ2XXS), drafter, ctx 32768, full-residency, and the BetterBench corpus/sampling. Sequential arms on one GB10.

Why: v0.5.2 is a field-robustness fast-follow (dead-client abort, deep--c serial serving, MTP accept guard,
disk-KV restore, trim-on-evict) whose release notes claim โ€œno performance re-bases โ€” byte-exact serving twins.โ€
This is the neutral-harness check on that claim for the hot path (short ctx, cold nonce, full-residency โ€” not
the disk-KV/deep-serial paths those fixes target), plus whether the memory right-sizing shifts the concurrency ceiling.

Provenance (locked): engine A Entrpi/ds4@v0.5.0 (d9c8587) ยท engine B @v0.5.2 (82d2a6f), both built
sm_121a via make cuda-spark. Base antirez/deepseek-v4-gguf โ€ฆIQ2XXSโ€ฆ-0731.gguf (81 GiB) + drafter
bleysg/โ€ฆDSpark-drafter-Q2K-Q8-0731.gguf (6.5 GiB), DSpark, ctx 32768. Harness BetterBench @365594755a
(corpus v1.0, temp 0.7, 20 runs/cat, cold nonce).

Results

axis v0.5.0 v0.5.2 ฮ” / note
combined decode t/s 28.9 30.1 +4% (within n=1 noise)
combined TTFT p50 701 ms 624 ms v0.5.2 lower
DSpark accept (median) 76.6% 77.4% โ‰ˆ equal (274 / 276 batches)
prefill PP t/s @32k 964 1053 v0.5.2 holds throughput at depth
concurrency agg @1 โ†’ @16 24.0 โ†’ 57.1 28.7 โ†’ 56.7 equal at saturation
max_seq fit (concurrency slots) 24 15 v0.5.2 right-sizes lower โ‡’ queues sooner
TTFT p50 @16-way 1302 ms 6064 ms v0.5.2 QUEUES the 16th req (16 > 15 slots)

Both arms completed 48/48 at every concurrency level; 64k prefill auto-skipped (ctx 32768). Single run per arm.

Read in 20 seconds

  • No regression โ€” entrpiโ€™s โ€œno performance re-basesโ€ holds. On the hot path v0.5.2 is at least as fast:
    combined decode +4%, TTFT modestly lower, DSpark acceptance equal. The +4% is within single-run variance
    (n=1, temp 0.7) โ€” call it neutral-to-slightly-better, not โ€œ4% faster.โ€
  • The real difference is a concurrency trade, not speed. v0.5.2โ€™s serial-session right-sizing reserves more
    memory per sequence โ†’ it fits 15 concurrent slots vs v0.5.0โ€™s 24. Aggregate throughput at 16-way is
    unchanged (~57 t/s), but latency at 16-way blows up (TTFT p50 1.3 s โ†’ 6.1 s, p99 to 33.7 s) because the
    16th request queues. For >15-way concurrent serving, v0.5.0 has more headroom; v0.5.2 trades slots for
    deep-serial robustness.
  • Prefill holds better at depth on v0.5.2 (32k PP 964 โ†’ 1053 t/s, flat across depth) โ€” consistent with the
    changelogโ€™s โ€œlong-context requests no longer fall off the CUDA-graph fast path.โ€
  • Verdict: safe to adopt v0.5.2 for the robustness fixes; itโ€™s perf-neutral single-stream. Caveat: if you
    serve >15 concurrent, its default right-sizing queues โ€” raise the reservation / expect higher tail latency.

Concurrency sweep (side-by-side)

Both arms completed 48/48 at every level. The story is the tail: v0.5.2 fits 15 concurrent slots vs v0.5.0โ€™s 24, so at 16-way the 16th request queues on v0.5.2 โ€” aggregate throughput stays equal but TTFT explodes (p50 6.1 s, p99 33.7 s). Below the slot limit the two are within noise.

conc. level agg t/s (v050 โ†’ v052) TTFT p50 ms (v050 โ†’ v052) TTFT p99 ms (v050 โ†’ v052) per-req decode t/s (v050 โ†’ v052)
1 24.0 โ†’ 28.7 793 โ†’ 623 1195 โ†’ 973 27.4 โ†’ 32.8
2 29.9 โ†’ 30.5 837 โ†’ 693 1640 โ†’ 1396 15.6 โ†’ 15.7
4 41.9 โ†’ 43.7 835 โ†’ 762 3139 โ†’ 2651 11.3 โ†’ 11.6
8 51.5 โ†’ 52.3 942 โ†’ 900 5429 โ†’ 4980 7.0 โ†’ 7.1
16 57.1 โ†’ 56.7 1302 โ†’ 6064 10914 โ†’ 33743 3.9 โ†’ 4.1

max_seq fit: v0.5.0 = 24 slots, v0.5.2 = 15 (right-sized lower for deep-serial reservation). The @16 TTFT blow-up on v0.5.2 is queueing (16 > 15), not slower decode โ€” per-request decode is unchanged (3.9 โ†’ 4.1).

Good report, and your headroom workaround is the right lever today. What you hit: at boot the batch path takes everything minus the headroom, and the serial lane (Anthropic and Responses APIs ride it) needs its own session graph with roughly a 7 GiB floor at that context. v0.5.2โ€™s right-sizing shrinks the serial ask down to the request size, but on your boot even the minimum window could not fit, which is exactly the refusal message you saw. Raising DS4_BATCH_FIT_HEADROOM_MB to 16384 leaves the serial lane room, at the cost of a couple of batch slots. For v0.5.3 I am planning to budget a serial-lane floor into the boot partition automatically whenever the server can receive those APIs, so the default boot serves both lanes without tuning.

On the speed gap: correct read. The serial lane has no speculative decoding today, so ~15.6 against ~28 with speculation is expected. Getting the drafter onto the serial lane is on the list. Meanwhile, if most of your traffic is Anthropic-API agents, a slightly shallower boot (say -c 131072) gives the serial lane a much larger margin.

I made a comparision (using the tool-eval-bench benchmark) with the NVFP4 quantization running on a 2x Spark cluster here: DeepSeek v4 Flash (Aiden Recipe from Reddit) - 1M token session operational, Cuda 12.1 tailored for DGX Spark GB10 - #543 by helge

Second trigger for the illegal-access class, different from the Codex/DS4_SESSION_LAZY_GRAPH lead.

Setup: GB10 (sm_121a), driver 580.159.03, engine v0.5.0 (d9c8587) built make cuda-spark,
DeepSeek-V4-Flash-0731 IQ2XXS (self-built quant, 86.7 GiB) + DSpark-drafter-Q2K-Q8-0731.gguf,
-c 131072, DS4_SERVER_COALESCE_MAX=2, disk KV enabled with
--kv-disk-dir ~/.ds4/server-kv --kv-disk-space-mb 32768.

DS4_SESSION_LAZY_GRAPH=0 was already set when this fired, so whatever this is, it is not the
lazy-session-graph allocation. Client is an OpenAI-compatible agent (hermes-agent), not Codex, and it
rides /v1/chat/completions, not the Responses API.

The sequence, in order, from one crash:

  1. Tool-calling request arrives at ~59k tokens.
  2. The engine restores a 56,247-token bank from the disk KV tier.
  3. ds4: cont admit rejected on comp-cache budget (bank 1: resident 439.0 MiB + need 36.6 MiB > budget 439.0 MiB)
  4. ds4: CUDA end commands failed: an illegal memory access was encountered

Then the usual aftermath you described: context poisoned, every later request 500s with
cuda prefill state reset failed, /v1/models still 200 so nothing outside notices.

What it looks like from here: the admission is rejected on the comp-cache budget, but the prefill
appears to proceed on that bank anyway โ€” i.e. a write into an extent that was never grown. The budget on
this box boots at budget=0.21 GiB because the weights plus the drafter fill unified memory, so the
reject path is hit constantly (measured over 20.6 h of uptime on v0.5.0: 1372 cont_admit_rejects,
1371 of 1401 requests ending up on the serial lane).

Kernel side: two Xid 13 (SM Warp Exception, Out Of Range Address) with a byte-identical ESR
across two separate crashes โ€” 0x1c81fb60. That determinism is why I do not think this is memory
pressure in the general sense.

Workaround that held: disabling the disk KV tier entirely. It is the only path that performs that
restore, so removing it removes the trigger. Cost is losing warm start across restarts.

Two things that did NOT help, in case they save someone else the time:

  • DS4_SESSION_LAZY_GRAPH=0 โ€” already on, crashed anyway.
  • Pinning DS4_BATCH_VMM_BUDGET_MB=1536 โ€” it took cont admit rejected from 6 to 0 in a short
    window, but a crash still occurred, and on v0.5.0 it cost about 1.3 GB of extra footprint
    (grow-only pool). I am re-testing that pin now on v0.5.2 since trim-on-evict changes the calculus.

Happy to run any instrumented build against this โ€” the trigger is reproducible here in the sense that
it recurs under normal agentic load with disk KV on, though I have not reduced it to a single request.

Edit, cause i forgot the max reasoning:

Making reasoning_effort: "max" actually reach the model (two silent locks, one patch)

Following up on the Think Max discussion (@helgeโ€™s hardmode TC-70-84 goes 60% โ†’ 80% with "reasoning_effort":"max"). I went to turn it on and found it does not arrive โ€” and, more to the point, it does not tell you it did not arrive. There are two independent gates, both silent, and each one alone is enough to leave you at โ€œhighโ€ while your client believes it asked for โ€œmaxโ€. Here is what they are, why the first one is safe to move, and a patch.

Lock 1: the context floor downgrades max โ†’ high with no log line

ds4.c has #define DS4_THINK_MAX_MIN_CONTEXT 393216u, and ds4_think_mode_for_context() silently rewrites DS4_THINK_MAX to DS4_THINK_HIGH when the serverโ€™s -c is below it. That is documented in --help (โ€œThink Max is applied only when --ctx is at least 393216 tokensโ€), so it is not hidden โ€” but nothing is logged at request time, so if you boot below 384K every max request comes back looking normal, just less thoughtful.

The part worth arguing about: this floor is policy, not a resource constraint. Think Max is nothing but a tokenized prompt prefix. The entire mechanism is one branch in the render path:

if (think_mode == DS4_THINK_MAX) {
    bpe_tokenize_text(vocab, DS4_REASONING_EFFORT_MAX_PREFIX, out);
}

No buffer, no graph, no KV allocation is sized by the think mode. The floor encodes DeepSeekโ€™s recommendation that Think Max wants room to deliberate, which is sound advice โ€” but on a single GB10 it is unreachable advice. At -c 262144 the context buffers alone are 5855 MiB and MemAvailable goes to zero (several of us hit that upthread); 384K is simply not a context this box can boot at while holding the weights. So the floorโ€™s practical effect on 1ร— Spark is not โ€œprotect the user from a cramped reasoning budgetโ€, it is โ€œThink Max does not exist hereโ€.

Setting it lower is not free โ€” a long deliberation can run past the context โ€” but that failure mode is the ordinary one the context handling already deals with, and it is visible when it happens. Being silently downgraded is not visible at all. I would rather have the sharp edge than the quiet one.

Lock 2: nothing in the request asks for max in the first place

Remove the floor and, for a lot of setups, nothing changes โ€” because no request ever carries the field. All four request parsers in ds4_server.c (parse_chat_request, parse_anthropic_request, parse_responses_request, parse_completion_request) initialize ds4_think_mode reasoning_effort = DS4_THINK_HIGH; and only move off it if the client sent reasoning_effort or output_config.effort.

That is correct behaviour, but it means Think Max is reachable only by clients that know the knob exists. Concretely, in my stack: hermes-agent has full reasoning_effort support including "max", but it only emits the field on its GitHub-Models and LM Studio provider paths โ€” for a generic OpenAI-compatible custom: provider (which is how everyone points hermes at ds4) it sends nothing. Same story for most agent CLIs: they were written against an API where the server default is the only reasoning setting there is. So the knob is there, the model supports it, and in practice it is never pressed.

The patch

Two env vars, both unset = current upstream behaviour exactly, both with a log line so you can see the config took effect. Against v0.5.2 (82d2a6f).

ds4.c โ€” make the floor overridable:

static uint32_t ds4_think_max_min_context_value(void) {
    static uint32_t cached = 0;
    if (cached == 0) {
        cached = DS4_THINK_MAX_MIN_CONTEXT;
        const char *e = getenv("DS4_THINK_MAX_MIN_CONTEXT");
        if (e && e[0]) {
            const long v = atol(e);
            if (v > 0) {
                cached = (uint32_t)v;
                fprintf(stderr, "ds4: Think Max floor overridden: %u tokens (upstream default %u)\n",
                        cached, (unsigned)DS4_THINK_MAX_MIN_CONTEXT);
            }
        }
    }
    return cached;
}

ds4_think_mode ds4_think_mode_for_context(ds4_think_mode mode, int ctx_size) {
    if (mode == DS4_THINK_MAX &&
        (uint32_t)(ctx_size > 0 ? ctx_size : 0) < ds4_think_max_min_context_value()) {
        return DS4_THINK_HIGH;
    }
    return mode;
}

ds4_server.c โ€” a server-side default effort, placed right after parse_reasoning_effort_name() so it can reuse the parser:

static ds4_think_mode ds4_server_default_think_effort(void) {
    static int resolved = 0;
    static ds4_think_mode cached = DS4_THINK_HIGH;
    if (!resolved) {
        resolved = 1;
        const char *e = getenv("DS4_THINK_DEFAULT_EFFORT");
        if (e && e[0]) {
            ds4_think_mode m;
            if (parse_reasoning_effort_name(e, &m)) {
                cached = m;
                fprintf(stderr, "ds4: default reasoning effort: %s\n", ds4_think_mode_name(cached));
            } else {
                fprintf(stderr, "ds4: DS4_THINK_DEFAULT_EFFORT='%s' not recognised, keeping high\n", e);
            }
        }
    }
    return cached;
}

then in each of the four parsers, replace ds4_think_mode reasoning_effort = DS4_THINK_HIGH; with ds4_think_mode reasoning_effort = ds4_server_default_think_effort();.

It is a default, not an override: a request that sends reasoning_effort still wins, including "none". The upstream unit test ds4_think_mode_for_context(DS4_THINK_MAX, 32768) == DS4_THINK_HIGH keeps passing, because with the env unset the value is still 393216.

Running it

DS4_THINK_MAX_MIN_CONTEXT=131072 DS4_THINK_DEFAULT_EFFORT=max \
  ds4-server --cuda -m <model.gguf> --dspark <drafter.gguf> --host 0.0.0.0 --port 8000 -c 131072

Both lines show up in the log on the first request that touches them:

ds4: default reasoning effort: max
ds4: Think Max floor overridden: 131072 tokens (upstream default 393216)

To confirm it is actually in the prompt rather than just configured, send a trivial question and look at prompt_tokens: the max prefix is ~70 tokens, so a one-line user message lands around 94 instead of ~24. That is the cheapest end-to-end check I found โ€” the mode is not echoed in the response.

What it costs

Not as much as I expected, which surprised me. The prefix asks for exhaustive deliberation, but the model scales it to the task rather than padding everything: โ€œwhat is the capital of Franceโ€ still came back in 23 completion tokens with a one-sentence reasoning_content. The cost shows up on hard problems, which is where you wanted it. One thing to watch on the client side: reasoning lands in reasoning_content and counts against the completion budget, so if your agent framework caps max_tokens low (mine defaults to 4096 on its mixture-of-agents path) a hard task can get truncated mid-thought. Raise that cap before blaming the model.

Setup for the numbers above: GB10, driver 580.159.03, v0.5.2 (82d2a6f) built make cuda-spark, DeepSeek-V4-Flash-0731 IQ2XXS (self-built, 86.7 GiB) + DSpark-drafter-Q2K-Q8-0731.gguf, -c 131072.

@entrpi โ€” if you would rather not ship a floor override, the second half stands on its own and is the one I would push for: a DS4_THINK_DEFAULT_EFFORT (or a --reasoning-effort flag) makes the setting reachable for every client that does not know the field exists, which today is most of them. Happy to send either half as a PR in whatever shape you prefer.

v0.5.3 is out

This one is mostly about making the server easier to support in the field, plus two community contributions I want to highlight.

What is new:

  • ds4-server --version, at last. Every crash report I have received so far arrived without a version,m. My fault for not having the flag. There is also --check-update, --upgrade, and a once-daily update check: shortly after the server starts listening it fetches a one-line file from this repo and prints a hint in the log if a newer release exists. Full disclosure: it is a plain GET of a static file, nothing about your machine or usage is sent, it never blocks or fails startup, and --no-update-check or DS4_NO_UPDATE_CHECK=1 turns it off.
  • Reasoning effort levels, contributed by Fabio Pili (PR #5). The 0731 model card documents low, high and max, and the server now implements all three with the exact prompt strings from the reference encoder. Before this, max silently gave you high, and โ€œoffโ€ returned a 400. Note the prefixed levels engage at context 393216 and above, matching the official 384K output guidance; below that everything runs as low.
  • A memory accounting fix for tight boxes, contributed by a-huk (PR #6). If your GB10 returned โ€œno graph fitsโ€ 503s on the Anthropic or Responses APIs even for tiny prompts, this was why: the free-memory estimate subtracted the drafterโ€™s mapped pages twice over. If you were running DS4_BATCH_FIT_HEADROOM_MB=16384 as a workaround, try dropping it after upgrading and tell me how it goes.
  • The Responses API now reports reasoning_tokens in usage instead of 0.
  • A new standing regression gate for the deep truncate path, built from the crash report in this thread. status on that crash: I replayed the exact session shape, the same cuts and suffixes and the 131072 boundary, on both v0.5.0 and v0.5.3, eighteen different ways, and it does not reproduce here. The one mechanism I cannot recreate on a short timescale is the v0.5.0 drafter residency release, which v0.5.1 removed. If you are on v0.5.0, upgrade. The regression box offer from that report is still the fastest way to settle it.

Upgrade:

curl -fsSL https://raw.githubusercontent.com/Entrpi/ds4-on-spark/main/install.sh | bash

From v0.5.3 onward your server will tell you when the next release is out. As always, logs and reports welcome, and now you can include ds4-server --version with them.

Awesome, thank you!

did you mean ds4-serve --version?

@helge Iโ€™d love if you could run some sort of quality benchmark beyond tool calling between this and the vLLM 2x node recipe. Iโ€™m really curious about it but I only have one machine to test on.

I compared `Entrpi/ds4-on-spark` to the API on an admittedly too open of a a problem related to my espresso machineโ€™s controller, alongside other local models, and then graded them all using Claude Fable 5 comparing to a known good solution. In that test, the local DS4 version got a 5 or a 5.5, depending on effort level and run, while the API got an 8 on high and 9 on max.

What concerns me a bit is the failure mode of a Q2 level quantisation. Some of the errors I found were related to incorrect line numbers or imprecise numbers, which makes me trust the model a bit less. That said, a synthetic needle test passed all the times and my experience has been great in general. Not sure if I should read too much into a single test.

  โœ“ Warm-up complete (1138 ms)

โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ๐Ÿ”ฎ Speculative Decoding Benchmark โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚ deepseek-v4-flash                                                                                                                                                                                                                                          โ”‚
โ”‚ tg=128  depth=[0, 16384, 32768]  prompts=['filler', 'code', 'structured']  method=auto                                                                                                                                                                     โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

  โœ“     filler @ d0  8.3 eff t/s  8.2 stream t/s
  โœ“       code @ d0  9.5 eff t/s  9.4 stream t/s
  โœ“ structured @ d0  9.4 eff t/s  9.4 stream t/s
  โœ“     filler @ d16384  6.5 eff t/s  6.5 stream t/s

I did the last update but now itโ€™s too slow โ€ฆ

$ ds4-serve --version
ds4-serve: DSpark speculative decode, MTP head dropped (--no-dspark / --no-spec to downgrade)
ds4-server 4ad370b
$ ds4-serve -c 350000 --host 0.0.0.0 --port 8002
ds4-serve: DSpark speculative decode, MTP head dropped (--no-dspark / --no-spec to downgrade)
ds4: DSpark drafter loaded: /home/acantic/gguf/DSpark-drafter-Q2K-Q8-0731.gguf (3 layers)
ds4: CUDA backend initialized on NVIDIA GB10 (sm_121)
ds4: direct I/O enabled for /home/acantic/gguf/DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2-imatrix-0731.gguf align=4096
ds4: direct I/O enabled for /home/acantic/gguf/DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2-imatrix-0731.gguf align=4096
ds4: q2k aligned repack base: 43 tensors 28.22 GiB in 7.5s (threads=6)
ds4: direct I/O enabled for /home/acantic/gguf/DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2-imatrix-0731.gguf align=4096
ds4: iq2 aligned repack base: 86 tensors 44.34 GiB in 11.3s (threads=6)
ds4: direct I/O enabled for /home/acantic/gguf/DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2-imatrix-0731.gguf align=4096
ds4: q8 aligned repack base: 345 tensors 6.15 GiB in 2.4s (threads=6)
ds4: CUDA integrated + in-process artifacts: skipping full host registration (80.76 GiB mmap left unpinned; expert weights served from device artifacts)
ds4: CUDA registered 6.49 GiB model mapping for device access
ds4: aligned artifacts built in-process (474 artifacts, 78.71 GiB, 21.2s)
ds4: cuda backend initialized for graph diagnostics
0802 18:55:45 ds4-server: context buffers 7695.00 MiB (ctx=350000, backend=cuda, prefill_chunk=4096, raw_kv_rows=4352, compressed_kv_rows=87502)
ds4: packed FP8 compressed-KV primary ACTIVE (default since v0.2.4; DS4_CUDA_FP8_KV=0 restores F32-primary)
ds4: packed FP4 indexer compressed-KV primary ACTIVE (default since v0.2.4; DS4_CUDA_FP4_INDEX=0 restores F32-primary)
ds4: batch fit: free=17.76 GiB headroom=8.00 GiB per_bank=623.0 MiB -> max_seq 16 (requested 32)
ds4: batch vmm: comp/index slabs demand-mapped (page=2048 KiB, virtual 5975.7 MiB/bank, floor 120.7 MiB/bank x 16 banks, budget=4.09 GiB)
0802 18:55:46 ds4-server: persistent batch ctx ready (max_seq=16 max_tokens=4096 ctx=350000 raw_ring=4352 seq_cap=350000) [reduced from requested to fit memory]
ds4: session graph allocated lazily (ctx=528 prefill_cap=528)
ds4: CUDA loading model tensors into device cache
ds4: C3-Inc5 GPU embed gather active at all widths (first small n=256)
ds4: hc mix using rms-fold f16 activations (P3-Inc1)
ds4: norm producer triple-emits q8 (flat-pool p5c, first rows=256 n=4096)
ds4: attn_norm producer dual-emits the f16 mirror (inc-12c)
ds4: batch f16 activations shared across attn_norm consumers (inc-11 F1)
ds4: CUDA Q8_0 dispatch: mmq (sm_121, 546 GB/s memory bandwidth) [default]
ds4: dense q8 mmq consuming producer q8 (flat-pool p5c, first label='q8_0' n_tok=256)
ds4: attention tokentile zero-prefix engage (first n_tokens=256)
ds4: grouped output_a using aligned Q8_0 artifacts (M1-Inc4)
ds4: attention out_a fused own kernel engage (flat-pool p1, first n_tokens=256)
ds4: attention out_b consuming producer q8 (flat-pool p5a, first n_tokens=256)
ds4: hc expand emitting rms-fold f16 activations (P3-Inc2)
ds4: ffn_norm producer dual-emits f16 for the router (inc-12c)
ds4: moe gateup consuming producer q8 (flat-pool p5c, first n_tokens=256)
ds4: moe gateup y-indirect q8 staging engage (flat-pool p5b, first n_tokens=256 n_assign=1536)
ds4: fused MoE gateup+swiglu+q8 D2R prefill engaged (inc-9 F7 direct)
ds4: hc expand folding deferred MoE sum (inc-8 F5)
ds4: attention tokentile zero-prefix engage (first n_tokens=256 n_comp=64)
ds4: dense decode using aligned Q8_0 artifacts (M1-Inc3)
ds4: boot prewarm done (512 tokens, 2 chunks) in 8.5s
0802 18:55:54 ds4-server: listening on http://0.0.0.0:8002
ds4-server: update available: v0.5.3 (running 4ad370b) -- upgrade with:
ds4-server:   curl -fsSL https://raw.githubusercontent.com/Entrpi/ds4-on-spark/main/install.sh | bash
ds4-server: (disable this once-daily check with --no-update-check or DS4_NO_UPDATE_CHECK=1)
ds4: batch_eval engaged (first call n=4096 pos0=4096)
ds4: continuous-batch per-layer graph capture armed (default ON; DS4_CONT_CAPTURE=0 to disable)
ds4: invalidated 1 captured graph(s): f16 gemm activations
ds4: dense q8 batched using D2R aligned path (first label='q8_0' M=32768 N=4096 K=1024)
ds4: dense q8 D2R consuming producer q8 (flat-pool p5c, first label='q8_0' n_tok=4096)
ds4: CUDA loading model tensors 7.87 GiB cached
ds4: indexer exact mxf4 score-select engage (first n_comp=1024 n_tokens=4096)
ds4: indexer q producer-mirror engage (13a.2, first n_tokens=4096)
ds4: M2-Inc1 fused HC stage active (coop 48-blk, in_dim 16384)
ds4: M2-Inc1b HC-stage q8 activation fold active (pair decode)
ds4: q8 pair decode using aligned Q8_0 artifacts (M1-Inc4)
ds4: C3-Inc1 batched fused router active (coop 128-blk)
ds4: MoE decode using aligned fused gate+up+swiglu (M1-Inc2)
ds4: M2 Q2K aligned moe-down active
ds4: C3-Inc2 batched fused compressor pair active (coop 128-blk)
ds4: CONT_MTP_ACCEPT(DSpark) D=4 steps=11 emit=43 drafts=34 hits=32 accept=94.1% tok/step=3.91
0802 18:56:18 ds4-server: continuous batch ctx=350000 path=cont served=1 fallback=0
0802 18:56:18 ds4-server: partial fork admit src=0 dst=1 cut=9104 suffix=125
ds4: fused MoE prefill on the materialized tier (inc-9 F7 shared-schedule; direct rc=-95)
ds4: CONT_MTP_ACCEPT(DSpark) D=4 steps=15 emit=40 drafts=35 hits=25 accept=71.4% tok/step=2.67
0802 18:56:25 ds4-server: continuous batch ctx=350000 path=cont served=1 fallback=0
ds4-server: thinking not closed, ignoring DSML in reasoning
ds4: CONT_MTP_ACCEPT(DSpark) D=4 steps=2 emit=3 drafts=1 hits=1 accept=100.0% tok/step=1.50
0802 18:56:46 ds4-server: continuous batch ctx=350000 path=cont served=1 fallback=0
ds4-server: thinking not closed, ignoring DSML in reasoning
ds4: CONT_MTP_ACCEPT(DSpark) D=4 steps=0 emit=0 drafts=0 hits=0 accept=0.0% tok/step=0.00
0802 18:56:47 ds4-server: continuous batch ctx=350000 path=cont served=1 fallback=0
ds4: M2-Inc2a q8_1 activation fold active (mmvq decode)
ds4: CONT_MTP_ACCEPT(DSpark) D=4 steps=43 emit=127 drafts=112 hits=84 accept=75.0% tok/step=2.95
0802 18:57:05 ds4-server: continuous batch ctx=350000 path=cont served=1 fallback=0
ds4: CONT_MTP_ACCEPT(DSpark) D=4 steps=56 emit=127 drafts=116 hits=71 accept=61.2% tok/step=2.27
0802 18:57:19 ds4-server: continuous batch ctx=350000 path=cont served=1 fallback=0
ds4: CONT_MTP_ACCEPT(DSpark) D=4 steps=45 emit=127 drafts=113 hits=82 accept=72.6% tok/step=2.82
0802 18:57:33 ds4-server: continuous batch ctx=350000 path=cont served=1 fallback=0
0802 18:57:50 ds4-server: fork admit src=1 dst=3 cached=9269 suffix=10
ds4: CONT_MTP_ACCEPT(DSpark) D=4 steps=20 emit=40 drafts=21 hits=12 accept=57.1% tok/step=2.00
0802 18:58:00 ds4-server: continuous batch ctx=350000 path=cont served=2 fallback=0

Same thing happened to me as well, after upgrading to latest version speed dropped drastically.

Is base-only VMM the correct setup for one DGX Spark?

Hi @entrpi,

I managed to run DeepSeek-V4-Flash-0731 on a single DGX Spark using:

ds4 v0.5.2
Base model: VMM weight server
  --scope base
  --backend vmm
  --reserve-gb 8

DSpark drafter: host mmap
--no-mtp
ctx=278528
DS4_SERVER_COALESCE_MAX=8

This setup is currently working.

However, without the VMM weight server, the model cannot load at all. Even with only ctx=4096, the NVIDIA driver reports several errors like:

NVRM: Out of memory [NV_ERR_NO_MEMORY]
_memdescAllocInternal

At that time, the host still had around 120 GiB of MemAvailable, and there was no Linux OOM-killer event. So it looked like an NVIDIA allocation problem rather than normal system RAM exhaustion.

Using base-only VMM fixed the loading problem, while the DSpark drafter remained on host mmap.

Is this the intended way to run the model on one DGX Spark, or is VMM only working around an allocation issue?

I am also concerned about the remaining memory. While the model is running, I sometimes see only around 1.2 GiB free and the desktop becomes less responsive.

Could you please confirm:

  1. Is base-only VMM plus DSpark host mmap the correct topology?

  2. Is it expected that direct loading fails with NV_ERR_NO_MEMORY?

  3. Is this configuration safe for long-running use?

  4. Should I judge safety using MemAvailable rather than MemFree?

  5. What minimum MemAvailable would you recommend?

  6. Is a 1 GiB RamGuard floor too low?

  7. Should I reduce context or DS4_SERVER_COALESCE_MAX to leave more headroom?

I prefer stability over maximum context. I mainly want to confirm that this setup is correct and not forcing the machine to operate too close to its memory limit.

@entrpi thanks for posting this. Used Hermes in spark studio using codex to create a recipe to use on spark studio, gave it this forum and told it to build me a running recipe:

This is the current recipe I am running, using max_model_len: 131072 but you can modify it to your liking. In practice if you are running spark studio, just save this as a YAML and drop it in llama.cpp tab:

recipe_version: โ€œ2โ€

name: DeepSeek V4 Flash 0731 โ€” ds4 Fast Agent

model: antirez/deepseek-v4-gguf:DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2-imatrix-0731

runtime: llama-cpp

min_nodes: 1

max_nodes: 1

container: Package dgx-vllm-eugr-nightly ยท GitHub

metadata:

description: >-

Entrpi ds4 v0.5.3 on one DGX Spark/GB10 with the 0731 IQ2XXS/Q2K

imatrix quant and matching Q2K DSpark drafter. Continuous batching,

prefix/KV reuse, OpenAI-compatible chat and Responses APIs, reasoning,

and native DSML function/tool calling for daily agent workloads.

maintainer: Spark Studio local recipe

model_params: 284B

model_dtype: iq2_xxs

model_vram: 88

kv_dtype: fp8

mods:

  • mods/ds4-deepseek-v4-0731

defaults:

port: 8000

host: 0.0.0.0

tensor_parallel: 1

max_model_len: 131072

ds4_ref: v0.5.3

env:

DS4_CONT_MTP_MODE: โ€œ2โ€

DS4_CONT_DSPARK: โ€œ1โ€

DS4_DSPARK_MODEL: /cache/huggingface/ds4-assets/DSpark-drafter-Q2K-Q8-0731.gguf

DS4_SERVER_COALESCE_MAX: โ€œ12โ€

command: |

/cache/huggingface/ds4-engine/ds4-server \

--cuda \\

-m {model} \\

-c {max_model_len} \\

--host {host} \\

--port {port}

benchmark:

framework: llama-benchy

pp: [2048]

depth: [0, 4096]

prefix_caching: true

Your setup works but it is a workaround, not the intended topology. The weight server exists for multi-node; single-box, the engine should load directly. NV_ERR_NO_MEMORY with 120 GiB available is a driver allocation failure, not RAM exhaustion, and I want to see it: please post the first ~30 lines of a failing direct boot (the lines around โ€œregistered โ€ฆ model mappingโ€ or its absence) plus your driver version from nvidia-smi. On your questions:

  1. Base VMM + drafter mmap works, but adds broker moving parts you should not need. I want to fix the direct load instead.
  2. No, direct loading failing there is not expected. 3-5. Judge by MemAvailable, not MemFree. Your 1.2 GiB steady state is too tight: the desktop dies first and takes the session with it. I would keep several GiB available under load; use DS4_BATCH_FIT_HEADROOM_MB to force it if needed.
  3. Yes, 1 GiB is too low.
  4. Lower DS4_SERVER_COALESCE_MAX before lowering context; each bank costs memory and 8 concurrent 278k slots is a lot for a desktop box.

Memory management is a focus point for next release.

+1 on the DS4_THINK_MAX_MIN_CONTEXT override idea from @GaelicThndr. On a 128GB box running 256k ctx with two banks there isnโ€™t really room to jump to -c 393216 just to unlock high/max, context buffers alone go from about 5.9 to 8.8 GB before the bigger per-bank reservations. An env override letting the prefixed levels engage at lower -c would let us pick effort per request and accept the output budget tradeoff knowingly. Happy to test it on the Ascent, same box as the truncate report in the other thread.

I have this implemented, will be in the next release later today.