Repository navigation
DeepSeek-V4.1-Flash on the Zig engine (CUDA, 2 and 4 GB10): our port is yours to use, plus test time #559
Description
Hi @ashhart,
Thank you for TensorFold. Moving our DeepSeek-V4.1-Flash work onto your Zig engine was the best decision we made this month: on the same two DGX Sparks, the Zig build decodes about twice as fast after a 128K prompt as our Python build did, with the same replies. That is your engine's work more than ours.
In #286 and #299 you said DeepSeek-V4.1 is on the Zig port list, that the design isn't decided, and that you rewrite contributions in Zig yourself. This issue doesn't ask you to merge anything. It offers what we have, in case any of it saves you time, and offers our hardware for receipts.
What exists (public, Apache-2.0, all on v1.0.2)
- Two nodes: branch
deepseek-v41-zig, recipe v0.6.1. - Four nodes: branch
deepseek-v41-zig-tp4, recipe v0.1.1. - Checkpoint:
Mia-AiLab/DeepSeek-V4.1-Flash-EXL3-2.9bpw, served as is, with the Engram tables from DeepSeek's original shards. - One family,
zig/src/families/dsv41, on the shared lane host and server: CSA2 attention with the indexer, mHC, EXL3 experts, Engram, DSpark drafts, images, up to 1,048,576 tokens a request.
What we checked (receipts in the recipes' results/)
- Drafted equals plain, concurrent equals solo, resumed equals fresh; replies equal our Python reference on every gate.
- 2,124 prompt lengths with no failure; images 9/9 alone and 18/18 four at a time.
- Four nodes: four 500K prompts at once equal to each alone, needles 4/4.
Where it does not yet meet CONTRIBUTING.md (so you know before you look)
- The lean checker adds problems over the v1.0.2 baseline (512 → 1,420), and nine files are over 600 lines.
- The CUDA kernels come from a kit captured from our Python line (Triton binaries plus EXL3 cubins), not from source in the build. The kit and its builders are public with a MANIFEST, but this is the part most likely to differ from how you want it.
- It is on v1.0.2;
zig-flashnextis 25 commits ahead. - Our performance receipts are in our own format, not yet
bench_openai.py/bench_concurrent.pywith the 2k/8k/32k/64k token arrays.
What we can do, whichever design you choose
- Rerun anything in your format on 2×GB10 and 4×GB10, against v1.0.2 and vLLM on the same weights, and post the receipts here.
- Split the shared-engine changes (NCCL, the AOT loader, lane host, server parsing; about 1,100 lines) into small pull requests against
zig-flashnext, one change each, if you want any of them, and clean them to the lean checker first. - Take any part as a reference or fixture and drop the rest; credit as you see fit.
Attribution and provenance
The engine is yours: upstream TensorFold v1.0.2, with its LICENSE and NOTICE kept, and the fork's changed files listed in NOTICE. The model math was re-implemented from DeepSeek's MIT inference code and tech report. The weights are Mia-AiLab's EXL3 quant on turboderp's ExLlamaV3 format. The port was written without opening #300 or the other DeepSeek-V4.1 ports, so whatever you take from those stays theirs. tools/dsv41-zig/ATTRIBUTION.md lists every source and license.
If none of this fits your plan, that is fine too. Thank you again for the engine.
Albert Lee (@bertholomus)
Activity
ashhart commentedon Oct 10, 2026
Thank you, @bertholomus, for offering the dsv41 family and test time on two and four GB10s, and for listing its gaps up front. We'd like DeepSeek-V4.1-Flash on the native engine, and this issue is now its tracker (#299 and #479 point here). Three things shape how it can land:
"draft": false, a resumed prompt equal to a fresh one, concurrent streams equal to their solo runs, and prefill and decode against the current release.Open the first shared piece as a pull request whenever you're ready, and we'll review it promptly.
ecohash-co commentedon Oct 10, 2026
Reproduction on a second four-GB10 cluster (@bertholomus's TP4 recipe v0.1.1), with numbers against SGLang on the same nodes
We moved our DeepSeek-V4.1-Flash work from the Python engine (our tuning log is #479) onto the Zig port described here. Thank you @bertholomus for publishing the recipe, the kernel kit and the receipts, and @ashhart for the engine. It came up on our hardware in an afternoon.
Setup. Two DGX Spark plus two ASUS Ascent GX10 (all GB10), one rank per node, RoCE through a switch with PFC. Engine
5940208built with Zig 0.17.0 innvcr.io/nvidia/pytorch:26.07-py3for sm_121.zig build test: 212 of 213 pass; the one failure is the direct-I/O test inside a container, which upstream fixed in 1.0.3 (#486). Checkpoint:Mia-AiLab/DeepSeek-V4.1-Flash-EXL3-2.9bpw, with Engram from DeepSeek's original shards 47 and 48.node_inputs.sh: all four weight files and the token map matchresults/inputs-5940208/inputs.sha256byte for byte (5 of 5).kit/verify_kit.sh: 356 of 356 files matchMANIFEST.tools/gates.py: 7 of 7, including drafted == serial.One change we needed. The RDMA rings' source GID index is hard-coded to
5(model.zigin two places,layer_main.zigin one). On our ConnectX ports the RoCE v2 IPv4 entry is index 3, so we made it a setting that keeps your default:Together with
NCCL_IB_GID_INDEX=3 NCCL_IB_TC=106for our fabric, everything else ran asconfig.env.exampleships.Numbers on our nodes.
MiaAI-Lab/DeepSeek-v4.1-Flash-DGX-Sparks, every performance flag ofdocs/tp4.md). On our fabric it runs about 7-17% below MiaAI's published figures, so read SGLang's columns as our SGLang deployment.Decode is a clear step up: 1.2-1.6x SGLang, and about 1.7x our Python build at c1. The
kit_bench.py decode --set bstructured row came out at 210 tok/s, matching the recipe's 219.5.Prefill is flat at about 1.8K tok/s, below the recipe's 2,778-2,900. We haven't yet found where the difference comes from (prompt content, fabric or settings). On our real-traffic replay (multi-turn agent sessions, cold prompts up to about 160K) that makes the build prefill-bound: c2 148 s vs SGLang 88 s, c4 476 s vs 238 s (SGLang replay from 10-06).
One failure. Two real-traffic cold prompts, of 146,421 and 145,152 tokens, ended after about 73 s:
Your 160K and 500K checks passed, so it looks like a specialization our prompt lengths reach and the kit doesn't cover. We are tracing which key it needs and will post what we find.
What we're working on, on this build: that variant first, then prefill throughput, since that's what real traffic waits on here. From the Python side we have measured, exact prefill levers in #479 that may transfer, such as sequence-parallel rows and overlapping the row all-to-alls. We're happy to rerun anything in your formats (
bench_openai.py/bench_concurrent.py, or the recipe's tools) on these four nodes, against v1.0.2 or a newer base.ecohash-co commentedon Oct 10, 2026
Follow-up: the
_index_scoreMissingTritonVariant at ~145K, root cause and an exact fixBoth failures trace to the same call. After the replay cut at P − 128, layers 20-39 score the last 128 prompt rows against the full prompt length in row blocks of
rb = max(16, min(rows, 2^26 / (4·n))). Blocks of more than 16 rows go to_index_score_rows, which the kit covers well (102 variants). A trailing block of 16 rows or fewer goes to the fp32_index_score(KEYS=0 HAS_BASE=0 HAS_CAND=0 HAS_TMAX=0).That happens for prompts of 131,073-149,796 tokens (rb = 127…112), and again in narrower windows further out, such as 262,145-299,593. Your 160K and 500K checks fall between those windows, which is why they passed.
In that case the launch can need specializations the kit never captured:
ntdivisible by 16 (nnot).ndivisible by 16, and VIS/KB/CAND 8 bytes off 16-byte alignment. This happens when rb is odd: rb = 115 givesr0·8 ≡ 8 mod 16.Roughly half the prompts in that window fail at the end of prefill.
Why a fallback is exact here. For these settings the kit holds three variants with
HAS_BASE=0and no int fixed to 1 in a way that matters:5f4fceb20c670fc9ffa0a0120ddd24All three have identical SASS (
cuobjdump -sass, addresses stripped). The kernel never readsnt,cs,KB,CANDorTMAX. It reads VIS with one 8-byte load, so 8-byte alignment is enough; the KEYS=1 pair9f83460754/45c81d5893differs only in VIS/KB/CAND alignment and is also SASS-identical.So we changed
aot.zig'sSet.find:tri_index.indexScoreallows this only for n, nt, VIS, KB and CAND, and only on the fp32 FP4 path (!keys and base == null).The diff is 3 files, +88/−7:
zig/src/cuda/aot.zig,families/dsv41/tri.zigandtri_index.zig, with a unit test.Verified on our four nodes:
MissingTritonVariantevery time.gates.py7/7.no exact captured variant of _index_score: took 5f4fceb20c… (a specialization its code does not depend on).Not fixed: the keyed layers 2-19 (
KEYS=1 HAS_TMAX=1) can hit the same gap on the last chunk's tail. Therentis read, and the SASS differs after the first ~1,250 instructions, so we didn't relax it; a recapture of those variants would be the right fix.We're happy to send the diff as a PR against
deepseek-v41-zig-tp4if that helps.ecohash-co commentedon Oct 10, 2026
Update 2: prefill on the TP4 2D split — first exact gain (+12-17%), and what limits it
Same four nodes as above, engine
5940208+TF_RDMA_GID+ the_index_scorefallback.Why prefill is ~2K here and flat from 4K to 128K.
wo_aexchange, which gathers from all four nodes though only the column partner's part is used. All of it is on the compute stream, serial with the GEMMs.kit_bench.py prefillgives about 2.05K here against the recipe's 2.78-2.90K, so part of the gap is likely our fabric (100G per node).Three opt-in levers, each with the default left byte-identical. Each arm passed
gates.py7/7 and gave identical replies to the base build on four short prompts (4,224 / 8,192 / 12,345 / 33,000 tokens) and two long ones (156,941 / 146,421), before any timing.kit_bench)TF_DS_PROMPT_CHUNK=4096: two 2,048-row chunks per encoder pass; row-count-dependent steps (cuBLAS SGEMMs, indexer selection) still per chunkTF_DS_PROMPT_SP=rows: sequence-parallel rows on the 2D splitx.wo_abecomes a send/recv with the partner node.n % 64 == 0; head and decoder chunks run as today.A third lever hung. Overlapping two 2,048-row sub-chunks one layer apart, on a second stream and a second NCCL communicator, deadlocked silently on the first overlapped pass.
NCCL_LAUNCH_ORDER_IMPLICITis the env-only cross-check if it doesn't hold.What actually decides real traffic.
Code is on local branches. We're happy to open PRs against
deepseek-v41-zig-tp4for any of it.ecohash-co commentedon Oct 10, 2026
Update 3: SP rows on real traffic, and the overlap result
SP rows (
TF_DS_PROMPT_SP=rows) as the served build.gates.py7/7, and the long-prompt replies are identical to the base build._index_scorefallbackThe overlap lever: the hang is fixed, but it doesn't pay.
NCCL_LAUNCH_ORDER_IMPLICITwould give the same ordering guarantee.Next.
ecohash-co commentedon Oct 10, 2026
Update 4: short-first admission, and a pause
Short-first admission is inconclusive. We tried admitting queued prompts of ≤32K uncached rows before longer ones. It's rank-0 only, exact by construction, and gates 7/7. We ran it as two order-swapped pairs on the real-traffic replay:
The second FIFO run matched short-first, so the first pair's gap was arrival-order noise. Wall time is unchanged, as expected, since this mix is bound by prompt rate. We've left it opt-in.
Where this stands. The served build is the recipe's engine plus three changes:
TF_RDMA_GIDsetting;_index_scorefallback;TF_DS_PROMPT_SP=rows.On our replay that build runs c1/c2/c4 in 245 / 133 / 423 s with 0 errors. Its decode stays 1.2-1.6× our SGLang deployment's. Prompt rate is still the gap.
We're pausing to move the cluster. Next, after the move:
We'll post that here.
ecohash-co commentedon Oct 10, 2026
Update 5: back after moving the cluster. SP rows for the head chunk, and a prompt phase profile
The cluster came back from the move with both RoCE rails at 100G on every node and the same GID table, so nothing changed on the engine side.
TF_DS_PROMPT_SP=rows2extends SP rows to the chunk that holds the replay point, and to row counts that aren't a multiple of 64. Nodes 0-2 own 16·⌊n/64⌋ rows each and node 3 owns the rest; at layer 20 the replay rows go back to every node.gates.py7/7. Replies are identical to the base build on 12 prompts, including new odd lengths chosen to put the cut on owner boundaries (300, 400, 1,500, 2,100, 2,200 and 6,000 tokens).Where a prompt row's time goes now. We added an opt-in per-chunk phase profiler (
TF_DS_PROMPT_PROF=1). It records CUDA events per phase on every rank and syncs once per chunk. These are encoder chunks with SP rows, on all four ranks; the ranks agree within 1%.The head chunk (2,048 rows, replay cut 128) costs about 1.03-1.07 s, of which the decoder layers on the last 128 rows are 114-139 ms.
Next. The two big items are the collectives and the EXL3 expert GEMMs.
ecohash-co commentedon Oct 11, 2026
Update 6: faster EXL3 prompt GEMMs, exact, +5% prefill
TF_DS_EXL3_PROMPT=v2is a new prompt-chunk kernel for a 2D node's routed-expert gate/up and down GEMMs.TF_DS_EXL3_WQ_CACHE, on with v2).Exactness.
_rnops to match the shipped SASS.gates.py7/7, and replies identical to the previous build on all 12 test prompts (300 to 156,941 tokens).Speed (
kit_benchprefill, two reps each; decode unchanged at 154 / 93 / 214 tok/s for code / prose / structured):Phase profile (encoder rows, µs per row, all four ranks):
Collectives are now 35-37% of a prompt row and the largest phase by a wide margin. They're the next target: moving prompt-size exchanges onto the RDMA rings, or sending fewer bytes. We'll also re-try two-chunk encoder passes (
TF_DS_PROMPT_CHUNK=4096), since v2's experts get cheaper per row at 4,096 rows (89 vs 107 µs on the bench).ecohash-co commentedon Oct 11, 2026
Update 7: what the prompt collectives are made of, two-chunk passes revisited, and an exact Engram cut (+2-8% prefill)
Where the comm bytes go. In an encoder-only chunk under SP rows (rows2) and the v2 EXL3 GEMMs, each node receives about 1,029 KB per prompt row, in 124 NCCL collectives per chunk. All of them run on the compute stream, and every one is larger than the RDMA rings' 4.25 MiB slot.
wo_aexchange with the column partnerTwo exact, opt-in comm levers. Each passed
gates.py7/7 and gave replies identical to the reference on all 12 test prompts (48/48 compares).TF_DS_PROMPT_ENGRAM=sp: in an SP chunk the Engram gate only reads a node's own rows, so each node now sums just its owned rows plus the rows its column partner needs.TF_DS_PROMPT_A2A=shift: the owner exchange as three permutation steps instead of one send/recv group, to avoid three senders per switch port at once. Exact, but 3-5% slower, so it's dropped.Two-chunk encoder passes again (
TF_DS_PROMPT_CHUNK=4096). In update 2 this was flat. With v2 the experts get cheaper per row at 4,096 rows (expert phase 90 → 85 µs/row), but comm rises (120 → 133). Net on its own: +5% at 8K, about 0 at 32K and 128K.Combined. These are means of two independent arms each, run in one session; decode is unchanged at 152-154 / 92 / 210-212 tok/s.
kit_benchprefill, tok/sThat last row is now our served config. Since the start of this thread, prefill has gone from about 1.8K to 2.5-2.7K tok/s, with every step byte-identical to the original replies.
Next. Comm is now about 40% of a prompt row, and at about 70% of line rate on a single 100G port, wire efficiency is the remaining lever. The engine has to wait for each exchange today, so we're looking at one of two things:
ecohash-co commentedon Oct 11, 2026
Update 8: comm overlapped with compute, exact, +2-3% prefill
Update 3's overlap lost because it split a pass into two sub-chunks on two streams and two communicators.
TF_DS_PROMPT_OVLkeeps one 4,096-row pass, one compute stream and one communicator. A side stream carries only the NCCL ops (plus two tiny Engram adds), ordered through one event chain, and no NCCL op is added. Three levers (all= all three):engram: Engram layer L's projection depends only on token ids, so it is issued at layer L−1. Its two exchange steps run under L−1's attention and router.xag: the attention-side x all-gather runs on the comm stream. Meanwhilewq_a,wkv,q_normandwq_brun on this node's own rows, then on the rest.wob:wo_bruns first on the other owners' rows. The exchange to the owners then starts on the comm stream whilewo_bfinishes this node's own rows.Exactness: every arm passed
gates.py7/7 and gave replies identical to the served build on all 12 test prompts (120/120 compares, including a build-default arm).Speed (means of two arms each, one session; decode unchanged at 153 / 92 / 211 tok/s):
kit_benchprefill, tok/sTF_DS_PROMPT_OVL=allengramalone (one arm)xag,wob(one arm)all+ comm stream at top priorityProfile (encoder rows, µs/row, same-day pair). It shows why the gain is real but small on GB10: NCCL's kernels share the SMs, so the compute they overlap slows down.
About 30 µs/row of exchange is now hidden, at a cost of about 12 µs/row of slower dense GEMMs. The net is −14.5 µs/row. Most of it comes from the Engram lever. Raising the comm stream's priority made things worse.
Served now: rows2 + EXL3 v2 + chunk 4096 + Engram sp + OVL=all, at about 2.53-2.75K tok/s prefill, every step byte-identical to the original replies.
Next. We'll look at fewer NCCL CTAs (fewer SMs taken from the overlapped compute). The expert-side exchanges (pack and down) are the large ones that can't overlap yet, because their member lists cover all rows.
ecohash-co commentedon Oct 11, 2026
Update 9: checkpoint on real traffic, served build after updates 5-8
This is the same real-traffic replay as update 3: multi-turn agent sessions, cold prompts up to about 160K, the same seeded draw, thinking off. It ran on the served build: SP rows (rows2) + EXL3 v2 + chunk 4096 + Engram sp +
OVL=all.gates.pywas 7/7 first.Other benchmarks:
kit_bench's, and it gains less at 32K-128K. We report it as measured.Where this leaves us. Wall time on this mix is set by prompt rate. To reach SGLang's c2 / c4 wall on our nodes, prefill still needs roughly 1.4-1.6× more. Comm wait is down to about 100 µs/row of about 325, and work is under way on:
We also tried capping NCCL's CTAs (
NCCL_MAX_CTAS2 / 4 / 8) to free SMs for the overlapped compute. It was exact but gave no gain: 8 matched the served build, and 4 and 2 were 3-6% slower. We've dropped it.ecohash-co commentedon Oct 11, 2026
Update 10: the replicated attention work goes, plus expert-side overlap: +6.6% prefill, exact (now 2.78K tok/s at 128K)
Two more exact, opt-in levers this session, tested separately (ZX1, ZT1) and then together (ZC2). Every arm reported below passed
gates.py7/7 and gave replies identical to the served build on all 12 test prompts before any benchmark: 120 + 116 + 84 compares, 0 mismatches.1.
TF_DS_PROMPT_ATTN=front,isplit: stop computing other nodes' rows. Under SP rows each node owns a quarter of a chunk's rows, but every node still ran the attention front and the indexer for all rows and kept only its own.front(17 of 20 encoder layers):wq_a,wkvandq_normnow run on the node's own rows only. qr and kv are all-gathered (3.5 KB/row) instead of x (10 KB/row), andwq_bruns on the own rows while that gather is in flight.isplit(indexer layers 2, 8, 14):idx_wq_b, RoPE, the FP4 quantize, the scores and top-k run on the own rows only, in today's row blocks with today's kernel per block. The selection is then all-gathered.wo_bpartial, the selection and the caches by sha256. All 9 cases match, covering starts up to 127K, uneven owners, a 1-row block intersection, and a 2-row_index_scoretail.tsel), passed the gates but hung on every prompt of 4K tokens or more. It is excluded and being debugged.2. More
TF_DS_PROMPT_OVLwords: the expert-side exchanges.pack3sends the expert pack in two column halves, each under the next half's gate/up or under the rest block.dnruns down and the combine in column halves, so half A's exchange to the owners runs under half B. Halves are whole 128-column Hadamard blocks, so the epilogue and combine read the same values for every element.fxagreplaces the FFN-side x all-gather with a row-partner then a column-partner exchange. On 4,096-row passes the pair's rows are exactly one of the router's 2,048-row cuBLAS runs, unchanged, which runs under the second exchange.Combined (means of three arms each, one session: plain, repeat and profiled; decode unchanged at 152 / 91 / 211 tok/s):
kit_benchprefill, tok/sPrefill no longer falls off with context: the indexer work that grew with context is now split four ways.
This is now the served config. Since the start of this thread, encoder-row cost has gone from about 500 µs to 292 µs, with every step byte-identical to the original replies.
Next:
tsel(top-k is about 9 µs/row at 128K).ecohash-co commentedon Oct 11, 2026
Update 11: two more exact levers, and the prompt path is now bound by the wire
Since update 10 we built and tested two more exact, opt-in levers. Every arm passed
gates.py7/7 and gave replies identical to the served build on all 12 test prompts: 108 + 120 compares, 0 mismatches.TF_DS_PROMPT_ROUTER=own: the router on owned rows. The router was the last per-row work still replicated on all four nodes._rnops. It reads bf16 x and fp16 weights directly, so the two cast launches go too.The one-launch top-k (
tsel) is fixed. The hang in update 10 was a radix-select pass with fewer than 32 bins that no lane claimed, so the loop never advanced. Invariant breaks now__trap()instead of hanging. One-GPU sweep: 1,311/1,311 cases equal to today's chain, from 512 to 524,288 keys. It saves about 1.5 µs/row in the profile.On the cluster both are exact, and they don't move prefill. The table is
kit_benchprefill, mean over 32K and 128K, two reps each, plus a profiled pair.The profile shows why. Per encoder row, the summed per-phase GPU time drops from 296.1 to 283.5 µs, but wall time doesn't follow. With the exchanges now overlapped, the critical path is set by the wire on the slowest ranks (comm wait 115-122 µs/row on ranks 0/1), and compute we remove off that path just becomes more waiting. We're keeping both levers on the branch, off by default, for when comm gets faster.
Next: the wire itself. Each node has one 100G port, and prompt-size exchanges run at about 66-71% of line rate. Before changing more code, we'll measure what NCCL can actually reach on this fabric with nccl-tests: all-gather and send/recv at 1-20 MB, ring vs tree, with and without the second rail.
ecohash-co commentedon Oct 11, 2026
Update 12: the fabric isn't the limit; NCCL settings +2.4%; the real stall is Engram reads on two nodes
What NCCL reaches on our fabric (stage ZW1). We ran nccl-tests v2.21.1 against NCCL 2.30.7, with 4 ranks, one GB10 per node, and the engine's own NCCL env. Busbw per node per direction, at the engine's message sizes:
results/stageZW1/summary.md.Those settings in the engine (ZN2). These are env-only changes on the served build, with the same gates and 12-prompt identity checks as before (all identical). Numbers are mean
kit_benchprefill over 8K / 32K / 128K, two reps each.Decode is unchanged. That last row is now served:
NCCL_IB_QPS_PER_CONNECTION=2 NCCL_MIN_NCHANNELS=16 NCCL_MAX_NCHANNELS=16. The 157K-token long check dropped from 52 s to 45.5 s.Where the rest of the "comm" time goes. We read the per-chunk phase profile by rank (served build, µs per encoder row):
Next.
ecohash-co commentedon Oct 11, 2026
Update 13: the comm gap, measured per collective
We added an opt-in per-collective profiler (
TF_DS_PROMPT_PROF=2). It records a ready event and an end event around every NCCL call on all four ranks, puts the ranks on one clock, and joins them offline. We also added a replay tool that runs the engine's exact NCCL sequence (225 ops per 4,096-row chunk) on the four nodes, either bare, with the recorded arrival skew, or under a synthetic memory/SM load. Replies were still identical with profiling on (gates 7/7, 12/12).Per chunk row, rank 0, served build (µs; "ideal" is the same bytes at the nccl-tests rates from update 12):
So the about 99 µs/row over ideal comes from three things:
By collective (wire µs per row, engine vs bare replay):
What we'll do about each:
pack3). We'll re-profile with update 12's NCCL settings, then try running it un-overlapped or with fewer channels.5 remaining items
ecohash-co commentedon Oct 12, 2026
Update 19: a soak found latent "missing Triton variant" gaps that hang TP4; fixed, and the soak now passes
We ran a 1 h soak of the update-17 build: c3, 60 multi-turn agent sessions, prompts up to 160K. The cluster hung after 12 minutes.
Gap 1:
_index_score.2^24 / n_comp_end. When that is odd (n = 80,809 gives 207), the trailing block of 16 rows or fewer starts at an odd row, soVIS = vis + row*8is 8-aligned and not 16-aligned.TF_DS_PROMPT_ATTN=isplitthe tail rows belong only to node 3. That follower refused the frame, rank 0 didn't, and ranks 0-2 waited in NCCL indefinitely.Gap 2:
_sparse_attn_part._sparse_attn_partwith 8-aligned POS/RBASE/CBASE.len % 2048odd and between 113 and 127, which is 776 lengths up to 200K, the first at 2,161.Fix (
homelab/night5, edf22ac + 79e9c25).*i64parameter's registers through the SASS of every captured cubin._gather_tilesOUT,_topk_selKEYS/OUT and others). Every call site passes a buffer base for them, never a row slice.lockstep_lostand exits (70), so a supervisor can restart it, instead of hanging.Validation.
gates.py7/7, the 157K canary, and the 12-prompt set plus two tail lengths (18,228, 116,704), all identical to the previous build.kit_benchgives 3,602 / 3,883 tok/s and decode 152 / 91 / 209.If upstream's Zig line launches the AOT kit the same way, it likely has both gaps: any isplit tail whose row block is odd, and any replay-cut tail of 16 rows or fewer. Happy to PR the selection rule and the SASS tool.
mimeding commentedon Oct 12, 2026
Hi @ashhart,
Thanks for TensorFold and for making this the DeepSeek-V4.1 tracker. Here is a short status from a three-GB10 setup, plus a question about the best route. Nothing to merge; everything below is experimental and unqualified.
What we're doing
What we've seen so far (12 Oct)
Correctness looks good:
Speed is the problem (single stream, no drafts, from our screening harness, which adds per-step logit copies and hashes, so it is pessimistic):
Our first reading of the cause:
fast_prefillis off because the fast prefill GEMMs change arithmetic.GDR 0on the GB10 HCAs.Questions
dsv41family instead? If so, is a three-rank layout something you'd consider there, or would you rather keep to 1/2/4?GDR 0on GB10: is that expected on GB10, and is there a recommended NCCL setup for a switchless three-node ring?We're happy to share logs, configs or receipts, or to run anything on the three-node setup. Thanks again.
ecohash-co commentedon Oct 12, 2026
Update 20: four prefill levers. bf16 owner exchange adopted, MXFP4 prefill experts go to port, two killed
We now gate changes in two tiers. Tier 1 must be byte-identical to the served build and faster. Tier 2 may change numerics, so it is env-switched and must pass five gates against the exact build:
long_check4×160K.kit_benchand on the real-traffic mix.1. bf16 owner exchange (
TF_DS_PROMPT_P16=1, tier 2): ADOPTED, now serving.long_checkpasses. 0 of 12 prompts diverge before token 32.kit_bench4K+8K +12.4% and 32K+128K +8.0% (repeat pair: +5.3% / +6.6%). Prefill is now about 3.9K tok/s at 4K/8K and 4.1K at 32K/128K. Decode is unchanged at 151 / 91 / 211 tok/s (code / prose / structured).2. MXFP4 routed experts for prefill (tier 2): microbench passes, engine port next.
TF_DS_PREFILL_MXFP4_LAYERS=n, starting at n=16, then run all five tier-2 gates.3. Exact pair-reduce (tier 1): killed before building.
ncclReduceScatteron split pair communicators.4. Retuned down kernel (
TF_DS_EXL3_DN64=1, tier 1): exact, but slower in the engine; not adopted.kit_benchis 5.7-6.0% slower in both pairs (3,611-3,637 / 3,858-3,860, against 3,855 / 4,099-4,116).Code is on local branches (
homelab/p16,homelab/dn64, MXFP4 work in progress). We're happy to open PRs againstdeepseek-v41-zig-tp4for any of it.ecohash-co commentedon Oct 12, 2026
Update 21: the Zig engine is now our production DeepSeek-V4.1-Flash endpoint; next is MXFP4 experts for prefill
Where we are. As of today, the Zig TP4 build from update 20 (
homelab/p16, night5 + bf16 owner exchange) serves all of our DeepSeek-V4.1-Flash traffic through our LiteLLM proxy. That covers the agent route, the deep and reasoning routes, and vision/OCR. It replaces our SGLang deployment on the same four GB10 nodes.What we checked before switching:
chat_template_kwargs(thinking) are accepted as well asenable_thinking, so no caller had to change.Same-day standing against our SGLang deployment:
kit_bench)On agent-style traffic it is at parity at c1-c2 and 11% behind at c4. The remaining gap is prefill.
Plan for the next several days: MXFP4 routed experts for prefill (tier 2,
TF_DS_PREFILL_MXFP4_LAYERS=n).expertPartsand refuses to boot below a memory floor.prompt2dexpert branch. The whole EXL3 chain on a layer (rotation, SwiGLU, down, combine) is replaced, so both column partners switch together.long_check, early divergence on 12 prompts, both benchmarks, and a 1 h soak. We start at n=16, then try n=20.We'll post the gate results when they're in.
ecohash-co commentedon Oct 12, 2026
Update 22: MXFP4 prefill experts are built and fast, but miss our quality floor by one item; not adopted yet
The port. It is the O2 MXFP4 grouped kernel from our Python line, built into the Zig engine behind
TF_DS_PREFILL_MXFP4_LAYERS=n. It covers layers [0, n) of the encoder, on prompt chunks only; decode stays on EXL3.Checks before any numbers changed:
Tier-2 gates at n=16, against the served build (P16):
code_interval_intersect); extension suites all perfectlong_check4×160Kkit_bench4K+8K / 32K+128KOther notes:
Next. A decision on gate 1, and possibly a repeat quality run. Separately, we'll look for the source of the code-decode dip.
plyldavid-maker commentedon Oct 13, 2026
@ecohash-co — Thanks for updates 16–22, extremely useful. I'm running the bertholomus TP2 recipe (v0.6.1, TF 1.0.2 native) on 2× DGX Spark with the same RoCE direct-attach. After applying the recipe's kit fix (58186d1) plus TF_DS_REPLAY_FLOOR=1 / TF_LOOP_GUARD=1, the short-prompt deadlock from bertholomus#16 no longer reproduces (11–13 token probes now return instantly). But we still hit the long-prompt variant gaps: an ~880k-char (~235k token) prompt hangs past 420s with no response byte, which looks like your Update 19
_index_scoreodd-tail /_sparse_attn_part<16-row tail findings. Would you be willing to share the night5 branch (or just commits edf22ac + 79e9c25) for a 2×GB10 TP2 setup? Happy to run your soak/qeval harness and report numbers back here.ecohash-co commentedon Oct 13, 2026
Update 23: the MXFP4 quality miss reproduces; the served build stays as it is
Repeat quality run. We booted n=16 again and re-ran qeval at temperature 0. It scored 71/75 again, item for item the same as the first run. The engine is bit-deterministic, so this was expected. It shows the miss is a stable property of the n=16 numerics, not run-to-run noise. The item it loses,
code_interval_intersect, is one that most builds we have measured fail, the MiaAI reference included. Only the two most recent served builds pass it.Code decode. In interleaved decode-only arms (A/X/A/X, 5 reps each), code decode was 151.8 / 152.1 tok/s on the served build and 147.5 / 147.9 on n=16. That is −2.7%, and it reproduces. Prose and structured decode are unchanged. Our working explanation: the decode prompts are short (23-43 tokens), and their prompt pass also runs layers 0-15 on MXFP4. That gives a different code reply, and with it a different speculative-draft acceptance. On this reading the slowdown comes from the content of the reply, not from the kernel. We have not yet checked this against the engine log.
Decision. We are not relaxing the quality gate after seeing the result, so n=16 is not adopted. Production stays on the P16 build (adopted after QS1), and the MXFP4 switch stays off.
Next. Apply MXFP4 only to long prompts. Prefill time matters most there, and our kit_bench gains were measured there. The plan is a rank-invariant prompt-length threshold, so short prompts (most qeval items, the decode prompts, and typical chat turns) keep byte-identical P16 numerics. Long prompts would still get the roughly 5% prefill gain. It will go through the same tier-2 gates.
ecohash-co commentedon Oct 13, 2026
Update 24: MXFP4 on long prompts only. Quality holds and long prefill gains ~6%, but the pre-declared speed rule fails at 4K, so it is not adopted
Same 16 MXFP4 layers as update 23. The new switch
TF_DS_MXFP4_MIN_ROWS=2048sends a prompt chunk to the MXFP4 experts only when it has at least 2048 rows. The chunk's row count is the same on every rank, and the cross-rank check now carries it.Exactness and quality (tier-2 gates, unchanged):
Speed (kit_bench, interleaved A / L / A2 / L2):
Decision. The rule declared before the run required L to be faster in both pairs and both length groups. Pair 1 at 4K+8K fails it, so this is not adopted and the switch stays off. We are not reinterpreting the result after the fact.
What we're changing in the method. The divergence check and qeval barely exercise long-context numerics: even the all-chunks MXFP4 build matched the served build on our long synthetic prompts. So before the next tier-2 attempt we are adding:
The next MXFP4 run will also give both arms the same warm-up of the MXFP4 path (one ≥2048-row prompt) before timing. That will be declared before the run.
ecohash-co commentedon Oct 13, 2026
Update 25: logprob output lands with a byte-identical build. MXFP4-long passes every old gate and is faster, but fails the new long-context KL gate, so it is not adopted
Logprob output in the engine (
homelab/logprobsb16a910).logprobs/top_logprobs(≤20) on chat and completions, streamed or not, withtoken_idon each entry. There is also a non-standardforced_tokensfield: it scores a fixed continuation teacher-forced, at decode speed. The cost is zero when logprobs are not requested.The full vocab is already gathered on rank 0 for sampling, so no new collectives were needed. Log-sum-exp and top-k are computed on the host in a fixed order.
Identity with production, all byte-identical:
New long-context gate. A replay set of 60 frozen real agent-session contexts (8K–256K tokens, private, so we report aggregates only). The served build's greedy replies (128 tokens) are scored teacher-forced by the candidate. We compare per-token top-20 distributions: KL over the shared top-20, and top-1 agreement.
The gate was declared before the run: mean KL ≤ 0.01 nats in every length bucket.
MXFP4-long result (n=16 layers,
MIN_ROWS=2048). Every timed arm got the same warm-up, also declared before the run.Replay KL by bucket:
Decision. The gate is not relaxed after the result: MXFP4-long is not adopted, and the served build is unchanged.
Next. Calibrate the KL gate against the last numerics change we accepted through the tier-2 gates (P16 vs pre-P16, same replay set). Our MXFP4 numbers are already known, so any MXFP4 call after calibration is an informed judgment, and we'll say so when we make it.
ecohash-co commentedon Oct 13, 2026
Update 26: calibrating the replay KL gate. The last numerics change we accepted (P16) drifts about half as much as MXFP4-long
Update 25's gate (mean KL ≤ 0.01 nats in every bucket) had no yardstick. CAL1 measures one, measurement only. It uses the served build with
TF_DS_PROMPT_P16removed, i.e. production as it was before P16. That build scored the same 60 teacher-forced replay items against the same P16 reference.What this shows:
The update 25 verdict stands: the gate was declared before the run and MXFP4-long failed it. Whether to adopt it with a recalibrated gate is an owner decision. Any new threshold would be chosen knowing MXFP4's numbers, so it would not be a blind gate.
ecohash-co commentedon Oct 13, 2026
Update 27: final state. This push is closed and the Zig TP4 engine is our production DeepSeek-V4.1-Flash backend
What's serving. The Zig TP4 engine on 4× GB10 (2× DGX Spark + 2× ASUS GX10), EXL3 experts, 1M context. It has been our production
flash/deep/vision/ocrbackend since 10-12, replacing SGLang. The config is night5 + Engram prefetch + router own-rows + tsel + P16 bf16 owner partials (update 20). A watchdog auto-restarts it, but only when it is confirmed down.Where it landed vs SGLang on the same hardware (sparkDash, same day, post-move; real mix = our replayed agent traffic):
What was tried and not adopted:
Eval tooling that came out of this:
logprobs/top_logprobs, plus non-standardforced_tokensfor teacher-forced scoring. Zero cost when off, and byte-identical to the served build.Why we're stopping. Our real traffic is mostly long agent sessions:
The remaining levers don't pay on that mix:
We'll reopen if traffic shifts toward cold long prompts or a stronger engine or model appears.
Thanks to @ashhart and the TensorFold authors for the engine and the recipe. This has been running real agent workloads reliably.
ashhart commentedon Oct 14, 2026
Thank you, @bertholomus, for offering the port and test time, and thank you, @ecohash-co, for updates 16 to 25. Running the native engine as a production DeepSeek endpoint on four GB10s, then turning down your own MXFP4 change because it missed a gate you set before measuring, is exactly the discipline we want here.
We've opened #847 to bring the DeepSeek-V4.1 work together as one family, with CUDA and Metal backends on the lane core, following the split @akol1 proposed on #635. @jayleaton's CUDA stack (#635 to #727) and @akol1's Metal family (#824 to #827) are the pull requests it covers.
Your branches are the most-run DeepSeek code anyone has on this engine. Where a piece of
families/dsv41is ahead of what's in those pull requests, please say so on #847. Anything we take from it carries your names as Co-authored-by. Your long-context KL gate would also be a good receipt for whichever backend lands first.Thank you again for all of it.
ashhart commentedon Oct 16, 2026
Thank you, @bertholomus, for the port and test time, and @ecohash-co for the four-GB10 production measurements and the corrected workload comparisons.
#847 now coordinates the shared DeepSeek family, the CUDA and Metal stacks, and the remaining qualification decisions. Your long-context KL receipts and any code ahead of those stacks belong there, with the source authors credited when their work is carried across.
We're closing this earlier offer onto #847 so the integration has one tracker. The branches and measurements in this thread remain useful references.
Posted by Ash's AI Assistant Synthena
mimeding commentedon Oct 18, 2026
TP3 on three GB10s (DGX Spark): DeepSeek-V4.1-Flash, Python engine, our measurements
Posted at @ashhart's request, to show whether TP3 helps our workload. All numbers come from our own runs on 3× DGX Spark (GB10; ConnectX-7 full mesh, RoCE, no GDR) with
dealignai/DeepSeek-V4.1-Flash-EXL3-2.9bpw, on two or more boots unless stated.Stack
c4646171) with jayleaton's G19 family (4b235ad2), plus our own TP3 derivative:wo_agroups 2/3/3), experts width-split 768 per rank;EXPERT_BLOCK ≥ 1463for the gm scratch);TP3 against TP2 on the same boxes and recipe
The 4- and 8-stream cells use the same prompt in every stream. Distinct-prompt runs are still to come.
Prefill (cold, drafts off, one stream, prose/code within 0.5 s)
fullWe have not run our own TP2 at 128K: our 8 GiB memory floor refuses it. The TP2 column is the G19 receipt (same pack, full + FULL_CONE).
Takeaway for our workload
Caveats
Next on our side
We will add those results here.