Skip to content

DeepSeek-V4.1 on the native engine: one family, CUDA and Metal (tracker) #847

Open

Description

@ashhart

Thank you to everyone who has put work into DeepSeek-V4.1 on the native engine. @jayleaton's CUDA stack (#635 to #727) is the most complete community port we've had. @akol1 brought the Metal family (#824 to #827) and proposed the layout on #635. @bertholomus and @ecohash-co's port (#559) has served real traffic on four GB10s, and their updates there are some of the most careful measurement anyone has posted here.

This issue is where those pieces come together, the way #832 does for EXL3.

The plan

  • One family package, zig/src/families/deepseek_v41/, with no backend code in it: configuration, weight and pack loading, the Engram tables and token map, cache and history state, and round planning. This is the split @akol1 proposed on cuda: a keyed graph cache and a device arena, off until a family installs them; failed launches name their kernel (DeepSeek-V4.1 stack 2/14) #635.
  • Kernels beside it: Metal in zig/kernels/metal/deepseek_v41/, CUDA in zig/kernels/cuda/deepseek_v41/.
  • Each backend implements the lane core's Backend. Drafts, the prompt cache and concurrency come from the core, so on every backend drafted output equals "draft": false, a resumed prompt equals a fresh one, and concurrent equals solo.
  • EXL3 goes through the one EXL3 module (EXL3 checkpoints on the native engine (tracker) #832). The Metal projection and the CUDA linear and grouped experts become that module's backends.
  • The default boot needs no capture directory. Captured Triton cubins can stay an optional input, in the layout the 27B's capture already uses (packaging/README.md).
  • Carried kernels keep their attribution, and their authors get Co-authored-by lines. We add those when the code lands.

Where the trees differ today

Order

  1. The shared pieces, each as its own increment with our gate, as described on cuda: a keyed graph cache and a device arena, off until a family installs them; failed launches name their kernel (DeepSeek-V4.1 stack 2/14) #635: cuda: a keyed graph cache and a device arena, off until a family installs them; failed launches name their kernel (DeepSeek-V4.1 stack 2/14) #635, fabric: tensor parallelism across CUDA processes (bootstrap, NCCL, mailbox and RoCE collectives, plan link, fail-fast) (DeepSeek-V4.1 stack 3/14) #636, sessions: session memory for paged-KV families (page pool, prefix trie, RAM residency budget, streamed NVMe tier) (DeepSeek-V4.1 stack 4/14) #637, lanes: a family's round planner drives the lane host with prompt pieces, and a halt ends the rounds at a drain deadline (DeepSeek-V4.1 stack 6/14) #662, server: graceful drain and pre-token keepalives (DeepSeek-V4.1 stack 7/14) #663.
  2. The family package, agreed here.
  3. The CUDA backend from DeepSeek-V4.1: host configuration, EXL3 packs and rank weight plans #667 to DeepSeek-V4.1: performance kernels, family images and device bench runners #727 and the Metal backend from DeepSeek V4.1: the Engram token map built in Zig, byte-identical to the release's #824 to DeepSeek V4.1: the lane backend — one stream per slot on the lane core #827, each through its own gates.

Open questions

Please comment here with anything that changes the plan.

Activity

  1. akol1 commented on Oct 14, 2026

    @akol1
    Contributor

    The KV-format question, answered from the reference's own inference code — and it mostly clears our packing, with one item to verify.

    What DeepSeek's reference stores (inference/model.py): every cache row is written by act_quant(..., inplace=True), whose docstring reads "fused quant+dequant back to BF16" — so the stored tensor is BF16 values that passed an FP8 roundtrip (per-32-block scales, e4m3, FP32 scales, no power-of-2 rounding): the window KV at line 707, the main KV at 1042, the decode-path KV at 1062. The index keys are FP4 with 32-block scales (fp4_act_quant(k, fp4_block_size, True), line 552), and the compressed latent is FP4 with 16-block scales (line 760). Nothing is stored as raw fp8 or fp4; the low-bit formats appear only as the roundtrip's midpoint.

    What #827's backend does: window_fp8, main_fp4 (16-block) and index_fp4 (32-block) — the same three roundtrip semantics, but with the mid-step values stored packed rather than widened back to bf16. Since the packed grids hold exactly the roundtripped values, the stored rows are value-identical to the reference's where the rounding matches; the layer-21 same-input capture showed the quantized KV stages at exactly 0, which supports that.

    The one open item: our window rows are stored as plain per-value e4m3, while the reference's roundtrip applies a per-32-block scale before the e4m3 cast — for rows whose block scale isn't 1 those roundings differ. We'll either match the block-scaled roundtrip in the pack kernel or show the two agree on the real rows, and the whole-model gate re-run after that is the test that matters.

    The three answers you asked for: served-side state lives in the family package — agreed. One loader whose plan becomes rank 0 of 1 on a Mac fits both backends as far as we're concerned; the lane backend already takes its rows from the plan. And the four conditions from your #827 comment are picked up: drafts on (the lane core's draft path wired to the MTP drafter), the four files consolidated per this tracker, the whole-model gate passing — we have a concrete lead there, see the cache item above — and the cache defaults per the reference, linked here.

  2. jayleaton commented on Oct 15, 2026

    @jayleaton
    Contributor

    Thanks for opening #847. I agree on one families/deepseek_v41 package with Metal and CUDA operators beside it. I have read the split that supersedes #690 in #824–#827. Here is the file-level direction I propose; this is not a claim that the two loaders already interchange.

    1. One host loader and an optional rank plan. The current config.zig, names.zig, exl3.zig, pack.zig, and plan.zig are host code; I agree EXL3 format handling should use the single module discussed in EXL3 checkpoints on the native engine (tracker) #832. CUDA starts at device upload in load.zig. The EXL3 slice rule returns the whole tensor at world 1, so I would share the header/name/plan layer and keep upload, device forms and residency per backend. Before treating it as the Metal loader, we need a rank-0/world-1 fixture, the kept-dense .weight forms, optional original Engram table sources, and agreement on strict config validation. The existing plan fixtures use world 2. The current build module still imports CUDA/TP/KV; the neutral loader needs its own import boundary.

    2. What overlaps. I have config/topology, EXL3 headers and named TP plans, an Engram host map/hash reader, packed per-rank NVMe table rows, CUDA paged/split KV and session history, index selection, and mHC/CSA2 operators. These are equivalents, not interchangeable files. My map is DSV41EH1; your Zig builder and reader use DSENGR01. The map format and out-of-range-token rule need agreement. I would adopt the Zig producer from DeepSeek V4.1: the Engram token map built in Zig, byte-identical to the release's #824 and give both backends one map contract. My Engram row format is DSV41EN1 (264-byte packed rows), whereas your backend reads the original tables resident. That storage policy should stay behind one family interface. KV ownership and index scratch are backend-specific; shared history/snapshot contracts can sit above them. Both index paths state lower-position tie ordering, but I have not measured their outputs against each other. I do not have backend-neutral host mHC/CSA2 arithmetic to substitute for your independent references.

      The files that collide directly are config.zig, load.zig, model.zig, and kv_state.zig; they need explicit consolidation or backend names before the two branches land. We also need one family build module, rather than parallel dsv41.zig and deepseek_v41.zig build wiring. The named core imports and relative core-file imports must be reconciled so one file belongs to one Zig module.

    3. Served state stays in the family. Main's GLM-5.3 family already keeps backend.zig, engine.zig, snap.zig, and follow.zig under families/glm_dsa. I would follow that pattern for DeepSeek: backend workspaces, KV/index/Engram state, slot state and chat/template/tool code in the family; lane scheduling, verification, generic sessions and halt/drain in the shared core. core/exlr8 is format code, not an example of served family state. A generic EXL3 rank-slice helper could belong beside the format reader, but a core/dsv41 served-state module would put family policy in core.

    I will keep backend qualification separate. The Metal whole-model gate-form discussion in #824–#827 is still yours and the maintainers' to settle; this package proposal neither changes that gate nor qualifies the CUDA stack. Each backend still needs drafted/plain, resumed/fresh and concurrent/solo receipts at its exact head.

    The source repository for the carried CUDA files is jayleaton/deepseek-v41-tensorfold-spark, rather than the frozen upstream Python line. The public kernel import is ed67bc8a, recorded under my public Git identity. The current public tree carries the native copies under engine/zig/zig/kernels/cuda/deepseek_v41/. The older src/tensorfold/... paths and blob IDs in the copy comments are not reachable in the public history I checked; those comments should not imply they came from upstream v0.6.6. This establishes the source repository and public port attribution, rather than proving the original author of every historical blob. The ExLlamaV3 operation-order/design provenance already described on #635 and the third-party notices still apply; they are not erased by that repository attribution.

    The family-neutral host pieces are now #860 and #861, both independent draft PRs with passing host checks and zero new lean findings. The current-head CUDA two-GB10 gate remains unrun. I will keep the CUDA family layers held until that exact-tree evidence is available.

  3. jayleaton commented on Oct 15, 2026

    @jayleaton
    Contributor

    I found the original paths in the Spark repository's exported patches, rather than as tree entries. Both archived patches record Jay Leaton as author.

    • 0001-spark-stack-060.patch contains src/tensorfold/families/glm5_next/spark/l2pf.cu. Reconstructing that new-file diff gives Git blob ace57d9cab362fb7bcc6fa1456d682a4f0433216, matching the copy comment exactly.
    • 0002-deepseek-v41-family.patch contains the DeepSeek CUDA sources. Its archived mhc_cuda.cu is blob c792c1251be6e7e76a2fc96abe3ab279127b9509 and carries Jay's MIT copyright notice. The later native import includes further changes, including the parallel tail; this archived version does not prove the later caa24ba4bb72 blob.

    The repository is the original source, as JJ confirmed. The public main branch holds the patch archives; I still cannot resolve the historical 78b703d commit or dsv41-quant-e2 ref from its advertised branches. My previous statement that the original paths were not reachable should be narrowed to Git tree entries: the paths and the exact L2 blob are recoverable from the public patches. Original patch authorship and later engine-port authorship should be credited separately, with third-party notices retained.

  4. akol1 commented on Oct 15, 2026

    @akol1
    Contributor

    Two decisions are now blocking the landing, and we want to make answering them as cheap as possible.

    1. The whole-model gate form — @ashhart. The evidence package on #827 (the comment above the split links) shows the literal 1% is unreachable: the model's own FP32 sensitivity compounds through depth, every precision-hardening experiment moved the match away from the reference, and the residual 3.05% survived a bit-exact 12-layer sweep. Three candidate forms are sketched there — the floor-relative bound, the per-layer bounds, the pack-quality slope — with the floor-relative bound our pick. Which should the merged series gate on?

    2. The Engram table format — @jayleaton. The 4-bit repack proposal (the memory case: the resident payload drops from 415 GB to about 240 GB, moving the one-Mac story to 256 GB machines) needs your loader's answer to three questions: (a) which of the three candidate formats can your CUDA loader take — the engine's own 4-bit KV packing, plain 4-bit with per-row or per-64-block f16 scales, or EXL3 trellis; (b) is per-row scaling right for the value distribution you see, or is 6/8-bit the right first step; (c) should the repack live in the pull or ship as prepacked hub alternatives. The proposal's full memory math and the format trade-offs are written up; we can post it here verbatim if that helps.

    3. The consolidation sequencing — @ashhart. The four files (config, load, model, kv_state) become one each shared with the CUDA stack per this tracker. Our branch carries Metal-tested versions of all four; jayleaton's carries the CUDA-tested ones. Do you want the consolidation PR to start from ours and absorb his differences, from his and absorb ours, or should the merged contents be drafted here first? The same question applies to the four files' tests.

    Everything else — the sparse-PV fix, the boundary-tie measurements, the drafts-on design — is in the four PRs and their receipts, and does not need a decision to review.

  5. ecohash-co commented on Oct 16, 2026

    @ecohash-co

    Thank you, @ashhart. Glad to fold this into #847.

    Where our tree is: https://github.com/ecohash-co/TensorFold

    Branch What it contains
    dsv41-tp4-p16 What served our production traffic from 10-12 to 10-16. Today's production is this plus the upstream fixes below.
    dsv41-tp4-logprobs dsv41-tp4-p16 plus logprobs and the opt-in MXFP4 prompt path.
    • Both branches sit on @bertholomus's deepseek-v41-zig-tp4.
    • Every change is behind an env switch. With a switch unset, the build is byte-identical to the build before that switch was added.

    Where families/dsv41 is ahead of the #635–#727 and #824–#827 pull requests, as far as we can tell. Each item is measured on 4× GB10, TP4, with the 2D split. Updates 16–27 on #559 have the gates and numbers.

    1. Prompt (prefill) path on the 2D split. This took prefill from ~1.8K to ~4K tok/s. All of it is exact unless marked tier 2.
      • TF_DS_PROMPT_SP=rows2: sequence-parallel rows on the head chunk at any row count.
      • TF_DS_PROMPT_CHUNK=4096: two-chunk encoder passes.
      • TF_DS_EXL3_PROMPT=v2: a 2D node's prompt EXL3 expert GEMMs, with the rotation in the program and a W_q cache.
      • TF_DS_PROMPT_OVL=all,pack3,dn,fxag: the chunk's exchanges run on a comm stream under compute.
      • TF_DS_PROMPT_ATTN=front,isplit,tsel: attention without per-node replicated work, plus a one-launch indexer top-k.
      • TF_DS_PROMPT_ROUTER=own: the MoE router on owned rows, with the served cuBLAS split-K arithmetic.
      • TF_DS_PROMPT_P16=1: owner partials sent as bf16. This one is tier 2, gated by the replay KL measurement.
    2. Engram on long prompts.
      • TF_DS_ENGRAM_PREFETCH=<depth>,scales: reads rows chunks ahead.
      • TF_DS_ENGRAM_SPLIT: column partners split the reads, and this fixes a long-prompt hang.
    3. Fixes for hangs and refusals that only show up under real traffic.
    4. Logprobs.
      • logprobs / top_logprobs (≤20) with token_id, plus forced_tokens teacher-forced scoring at decode speed.
      • It adds no new collectives and costs nothing when off.
      • This is what makes the long-context KL gate cheap to run on any backend.
    5. Opt-in MXFP4 prompt experts (TF_DS_PREFILL_MXFP4_LAYERS, TF_DS_MXFP4_MIN_ROWS).
      • +6% long prefill, but it is not adopted: it failed our pre-declared KL gate (updates 24–26).
      • It's here for reference, not as a landing candidate.

    The KL gate as a receipt. We'd be glad to supply it.

    • Method: a candidate backend scores the reference build's greedy replies teacher-forced. We report mean KL over the shared top-20, plus top-1 agreement, per length bucket from 8K to 256K.
    • Yardstick: the last numerics change we accepted (P16) measures 0.0064 nats overall, and 0.0104 in the 32–64K bucket.
    • Data: our replay contexts are private agent sessions. We can't publish them, but we can run any backend build against them and post the aggregates. The harness itself is generic.

    Upstream fixes, run on four nodes. We folded @bertholomus's #4, #9, #12 and #14 into the served build, on top of dsv41-tp4-p16.

    Results:

    • Output: byte-identical to production on the 12 reference prompts, on the 9 prompt lengths that used to hang or refuse (including 145152, which is the misaligned-vis case), and on all 36 replay items ≤64K.
    • Logs: 0 refusal or lockstep lines. All 40 RDMA rails negotiated a 4096 MTU.
    • Speed: decode within 0.8% and prefill within 1% over two interleaved pairs, i.e. noise.
    • Soak: 3 concurrent streams, 60 sessions, 210 requests, 0 errors, memory flat.

    It has been our production build since 10-16.

    Caveats.

    Happy to split any of this into increments that fit your gate order. Thanks again.

  6. ecohash-co commented on Oct 16, 2026

    @ecohash-co

    Follow-up: the build we now serve is on the fork as dsv41-tp4-sync2. It is dsv41-tp4-p16 plus @bertholomus's #9, #14, #4 and #12, with their authors kept, plus ecd0a6a, which extends #4's vis alignment to the isplit indexer path. The gates and soak are in the comment above.

  7. ashhart commented on Oct 16, 2026

    @ashhart
    OwnerAuthor

    Thank you, @akol1, for separating the whole-model gate, Engram format and consolidation questions, and keeping the measured 3.05% residual visible. Thank you, @ecohash-co, for both updates, the branches and the four-GB10 checks.

    We're keeping one DeepSeek family on the shared lane backend. Your dsv41-tp4-p16, dsv41-tp4-logprobs and current dsv41-tp4-sync2, built on @bertholomus's deepseek-v41-zig-tp4, give us concrete increments to review: prompt overlap, Engram reads, failure handling, alignment and logprobs. We'll bring across reviewed commits with their original authors and credit kept, against #847's gates.

    The reported 1.8K to 4K prompt speed and unchanged output with switches off are useful evidence for your TP4 setup. We still need capture-free default execution and our exactness checks; P16's numerical change needs separate review, and the MXFP4 path that failed its gate stays out. Your generic replay harness and published aggregates would help.

    For @akol1's remaining questions, the whole-model criterion and consolidation base still need a decision. Please post the full Engram format comparison with bytes, scale layout and fidelity evidence; we also need @jayleaton's loader response before selecting a repack.

    Posted by Ash's AI Assistant Synthena

  8. ashhart commented on Oct 17, 2026

    @ashhart
    OwnerAuthor

    Thank you, @akol1, for the pinned whole-model and Engram evidence.

    Infrastructure comes first, followed by common metadata/EXL3 and the CUDA/Metal family with real drafting and state gates.

    @akol1, please spell out both remaining proposals here: which whole-model gate keeps or replaces the 1% NRMS bound, with exact thresholds and long-token greedy controls; and which Engram storage/scales both loaders should read, with pull-time conversion or a pinned prepacked format. Keep the original-versus-repacked bytes and fidelity comparison beside that format proposal.

    We will use those answers to settle the combined gate, preserving both contributors' work. The existing 3.0517% result remains visible; no relaxed bound is assumed.

    Posted by Ash's AI Assistant Synthena

  9. plyldavid-maker commented on Oct 17, 2026

    @plyldavid-maker

    Thanks for the pointers — taken. One more data point for the tracker: on the TP2 recipe (bertholomus/deepseek-v4.1-tensorfold-tp2-2xgb10#25) we hit a >4-stream hang with exactly the signature your "refusal not shared by every rank" fix describes: rank 0 spins at ~96% GPU, rank 1 idles at 0%, no error, restart recovers. Kernel coverage is ruled out (rebuilt kit, same hang); the Python engine runs the same 8-stream load clean. Would 5a3ea61 / a742532 apply to the TP2 line, or is that a separate fix to port?

  10. Arminova commented on Oct 17, 2026

    @Arminova
    Contributor

    DeepSeek-V4-Flash-0731 on one DGX Spark, for #628's 0731 request

    @ashhart, #628 asks for the 0731 checkpoint, and you asked for exact checkpoints and pinned revisions. I have a native CUDA
    engine for 0731 in the Zig tree that runs on one GB10. I'd like to know where it fits in #847's plan before
    porting it to the family layout.

    Checkpoints

    • Weights: 0xSero/deepseek-v4-flash-0731-spark at
      22f28d32b9b29b4352eaa380ff8c2c170b2847ab: the routed experts are EXL3 3.0 bpw, REAP-pruned to 216 of 256 per
      layer, and the checkpoint's MTP stages serve as the drafter. The engine reads the TP1 pack that the checkpoint's own
      recipe coalesces from the published TP4 rank slices.
    • Reference model: deepseek-ai/DeepSeek-V4-Flash-0731 at 9e165c30e2704aec5d9d593cce3eebd58bbef1cb.

    What it is

    • A deepseek_v4 family that serves as a whole engine through engine_api: one request at a time, with its own
      prompt cache. It is not on the lane core's Backend yet.
    • It runs all 43 layers at 262,144 context, with DSpark drafts.
    • It is a port of a Python CUDA engine I built inside the Python line's deepseek_v4 family (v0.6.5), against
      DeepSeek's reference inference code. That Python engine was never upstreamed, because the Python line froze (Landing next #286).
    • The Zig engine reproduces it byte for byte.

    Gates on today's main (6ee828c, 1.0.12, plus the family as a branch)

    gate result
    weight digests 30,611 equal, 0 wrong
    greedy decode vs the Python engine, 4 prompts × 256 tokens, serial and drafted, 262,144 capacity identical tokens, plus identical rounds and drafted/accepted counts
    resumed from a kept state, or from rewound live rows, vs fresh (greedy + sampled) identical (check-resume)
    logits at 16K, 128K and 252K depth equal to the reference path
    server: same request to the previous build and to this one, then again from cache identical replies

    So drafted equals "draft": false, and resumed equals fresh. Concurrent equals solo doesn't apply yet, because it
    serves one stream.

    Speed (one GB10, the pack above, context capacity 262,144; ms per decode step)

    depth 1 row 6 rows 32 rows draft prompt chunk (4,096 rows)
    16K 50.3 92.9 205.5 8.9 1,424 tok/s
    128K 52.7 89.7 178.7 9.9 1,129 tok/s
    252K 54.4 92.3 187.8 9.6 ~950 tok/s
    • Filling the context runs 1,490 tok/s at 0-16K and 955 tok/s at 240-252K.
    • Greedy code replies run at about 19 ms/token with drafts (~52 tok/s).
    • Against a straight port of the Python engine, measured in the same process with identical outputs, the 1-row step
      is 1.20× / 1.24× / 1.28× faster at 16K / 128K / 252K, and prompt chunks are about 1.5× faster.
    • The same benches on 1.0.11 and on today's main agree within run-to-run noise.

    Known gaps against #847

    Provenance and credit for every file will come with the code when it's up for review.

    Questions

    1. Should 0731 be a variant inside families/deepseek_v41, sharing the loader and plan with a V4 configuration (43
      layers, 256 experts, no Engram)? Or should it be its own family next to it?
    2. Should I wait for the family package and the lane Backend before porting? Or would you look at a whole-engine
      version first?
    3. Once the layout is settled, would the speed work be useful to the CUDA stack in DeepSeek-V4.1: host configuration, EXL3 packs and rank weight plans #667-DeepSeek-V4.1: performance kernels, family images and device bench runners #727 as separate increments
      with in-process A/B receipts?

    I can share the branch for review when it's time, and re-run any gate on a GB10 at a revision you name.

  11. ecohash-co commented on Oct 17, 2026

    @ecohash-co

    Three items for the tracker, plus a question about the base.

    1. @plyldavid-maker, TP2. 5a3ea61 and a742532 sit on our TP4 line, and neither cherry-picks onto deepseek-v41-zig. They conflict in cuda/aot.zig, tri_index.zig, tri.zig and dsv41.zig, because both build on the captured _index_score / Triton variant tables in our tree.

    Two things that do apply to you:

    2. Replay harness and aggregates, as asked: dsv41-replay-harness.

    • It's one standard-library file, replay_run.py, which runs and compares paired replays against any OpenAI-compatible server. Teacher-forced KL uses forced_tokens and logprobs from dsv41-tp4-logprobs.
    • The README has our calibration. These are 60 private agent contexts, 12 per bucket, comparing P16 against the exact build:
    bucket top-1 mean KL (nats) max KL
    8–16K 0.986 0.0065 1.07
    16–32K 0.986 0.0040 0.16
    32–64K 0.977 0.0104 1.34
    64–128K 0.982 0.0059 1.64
    128–256K 0.988 0.0053 0.55
    • Free-running greedy text matched byte for byte on only 25–42% of items in the same buckets.
    • Our view: a long-context whole-model gate needs a distributional measure like this, alongside the short greedy controls. This is in response to @akol1's open question about the 1% bound.
    • We'll run any candidate build against these contexts and post the aggregates. The contexts themselves stay private.

    3. DeepSeek-V4.1-Flash-1017 (released today). We compared it file by file against deepseek-ai/DeepSeek-V4.1-Flash:

    • Byte-identical: config.json, model.safetensors.index.json (same tensors, same shard map), the tokenizer, and the encoding/ reference (prompt and tool format).
    • Changed: all 48 shards, including the Engram shards 47–48.

    So the family code should carry over unchanged, but derived artifacts are per revision: EXL3 quants, rank caches, and any repacked Engram format chosen here. We'd key the Engram format decision and the qualification receipts to a pinned checkpoint revision, so 1017 can be qualified as its own row.

    Capture-free and base.

    • On our side, the increments that don't depend on captured cubins are logprobs, Engram prefetch/split, P16 (separate numerics review), and @bertholomus's RDMA GID/MTU change, kept under his name.
    • The prompt-path work (SP rows, overlap, router, isplit/tsel) adds compiled .cu files. It still launches the captured attention and indexer kernels, so it follows the CUDA backend.
    • We're preparing a branch off main with the self-contained groups, as small commits that keep their authors. @ashhart, should it target main directly, or wait for the family package base to be chosen?
  12. cwschroeder commented on Oct 17, 2026

    @cwschroeder
    Contributor

    @ecohash-co, one CPU-only finding in the published harness at 2b5d7d70: pair() takes only the intersection of IDs. A missing candidate record can therefore disappear with its entire bucket, and compare --gate returns PASS despite the documented missing-result rule.

    With two synthetic reference items, one short and one in 128–256K:

    Candidate Current gate Proposed fix
    Both matching records PASS PASS
    Long record has a timeout error FAIL FAIL
    Long record absent PASS, long bucket omitted FAIL, long bucket retained

    Patch and CPU reproduction, based on that exact branch tip. The fix compares the union of IDs and counts each missing side as an error. All six CLI tests pass after the fix; the base fails four regressions, including both missing sides and a partially populated bucket. Apply-check and whitespace checks pass.

    The probe uses synthetic local files. I have not rerun the private 60-case calibration or any server/model measurement.

  13. easthambuilds commented on Oct 17, 2026

    @easthambuilds
    Contributor

    DeepSeek-V4.1-Flash-1017: an EXL3 build with published provenance, and two questions on the target format

    DeepSeek published deepseek-ai/DeepSeek-V4.1-Flash-1017 (1bae0e10) yesterday. Its config and reference model.py are the same as V4.1-Flash, with new weights. We are building a 1017 EXL3 that the current dsv41 engines load as they are, and we will document every step so anyone can reproduce or audit it:

    • Source: all 83 files pinned to that commit and checked against the Hub's LFS sha256.
    • Layout: the same as Mia-AiLab/DeepSeek-V4.1-Flash-EXL3-2.9bpw (64ba41b6).
      • mul1 codebook, out_scales = always, Mia's per-tensor K map.
      • The vision tower is kept at 16 bits, along with the bias_vl routing biases.
      • The Engram tables are not requantized; serve with the original shards 47/48.
      • The recipe is generated from Mia's quantization_config.json and index by a script, and its output is deterministic.
    • Converter: Add DeepseekV41ForCausalLM (DeepSeek-V4.1-Flash) turboderp-org/exllamav3#449 at c341a8f5, plus small patches:
      • Keep both n-gram tables unquantized (the PR asserts a single table).
      • Give indexer.wk and engram.wkv a qmap so they are EXL3, because the TensorFold load plan requires them that way.
      • Two V4.1 conversion fixes reported on the PR: calibrate with all experts active (otherwise the shared gate/up Hessian sees only expert 0's tokens), and carry the shared KV pool between modules (otherwise conversion stops at layer 3).
    • Calibration: exllamav3's default mix, 128 × 2048, with every expert active. Mia used a private trace, so our tensors will differ from a straight re-run of theirs.

    When it is done, we'll post its numbers against #847's gates (TP2 first: drafted == serial, concurrent == solo, images) and KL / top-1 against the 2.9 bpw base. Then the recipe, patches and hashes go up alongside the weights.

    Two questions so the next quants line up with the family:

    1. @ashhart, for the common EXL3 path (EXL3 checkpoints on the native engine (tracker) #832) and the family's Engram loader: should a new V4.1 quant keep the current layout (original FP8 tables from shards 47/48), or wait for the storage @akol1 is proposing here? If a prepacked Engram format is coming, we'd rather produce it than a layout the loader will drop.
    2. @jayleaton, your q28-v2 (per-expert widths, agentic calibration) beats the 2.9 bpw base on KL and size. Do you plan a 1017 run? If you would publish the width allocator, or a definition of the calibration set, a second producer could reproduce it on 1017 rather than approximating it from the per-layer counts.
  14. ashhart commented on Oct 17, 2026

    @ashhart
    OwnerAuthor

    Thanks @Arminova, @ecohash-co, @cwschroeder and @tovereast for the pinned runs, replay harness and conversion work.

    We will ship one DeepSeek family on the shared lane core once its whole-model gate passes. Checkpoints such as 0731 and 1017 join that family. A separate one-request engine with its own cache must move inside it after the series is public. Bring your own implementation, not a port of DeepSeek's reference inference code.

    We qualify Mia-AiLab/DeepSeek-V4.1-Flash-EXL3-2.9bpw at 64ba41b6 first. A 1017 checkpoint in that layout is welcome next. Keep the original FP8 Engram tables and scales from the pinned checkpoint. CUDA row packs and the Metal table view share the E4M3/UE8M0 interpretation; derived packs must match that checkpoint and rank split. No replacement distribution format is agreed. @jayleaton's 1017/q28-v2 plans and publication of the width allocator or calibration definition remain open questions for him.

    The contributor base will be the series carried by a release; we will post its commit then. Keep the TP2 refusal work separate from incompatible captured-variant patches, retaining authorship.

    The union-of-IDs correction is right: a missing record must fail the gate. Private aggregates do not replace reproducible controls. The whole-model bound is unchanged; #827's questions to @akol1 still stand.

    Posted by Ash's AI Assistant Synthena

  15. cwschroeder commented on Oct 18, 2026

    @cwschroeder
    Contributor

    DeepSeek-V4.1-Flash-1017 on one M3 Ultra with 256 GB: the Metal stack with the Engram tables read from disk

    @akol1, your Metal backend (#824 to #827, head cfd6f8c3) runs 1017 on a 256 GB Mac when the two Engram tables stay on the SSD. Numbers below, for the Metal side of #847. Thank you for the receipts and gates in the run tool; they made this quick to check.

    Setup

    • Weights: LibertAIDAI/DeepSeek-V4.1-Flash-1017-EXL3-2.9bpw at afc7aa0a (same tensor layout as Mia-AiLab's 2.9bpw pack, so weights.bind takes it unchanged). Engram tables from deepseek-ai/DeepSeek-V4.1-Flash-1017 shards 47/48, sha256 76a05106… / 9dc51433… (the 1017 tables differ from V4.1's; the older ones load silently and give a wrong model).
    • A local patch on top of cfd6f8c3, --engram-stream: the tables are checked like a resident load but not staged; each chunk hashes its row IDs in its own command buffer, reads the distinct rows with uncached preads, and runs the unchanged gather kernel on the staged rows. Only the tensors weights.bind reads are staged (no mtp.*, vision.*). The receipt gate in tf-dsv41-run then allows disk reads up to the counted Engram row bytes and no page-ins.
    • Template: V4.1's chat_template.jinja (1017 ships only encoding/), checked 5/5 against 1017's encoding/tests goldens. 1017's DSML tool calls parse.

    Results (one M3 Ultra, 256 GB, greedy, streamed Engram)

    Check Result
    Load 202.37 GB staged in ~300 s, footprint 211 GB, Metal admission 239.1 GB
    generate, 32-token prompt, 128 tokens coherent reply ending in EOS; disk reads = Engram row bytes (126.8 MB), 0 page-ins, gate passed
    seams, 268 tokens, splits 1 to 256 max logit error 0, 0 bit disagreements at every split
    lanes, 2 slots, 268 tokens, chunk 16 0 bit disagreements against each slot's solo run, gate passed
    Server, --context 32768 chat reply correct; a tool call returns get_weather with {"city":"Berlin","unit":"celsius"} and finish_reason: tool_calls
    Engram read cost 0.66 s of 67 s decode; 0.10 s of 7.1 s prefill (256 tokens)

    Speed, and where it goes

    On the unchanged stack, decode is 1.9 tok/s and prefill about 36 tok/s (256 tokens in 7.08 s grouped, 13.65 s ungrouped). tf-dsv41-run profile on one token without history:

    Stage GPU per token Calls
    attention 223 ms 40
    moe 119 ms 40
    mhc 108 ms 120
    head 3 ms 1
    engram 2 ms 2
    total 455 ms

    From the pack's shard headers a token reads about 8.07 GB of weights (5.01 GB dense, 6 of 384 routed experts per layer), so the bandwidth floor is roughly 10 to 14 ms per token. The mhc row, about 0.9 ms per call for very little arithmetic, pointed at dispatch geometry rather than bandwidth.

    Bit-identical decode changes, measured on the same machine

    Four local changes on top of cfd6f8c3, each with its own GPU bit test that fails on a wrong mapping:

    • dsv41_dense: one output column per SIMD group with loads issued ahead (the mHC mixes, k=20480 n=24, ran on 2 threadgroups); per-column summation order unchanged.
    • EXL3: a two-column matmul for single-row products too small to fill the GPU (wkv, wq_a, wo_b), used up to 128 groups.
    • wo_a: the eight slices as one indexed product, only for up to 4 rows (above that the eight separate calls share each decoded tile across rows and win).
    • MoE: the shared expert as a seventh row of the routed gate/up/down products, only for up to 2 rows.
    Check (original binary vs changed) Before After Bits
    generate, 128 tokens 1.9 tok/s 3.84 tok/s same tokens
    teacher, 268 tokens, chunk 1 (one row per step, with history) 143.8 s 73.1 s logits.f32 byte-identical
    teacher, 268 tokens, default chunk 8.23 s 7.88 s logits.f32 byte-identical
    profile, one token: mhc / attention / moe 108 / 223 / 119 ms 11.7 / 104 / 111 ms (after the first three) same logits
    lanes, 2 slots 22.2 s 16.1 s 0 bit disagreements

    Still far from the floor. Next I'm trying independent projections in a concurrent encoder (bit-identical); split-K would change rounding and stays a proposal.

    Two small findings

    • tf-dsv41-run lanes never reaches its eight-argument form: lanes is also in the six-or-seven list in validArguments, so CONTEXT is rejected and two slots always ask for the full 1M window. Removing it from that list fixes it.
    • The token_map test "the normalizer chain matches the release's recorded behavior" fails at cfd6f8c3 for 가, so a map built at open may differ from the release's for Hangul keys.

    This keeps the original FP8 tables and scales from the pinned checkpoint, as you asked above; only where they are read from changes. The LibertAI pack is a 1017 checkpoint in Mia's layout, so it may also serve the "1017 next" step.

    @ashhart, one question for the family's Metal backend: is a streamed-Engram mode (tables left in their shards, rows read per chunk) welcome as an option, so a 256 GB Mac can serve the 2.9bpw pack? I'll hold the patch and the decode changes until you post the contributor base, then open them against it with these receipts.

  16. 13 remaining items

  17. akol1 commented on Oct 19, 2026

    @akol1
    Contributor

    The restated gate at 35e83655 on the 2.9 bpw pack: the FP64 axis clears its band on the anchor, and no single receipt has passed both gates yet

    @ashhart — the restated form works, and we can put the head's numbers on it now. Short version: on the 8-token anchor the engine sits at 1.0374× the floor F against FP64, inside the 1.05× clear band, where the first whole-model capture sat at 1.6839×. But the receipt that passes residency is from the older arithmetic and sits at 1.1998× F, so the two gates have not yet met on one run.

    Pack, anchor, host. Mia-AiLab/DeepSeek-V4.1-Flash-EXL3-2.9bpw at 64ba41b6, Engram tables from the pinned original checkpoint with token map engram-token-map.bin sha256 b9a7cf9d…, anchor [0, 671, 6102, 294, 8760, 344, 11111, 16] teacher-forced, 129,280-token vocabulary, 8 positions. Apple M3 Ultra (80-core GPU, 512 GB unified memory), one model process machine-wide: a second 413 GB resident process makes the resident gate fail (execution_disk_read_bytes 8,192 on the run that overlapped), so every run cited here is serial.

    The floor, unchanged. FP32 reference chain vs FP64 reference chain, same equations, same encoded weights, no GPU: NRMS 0.028957653279577746 (2.8958%), max abs 1.0826212214313418, 8/8 argmax, 4,208 stage finiteness scans with 0 non-finite values, execution_disk_read_bytes 0 and execution_pageins 0, FP32 356.86 s against FP64 358.54 s. The FP32 instance is byte-identical to the head run's oracle logits (sha256 9698a8aa… on both), so the FP64 chain differs from the head's oracle only in accumulation width. F stays keyed to that pack, token map and reference arithmetic.

    The head under both axes. 35e83655, same pack, token map and anchor, full pack resident (weight_bytes 413,338,642,120), three invocations with byte-identical logits (sha256 f8115d5b…), the same NRMS and max abs in all three, 8/8 greedy agreement:

    comparison NRMS × F max abs argmax
    engine vs FP64 chain (numeric axis) 0.030041220837310405 (3.0041%) 1.0374 0.9798279093 8/8
    engine vs FP32 chain (declared contract, recorded separately) 0.030516712829122484 (3.0517%) 1.0538 1.1435151100158691 8/8
    floor: FP32 chain vs FP64 chain 0.028957653279577746 (2.8958%) 1.00 1.0826212214313418 8/8

    The FP64 figure is derived from the retained head logits against the retained FP64 chain logits over the same 8 ids; the byte-identical FP32 oracle logits are what make those two runs commensurable. The derivation will land as a receipt with the limit change so the number is reproducible from files, not prose. The 1.0374× has 0.036 percentage points of headroom under the 1.05× line, so it is a pass on this anchor only, not a licence to relax the other controls.

    How it got there. The compensated reduction is what moved this axis: 1.6839× on the first whole-model capture (2026-10-13, FP32 stage), 1.1998× on the last receipt that passed the residency gate (2026-10-14, engine-vs-FP32 1.0770×), 1.0374× at head (engine-vs-FP32 1.0538×). Every one of those runs was 8/8 argmax against both chains, and none of the moves touched the floor — the FP32 oracle logits are bit-identical across the three trees back to the first capture. So this axis moved by changing the engine's evaluation, exactly as the policy intends.

    Why no receipt passes both gates yet. The residency gate is a strict equality and the head reads a few kilobytes during execution: 4,096, 24,576 and 12,288 bytes across the three invocations, execution_pageins 0 each, bit-identical numerics and resident footprints that differ by megabytes. The weights are fully resident before execution begins (ready_footprint_bytes 415,894,080,968 at head), so this is not weight traffic; it is consistent with first touch of a file region only the rebuilt tree maps, and we have not isolated which. The order we are checking is lazy command-buffer/shader-library load, then a symlinked pack, then staged weights. Until the equality passes, 1.0374× is a measurement, not a signed gate — the same reason 3.0517% was not quoted as a passing run when the reads first appeared.

    Not claimed. The named controls are not measured on the whole model yet: the 16-token anchor (our oracle CLI caps at 8 ids; the chain handles longer histories, so this is a CLI change), the frozen 268-token replay (about 3.5 CPU-hours on this host, once per checkpoint), and drafted == plain, resumed == fresh, concurrent == solo. The three invocations above are re-invocation reproducibility only; they are not the resume or concurrency equality, and we will not stretch them. qualify.zig still carries the historical 0.01 limit — the floor-relative limit plus its receipt is the stacked change on #826 we described on 2026-10-18, not part of #827 as it stands.

    Sequencing. #1052 carries the NaN-reference repair in math.zig, #1051 the Engram fp64 harness and the non-finite metric; both are ready for the 1.1.2 line, and #1051's three-way gather/gate harness is still the named gap. Real MTP and the whole-model gate follow them, and the release refusal stays until then.

  18. joeynyc commented on Oct 20, 2026

    @joeynyc
    Contributor

    DeepSeek has open-sourced DeepInfer (Apache-2.0), the engine behind their V4.1-Flash production service. It declares the KV cache formats explicitly, so it can answer the KV-format question above with production code rather than inference/model.py. Links below are pinned to 774ce244.

    What DeepInfer stores

    Cache Values Scale Bytes per token Where
    SWA window e4m3, 512 per token UE8M0, one per 32 values, rounded to a power of two (round_sf=True) 528 attn.py:413-428, cache_engine.py:262
    Compressed main e2m1, 512 per token E4M3, one per 16 values 288 cache_common.py:119, default from config.py:303
    Index K (and index Q) e2m1, 128 per token UE8M0, one per 32 values 68 index_attn.py:85-86

    The compressed main cache switches to the SWA format (e4m3 with UE8M0 per 32, 528 bytes) only when a checkpoint's config sets use_fp4_sparse_kv: false. The default is FP4. No cache path applies a Hadamard rotation before the cast. On the prompt path the window rows go through a quant-dequant roundtrip (cast_back=True) and attention reads bf16. On the decode path they are stored packed. That is the same value equivalence akol1 described for #827.

    What this means for #827

    1. Window rows. This settles the open item from akol1's comment: production applies a per-32-value block scale before the e4m3 cast. Plain per-value e4m3 matches neither DeepInfer nor inference/model.py.
    2. A new difference: scale rounding. akol1's reading of inference/model.py is FP32 block scales with no power-of-two rounding. DeepInfer stores UE8M0 scales, which are powers of two by construction, for the window, index and Engram tables. So the reference script and the production engine round window rows differently. The whole-model gate should name which one it targets. I'd suggest DeepInfer, because it is what the official API serves. One caveat: the exact power-of-two rounding rule is in tile_kernels.quant.per_token_cast, an external package, and I have not checked whether it rounds up or to the nearest power.
    3. Compressed main cache. The 16-value block size matches DeepSeek V4.1: the lane backend — one stream per slot on the lane core #827's main_fp4. The scale is E4M3, not UE8M0. The reference formula is sf = e4m3(clamp(amax / 6, min=e4m3_min_positive * 6)) (cache_common.py:339-345). If DeepSeek V4.1: the lane backend — one stream per slot on the lane core #827 stores a different scale type here, this is a second place to align.
    4. Index cache. FP4 with UE8M0 per 32 values matches DeepSeek V4.1: the lane backend — one stream per slot on the lane core #827's index_fp4.

    Two more pointers for open items on this tracker

    • Engram map contract (DSENGR01 vs DSV41EH1). DeepInfer's hash is NgramHashMapping. Table sizes are successive primes from sympy.nextprime, and the multipliers are odd values drawn from a seeded RNG. FP8 tables carry UE8M0 scales packed four to an int32 on CUDA (engram.py:27-32, engram.py:173-188). Whichever map format lands, a fixture generated from this code would let both loaders prove they agree with production.
    • Keyed sampling. DeepInfer samples by hashing the sequence seed with 3 * position + case through SplitMix64 and inverting the fp64 CDF, with a pure-PyTorch reference in random_sampling.py:70. If the family's keyed sampler used the same function, a seeded request would pick the same tokens as DeepInfer.

    Caveats

    • DeepInfer targets deepseek-ai/DeepSeek-V4.1-Flash-1017. The DeepSeek V4.1: the lane backend — one stream per slot on the lane core #827 and DeepSeek V4.1: the FP32 and FP64 oracles run side by side #1052 receipts pin the earlier DeepSeek-V4.1-Flash@2cba9e42. I have not checked whether the cache formats differ between the two revisions.
    • DeepInfer runs only on SM100 and Ascend 950, so it cannot run on GB10 or a Mac. Its golden_token_ids + prompt_logprobs + DEEPINFER_BATCH_INVARIANT=1 path would make a good production oracle for the whole-model gate: teacher-forced logprobs dumped once per checkpoint revision and checked in as fixtures. That needs someone with SM100 access. I'm happy to write the dump tool if that would help.
  19. akol1 commented on Oct 20, 2026

    @akol1
    Contributor

    Thanks — this is the most useful thing to land on the tracker in a while. I checked your reading against 774ce244 and against our Metal backend, and most of it holds; two of your "new difference" items turn out not to be differences, and one of your caveats resolves in your favour. Details, then the two asks I'd like to take you up on.

    Where our backend already agrees with DeepInfer

    I diffed the three layouts against zig/src/families/deepseek_v41/kv_state.zig and the pack kernel zig/kernels/metal/deepseek_v41/kv.metal.

    Cache DeepInfer 774ce244 Our stride constants Scale values
    Window e4m3, 512/4-per-32 ue8m0, 528 B kv.metal:52 E4M3 with max(a, 6·2⁻⁹)/6 ceil-to-pow2
    Compressed main e2m1, 512/16, e4m3 sf, 288 B main_stride = 288, kv.metal:52 E4M3
    Index e2m1, 128/32, ue8m0, 68 B index_stride = 68, block 32 ceil-to-pow2

    So on point 3: the main cache already stores E4M3 scales, not ue8m0 — kv.metal:52 encodes exactly your clamp(amax/6, 6·2⁻⁹) then casts to e4m3, and only kind == 0/2 (window, index) take the power-of-two path at :58. The 288 and 68 byte strides match your table digit for digit.

    On point 2, the rounding rule you flagged: round_sf is ceil(log2(sf)) implemented as ((bits - 1) >> 23) + 1 - 127 in tile_kernels/quant/common.py (get_sf_and_inv, TileKernels 2.1.0 per requirements.txt). Our kv.metal:58 computes exp = bias - 127 + (mantissa != 0), which is the same round-up-to-next-power-of-two for positive normals. Where it differs is the bits - 1 term: theirs maps an exact power of two to itself, ours does too via the != 0 predicate — but ours is a bf16-truncated amax (the dsv41_bf() on the load), so a value landing just under a power of two truncates down in ours and may round the other way in theirs. That is the one place in the KV path where I think our numbers can legitimately diverge from production, and it's worth a targeted test rather than a doc note.

    On the cache format default

    Your caveat about config revision is the right instinct, and it matters more than the rest: use_fp4_sparse_kv defaults True (config.py:303), so the compressed main cache is FP4 by default and V41_FORMAT is only selected for the SWA path explicitly (cache_engine.py:257, use_fp4=False hardcoded there). I'd want to confirm the 2cba9e42 config we pinned doesn't set use_fp4_sparse_kv: false, because if it does, the main cache is e4m3/528B and our whole main_fp4 path is testing a cache the production service never builds for that checkpoint. Nobody has checked this. I'll check our side if you can check whether 1017 differs.

    Engram map: your pointer is better than what we have

    NgramHashMapping derives table sizes as unique successive primes from sympy.nextprime seeded off int(vocab_size) - 1, with multipliers rng.integers(0, bound, size=max_ngram_size) * 2 + 1 over default_rng(seed + 10007 * layer_id). Our engram_hash.zig currently hardcodes raw_vocab = 129280, compressed_vocab = 99092 and reads primes/multipliers from a DSENGR01 blob, validating them against a fixed table. We never derived them independently — the blob came from the pack. Your offer to generate a fixture from production code is exactly the check that would turn that constant into a proven one, and it's the single highest-value thing in your message.

    Yes to the fixture. If you generate one, please pin it to a checkpoint revision and include: the raw engram_vocab_size and engram_max_ngram_size from its config, the derived prime list per layer per n-gram, the multiplier list, and the compressed-vocab mapping length. A JSON blob plus a checksum is enough — we'd check it in under zig/src/families/deepseek_v41/fixtures/ and have the loader prove the two agree.

    Keyed sampling: we need to know if these are the same function

    Your random_sampling.py:70 is SplitMix64 over (seq_seed + (seq ^ (seq * 2¹³))) where seq = (3 * position + case) * 0x9E3779B97F4A7C15, then invert an fp64 CDF.

    Our zig/src/core/lanes/sampling.zig:44 is also SplitMix64, same golden-ratio constant, but the composition is mix(seed + GOLDEN) → mix(x ^ position·0xD1B54A32D192ED03) → mix(x ^ id) with a separate id, and the uniform is an open (0,1] at x >> 11 · 2⁻⁵³ + 2⁻⁵⁴. That is not the same bit-mixing from the same inputs. Same primitive, different function.

    This matters because if it were the same function, a seeded request would pick identical tokens across both engines and we'd get a free cross-engine equivalence test at the sampler. As written it isn't, so we can't claim that. I'd rather know which way you want it: does anything upstream depend on DeepInfer's exact keyed-sample bits being reproducible, or is it free to be engine-specific as long as it's batch-invariant? We can match yours bit-for-bit, but only if drifting our sampler is worth something.

    The oracle offer

    Yes to the dump tool, with one caveat you already named: we can't run it. If you write golden_token_ids + prompt_logprobs dumps at DEEPINFER_BATCH_INVARIANT=1, check them in as fixtures keyed by checkpoint revision, and post the hashes, then our whole-model gate gets a production oracle instead of only our own FP64 reference — which is the thing #827 has been missing. Two requests on the format: teacher-forced logprobs at full vocab for a fixed token sequence, so we can score NRMS without needing greedy agreement; and the exact DeepInfer commit plus config hash alongside each dump, since we've now seen the cache format move with use_fp4_sparse_kv.

    One correction to my own tracker text while I'm here: the receipts on #1052 pin 2cba9e42, and if the 1017 cache formats differ, those receipts describe a checkpoint the production service doesn't serve. That's worth stating plainly rather than letting the pin imply more than it should.

    I'd take the Engram fixture first, the sampler answer second, the oracle third.

  20. akol1 commented on Oct 20, 2026

    @akol1
    Contributor

    Correction and upgrade to my own asks above, after checking the rest of the way through.

    The Engram fixture is no longer needed — I derived it and it agrees. I implemented DeepInfer's EngramLayout.from_cfg + NgramHashMapping rules independently (successive unique nextprime from engram_vocab_size - 1, multipliers default_rng(10007·layer_id).integers(0, bound, max_ngram_size) * 2 + 1) against the released DeepSeek-V4.1-Flash config, and every value our engram_hash.zig hardcodes comes out identical:

    Derived In our tree
    layer 1 primes (first 6) 16000057, 16000079, 16000081, 16000097, 16000121, 16000129 identical
    layer 1 Σprimes 384006168 = engram_num_embeddings[0]
    layer 14 Σprimes 384016682 = engram_num_embeddings[1]
    layer 1 multipliers 76632096046245, 4839876093313, 35959672319349, 73987337458391 identical
    layer 14 multipliers 67716810739261, 51510806800915, 30921347202721, 82619226485591 identical

    raw_vocab = 129280, compressed_vocab = 99092, pad 2, order 4, heads 8, 24 columns per layer also match the config. So the DSENGR01 constants are not just self-consistent, they're correct against production. Scratch that ask.

    use_fp4_sparse_kv resolves in favour of what we already build. The pin we use, DeepSeek-V4.1-Flash@2cba9e42, has no use_fp4_sparse_kv key in text_config at all, so config.py:305's .get(..., True) default applies: production builds the FP4 compressed main cache for that exact revision. Our main_fp4 path is unconditional (attention.zig:253, block = 16, main_stride = 288) and that is the right thing to be testing for this checkpoint. The caveat I raised in my previous comment about our receipts describing an unserved checkpoint does not apply — withdraw it. Worth noting for anyone pinning 1017 later that they should check the same key.

    One real divergence remains, and it's narrow. The window path is e4m3 with ue8m0 scales, and both sides round scales to a power of two via ceil(log2(sf)) — theirs as ((bits - 1) >> 23) + 1 - 127, ours as exp - 127 + (mantissa != 0). Those agree for every positive normal, but our amax is bf16-truncated on load (dsv41_bf() in kv.metal) while theirs is taken from the bf16 tensor directly. A block whose true amax sits just above a power-of-two boundary can therefore round its scale in different directions between the two, which shifts every value in that block by one exponent step. That's the single place I'd expect a whole-model NRMS gap between Metal and production SM100 that is not a bug in either engine, and it's cheap to write a targeted test for: construct blocks straddling 2ⁿ and compare the chosen scale against the reference formula.

    So: Engram closed, cache format closed in our favour, sampler question still open and still the one I'd most like an answer to, oracle dump offer still welcome.

  21. akol1 commented on Oct 20, 2026

    @akol1
    Contributor

    Striking my last paragraph. The window-scale divergence I described is not real — I tested it and it does not exist.

    I said our bf16-truncated amax could round a scale the opposite way from production. That was wrong twice over. First, our pack kernel takes device ushort * input (kv.metal:43), so dsv41_bf() is a bf16→fp32 widen, not a truncation — the input is already bf16, exactly as DeepInfer's cache_common.py:335 asserts. Second, the clamp constants match too: their ('e4m3', False) → 1e-4 and ('e2m1', False) → 6·2⁻¹²⁶ are our 1e-4f and 6.0f * 0x1p-126f.

    With both clamps applied and sampling only bf16-representable amax values across the full exponent range (including subnormals), our exp - 127 + (mantissa != 0) and their ((bits - 1) >> 23) + 1 - 127 agree on 544,094 / 544,094 samples. Zero mismatches, both formats.

    My earlier "diverges" result came from feeding the comparison an amax that isn't representable in bf16 and from omitting our clamp floor. Neither is a state our kernel can be in. Retract point 2 of my first comment as well, and the caveat in the second — the KV quantization path has no known difference from production to document.

    Summary of where this nets out, so nobody has to read three comments to find it:

    • Cache byte layouts: match (528 / 288 / 68).
    • Cache scale types: match (e4m3 for main, ue8m0 for window and index).
    • Cache rounding: match, bit-identical.
    • Main cache is FP4 for our pinned revision: correct, key absent so default applies.
    • Engram primes and multipliers: match, derived independently.
    • Keyed sampler: does not match — different composition of the same SplitMix64 primitive. This is the only open one, and it's a design question rather than a defect.
  22. joeynyc commented on Oct 21, 2026

    @joeynyc
    Contributor

    @akol1 Thanks for checking the layouts, rounding and Engram constants—and for correcting the suspected differences. I’ll treat those as resolved on the evidence you posted.

    The full-vocabulary dump prototype is now available, with a patch, setup instructions, tests and artifact documentation:

    Tool and instructions (pinned revision)

    It applies to DeepInfer 774ce244b87cc0eb8d4b94ab9f5de2ab22049f01. I checked that the patch applies cleanly to that upstream commit and reproduces the tested files.

    • Exports full-vocabulary FP32 logits.npy and DeepInfer-kernel logprobs.npy in token-ID order for a fixed teacher-forced continuation. The normal top_logprobs API caps at 1,000, so the example instruments process_sampling after warmup without changing engine source.
    • Records checkpoint revision, config/tokenizer hashes, engine commit, exporter hashes, runtime settings and output checksums.
    • Validates prediction positions, emitted tokens and every chosen-token logprob against the normal engine output before accepting the capture.
    • Uses batch invariance, no drafts, temperature 1, no vocabulary filtering, eager/synchronous execution and unchunked prefill. It is a diagnostic configuration, not a throughput benchmark.

    Validation so far: 9 CPU tests and 4 subtests pass; Ruff lint and formatting pass. No SM100/model run has been performed, and there are no qualified reference fixtures yet.

    Does anyone with access to a Linux NVIDIA SM100 node—such as B200/GB200—with a working DeepInfer installation and enough memory for the checkpoint and Engram tables want to try the first GPU run? This exporter currently supports one node, CP=1, with configurable EP/DP. It does not have a GB10/SM121, Metal or Ascend capture path.

    For the first run, let’s pin the same checkpoint revision and a short fixed token sequence, run it twice, and compare the array hashes. A completed artifact bundle or a failure traceback would both be useful. The instructions include a CPU-only --check-only preflight. Please review local paths/environment metadata before sharing manifests.

    On the sampler question: this comparison requires no change to TensorFold’s RNG composition. Teacher forcing fixes the trajectory, so we can compare distributions without requiring the two samplers to choose identical tokens. I’d keep TensorFold’s existing seeded/batch-invariant behavior for this work. Raw-logit NRMS and logprob metrics should also be named separately when defining the gate.

  23. EugeneClaw commented on Oct 21, 2026

    @EugeneClaw
    Contributor

    The 1017 side of the use_fp4_sparse_kv question, checked

    Following @akol1's 2026-10-20 note ("I'll check our side if you can check whether 1017 differs" — the 2cba9e42 side is settled in the comment above), here is the 1017 side:

    • deepseek-ai/DeepSeek-V4.1-Flash-1017@1bae0e10, config.json: sha256 8be45ce0476004a3f529fd896115a4a2e800a129ad2d3ec05b16050f52e21879
    • deepseek-ai/DeepSeek-V4.1-Flash@2cba9e42, config.json: the same sha256 — the two files are byte-identical (diff is empty)
    • use_fp4_sparse_kv occurs 0 times in both files.

    So the two revisions carry the same config.json, the key is absent at both revisions, and DeepInfer's default applies to 1017 exactly as it does to 2cba9e42 (deepinfer/hugging_face/config.py:303 at 774ce244: use_fp4_sparse_kv=text.get("use_fp4_sparse_kv", True)): the compressed main cache is FP4 (e2m1, 512/16, e4m3 scale, 288 B) for 1017 too, not the e4m3/528 B SWA fallback. The cache byte layouts @akol1 verified carry over to 1017 unchanged. Nothing differs between the two revisions on this key.

    (We serve 1017 through @BobClawblaw's Metal engine at aee8036c, so this was also a serving-correctness check for us.)

  24. EugeneClaw commented on Oct 21, 2026

    @EugeneClaw
    Contributor

    The three family suites at 4f69fea10 on an 80-core M5 Ultra (the rerun @akol1 asked for on 10-18)

    @tpischke's M5 receipt is against cfd6f8c3a, and the tree has moved twice since (the 1.1.0/1.1.1 rebases, plus the current carry head). Per @akol1's 10-18 ask, the same three suites on the current head:

    • tree: akol1/TensorFold@pr-dsv41-bc-family-11, 4f69fea1070951fe53e9e1fb72e26e6ace66f4ae
    • hardware: M5 Ultra, 80-core GPU, 256 GB
    • Zig 0.17.0, macOS 27.0 (26A428), Xcode 27.0 (27A266a)
    • cold-cache runs (.zig-cache removed first; every Metal shader compiled fresh in the run)
    Suite Steps Tests Result
    zig build test-deepseek-v41 -Dcpu=apple_m1 41/41 78/78 passed exit 0
    zig build test-deepseek-v41-reference -Dcpu=apple_m1 43/43 13/13 passed exit 0
    zig build test-deepseek-v41-weights -Dcpu=apple_m1 3/3 13/13 passed exit 0

    No failing or skipped cases. Raw summaries:

    Build Summary: 41/41 steps succeeded; 78/78 tests passed
    test-deepseek-v41 success
    +- run test 78 pass (78 total) 474ms MaxRSS:35M
    
    Build Summary: 43/43 steps succeeded; 13/13 tests passed
    test-deepseek-v41-reference success
    +- run test 13 pass (13 total) 134ms MaxRSS:12M
    
    Build Summary: 3/3 steps succeeded; 13/13 tests passed
    test-deepseek-v41-weights success
    +- run test 13 pass (13 total) 133ms MaxRSS:7M
    

    Two notes against @tpischke's receipt: the suite-1 count is 78 tests now (77 at cfd6f8c3a), and the reference suite is 13 tests (26 there) — the test sets changed with the rebases; the step counts (41/43/3) are unchanged. One artifact: the runner's first listen-mode invocation prints failed command: … --listen=- and is not adopted; the plain run then executes and the Build Summary above is the authoritative result (exit 0).

    The optional whole-lane step (zig build native test test-golden test-kernel-contract -Dcpu=apple_m1 -Dversion=1.1.0 -j2) was not run: every lane step returned OutOfMemory against the resident ~198 GB serve. Can run in a serve-down window.

  25. EugeneClaw commented on Oct 21, 2026

    @EugeneClaw
    Contributor

    @cwschroeder's two 10-18 findings, re-checked: the Hangul case passes; lanes was a real defect, fixed in #827 (my item 2 below was wrong)

    1. The Hangul 가 token-map case passes. zig test zig/dsv41_token_map.zig at 4f69fea10: all 10 tests passed, including "the normalizer chain matches the release's recorded behavior" with the .in = 가 (U+AC00) / .want = U+1100 U+1161 case. The mismatch @cwschroeder saw at cfd6f8c3 does not reproduce at the current carry head.
    2. The lanes CONTEXT rejection — this item was wrong, corrected below. lanes exists in DeepSeek V4.1: the lane backend — one stream per slot on the lane core #827 and the finding was fixed there, not absent (18272d95f: out of the six-or-seven list, its own arity line accepting 6/7/8 arguments, a regression test at zig/deepseek_v41_run.zig:50-68). I checked the carry tree (4f69fea10) and upstream main — neither has lanes — and generalized past those trees. @cwschroeder's report was a real defect, fixed rather than absent.

    The Hangul case passes in the tests we ran; the lanes item was wrong.

  26. akol1 commented on Oct 21, 2026

    @akol1
    Contributor

    Thanks @EugeneClaw — the 1017 answer is exactly the check I asked for on 10-20, and one of the two reruns needs a correction before it is read as settling cwschroeder's lanes finding.

    The 1017 side of use_fp4_sparse_kv: accepted

    Identical config.json sha256 at both revisions, the key absent from both, and DeepInfer's use_fp4_sparse_kv=text.get("use_fp4_sparse_kv", True) default applying to 1017 the same way. That closes the question: the 288 B compressed main cache and the layouts I verified carry over to 1017 unchanged, and nothing differs between the revisions on this key. Thank you for checking it against both.

    The rerun is real, but it is not against a PR head

    4f69fea10 is pr-dsv41-bc-family-11, which is the combined-carry artifact I put up on 10-19 for review, not any of the five PR heads. It is 108 commits behind #1051 and #1052 and an ancestor of neither. So the three suites are a valid receipt for that tree; they are not a statement about the open PRs.

    I re-ran the same three suites at the current precision-lane head, 8a313b902:

    Suite Steps Tests Result
    test-deepseek-v41 41/41 79/79 passed exit 0
    test-deepseek-v41-reference 43/43 29/29 passed exit 0
    test-deepseek-v41-weights 3/3 13/13 passed exit 0

    The step counts are identical to your 41/43/3. The test counts differ from your 78/13/13 because the test sets moved with the rebases, as you said for @tpischke's receipt.

    One warning on the measurement, since it bit me: zig build's exit status and its --listen line are not the authoritative signal. Two of my three runs printed only failed command: … --listen=- with exit 0 and no summary at all, which reads as "not adopted". Running the cached test binaries directly then gave me two spurious failures — exl3_format.zig's two oracle tests read zig/tests/fixtures/exl3_oracle.bin relative to the working directory, so they fail with FileNotFound from anywhere but the repo root. From the repo root they pass. --summary all is what gives the numbers above, and it is the only reading I would quote.

    lanes: the finding does not reproduce, but for a different reason than stated — and it was real

    The original 10-18 finding from @cwschroeder was precise: lanes sat in the six-or-seven list in validArguments, so its eight-argument form with CONTEXT was shadowed and rejected, and two slots always asked for the full 1M window. That was correct, and it is fixed. At #827's head 18272d95f, lanes is out of that list and has its own line:

    • zig/deepseek_v41_run.zig:50 — the six-or-seven list now reads teacher, generate, attention-sameinput, hc-sameinput, moe-sameinput, margins; lanes is no longer in it
    • zig/deepseek_v41_run.zig:52 — if (std.mem.eql(u8, mode, "lanes")) return args.len == 6 or args.len == 7 or args.len == 8;
    • zig/deepseek_v41_run.zig:57-68 — a test that asserts the six-, seven- and eight-argument forms are accepted and the five- and nine-argument ones are not; it passes

    So "the mode is gone from both current trees" is not what happened. lanes is present and load-bearing in #827, appearing eight times in that file (arity at :52, the fixture literals at :58-62, and the two is_lanes branches at :200 and :274) and in the history (c500068ed, 1924bfc4a). Upstream's zig/src/families/deepseek_v41/run.zig genuinely has no lanes — that part is right — but the conclusion that the shadowing "cannot occur" is not, because the branch where it could occur is #827, and there it was fixed rather than absent.

    I would rather this stayed attached to @cwschroeder's original report: he found a real defect, and "it cannot occur" would retire it on a false premise. One further check for you, if you rerun on a serving head: margins joined the six-or-seven list after 4f69fea10 (zig/deepseek_v41_run.zig:50, 1c810ca73), so a mode list quoted from that tree will look one entry short at the current head.

    The Hangul case: agreed

    Confirmed independently. zig/dsv41_token_map.zig:456 carries the .in = "가", .want = "\u{1100}\u{1161}" case, and test 2 of that file passes at both heads: 10 tests at 4f69fea10, 5 at 8a313b902 after the rebases folded the tokenizer suites in. The case count changed; the case did not. That finding is closed.

    On the whole-lane step

    Not running native test test-golden test-kernel-contract against a ~198 GB resident serve is the right call, and OutOfMemory from lane steps under that pressure is expected. I would not chase it in a serve-down window on our behalf — the no-GPU workflow covers those gates on CI, and the per-suite runs above cover the family.

  27. EugeneClaw commented on Oct 21, 2026

    @EugeneClaw
    Contributor

    Thank you for the correction, and apologies for the miss — I've edited the previous comment to avoid tripping anyone up. At 18272d95f: out of the six-or-seven list, its own arity line accepting 6/7/8 arguments, and a regression test (zig/deepseek_v41_run.zig:50-68). I checked the carry tree (4f69fea10) and upstream main — neither tree's qualification runner has a lanes mode, and upstream has no qualification CLI — and generalized past those trees. @cwschroeder's report was correct.

  28. ashhart commented on Oct 21, 2026

    @ashhart
    OwnerAuthor

    Thank you both for the implementation and review. The release includes DeepSeek-V4.1 CUDA pieces, but serving DeepSeek checkpoints remains refused until the whole-model gate passes; a complete Metal backend is still separate. The numerical, residency, draft, resume and concurrency checks must pass together on the pinned implementation before that refusal can be removed. We are keeping this tracker open and are not claiming a new passing receipt for 1.1.2.

    - Synthema, Ash's AI Assistant.

  29. EugeneClaw commented on Oct 22, 2026

    @EugeneClaw
    Contributor

    The whole-lane step at the 1.1.2 port

    • tree: akol1/TensorFold@pr-dsv41-d-lane, 636030f2a73bda7942b97be37c1eafaab2f9602b — "Port the DeepSeek V4.1 Metal line onto 1.1.2's flat config"
    • hardware: M5 Ultra, 80-core GPU, 256 GB unified memory; macOS 27.0.1 (26A434); Zig 0.17.0, Xcode 27.0 (27A266a)
    • .zig-cache removed, then zig build native test test-golden test-kernel-contract -Dcpu=apple_m1 -j2 --summary all
    Build Summary: 420/420 steps succeeded; 5909/5931 tests passed (22 skipped)
    

    Exit 0. At 4f69fea10 the same step was 378/378 steps and 5106/5128 tests.

    stderr carries other families' refusal controls (lane L0.att.o: LaneRowsNotFours, lane L21.gdn.in: OutOfMemory, from pack_lane_test.zig and load_admission_test.zig) — exercised expectations, not failures.

    Three family suites, same tree and host: test-deepseek-v41 83/83 (41/41 steps), test-deepseek-v41-reference 26/26 (43/43 steps), test-deepseek-v41-weights 17/17 (3/3 steps), exit 0.

  30. EugeneClaw commented on Oct 22, 2026

    @EugeneClaw
    Contributor

    A checkpoint-identity constraint on the exporter's first-run comparison: no two capture sides can share checkpoint files

    @joeynyc's exporter ask pins "the same checkpoint revision" on both sides. Whatever revision is pinned, a DeepInfer capture and a TensorFold capture cannot come from the same files, and settling that before anyone books the run is cheaper than running it twice.

    • DeepInfer @ 774ce244 reads fp8 only. deepinfer/hugging_face/config.py, in load_hf_model_config: quant_method must be fp8, expert_dtype fp4, activation_scheme dynamic, weight_block_size [32, 32], scale_fmt ue8m0, else ValueError. An EXL3 pack fails at the first key.
    • TensorFold's DeepSeek-V4.1 family reads EXL3 only. zig/src/families/deepseek_v41/config.zig, requireCheckpoint, lines 432-440 — the same on main at da275a36 and on akol1@pr-dsv41-d-lane at 636030f2: quant_method must be exl3 and version 1/1.*, else error.UnsupportedCheckpoint. The fp8 release fails on both counts.
    • @BobClawblaw's Metal family reads affine only. zig/src/families/deepseek_v41_metal/config.zig, lines 146-151: mode must be affine, bits in 2/3/4/5/6/8, group_size in 32/64/128, else error.UnsupportedDeepseekQuantization.

    So the DeepInfer side would come from the fp8 release, a TensorFold-side capture from an EXL3 repack, and mine from the affine conversion. The exporter doc's own rule — that a different checkpoint cannot establish numerical correctness for another — applies to every TensorFold-side capture until a quant-format bridge exists on one side.

    What this does not change: the DeepInfer-side first run is still worth doing on the pinned fp8 release, and the FP32/FP64 chains can be extended to consume it. What it changes: a direct DeepInfer-versus-TensorFold array diff needs either an EXL3 loader in DeepInfer, an fp8 path in the TensorFold family, or an agreed weights-level (KL/NRMS) comparison across formats, as the fp4 study's receipts do. Pinning that decision before the first capture seems cheaper than running it twice.

  31. EugeneClaw commented on Oct 23, 2026

    @EugeneClaw
    Contributor

    Scope: this is about @akol1's fork, not upstream. akol1/TensorFold, branch pr-dsv41-d-lane, commit 3de06533 (2026-10-23T05:26:34Z). Nothing below concerns ashhart/TensorFold main, the CUDA lane, or any merged upstream state. It is posted here because the fork has issues disabled and this tracker covers the DeepSeek V4.1 native-engine work.

    Two failing compiles at 3de06533: the native server, and core's host tests

    Clean checkout, nothing local (M5 Ultra, 256 GB, macOS 27.0 build 26A428, Zig 0.17.0, -Dcpu=apple_m1).

    zig build test at this commit reports 396/402 steps succeeded (2 failed). Those two are the native executable and
    the core-rooted test in the CUDA host graph. zig build native fails the first on its own, 51/54 steps succeeded (1 failed).

    1. The native server — load.zig:6

    zig/src/families/deepseek_v41/load.zig:6:30: error: root source file struct 'tensorfold' has no member
    named 'paced_read'
    error: 1 compilation errors
    Build Summary: 51/54 steps succeeded (1 failed)
    

    Line 6 is const paced = @import("core").paced_read;. In the shared graph core is zig/dsv41_core_family.zig,
    which publishes paced_read at line 11. The compilation that fails resolves core to zig/src/tensorfold.zig — the
    error names that root — and that file exports safetensors (line 4) and checkpoint_metal (line 7) and mentions
    paced_read nowhere.

    The member it wants, in the same style as line 4:

    pub const paced_read = @import("core/paced_read.zig");
    

    Locally the patch that reached 54/54 also carried two comment lines, so it is a three-insertion diff rather than
    literally one line; the export is the member the compile is missing, and paced_read.zig needs only std and sys,
    both already in that module.

    2. Core's host tests — the CUDA host graph has no metal

    zig/src/core/exl3_projection.zig:5:21: error: no module named 'metal' available within module 'root'
    zig/src/core/gpu_counters.zig:3:21: error: no module named 'metal' available within module 'root'
    zig/src/core/gpu_profile.zig:3:21: error: no module named 'metal' available within module 'root'
    error: 3 compilation errors
    

    core/root.zig's test block gained six imports in this commit: exl3_projection, exl3_projection_decode,
    gpu_counters, expert_groups, gpu_profile, safetensors_security_test. Four of the six @import("metal") —
    exl3_projection, gpu_counters, expert_groups, gpu_profile. Only three errors appear because expert_groups'
    single test never touches its mtl binding, so under lazy analysis that file reports nothing.
    zig/build/cuda.zig:29-36 builds that same root with gpu_memory, tokenizer and living and no metal, so
    analysing that test block fails: the imports resolve, but any declaration in those files that reaches mtl cannot
    find the module, and the test binary never runs. root.zig's suite therefore does not execute under zig build test.
    (test-core is fine — it maps metal — but it is a step of its own and is not part of test. Individual steps are
    unaffected: test-kv-pool still runs, its roots are the kv_pool files.)

    To keep that graph compiling it needs an inert metal module — the pattern already in the repository, as
    zig/gemma4_inert_metal.zig does for the Gemma 4 host tests (zig/build/gemma4_host.zig:7), or as
    zig/build/hip.zig:25 stubs hip_kernels. Such a module has to carry what the analysed tests touch: gpu_counters
    reads mtl.counters, gpu_profile builds a Trace holding mtl.Queue, and exl3_projection holds mtl.Buffer.
    expert_groups additionally needs kernel_sources, used by Pipelines.load, which that module does not provide
    either. If instead those four files should not be in the shared test block at all, that is the other way to keep the
    step green.

    test-dsv41-shared builds and passes at the same commit (3/3 steps, 47/48 tests, 1 skipped), so the V4.1 family's
    own suites are unaffected; both failures are in the other graphs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions