Repository navigation
cuda: a keyed graph cache and a device arena, off until a family installs them; failed launches name their kernel (DeepSeek-V4.1 stack 2/14) - #635
Conversation
jrb-ai
left a comment
There was a problem hiding this comment.
I reviewed 5fa8ad0fd9c53131c599c39f8885117d5a582fda and prepared two focused fix commits with regression tests, plus a separate reproducer/receipt commit. The code fixes have passed a bounded, model-free CUDA check on one DGX Spark. Compare branch. Please take or cherry-pick whichever parts fit.
Findings and changes
- RDMA-with-VMM device attribute.
gpu_direct_rdma_with_cuda_vmm_supportedis 110, while 126 isMEM_SYNC_DOMAIN_COUNTin the CUDA driver ABI. This Spark returns 0 for 110 and 4 for 126, so the current query sets the RDMA allocation flag incorrectly. The submitted code fails the VMM arena reuse check; changing only 126 to 110 passes the identical probe. The exact failing VMM driver-call return code was not instrumented. - Captured executable ownership. A successful capture can be lost if the following
Agreecallback throws or the map insertion runs out of host memory. The fix reserves map capacity and agrees on success before capture, then keeps scoped ownership until an infallible insertion transfers it to the cache. - Rank-local invalidation versus a cache hit. One rank can invalidate its fingerprint and enter the miss-path agreement while a peer replays without participating. Two host threads with blocking agreement reproduce the mismatch. The fix agrees on active state, fingerprint validity and cache hits before branching, invalidates all ranks when needed, and coordinates allocation failure before capture.
- Configured cache maximum. With
max = 2and the defaultmin_keep = 8, memory pressure can raise the cap above the maximum;max = 0can later capture too. The fix clamps the floor/cap, keeps zero-max caches eager, and allows a positive-max cache to recover from a zero pressure cap.
Commits
2b9d983e: CUDA attribute correction and ABI regression.26c82f71: graph ownership, rank decisions and cache bounds, with regressions.82ecb0f9: portable tests and validation receipt, separate so it can be omitted when taking the fixes.
Test evidence
Exact Zig 0.17.0; DGX Spark / GB10, Ubuntu 24.04.5 LTS aarch64, NVIDIA driver 580.178.04.
| Check | Submitted 5fa8ad0f |
Code fixes |
|---|---|---|
zig build test-cuda-host -j2 on Linux |
293/293 pass | 305/305 pass |
| Same target on macOS | 286 pass, 2 Linux-only skips | 298 pass, same 2 skips |
| Six independent regression probes on macOS | 0/6 pass | 6/6 pass |
| Independent probes on Linux | Not run | 6/6 pass |
| Same model-free CUDA probe on Spark | VMM reuse assertion fails | Plain arena, graphs and VMM pass |
| Lean checker | 542 inherited findings | 542, zero new |
The device probe covers capture/replay, eviction, fingerprint invalidation, an injected capture-body failure and eager recovery. The runtime files match the qualified source byte-for-byte. The published probe differs only in formatting, and the independent host probes only in comment formatting; hashes and commands are in the receipt. No checkpoint was loaded, no device reset was performed, and existing services stayed unchanged.
Multi-rank trade-off
With Agree installed, a cache hit now makes three agreement calls instead of zero, and a successful capture miss makes six instead of two. This fixes the demonstrated decision mismatch, but the collective/host-synchronisation cost has not been measured on a GPU pair. With no Agree, the checks stay local. I would treat the rank change as a correctness candidate needing your multi-rank cost review; the attribute fix can be taken independently. Actual multi-GPU/NCCL execution, model serving, performance and quality are outside this validation.
Jonathan Regan-Beasley, contributing in a private capacity. This does not represent the views or positions of any organisation.
5fa8ad0 to
842c32e
Compare
|
Thank you, Jonathan. This was a careful review, and all four findings are real. The stack is rebased onto current main, and this layer now has two new commits. Head: Taken
Changed: rank-local invalidation (3). You're right that a pointer fingerprint can change on one rank and not on the other. I fixed that through the contract instead of with more agreements:
The reason is the cost on our decode path. Each agreement there is an allGather plus a host sync on the compute stream. Three of them per hit would stop a graph from being queued while the previous one still runs. Declined: 82ecb0f. It is a one-PR review harness and receipt, and Gates on the new head, host only (Linux x86_64, Zig 0.17.0): |
|
Added |
|
Independent receipt for cee12a2 on two ThinkStation PGX (GB10), aarch64 Linux, sm_121. Zig 0.17.0, driver 580.178.04, CUDA 13.0.2 (nvcc 13.0.88), NCCL 2.28.9+cuda13.0.
Existing |
|
Thank you, @jayleaton. This is a big piece of work. At #695 the stack adds about 85,000 lines in 366 files, and our lean check passes on all of it: no new findings, and the largest new file is 596 lines. We read through the whole stack this evening. Three things need to change before any of it can land. Then there's the order we'd like to take it in. Python tools. The stack adds 40 Python files under License headers. Thirty files carry Provenance. Order. We'd take the shared pieces first, each reviewed and gated on its own:
On #663, we'd like to understand the compatibility wire ( The family layers would follow, gated on CUDA machines that hold the whole model, under the same contract as every family: drafted output equals None of this will make 1.0.8, which is closing now. If you can make those three changes, we'll start with #635. |
cee12a2 to
c58d83c
Compare
|
The combined correction is published across the stack. On provenance: decode.cuh and experts_grouped.cuh are TensorFold's own 0.3.6 kernels, introduced in b16fb91 and 1bbd2dd and released in 85653c7. The three routines we identified follow ExLlamaV3's operation order, written differently; that order gives bit identity. So the relationship includes structure, not just the format. Our scan found no copied text, and we defer to your copy scan. We also checked the other EXL3 kernels: exl3_experts.cu, linear.cu, x3ld/x3gm/x3seg/x3pf.cu, dense3.cu, the pfdense family and lin2.cu. Their includes carry the same decode routines. Beyond those, rotation/finish order and split-K completion follow the same operation order, and parts of the load ring, lane-shuffle decode, linear loop and fused epilogue follow ExLlamaV3's designs. No copied text was found in this expanded check either. Line matching cannot establish originality or detect every renamed/reflowed expression; your scan remains the authority. All third-party notices remain, and the lost antirez/ds4 load-geometry credit in dense3.cu is restored. The personal MIT headers are removed at their introducing layers. Development Python tools are out of the upstream tree; native tests consume the stored fixtures. The tools are in jayleaton/deepseek-v41-tensorfold-spark#30 under tools/tensorfold-dsv41/, and the family docs link the pinned commit c1cb91840fb8ff5db0bd27418fc7cc766a222cbc. The compatibility wire and its settings/log plumbing/tests are removed from #663. Family hooks, graceful drain and keepalives remain, with default-wire coverage on the native surface. The HCA correction and executable-mode fixes are included. Each rewritten head passed its host/family/receipt gates, formatting, the zero-new lean comparison against 534 inherited findings, and mode checks. Compiler/SASS receipts are separate from GPU/model qualification; no hardware inference or transport run is claimed for these rewritten heads. |
|
@jayleaton, a data point for the graph cache, and one request. Before #635 we tried a family-local graph for GLM-5.3-Flash's verify forward (#548): one capture per key, the key being the window's rows, the KDA chain's parity and the chunk and pool counts the position decides; staging, the draw and the commit stayed eager. On one GB10 it replayed as intended (on one slot, 37 captures and 1,740 replays over the lanes cover check). On two GB10s at TP=2, graphs on and off alternating twice in one sitting:
Level without drafts, and 0.9 to 1.5% slower with them, so we're not taking ours. Our reading, not yet profiled: decode on GB10 is GPU-bound here (about 1.4% of a step is GPU idle), so there's little launch time to save, while MTP windows change their row count and the position-dependent counts move on, so captures keep coming back. We'll use your cache rather than our own once #635 lands, and we can run the same A/B through it. The request: since a graph can be slower on some workloads, as here, could the cache be turned off by the operator without a rebuild? An environment variable or a serve flag that sets |
|
Thank you, @jayleaton, for turning all three around in a day. The development tools now live in your repository and the fixtures stay here, the personal headers are gone, the compatibility wire is out of #663, and the provenance note is the clear answer we needed. As promised, we'll run our copy scan against ExLlamaV3 over the EXL3 kernels you listed before any of them lands. Here's how we'd like to go from here. The shared pieces first, gated by us. We'll take #635, #636, #637, #662 and #663 in that order, each as its own increment, on our two DGX Sparks:
Each one lands when its gate passes, as your commits under your name. The family layers need one run at the current heads. As you said, the rewritten heads haven't run on a GPU yet. Before we review the family layers (#667 to #727, about 85,000 lines) in depth, could you post one run of #727 (or #703) on two GB10s with:
@ecohash-co's production numbers on #559 are encouraging, but they come from their own build, so a run at these heads is what we can review against. Shared changes as their own pull requests. #702's increment touches 57 files under Thanks again. This is the most complete community port we've had, and we want to bring it in properly. |
|
Thanks, @Bizuayeu. That A/B is useful: on two GB10s, GLM-5.3-Flash's verify graphs were level without drafts and 0.9 to 1.5% slower with 2- to 4-row MTP windows. Yes to the switch. An operator should be able to turn the keyed cache off at serve time, without a rebuild, for any family that installs it. We'll make it part of #635's gate: the same tokens with the cache on and off, and speed measured both ways. @jayleaton, could you add it in #635, as a serve flag or an environment variable that sets |
|
@jayleaton — bringing the family-package question here, as ashhart asked on #690: your stack and the Metal draft both write Concretely: #667 adds Proposed split, following ashhart's note that config, weights, Engram tables and the host side should be one package with Metal and CUDA kernels beside it:
To agree the file-level split, three questions:
The Metal draft itself stays draft-only for now — its whole-model numerical gate is still failed at 4.0961% against the unchanged 1% limit, and the next run is being rebuilt on the lane core. Until you confirm the split, we'll keep our work in worktrees and write nothing further into |
|
Thank you, @jayleaton, and thank you, @akol1, for raising the split here. We've opened #847 to bring DeepSeek-V4.1 together as one family with CUDA and Metal backends. It takes @akol1's layout from this thread: one package in Nothing changes for the shared pieces. #635, #636, #637, #662 and #663 still come first, each through the gate described above. The notes on #636, #637 and #663 are what those need. @jayleaton, two things would help most:
The GPU run at the current heads is still the step before the family layers. Thanks again for all of this work. |
…alls them. graph_cache.Cache captures one executable graph per caller-named key and replays it when the key comes back; ranks agree on every capture, eviction and fallback through an Agree hook, and a memory floor turns misses eager. arena.Arena sub-allocates DeviceBuffer.alloc from a few large chunks (VMM or plain cuMemAlloc) through memory.setHook; borrowed spans and pointers it does not own are freed as before.
…; CUresult names fall back to a table when the driver cannot answer. cuFuncGetName is optional (CUDA 12.3+), so an older driver still opens. During the exit's teardown cuGetErrorName fails too, which printed CUDA_ERROR_DEINITIALIZED as "CUDA_ERROR_UNKNOWN_CODE (4)"; code 4 now says the process is exiting.
Use CUDA device attribute 110 for GPUDirect RDMA with VMM support. Attribute 126 reports the memory sync domain count and can incorrectly enable the RDMA allocation flag. Add a regression for the external ABI contract.
…r stays under the max, a max of 0 is eager - A capture whose agreement throws, or whose insert runs out of memory, no longer leaks its executable: the map's slot is reserved before the capture, and the executable is owned here until the insert, which cannot fail. - A host map that cannot grow counts as no room in the existing room vote, so every rank declines that miss together. - `min_keep` is clamped to `max`, so under the memory floor the cap never rises above the configured max. A max of 0 runs every step eagerly, with no collective. - A cap lowered to 0 under the floor goes back to max once every rank has room, instead of staying eager for good. - Hits make no collective: with `Agree` set, keys, settings and fingerprint changes must already match on every rank. The docs now say so: the fingerprint changes at the same step on every rank, and `fingerprintOf` is for one rank. Co-authored-by: Jonathan Regan-Beasley <197533392+jregan-beasley-smc@users.noreply.github.com>
The arena's `live` (small ranges) and `large` (own chunks) maps drop an entry on every free and gain one on every allocation for the life of the process. std's tombstones never clear, so in time they take every free slot, and each allocation, free and miss then probes the whole map. Each map now counts its removals and is rehashed in place once they reach a tenth of its capacity. Test: 20,000 random allocations and frees (small ranges and large chunks) keep a tenth of each map free at every step; without the rehash it fails.
…amily that installs it, without a rebuild Cache.init applies the operator's value through Settings.operator, so a deployment that measures graphs slower can turn them off at serve time. The value is read once a cache, and is set alike on every rank like the settings themselves: no agreement is added, and hits still make no collective.
|
The family-neutral extractions are now separate draft PRs: JSON module #860 and lane host/core #861. Each is independent on main |
c58d83c to
d335520
Compare
…at start; RUNBOOK says what a mismatch does An off cache never calls Agree, so a rank left on waits at its first miss for a vote that never comes. Settings.effective applies TF_GRAPH_CACHE, clamps min_keep and maps every off cache (0, off, OFF, a max of 0) to one value; Cache.init runs with it, and Settings.digest gives ranks those bytes to compare before building a cache. Hits still make no collective.
|
Thanks, @Bizuayeu. You're right: an off rank returns at the top of
The startup refusal itself is in #636's bootstrap agreement, as you suggested; there's a note there. Host gate at |
The CUDA root keeps main's ck and tp_watch test imports beside the graph cache and arena. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thank you, @jayleaton, the effective-settings digest and two-rank missing-vote test make the off/on startup problem explicit, with the capture-fix credit retained. We checked abcad7d. Capture ownership and keep-floor repairs are present. arena.zig:89-90 still pops a reusable range before live.put can fail; we can restore that ownership sequence and add retry/conservation coverage at landing. The global allocation hook also needs its installation/restoration tied to the real family owner. We will carry these with the common runtime and #636's actual bootstrap agreement, keeping cache-on/off token parity and rank key/invalidation checks with the first consumer. Your host receipts remain useful; the real capture/replay gate is still open. Posted by Ash's AI Assistant Synthena |
… free allocLocked reserves room in `live` before it takes a range. A failed insertion used to pop a freed range and then lose it: the range was in neither the free list nor the live map. Both small-path inserts now cannot fail. A failure leaves the free list, the cursor and the chunks as they were, and the retry gets the same range. The new test spends the map's spare room so that the reuse's insertion must grow it, fails that allocation, and checks range, cursor and chunk conservation, the retry, and a clean deinit with every chunk released. Not in this change: tying memory.setHook / currentHook installation to the family that owns the arena. Jonathan Regan-Beasley's capture-ownership fix (5826f89) is unchanged. Spark receipt: lane job doug-635-arena-01 on dgx-02 (aarch64, Zig 0.17.0), 2026-10-17T02:10:06Z, VERDICT PASS, candidate tree 663ff7b. zig fmt --check clean; lean 517/517 against merge-base 46a08c1, 0 new; zig build 12/12; test test-kernel-contract test-golden fuzz-cortheon 94/94 steps, 1430/1437 (7 skipped); test-cuda-host 39/39. With only the new test on abcad7d, it fails (the freed range left its free list). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ck/03-tp-fabric Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…x fixes) into stack/04-sessions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fixes) into stack/07-server Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ixes) into stack/08a-config Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fixes) into stack/08b-engram Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fixes) into stack/08c-kernels Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fixes) into stack/08e-block Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ixes) into stack/08r-runtime zig/src/cuda/arena.zig: both sides appended tests; both are kept (this branch's fallback and VMM tests, then the reuse-insertion test). The fix's reservation lands in this branch's allocLocked unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ixes) into stack/09a-kv Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ixes) into stack/09b-forward Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ixes) into stack/10-prefill Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ixes) into stack/11a-sampling Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ixes) into stack/11b-draft Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ixes) into stack/11c-device-drafts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ixes) into stack/11d-batched-rows Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ixes) into stack/12a-serving-host Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ixes) into stack/12b-serving-engine Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ixes) into stack/13a-performance-host Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… prefix fixes) into stack/13b-performance-device Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
GPU receipt for
Not covered: the real-model capture/replay gate (same tokens with the cache on and off, speed both ways) needs a consumer family, and the cross-rank |
|
Thanks @cwschroeder for the 15/15 device groups on each GB10 and the explicit capture-error, reuse and memory-floor controls. Those are useful runtime receipts, separate from a model consumer's qualification. Our integration also reserves ownership before consuming reusable arena ranges and preserves the allocating hook's lifetime. The real-model cache-on/off token comparison, speed in both modes, rank agreement and invalidation checks remain open on the integrated consumer. The supplied helper and host results should stay pinned to their tested head; they do not establish that final model gate. Posted by Ash's AI Assistant Synthena |
Conflicts, both sides kept: - zig/src/cuda/memory.zig: main's allocation limit (reserveAllocation/finishAllocation) now wraps the arena hook path. A hooked allocation reserves, tries the hook, falls back to cuMemAlloc and is counted by finishAllocation like any other. - zig/src/cuda/root.zig: main's memory_receipt, setAllocationLimit and new kernel modules beside graph_cache and arena. - RUNBOOK.md: main's GLM heat-band paragraph, then the TF_GRAPH_CACHE paragraph. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r.check does launched() named the failing kernel for every non-success code, so a launch that main's tests answer with CUDA_ERROR_NOT_READY (qwen_cuda_test, experts3_host_test) reached errorName on a fake driver and crashed. It now returns error.NotReady first, like Driver.check. The ranks test reads the monotonic clock through std.Io, so it also compiles on Windows hosts (zig-windows). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks @jayleaton. 1.1.1 ships the shared keyed graph cache, family-installed arena, rank agreement and named launch failures. The range-reuse safety in 3aeebb9 is already covered by our maintained insert-before-pop path; adbdb8a's NotReady handling is also present. We'll carry the additional allocation-failure and portable-clock regression coverage into 1.1.2 with your credit. Closing for the shipped infrastructure scope. Posted by Ash's AI Assistant Synthena |
What this changes
cuda: a keyed graph cache and a device arena, off until a family installs them; failed launches name their kernel. Depends on #634. This cumulative PR targets
main; the incremental range ise7c523eb..c58d83ca.Commits in this layer:
3688f3ceCUDA keyed graph cache and device arena, both off until a family installs them.b7be793dA failed CUDA kernel launch names its kernel and launch configuration; CUresult names fall back to a table when the driver cannot answer.faf59014fix(cuda): correct RDMA VMM capability attribute3cc03974Graph cache: a capture is freed until the map holds it, the keep floor stays under the max, a max of 0 is eagerc58d83caCUDA arena: the address maps are rehashed before tombstones fill themThe combined correction removes personal MIT headers while retaining third-party notices, relocates development Python tools, and preserves stored fixtures with native consumers. Expanded provenance covers the EXL3 kernel stack; no copied text was found by the performed scan, and maintainer copy scanning remains authoritative.
Receipt
Before: public head
cee12a2b. After: exact corrected headc58d83ca53086a0f60944ce050cab5167838f40a, gated on 2026-10-12, Linux x86_64 and Zig 0.17.0.Executed gate output:
Lean uses pinned main
e7c523eb: 534 inherited findings, zero new or removed identities. The mode gate covers all 57 introducing pins with zero executable-mode losses. Family suites and CLI receipts run where introduced. Host checks do not qualify checkpoint inference, GPU execution, concurrent/resumed serving parity, prompt/decode speed, or macOS/Metal behavior on this rewritten tree.The family documentation pins development tools at recipe commit c1cb9184.
Exactness and prompt/decode speed: not run on this host, which has no GPU or checkpoint weights. No new performance claim. MLX/PyTorch/checkpoint revision: not applicable to the host gate.
Checklist
"draft": false, a resumed prompt equals a fresh one, and concurrent equals solo.Merge Danger
Door: two-way. Reverting restores the parent without a data migration.
Blast Radius: engine. The incremental commits define the affected runtime, build and test surfaces. The production branch is unchanged.