Skip to content

Living Weights on Qwen3.8 Flash Next (CUDA, GB10): the pocket in every pass, the trainer, and --slide - #896

Draft
ZackSample wants to merge 57 commits into
ashhart:mainfrom
ZackSample:lw-d-fnlearn
Draft

ZackSample wants to merge 57 commits into
ashhart:mainfrom
ZackSample:lw-d-fnlearn

Conversation

@ZackSample

@ZackSample ZackSample commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Stack

This sits on two open PRs, so it opens as a draft until they land:

The branch is lw-c-fnback plus one merge commit of lw-b-core (46627a8), then the 4 commits below (the fourth is docs). That merge is
the only one: the trainer needs the CUDA family from #869 and the learner in zig/src/living from #878, and neither
contains the other. It touched zig/build/cuda.zig (both modules kept) and native/cuda.zig (the host keeps
@hasDecl(F, "slide") until Flash Next declares one). Once #869 and #878 land, I will rebase this onto main and the
merge goes away; the diff to review now is lw-c-fnback...lw-d-fnlearn after the merge, or the 4 commits.

What this changes

  1. A taught folder's pocket in every pass (03767e5). Flash Next on CUDA: the qwen4_exp family on the native CUDA host, every stream through the lane rounds #869 refused a folder whose shared experts carry Living
    Weights neurons. Now the engine packs each shared expert's own 640 rows as before and keeps the pocket's neurons in
    fp32 (cuda_pocket.zig); every pass (prompt chunks, drafted windows, decode rounds) adds them behind the shared gate
    on the layers that have any, through nemotron cuda: skip empty pocket layers and spread the pocket over many blocks #787's two-kernel form ported to this family (tf_fn_pocket_in, then _out;
    a layer with no neuron launches nothing). On such a layer the MoE output is summed in fp32 in MODE 2's order
    (fn_moe_sum), the pocket added, and written back rounded once (the write-back's one-slice MODE 4). Every other
    layer writes back exactly as before, so an untaught folder's bits do not change (receipt below). The pocket's
    kernels are row by row, so drafted equals "draft": false and concurrent equals solo with a pocket too.
    The batched pass can also keep what a backward reads: each read-out's input streams, normed rows, v, up rows and
    inject gates, the mixer's and the MoE's branches, the MoE's input and slots, attention's projections and gated rows,
    the final mixer's read-out. The up rows come from fn_hc_upmix_up, our read-out kernel writing the rows its mix
    reads (the same mixed bits as fn_hc_upmix). A captured comparison set's _qmm_upmix keeps none, so --slide
    refuses a captured set.
    Flash Next on CUDA: the qwen4_exp family on the native CUDA host, every stream through the lane rounds #869's config check refused a taught folder's wider shared expert, which the first restart in the receipt below
    found: cuda_config.zig now takes the width with the pocket's neurons (tensorfold_learned_neurons, a whole number
    of groups, 128 at most), with a test.
  2. The trainer (6db9e3e). cuda_train.zig runs the learner's passes (keys, loss, target) with sequences side by
    side on the engine's own batched pass: cuda_batch_train.zig runs the row-wise launches over every row, and the
    n-gram block, DeltaNet and attention one sequence at a time from position 0 and zeros. The open fact's neurons are
    scaled per sequence, and each nudge joins its layer's MoE output. The loss's gradient then runs from the 4-bit head
    down to the lowest nudged layer with flashnext.fn_back and cuda.gdn_train, reading the engine's packed experts
    where they sit and MLX copies of the small linears (cuda_train_buf.zig). Both files are under 600 lines (the old
    676-line trainer is split in two). zig build flashnext-learn checks it on the GPU (receipt below).
  3. --slide for qwen4_exp on CUDA (0986c5a). living/families.zig lists qwen4_exp: a SwiGLU shared expert at
    every layer behind its own gate, 4-bit groups of 32, keys under text_config, the MTP layer's expert in the index.
    This replaces Living Weights as a core module: the learner, pocket and fits in zig/src/living, a family described as data (Nemotron only) #878's stand-in test family, whose tests now read the real entry. flashnext/living.zig gives the
    layout and writes the pocket through the core's shard writer (pocket_shards_test.zig: the first save widens every
    expert, the MTP head's with zeros, and tells text_config; a later save lands in place; every other byte kept).
    cuda_slide.zig hosts the core learner over the trainer, and the lane backend opens with --slide: the trainer's
    1,024 rows in the pass's scratch, the layout from config.json, and the trainer's bytes kept out of the streams' room.
    The native host's existing rules still apply (--slide on a GB10 only, no --segments, a folder of links refused).
    Macs keep refusing --slide for this model.
  4. Docs (47748f7): the Flash Next CUDA recipe and the Living Weights page (what a GB10 needs for Flash Next, and
    this receipt as measured), and a CHANGELOG line.

Receipt

  • Environment: lw-d-fnlearn 47748f7 (this PR's 4 commits on the merge 46627a8 of lw-b-core 75978a1 into
    lw-c-fnback a5d183d, itself on Flash Next on CUDA: the qwen4_exp family on the native CUDA host, every stream through the lane rounds #869's 7746bb9), Zig 0.17.0; NVIDIA GB10 (DGX Spark, sm_121), driver 580.178.04,
    CUDA 13.0.88, Ubuntu 24.04.5; checkpoint leimroth-lab/Unbound-LR7 revision 2ae3a2ff (Qwen3.8 Flash Next, MLX affine
    4-bit group 32, MTP head). "Probes" below are 23 greedy chat requests of up to 120 tokens: 12 questions about the
    user (two or three wordings for each fact taught below) and 11 unrelated prompts (capitals, arithmetic, a haiku, a
    Python one-liner and so on).

  • An untaught folder serves as before. Flash Next on CUDA: the qwen4_exp family on the native CUDA host, every stream through the lane rounds #869's head and this branch's head on the same snapshot: the 23 probes give
    the same replies, drafted (23/23) and with "draft": false (23/23). Decode speed (tools/bench_openai.py, 256
    tokens, median of 3): drafted 83.9 and 62.3 tok/s on code and chat against Flash Next on CUDA: the qwen4_exp family on the native CUDA host, every stream through the lane rounds #869's 84.1 and 62.6; --no-drafts 35.1
    and 35.1 against 35.0 and 34.9. A cold 3,001- and 12,001-token prompt (tools/bench_cache.py --require-cold, time to the first token): 2.66 and 10.27 s
    against Flash Next on CUDA: the qwen4_exp family on the native CUDA host, every stream through the lane rounds #869's 2.71 and 10.28 s. The model takes 75.82 GiB against Flash Next on CUDA: the qwen4_exp family on the native CUDA host, every stream through the lane rounds #869's 75.81.

  • The trainer's forward is the serving forward. zig build flashnext-learn on the snapshot (one prompt of the
    Python engine's reference set, its first 256 tokens, the last 32 the answer):

    • a row's scores never depend on later rows (the last token changed, every earlier row's scores byte for byte, at 255
      and 254 rows);
    • the trainer's keys pass and loss pass over 119 rows against the serving forward (Engine.forward, every row's
      scores): every layer's MoE input and shared gate and every row's scores byte for byte, untaught and with a pocket
      neuron at layer 31;
    • six sequences side by side give each one's loss exactly as alone (672 rows, 0 difference);
    • a pocket neuron moves the forward by exactly gate act v at its row (1.503 against 1.503).
  • The gradient against finite differences. In bf16 a finite difference is noisy: below about a bf16 ulp of the
    residual the rounding decides, above it the loss's curvature does. The check nudges each layer's MoE output at the
    last row (its nudge reaches only its own loss: the direct path) and at a row before the answer (it reaches the loss
    only through later rows' attention and DeltaNet: the cross-row paths), at layers 12, 23, 31 and 42, along the push and
    half along a random direction, in four windows of the prompt (offsets 0 to 3), with least-squares slopes over
    steps of plus and minus one to six times 0.4% and 3% of the residual's size (32 checks a path and a step). Over all
    four windows the direct path's median relative error is 10.6% at the small steps and 111% at the large ones; the
    cross-row paths' is 43% at the small steps and 16% at the large ones (worst 204% and 69%). The first window's last
    row disagrees at every layer (2% to 73% at the small steps, about 2x at the large ones); in the other three windows
    the direct path agrees with a median of 6.2% at the small steps (worst 25%, 71% in one check of 8, and 17%), and
    the last row of the same prompt's first 255 tokens agreed within 0.3% to 11% at layers 12, 23, 42 and 46. So the
    direct path holds where a finite difference can see it, and the cross-row paths stay noisy, as they were with the
    captured kernels before Flash Next on CUDA: the qwen4_exp family on the native CUDA host, every stream through the lane rounds #869's own. The kernels themselves are each checked against
    float64 in the previous PR.

  • Lessons. A fresh copy of the snapshot (the files a lesson rewrites copied, the rest hard-linked), served with
    --slide --context 8192 --no-thinking --temperature 0, taught these five facts one learn request each ("source": "chat"), with the probes after each lesson:

    fact kept rounds and steps seconds its probes right after after all five
    "My name is Zack." yes 2 rounds, 2 steps 571 3/3 0/3
    "I used to live in [a US city]." (city left out here) yes 2 rounds, 2 steps 524 3/3 3/3
    "My GitHub name is ZackSample." yes 2 rounds, 2 steps 631 1/2 1/2
    "I contribute to TensorFold." no: "too few clean answers to learn from" (one clean question) none 14 0/2 0/2
    "I have two DGX Sparks at home." no: the held-out answers did not come back 5 steps over several rounds 2,389 0/2 0/2

    None of the 12 probe wordings was among the 85 questions the lessons trained on, so every probe is held out: 4 of 12
    after the five lessons. The first fact came back on all three wordings after its lesson and on one after the
    second lesson, none after the third. After the second lesson that is Living Weights: a later lesson's quiet refit can silence an earlier fact on its own questions, and nothing asks it again #870 (a refit silences the earlier fact). After
    the third it is the corrections rule: the server took "My GitHub name is ZackSample." as replacing "My name is Zack."
    and retired the name fact's neurons (the graph says replaced by fact 3), because both answer a "what is my ... name"
    question. This PR uses Living Weights as a core module: the learner, pocket and fits in zig/src/living, a family described as data (Nemotron only) #878's learner as it is; with Living Weights: a quiet refit holds an earlier fact's neuron on its own questions as read now #876/Living Weights: every earlier fact asked again after a lesson, and a take-out that puts the pocket back bit for bit #877 applied the name fact survives the second lesson (see
    the comment below), and the false replacement is a separate lesson-writer fix. The 11
    unrelated prompts kept the untaught model's replies byte for byte. A kept lesson took 8.7 to 10.5 minutes: 2.5 to
    4.1 writing the lesson, 2.9 to 4.1 the trainer's first round (one step), then 0.6 to 2.1 for the second round and
    about 25 s of checks a round.

  • A taught folder serves. The copy after the five lessons (27 neurons in its pockets) restarted without --slide
    and with it: the 23 probes give the same replies as the teaching server did after the last lesson (23/23 each).
    Drafted equals "draft": false (23/23), and four streams at once (--parallel 4) equal each alone (23/23). A
    1,670-token prompt asked again resumes 1,663 tokens from the prompt cache and gives the fresh reply, drafted and
    with "draft": false. Decode:
    drafted 79.8 and 59.1 tok/s on code and chat (5% below untaught), --no-drafts 34.7 and 34.5 (1% below). Cold
    prompts: 2.67 and 10.30 s for 3,001 and 12,001 tokens.

  • Memory. The server reports the model at 75.82 GiB and 10.59 GiB kept for the trainer (its buffers take
    10.23 GiB: MLX copies of the small linears it reads, the keep lists for 1,024 rows, the backward's scratch);
    flashnext-learn holds 84.24 GiB of device memory with the engine and the trainer, 84.59 GiB at its peak. During a
    lesson the GB10's 121 GiB showed 92 GiB in use.

  • Tests: zig build test-cuda-host from a fresh cache: 33/33 steps, 546/546 tests pass; zig build test-living: 39/39
    (Living Weights as a core module: the learner, pocket and fits in zig/src/living, a family described as data (Nemotron only) #878: 38, the one more is Flash Next's shard writer test). Every commit after the merge builds (zig build, native,
    test-cuda-host, test-living) and passes zig fmt --check build.zig zig; python3 tools/zig/lean_check.py gives
    527 problems against 527 on main (0 new); all 46 fatbins build for sm_86, sm_120 and sm_121 and check_fatbins
    passes. On the head build, tf-cuda-test flashnext-hc (its read-out kernel now also has the form that keeps the up
    rows), flashnext-attn, route-rows, norm-rows, experts-rows, fn-back and fn-pocket all pass.

  • Not run: Metal (Macs refuse --slide for this model as before; whether a Mac serves a folder taught here is not
    checked), sm_86 and sm_120 cards.

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.
  • No precision traded for speed. If the bits change, the description says which sums change and why. (Untaught:
    no bits change. Taught: a layer with a pocket rounds its MoE output once after the pocket is added in fp32.)
  • Prompt processing is no slower than the last release. (Untaught, same speed as Flash Next on CUDA: the qwen4_exp family on the native CUDA host, every stream through the lane rounds #869; see the receipt.)
  • The description says which platforms I ran, Metal M1 to M5 and CUDA, and which I could not. (GB10 only.)
  • New tests fail before the change, pass after it, and skip cleanly without their dependency.
  • Comments and docstrings are one line. No measurements or history in the source.
  • No personal data, machine names, internal hosts or local paths. No AI attribution lines.

Not in this PR, said plainly

Questions

heitke and others added 30 commits October 14, 2026 08:24
A resident prompt state died (its slot's rows overwritten, or its pool
blocks given up) left the Store serving the dead entry: every later
lookup found it, its restore failed, and the pass prefilled the whole
prompt from the start — the repeated full-refill cascade seen on live
servers with long conversations. The backend now tells the prompt cache
the moment a state goes stale (Slots.expire), so the Store drops that
entry immediately and the next turn keeps a fresh state instead of
paying for the same failure again.
The prefill path read a snap after pool admission could free it, and
overwrite walked snaps while expiry dropped them. A resident state that
dies now expires its cache entry at the host boundary; each lookup
borrows its entry and snap until resumed or release ends that ownership;
reclamation runs at host boundaries and skips active borrowers.
# Conflicts:
#	zig/src/core/prompt_cache.zig
…unchanged

The learner, its whole-lesson and routed paths, the pocket and its fits and helpers move from families/nemotron to a
`living` module that both builds import, as 19 renames with no change to their contents. The family-neutral half of
learn_shape.zig (block sizes, Adam settings, direction lengths) moves with them; Nemotron's learn_shape.zig keeps where
its blocks sit and names the shared constants from the module. Nemotron's files import the moved ones through the
module. The learner's tests now also run on Linux, as `zig build test-living` and under test-cuda-host. No behaviour
change.
… layout, neuron kind and shard format

A family now describes itself once, as data (living/families.zig): where its shared experts sit and their tensor
names, the neuron's activation (relu^2, or SwiGLU with an optional per-row gate), the quantization, the config keys and
the object they live in, and its MTP head's expert. Its host gives the learner a layout and the pocket hooks
(living/family.zig). Nemotron is the one family listed, and its lessons, routed blocks, edits and probes are unchanged.

- The pocket reads its layers and width from the family's layout, not from Nemotron's config, and the learner offers
  routed blocks, edits and probes only to a trainer with `sites` (Nemotron's).
- living/shards.zig widens any family's experts at its group and width; Nemotron's writer is now a wrapper over it.
  shard_edit sets and reads nested config keys (text_config).
- A SwiGLU neuron fitted as u is written as gate row 16u and up row u/16: relu(u x)^2 on its fact's rows, at most 8e-5
  of it at a 0.5 margin; a gated family's outputs fit gate * act(x). No listed family uses it yet; the tests use a
  stand-in family shaped as Qwen3.8 Flash Next's shared expert.
- The server's view reads layers, pockets and shards through the family its config's model_type names, and
  native/nemotron_slide.zig is native/slide_adapter.zig.
- pocket.zig's quiet refit and its loading move to pocket_quiet.zig and pocket_load.zig, for the 600-line rule.
- Tests: the generic writer equals affine4_host at 4 bits in groups of 64, Nemotron's save test over the wrapper, a
  SwiGLU neuron fitted and stored at 4 bits in groups of 32, and a whole pocket lesson over a stand-in host for each
  activation (test-living, also under test-cuda-host).
…roup-32 token, so the CUDA replay starts from the row the Python engine reads.
… so the linear-attention input is that mixed row.
…f the mixed row on one token, so the DeltaNet input is that projection.
…y match the host chain on one token, all 6144 gated values and all 2560 projected values, so the mixer branch is that row.
…, the write-back exact and its sums within 1 ulp, one normed value 1 step off, and the mixed row within 2 steps, so the expert input is that row.
…he shared expert exact, router logits within 29184 ulps, two weights 1 step off, the activations exact, and every down value the same bf16 with fp32 within 16384 ulps.
…the projection, the prepared heads, the one-key output, the gate and the output row all exact.
…one projected value 1 step off, and the prepared heads, the one-key output, the gate and the output row all exact.
… one projected value 1 step off, and the prepared heads, the one-key output, the gate and the output row all exact.
… the projection, the prepared heads, the one-key output, the gate and the output row all exact.
… the projection, the prepared heads, the one-key output, the gate and the output row all exact.
… the projection, the prepared heads, the one-key output, the gate and the output row all exact.
… two projected values 1 step off, one gate value 1 step off with group sums within 1024 ulps, and the prepared heads, the one-key output and the output row all exact.
… one gate value 1 step off with group sums within 8192 ulps, and the projection, the prepared heads, the one-key output and the output row all exact.
… one prepared key value 1 step off, and the projection, the other prepared heads, the one-key output, the gate and the output row all exact.
… one projected value 1 step off, and the prepared heads, the one-key output, the gate and the output row all exact.
… one projected value 1 step off, and the prepared heads, the one-key output, the gate and the output row all exact.
… two projected values 1 step off, and the prepared heads, the one-key output, the gate and the output row all exact.
…token, all 2560 mixed values exact, and three of 248320 scores 1 step off.
…m; prompts past 512 keys; LR7 capture.

The ids and position uploads were synchronous copies, which do not wait for the non-blocking compute stream, so
a later row overwrote a queued row's token id or position: wrong tokens on most prompts. Both now go on the
stream. attnStep launches one 512-key chunk per started chunk of the row's keys (as attention.attention does)
instead of always one, so prompts run to the captured NCH * 512 keys; the lanes engine defaults to that window
and refuses a larger one. The prompt check prints each run's top five scores and its timing.

zig/tests/cuda/flashnext: capture.py and capture.sh (Python engine 0.6.1, drafts off, greedy, 1536 cache slots)
and the Unbound-LR7 prompts with their references; flashnext-prompt matches the teacher paths on all three.
…ules and device weight bytes from what it declares; Flash Next registers

The registry gains flashnext.native. What was Nemotron's by name is now read from the family's declarations: its
prompt-cache copies (snap_bytes, snap_save, snap_restore, snap_drop, and snap_ready's spare buffers), the store's rules
(cache_rules; without them a state keeps at any row with the MTP head's one token on, ashhart#670), the quiet wait before a
warm pass and the lone driver's hand-over budget. Nemotron declares the copies, the wait and the budget, so its paths
are unchanged; a family without copies gets no prompt cache, and the startup line says so. A family that keeps part of
its checkpoint off the device says what it uploads (deviceWeightBytes) and admission counts that, not every shard:
Flash Next leaves its n-gram tables mapped and loads no vision tower, so the MLX 4-bit checkpoint uploads 73.3 of its
105.4 GiB, where counting every shard made the default reserve on a GB10 refuse it by 0.1 GiB. --slide is refused
before anything loads for a family that declares no learner.

The Flash Next family names what the host reads: one segment, its prompt rows, each stream's bytes, its refusals in
words, and the native host's lone-driver signature.
…the decode kernel's sums: experts_rows.cu

experts_rows.cu is experts.cu's decode expert kernel with two 16-row tiles a warp, so a warp reads each group of an
expert's weights once for up to 32 pair rows. Every row keeps the decode kernel's arithmetic: the same group sums,
the same two m16n8k16 steps from zero a group, the same fma epilogue order and the same SwiGLU, built with experts.cu's
flags. A row's bits therefore do not depend on how many rows share its window or chunk, which is what the lane core's
exactness contract needs of a lane kernel. It is instantiated at groups of 32 (gate and up stacked with SwiGLU, and
the down projection to fp32), the MLX 4-bit group-32 experts Flash Next reads.

cuda.experts.RowsExperts resolves the two instances and launches them on the shared plan (cuda.grouped.Router at its
32-pair tile). `tf-cuda-test experts-rows` packs random group-32 experts with experts_pack.cu, routes 1, 2, 5, 16,
33, 100 and 300 rows (three routed experts and a shared one a row) and checks every pair row of the rows kernel
against experts.cu's one-row launches of the same row, byte for byte, for the SwiGLU rows and the fp32 down rows.
…te_rows.cu)

tf_route_logits gives OUT[m, e] = x[m] . w[e] in fp32 for bf16 rows and router rows on mma tiles: one chain over K in
16-wide steps from zero, the inputs staged with cp.async and read back with ldmatrix. A row's sums never depend on the
row count or the tile, so the few-row form (up to 16 rows, three 256-wide stages in flight) and the prompt form (two
128-wide stages) give the same bits. tf_route_topk takes a row's top k of its first E logits (the lower id among
equal ones) with weights bf16(e^(l_k - l_0) / their sum, summed in pick order), and optionally a shared expert as slot
k at bf16(sigmoid(bf16(logit E))), the softmax routing Qwen-style MoE families use.

core/route_ref.zig is the float64 reference, and tf-cuda-test route-rows checks both kernels against it at 1, 3, 16,
17 and 40 rows (logits within the fp32 dot's bound, picks equal to the reference rule's on the kernel's own logits, a
row of ties too, weights and the shared gate within half a bf16 ulp plus 2^-18), and every row count's bits equal.
…ith their group sums

embed_rows.cu's affine 4-bit rows take the scale group (32 or 64) and a copy count (EmbedRows.affine4Groups; affine4
keeps groups of 64 and one copy, so Nemotron's launches are unchanged). norms.cu gains tf_rms_rows_sums (Norms.rmsSums):
bf16 rows read x_ld apart, y = bf16(x * rsqrt(mean(x^2) + eps) * w) with fp32 weights and IEEE division and square
root, then each group's sum of the stored y, for a group-quantized matmul's input. tf-cuda-test norm-rows checks both
against float64 at every row count (Flash Next's 2,560-wide embedding in 4 copies, its MTP head's 2,560- and 10,240-wide
norms) with the bits of every row count equal.
…attn.cu, against float64

Our kernels for every entry point the batched pass took from a captured Triton set, with the Python engine's formulas
and bf16 rounding points, IEEE division and square root and the core's within-one-ulp exp (glue_math.cuh), built
without contraction:
- flashnext_hc.cu: the hyper-connection write-back (stream sums alone, the MoE slots, K slices), the down projection
  with its normed rows (the tiled 4-bit dots on mma tiles, groups in order, per K slice), the slices' sum with the bf16
  SiLU and inject gates, the up projection with the stream mix, the n-gram gate and dilated conv, and the MTP head's
  stream adds;
- flashnext_attn.cu: the q, k and indexer heads' norm and RoPE with the cache writes, the output gate, the indexer's
  pooled blocks and block scores, and attention over 512-key chunks (q . k and p . v on mma tiles, an online softmax
  with fp32 sums, dense or through a sparse row's list) with the chunks merged in order.
Every kernel keeps a row's own sums in a fixed order, so a row's bits are the same at any row count; attention has a
form that reads the next tile ahead for a decode round's few rows, with the same sums. cuda_glue.zig launches them
(Glue), with the core's token rows, RMS rows and route rows for the embedding, the MTP head's norms and the router.

tf-cuda-test flashnext-hc and flashnext-attn check each kernel at the model's shapes against a float64 chain that rounds
where the kernel rounds: each bf16 output within one bf16 ulp (an intermediate rounding can tip either way), fp32 sums
within their sum-of-|terms| bound, attention within 2^-8 of sum p |v| / l plus half an ulp (p enters p . v as bf16),
copies and the stream adds exact, and the bits of 1, 3 and 17 rows (attention: 1, 3 and 20, both forms) equal.
…y entry point is used for comparison

The engine no longer needs TENSORFOLD_CUDA_KERNELS: every launch Tri made from a captured cubin now runs our kernels
(cuda_glue.zig) unless a set is loaded. A set given by TENSORFOLD_CUDA_KERNELS, or found beside the binary, replaces
them only when it holds all 15 entry points the pass launches (17 with the MTP head); one without them, Nemotron's
say, is passed over with a log line, and the startup line says "own glue kernels" (native/cuda.zig asks the family's
Loaded.own_glue; aot.Set.has names a set's functions). flashnext-prompt takes native in place of a set's folder.

flashnext-compare loads the engine once and runs a prompt on our glue and on a captured set: every prompt row's scores
(top-1 agreement, the largest score difference, KL, each path's perplexity on the prompt's own next tokens), then
each path's greedy reply and the token where they part.
…ue beside a captured set

With a captured set loaded, TF_FLASHNEXT_OWNED (all, or a list of Triton names such as _chunks,_merge) makes those
entry points run our kernels and the rest the set's, so a difference can be put down to one kernel. Glue reads it once
at load into a mask; without a set every entry is ours, as before. flashnext-compare takes it on its own side, so it
compares the captured set against the captured set with the named entries swapped.
…for comparison

The recipe, the RUNBOOK line, the CUDA and Flash Next pages and the CHANGELOG say the engine needs no capture: every
kernel is built from the repository, TENSORFOLD_CUDA_KERNELS names a captured set only to compare against the Python
engine's teacher path, and TF_FLASHNEXT_OWNED swaps single entry points. The recipe lists the new tf-cuda-test cases
and flashnext-compare, the semantic settings the config check refuses, measurements on our kernels beside the
captured set's, and where our tokens part from the teacher path. The family recipe's lane-kernel row names
route_rows.cu.
@ZackSample

Copy link
Copy Markdown
Contributor Author

The same five lessons with the learner fixes applied

The lesson receipt above runs the learner as it is on main/#878. To see how much of its weak recall comes from that
learner, I applied #876 and #877 (the refit fix, the re-check and the exact take-out; the #870 ground your fix covers)
and #891 (facts learned and checked in conversation) on top of this branch. All three are ported by path onto #878's
zig/src/living, and that comparison build is local only. Then I ran the same sequence: a fresh copy of the snapshot,
the same flags (--slide --context 8192 --no-thinking --temperature 0), the same five facts in the same order, one
"source": "chat" learn request each, and the same 23 greedy probes after each lesson.

fact this PR: kept, steps, s with the fixes: kept, steps, s probes right after: this PR / fixes after all five: this PR / fixes
"My name is Zack." yes, 2, 571 yes, 2, 710 3/3 / 3/3 0/3 / 0/3
"I used to live in [a US city]." yes, 2, 524 yes, 2, 445 3/3 / 3/3 3/3 / 3/3
"My GitHub name is ZackSample." yes, 2, 631 yes, 2, 632 1/2 / 1/2 1/2 / 1/2
"I contribute to TensorFold." no, writer refused, 14 no, writer refused, 14 0/2 / 0/2 0/2 / 0/2
"I have two DGX Sparks at home." no, 5 steps, 2,389 no, 5 steps, 1,398 0/2 / 0/2 0/2 / 0/2
held-out probes after lesson 2: 4/12 / 6/12 4/12 / 4/12
  • Living Weights: a later lesson's quiet refit can silence an earlier fact on its own questions, and nothing asks it again #870 is fixed here. After lesson 2, this PR's learner had refitted the name fact's neurons and it came back on 1
    of 3 wordings. With the fixes it came back on 3 of 3. The re-check asked it again in 0.5 s and found it recalled.

  • The name fact's loss at lesson 3 is not Living Weights: a later lesson's quiet refit can silence an earlier fact on its own questions, and nothing asks it again #870. The description had this wrong. On both builds, the server
    takes "My GitHub name is ZackSample." for a correction of "My name is Zack." (... replaces "My name is Zack." (fact 1), whose neurons are taken out first). It then retires the name fact's neurons, and the graph marks fact 1
    replaced by fact 3. That is the corrections rule (Living Weights: conflicting facts, updates and forgetting (measurements + proposal) #678) matching two different "name" facts. None of the three
    fixes touches it, so the name fact ends at 0/3 on both builds. I have corrected the description.

  • Lessons 4 and 5 fail the same way on both builds.

    • For "I contribute to TensorFold.", the lesson writer drafts only yes/no questions that name the fact, and the
      lesson keeps 1 clean question.
    • "I have two DGX Sparks at home." is not recalled after 5 steps. With the fixes it took 23 minutes, against 40 on
      this PR. Its take-out logged the pocket put back as the lesson found it (..., the same bits).
  • Unrelated prompts: with the fixes, 9 of 11 kept the untaught model's replies byte for byte (this PR: 11/11).
    After lesson 3, two replies changed wording but stayed right: "212°F (degrees Fahrenheit)" became "212°F
    (Fahrenheit)", and the Jupiter reply changed further down.

  • Restarts: with the fixes, plain and --slide restarts gave the same 23 replies as after lesson 5. This PR's
    restarts did the same. The base snapshot was untouched.

  • In conversation (the context study's ~4k-token task chat, then each probe; and two chains of the five facts' questions):

    this PR with the fixes
    after the task chat 1/12 (GitHub) 2/12 ([a US city], GitHub)
    chained 1/10 3/10

    One more chained reply matched the name keyword, "you are called ZackSample". That is a wrong answer, so it is
    not counted. Flash Next has no tapRows hook yet, so these lessons left out the 24 conversation prompts longer than
    the trainer's 512-row passes. They learned the short chains, and every lesson's in-conversation checks ran.

So on this sequence, the fixes bring back what #870 took (lesson 2), make the take-out exact and halve the failing
lesson's time. The final count stays at 4 of 12 for reasons outside the learner's refit: a correction false positive
and two lessons the writer and the fit cannot finish. I would rather say that plainly than claim the fixes raise the
headline number. If it helps, I can rerun with the name and GitHub facts worded so the corrections rule does not pair
them. That would show the name fact surviving lessons 3 to 5 with the fixes.

…rnels run

The check compared each label only with linear_attention, so a full-attention layer labelled sliding_attention (or
any other word) was accepted and served as full attention. Every label now has to be linear_attention or
full_attention, as the layer's place in the stack says. The new test relabels full and linear layers in turn and
expects layer_types back; on the old check the full layer relabelled sliding_attention came back accepted.
…he staging is reused or freed

A pass queues its uploads from the scratch's pinned staging, then records an event behind them, and the next pass
waits on that event before it writes the staging again. If a later upload or the record failed, an earlier upload of
the same pass stayed queued while the event still marked the previous pass, so the next pass, or the scratch's free,
could change or release bytes a copy had yet to read. The staging now notes an upload queued after the last record
and, until a record covers it, waits by draining the stream instead of on the event. Scratch.free waits the same way
before it frees the staging. The new test runs a clean pass and then one whose second upload, or whose record,
fails, against a stand-in queue, and expects nothing queued at the next wait; waiting on the event alone, as before,
leaves one copy queued.
…t within float64's tolerance (tf-cuda-test gdn-back)
…erts with a shared one, and softmax top-k routing

train_q4.cu with cuda.train_q4: a linear's input gradient on tensor cores (split K for a head), the experts' products
over MLX stacks or experts_pack.cu's packed table, each pair's SwiGLU, and the top_k softmax and sigmoid shared gate
back through the router. tf_train_sort_pairs takes up to 1024 experts. tf-cuda-test q4-back and moe-back check them
against float64 at 64 and 256 rows, each reference against central differences; packed experts give the stacks' bits.
…d's q then its gate, RMS-normed q and k, partial RoPE)

train_attn.cu with cuda.train_attn: the prepared heads again, the output gate, causal attention to 2048 keys with
train.cu's key and value part, the heads' RoPE and norms undone, and the q, k and v projections' input gradients through
cuda.train_q4. tf-cuda-test attn-back checks every gradient against float64 at 64 and 256 rows (24 query heads, 2 kv
heads, head dim 256), the reference against central differences.
…nts, and fn_back over the core's layers

flashnext_train.cu: the write-back's and read-out's backward (mix, activations, down and inject, the streams' RMS norms),
and the pocket's SwiGLU neurons beside the shared expert behind its gate: the forward in two kernels (tf_fn_pocket_in, then
_out, tiled for many rows; a layer with no neuron launches nothing), its input gradient, and its output vectors'
gradient. flashnext.fn_back composes them with cuda.train_q4 and cuda.train_attn on one stream. tf-cuda-test fn-hc-back
and fn-moe-back check against float64 at 64 and 256 rows; fn-pocket checks the two kernels against tf_fn_pocket_rows
byte for byte over 444 cases; fn-back runs every input-gradient case the trainer uses.
…w-c-fnback (ashhart#869 and the backward kernels)

Flash Next's trainer needs both open PRs: the CUDA family (ashhart#869) and the learner in zig/src/living (ashhart#878). The two
touched zig/build/cuda.zig and native/cuda.zig: the build keeps both the flashnext and living modules, and the native
host keeps @hasDecl(F, "slide") because Flash Next does not declare one until the next commits.
…hed pass's keeps for a backward

A folder Living Weights taught holds wider shared experts: the engine packs their own 640 rows as before and keeps
the pocket's neurons in fp32 (cuda_pocket.zig), and every pass (prompts, drafted windows, rounds) adds them behind
the shared gate on the layers that have one, through tf_fn_pocket_in and _out. Such a layer's MoE output is summed in
fp32 (fn_moe_sum, MODE 2's order), the pocket added, and written back rounded once (MODE 4 of one slice); every other
layer writes back as before, so an untaught folder's bits are unchanged. The config check takes the shared expert's
width with the pocket's neurons (tensorfold_learned_neurons). A pass may keep what a backward reads: each read-out's
streams, normed rows, v, up rows (fn_hc_upmix_up, the same mixed bits) and inject gates, the mixer and MoE branches,
the MoE's input and slots, attention's projections and gated rows, and the final mixer's read-out.
…lashnext-learn against the serving forward

cuda_train.zig runs the learner's passes (keys, loss, target) with sequences side by side on the engine's own batched
pass (cuda_batch_train.zig: the row-wise launches over every row, the n-gram block, DeltaNet and attention one
sequence at a time from zeros), the open fact's pocket neurons scaled per sequence and each nudge added to its layer's
MoE output; then the loss's gradient from the 4-bit head down to the lowest nudged layer with flashnext.fn_back and
cuda.gdn_train, reading the engine's packed experts where they sit and MLX copies of the small linears
(cuda_train_buf.zig, 10.2 GiB beside the engine). A captured set's glue keeps no up rows, so the trainer runs on our
own. zig build flashnext-learn: a row's scores never depend on later rows; the trainer's MoE inputs, shared gates and
scores equal the serving forward's byte for byte (untaught and with a pocket neuron); packed sequences give each
one's loss as alone; the push against finite differences over four windows of the prompt (the direct path's median
must agree at small steps; the cross-row paths are reported); a pocket neuron moves the forward as gate act v.
…: qwen4_exp in the core's families

living/families.zig lists qwen4_exp: a SwiGLU shared expert at every layer behind its own gate, 4-bit groups of 32,
keys under text_config, the MTP layer's expert in the index; the stand-in test family goes, its tests now read it.
flashnext/living.zig gives its layout and writes its pocket through the core's shard writer (pocket_shards_test.zig:
the first save widens every expert and the MTP head's with zeros, a later one lands in place). cuda_slide.zig is the
learner's host over the trainer, and the lane backend opens with --slide: the trainer's pack of rows in the pass's
scratch, the layout from config.json, its bytes kept out of the streams' room. A captured comparison set refuses
--slide; Macs refuse it for this model as before.
…'s and judge's reads see the model as shipped

A lane stream's untaught flag rides its Seq. A prompt or window pass of an untaught stream runs without the pockets; a
decode round of untaught streams alone leaves them out, and a mixed one scales the pocket by 0 on the untaught rows, as
Nemotron's pocket skips untaught segments. fn-pocket checks that a 0-scaled row keeps every output bit.
@ZackSample

Copy link
Copy Markdown
Contributor Author

514e4ee makes Flash Next honour untaught streams, which the lesson writer and the judge send. Before it they read the taught model. The stream's flag now rides its Seq. A prompt or window pass for an untaught stream runs without the pockets. A decode round of only untaught streams leaves the pockets out, and a mixed round scales the pocket by 0 on the untaught rows, the way Nemotron's pocket skips untaught segments.
Tests: a host test for the row split (test-cuda-host 550/550, was 549), and a new tf-cuda-test fn-pocket case. On 2 to 300 rows with 13 or 64 neurons, a 0-scaled row keeps every output bit and the taught rows match the unscaled pocket byte for byte (the earlier 444 cases still pass).
Not yet run: a served check on a taught folder, because no taught copy was on disk. zig fmt is clean and lean_check gives 527 problems, as before (0 new).

@ZackSample

ZackSample commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

The same five lessons with the two lesson-writer fixes added

My last comment found that the name fact's loss at lesson 3 was the corrections rule (#678) matching two different
"name" facts, and that lesson 4 was refused by the lesson writer. Both fixes are now a separate PR on main with
Nemotron receipts (#910, 2 commits):

  • a fact that only answers an earlier fact's question no longer replaces it; the earlier one gives way when the model
    says the new one changes it or that the earlier one no longer holds;
  • a told fact's subject that most of its open questions never name (for "I contribute to TensorFold." the subject
    prompt names TensorFold, its answer) is no subject, so the fact keys on the reply.

I added both commits to the local comparison build from my last comment (this branch with #876, #877 and #891 ported).
Then I ran the same sequence again: the copy's rewritten files reset from the snapshot (sha256 checked), the same flags
(--slide --context 8192 --no-thinking --temperature 0), the same five facts in the same order, one "source": "chat"
learn request each, and the same 23 greedy probes after each lesson.

fact this PR: kept, steps, s #876/#877/#891: kept, steps, s + writer fixes: kept, steps, s after all five: this PR / #876-#891 / + writer fixes
"My name is Zack." yes, 2, 571 yes, 2, 710 yes, 2, 661 0/3 / 0/3 / 3/3
"I used to live in [a US city]." yes, 2, 524 yes, 2, 445 yes, 2, 436 3/3 / 3/3 / 3/3
"My GitHub name is ZackSample." yes, 2, 631 yes, 2, 632 yes, 2, 581 1/2 / 1/2 / 1/2
"I contribute to TensorFold." no, writer refused, 14 no, writer refused, 14 yes, 3, 614 0/2 / 0/2 / 2/2
"I have two DGX Sparks at home." no, 5 steps, 2,389 no, 5 steps, 1,398 no, 5 steps, 2,096 0/2 / 0/2 / 0/2
held-out probes 4/12 / 4/12 / 9/12
  • Lesson 3 no longer takes out the name fact. No replaces line this time. The re-check after lesson 3 asked both
    earlier facts and found both recalled. The name fact answers on all three wordings after all five lessons.

  • Lesson 4 is learned. The writer kept 20 questions instead of 1 (the subject is dropped, so the open questions stay)
    and the fact came back in 3 steps. Both of its probes answer "TensorFold" after all five lessons.

  • Lesson 5 still misses, after 5 steps in 35 minutes. Neither fix touches it.

  • None of the 12 probe wordings was among the 93 questions these lessons trained on, so all 9 hits are held out.

  • Unrelated prompts: 8 of 11 kept the untaught model's replies byte for byte. The three that changed are still right:
    the 212°F reply ("(degrees Fahrenheit)" became "(Fahrenheit)"), the 17 x 23 working (laid out differently in the
    first 120 tokens), and a different haiku about the sea.

  • Restarts: plain and --slide restarts gave the same 23 replies as after lesson 5. The base snapshot was untouched.

  • In conversation (the same checks as my last comment):

    this PR Living Weights: a quiet refit holds an earlier fact's neuron on its own questions as read now #876/Living Weights: every earlier fact asked again after a lesson, and a take-out that puts the pocket back bit for bit #877/Living Weights: facts learned and checked in conversation, not only asked cold #891 + writer fixes
    after the ~4k-token task chat 1/12 2/12 7/12
    chained 1/10 3/10 7/10

    I read every hit and all are right answers. One chained hit is for the DGX Sparks fact, which was never learned, so
    that one comes from the chain's earlier turns, not from the pocket.

So on this sequence, the headline goes from 4 of 12 to 9 of 12 once the learner fixes and the two writer fixes are both
in. Everything left is lesson 5, which the fit does not finish. I am keeping the writer fixes out of this PR, which
still runs the learner as it is on main/#878.

@ashhart ashhart mentioned this pull request Oct 17, 2026
@ashhart

ashhart commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Thanks @ZackSample for separating the learner and writer effects. The latest held-out result is 9/12 with both sets of fixes, not the earlier 4/12; lesson five still fails, and unrelated replies are not all byte-identical.

We are carrying the learner repairs onto the selected Flash Next CUDA engine while retaining the landed writer safeguards, conversation checks, exact take-out and untaught streams. The zero-scaled-row control is useful, but the taught-folder served untaught check is still owed. Retain that case, restart parity, retention and save/reload in the integrated gate. The improved recall receipt does not qualify the remaining fit failure.

Posted by Ash's AI Assistant Synthena

ashhart added a commit that referenced this pull request Oct 18, 2026
…son gate (#895, #896)

Flash Next on CUDA takes --slide. A lesson's neurons go into its MoE layers' pockets through the shared learner and
the gate Nemotron already uses: the conversation reader, the thresholded quiet refit, up-row neuron counting, family
validation, context-aware rechecks, and delete rounding and put-back. A rejected repair restores every neuron, positive
row and retired lesson, and the final fit is rounded by the shard encoder and checked for stability before it is
judged. A save that fails after an accepted lesson keeps the live values and leaves an admission marker, so no engine
opens the folder until a checked retry or a rollback; close drains the trainer and its sources before it frees them.
The gradient, train_q4, train_attn and Flash Next training kernels are new images whose launches stay unqualified
until their records are measured. Existing kernels are unchanged; Flash Next's two serving images keep their old bytes
and gain the training entry points after them.

Co-authored-by: ZackSample <134018356+ZackSample@users.noreply.github.com>
Co-authored-by: akol1 <15747200+akol1@users.noreply.github.com>
Co-authored-by: BobClawblaw <288991054+BobClawblaw@users.noreply.github.com>
ashhart added a commit that referenced this pull request Oct 18, 2026
…896, #911), which move to 1.1.1

This reverts a291b68de and 4e0d7927c with the follow-ups that only served them: ca7a0882d (Nemotron's learning
drain), 493cdf5d4 (the save marker), 650fac5d7 (the MoE backward pack), the learning records in cda734f7d, ec141ed97
and d9566f585, and the trainer tests that 2708455af moved into cuda_close_tests.zig. The training images leave the
kernel list and the CUDA contract's image list, gradient_pending, qwen_learning_pending and registry_learning go, and
the 13.2 set and the live audit keep the serving, prompt, GLM and paging records. The lean moves that later commits
build on stay: the kernel list remains in cuda_kernels.zig and the close-test file goes only because every test in it
was a trainer's.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants