Skip to content

kv ledger: count the paged pool's promises in the lane host's ledger, no behaviour change (#1943 PR 1) - #2127

Merged
ashhart merged 2 commits into
ashhart:mainfrom
jschmied:tf-1943-ledger-bytes
Nov 5, 2026
Merged

ashhart merged 2 commits into
ashhart:mainfrom
jschmied:tf-1943-ledger-bytes

Conversation

@jschmied

@jschmied jschmied commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

#1943 PR 1: the pool's promises are counted in the lane host's kv_ledger.Ledger, which is now the only admission check, as approved in #1943. This PR changes no behaviour: on every paged family I could run, the startup line, the admitted count and the stop census match main.

What changes

  1. The ledger decides. The pool asks the lane host's ledger whether a promise fits. Its own promised stays a count inside the ledger's bytes, never a second gate.
  2. The prompt store's reclaimable pages count as room. They are reclaimed before LaneMemory.
  3. No import cycle. kv_pool and kv_ledger don't import each other, so kv_pool stays its own module for test-kv-pool.
  4. When the pool is counted. The lane host counts the pool from start to stop. Per-request admission still applies only to families that set Info.ledger.
  5. Which families. native/cuda.zig counts the pool of every paged family that has an admission check: Gemma 4 and GLM through kvPool, and six others through a new ledgerPool. A pool counted alone admits exactly what it did before.

Before and after (GB10)

Main is b331731e6, this branch is 9dba5af72. Each server runs serve MODEL --parallel 4 --context 8192 and gets 8 concurrent greedy requests, with /v1/load polled every 50 ms.

family paged startup line running at once, main / PR requeued stop census, main = PR reply hashes
Dory (control) no identical 4 / 4 0 / 0 none printed identical
Spark2.5 132 pages identical 4 / 4 0 / 0 promised 0, free 132 + store 0 == total 132 identical
Qwen3.6-35B-A3B (TF_QWEN_KV_PAGES=1) 75,348 pages same counts* 4 / 4 0 / 0 none printed on CUDA identical
Gemma 4 31B 132 pages identical 4 / 4 0 / 0 promised 0, free 132 + store 0 == total 132 identical
Nemotron 3.5 Lightning 51 pages + 64 slots identical** 4 / 4 0 / 0 free 51 + store 0 == total 51; slots free 55 + held 9 == 64 identical
Kolibri-1 68 pages same counts* 4 / 4 0 / 0 promised 0, free 68 + store 0 == total 68 identical

* Only the measured free device memory differs between starts. The stream, page and fixed counts are the same.
** One difference on Nemotron: this branch prints one extra informational line before the startup line (per-CPU page lists ... 0.33 GiB). It's a memory reading, not an admission line. I can drop it if you want the output byte-identical.

GLM and Flash Next have no checkpoint on this box, so they are covered by the host tests below only.

Tests

  • New kv_pool_ledger_test.zig. A 4,000-step random walk of pool operations, with and without the ledger, gives the same outcomes, census, fits(0..29) answers and reclaim requests. It runs at a page size of 4,096 and of 0.
  • kv_pool_host_test.zig. It reruns the lane host's waits and the past-the-pool refusal with the pool counted.
  • Host suites: 329/329 steps (5,150 tests passed, 24 skipped) across test-kv-pool, test-cuda-host, test-shared-lane-regressions-host, the GLM pages and admission tests, kv-safety, kv-merge, the Qwen3.5 page runs and the Flash Next pages contract.
  • The rest: zig build test and the CI set pass, diff-check is clean, and lean reports 0 new problems.

Questions

  1. Should everything stay in one PR, or should the core change land first and the family wiring follow?
  2. Should the Metal and HIP pools (Qwen3.5 Metal, Nemotron HIP, Metal glm_host) be counted here or in a later PR? For now they still use only their own pool's pages.
  3. For GLM on several GPUs, only rank 0's pool is counted. Is that right?

Overlaps (ATC): conflicts with #1335 and #2048. Clean with #1394, #2077, #2020, #1953, #2061, #1501, #1835 and #1522.

@ashhart

ashhart commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Thanks @jschmied. With only the pool counted, every test the pool makes comes out to the same comparison as on main: want(n) is max(available, n), so promise, hold and fits answer exactly as before, and the random walk and your six-family GB10 table say the same. Your questions: one PR is fine, since it changes nothing; the Metal and HIP pools can follow in their own PR; and rank 0's pool alone looks right for now, as long as the other ranks only replay what rank 0 admits; we'll look at that again with the next PR, where leases and pages share the ledger. Please drop the extra Nemotron memory line, so the startup output stays the same, and this is ready to land.

@jschmied

jschmied commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks @ashhart. I checked the Nemotron line, and it doesn't come from this PR. It's main's noteParked (native/cuda_family_memory.zig:58), which this diff doesn't touch. It logs only when the CUDA budget's reading finds at least 0.25 GiB free on the kernel's per-CPU page lists at that moment. On the base start the reading stayed below 0.25 GiB; on the head start it was 0.33 GiB, so the line came from the box's memory state, not from the code.

To show that, I'll rerun the Nemotron pair once the GPU is free, with the page cache dropped before each start, and post both startup lines here. If the line still appears only on one side, I'll find out why before this lands.

@ashhart

ashhart commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Thanks @jschmied, you're right: that line is main's noteParked, which logs whenever the budget's reading finds at least 0.25 GiB on the per-CPU page lists, so it depends on the box's memory at that moment, not on this PR. Nothing else is owed; this is ready to land. Your rerun with the page cache dropped is still welcome if the GPU frees up, but we won't wait for it.

@jschmied

jschmied commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Rerun of the Nemotron pair as promised: main b331731e6 (binary md5 ac426f69) and this branch 9dba5af72 (md5 474dfa72), two rounds, with the page cache dropped before every start.

start per-CPU line startup line stop census
main, round 1 0.53 GiB — promised 0, free 51 + store 0 == total 51; slots free 55 + held 9 == 64
PR, round 1 0.46 GiB identical to main the same
main, round 2 0.45 GiB — the same
PR, round 2 0.66 GiB identical to main the same

Main prints the line on every start too. It's noteParked reporting the box's free per-CPU pages at that moment. In the first run the base start happened to read below 0.25 GiB, and that's the only reason the line was missing there. The startup lines are identical except for the measured free memory, so I've left the code as it is. If you'd still rather have the line dropped, it's main's code, so I'd do it in its own PR.

ashhart added a commit to jschmied/TensorFold that referenced this pull request Nov 4, 2026
@ashhart
ashhart force-pushed the tf-1943-ledger-bytes branch from 9dba5af to 86fb259 Compare November 4, 2026 22:21
jschmied and others added 2 commits November 4, 2026 23:59
…shhart#1943 PR 1)

The lane host's kv_ledger.Ledger becomes the one test a paged family's
pool promises pass. The pool's `promised` stays its census and is a
sub-count of the ledger's bytes, never a second gate: `fits`, `promise`
and `hold` ask the ledger for the available pages n more need, and the
store's reclaim is asked for exactly that before LaneMemory.

A ledger that counts one pool alone has `want(n) = max(n, available)`,
so every paged family admits what it admitted before: no startup line,
admitted count or stop census changes. A pool without a host's ledger
keeps want(n) = n, today's code.

- kv_ledger: a type-erased pool view (kv_pool stays its own module),
  free/fits/holds/want, detach.
- kv_pool: an optional Gate; unit() counts empty pages as one byte.
- lane host: countPool before start, uncount at stop; per-request
  admission keys on Info.ledger (priced), not on a ledger existing.
- native/cuda.zig counts the pool of every paged family with an admit
  check: kvPool (Gemma 4, GLM) or ledgerPool (Qwen3.5, Kolibri-1,
  Spark2.5, Dory, Nemotron, Flash Next).
- Tests: a 4000-step walk of promises, takes, holds and reclaims gives
  the same outcomes, census, fits and reclaim requests with and without
  the ledger; the lane-host pool scenarios rerun counted.
@ashhart
ashhart force-pushed the tf-1943-ledger-bytes branch from 86fb259 to 8a44393 Compare November 5, 2026 00:00
@ashhart
ashhart merged commit ac51580 into ashhart:main Nov 5, 2026
2 of 7 checks passed
@ashhart

ashhart commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thank you @jschmied. 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.

@ashhart

ashhart commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thanks @jschmied! This is in TensorFold 1.2.3, out now: https://github.com/ashhart/TensorFold/releases/tag/v1.2.3

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