Skip to content

glm cuda: TF_GLM_PROMPT_TAIL=1 keeps raw prompts' tail in the unpaged prompt cache, as the KV pages' store does - #2226

Merged
ashhart merged 4 commits into
ashhart:mainfrom
Bizuayeu:pr/glm-prompt-tail
Nov 6, 2026
Merged

ashhart merged 4 commits into
ashhart:mainfrom
Bizuayeu:pr/glm-prompt-tail

Conversation

@Bizuayeu

@Bizuayeu Bizuayeu commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

On two GB10s at TP=2 with the prompt cache off the pool's pages (TF_KV_SHARE unset), TF_GLM_PROMPT_TAIL=1
makes a raw /v1/completions prompt keep the state at its last chunk start, so its repeat and a longer prompt resume
there. Unset or 0, as by default, such a prompt keeps none, as on main, and GLM refuses any other value by name
before the weights load. The tail shares the cache's budget with chat states: at the default size, five raw 32K prompts
between two chat turns pushed the chat's state out, so the tail waits behind the setting until that trade is measured.
Every reply's tokens are main's:

main the second commit, unset the second commit, TF_GLM_PROMPT_TAIL=1
bench_cache.py --require-cold --require-reuse at 8K / 32K tokens: the repeats' cached tokens, qualified 0 / 0, false / false 0 / 0, false / false 6,144 / 30,720, true / true
A raw prompt that grows (its first 8,192 tokens, then 12,288, then 12,288 again): cached tokens 0, 0, 0 0, 0, 0 0, 6,144, 10,240
The same requests' first token (s) 6.26, 8.67, 8.68 6.48, 8.70, 8.71 5.97, 4.29, 1.44
One chat of five turns, two raw 32K prompts before turn 4 and five after it: turns 4 and 5's cached tokens 28,672 and 38,912 28,672 and 38,912 28,672 and 0
Turn 5's first token (s) 6.91, 6.93 6.92 34.15

The raw prompts are upstream's tools/*.py at c085aab04, tokenized by the server's /tokenize: the 8K and 32K
fixtures are disjoint slices of them, the growing prompt takes the first tokens of a 64K slice, and the chat run's raw
prompts are 32K slices of that one, 4,096 tokens apart, so none shares a prefix with another. The chat's turns are
numbered lines, about 9.8K tokens a turn, each followed by its 32-token greedy reply, thinking off. Main's chat row is
two launches; the first commit alone, the tail always on, also gave 0 at turn 5 in two launches (34.16 and 34.02 s). The
second commit's columns were measured at 56411145a, on main 8abb30f6f: the third changes only how the setting is
read, and its own short receipt, taken at 6ad19aae1, is under Receipt.

What this changes

The paged store keeps a prompt's last page under Rules.tail, for a later turn whose history the request did not name,
and GLM's KV pages' store sets it (native/cuda_prompt_pages.zig:37). The unpaged store had no tail, and the repeat log
marks no planned family's prompt (prompt_cache_repeat.zig), so with TF_KV_SHARE unset a /v1/completions
prompt, which names no history, kept no state at all.

marksRewind now adds the tail as a mark when Rules.tail is set and the request names no history (history_len 0).
On a planned family it snaps to a chunk start like every other mark, so no pass gains a cut. A chat request names its
history and keeps that mark alone. cacheRules sets Rules.tail when the switch a family's snap_tail names reads 1
through core.profile.lookup. GLM names TF_GLM_PROMPT_TAIL, a row in core/profile.zig beside TF_GLM_KV_HOST
(identical, exact one, glm5_next), so capabilities --json and the startup line list it, and Settings.read
refuses any value but 0 or 1 by name on every rank before the weights load. The KV pages' store still sets its own
tail. A kept state costs what a chat's costs, about 0.57 GB a rank at 32K tokens on GLM-5.3-Flash at TP=2, from the
same budget, and the store frees the least recently used state first: in the chat run the five raw prompts' tails
(564 MiB each) pushed turn 4's state out.

Checks on this head

  • Head commit: 16f6b362d.
  • Lean checker on the base and on this head: 425 and 425 problems, 0 new (the 425 are inherited).
  • Tests run (Zig 0.17.0, WSL, x86_64): zig build test 7,253 tests, 7,194 passed and 59 skipped, test-cuda-host,
    test-golden, test-glm-prompts-host 8/8, test-glm-prompt-settings-host 6/6, test-glm-tp3-host 8/8,
    test-glm-prompt-arena-host 1/1, test-glm-startup-host, test-glm-pages-host, compile-cuda-tests (also
    aarch64), compile-config-server: no failures. zig fmt --check build.zig zig and git diff --check: clean.
  • CI: 7 of 7 passed on this head (No GPU's six jobs and Zig format).
  • Not run, and why: no GPU run on this head, as its code changes are 6ad19aae1's (only the recipe and
    docs/profiles.md differ); the receipts below were taken at 6ad19aae1 and before. At none of them: TP=3 (the three
    hosts that run it were in another session's use), and the KV pages' store, which this does not change.

Receipt

  • Environment: main 8abb30f6f and the second commit for the table, and 6ad19aae1 for the setting's reading, each
    built with Zig 0.17.0 and nvcc 13.3.73 for sm_121; two GB10s (128 GB unified memory each, Ubuntu 24.04.5, kernel
    7.0.0-1019-nvidia, driver 580.178.04) at TP=2 over RoCE; Bizuayeu/GLM-5.3-Flash-XSFP4 at 88736f91;
    --context 262144 --parallel 4 with the cache not named (2.2 to 2.4 GiB by the room each start found; main's
    bench_cache.py and growing-prompt rows came from a --prompt-cache-gib 4 start, which on 8abb30f6f, before the
    change in core kv_pool: an asked prompt cache comes before every window but the engine's own #2220, gave the same 2.4 GiB), TF_KV_PAGES unset (pages), TF_GLM_KV_SHARE unset; the second commit's
    columns with TF_GLM_PROMPT_TAIL empty (which it read as unset) and set to 1.
  • The setting's reading on 6ad19aae1 (binary eb8b73e7c4d3bbc5), a launch each: unset, the startup line reads
    profile: stable; registered overrides: none and bench_cache.py --require-cold --require-reuse's 8K repeats run
    cold, as on main; with =1 it reads registered overrides: TF_GLM_PROMPT_TAIL=1 (identical, env) and the repeats
    resume from 6,144 tokens; with =false both ranks stop before the weights load with
    error: TF_GLM_PROMPT_TAIL: 0 or 1. Each reply's tokens were main's first request's, and capabilities --json
    lists the row (glm5_next, cuda, identical, one, #2226).
  • Paths and platforms: the unpaged prompt cache's marks, for families whose snap_tail setting is on (GLM on CUDA with
    TF_GLM_PROMPT_TAIL=1) and requests that name no history; chat requests and the KV pages' store are unchanged, and
    with the setting unset or 0 nothing is. Run: TP=2 on two GB10s (sm_121). Not run: TP=3, other SMs.
  • Exactness: bench_concurrent.py --tokens 256 --reps 3 --temperatures 0 --levels 1,4 --alone --serial on the second
    commit with the setting on: every reply at one and four streams equalled its request alone (code and chat, 3 and 12 of
    3 and 12), and the alone runs equalled "draft": false. Resumed equals fresh: bench_cache.py's repeats and
    concurrent requests gave the first request's tokens, which were main's, on every start, resumed from the tail (6,144
    and 30,720 tokens cached) with the setting on; the growing prompt and every request of the chat run gave main's
    tokens, resumed or cold.
  • Decode speed before and after: unset, this head runs main's path; on, the first commit's, where
    bench_openai.py --tokens 256 --reps 3 --temperatures 1.0,0 gave 41.48 / 35.02 tok/s sampled and 44.18 / 35.95
    greedy on fibonacci-raw / gpu-chat-no-think, against main's 41.46 / 34.02 and 43.99 / 35.99 (medians).
  • Prompt speed before and after: cold 32K raw prompts' first token on the first commit, three in each of two alternating
    launches: main 23.30 s median (23.00 to 23.50), the first commit 23.20 s (23.02 to 23.37). The chat run's cold raw
    prompts gave 23.06 to 23.33 s unset and 23.20 to 23.79 s with the setting (one launch each), main 23.01 to 23.72 s.
  • Licence of any new weights or drafter: n/a.
Raw results

bench_cache.py --require-cold --require-reuse, the server's first requests, main = m, the second commit unset =
su, with TF_GLM_PROMPT_TAIL=1 = sn:

{"run": "m", "tokens": 8192, "qualified": false, "first_token_sha256": "fe0ef359af30fb6de6e26602f88ac9dc76612b1c69c2735efec904385fccacca", "rows": [["first", "cold", 0, 5.893, null], ["repeated", "cold", 0, 5.826, true], ["repeated", "cold", 0, 5.827, true], ["concurrent", "cold", 0, 5.839, true], ["concurrent", "cold", 0, 14.763, true]]}
{"run": "m", "tokens": 32768, "qualified": false, "first_token_sha256": "47c8b22cb55a3736b6cec2795f03938d3187b02a107c401e3ebbb5490fcf3a45", "rows": [["first", "cold", 0, 23.01, null], ["repeated", "cold", 0, 23.483, true], ["repeated", "cold", 0, 23.701, true], ["concurrent", "cold", 0, 25.072, true], ["concurrent", "cold", 0, 48.426, true]]}
{"run": "su", "tokens": 8192, "qualified": false, "first_token_sha256": "fe0ef359af30fb6de6e26602f88ac9dc76612b1c69c2735efec904385fccacca", "rows": [["first", "cold", 0, 5.883, null], ["repeated", "cold", 0, 5.831, true], ["repeated", "cold", 0, 5.849, true], ["concurrent", "cold", 0, 13.846, true], ["concurrent", "cold", 0, 5.852, true]]}
{"run": "su", "tokens": 32768, "qualified": false, "first_token_sha256": "47c8b22cb55a3736b6cec2795f03938d3187b02a107c401e3ebbb5490fcf3a45", "rows": [["first", "cold", 0, 23.096, null], ["repeated", "cold", 0, 23.703, true], ["repeated", "cold", 0, 23.712, true], ["concurrent", "cold", 0, 25.06, true], ["concurrent", "cold", 0, 48.267, true]]}
{"run": "sn", "tokens": 8192, "qualified": true, "first_token_sha256": "fe0ef359af30fb6de6e26602f88ac9dc76612b1c69c2735efec904385fccacca", "rows": [["first", "cold", 0, 5.88, null], ["repeated", "reused", 6144, 1.437, true], ["repeated", "reused", 6144, 1.437, true], ["concurrent", "reused", 6144, 1.441, true], ["concurrent", "reused", 6144, 2.956, true]]}
{"run": "sn", "tokens": 32768, "qualified": true, "first_token_sha256": "47c8b22cb55a3736b6cec2795f03938d3187b02a107c401e3ebbb5490fcf3a45", "rows": [["first", "cold", 0, 23.193, null], ["repeated", "reused", 30720, 1.461, true], ["repeated", "reused", 30720, 1.459, true], ["concurrent", "reused", 30720, 1.459, true], ["concurrent", "reused", 30720, 3.001, true]]}

The setting's reading on 6ad19aae1, a launch each unset (zu), with =1 (zn) and with =false (zf): the startup
line, bench_cache.py --require-cold --require-reuse at 8K, the refusals, and capabilities --json:

zu  [tensorfold] profile: stable; registered overrides: none
{"run": "zu", "tokens": 8192, "qualified": false, "first_token_sha256": "fe0ef359af30fb6de6e26602f88ac9dc76612b1c69c2735efec904385fccacca", "rows": [["first", "cold", 0, 5.891, null], ["repeated", "cold", 0, 5.83, true], ["repeated", "cold", 0, 5.835, true], ["concurrent", "cold", 0, 13.765, true], ["concurrent", "cold", 0, 5.839, true]]}
zn  [tensorfold] profile: stable; registered overrides: TF_GLM_PROMPT_TAIL=1 (identical, env)
{"run": "zn", "tokens": 8192, "qualified": true, "first_token_sha256": "fe0ef359af30fb6de6e26602f88ac9dc76612b1c69c2735efec904385fccacca", "rows": [["first", "cold", 0, 5.9, null], ["repeated", "reused", 6144, 1.435, true], ["repeated", "reused", 6144, 1.435, true], ["concurrent", "reused", 6144, 1.437, true], ["concurrent", "reused", 6144, 2.949, true]]}
zf  rank 0: error: TF_GLM_PROMPT_TAIL: 0 or 1
zf  rank 1: error: TF_GLM_PROMPT_TAIL: 0 or 1
capabilities --json, the row: [{"id": "TF_GLM_PROMPT_TAIL", "report_values": null, "families": ["glm5_next"], "backend": "cuda", "class": "identical", "effect": "Keep a raw prompt's state at its last chunk start, from the chat states' budget", "evidence": "#2226", "syntax": "one", "stable": null}]

A raw prompt that grows (the 64K prompt's first 8,192 tokens, then 12,288, then 12,288 again; 64 greedy tokens):

{"run": "m", "request": "first 8192", "cached_tokens": 0, "cache_state": "cold", "ttft_s": 6.258, "token_sha256": "7519437a05b69c92f2a5b0a9a55809910b132e3cb49da6acceb45c3df179cabb"}
{"run": "m", "request": "first 12288", "cached_tokens": 0, "cache_state": "cold", "ttft_s": 8.667, "token_sha256": "ef06361691497afbd41740b610355538a2be3c7da1048737d3becce0312ca591"}
{"run": "m", "request": "first 12288 again", "cached_tokens": 0, "cache_state": "cold", "ttft_s": 8.683, "token_sha256": "ef06361691497afbd41740b610355538a2be3c7da1048737d3becce0312ca591"}
{"run": "su", "request": "first 8192", "cached_tokens": 0, "cache_state": "cold", "ttft_s": 6.481, "token_sha256": "7519437a05b69c92f2a5b0a9a55809910b132e3cb49da6acceb45c3df179cabb"}
{"run": "su", "request": "first 12288", "cached_tokens": 0, "cache_state": "cold", "ttft_s": 8.7, "token_sha256": "ef06361691497afbd41740b610355538a2be3c7da1048737d3becce0312ca591"}
{"run": "su", "request": "first 12288 again", "cached_tokens": 0, "cache_state": "cold", "ttft_s": 8.705, "token_sha256": "ef06361691497afbd41740b610355538a2be3c7da1048737d3becce0312ca591"}
{"run": "sn", "request": "first 8192", "cached_tokens": 0, "cache_state": "cold", "ttft_s": 5.972, "token_sha256": "7519437a05b69c92f2a5b0a9a55809910b132e3cb49da6acceb45c3df179cabb"}
{"run": "sn", "request": "first 12288", "cached_tokens": 6144, "cache_state": "reused", "ttft_s": 4.291, "token_sha256": "ef06361691497afbd41740b610355538a2be3c7da1048737d3becce0312ca591"}
{"run": "sn", "request": "first 12288 again", "cached_tokens": 10240, "cache_state": "reused", "ttft_s": 1.439, "token_sha256": "ef06361691497afbd41740b610355538a2be3c7da1048737d3becce0312ca591"}

The chat run (the client below; main mx1 mx2, the first commit tx1 tx2, the second sum unset and snm with
the setting, each a fresh launch):

{"run": "mx1", "build": "main", "cache_gib": 2.4, "chat_cached": [0, 8192, 18432, 28672, 38912], "chat_ttft_s": [7.05, 7.93, 7.63, 7.41, 6.91], "raw_cached": [0, 0, 0, 0, 0, 0, 0], "raw_ttft_s": [23.72, 23.03, 23.04, 23.01, 23.02, 23.01, 23.05]}
{"run": "tx1", "build": "first commit", "cache_gib": 2.3, "chat_cached": [0, 8192, 18432, 28672, 0], "chat_ttft_s": [7.11, 8.0, 7.71, 7.48, 34.16], "raw_cached": [0, 0, 0, 0, 0, 0, 0], "raw_ttft_s": [23.37, 23.32, 23.38, 23.36, 23.18, 23.38, 23.4]}
{"run": "mx2", "build": "main", "cache_gib": 2.2, "chat_cached": [0, 8192, 18432, 28672, 38912], "chat_ttft_s": [7.15, 7.97, 7.67, 7.41, 6.93], "raw_cached": [0, 0, 0, 0, 0, 0, 0], "raw_ttft_s": [23.53, 23.25, 23.25, 23.24, 23.06, 23.24, 23.26]}
{"run": "tx2", "build": "first commit", "cache_gib": 2.2, "chat_cached": [0, 8192, 18432, 28672, 0], "chat_ttft_s": [7.08, 8.0, 7.71, 7.46, 34.02], "raw_cached": [0, 0, 0, 0, 0, 0, 0], "raw_ttft_s": [23.28, 23.31, 23.29, 23.29, 23.17, 23.3, 23.28]}
{"run": "sum", "build": "the second commit, unset", "cache_gib": 2.3, "chat_cached": [0, 8192, 18432, 28672, 38912], "chat_ttft_s": [6.9, 7.97, 7.68, 7.42, 6.92], "raw_cached": [0, 0, 0, 0, 0, 0, 0], "raw_ttft_s": [23.25, 23.25, 23.26, 23.26, 23.06, 23.27, 23.33]}
{"run": "snm", "build": "the second commit, TF_GLM_PROMPT_TAIL=1", "cache_gib": 2.2, "chat_cached": [0, 8192, 18432, 28672, 0], "chat_ttft_s": [6.9, 7.95, 7.67, 7.42, 34.15], "raw_cached": [0, 0, 0, 0, 0, 0, 0], "raw_ttft_s": [23.79, 23.44, 23.44, 23.2, 23.24, 23.47, 23.46]}
{"every_run_token_sha256": {"chat turn 1": "6857787d687b0b17", "chat turn 2": "1d51f0e2f81302ca", "chat turn 3": "02d7356c64c970d1", "raw 0": "c68c3ce03dee5c85", "raw 1": "a7335553cfc1c26c", "chat turn 4": "3ed736ec073af24e", "raw 2": "1922f70fd8ff7970", "raw 3": "d62384ddf02cef92", "raw 4": "04055e3159edc928", "raw 5": "228f4dec5b7968af", "raw 6": "fdebd14ad3aeeb91", "chat turn 5": "c5c145ead59085aa"}}

bench_concurrent.py --tokens 256 --reps 3 --temperatures 0 --levels 1,4 --alone --serial, the second commit with
the setting:

{"prompt": "code", "temperature": 0.0, "streams": 1, "failed": 0, "aggregate_tps": 44.2, "per_stream_tps": 44.2, "alone": {"equal": 3, "unequal": 0, "failed": 0}, "serial": {"equal": 1, "unequal": 0, "failed": 0}}
{"prompt": "code", "temperature": 0.0, "streams": 4, "failed": 0, "aggregate_tps": 116.4, "per_stream_tps": 29.7, "alone": {"equal": 12, "unequal": 0, "failed": 0}, "serial": {"equal": 1, "unequal": 0, "failed": 0}}
{"prompt": "chat", "temperature": 0.0, "streams": 1, "failed": 0, "aggregate_tps": 36.2, "per_stream_tps": 36.2, "alone": {"equal": 3, "unequal": 0, "failed": 0}, "serial": {"equal": 1, "unequal": 0, "failed": 0}}
{"prompt": "chat", "temperature": 0.0, "streams": 4, "failed": 0, "aggregate_tps": 94.2, "per_stream_tps": 24.4, "alone": {"equal": 12, "unequal": 0, "failed": 0}, "serial": {"equal": 1, "unequal": 0, "failed": 0}}

On the first commit, the setting's path: cold 32K raw prompts (cold.py: three slices 4,096 tokens apart; launches m1,
t1, m2, t2 in turn):

{"run": "m1", "ttft_s": [22.996, 23.499, 23.216], "cache_state": ["cold", "cold", "cold"], "token_sha256": ["c68c3ce03dee5c85", "a7335553cfc1c26c", "1922f70fd8ff7970"]}
{"run": "t1", "ttft_s": [23.044, 23.373, 23.175], "cache_state": ["cold", "cold", "cold"], "token_sha256": ["c68c3ce03dee5c85", "a7335553cfc1c26c", "1922f70fd8ff7970"]}
{"run": "m2", "ttft_s": [23.063, 23.431, 23.387], "cache_state": ["cold", "cold", "cold"], "token_sha256": ["c68c3ce03dee5c85", "a7335553cfc1c26c", "1922f70fd8ff7970"]}
{"run": "t2", "ttft_s": [23.018, 23.258, 23.233], "cache_state": ["cold", "cold", "cold"], "token_sha256": ["c68c3ce03dee5c85", "a7335553cfc1c26c", "1922f70fd8ff7970"]}

bench_openai.py --tokens 256 --reps 3 --temperatures 1.0,0, main = m, the first commit = t:

{"run": "m", "prompt": "fibonacci-raw", "temperature": 1.0, "decode_tps_all": [42.35, 41.46, 39.98], "token_sha": ["bb64b44e5544", "ba0b76c1abe6", "e4b32f31655e"]}
{"run": "m", "prompt": "gpu-chat-no-think", "temperature": 1.0, "decode_tps_all": [34.02, 35.62, 33.79], "token_sha": ["9d5725485ba0", "402df752cd35", "00c716572e0e"]}
{"run": "m", "prompt": "fibonacci-raw", "temperature": 0.0, "decode_tps_all": [43.99, 42.46, 44.25], "token_sha": ["5b8af3e0ec61", "5b8af3e0ec61", "5b8af3e0ec61"]}
{"run": "m", "prompt": "gpu-chat-no-think", "temperature": 0.0, "decode_tps_all": [36.08, 35.99, 35.0], "token_sha": ["68b81544a50b", "68b81544a50b", "68b81544a50b"]}
{"run": "t", "prompt": "fibonacci-raw", "temperature": 1.0, "decode_tps_all": [42.39, 41.48, 39.18], "token_sha": ["bb64b44e5544", "ba0b76c1abe6", "e4b32f31655e"]}
{"run": "t", "prompt": "gpu-chat-no-think", "temperature": 1.0, "decode_tps_all": [35.02, 35.7, 33.21], "token_sha": ["9d5725485ba0", "402df752cd35", "00c716572e0e"]}
{"run": "t", "prompt": "fibonacci-raw", "temperature": 0.0, "decode_tps_all": [44.2, 44.18, 44.11], "token_sha": ["5b8af3e0ec61", "5b8af3e0ec61", "5b8af3e0ec61"]}
{"run": "t", "prompt": "gpu-chat-no-think", "temperature": 0.0, "decode_tps_all": [35.51, 36.25, 35.95], "token_sha": ["68b81544a50b", "68b81544a50b", "68b81544a50b"]}

bench_concurrent.py --tokens 256 --reps 3 --temperatures 0 --levels 1,4 --alone --serial, the first commit:

{"prompt": "code", "temperature": 0.0, "streams": 1, "failed": 0, "aggregate_tps": 44.2, "per_stream_tps": 44.2, "alone": {"equal": 3, "unequal": 0, "failed": 0}, "serial": {"equal": 1, "unequal": 0, "failed": 0}}
{"prompt": "code", "temperature": 0.0, "streams": 4, "failed": 0, "aggregate_tps": 115.0, "per_stream_tps": 29.6, "alone": {"equal": 12, "unequal": 0, "failed": 0}, "serial": {"equal": 1, "unequal": 0, "failed": 0}}
{"prompt": "chat", "temperature": 0.0, "streams": 1, "failed": 0, "aggregate_tps": 35.9, "per_stream_tps": 35.9, "alone": {"equal": 3, "unequal": 0, "failed": 0}, "serial": {"equal": 1, "unequal": 0, "failed": 0}}
{"prompt": "chat", "temperature": 0.0, "streams": 4, "failed": 0, "aggregate_tps": 95.0, "per_stream_tps": 24.3, "alone": {"equal": 12, "unequal": 0, "failed": 0}, "serial": {"equal": 1, "unequal": 0, "failed": 0}}

Chat prompts (about 7K, 15K and 30K tokens and a count prompt, 256 greedy tokens, drafted then "draft": false), main
= m, the first commit = t:

{"run": "m", "rows": [["r6k", true, 0, "adf8085e5e8c0257"], ["r6k", false, 6144, "adf8085e5e8c0257"], ["r12k", true, 0, "52c198c1b2061f4a"], ["r12k", false, 14336, "52c198c1b2061f4a"], ["r24k", true, 0, "e21e9543757898e8"], ["r24k", false, 28672, "e21e9543757898e8"], ["count", true, 0, "4e1c090b9bd9c7ea"], ["count", false, 0, "4e1c090b9bd9c7ea"]]}
{"run": "t", "rows": [["r6k", true, 0, "adf8085e5e8c0257"], ["r6k", false, 6144, "adf8085e5e8c0257"], ["r12k", true, 0, "52c198c1b2061f4a"], ["r12k", false, 14336, "52c198c1b2061f4a"], ["r24k", true, 0, "e21e9543757898e8"], ["r24k", false, 28672, "e21e9543757898e8"], ["count", true, 0, "4e1c090b9bd9c7ea"], ["count", false, 0, "4e1c090b9bd9c7ea"]]}
The chat run's client
"""Chat turns beside long raw prompts at the default cache size.

    python3 mix.py TOOLS PROMPT.json OUT.json

One chat of five turns, each adding a user message of about 8K tokens (numbered lines) and its greedy
32-token reply, thinking off. Between turns 3 and 4 two raw prompts, within the cache's budget beside the chat's
state; between turns 4 and 5 five more, past it: 32,768-token slices of the 64K token file starting 4,096 tokens apart
(no shared prefix), through upstream's bench_concurrent.stream on /v1/completions, greedy, 16 tokens, seed
1234. A row a request: its prompt_tokens, cached_tokens, first-token time and token_sha256.
"""

import json
import sys
import time
import urllib.request

sys.path.insert(0, sys.argv[1])
import bench_concurrent as bench  # noqa: E402

BASE = "http://127.0.0.1:8095"
MODEL = "glm-tf"
KEYS = ("prompt_tokens", "cached_tokens", "ttft_s", "token_sha256")


def chat(messages, tokens):
    """The reply's text and the request's row, streamed as bench_concurrent.stream reads a chat."""
    body = {"model": MODEL, "messages": messages, "max_tokens": tokens, "temperature": 0, "seed": 1234,
            "stream": True, "stream_options": {"include_usage": True},
            "chat_template_kwargs": {"enable_thinking": False}}
    req = urllib.request.Request(BASE + "/v1/chat/completions", json.dumps(body).encode(),
                                 {"Content-Type": "application/json"})
    sent = time.perf_counter()
    first = None
    usage, runtime, reply = {}, {}, []
    with urllib.request.urlopen(req, timeout=1800) as r:
        for raw in r:
            line = raw.decode().strip()
            if not line.startswith("data:") or line == "data: [DONE]":
                continue
            chunk = json.loads(line[5:])
            usage = chunk.get("usage") or usage
            runtime = chunk.get("tensorfold") or runtime
            for choice in chunk.get("choices", []):
                piece = (choice.get("delta") or {}).get("content") or ""
                if piece:
                    first = first or time.perf_counter()
                    reply.append(piece)
    row = {"prompt_tokens": usage.get("prompt_tokens"),
           "cached_tokens": (usage.get("prompt_tokens_details") or {}).get("cached_tokens") or 0,
           "ttft_s": round(first - sent, 3) if first else None,
           "token_sha256": (runtime.get("token_sha256") or "")[:16] or None}
    return "".join(reply), row


def turn_text(t):
    lines = "".join(f"turn {t} line {i}: the reading stays within its band.\n" for i in range(8000 // 12))
    return f"{lines}\nSay in one sentence what the record of turn {t} shows."


ids = json.load(open(sys.argv[2]))
messages, rows = [], []


def emit(row):
    rows.append(row)
    print(json.dumps(row), flush=True)


def say(t):
    messages.append({"role": "user", "content": turn_text(t)})
    reply, row = chat(messages, 32)
    messages.append({"role": "assistant", "content": reply})
    emit({"step": f"chat turn {t}", **row})


def raw(k):
    item = {"name": f"raw {k}", "kind": "completion", "prompt": ids[k * 4096:k * 4096 + 32768]}
    r = bench.stream(BASE, MODEL, item, 16, 0, 1234)
    emit({"step": f"raw {k}", **{key: r.get(key) for key in KEYS + ("cache_state", "error")}})


for t in (1, 2, 3):
    say(t)
for k in (0, 1):
    raw(k)
say(4)
for k in (2, 3, 4, 5, 6):
    raw(k)
say(5)
json.dump(rows, open(sys.argv[3], "w"), indent=1)
print("chat cached: " + ", ".join(f"{r['step'][-1]}: {r['cached_tokens']}" for r in rows if r["step"].startswith("chat")),
      flush=True)

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 skip cleanly without their dependency (the first commit
    without its two lines of fix, the tail mark in marksRewind and cacheRules' snap_tail line, failed
    prompt_cache_tail_test's test and cuda_snaps' test in zig build test and test-cuda-host; the second without
    cacheRules' setting line failed cuda_snaps' test at the setting's on case in zig build test; the third without
    Settings.read's check failed cuda_ranks_test's new test in zig build test and test-cuda-host (there
    health_test's manifest test also timed out once on its 5 s socket poll, and passed on the head), and with
    cacheRules' line read as the second commit read it, cuda_snaps' test failed at 2 in zig build test;
    profile_test's new test names the row's id, so it does not build without the row; they have no dependency to skip
    on).
  • 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.
  • Every token still goes through the lane rounds. No serial path, and nothing that needs drafts off.
  • Drafted output equals "draft": false, a resumed prompt equals a fresh one, and concurrent equals solo (Exactness
    above).
  • No precision traded for speed. If the bits change, the description says which sums change and why (no arithmetic
    changes: which states the cache keeps).
  • Decode and prompt processing are no slower than main at one stream and the widest admitted; with --drafter,
    no slower than without it, greedy and sampled (unset, this head runs main's path; on, the first commit's, in the
    Receipt; the setting keeps one more state for a raw prompt and touches no decode or drafter path).

@Bizuayeu
Bizuayeu marked this pull request as ready for review November 5, 2026 05:30
@ashhart

ashhart commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thanks @Bizuayeu, the growing raw prompt going from 8.68 s to 1.44 s on its repeat, with the same tokens as main's cold run, is a clear win, and finding it while measuring #2220 was a good catch. One question before it lands: raw prompts now add a kept state to the same budget chat states use, and it's on by default for GLM. Could you run chats and raw completions together at the default cache size, for example a few chat turns, then several long raw prompts, then the next chat turn, and post the chat turns' cached_tokens on main and on this head? If chat reuse holds, we're glad to take it as is. If it drops, please keep the tail behind a setting until it's measured. We'll re-run CI on the head ourselves.

@Bizuayeu Bizuayeu changed the title glm cuda: raw prompts keep their tail in the unpaged prompt cache, as the KV pages' store does glm cuda: TF_GLM_PROMPT_TAIL=1 keeps raw prompts' tail in the unpaged prompt cache, as the KV pages' store does Nov 5, 2026
@Bizuayeu

Bizuayeu commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Could you run chats and raw completions together at the default cache size, [...] and post the chat turns'
cached_tokens on main and on this head? [...] If it drops, please keep the tail behind a setting until it's measured.

#2226 (comment)

It drops once the raw prompts outgrow the budget, so the tail now waits behind TF_GLM_PROMPT_TAIL=1 (56411145a);
unset, a raw prompt keeps no state, as on main.

One chat of five turns, about 9.8K tokens a turn, with two raw 32K prompts before turn 4 and five after it, at TP=2
with the cache not named (2.2 to 2.4 GiB), the chat turns' cached_tokens:

turn 2 turn 3 turn 4 turn 5 turn 5's first token
main, two launches 8,192 18,432 28,672 38,912 6.91 and 6.93 s
the first commit (the tail always on), two launches 8,192 18,432 28,672 0 34.16 and 34.02 s
this head, unset 8,192 18,432 28,672 38,912 6.92 s
this head, TF_GLM_PROMPT_TAIL=1 8,192 18,432 28,672 0 34.15 s

Two raw prompts fit beside the chat's state; with five, the store, which frees the least recently used state first,
let turn 4's state go for their tails (564 MiB each). Every request gave main's tokens. The description now has the
receipt for both settings, the raw rows and the client.

@ashhart

ashhart commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thanks @Bizuayeu, that table answers it: 38,912 cached against 0 on turn 5, 6.9 s against 34 s, because the store frees the least recently used state first. Keeping the tail behind TF_GLM_PROMPT_TAIL=1 is the right call, and unset matching main turn for turn is what we needed. We'll read the new head next.

@ashhart

ashhart commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thanks @Bizuayeu. Behind TF_GLM_PROMPT_TAIL=1 the default is main's mark list exactly, and the recipe now says plainly that raw-prompt states share the budget with chat states. Two small things. The check is "set, not empty, not 0", so TF_GLM_PROMPT_TAIL=false turns it on; please accept only 0 or 1 and refuse anything else by name before load, as the other GLM settings do. And please add it to core/profile.zig beside TF_GLM_KV_HOST (identical, .syntax = .one, glm5_next, evidence #2226) and read it through core.profile.lookup, so it shows in capabilities --json with the others.

Bizuayeu added a commit to Bizuayeu/TensorFold that referenced this pull request Nov 5, 2026
…y, as TF_GLM_KV_HOST does

The setting took any value but empty and 0 as on, the reading of a switch that defaults on, so =false turned it on.
It now reads as TF_GLM_KV_HOST does: Settings.read refuses any value but 0 or 1 by name on every rank before the
weights load, and the switch sits beside it in core/profile.zig (identical, exact one, glm5_next, ashhart#2226), where
cacheRules reads it through core.profile.lookup and capabilities --json lists it with the others.
@Bizuayeu

Bizuayeu commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

The check is "set, not empty, not 0", so TF_GLM_PROMPT_TAIL=false turns it on; please accept only 0 or 1 and
refuse anything else by name before load, as the other GLM settings do. And please add it to core/profile.zig
beside TF_GLM_KV_HOST (identical, .syntax = .one, glm5_next, evidence #2226) and read it through
core.profile.lookup, so it shows in capabilities --json with the others.

#2226 (comment)

Both are in a third commit, 6ad19aae1, and the branch now sits on main 9200a3064, the recipe's bullet after
#2220's.

  • Settings.read refuses any value but 0 or 1 with TF_GLM_PROMPT_TAIL: 0 or 1 on every rank before the weights
    load, and cacheRules turns the tail on at 1 only.
  • The switch is a row after TF_GLM_KV_HOST_SHARED in core/profile.zig (identical, .syntax = .one,
    glm5_next, evidence glm cuda: TF_GLM_PROMPT_TAIL=1 keeps raw prompts' tail in the unpaged prompt cache, as the KV pages' store does #2226); cacheRules reads it through core.profile.lookup, and docs/profiles.md lists it
    with GLM's.
  • Tests: cuda_ranks_test.zig takes unset, 0 and 1 and refuses empty and yes; profile_test.zig checks the
    row and that only 1 differs; cuda_snaps' test now has 2 off.

On this head at TP=2 on two GB10s, a launch each:

TF_GLM_PROMPT_TAIL startup bench_cache.py at 8K, the repeats
unset registered overrides: none cold, as on main
1 registered overrides: TF_GLM_PROMPT_TAIL=1 (identical, env) 6,144 tokens cached
false both ranks: error: TF_GLM_PROMPT_TAIL: 0 or 1, before the weights load

capabilities --json lists the row (glm5_next, cuda, identical, one, #2226). The description's checks and
receipt are updated for this head.

@ashhart

ashhart commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thanks @Bizuayeu. A profile row and a strict 0-or-1 check on every rank is exactly it. Queued for after the 1.2.3 cut.

Bizuayeu added a commit to Bizuayeu/TensorFold that referenced this pull request Nov 6, 2026
…y, as TF_GLM_KV_HOST does

The setting took any value but empty and 0 as on, the reading of a switch that defaults on, so =false turned it on.
It now reads as TF_GLM_KV_HOST does: Settings.read refuses any value but 0 or 1 by name on every rank before the
weights load, and the switch sits beside it in core/profile.zig (identical, exact one, glm5_next, ashhart#2226), where
cacheRules reads it through core.profile.lookup and capabilities --json lists it with the others.
@Bizuayeu

Bizuayeu commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main after #2211 landed: the code is unchanged, the recipe's bullet names TF_KV_SHARE as #2211 renamed it, and docs/profiles.md's GLM line keeps both sides. The description's stack, atc and checks are updated for this head.

@ashhart
ashhart force-pushed the pr/glm-prompt-tail branch from 16f6b36 to 0d89383 Compare November 6, 2026 05:38
ashhart pushed a commit to Bizuayeu/TensorFold that referenced this pull request Nov 6, 2026
…y, as TF_GLM_KV_HOST does

The setting took any value but empty and 0 as on, the reading of a switch that defaults on, so =false turned it on.
It now reads as TF_GLM_KV_HOST does: Settings.read refuses any value but 0 or 1 by name on every rank before the
weights load, and the switch sits beside it in core/profile.zig (identical, exact one, glm5_next, ashhart#2226), where
cacheRules reads it through core.profile.lookup and capabilities --json lists it with the others.
ashhart added a commit to Bizuayeu/TensorFold that referenced this pull request Nov 6, 2026
Bizuayeu and others added 4 commits November 6, 2026 06:08
… the KV pages' store does

The paged store (prompt_radix.zig) keeps a prompt's last page under Rules.tail, for a later turn whose history the
request did not name, and GLM's KV pages' store sets it (native/cuda_prompt_pages.zig). The unpaged store had no such
rule, and the repeat log never marks a planned family's prompts, so on GLM without TF_GLM_KV_SHARE a /v1/completions
prompt kept no state: the same prompt or a longer one never resumed.

marksRewind now adds the tail as a mark when Rules.tail is set and the request names no history (history_len 0),
snapped like every planned mark to the family's chunk starts. A chat keeps its history's mark alone, so its states do
not change. cacheRules reads a family's snap_tail, and GLM sets it.
A raw prompt's tail shares the prompt cache's budget with chat states, and the store frees the least recently used
first, so long raw prompts between two chat turns can push the chat's state out. Until that trade is measured, GLM
keeps the tail behind a setting: snap_tail names TF_GLM_PROMPT_TAIL, and cacheRules turns the rule on when it is set,
not empty and not 0, as TF_FLASHNEXT_REPEAT_CACHE turns Flash Next's repeat rule off.
…y, as TF_GLM_KV_HOST does

The setting took any value but empty and 0 as on, the reading of a switch that defaults on, so =false turned it on.
It now reads as TF_GLM_KV_HOST does: Settings.read refuses any value but 0 or 1 by name on every rank before the
weights load, and the switch sits beside it in core/profile.zig (identical, exact one, glm5_next, ashhart#2226), where
cacheRules reads it through core.profile.lookup and capabilities --json lists it with the others.
@ashhart
ashhart force-pushed the pr/glm-prompt-tail branch from 0d89383 to 16a11eb Compare November 6, 2026 06:08
@ashhart
ashhart merged commit d1e91b5 into ashhart:main Nov 6, 2026
ashhart pushed a commit that referenced this pull request Nov 6, 2026
…y, as TF_GLM_KV_HOST does

The setting took any value but empty and 0 as on, the reading of a switch that defaults on, so =false turned it on.
It now reads as TF_GLM_KV_HOST does: Settings.read refuses any value but 0 or 1 by name on every rank before the
weights load, and the switch sits beside it in core/profile.zig (identical, exact one, glm5_next, #2226), where
cacheRules reads it through core.profile.lookup and capabilities --json lists it with the others.
@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