Skip to content

Flash Next: images and video on native Metal, and warm passes that keep nothing skipped (stacked on #606) - #986

Closed
BobClawblaw wants to merge 49 commits into
ashhart:mainfrom
BobClawblaw:flashnext-shared-vision
Closed

BobClawblaw wants to merge 49 commits into
ashhart:mainfrom
BobClawblaw:flashnext-shared-vision

Conversation

@BobClawblaw

@BobClawblaw BobClawblaw commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Qwen3.8 Flash Next reads images and videos with --vision on native Metal, on one stream and on #606's shared sessions. A fix to the lane host's warm pass also recovers #606's first-token and draft-check speed.

Stacked on #606. The commits up to #606's head (5164380e) are #606's. Until it lands, only the eight after it are this PR.

What changes

  • Warm passes that keep nothing are skipped (core/lane_host.zig). After each reply, the lane host prefills the next turn in the background. For Flash Next's shared sessions on a short chat, the cache plans no mark and doesn't keep the pass's end (one token of lookahead), so the pass kept nothing while the next request waited on it. The pass now ends before its first chunk when its cache would keep nothing. With a cache that keeps something, nothing changes. A new test in lane_host_warm_test.zig fails without the fix and passes with it.
  • The tower is core's (core/qwen_vl/). Flash Next's vision_config is the 27B's tower with a 2,560-wide merger. native/flashnext_images.zig opens it, checks the width and the text's M-RoPE sections (interleaved 11, 11, 10 over 64 dimensions), and prepares images with qwen_vl/geometry.
  • Prompt rows (families/flashnext/vis_prompt.zig, replay.zig, kernels/metal/prefill/fn_mrope.metal):
    • The tower's rows replace the image tokens' embeddings in all four hyper-connection streams.
    • Rows up to the last image's end run fn_attn_prep_mrope, the recorded q4_attn_prep at three positions a row; a text row keeps its bits.
    • Positions are transformers' get_rope_index, shifted so that the text after the last image rotates at its own slot. Decode, drafts and MTP run as for text.
    • Image and video chats resume from the prompt cache. The shift moves every row before the last image, so a kept state is named by the shift as well as its tokens and images (LaneHost.frame_key, flashnext_images.key): a later turn with the same images resumes, and a turn that adds an image reads its prompt again. The tower skips the images a resumed state already holds.
  • Hosts:
    • The one-stream host (flashnext_host.zig) and the shared one (flashnext_batch_host.zig) both offer images. The shared backend's fill takes a stream's image rows through a hook (Backend.images).
    • The tower's buffers join the residency set.
    • --vision on a speed-up pair is refused by name.
  • Video (core/qwen_vl/video.zig, server/images.zig, server/messages.zig):
    • Frames follow transformers 5.17's Qwen3VLVideoProcessor: 2 a second (4 to 256 here), smart_resize over the clip's pixels, torch's uint8 bicubic, and frame pairs as tower images.
    • Times are each pair's mean, as <X.Y seconds> before the pair's block, as Qwen3VLProcessor writes them.
    • VideoOffer.time_first names that layout and the rows' token.
    • A video part carries "video": null, so Qwen's template, which tests 'video' in item, renders it; GLM's reads the type.
  • Structured output on shared sessions (families/flashnext/grammar.zig): response_format, the guided_* fields, tool_choice: "required" and a named function.
    • fz_mask_rows sets a grammar row's disallowed logits to -inf before fz_argmax or tf_sample_full. A round with no grammar stream skips it.
    • A grammar stream's last prompt row is kept, and its first token is drawn on the host among the allowed ones, as the GPU draws. Backend.held hands the MTP drafts to the grammar, so drafting stays on.
    • The shared host sets .structures = true. The single-reply driver (--parallel 1) still answers 400.
    • A test checks that masked rows' GPU picks equal the host draw, greedy and sampled, with a compiler vocabulary smaller than the head's.
  • Docs: docs/vision.md (a Qwen3.8 Flash Next section, video), docs/recipes/flash-next-vision.md, THIRD_PARTY_NOTICES.md.

Checks (M5 Ultra 256 GB, TensorFold/Qwen3.8-Flash-Next-MLX-6bit-MTP, --parallel 2 --context 262144 --vision):

  • Speed: the recipe harness's speed plan. Flash Next: shared rounds by default on native Metal #606's first-token and draft-check numbers below are its head as is, run in the same session:

    Flash Next: shared rounds by default on native Metal #606 head this PR
    First token, 1 stream 0.28–0.31 s 0.11–0.12 s
    First token, 2 streams 0.34–0.38 s 0.17–0.19 s
    Decode, 1 stream (prose / code / structured / list) 140 / 207 / 168 / 210 tok/s 138 / 206 / 169 / 211 tok/s
    Decode, 2 streams, total 189 / 248 / 210 / 261 tok/s 195 / 262 / 221 / 270 tok/s
    Drafts, drafted / plain 126 / 73 tok/s 155 / 84 tok/s
  • Text: three fixed text prompts, drafted and plain, give main's tokens on one stream without --vision and with it, and on shared sessions with it.

  • Tower:

    • tf-qwen-vision on five pictures: the grids and patches are bit-identical to transformers' Pillow processor. Against the default torchvision processor, at most 0.24% of values differ, each by one level.
    • Against transformers' float32 Qwen4ExpVisionModel on the same patches, the mean row error is 1.4 to 3.1%; transformers' own bf16 run gets 1.8 to 5.7%.
  • Images:

    • Six fixed prompts (colours, text in the image, tall or wide, a description, two images, an image then a turn) give the expected answers.
    • Drafted equals "draft": false, and replies are the same tokens on one stream, on shared sessions, and with the lone path on or off.
    • vision_served.py passes 19 of 19, on one stream and on shared sessions.
    • Prompt cache: in a five-turn image chat (a second image in turn 4) and a three-turn video chat, plain and drafted, every reply equals a cache-off server's, on one stream and on shared sessions. Follow-ups resume past their images: 208 to 618 tokens on one stream, 277 to 580 on shared sessions (whose cache cuts only at replies 256 or more tokens apart), and 371 to 416 past the video.
  • Video:

    • On three clips, the served prompt has the processor's length: 239, 387 and 8,215 tokens.
    • The recipe's video_check.py passes 7 of 7: four clips with known answers (H.264 and VP9), a 30-second 720p clip's two scenes in order, a video beside an image, and a video and a text request at once.
    • First token: 0.20 to 0.42 s on the short clips, 3.25 s on the 720p clip.
  • Structured output:

    • The recipe harness's structured check passes 18/18: six grammar kinds, greedy, sampled and thinking, and a bad regex is a 400. On the previous commit it is 0/18: the server refuses grammars.

    • Drafted equals "draft": false in all 12 greedy and sampled cases, and a grammar stream beside a plain one leaves the plain reply unchanged.

    • tool_choice required and named give valid calls. Eight plain replies, greedy and sampled, are token-identical to the previous commit's.

    • Decode speed is unchanged with a grammar on. The harness's structured_speed check (greedy; a 12-person json_schema reply and a json_object one, each prompt with the grammar off and on) measured:

      grammar off grammar on
      1 stream (json_schema / json_object) 194 / 184 tok/s 206 / 180 tok/s
      2 grammar streams, total 253 / 239 tok/s 263 / 236 tok/s
      A plain stream beside the other 125 / 114 tok/s 129 / 123 tok/s
  • Repo checks: zig build test passes, as do test-kernel-contract, test-golden, fuzz-cortheon, zig fmt --check and the lean check (no new problems against Flash Next: shared rounds by default on native Metal #606's head).

cwschroeder and others added 30 commits October 10, 2026 19:36
Pack session windows into one target layer loop behind the shared lane core. Isolate recurrent and attention caches, normalize kept states with a fenced GPU copy, preserve draft and snapshot semantics, and keep the single-reply and TP drivers as defaults. Include CPU metadata contracts, checkpoint exactness/lifecycle gates and served concurrency comparisons.
…ke the server past 70% of RAM, as GLM_LOAD_LIMIT_GB does for GLM
… token on the host from the head's logits

The first token from the prompt pass's last row, each verify row from the window's logits, with the lane core's keyed sampler (top-k in one pass over bf16 keys, equal to qwen3_5's draw; families/flashnext/sample.zig, the same file as ashhart#719). Sampled replies are the same drafted and undrafted, alone and beside other streams; greedy replies are unchanged.
…ort of bf16 keys, not a full sort, the same draws

top_k 0 (or above 256) went through the reference: every candidate in f64, sorted, then choose. Now a counting sort of the 65,536 bf16 keys gives choose's order (ids ascending within a key), top-p and min-p cut it as choose does with libm's exp and the same pairwise sum, and the keyed Gumbel draw runs in that order, passing over candidates whose noise is provably a nat short of the best score (one hash, no logs) and stopping once none can reach it. 248k vocabulary: 13.7-16.8 ms a row to 0.33-0.65 ms; the host test now also compares top_k 0, 300 and the whole row, top-p 1, min-p and wide logits.
…dget is what the sessions leave under it

The slot checks reserved a given cache against the selected limit while fit.budget still planned the cache against 70% of RAM, so an automatic cache could take the server past the limit and a raised limit did not raise a given cache's own gate. cache_fit.budgetUnder plans the cache under the same ceiling (fitUnder; budget and fit keep 70% for the other hosts), with a test for the automatic and the given budget under a limit. The recipe doc, the limit's comment and the OverMemoryLimit message name the contract.
…nf noise wins as in the reference

lanes.sampling.uniform's largest value, (2^53 - 1) 2^-53 + 2^-54, rounds to exactly 1.0 in f64, so the reference's Gumbel score is +inf there; the early stop assumed noise of at most 37.43 and could pass that candidate over (a seed that hits it: expected 4, found 0). Past a 38-nat shortfall the draw now still asks each candidate whether its uniform is 1 (one hash, no logs) and stops only once the best score is +inf; the pruning bound reads 1 - u from the f64 uniform itself (exact), so a u of 1 is never passed over. The reviewer's case is a host test, with the same candidate inside the bound and behind another leader. Speed unchanged (0.33-0.63 ms a row at 248k).
…_LIMIT_GB lets the slots take the server past 70% of RAM
…st's temperature draws each token on the host from the head's logits
…ching

Conflicts: README.md keeps both the batching note and main's 27B wording; cache_fit.zig keeps FLASHNEXT_LOAD_LIMIT_GB's budget under main's 16 GiB automatic cap and deferred stream accounting; cuda.zig is main's; tensorfold.zig exports both sides.
…ream's drafts in one command buffer

The batch backend gave the lane core no costs, so each shared round filled up to 16 rows with drafts however few
landed, and each stream's head ran in its own command buffers (a wait per draft).

- batch_costs.zig times, on the last session before any stream holds it, a shared forward at 1..16 rows (the fastest
  of four, after six warm-up windows) and a chained head step (absorb plus 8 drafts against absorb plus 1, in one
  command buffer). Backend.facts reports them: shared_costs for the allocator, and a lone stream's window_costs and
  mtp_step_ms for its depth (FZ_LONE_UNPRICED=1: the old heuristic).
- The lane core prices each draft past a stream's first at that step (Model.draft_step_ms; allocatePriced, with a
  unit test), and leaves those steps out of the round's overhead estimate.
- Backend.draftTogether encodes every request's absorb and chained drafts into one command buffer (Session
  encodeDrafts / heldDrafts) and waits once; FZ_BATCH_MTP_SERIAL=1 keeps the per-stream path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…XTS, and a stream that outgrows its session moves

Every session held the whole window, so few fit beside the model (three 262K sessions in 15.4 GiB on a 256 GB Mac).

- FLASHNEXT_SLOT_CONTEXTS lists the sessions past the engine's own (token counts, CONTEXTxN for N alike, e.g.
  "65536,32768x3,16384x11": 16 streams in 11.6 GiB); unset, nothing changes.
- A new stream takes the smallest free session that holds its prompt and its reply's next FLASHNEXT_SLOT_GROW tokens
  (default 8192); LaneHost's admit hook (as on main since ashhart#639) holds a request until one is free.
- Before a round, a stream left too little room moves to the smallest free larger session that holds its next
  FLASHNEXT_SLOT_GROW tokens (Session.moveTo: its cache rows, DeltaNet states, n-gram tail, head rows and pooled
  blocks copied on the GPU at a round's end, states settled first), or pauses (Stream.paused) until one frees.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the shared rounds when a second arrives

One stream through the shared rounds decodes slower than the single-reply driver: a host-driven round each window.
The batch host now gives LaneHost a lone driver (api.Lone, as the 27B's host does):

- The lone stream's prompt pass is the shared sessions' own, in the engine's session (session 0, whose buffers the
  engine's rounds use), so the states the prompt cache keeps are the shared path's.
- It then decodes on the engine's GPU-side rounds: generateFrom's decode, split out as Engine.decode, two rounds in
  flight.
- When LaneHost's yield hook asks (a request waits, or a cancel), the loop stops after a landed round; the round
  ahead has already started as rounds do (the kept DeltaNet states, the n-gram tail, the head's absorb of the last
  window), so the session's host fields are set to match (position, history, head position, pooled counts clamped)
  and the lane core adopts the stream: its next window starts at the cache's end with nothing held.
- FLASHNEXT_NO_LONE=1 serves every request through the shared rounds. The lone driver takes greedy streams only (api.Lone.sampled stays false): sampled streams, and those with a thinking budget, stay on the shared rounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wait for a session that doesn't exist; old path when unset

Review fixes (cwschroeder on cwschroeder#10):

- slots.room decides before each round from the reply's end (prompt + max_new + a window): a stream whose session
  holds it is never moved or paused, so one in the largest session (admission's guarantee) runs to its end however
  many drafts are rejected near the limit. A stream that does need more room moves while a larger session is free,
  else pauses on a round that still fits; it only waits when a larger session exists, and the waits go up in size,
  so a paused stream resumes once that session frees.
- slots.parse validates each repetition count against the sessions still allowed before appending, and adds the
  margin with checked arithmetic (an overflowing count or repeat is refused, nothing allocated for it).
- FLASHNEXT_SLOT_CONTEXTS unset keeps the old path: the first free session (ContextFull when it can't hold the
  reply), no admission hook, no growth checks.
- Tests (slots.zig, in the core test root): the list's parsing, overflow and session-count refusals; session choice
  unsized (first free) and sized (smallest that holds the first grow); near-limit rounds with every draft but one
  rejected in a session that holds the reply (never pauses); a smaller session moving while a larger one is free and
  pausing on a round that fits otherwise; and no wait when no session is larger.
… run, FZ_BATCH_UNPRICED=1 skips timing, fuller pricing tests

Review fixes (cwschroeder on cwschroeder#9):

- batch_costs.time prefills a 192-token scratch prompt and runs windows of up to MAXR rows past it, so a session
  needs batch_costs.ROWS (209) rows. timeCosts now checks fits(s.m.cap) first and serves unpriced (the allocator as
  before) when the session is smaller, as at --context 128; time itself refuses with SessionTooSmall. Boundary test
  at ROWS - 1 and ROWS.
- FZ_BATCH_UNPRICED=1 skips the timing entirely: no costs, so the allocator, draft budgets and the lone depth rule
  are the unpriced ones from before this PR.
- allocate.zig: tests for the first-draft exemption (a 100 ms step still takes a stream's first draft), asymmetric
  streams (priced [3, 1] against unpriced [3, 3]) and a zero step (priced equals allocate).
- Doc comments in batch_costs.zig, session.zig and backend.zig cut to one line.
- batch_costs.zig joins the core test root (tensorfold.zig), so its boundary test runs.
…t, which can move the context

The prompt slice was taken before the first token's commit and handed to Engine.decode after it; a commit that
grows the context frees the old buffer, so decode copied freed memory into the rounds' history.

- Backend.commitFirst commits the first token and returns stream.prompt() from the context as it is then. decode
  copies the prompt to the rounds' history before any round commits, so later commits leave it alone.
- A test fills the context to capacity, refuses growth in place (FailingAllocator resize_fail_index 0), and checks
  the returned prompt is the moved context's, with the prompt's tokens.
- backend.zig joins the core test root (tensorfold.zig), so this test runs.
…rwards timed at load; every stream's drafts in one command buffer
Conflicts: backend.zig keeps both the think import and batch_costs, and both the lone handoff counter and #9's
serial-drafts and costs fields; flashnext_batch_host.zig's startup line names both the drafts mode and the lone
driver; tensorfold.zig's test root takes both backend.zig and batch_costs.zig.
…rst had taken it), doc comments on one line
…ssions

Conflicts: backend.zig keeps the sized-session fields beside #9's serial-drafts and costs; session.zig keeps moveTo
beside #9's encodeDrafts and heldDrafts; flashnext_batch_host.zig sets the sized sessions, then times the costs (on
a session too small to time, #9 serves unpriced), and its startup line lists the sessions and the drafts mode;
tensorfold.zig's test root takes both slots.zig and batch_costs.zig.
…rge left shadowing the lone driver (Backend.lone)
… an error; the lane core sweeps streams a release ends; harness on Backend.init's capacities

Review fixes (cwschroeder on cwschroeder#10):

- slots.resumePaused: LaneMemory keeps a paused stream waiting; any other move failure (GpuFailed, StateNotSettled,
  ...) ends it through Backend.failed: slots.endPaused marks it finished with an error the host reports as a failed
  request, and its session frees at once. Fault-injection test: the last larger session frees, one paused stream's
  move fails on the GPU (ended), one finds none free (waits), one moves.
- The lane core: a release can now end another stream, so Engine.sweep releases finished live streams until a pass
  ends none, from step's prune and from discard. Without it a stream ended during another's release could stay in
  the live list after the host freed its job (failAll, cancels and drop skip finished streams). Tests on the fake
  target, which gains a release hook: a stream ended by another's release in a step, and by a discard, leaves the
  live list.
- zig/tests/flashnext_batch.zig: Backend.init takes the sessions' capacities (refusing more than MAXR as init did),
  and its two generateFrom calls pass the thinking budget (.{}) that 1.0.8 added, which already broke the harness on
  04c8571.
… one field, and comments inside the lean rule
… state is named by its image frame, and the tower skips the images it holds
@BobClawblaw

Copy link
Copy Markdown
Contributor Author

Pushed 8a509ab5: image and video chats now resume from the prompt cache. Kept states are named by the image frame's shift, so a later turn with the same images resumes past them, and a turn that adds an image reads its prompt again. vision_served.py passes 19/19 on both hosts, replies equal a cache-off server's in multi-turn image and video chats, and the speed plan is unchanged (155/84 tok/s drafts, 0.11–0.12 s first token). Running vis_prompt.zig's tests, which no test root reached before, also caught a leak when frame() refuses bad spans; it's fixed here. The body is updated.

…ked to the grammar's tokens on the GPU, the first token drawn among them on the host

response_format, the guided_* fields, tool_choice "required" and a named function now work on Metal with --parallel 2
or more (libtfgrammar beside the binary). fz_mask_rows sets a constrained row's disallowed logits to -inf before the
argmax and tf_sample_full; rounds without a constrained stream skip it. The prompt pass keeps a grammar stream's last
row for its first draw (gpu_full's host reference of tf_sample_full, or the argmax), and the head's held drafts come
back as host tokens for the grammar. Backend.timeCosts moves to batch_costs.zig to keep backend.zig under 600 lines.
@BobClawblaw

Copy link
Copy Markdown
Contributor Author

Pushed 8e4ba6db: shared sessions now enforce structured output (response_format, guided_*, tool_choice: "required" and named functions).

  • Each verify row is masked to the grammar's tokens on the GPU before the argmax or sample. A round with no grammar stream skips the mask.
  • The first token is drawn on the host among the allowed tokens. The MTP drafts go to the grammar, so drafting stays on.
  • --parallel 1 still answers 400.

The PR body's new Structured output check lists the measurements. In short:

  • The recipe harness's structured check passes 18/18.
  • Drafted replies equal "draft": false.
  • Plain replies are token-identical to the previous commit's.
  • Decode speed is the same with a grammar on as off.

@cwschroeder cwschroeder left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Resolved at 440ca2b on source/CPU review. Follow-up: #986 (review)

Reviewed the structured-output increment from 8a509ab5 to 8e4ba6db. The first-token, held-draft, cache-resume and slot lifecycle paths showed no further material issue in this source pass. A CPU-only probe of the actual grammar.draw passed eight checks in both Debug and Fast; whole-tree zig fmt --check build.zig zig also passed with Zig 0.17.0. All four current-head CI checks are green.

The new masking kernel still needs the required kernel-contract registration and evidence below. I have not run Metal, a model, or the served speed/parity checks. The local native Safe build completed (45/45 steps). It does not compile the inline masking shader at build time, and I have not qualified the earlier vision/cache stack.

Please also link the command/source and raw case/token/timing receipts for the reported 18-case recipe check, so the reported parity and speed results can be traced to this head.

\\#include <metal_stdlib>
\\using namespace metal;
\\// a token whose bit is off gets -inf, so the argmax and tf_sample_full never draw it
\\kernel void fz_mask_rows(device bfloat* logits [[buffer(0)]], device const uint* bits [[buffer(1)]],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Register fz_mask_rows with the kernel contract before landing

This adds a new GPU kernel, but neither the explicit Metal contract runner nor contract/registry.zig includes it. The new four-row unit test therefore does not put this kernel through the contract's non-finite, repeat/aliasing, shape or recorded launch-limit gates. Green contract checks currently cannot detect a failure in this kernel.

CONTRIBUTING.md and the kernel contract require registration before landing. Please add its test/spec and runner entry, declare the in-place mask and non-finite rule (accum = .none), cover the served row envelope and mask-word boundaries, register the 256-thread launch with its required Metal limit records, and post the corresponding SUMMARY with controls and chips not run. This is a missing required gate, rather than a demonstrated masking error.

…place, byte-exact by its stated non-finite rule at 1 to 16 rows of the vocabulary, its 256-thread launch registered with M1/M2's compiled limits
@BobClawblaw

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Pushed 440ca2b7, which registers fz_mask_rows with the kernel contract.

The registration

  • Test spec: zig/tests/contract/metal_mask.zig, listed in metal_main.zig.
    • accum = .none, out = .bf16, in_place = true.
    • Stated non-finite rule: an allowed token keeps its bits (NaN, +Inf and -Inf included), and a disallowed one becomes -inf (0xff80).
  • What its check compares: every token's bits against that rule, byte for byte, not within a bound. At the non-finite gate it also requires each planted class (NaN, +Inf, -Inf) on both an allowed and a disallowed token in the first rows.
  • Envelope:
    • Served shapes: 16, 1 and 5 rows of the 248,320-token vocabulary (MAXR is 16).
    • Edges: the 256-thread group (n = 1, 255, 256, 257), so the mask's last word ends early at 1, 255 and 257.
    • Masks: whole words off, whole words on, and random words.
  • Launch: 256 threads and no threadgroup memory, in registry.zig. The offline compile records 1,024 threads on M1 and on M2 (--offline). metal_limits.zig finds its source.
  • Docs: docs/kernel-contract.md's coverage.

Run (M5 Ultra, tf-kernel-contract --offline DIR):

PASS nonfinite metal flash next fz_mask_rows: the stated rule holds: an allowed token keeps its bits (NaN, +Inf and -Inf included), a disallowed one becomes -inf (0xff80)
PASS repeat metal flash next fz_mask_rows: five runs byte-identical (7946240 bytes), inputs and guards untouched, no overlap
PASS shapes metal flash next fz_mask_rows: 7 shapes exact by its own check (worst 0.000 of the bound), 0 edges refused by the kernel
N/A  sums metal flash next fz_mask_rows: declares none sums
PASS limits here flash next grammar fz_mask_rows: 256 threads launched, the pipeline takes 1024; 0 threadgroup bytes and 0 set (declared at most 0, the GPU's most 32768)
PASS offline flash next grammar fz_mask_rows: 256 threads; M1 pipelines take 1024, M2 1024 (recorded 1024, 1024)
SUMMARY 27 kernels: 27 pass gates 1-4, 0 fail, 2 known findings; 5 controls: 5 red as planted, 0 missed; 0 limit problems; gates 119919 ms, limits 2340 ms
  • Chips not run: only M5 Ultra GPUs ran the gates. M1 and M2 were covered by the offline compiles; M3 and M4 by gate 5's declarations only.
  • Repo checks: zig build test, test-kernel-contract, test-golden, fuzz-cortheon, zig fmt --check and the lean check (no new problems against 8e4ba6db) pass.

The 18-case receipts are in evidence/2026-10-17-structured/, from a harness run on this PR's previous head 8e4ba6db (binary sha256 d6b9a33d..., the same binary as the PR body's runs). The commit above adds only the contract test and registration, so the served code is the same.

  • Command: python3 -m harness run qwen38-flashnext-vision-m5 --plan plan.toml.
  • Source: the structured check in harness/checks/server.py at recipe-harness 0baf46f. It now keeps every case's request, reply, finish, usage and seconds, and on a drafting target sends greedy and sampled cases again with "draft": false.
  • Result: 18/18 obey, drafted equals undrafted 12/12, and the bad regex is a 400.
  • Speed: structured_speed.json has the grammar-speed run's streamed replies, each with its usage, time to the first token and seconds.

@cwschroeder cwschroeder left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The kernel-contract registration request is resolved at 440ca2b7 on source/CPU review. metal_mask.zig checks every output bit against the stated rule, exercises the vocabulary shapes and mask-word/threadgroup edges, and is wired into the Metal runner and launch-limit registry. The served grammar source is unchanged from 8e4ba6db.

Locally, Zig 0.17.0 test-kernel-contract passes 18/18, whole-tree format passes, and zig build tf-kernel-contract -Dcpu=apple_m1 -Doptimize=safe -j2 --summary all builds successfully. I did not execute the GPU binary. All four current-head CI checks are green.

I also checked the 10 published evidence files at receipt commit cb1c5a08, verifying their Git blobs:

  • All 18 saved replies satisfy their requested schema, regex, choice or grammar and record HTTP 200 with finish: stop.
  • The 12 saved drafted/plain pairs have identical text and usage; the bad regex records 400. Plain responses do not retain separate status/finish fields.
  • All saved speed metrics recompute with the pinned harness formula. These are individual measurements, not a repeated equivalence test; JSON-object single-stream off/on is 184.4/179.8 tok/s.

The GPU contract result remains your reported M5 Ultra run, with the M1/M2 offline and M3/M4 declaration limits you described. This follow-up closes my registration request, without extending it into a cross-device qualification.

@ashhart

ashhart commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Thanks @BobClawblaw for registering the mask and preserving the 18/18 structured and 12/12 drafted/plain receipts. The M5 execution, offline compiles and unrun chip cells should remain distinct.

Our prepared merge also refuses a masked row with no selectable value, preserves image frame keys through cache lookup, and retains the target-format dispatch fix. The mask registration does not by itself qualify the image M-RoPE bindings or their resource records. Keep those checks and image/video cache, grammar, draft and concurrent-neighbor comparisons on the combined source. These receipts are useful evidence for their pinned head, not a device run of our merge.

Posted by Ash's AI Assistant Synthena

ashhart pushed a commit that referenced this pull request Oct 18, 2026
…ge chats by frame, and enforces structured output on shared sessions (#986)

The branch's eight commits come in as this one. It builds on #606's branch, whose own commit is already on this line.
Against this tree: GLM's labelled video groups stay, with Qwen's time-first layout added in the vision API; image
frame keys apply in lane_stream before the cache lookup; job reporting keeps the restore-failure reason under the same
key; the job, pricing and residency helpers move to files of their own, so each stays under 600 lines. The Qwen CUDA vision test expects the shared parser's own refusal of a negative video token id, which the parser now reads.
ashhart pushed a commit that referenced this pull request Oct 18, 2026
…past the trained window, and image refusals name the decoder's problem (#1005)

The branch's two commits after #986 come in as this one. Against this tree it keeps the shared forward's width-aware
calls, the vision and grammar fixes, the frame cache keys and every earlier regression test.
ashhart added a commit that referenced this pull request Oct 18, 2026
ashhart added a commit that referenced this pull request Oct 18, 2026
…6-token window (#1005), which move to 1.1.1

This reverts 5fe981eca and bd25c56b5 with our follow-ups 3bee91d59 and 9163df4f3, and 5f104a174, which only moved the
admission test after 9163df4f3. Five of the eleven contract probes their vision and rotary launches brought exceed
their float64 bounds on an M3 Ultra (fn_attn_prep_mrope at 2.20 times its bound, 15.81 with YaRN x4, and the target
and MTP rotaries at 2.09 to 2.20), and every supported Mac has to pass them. No later commit builds on either PR.
ashhart added a commit that referenced this pull request Oct 18, 2026
…ank path is a known issue

GLM-5.3-Flash's ModelOpt NVFP4 checkpoint on two CUDA ranks was not run for 1.1.0: on two GB10s its weights leave no
room for streams beside the memory reserve. The release's GLM work leaves that path as 1.0.12 ships it.
@ashhart ashhart mentioned this pull request Oct 18, 2026
@ashhart

ashhart commented Oct 18, 2026

Copy link
Copy Markdown
Owner

Thank you, @BobClawblaw, for Flash Next images/video and the cache equality receipts, and @cwschroeder for the contract review.

This work and its bound repair move to 1.1.1, the next patch release, with the long-window follow-up. Rotary probes exceeded their bounds on the M3 Ultra. The probe's allowance can reject ordinary rounding near sine or cosine zeros.

We need a reviewed bound repair, then the full Metal image/video, constrained-drafting, cache and concurrency gates.

Apple M3 Ultra Flash Next arithmetic checks passed 11/11; 12 plain and 18 offline limit failures leave vision/window changes held from the current 1.1.1 track pending fixes and reruns.

The separate shared-session cache-count and 7% lone-decode repairs are held for a later 1.1.x and are not carried by this 1.1.1 series.

Posted by Ash's AI Assistant Synthena

@cwschroeder

Copy link
Copy Markdown
Contributor

I can take the host-side bound repair and regression controls to help unblock this integration. I checked current main 76aafcf274e60a5e31a59ce145b2f71f7b366e9d, #986 at 440ca2b7, and the #1005 stack: I cannot find the reported Flash Next rotary probes in these published sources.

@ashhart, could you publish the prepared integration commit (including the rotary probes and resource records) and the M3 Ultra failure logs for the 12 plain / 18 offline limit failures? For the bound failure, the failing shape/position, reference, observed value and allowance would let us reproduce the exact case. Please include any bound repair already in progress so I can review or extend it without duplicating your work.

The host regression should accept the justified rounding case near sine/cosine zeros while still rejecting planted wrong-axis, wrong-position and wrong-scale errors. I will keep that separate from the image/video, grammar/drafting, cache and concurrent-neighbor device gates on the combined commit. No M3 device qualification is claimed by this offer.

ashhart added a commit that referenced this pull request Oct 19, 2026
Credit: @BobClawblaw, @cwschroeder, @sethforprivacy, original YaRN attribution and TensorFold maintainers. Follow-up to #986 and #1005.

Share the scalar rotary source with an exhaustive bounded-memory accuracy sweep. Cover every admitted position, frequency, image axis and pooling block for normal and YaRN tables. Record FP64 absolute/ULP extrema and actual normalization fixtures.

Bind receipts to the runtime compiler deployment, OS, GPU, compile options and complete source/table identities. Unrecorded or incomplete deployments refuse numerical probes. Signed pointwise intervals preserve FP32 phase rounding and the allowed contraction schedules.

All five changed production launches remain pending. Actual exhaustive tables, source-matched device probes and within-noise decode/prefill gates are required before qualification.

Keep the absolute pooling LOG2BASE parameter in the constant address space required by the shared helper. All native and probe bindings remain identical. Check every production role and diagnostic call site in the shader-free source guard. Metal compilation and complete fresh GPU receipts remain required.

Co-authored-by: BobClawblaw <288991054+BobClawblaw@users.noreply.github.com>
Co-authored-by: Christian Schröder <1188270+cwschroeder@users.noreply.github.com>
Co-authored-by: Seth For Privacy <40500387+sethforprivacy@users.noreply.github.com>
@ashhart

ashhart commented Oct 19, 2026

Copy link
Copy Markdown
Owner

Thanks @BobClawblaw, with @cwschroeder and @sethforprivacy. 1.1.1 ships Flash Next Metal images/video with media-aware cache identity and the qualified precise rotary work: 11 probes and 34 comparison cells retain tokens on M3 Ultra and M5 Ultra. Reported M3 cost is unchanged; M5 decode costs 0.4-0.7%, with cold prompts unchanged. Closing that scope. The later 440ca2b grammar-mask contract coverage goes into 1.1.2 once alongside #1005, with credit and integrated contract checks; its mask kernel is unchanged. The separate shared-session/lone-decode repair remains outside this release.

Posted by Ash's AI Assistant Synthena

@ashhart ashhart closed this Oct 19, 2026
BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
…ge chats by frame, and enforces structured output on shared sessions (ashhart#986)

The branch's eight commits come in as this one. It builds on ashhart#606's branch, whose own commit is already on this line.
Against this tree: GLM's labelled video groups stay, with Qwen's time-first layout added in the vision API; image
frame keys apply in lane_stream before the cache lookup; job reporting keeps the restore-failure reason under the same
key; the job, pricing and residency helpers move to files of their own, so each stays under 600 lines. The Qwen CUDA vision test expects the shared parser's own refusal of a negative video token id, which the parser now reads.
BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
…past the trained window, and image refusals name the decoder's problem (ashhart#1005)

The branch's two commits after ashhart#986 come in as this one. Against this tree it keeps the shared forward's width-aware
calls, the vision and grammar fixes, the frame cache keys and every earlier regression test.
BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
…,048,576-token window (ashhart#1005), which move to 1.1.1

This reverts 5fe981eca and bd25c56b5 with our follow-ups 3bee91d59 and 9163df4f3, and 5f104a174, which only moved the
admission test after 9163df4f3. Five of the eleven contract probes their vision and rotary launches brought exceed
their float64 bounds on an M3 Ultra (fn_attn_prep_mrope at 2.20 times its bound, 15.81 with YaRN x4, and the target
and MTP rotaries at 2.09 to 2.20), and every supported Mac has to pass them. No later commit builds on either PR.
BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
…'s NVFP4 two-rank path is a known issue

GLM-5.3-Flash's ModelOpt NVFP4 checkpoint on two CUDA ranks was not run for 1.1.0: on two GB10s its weights leave no
room for streams beside the memory reserve. The release's GLM work leaves that path as 1.0.12 ships it.
BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
Credit: @BobClawblaw, @cwschroeder, @sethforprivacy, original YaRN attribution and TensorFold maintainers. Follow-up to ashhart#986 and ashhart#1005.

Share the scalar rotary source with an exhaustive bounded-memory accuracy sweep. Cover every admitted position, frequency, image axis and pooling block for normal and YaRN tables. Record FP64 absolute/ULP extrema and actual normalization fixtures.

Bind receipts to the runtime compiler deployment, OS, GPU, compile options and complete source/table identities. Unrecorded or incomplete deployments refuse numerical probes. Signed pointwise intervals preserve FP32 phase rounding and the allowed contraction schedules.

All five changed production launches remain pending. Actual exhaustive tables, source-matched device probes and within-noise decode/prefill gates are required before qualification.

Keep the absolute pooling LOG2BASE parameter in the constant address space required by the shared helper. All native and probe bindings remain identical. Check every production role and diagnostic call site in the shader-free source guard. Metal compilation and complete fresh GPU receipts remain required.

Co-authored-by: BobClawblaw <288991054+BobClawblaw@users.noreply.github.com>
Co-authored-by: Christian Schröder <1188270+cwschroeder@users.noreply.github.com>
Co-authored-by: Seth For Privacy <40500387+sethforprivacy@users.noreply.github.com>
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