Skip to content

glm cuda: TF_KV_SHARE keeps GLM's prompts in the shared prompt store, and GLM's own store goes - #2211

Merged
ashhart merged 4 commits into
ashhart:mainfrom
Bizuayeu:pr/glm-kv-share-store
Nov 6, 2026
Merged

ashhart merged 4 commits into
ashhart:mainfrom
Bizuayeu:pr/glm-kv-share-store

Conversation

@Bizuayeu

@Bizuayeu Bizuayeu commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

GLM's half of #1657: TF_KV_SHARE=n now keeps GLM's prompts in the shared prompt_radix store over the core KV pool, through the native-CUDA page adapter that Spark 2.5, Gemma 4 and Kolibri-1 use, and GLM's own prefix code (cuda_prefix.zig) goes. With the store on, this head does what main's TF_GLM_KV_SHARE=16 does, at the same marks, pages and memory, with the same tokens. On two GB10s at TP=2 (524k window, PARALLEL=4, the drafts on):

main, TF_GLM_KV_SHARE=16 this head, TF_KV_SHARE=16
64k chat message sent again (s) 2.10 2.09
128k chat message sent again (s) 2.36 2.36
136k of token ids sent again, ttft (s) 0.84 0.84
136k of token ids cold, ttft (s) 98.77 98.74
decode (tok/s, bench_openai) 39.82 39.89
pool at start 1,028 pages of 512 rows (6.40 GiB) the same
MemAvailable after load, rank 0 / rank 1 (GiB) 14.83 / 15.62 14.75 / 15.55

Unset, both are main's paths: decode 39.86 tok/s on main and 39.88 on this head, and the cold 136k prompt 100.07 and 98.59 s.

At TP=3 on three GB10s (1M window, the same settings):

main, TF_GLM_KV_SHARE=16 this head, TF_KV_SHARE=16
64k chat message sent again (s) 1.73 1.73
128k chat message sent again (s) 2.02 2.32
136k of token ids sent again, ttft (s) 0.68 0.67
136k of token ids cold, ttft (s) 81.34 81.12
decode (tok/s, bench_openai) 51.32 51.14
pool at start 6,147 pages of 512 rows (38.27 GiB) the same
MemAvailable after load, ranks 0 / 1 / 2 (GiB) 11.06 / 12.24 / 16.75 10.89 / 11.81 / 16.88

Unset at TP=3, main's flat store has no room for the 64k and 128k states, so those messages sent again prefill from the 6,144-row mark on both (32.1 and 66.8 s on main, 31.9 and 66.4 s on this head); decode is 51.40 and 51.16 tok/s, and the cold 136k prompt 80.94 and 80.71 s.

At TP=3 the 128k message sent again took 2.32 s on this head, its first token at 0.86 s against 0.61 s on main's store. That resume is sometimes slow on main's store too. In a count with eight different 128k messages per launch, each sent fresh and then again twice, over two launches of each store (16 first resends a side, the same binaries and settings), the first resend's first token came past 0.7 s twice on each side (0.78 and 0.77 s on this head, 0.94 and 0.79 s on main's store), each time on the launch's first 128k message, and the second resend's once on each (0.85 and 0.73 s, both on the fifth message); the medians were 0.62 and 0.63 s, and each message had one token hash over the four launches.

What this changes

Checks on this head

  • Head commit: 9754b6c99
  • Lean checker, python3 tools/zig/lean_check.py, on the base and on this head: 425 and 425 problems, 0 new.
  • Tests run, with the command and pass, fail and skip counts (Zig 0.17.0, Linux x86_64 under WSL2, -j4): zig build test 545/545 steps (2,259 of 2,295 tests, 36 skipped; it runs test-profile), zig build test-cuda-host 313/313 steps (4,877 of 4,900, 23 skipped), test-kv-pool 38/38, test-glm-image-cache-host 3/3 (now over the adapter), test-glm-snapshots-host 21/21, test-glm-pages-host 12/12, test-glm-prompt-settings-host 6/6, test-glm-tp3-host 8/8, test-glm-prompts-host 8/8, and test-glm-startup-host, test-glm-fp8-kv-host, test-glm-admission-host, test-gemma4-shared-host and compile-glm-native-open with every step passing; compile-cuda-tests (x86_64 and -Dtarget=aarch64-linux-gnu), compile-config-server, and zig fmt --check build.zig zig; with -Drerun-tests, as CI runs them, test-kernel-contract 22/22 steps (180 of 180 tests) and test-server-security test-pull-security 14/14 steps (29 of 29 tests).
  • New tests: the adapter's rules for GLM (glm_snapshot_host.zig: planned, grid 2,048, lookahead 1, min_prompt 0, no warm or fork), the TP and pass tests in cuda_pages_test.zig over a store opened with GLM's decls, the image cache over the real adapter, keptPpm and opensStore in native/cuda_streams.zig, and fewStates and moved (which TF_GLM_KV_SHARE values the start refuses) in cuda_share.zig. On main they don't build, as cuda_share.zig and GLM's share_pool, share_attach and share_kept_ppm aren't there (test-glm-pages-host, test-glm-snapshots-host and test-glm-image-cache-host with this head's test files stop at unable to load "cuda_share.zig").
  • CI: all seven jobs pass on 9754b6c99 (fmt; zig, zig-windows, zig-macos-dev-tools, release-paths, and benchmark-clients on Ubuntu and Windows).
  • Not run, and why: image prompts on a GPU (the image cache's host test runs over the adapter); Metal and HIP devices, since the change is GLM's CUDA path.

Receipt

  • Environment: TensorFold cbb31ac48 (this branch's first commit before the rebases; on a6e25faea it is 4baed8d62, whose diff changed only in context and in the file that imports cuda_streams.zig for its tests, cuda_kv_pool.zig instead of native/cuda.zig, which main has brought to the lean checker's 600 lines; the two commits after it change only which TF_GLM_KV_SHARE values the start refuses and add the CHANGELOG line) and main 8abb30f6f, each built with Zig 0.17.0 (.zig-version) and zig build native -Dnvcc=<nvcc 13.3> -Dsm=121 (the tf3-build container, CUDA 13.3.1); GLM-5.3-Flash XSFP4 at 88736f91, whose own MTP head drafts; PARALLEL=4, TF_KV_PAGES=on, serve.sh's window and prompt cache, the drafts on; GB10 (sm_121, Ubuntu 24.04, 128 GB), two joined by one QSFP cable (TP=2, 524k window) and three cabled in a ring (TP=3, 1M window). Launches at each TP: main unset, main TF_GLM_KV_SHARE=16, this head TF_KV_SHARE=16, this head unset, this head TF_KV_SHARE=16 under TF_KV_POISON=1, and this head with --prompt-cache-gib 0, whose replies are all fresh; at TP=2 also this head with TF_KV_SHARE=16 and TF_GLM_KV_HOST=1. In each: one chat message of filler text at 2.5k, 4.5k, 6.5k, 64k and 128k tokens sent fresh, again and twice at once (the filler shares its start, so each message after the first resumed from the one before's mark), tools/bench_openai.py, tools/bench_concurrent.py --levels 1,2,4,8 --alone --serial and tools/bench_cache.py --require-cold on a 136k prompt of token ids; the launch without a prompt cache sends the chats only, and the TF_GLM_KV_HOST=1 one all but the 136k prompt.
  • The issue's gates, the same at TP=2 and TP=3:
    • Outputs equal to main with the flag unset: each prompt's replies have one token hash (rank 0's done lines) over every launch: 10 prompts, and 419 replies over the seven launches at TP=2 and 355 over the six at TP=3.
    • Resumed equal to fresh at several marks: the chats resumed at 2,048, 4,096, 6,144, 63,488 and 126,976 rows and the 136k prompt at 135,168, each with the same hash as the launch without a prompt cache. Main's store and this head resume at the same rows.
    • Concurrent equal to alone at 1, 2, 4 and 8, and drafted equal to plain: all cells equal alone and "draft": false in each launch that ran them, the five with the 136k prompt at each TP and the TF_GLM_KV_HOST=1 one (at PARALLEL=4, four of the 8 wait in the queue).
    • Lane pages at idle the same first and last: the five launches with the 136k prompt logged kv pool idle 42 times each, and no launch logged UNEVEN. With the store on, the first idle was promised 0, free 1024 + store 4 == total 1028 and the last free 516 + store 512 at TP=2 (free 6143 + store 4 == total 6147 and free 5635 + store 512 at TP=3), the same on main's store and on this head.
    • Under TF_KV_POISON=1 on every rank the hashes and cells didn't change.
  • TF_GLM_KV_HOST=1 beside the store, at TP=2: the same hashes as every other launch, the chats resumed at the same rows as the store without it, all cells equal alone and "draft": false, and kv pool idle 39 times with promised 0, free 1037 + store 4 == total 1041 first and free 793 + store 248 last. At start the pool took 4,100 pages of 512 rows (1.50 GiB) on the device and 1,041 pages (6,636 MiB) in pinned host memory, MemAvailable after load was 13.63 / 14.62 GiB on ranks 0 / 1, and decode 39.83 tok/s against 39.89 without it.
  • Decode speed before and after, from tools/bench_openai.py (256 tokens, 3 repeats, temperature 0, the mean of its two prompts' medians): the tables above; this head against main with the store on, +0.18% at TP=2 and −0.34% at TP=3, and unset +0.03% and −0.46%.
  • Prompt speed before and after, the cold 136k prompt's first token from tools/bench_cache.py --require-cold: the tables above.
  • Cache bytes per kept state, before and after: unchanged. Both keep the snapshot past the pages in the cache's room, and the start line, the pool and MemAvailable above are the same.
  • The setting's edges, on a GB10 at TP=2: cbb31ac48 started with TF_GLM_KV_SHARE=16 stops on both ranks before serving, naming TF_KV_SHARE, and TF_KV_SHARE=4 at --parallel 4 starts with the warning that names 16. With the review's refusal (the same change as 4efafeebf, run on cbb31ac48), 16 still stops both ranks before serving, and 0 and an empty value start.

Checklist

  • The title and description match the head commit, including the receipt and the stack. (Three commits on a6e25faea; the receipt was taken on cbb31ac48, the first of them before the rebases, and on main 8abb30f6f, as the Environment line says.)
  • Every token still goes through the lane rounds. No serial path, and nothing that needs drafts off. (A resume adopts pages and restores a state before the pass; the tokens after it go through the rounds as before, with the drafts on.)
  • Drafted output equals "draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. (The gates above, at TP=2 and TP=3.)
  • No precision traded for speed. If the bits change, the description says which sums change and why. (No bits change: a resume restores the rows a fresh pass wrote, and the hashes match the launch without a prompt cache.)
  • A new or changed GPU kernel is registered with the kernel contract (docs/kernel-contract.md); each backend's SUMMARY line is in the receipt. (No GPU kernel is added or changed.)
  • Prompt processing is no slower than the last release. (Unset the paths are main's; with the store on, the cold 136k prompt is 98.74 s against main's 98.77 s at TP=2 and 81.12 against 81.34 s at TP=3.)
  • The description says which platforms I ran, Metal M1 to M5 and CUDA, and which I could not. (CUDA on GB10 at TP=2 and TP=3, host tests on Linux; Metal and HIP devices not run.)
  • New tests fail before the change, pass after it, and skip cleanly without their dependency. (On main they don't build: unable to load "cuda_share.zig", and GLM's adapter decls aren't there; none needs a device.)
  • Comments and docstrings are one line. No measurements or history in the source.
  • No personal data, machine names, internal hosts or local paths. No AI attribution lines.

@ashhart

ashhart commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thanks @Bizuayeu, moving GLM onto the shared store with 355 replies on one hash at each TP, and the idle census matching main's, is the careful version of this. Three small things. First, we'll keep the refusal rather than an alias, since it stops before the weights load and names TF_KV_SHARE, but please let TF_GLM_KV_SHARE=0 or an empty value through, as those asked for nothing. Second, please add a CHANGELOG line that names the move from TF_GLM_KV_SHARE to TF_KV_SHARE. Third, we'll wait for the re-taken receipt on this head with TF_GLM_KV_HOST=1, as you said. After #2219 or #1411 lands, a one-line rebase in cuda_native.zig or cuda_follow.zig may be needed.

@Bizuayeu

Bizuayeu commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

First, we'll keep the refusal rather than an alias, since it stops before the weights load and names TF_KV_SHARE, but please let TF_GLM_KV_SHARE=0 or an empty value through, as those asked for nothing. Second, please add a CHANGELOG line that names the move from TF_GLM_KV_SHARE to TF_KV_SHARE. Third, we'll wait for the re-taken receipt on this head with TF_GLM_KV_HOST=1, as you said.

#2211 (comment)

@ashhart

ashhart commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thanks @Bizuayeu, all three are in, and counting the TP=3 resend on both sides was the right check. One small thing from main moving: on today's main this puts native/cuda.zig at 601 lines, one past the limit. Could you trim a line there? Then it's queued for after the 1.2.3 cut.

Bizuayeu added a commit to Bizuayeu/TensorFold that referenced this pull request Nov 5, 2026
@Bizuayeu
Bizuayeu force-pushed the pr/glm-kv-share-store branch from 559ccef to 7940c3d Compare November 5, 2026 12:45
Bizuayeu added a commit to Bizuayeu/TensorFold that referenced this pull request Nov 5, 2026
@Bizuayeu
Bizuayeu force-pushed the pr/glm-kv-share-store branch from 7940c3d to 9754b6c Compare November 5, 2026 12:48
@Bizuayeu

Bizuayeu commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

on today's main this puts native/cuda.zig at 601 lines, one past the limit. Could you trim a line there?

#2211 (comment)

Done in 9754b6c99, rebased onto main a6e25faea: the test import of cuda_streams.zig moves to cuda_kv_pool.zig's test block, so native/cuda.zig stays at main's 600 lines and this changes one condition there. The lean checker finds 425 problems on main and on the head, 0 new; the description has the checks.

@ashhart

ashhart commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thanks @Bizuayeu, native/cuda.zig is back at 600. Nothing else is owed; it's queued for after the release cut.

@Bizuayeu

Bizuayeu commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for rebasing this. The two code commits match the ones I pushed: the first differs only in context, where #2288's note now sits above its recipe hunk. One line moved, though: at 029bb696c this PR's CHANGELOG.md line is line 53, under ## 1.2.3 (line 43) instead of ## Unreleased (line 5). The commit was written against the old ## Unreleased lines, which are 1.2.3's section now, so git placed it there. It belongs under ## Unreleased, beside #2271's.

Bizuayeu and others added 4 commits November 6, 2026 02:38
… and GLM's own store goes

GLM declares the core page adapter's hooks (share_pool, share_attach), so the server opens the one prompt_radix
store for it, as for Spark 2.5, Gemma 4 and Kolibri-1 (ashhart#1657). cuda_prefix.zig goes. What stays GLM's is its side of
a pass: which passes the store keeps (the content key), the keeps at the store's marks without table swaps, and a
resumed state waiting in its owner for the pass's start on every rank (snap_restore). The rules are the old store's:
512-row pages, the 2,048-row grid, a lookahead of 1 and the tail. share_kept_ppm = 0 keeps GLM's cache as copies,
since its states past the pages are each rank's snapshots in the cache's room, so the pool and the cache plan as
under TF_GLM_KV_SHARE, and the server opens the store for it at any cache, as GLM's own store kept prompts whatever
the cache's size. TF_GLM_KV_SHARE is refused at startup, naming TF_KV_SHARE.
…o 0 and empty start

The refusal stopped a server with any TF_GLM_KV_SHARE. 0 and an empty value ask for no store, so they now start as if
it were unset; a count or any other value is still refused before the weights load, naming TF_KV_SHARE.
cuda_share.zig's host test pins the four cases.
@ashhart
ashhart force-pushed the pr/glm-kv-share-store branch from 029bb69 to de7cdcb Compare November 6, 2026 02:38
@ashhart
ashhart merged commit 17efddf into ashhart:main Nov 6, 2026
0 of 7 checks passed
ashhart pushed a commit that referenced this pull request Nov 6, 2026
@ashhart

ashhart commented Nov 6, 2026

Copy link
Copy Markdown
Owner

Thank you @Bizuayeu. This is merged into main with your commits under your own name, so you're listed as a TensorFold contributor. It ships in the next release, credited to you in the changelog and the release notes.

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.

2 participants