Skip to content

server: GLM tool calls stream their arguments, and one-call streams match the whole reply (upstream #1975, #1396) - #2

Merged
devopsfortunato merged 5723 commits into
mainfrom
card/t_14cd2239
Nov 3, 2026
Merged

devopsfortunato merged 5723 commits into
mainfrom
card/t_14cd2239

Conversation

@devopsfortunato

Copy link
Copy Markdown
Owner

Streamed chat replies with tools now match the whole (non-streamed) reply in more cases, and GLM tool calls stream their arguments while the model writes them. The work comes from upstream ashhart/TensorFold issues ashhart#1975 and ashhart#1396. I picked those two after going through all 121 open upstream issues and keeping the ones a single DGX Spark can test. Every check below ran on that Spark (GB10, aarch64, Ubuntu 24.04, Zig 0.17.0).

Base branch note. This fork's main is still the Python 0.3.4.1 tree, 5,721 commits behind ashhart/TensorFold:main. Upstream's Python line is frozen and every open issue is about the Zig engine, so this branch starts from upstream main at cfcbecb0b and adds two commits. The PR list shows the whole upstream history because the fork's main hasn't been synced. Only the last two commits are this work: compare cfcbecb0b...card/t_14cd2239. After the fork's main is synced to upstream (the GitHub "Sync fork" button), this PR will show just those two commits.

What this changes

Commit Issue Before After
server: one-call streams send the whole reply's content Refs ashhart#1396 (items 1, 2; 4 rechecked) With parallel_tool_calls: false, the stream dropped a rejected envelope such as <foo:tool_call>, <TOOL_CALL> or <Tool_Call> that the whole reply keeps in its content. It also dropped a rejected envelope before a max_tokens cut, and the cut prose's trailing newline. The stream sends exactly the whole reply's content and finish reason in every case listed in ashhart#1396.
server: GLM tool calls stream their arguments as they are written Refs ashhart#1975 (GLM part) A GLM call (<tool_call>NAME<arg_key>K</arg_key><arg_value>V</arg_value>…) aborted the streamer, so the whole call went out in one piece at the finish. The header goes out once the name is whole and names an offered tool. String values then stream as JSON-escaped pieces, holding back only a partial </arg_value>, and typed values go out as compact JSON at their closer. The joined pieces equal the whole reply's arguments byte for byte.
same commit found while doing it (same bar) E1/E2: after one call had streamed, any envelope the streamer couldn't handle (an unoffered <function=other>, a GLM or JSON payload, <|tool_call>, <NAME:tool_call>) aborted it, so every later call was missing from the stream. The whole reply still returned them. D1/D2: typed XML values streamed as ["a", "b"] while the whole reply sent ["a","b"]. Once something has streamed, such an envelope is parsed at its closer and its calls are sent. Typed values stream compact.

Nothing changes for requests without tools, for whole replies' content, or for the model's tokens. These are parser and stream changes only, so no inference path is affected.

DSML (the other half of ashhart#1975) is left as it is, on purpose. Streaming a DSML invoke before its block closes would break the existing containment contract in stream_dsml_test.zig ("unfinished and malformed DSML never emit their complete nested call"): an unclosed block, or a closed one whose invoke never closes, must stream no call. DSML blocks also parse all or nothing (tool_dsml.calls): one malformed or unoffered invoke rejects the whole block, so a call already streamed from an earlier invoke can't be taken back. That trade-off is a maintainer decision. Upstream also has its own DSML streamer for DeepSeek-V4.1's served engine (zig/src/families/deepseek_v41/serve/tool_stream.zig), which is related.

Steps taken

  1. Checkout. The card branch came from the fork's main (Python 0.3.4.1, bb4b4a358). The fork has no unique commits, and its main is an ancestor of upstream/main. I reset card/t_14cd2239 to upstream/main cfcbecb0b, following the fork rule "a fix that still matters is re-made on a fresh branch from upstream/main", and read upstream's AGENTS.md, CONTRIBUTING.md, docs/atc/SKILL.md and docs/recipes/SKILL.md.
  2. Issues. I saved all 121 open upstream issues (bodies and comments) and an ATC board snapshot (atc --repo ashhart/TensorFold --json, 3 Nov 11:40 UTC), then classified each issue by the hardware its check needs. The full table is at the end. 33 can be tested on one Spark, 46 only partly, and 42 not at all (TP=2/3/4, Metal, HIP, XPU, Vulkan, RTX-only). Of the 33, 13 have no claim and no open PR. I picked the two that (a) are free, (b) are host-testable, so they don't need the Spark's GPU, which is serving a model I must not stop (docs/recipes/SKILL.md: "Do not stop somebody else's server"), and (c) have a bar the maintainer already set ("stream and whole reply send the same content, arguments and finish reason in every case"). The other free ones need a model loaded on the busy GPU (Flash Next CUDA on GB10: NV_ERR_NO_MEMORY in request state/KV allocations on v1.2.0 and main ashhart/TensorFold#1856, Living Weights: the Qwen3.8-27B CUDA learner's gradient is 1.6-1.8x too large at the last prompt row ashhart/TensorFold#1806, Living Weights on Qwen3.6-35B-A3B on CUDA ashhart/TensorFold#1802, Living Weights: conflicting facts, updates and forgetting (measurements + proposal) ashhart/TensorFold#678, Living Weights: unrelated-reply drift and recall after lessons (follow-up to #646) ashhart/TensorFold#1389), nvcc 13.2 where this Spark has 13.0 (RFC: qualify nvcc 13.2 for the release fatbins (native FP4 decode, prompt16 −4.5 %, bit-equal) ashhart/TensorFold#1448), the moss_image engine (moss_image: serve pictures over POST /v1/images/generations ashhart/TensorFold#1573), or a maintainer decision first (RFC: --profile stable | fast-exact | fast, one table for the opt-in switches ashhart/TensorFold#1660, RFC: one load-time fit for every affine-4 draft head, weighted by diag(H) (Flash-Next: affine-4 head near bf16's acceptance, 7.8 % less time) ashhart/TensorFold#1870, glm: the released Python engine renders the --no-thinking prompt 6-7 tokens shorter than the checkpoint template; native renders it verbatim ashhart/TensorFold#1911, Studio: carry #941 to #944 onto main as reviewed increments ashhart/TensorFold#1204).
  3. Spark setup. I cloned upstream at cfcbecb0b into a scratch directory on the Spark and used the Zig 0.17.0 already installed there (zig version → 0.17.0). The local tree was rsynced over for every run. Nothing on the Spark was installed, stopped or reconfigured.
  4. Baseline. I ran the no-GPU CI's Zig and Python steps on unmodified main; results are in the table below.
  5. Recheck. A throwaway probe test, deleted afterwards, drove the real server path (openai.run with a fake byte-token engine, batched and one character per round) for every case in Streamed chat: four more cases where the stream and the whole reply disagree (single-call envelopes, max_tokens cuts, DSML whitespace) ashhart/TensorFold#1396 and some suspects:
  6. Implementation. Two coders worked in parallel on separate parts of the code: the GLM streaming state machine in a new tool_glm_stream.zig (tool_stream.zig would otherwise pass 600 lines), and the one-call TextFilter/endProse/kept fix. Each new test was confirmed red on the old code first.
  7. Review. Two rounds, both by GLM-5.3.
    • Round 1: one medium finding. The stream repaired a lone </arg_key> unconditionally, while repairGlmKeys repairs only when closers outnumber openers, so the stream could invent an argument. The counter-example <tool_call>lookup<arg_key>a<arg_key>b</arg_key><arg_value>1</arg_value> w</arg_key><arg_value>2</arg_value></tool_call> is now a test that failed without the guard. The fix: repair only when the counts seen so far say repairGlmKeys would, with the counts carried incrementally so it stays linear. Two simplifications were applied too (one glmStep helper, one sendValue helper).
    • Round 2: approve, no correctness findings.
  8. Final checks. I cleared the Spark clone's local Zig cache, reran every step on the final tree, and checked that the first commit builds and passes on its own.

Checks on this head

Head commit: 3fda03c56 (parent 6c30fd845, base cfcbecb0b). Machine: DGX Spark GB10 (sm_121), Linux 7.0.0-1019-nvidia aarch64, Zig 0.17.0, Python 3.12.3. Each run used an empty local .zig-cache. The Spark's GPU stayed on its existing server for the whole run and these steps don't use it.

Step (no-GPU CI, .github/workflows/no-gpu.yml) Base cfcbecb0b This head
zig build test --test-timeout 10m 489/489 steps; 6534/6580 passed, 46 skipped, 0 failed 489/489 steps; 6546/6592 passed, 46 skipped, 0 failed (+12 new tests)
zig build test-kernel-contract 22/22 steps; 177/177 22/22 steps; 177/177
zig build test-golden 5/5 steps (0 unexpected differences) 5/5 steps, no golden file changed
zig build test-server-security test-pull-security 14/14 steps; 28/28 14/14 steps; 28/28
zig build fuzz-cortheon 3/3 steps; 19/19 3/3 steps; 19/19
zig build test-cuda-host (CUDA host subset) 293/293 steps; 1273/1277 (4 skipped) 293/293 steps; 1429/1441 (12 skipped)¹
zig build (plain, every target) 31/31 steps 31/31 steps
zig build test-server-cpu (server unit tests) all passed 962/962; the first commit alone: 959/959
zig build test-tool-parse-host test-tool-visibility-http-host test-tool-required-host test-golden — 14/14 steps; 46/46
zig fmt --check build.zig zig clean clean
python3 -m unittest discover -s tools/tests -p "test_bench*.py" 74 OK 74 OK
python3 -m unittest discover -s tools/zig -p flashnext_batch_serve_test.py 8 OK 8 OK
python3 -m unittest discover -s tools/zig -p build_options_test.py 5 OK (2 skipped) 5 OK (2 skipped)
Lean checker tools/zig/lean_check.py + lean_new.py 424 problems 424 problems, 0 new
git diff --check clean clean

¹ The test-cuda-host totals count only run steps that executed in that invocation. Some of them share runs with the preceding test step, so the count moves between invocations. Every skip on both sides is an environment skip: libjpeg-turbo 3.1.4.1 not found / libwebp 1.6.0 not found pixel cases. There were 0 failures on both sides.

New tests: 12 total, each red before its fix except the regression-only cases noted.

  • zig/src/server/tool_glm_stream_test.zig (8 tests):
    • the 2,000-byte value streams before it closes and joins to the whole reply's arguments;
    • every feed boundary for three replies, including typed values, non-ASCII text, a repaired key and the round-1 counter-example;
    • ten GLM shapes: newlines, junk between pairs, escaped keys, emoji, a key without a value, and others;
    • unstreamable envelopes (unoffered, zero-argument, Gemma, JSON, a < in the name) send nothing;
    • a GLM call after a DSML block stays buffered as today;
    • the end-token closeCall path;
    • cut replay equals tool_glm_cut;
    • a whole-vs-streamed chat table through the real server path, batched and per character: prose plus GLM, two GLM calls, E1, E2, D1/D2, a max_tokens cut inside a value, a zero-argument call, a key without a value.
  • zig/src/server/tool_single_stream_test.zig (4 tests), every Streamed chat: four more cases where the stream and the whole reply disagree (single-call envelopes, max_tokens cuts, DSML whitespace) ashhart/TensorFold#1396 case whole vs streamed in one-call mode, batched and per character: 1a–1d, 2a–2c, 4a/4b with and without a trailing newline in both modes, and an accepted call whose prose streams before it.

ashhart#1975 probe (2,000-character Write content, fed 3 bytes at a time):

headers argument deltas first argument byte joined == whole reply
main 0 0 never (state abort, the call goes out whole at the finish) n/a
this head 1 677 while the value is still being written yes (2,033 bytes)

Not run, and why:

  • The Windows, macOS release-path and macOS dev-tools CI legs need those OSes.
  • The NFKD-table check needs Python 3.14's unicodedata 16.0.0; the Spark has 3.12, and this change doesn't touch the tables or their generator.
  • GPU and model checks (kernel contract on device, served exactness and speed receipts) don't apply: no kernel, engine or token path changed. The Spark's GPU was also serving a model throughout, which I left untouched.
  • No /v1/messages end-to-end test. Anthropic's input_json_delta is built from the same argument deltas (anthropic.zig drain), so GLM calls now produce many of them instead of one.

Receipt

n/a for inference. The change is in the HTTP server's tool-call parsing and streaming and never reaches the model or its tokens. "draft": false, resume and concurrency exactness are unaffected for the same reason.

Known limitations

These cases are left as they are, documented so a maintainer can decide.

  1. A </tool_call> written inside a GLM <arg_value> isn't special-cased. The XML path has the same limitation: the whole reply's parser ends the envelope there, while the stream has already sent part of the value.
  2. If the end token arrives inside a GLM key (<arg_key>K</arg_key> with no value yet), closeCall's strict reading appends no closers. The stream keeps the call it opened and the whole reply returns none. XML has the same case for an end token inside <parameter=.
  3. repairGlmKeys decides over the whole payload: it gives up on all repairs at a wordless lone </arg_key>, and repairs only when closers outnumber openers. A malformed call with two problems, where later text changes that decision, can still differ. The single-problem case is guarded and tested. This is noted in the module header.
  4. In one-call mode, prose written after a hidden call envelope now arrives at the finish, as a single content delta from the reply's final content, instead of streaming. This is what lets the stream equal the whole reply.
  5. Optional simplifications from review round 2, not applied (net: -13 lines possible): drop typedSchema in tool_stream.zig; add keyAt/pendingTag helpers in tool_glm_stream.zig; set .counted in the Glm reset and drop the clamp.

Reviewed by GLM-5.3 (2 rounds: round 1 changes needed, fixed; round 2 approve).

Checklist

  • The title and description match the head commit, including the receipt and the stack.
  • New tests fail before the change, pass after it, and need no extra dependency (host only).
  • Comments and docstrings are one line. No measurements or history in the source.
  • No personal data, machine names, internal hosts or local paths in the diff or its commits. No AI attribution lines (commits use the GitHub noreply address).
  • Inference unaffected: tokens, lane rounds and drafting are untouched.
All 121 open upstream issues, by whether one DGX Spark can test them (ATC board snapshot, 3 Nov 2026)
Issue One Spark? Hardware ATC / open PRs Why
#503 Geometry-general native Qwen3.5 dense loader no Metal claimed, PR 1817,1851,1871 Metal-only gates: captured MLX kernels and the 9B run need an M5.
#548 Zig CUDA shared layers below lanes.Backend (tracker) partial one GB10 free Shared CUDA steps run on GB10; Metal marks and GLM TP=2/3 pairs need other hardware; marks list is maintainer's call. (needs a maintainer decision)
#678 Living Weights: conflicting facts, updates, forgetting yes one GB10 free Restart-case host test and the delete device run (Nemotron 30B) fit one Spark; dated-change design deferred.
#725 MiMo-V2.6-Flash on CUDA across two GPUs (EXL3, DFlash) no 2+ GPUs/nodes free, PR 728 Two GB10s needed; waits on maintainer's call on the DeepSeek stack. (needs a maintainer decision)
#743 Restore single-Mac GLM-5.3-Flash on M3 Ultra no Metal free Gate is the M3 Ultra Metal run.
#840 Python engine features to carry into native engine partial one GB10 free Landed rows used host tests plus GB10 receipts; GLM fp32 activations item needs a Mac.
#850 Ling-3.1-flash on CUDA: KDA kernel, then TP=1/TP=2 partial one GB10 free KDA, host fixtures and TP=1 run on one Spark; TP=2 and 3-4 Spark runs need more.
#974 community-v1: 1-8 rank results and attached manifest partial 2+ GPUs/nodes free, PR 1985 1-2 rank manifest checks fit the Spark; more ranks need agreement, then a 4-Spark run. (needs a maintainer decision)
#1136 Dense BF16 Qwen3.5 CUDA scoring yes one GB10 claimed, PR 2020 One GB10 serves 27B BF16; claimed, PR ashhart#2020 open.
#1175 New family: Recursive Flint MoE on Metal no Metal free Proposed gate is one M5 Ultra.
#1204 Studio: carry ashhart#941-ashhart#944 onto main as increments yes host-only free Reviewed app increments with host checks.
#1222 Gemma 4 E4B: per-layer inputs and shared KV partial one GB10 claimed Family parts host-tested and CUDA lands on GB10; Intel Arc receipts need an Arc.
#1236 Tensor parallel across Metal and CUDA ranks (tracker) no Mac+NVIDIA claimed Mixed Metal+CUDA group needs a Mac plus GB10.
#1247 Ling-3.0-tiny: Ling family on Metal, then ARM CPU partial Metal claimed Host family package landed; Metal PRs and the Qualcomm board remain.
#1269 Cosmos3-Edge reasoner as native family (Metal+CUDA) partial one GB10 free CUDA half has GB10 receipts; Metal PR comes first per the maintainer.
#1293 Metal: KV pages from the core pool no Metal claimed, PR 1850 Metal-only steps.
#1312 GLM CUDA: keep conversation states on disk partial 2+ GPUs/nodes free, PR 1411 Host image-layout test runs on Spark; pair receipts need two GB10s. (needs a maintainer decision)
#1324 DeepSeek-V4.1 at world 4 on four GB10s no 2+ GPUs/nodes claimed All attests are world-4/TP=2 on Sparks.
#1329 Sage-A1.5 on the XPU backend no XPU free, PR 1425 Xe2 kernels and receipts run on an Intel Arc Pro B70.
#1334 Qwen 27B CUDA: resume only at the 4,096-row grid yes one GB10 free, PR 1335 Receipts and the default cut decision come from GB10 runs; PR ashhart#1335 open. (needs a maintainer decision)
#1360 Cloudflare Clef decisions on native CUDA partial one GB10 claimed CUDA measurements on one Spark; HIP-first stack needs a Radeon.
#1374 LFM2.5-24B-A2B on Metal with DSpark drafter no Metal free, PR 1394 All receipts are M5 Ultra/M4 Pro Metal runs.
#1389 Living Weights: drift and recall after lessons yes one GB10 free The 155-question panel reruns on one Spark; drift thresholds await the maintainer. (needs a maintainer decision)
#1395 DeepSeek-V4.1 TP=2: repeat reply can differ no 2+ GPUs/nodes free Repro is TP=2 on two GB10s.
#1396 Streamed chat: stream vs whole reply disagreements picked host-only free Parser-only change; host recheck of items 1, 2, 4 on current main.
#1399 Hermes Agent and OpenClaw provider plugins partial Mac+NVIDIA free, PR 1913 Loopback fixture tests are host-side; release E2E needs a Mac and a GB10.
#1409 RFC: shared CUDA device primitives yes one GB10 free, PR 2018 SASS compares offline; contract and token checks run on the GB10; PR ashhart#2018 open.
#1414 llama family: Nanbeige4.3-33B on Metal partial Metal claimed, PR 1415,1782 Metal family gate on M5 Ultra. (needs a maintainer decision)
#1419 EXL3 Qwen3.8-Flash-Next on CUDA sm_120 pilot partial one GB10 claimed, PR 1441,1442,1443,1444,1445,1446 sm_120-only pilot device gates; host suite and offline sm_121 records run on Spark.
#1420 Native Vulkan backend for Linux GPUs no Vulkan free, PR 1544,1990 Mesa Honeykrisp on M1 Macs.
#1431 GLM CUDA: latent split across ranks at TP=2 no 2+ GPUs/nodes claimed Receipts are TP=2 on two GB10s and TP=3.
#1448 RFC: qualify nvcc 13.2 for release fatbins yes one GB10 free nvcc 13.2 builds, bit-equal and prompt16 checks run on a Spark (needs nvcc 13.2; this Spark has 13.0).
#1450 App: integrate workflow improvements, engine 1.1.6 partial Metal free Linux app suite runs on Spark; macOS packaged checks need a Mac.
#1481 Benchmarks: pending v1 receipt, protocol_complete=false partial decision-only claimed Host JSON/validator checks reproducible; the receiver predicate needs its maintainer. (needs a maintainer decision)
#1486 Flash Next Metal: grouped gate/up, two verified lanes no Metal free Served gates are M3/M5 Ultra runs.
#1488 LFM2.5-24B-A2B on CUDA: plain serve yes one GB10 claimed, PR 1492,1503 Plain CUDA serve of LFM2.5-24B-A2B; claimed, PRs ashhart#1492 ashhart#1503 open.
#1490 GLM CUDA: CUDA graphs for decode and verify partial 2+ GPUs/nodes free TP=1 graph parity runs on one Spark; profile and served numbers also need TP=2.
#1491 Native Metal: LFM3-1.7B and LFM3-VL-2B MLX checkpoints partial Metal claimed, PR 1502,1508,1514 CUDA NVFP4/FP8 slices run on GB10; Metal slices need Macs.
#1497 GLM CUDA: pad KDA input stack for glm_mm at TP=3 no 2+ GPUs/nodes claimed Tables are TP=3 plus TP=2 parity.
#1511 KV pool: host-RAM tier yes one GB10 claimed, PR 1444,1501,1522 kv_tier host tests plus EXL3 Flash Next K/V device receipts; claimed, PRs open.
#1520 Ornith-1.5-122B on Metal with DFlash2 drafter no Metal claimed Gate is M5 Ultra served checks.
#1529 moss_image: native flow-matching schedule, per-step gate yes one GB10 claimed Host-compared schedule plus CUDA teacher check; claimed.
#1531 moss_image: generate from options, no case file yes one GB10 claimed Builds on CUDA for Spark; claimed.
#1540 Qwen3.8 CUDA NVFP4 prefill launch fusion yes one GB10 claimed Contract on sm_121 and A/B with Qwen3.8-27B-NVFP4; claimed.
#1549 Nemotron 3.5 Transcribe 30B-A4B on native CUDA yes one GB10 claimed, PR 1631,1632 CUDA on one GB10 against NeMo Speech; claimed, PRs open.
#1559 GLM CUDA TP=3: prompt_logprobs gather hangs ranks no 2+ GPUs/nodes claimed, PR 1571 Repro uses three GB10 ranks at TP=3.
#1563 DFlash2 on HIP for Qwen3.8-27B (gfx1151) no HIP claimed, PR 1694 gfx1151 only.
#1567 Living Weights on GLM-5.3-Flash (CUDA and Metal) partial Metal free CUDA pocket slices run on GB10; Metal and TP=2 receipts need more hardware.
#1568 Living Weights on Qwen3.8-27B on Metal no Metal free, PR 2023 Metal-only pocket.
#1569 Living Weights on DeepSeek V4.1 Flash (CUDA, Metal) partial Metal free CUDA EXL3 pocket runs on GB10; Metal half needs a Mac.
#1570 GLM CUDA: 4096-row prompt chunks cut TTFT 13% at TP=3 no 2+ GPUs/nodes claimed, PR 1576 Three GB10s at TP=3.
#1572 GLM Metal 1.1.8: idle memory, restart frees 44.5 GiB no Metal claimed M5 Ultra footprints; rerun is Mac-only.
#1573 moss_image: serve images over /v1/images/generations yes one GB10 free Fake-engine route tests are host-side; MOSS-Image CUDA host builds on Spark.
#1578 Backlog: land open PRs, fix open issues partial decision-only free Tracker; each item needs maintainer triage. (needs a maintainer decision)
#1581 d1-2B (LFM3-VL decisions) on native CUDA partial Metal claimed CUDA parity runs on Spark; Metal receipts need the M5 Ultra.
#1583 glm cuda: load Apache-2.0 DFlash2-G drafter no 2+ GPUs/nodes claimed Gate demands TP=2 and TP=3 receipts.
#1587 Living Weights: Nemotron lesson refused in some runs partial Metal free, PR 2028 slide_verify host tests run anywhere; repro was M3 Ultra Metal.
#1594 MCDMA over ConnectX; serve --cluster on real hosts partial Mac+NVIDIA claimed, PR 1957 Link-selection host tests run on Spark; collective gate needs Mac plus GB10.
#1597 HIP Flash Next: WMMA sparse attention (gfx11) no HIP claimed, PR 1622,1658,1877 gfx1151 HIP runner only.
#1601 HIP: 16/32-row routed-expert prompt tile no HIP free, PR 1658 gfx1151 only.
#1602 HIP Flash Next: first request after load at half speed no HIP free, PR 1615 gfx1151 only.
#1603 HIP Flash Next: fuse ~850 small kernels per verify round no HIP free gfx1151 only.
#1621 Qwen3.8-27B on pre-M5 Macs: 32 rows a pass for one stream no Metal free, PR 1841 Measured on M3 Ultra/M4 Max.
#1625 DeepSeek-V4.1 prompt cache: 2048 boundaries, ignores cache-gib partial one GB10 claimed, PR 1892 Items 2-3 testable at TP=1; item 1 needs a world-4 run.
#1627 LFM3-Omni-2B on Metal and CUDA partial Mac+NVIDIA claimed, PR 1635 CUDA leg on GB10; Metal leg needs an M5 Ultra.
#1647 Metal: 100% GPU residency and ~115 W persist after one request no Metal claimed Repro on Macs only.
#1654 Nemotron 3.5 Bolt CUDA: BoltForward 500s under concurrency yes one GB10 claimed Bolt serves on one GB10; claimed by the maintainer.
#1657 CUDA: one paged prompt store for every family on the KV pool yes one GB10 claimed, PR 1755 Per-family GB10 device runs; claimed, PR ashhart#1755 open.
#1660 RFC: --profile stable / fast-exact / fast yes one GB10 free Host/fake-engine suites plus GB10 gates; RFC awaiting decisions. (needs a maintainer decision)
#1661 HIP Flash Next on gfx1151: branch results as reference no HIP claimed, PR 1667 AMD Strix Halo only.
#1664 dsv41: structured output and required tool calls no 2+ GPUs/nodes claimed, PR 1665 Gate requires grammar checks at TP=2.
#1666 LFM3-1.7B on the XPU backend partial XPU claimed, PR 1696 FP64 host replays run on Spark; Iris Xe serve needs the GPU.
#1676 DeepSeek-V4.1 on Metal (M3 Ultra) partial Metal claimed, PR 1720,1721,1722 Host-tested parts on GB10; serving stack needs the M3 Ultra.
#1693 Linux command sandbox: close the git and worktree gaps yes host-only claimed, PR 1699 bwrap conformance and mount-plan unit tests are CPU-only; claimed, PR ashhart#1699 open.
#1700 CUDA: narrow affine-4 MLP tile tuning for RTX PRO 4500 no one GB10 free, PR 1730 Gain measured only on sm_120 RTX PRO 4500.
#1701 CUDA: context-aware FP8 attention row tiles for RTX PRO 4500 no one GB10 free, PR 1731 Receipts only from sm_120 RTX PRO 4500.
#1754 Flash Next CUDA: serve Qwen3.8-Flash-Next-NVFP4 on GB10 and TP2 partial 2+ GPUs/nodes claimed, PR 1784,1792,1846,1847,1848,1849 PR1 verifies on one GB10; PR2 needs TP=2; claimed. (needs a maintainer decision)
#1757 idol: image-to-3D denoiser as a native Metal family partial Metal claimed, PR 1766,1767,1773,1774,1775,1780,1787,1858,1859,1860,1861,1862 Denoiser lands on Metal; CUDA PR runs on GB10.
#1758 Proposal: off-by-default tap for per-step MoE top-k picks yes one GB10 free, PR 1886 Tokens equal on/off and speeds at 1/8 streams fit one GB10; PR ashhart#1886 open.
#1762 Flash Next CUDA on GB10: short series of byte-identical PRs yes one GB10 free, PR 1786,1866,1942,2008 Receipts run on one GB10 with FP4-Local; four PRs open.
#1798 qwen3_5_moe: experts' prompt sort on one GPU thread partial Metal claimed, PR 1799 Byte checks on GB10; gate wants M5 Ultra prompt speed.
#1801 Living Weights on every model and backend partial Mac+NVIDIA claimed CUDA parts on GB10; Metal learners need Macs.
#1802 Living Weights on Qwen3.6-35B-A3B on CUDA yes one GB10 free Learner checked on a GB10; MoE backward trace is host-side.
#1803 Living Weights on Gemma 4 partial Mac+NVIDIA free CUDA side checked on GB10; Metal side needs a Mac.
#1804 Living Weights on Qwen4 dense partial Mac+NVIDIA free, PR 2021 CUDA learner checks on GB10; Metal checks need a Mac; PR ashhart#2021 open.
#1806 Living Weights: 27B CUDA learner gradient too large at last row yes one GB10 free tf-qwen35-learn runs on GB10 (needs the 27B loaded); maintainer PR ashhart#2029 open.
#1821 hip: prompt attention tile non-finite cases differ from float64 no HIP free gfx1151 HIP kernel contract. (needs a maintainer decision)
#1827 CUDA memory regression: Qwen3.5-4B on a 4 GB RTX 3050 partial one GB10 claimed, PR 1879 Scratch sizing on GB10; the 4 GB fit needs an RTX 3050 Laptop.
#1828 GLM CUDA TF_GLM_KV_SHARE: image prompt never keeps state yes one GB10 free, PR 1833 Image keep/resume verifies at TP=1; PR ashhart#1833 open.
#1829 Flash Next CUDA across ranks: kept state not restored drafting no 2+ GPUs/nodes free Restore check needs TP=2 with MTP drafts.
#1834 GLP control vector support for Qwen3.8-27B on Metal no Metal free, PR 1835 Server refuses any backend but Metal.
#1852 RoPE inverse frequencies: copies differ from transformers 1-5 ulp partial one GB10 free, PR 1998,2000 fp64 host reproduction runs on Spark; Metal entries need Macs; two PRs open. (needs a maintainer decision)
#1856 Flash Next CUDA on GB10: NV_ERR_NO_MEMORY in state/KV allocations yes one GB10 free Rerun on 1.2.1/main needs Flash Next loaded on the GB10 (this Spark's GPU is busy serving). (needs a maintainer decision)
#1857 Memory admission on GB10: page-cache growth moves the minimum yes one GB10 free, PR 1924 Load-time admission on one GB10; PR ashhart#1924 open.
#1863 Host fp32->bf16 copies: panic on negative NaNs in Debug yes host-only free, PR 1991 Host-only zig tests of bf16 copies and layer-pattern parsers; PR ashhart#1991 open.
#1870 RFC: one load-time affine-4 fit for draft heads yes one GB10 free Host fit plus Flash-Next TP=1 arms; RFC needs a maintainer choice. (needs a maintainer decision)
#1893 Flash Next on Metal: --parallel 2+ loses prompt-cache reuse no Metal free Measured on M5/M3 Ultra Metal builds.
#1894 Flash Next on Metal: structured output and tool_choice no Metal free, PR 1969 Flash Next Metal engine checks; PR ashhart#1969 open.
#1895 Proposal: POST /v1/rerank and Qwen3-Reranker on Metal partial Metal free, PR 1981 Route host tests run on Spark; family is Metal-first; PR ashhart#1981 open.
#1899 CUDA (TP=2): cached_tokens_total grows ~2x the prompt no 2+ GPUs/nodes free Counters from TP=2 on the Python 0.6.0 path; native has no such path.
#1909 Prompt cache: bounded intermediate checkpoints yes one GB10 claimed, PR 1953 Host reproduction plus TP=1 checks; claimed, PR ashhart#1953 open.
#1910 glm cuda native: copy drafts missing no 2+ GPUs/nodes free, PR 2022 Parity gate is A/B at TP=2; PR ashhart#2022 open.
#1911 glm: Python renders --no-thinking prompt 6-7 tokens shorter yes one GB10 free Maintainer will measure both renderings before changing anything. (needs a maintainer decision)
#1912 glm 0.6.0 (Python, TP=2): re-prefill from 0 no 2+ GPUs/nodes free Python lane at TP=2; engine frozen.
#1920 Native GGUF on Apple Silicon partial Metal claimed, PR 1955 Reader and repack are host tests; device gates need a Mac; claimed.
#1927 Native OCR on HIP partial HIP claimed, PR 1938,1940,1941,1965 Host suites run on Spark; model receipts on gfx1201; claimed.
#1943 One KV admission ledger for unpaged streams partial 2+ GPUs/nodes claimed, PR 2011 Host tests run on Spark; device gate used TP=2; claimed. (needs a maintainer decision)
#1948 Flash Next EXL3: elastic device memory between experts and K/V partial 2+ GPUs/nodes claimed TP=1 arms on Spark; TP=2 needs a second node; claimed.
#1950 Benchmark receipts record the Mac's product family partial Metal free, PR 2009 Host round trip; hw.product probe needs macOS; PR ashhart#2009 open.
#1952 Qwen3.5 BF16 on HIP: decision backbone partial HIP claimed Host build on Spark; gates need gfx1201; claimed.
#1968 Living Weights: locate-then-place, shrink-after-keep, audit yes one GB10 free, PR 1980,2026 Nemotron Lightning on GB10; maintainer PRs ashhart#1980 ashhart#2026 open.
#1970 Move Starlight to its own repository partial Metal free, PR 1972 Linux CI leg on Spark; macOS leg needs a Mac; PR ashhart#1972 open.
#1971 roce_multi on a switchless ring of four no 2+ GPUs/nodes free Needs a ring of four nodes.
#1975 Streamed chat: GLM and DSML tool calls arrive in one piece picked host-only free Pure zig test on the shared Streamer, no GPU; parser only.
#1976 Bug: Cannot change the model away from planning-only mode no Metal free, PR 2004 Studio is the macOS app; PR ashhart#2004 open.
#1977 Qwen3-8B embedding and reranker on native HIP partial HIP claimed, PR 1978,1981 Host probes on Spark; fidelity receipts on gfx1201; claimed.
#1994 GLM CUDA TP=3: CHUNK_SOLO with ROCE_MULTI costs ~7.5% no 2+ GPUs/nodes free, PR 1996 Three GB10s at TP=3.
#2002 Agree the gate for Drama 3 native speech context partial decision-only free Gate matrix needs maintainer agreement. (needs a maintainer decision)
#2003 CUDA: decode-window L2 prefetch on Qwen/Nemotron yes one GB10 claimed, PR 2014 TP=1 decode A/B on GB10; claimed by the maintainer, PR ashhart#2014 open.
#2005 RFC: layer-range pipeline layout across mixed nodes partial Mac+NVIDIA claimed, PR 2012 Steps 1-2 on one GB10; Metal follower needs a Mac; claimed. (needs a maintainer decision)
#2031 CI never reaches 246 of 393 named test steps partial Metal free Host steps run on Spark; option 1 vs 2 is the maintainer's call; fixtures in PR ashhart#2024. (needs a maintainer decision)

BHCC2025 and others added 30 commits November 2, 2026 11:47
… MLP, group counters through grownCounters, the replacements in the kernel contract

- The 27B's fixed plans and exl3-plan-f64 are gone: every projection takes plan()'s launch (Weight.sk/wk stay).
- reserveGroups runs after the layer's MLP is set; a host test checks a dense layer's gate/up group decodes 1-16 rows
  without allocating.
- A group's counters come from main's grownCounters (ashhart#1475) with its members' summed bytes; scratch() never zeroes.
- contract_exl3.zig registers the mid-M kernels, linear_wc, the groups and rot_group in gates 1-4 as byte-equal
  replacements of linear_body (rot_in), each with a planted control; host tests for groupable, wcPick and planOf.
… in the kernel contract

- contract_exl3 registers the mid-M kernels at 3 and 5 bits too, each with a control, and its served shapes reach
  every form plan() gives a width: both row passes, 4 and 8 warps, split and unsplit K, every linear_wc tile. The
  groups gain 4 warps unsplit.
- exl3.pick is matmul's dispatch alone. A host test sweeps K and N to 16384 through it and groupable, and fails on a
  form no registration's gate-3 shapes run. A family's fixed plan keeps the linear.
- Gate 4's stress weights are all-ones states, one weight at every width (0xaaaa decodes to two at 3 and 5 bits).
…ig stays under 600 lines on main

ashhart#1475's grownCounters and its test took exl3.zig past the lean check's 600 lines; the variant tables, wcShared, wcPick and
mpgIndex move as they are (exl3.zig re-exports what other files name).
The drafter's trimmed-vocabulary head runs through the target's quant kernels,
whose lane pipelines compile from source on first use. Its shape is unique, so
it compiled inside the first draft round after a prompt. Backend.init now asks
Kernels.prepareDecode for that pipeline; the lookup moves into laneEntry with
the same key, so every later launch selects the same pipeline as before.
Exercise the actual preparation, cache lookup and lane encoder with a
recording Metal boundary. Check that short and long target prompts leave
first head launches on the prepared decode entry, while long prompt
projections retain their own key and use the entry's row capacity.

Cover preparation rollback, reuse and no-op guards, and run the regression
in the default CPU test graph as well as test-qwen27-quant-host. The supplied
head preparation already preserves main's long key and max_rows dispatch;
keep that production change intact.

Co-authored-by: kieranscotney <346361909+kieranscotney@users.noreply.github.com>
… and running replies

TENSORFOLD_PREFILL_ORDER=srpt: before each long prompt step the lane host compares each running
reply's estimated time left (its token budget over measured tokens a round and round time) with
the prompt's (rows left over measured time a row). When a reply has less left, the step owes the
replies nine times its time, so the prompt keeps at least a tenth. Unset, nothing changes.
… first, at most the floor otherwise; 0 stays whole)
Adds starlight/, a Go module with three binaries: starlight (gateway, control
plane, admin console, SSO, SCIM and metrics), starlight-node (enrols a machine
and supervises its engines and rank groups) and tensorfold-connect (desktop
sign-in and a local endpoint for tools). The engine is untouched: Starlight
drives tensorfold serve through its documented flags, key files and routes.

.github/workflows/starlight.yml tests the module and runs only for changes
under starlight/. The README points to it.
hashicorp/mdns keeps updating an entry it has already sent as more answers arrive, so reading entries while the query ran raced (seen on Linux CI). A test sends an entry and keeps changing it.
An Admitter built without a Waker made one on first use, unguarded, so concurrent admissions raced on the field (seen on Linux CI). The server always passes one; the fallback now runs under sync.Once, and a test admits concurrently on a fresh Admitter.
…LASHNEXT_ATTN_LANES (ashhart#1739)

Squashed from ashhart#1739's branch, which merges main in; its eight commits replayed onto main conflicted where the merge had already resolved them. The tree is the PR merged with current main.
ashhart#1739 moved fn_attn_merge's loop into fn_attn_merge_body, which the kernel
calls; the Qwen4 CUDA attention host instrument extracted only the kernel, so
test-qwen4-host, test-qwen4-gdn-host and test-qwen4-projection-bodies stopped
compiling. Extract the body too, as inline host code.
…; nvfp4_slice.shapes checks without reading bytes
ashhart and others added 28 commits November 3, 2026 07:07
…6 activation dtype

docs/api.md's Decisions section named the MLX server and CUDA GLM, but only
Flash Next on one CUDA GPU, LFM2 on CUDA and Gemma 4 on XPU score today; every
other engine returns HTTP 400.

glm/config.zig now refuses tensorfold_activation_dtype unless it is absent or
bfloat16, naming the key and the value. A float32 checkpoint was silently served
in bf16 before. The host test parses the real config absent and with bfloat16,
and refuses float32 and float16.
core/rope_freq.zig computes 1 / f32(pow64(theta, f32(2i)/f32(dim))), the
transformers words, and its proportional form gives a prefix of pairs then
zeros. lfm3's Rotary.inverseFrequencies keeps main's () [64]f32 signature and
now calls proportional; spark25's inverseFrequencies and the GLM vision tower's
rotary call the helper directly, keeping their output slices and zero tails.
Host tests compare each copy's old expression with the new words word for word;
finite values cannot move. The Metal core root exports rope_freq for the tower
and Spark2.5's Metal build.

D237's rebase keeps main's proportional mode (Gemma 4 CUDA), drops the duplicate
rope_freq.zig and core/root.zig hunk, and puts the GLM old-against-new test in
the helper's own test file.
Under the share the prompt cache holds pool pages, so a balanced idle pool
logged (UNEVEN): Pool.idle() already checks free + kept == total, free ==
total holds only for the copy store. Spark 25's form from 004655c.
…ing the race

The concurrent writers-and-readers test read the table straight after each
save and failed on a null load. On Windows a load can miss there: std.Io's
Atomic.replace documents a window in which operations on the destination
return AccessDenied while a replacement settles, and the cache's Win32
reader returns a pending or refused open as a silent miss, which
cost_cache_windows_test.zig already requires. A miss is not a torn table,
but loadAt's null could not tell the two apart, so the test treated both
as failures and flaked on windows-latest.

readAt now reports missing, refused or the value; loadAt keeps its
results on every platform. The race counts each read: no refused or
incomplete table anywhere, no miss off Windows, and one whole table once
the writers have joined. Two planted races make it deterministic: a
replace window that refuses every other open after a thread's own
rename (0/300 pass with the old checks, 300/300 now) and a publisher
that renames half-written tables (every read refused).

Receipt (macOS arm64): the unplanted test passes 300/300 before and
after; zig build test fails only the HTTPS-tunnel test (2 steps, same
test); zig build dev-tools passes; zig fmt --check is clean; lean has
424 problems on both base and head, 0 new. Both test binaries compile
for x86_64-windows-gnu with the host retargeted; they were not run on
Windows.
…aders with core

Loader.addShard took its allocation from the length word and indexed the JSON
without checks, so a malformed header panicked. It now reads the length with
headerSize and the header with parseHeaderOptions, leaving out a dtype or rank
core does not name, as before; offsets are checked against the file and the
fd is closed when the header is refused. xpu/safetensors.zig parses with
parseHeader and keeps its error names, Tensor and the header's dtype spelling.
mv.metal runs the router, indexer, compressor and head matvecs as lane chains over K with one butterfly, so the
bits never depend on the row count. glue_tmp.metal is the layer's phase-1 glue (norms, act_quant, RoPE, FP4, pools,
candidates, hc and MoE glue), the reference bits the registered glue kernels are checked against.
…und trips, embed, join, mean)

glue.metal holds the layer's small launches in one stated order each: RMSNorm with fp32 or bf16 weights, RoPE,
act_quant and fp4_act_quant round trips, the embedding, the MoE join and the hc mean. Registered with the contract
against float64, and bit-equal to the phase-1 glue.
index.metal scores FP4 queries against FP4 keys in one stated order with causal and block masks, topk512.metal
keeps each row's top 512, cand.metal picks the candidate blocks as a byte mask, and route.metal takes the top-k of
sqrt(softplus(logit)) + bias for the backbone and the drafter. Each is registered with the contract.
compress.metal pools the ratio-2 and ratio-4 compressed KV and round-trips it through FP4; engram.metal dequantises
the FP8 Engram rows, projects them, gates them and adds them to the residual. Registered with the contract against
float64 and bit-equal to the phase-1 glue where they replace it.
…lded decode glue

hc.metal runs hc_mixes with Sinkhorn, hc_pre and hc_post in fixed orders; fused.metal and fold.metal split or fold
neighbouring glue launches at the same bits. Registered with the contract against float64 and bit-equal to the
phase-1 glue.
… by stage against float64

The Metal package over the family's neutral host files: the kernels' pipelines, release weights (copied or mapped
from the shards), a slot's KV regions, RoPE tables, scratch and one layer's encoders (attention with the indexer and
compressor, MoE, hyper-connections). test-dsv41-metal checks every stage of every layer kind against float64 and
rows at once against one at a time; test-dsv41-metal-real runs one release layer under TF_DSV41_REAL.
…d the DSpark taps

forward.zig runs the stack for 1-16 rows of one or several slots in one launch: Engram before its layers, the
hc mean into the taps, the head. model_weights loads a whole checkpoint, its shards mapped zero-copy where aligned.
The tiny model's stages are checked against float64 and R rows in a launch against one at a time.
The prompt runs the encoder layers over every row in chunks, then the decoder's suffix cone: layer L of the decoder
computes only the rows the prompt's end depends on (kept + (39 - L) x 127), with the bits of running every row.
ced gains the cone's rules and the snapshot replay; tests check chunk sizes, the cone against every row, resumes
and the grouped experts against the decode rows.
The drafter on Metal: its weights, the ingest of committed rows, the propose launches for several slots at once
and the pass's plan. Its stages are checked against float64, R rows ingested at once against one at a time, and
several slots in one pass against each alone.
serve_lanes.Model is what a backend gives the lanes (target, pass, shape, costs, hooks) and Served runs it under
LaneHost; calib_lanes holds the draft-cost tables. The CUDA engine (serve_engine) is unchanged and keeps its own lane
half; the Metal backend in the next part is the first to serve through this module.
With parallel_tool_calls false, a streamed reply now carries the same content
and finish reason as the whole reply in the cases ashhart#1396 lists that still
reproduced on main:

- item 1: a rejected whole envelope in any case or NAME: spelling
  (<foo:tool_call>, <TOOL_CALL>, <Tool_Call>) stayed in the whole reply's
  content but was dropped from the stream. The one-call text filter now holds
  the prose after a hidden closed envelope, and the finish sends the reply's
  own content past what streamed.
- item 2: the stream's end-of-reply prose took the cut-call branch in one-call
  mode, which the whole reply never does, so a rejected envelope before a cut
  call and the cut prose's trailing newline were lost. endProse now uses
  finishedProse in one-call mode.
- item 4 already agrees on main; it is kept as a regression case, and the
  trailing-newline variant that did differ in one-call mode is fixed by the
  finishedProse/kept change.

tool_single_stream_test.zig drives every case whole and streamed, one byte a
round and batched.
…hhart#1975)

A GLM call (<tool_call>NAME<arg_key>K</arg_key><arg_value>V</arg_value>...)
used to abort the streamer and go out in one piece at the finish. Its header
now goes out once the name is whole and names an offered tool, then its string
values as JSON-escaped pieces (holding back only a partial </arg_value>) and
its typed values as compact JSON at their closer. tool_glm_stream.zig mirrors
tool_parse.glmCall's reading, valueKey and repairGlmKeys included, over
resumed indexes, so the joined pieces equal the whole reply's arguments byte
for byte. DSML calls are unchanged.

Two stream/whole differences found on the way, same bar:
- after a call had streamed, an envelope the streamer could not stream set it
  to abort, so every later call was missing from the stream; it now parses
  that envelope at its closer instead, and aborts only while nothing streamed.
- typed XML values streamed with Python's ", " separators while the whole
  reply's arguments are compact.
@devopsfortunato
devopsfortunato merged commit 2499e29 into main Nov 3, 2026
20 of 25 checks passed
@devopsfortunato

Copy link
Copy Markdown
Owner Author

The same work is now proposed upstream as two separate pull requests, one per issue: ashhart#2049 (issue ashhart#1396, one-call stream parity) and ashhart#2050 (issue ashhart#1975, GLM argument streaming), both rebased onto upstream main a6c5b97eb and re-verified on the same Spark. This fork PR collects both and can be closed once the upstream ones land.

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.