Skip to content

dsv41 Zig: the TFE draft head (TF_DS_DRAFTER=eagle), off by default - #42

Open
Elevatormusic wants to merge 12 commits into
dsv41-zig-devfrom
dsv41-zig-eagle
Open

Elevatormusic wants to merge 12 commits into
dsv41-zig-devfrom
dsv41-zig-eagle

Conversation

@Elevatormusic

@Elevatormusic Elevatormusic commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner

What this changes

Adds an EAGLE-style draft head (the "TFE head") to the DeepSeek-V4.1 Zig engine as a second drafter. It is off by
default; TF_DS_DRAFTER=eagle on both ranks and TF_DS_EAGLE_HEAD=<file>.tfe on rank 0 turn it on. The head runs
on rank 0 only and sends its drafts to rank 1 in the verify frame, so a draft pass needs no collective. In eagle mode
no rank loads DSpark. Stacked on dsv41-zig-dense (the dense-linear commits the serving image is built from).

  • zig/src/families/dsv41/eagle.zig: head file reader, rings, prompt and round absorbs, the chained pass, and a
    self-test against reference vectors (TF_DS_EAGLE_CHECK).
  • zig/kernels/cuda/dsv41/eagle.cu: row kernels (RMSNorm, RoPE, window attention, SwiGLU, top-1 with confidence).
    The GEMMs are bf16 cuBLAS calls with fp32 sums (cublas.linearDenseAdd added).
  • model.zig, lanes.zig: the drafter switch, tap capture (taps read from the head file: layer inputs after
    Engram, and tap 1000 = the final state), the draft pass in the existing rank depth policy.
  • TF_DS_ACCEPT_LOG=N: drafts asked and kept by window position on stderr, for either drafter.
  • tools/dsv41-zig/eagle/: .pt to .tfe converter, torch reference and vector writer, kernel test, fatbin
    extractor, and the live check client (tfe_bench.py, tfe_seg.py, tfe_sum.py).

Update (8d8691c..ce9a427): main3x heads, pass kernels, FP8 linears

  • Heads with qk_norm, a window up to 511 and the two_stage vocabulary head (top cand_k low-rank logits
    rescored by a dense bf16 copy of the target's own head in the head file; TF_DS_EAGLE_TOPK up to 512). The
    converter records the copy's source files and sha256.
  • TF_DS_EAGLE_GRAPH=1: a pass's steps as one CUDA graph (off by default; the gain was inside the noise).
  • Pass kernels: an own GEMV for up to 16 rows (TF_DS_EAGLE_GEMV=wo default: wo and w2; all; cublas), the
    attention with a thread a key (window 511 in one block), the vocabulary top-1 split over 64 blocks.
  • FP8 linears (the default when the head file has the FP8 forms; TF_DS_EAGLE_FP8=0 runs bf16, =1 requires FP8): the head's linears in e4m3 with an fp32 scale a row (scale = row absmax /
    448, round to nearest even, from the bf16 weight); the converter writes both forms. Only the draft head's numerics
    change; the target's tokens do not (token_sha below).

Receipt of the update (a build of ce9a427, the same setup as below; head b1 with FP8 forms; nsys medians):

  • Self-test: pass for b1 (bf16 in the 3 GEMV modes, FP8, FP8 + graph) and for a random two_stage head (window
    511, qk_norm) in bf16 and FP8. token_sha equal to "draft": false in every phase, 1 and 4 streams.
  • 5-step pass at 1 stream: 10.29 ms before, 8.73 ms (bf16, default), 4.96 ms (FP8). Step at 4 streams: 1.53 ms
    (bf16), 0.76 ms (FP8; its weights read at the bandwidth roofline).
  • Decode, prose, 1 stream: TFE b1 bf16 35.1, FP8 39.4 tok/s; drafts off 42-44; DSpark 57.5. b1 keeps 38% of its
    drafts, so the pilot head, not the pass, is now the limit.
  • Kernel test (sm_86): the new kernels against torch, all within bounds. Host tests: 1184/1186 pass, 2 skipped.

Update (a300c33, 1513312): FP8 by default, 4-tap heads, the tap dump

  • FP8 became the default after the draft head's offline acceptance check on main3: position-1 acceptance moved by
    -0.003 to +0.005 on every tag (the rule: at most 0.01). A head file without FP8 forms runs bf16 as before.
  • 4-tap heads with tap_norm (a layer-2 tap): already supported; eagle_rand_head.py makes a random head of any
    shape, and eagle_ref.py cross against the training code passes on one (worst 2.0e-6, drafts equal).
  • TF_DS_EAGLE_TAPDUMP=FILE and tfe_tapcheck.py: a check that a tap below the middle layer is read at the
    prompt chunk's replay cut. Not yet run on the pair.
  • Host tests: 1184/1186 pass, 2 skipped.

Receipt

  • Environment: this branch (09276c39, same tree as the measured build) cross-built with Zig 0.17.0 for
    aarch64-linux, the dense image's kernels plus dsv41_eagle (nvcc 13.4, sm_121); 2x DGX Spark (GB10), TP=2;
    DeepSeek-V4.1-Flash-1017, EXL3 2.9 bpw pack; pool = context 262,144, eager decode, 4 streams; head b1 (271.7M
    parameters, taps 26, 37, 39).

  • Exactness: token_sha against "draft": false, 24 groups (prose, code, tool calls x 4 prompts x temperature
    0 and 1.0 with a seed, 1 and 4 streams): one value in every group across TFE on and off, DSpark on and off, and
    the stock image's binaries.

  • Decode speed (aggregate tok/s of a batch): drafts off / DSpark / TFE b1.

    • prose, 1 stream: 43-45 / 56.6 / 33.8
    • code, 1 stream: 43-45 / 90.8 / 35.9
    • prose, 4 streams, t0: 91-93 / 102.4 / 84.2
    • code, 4 streams, t0: 94-96 / 145.2 / 87.1

    The pilot head b1 is slower than drafts off on 8 of 9 workloads. Its acceptance is lower than DSpark's, and its
    pass costs about 14-20 ms (estimate from round accounting) against DSpark's 4.3 ms. This PR adds the drafter
    (off by default) for the trained heads that follow. DSpark speed on this build equals the stock image to 0.3%.

  • Prompt speed: n/a. Eagle mode adds tap copies and one fc GEMM per prompt chunk only when it is on; with it off
    the prompt path is unchanged.

  • Tests run: zig build test-cuda-host (Linux x86_64 CPU): 43/43 steps, 1184/1186 tests pass, 2 skipped. Engine
    self-test on the pair: every draft equals the torch reference, step-1 state error 0.4-1.2%. Kernel test against
    torch (sm_86): all within bounds.

  • Not run: Metal and the other CUDA families (the code is DSv4.1-only and off by default). Graph decode with the
    TFE head (eager only). Thinking and structured-output workloads.

Checklist

  • Every token still goes through the lane rounds. No serial path, and nothing that needs drafts off.
  • Drafted output equals "draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. (Drafted = "draft": false and 4 streams = 1 stream: checked. A resumed prompt with the TFE head: not checked.)
  • No precision traded for speed. If the bits change, the description says which sums change and why.
  • A new or changed GPU kernel is registered with the kernel contract (docs/kernel-contract.md); each backend's SUMMARY line is in the receipt. (Not done: dsv41_eagle follows the other dsv41 kernels, which are not in the contract either.)
  • Prompt processing is no slower than the last release. (TTFT of the 1-stream requests: 0.10-0.13 s with TFE and with DSpark.)
  • The description says which platforms I ran, Metal M1 to M5 and CUDA, and which I could not.
  • New tests fail before the change, pass after it, and skip cleanly without their dependency. (The slot bookkeeping test is new; the GPU checks need a head file and run by env.)
  • Comments and docstrings are one line. No measurements or history in the source. (No measurements or history. Some doc comments run over several lines, as in the surrounding dsv41 files.)
  • No personal data, machine names, internal hosts or local paths. No AI attribution lines.

An EAGLE-style draft head trained on the target's taps (the EAGLE study's
eagle_train.py Head) drafts in place of DSpark: eagle.zig (rings, absorbs,
the pass), eagle.cu (its row kernels), the converter and the reference
(tools/dsv41-zig/eagle), and the self-test (TF_DS_EAGLE_CHECK). Rank 1 runs
no head: rank 0 sends every draft in the verify frame.
…rnel test

TF_DS_ACCEPT_LOG=N writes one JSON line on stderr each N verify rounds:
the drafts asked and kept at each window position (a draft is kept when
it and every earlier draft equal the round's draws), for the TFE head and
DSpark alike. eagle_kernels_test.py checks eagle.cu's kernels against
torch on any CUDA GPU.
…ck client

An absorb of more rows than the ring wrote two rows to one ring row in one
scatter, and either one could stay: the self-test's case q0 = 160 (160 rows,
a 144-row ring) failed its 3% bound. The self-test now absorbs at most a
ring of rows a time, and absorb refuses a larger span. Serving absorbs at
most a ring of rows, as before.

tfe_bench.py runs prose, code and tool-call prompts at 1 and 4 streams with
drafts on and off and compares token_sha; tfe_seg.py splits the engine's
TF_DS_ACCEPT_LOG lines into the drafted batches; tfe_sum.py tabulates a run.
@Elevatormusic Elevatormusic added the type: feature new code (feature, tool, add-on) label Oct 24, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T23:13:29.246616Z 1acd8f8 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 73084ac8f5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tools/dsv41-zig/eagle/eagle_ref.py Outdated
…ocabulary head

A head file now carries qk_norm (an RMSNorm on each query and key head
before RoPE), head_type and cand_k. A two_stage head takes the top cand_k
low-rank logits (fp32, a tie at the k-th value by the lower token id) and
drafts the argmax of their exact logits from the target's own head, a dense
bf16 copy in the head file on rank 0 (eagle_convert.py --target-head: the
head dump files, their sha256 and shapes in the header). TF_DS_EAGLE_TOPK
sets the candidates (1 to 512). The window goes up to 511 (512 keys), and
a prompt chunk gives the head its last ring of rows.

New kernels: tf_eagle_topk_kernel, tf_eagle_rescore_kernel, and the top-1
kernel through an id map. eagle_ref.py follows the same rules and checks
its drafts against the trainer's two_stage logits.
The steps of a pass (each step's layer, logits and draft for every stream)
are captured once a (streams, steps) and replayed; the tables they read are
uploaded before, at fixed places. The extend of the real rows stays eager.
Off by default.
…nears

The profile of window tfe-1792838409: a 5-step pass is GPU-bound (10.1 ms
busy of 10.3 ms); cuBLAS's few-row kernels read wo and w2 at 165 GB/s, the
attention takes 100 us at window 128 and the top-1 51 us on one block.

- eagle.cu: tf_eagle_gemv_{bf16,fp8}_kernel for up to 16 rows (out bf16,
  fp32, or added to bf16); the attention with a thread a key and the
  values split over the warps (window 511 in one block); the top-1 of a
  vocabulary row in 64 parts and a join.
- eagle.zig: Lin linears; TF_DS_EAGLE_GEMV=wo (default: wo, w2 through the
  own GEMV) | all | cublas; TF_DS_EAGLE_FP8=1 loads the e4m3 forms with a
  row scale (own GEMV only); the loader uploads only the tensors in use.
- eagle_convert.py writes each linear's FP8 form (s = amax / 448, e4m3
  RNE) and its sha256; eagle_ref.py --fp8 vectors and fp8cmp.
- eagle_kernels_test.py covers the new kernels; eagle_kernels_time.py.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a300c333e6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +907 to +909
const total = @min(ch.n, eg.ring);
const cut = ch.start - start0;
var at = ch.n - total; // (the rows before: no window reaches them)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve enough prompt taps for the draft head's window

For a long prompt with a supported 511-token head window, this absorbs only the target's replay suffix, not the head's full context. Lanes.fillFn sets replay = len -| m.cfg.window (128 tokens), and prompt.replayCut trims ch.n before this function runs; earlier encoder-only chunks never reach this absorb. Consequently, a 4096-token prompt initializes only 128 head rows instead of the 512 keys required by its attention window, and Eagle.pass silently clips attention to that truncated history. The initial draft chains therefore compute different outputs from the full-window reference, potentially depressing acceptance for hundreds of generated tokens. Initialize the head's required prompt history, or explicitly reject unsupported window/replay combinations rather than silently truncating them.

Useful? React with 👍 / 👎.

@Elevatormusic Elevatormusic Oct 24, 2026 •

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Known limit, now explicit in 05862c8. The rows below the replay floor (prompt length - sliding_window 128) do not run the layers the taps come from (26, 37 and the final state), so the head cannot get them without a longer replay. A longer replay changes the target's prompt path and its compressed-layer snapshots, so it is a separate change. For now Eagle.open logs a warning for a head window past the model window, and tools/dsv41-zig/eagle/README.md states the limit. The deployed heads (b1f, main3) have window 128. The EAGLE long run trains with --serve-floor 128, so its heads see the serving history in training; earlier long-window heads did not. I leave this thread open for the reviewer to decide on the longer replay.

…-path tap check

- FP8 is the default when the head file has the FP8 forms (main3 met the
  rule: position-1 acceptance within 0.005 on every tag); TF_DS_EAGLE_FP8=0
  runs the bf16 forms, =1 requires the FP8 ones. A file without them runs
  bf16, as before.
- TF_DS_EAGLE_TAPDUMP=FILE (rank 0): every prompt-chunk and round absorb
  writes its tap rows with the chunk's replay cut (eagle.zig tapDump).
- tfe_tapcheck.py: three prompts that share a prefix and cut at three places;
  the prompt records must agree at their shared positions (a tap below the
  middle layer reads its rows at the cut).
- eagle.zig is split: eagle_file.zig (the head file and Eagle.open) and
  eagle_check.zig (the self-test). Each file is below 600 lines.
- Every comment is one line of 120 columns or less (eagle.zig, eagle.cu,
  cublas.zig, lanes.zig, model.zig). The long design text is now in
  tools/dsv41-zig/eagle/README.md.
- eagle_ref.py (rnd=True) rounds SiLU(g) to bf16 before the multiply and
  rounds each residual add once, as tf_eagle_silu_mul_kernel and
  linearDenseAdd / own GEMV mode 2 do. On main3 the step o changes by
  0.3-2.1% and all 30 drafts stay the same. cross (rnd=False) is the same.
- Eagle.open logs a warning for a head window past the model window (a
  prompt gives the head only its last sliding_window rows).
Base automatically changed from dsv41-zig-dense to dsv41-zig-dev October 24, 2026 19:25
The 2026-10-24 window's dump (taps 2, 26, 37, final; cuts 1605, 1659,
1705): tap 2 differs by 0.0195 (bf16 rounding with other prefill chunk
shapes; a row-offset error gives about 1), and the taps past layer 20
differ by 0.26-0.84, because rows below each prompt's replay floor run no
upper layer. --gate selects the taps that must pass; the limit is 3e-2.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feature new code (feature, tool, add-on)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant