Skip to content

pool: frozen pages, a token-keyed prefix tree, a host tier; ablated to what kern-serve uses and gated by one GPU e2e - #17

Merged
xiaguan merged 27 commits into
masterfrom
pool-radix
Sep 15, 2026
Merged

xiaguan merged 27 commits into
masterfrom
pool-radix

Conversation

@xiaguan

@xiaguan xiaguan commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Why

A review found a P1 in the host tier: a checkpoint taken inside a page shared that page with the lease that kept writing it, and Host::park, deduplicating parked pages by device node id, reused the stale copy. It was a missing invariant, not a typo: nothing in the types said a chain's page is frozen. This PR puts the invariant into the types, rebuilds the index and the host tier on top of it, then cuts the crate back to what kern-serve uses and closes the gate with one GPU e2e over every target. Design record: docs/pool.md (validated against Dynamo KVBM / kv_router and SGLang's radix caches).

Pool

  • Frozen pages. A Node's page is never written after the node exists. Whole pages are shared through the chain, the page a checkpoint ends inside is copied for it, and the only writer of any page is a live lease into its own pages. restore and fork already did this; checkpoint was the exception.
  • One set of types over both tiers. Storage (sealed; Pool and Host), Store<T> = a prefix's bytes in one tier, Checkpoint = Store<Pool>, Parked = Store<Host>. Value semantics: a clone is another holder, the last drop returns what nobody else holds. No ids cross a module boundary.
  • Twin link. A device node holds a Weak to its host copy: parking a session again copies only the new pages, and a woken checkpoint is born twinned and parks again for free (the lessons.md gap).
  • Wake is one allocation. Host::restore(&Parked, &Pool, len, tokens) has the shape of Pool::restore: the pages the request needs taken once, the parked prefix copied into them (whole pages sealed and twinned, so the next park is free), and the scheduler admits the landed row at once. The parked entry stays in the index; the request's finish indexes the longer context.
  • Token-keyed radix prefix index. Entries own their stores, so the tree branches at any token while no page is ever split; a Hit holds a clone and survives make_room. Longest usable entry wins, resident first at equal length.
  • Ablated to what kern-serve uses. Prefix makes room itself (evict(park)Evicted::Parked / Dropped); coldest / park / remove are no longer public, Store exposes tokens / has_slot only, Host is built from bytes, Waking::tokens and Tray::fork are gone. No mod tests: the accounting is tested through what it hands out (tests/pool.rs reference Mapping model landing Remap plans, tests/prefix.rs brute-force model learning tiers from Evicted, tests/host.rs).
  • Host-tier visibility. kern-serve's stats line and the AgentX replay report hits by tier: resident_hits / resident_hit_tokens / host_hits / host_hit_tokens.

One GPU e2e over the four spec families

tools/e2e/e2e.py drives every target of a kern.toml through kern-serve the way a client does (README in the dir): kern test, conc1 vs kern run, repeat / turn2 hits, warm vs kern run and vs a cold server, twelve at once, a client hanging up mid-stream, a small pool with a host tier filled until the server parks, slot growth from two slots, --rows 1. Byte identity gates; a divergence on a single-rank target is excused only by the logits of that step (kern run --prompt-ids <shared context> --rows 1 --probe-dir, both tokens within 4 ULPs of the top); tray targets report cross-path identity instead of gating it.

Writing the scenarios found three interface problems, fixed here:

  • kern run --prompt-ids: an answer's text need not tokenize back to its ids, so turn2 is sent as ids and the oracle replays them.
  • kern-serve: a speculative round accepting past max_tokens or the stop left a request-end snapshot keyed by tokens no next turn sends (DSv4.1 2/4, dflash2 1/4 turn2 misses, a slot held until evicted). Not kept any more (not kept debug line); a state ending exactly at the stop token is.
  • kern-serve: a scheduler panic left the port open with every request hanging; the process exits 101.

A toy gate over the same path, exact, in three minutes

tools/toy: manifests that are not models. Each token slot holds 64 marks of its token and position; the next token is the sum over every word of every slot the sequence holds (plus a line's ordered fold for a per-sequence state); a speculative round chains its own rows and takes 1 + S % rows. tools/toy/model.py is the same arithmetic, so e2e.py --reference tools/toy/model.py gates every scenario token-exactly, including warm vs cold and woken vs cold that a real model can only report. Six manifests (paged, stateful, spec, stateful spec, a 64 KiB/token 281 GiB one, and a 128-thread build as kern test's A): all pass on one GB300 in 2 min 54 s with 256 tokens per request. On its first day it caught a regression: master's 64 MiB chunk cap (bb50189) turned --capacity <tokens> into whole chunks (2832 tokens → 1023 pages, the small-pool scenario parked nothing), invisible on real models whose pages exceed a chunk; Pool::new now takes the tokens asked for and caps the pages. (The kern run / kern-serve disagreement on a weights file's checkpoint directory it also hit was fixed on master the same day.)

The review, fixed test-first

An Opus design review of this PR read ten confirmed defects. Each got a test that failed on the code as it was, then the fix (commit per finding):

  • concurrent was not a gate, and under the exact oracle toy-stateful's twelve concurrent answers were all wrong: gen.py bound the decode fold's rows to the batch's tokens, so every group folded the others' rows into its line. Gated → FAIL: concurrent 0/12 → decode folds a literal 1.
  • A near-tie excused the whole answer. Same now excuses that token alone, takes the oracle's continuation from the server's token and compares on (≤ 3 per answer); qwen3-4b's repeat answer has an exact tie at token 46 (two logits at 21.5), excused, the remaining 18 match. The bf16 ulp is frexp's exponent, not int(x).bit_length().
  • turn2_hit paired misses with a count of not kept lines; a miss now pairs with its own first turns' request ids. spec_acceptance was a fixed 20%; it is half of the serial rate, inside the bounds an exact oracle states (toy: 40–60). A single-rank target with no oracle fails instead of reporting; all-skipped exits nonzero; with --reference, kern run on the spare GPU is itself held to the reference. The verdicts are pure functions, tools/e2e/test_e2e.py, run by CI.
  • kern-pool: Prefix::evict's successful park lost the count of parked entries dropped for the room (host_evictions undercounted); lookup restamped an off-path candidate sharing no whole page, so a query that found nothing made the coldest entry warm. Both reproduced in tests/prefix.rs first.
  • Toy coverage: slots and lines are written and checked whole (tails are functions of the head / carry, a mismatch poisons the sum), head words rotate by position so page order is in the answer, every shape has a -ref for kern test. That found kern test's prefill sweep going to tokens.max (8192) past the 4096-token lease, and the k3 golden harness reading slots a lease had sealed.
  • Driver races, from running the gate repeatedly: the small-pool fill stopped at "four parked lines" and a log-read race decided whether turn2 woke anything; serving is logged before pegainfer binds the port. The fill runs until the checkpoints turn2 hits have parked; ready is when the port answers.

Toy gate 2026-09-15 tray06 (two GPUs): five of five in 3 min 09 s. qwen3-4b same day: nine of nine. Docs: docs/serve.md "toy 门禁", docs/pool.md (sealed rule, Pool::new(.., tokens)).

The pre-merge DSv4.1 run found a livelock, fixed the same way

DSv4.1 EP4 on tray06 (--chunk 128 --max-seqs 16) hung in the host session's second turn: 2.36M parked tokens=86 lines. The wake path decided room twice — wake the checkpoint (one allocation), index it, lease from it (another) — and when the woken pages plus the half page's copy plus the continuation were one page more than the pool, the only resident make_room could park was the one just woken. A new e2e scenario, wake_room (a pool sized for exactly one worst-case second turn), reproduces it on toy-stateful in 30 s (6.08M lines), then:

  • pool / runtime / kern-serve: wake into the lease (above); Host::wake and the scheduler's woken list are gone.
  • kern-serve: the same scenario then exposed that max_request_tokens counted the pad's page — a request filling the pool exactly was neither rejected nor ever seated. Tray::max_seq_tokens leaves the pad its page and an over-cap request is a ContextLength reject before any lease.

Rerun: toy five of five (stateful targets park once and wake into the row), qwen3-4b / -dspark all pass, DSv4.1 EP4 8 gated all pass (wake_room parks=1, woken into a 2-page lease). docs/lessons.md: "一个请求的房间只问一次".

Gate

  • cargo test -p kern-pool: property tests against reference models, green; workspace tests, clippy, fmt, hawk green on the host; kern-serve builds, tests and clippy clean in the kernel-lab container.
  • e2e 2026-09-14 (tray06 / tray07 / tray09, 4×GB300 each): qwen3-4b, qwen3-4b-dspark, qwen3.8-27b, qwen3.8-27b-dflash2, DeepSeek-V4.1-Flash EP4, Kimi-K3 4-layer EP4 — all pass. Table in docs/serve.md "e2e 门禁". Toy gate 2026-09-15 (tray06, two GPUs, after the review round): five targets, all pass; qwen3-4b under the same rules, all pass.
  • AgentX replay, qwen3.8-27b shape (page 784, 64 KiB/token, 147 MiB slot, 250 GiB, concurrency 32):
host tier hit resident hits host hits (woken) extend p50 / p90 / p99
none 93.1% (unchanged) 85 217 req 0 1202 / 12 956 / 430 464
512 GiB 96.4% (was 95.6%) 85 972 req, 94.4% 1 276 req, 443.6M tokens (2.1%) 1174 / 10 128 / 129 728 (was 208k)

Open, noted in serve.md: K3 4-layer at the default budget is 131k chunks of 2 MiB (chunk = half the smallest object), 40–60 s of mapping per rank and one cuMemSetAccess OOM — the chunk should scale with the budget. The review's P2 (rebalance over-counting shared chunks) is orthogonal and still open.

🤖 Generated with Claude Code

https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-14T10:19:33.483547Z ece29fd PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Err(Error::Denied(Denied::Busy)) if self.make_room()? => {}

P1 Badge Reserve capacity before repeatedly waking a parked prefix

When a parked prefix fits in the currently free pages but the request's full worst allocation does not because active rows hold the remainder, the wake succeeds, then lease_from returns Busy here. make_room can park the newly woken snapshot—the only resident checkpoint—so the next retries wake and park the same multi-GiB prefix repeatedly instead of waiting for an active row to finish, consuming transfer bandwidth and delaying admission. Preserve the woken resident snapshot on this Busy path or reserve the full request capacity before starting the wake.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +164 to +166
plan.pages.push((dev.page, at));
let n = Node::new(at, chain.take(), self, ());
*lock(&dev.twin) = Arc::downgrade(&n);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Publish host twins only after their copy is committed

When callers use the documented two-phase API to reserve several checkpoints from the same runtime, two checkpoints may share a device node: room(cp1) publishes this weak twin before any transfer is queued, so room(cp2) reuses it and omits that page from its copy plan. If the first room is then dropped or refused while the second is parked, the second room keeps the host node alive but no operation ever initializes its bytes, and a later wake silently restores stale memory. The twin must not become reusable until the room that owns its copy is committed, or dependent rooms must retain that copy obligation.

Useful? React with 👍 / 👎.

@xiaguan xiaguan changed the title pool: frozen pages, a token-keyed prefix tree, a twin-linked host tier pool: frozen pages, a token-keyed prefix tree, a host tier; ablated to what kern-serve uses and gated by one GPU e2e Sep 15, 2026
@xiaguan xiaguan closed this Sep 15, 2026
@xiaguan xiaguan reopened this Sep 15, 2026
xiaguan added a commit that referenced this pull request Sep 15, 2026
`Prefix::evict` dropped the coldest parked entries until the host had
room, then reported that count only when the resident entry was dropped
too; a park that succeeded after dropping lost it, and the scheduler's
`host_evictions` counted none of those. Both variants of `Evicted` now
carry `dropped`, and the scheduler and the replay add it whichever way
the eviction went. `Prefix::remove`'s unread result goes.

Found by the design review of PR #17; reproduced first in
tests/prefix.rs (`a_full_host_drops_its_coldest…`: the park of `c`
after `a` is dropped from the host says `dropped: 1`).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
xiaguan added a commit that referenced this pull request Sep 15, 2026
An off-path candidate sharing fewer whole pages than one with the prompt
contributes no hit, yet `lookup` restamped it as the most recently used:
a query that found nothing made the coldest entry warm, and the next
eviction took the wrong one. Only a candidate sharing at least a page is
touched now. The stray `pool_of` call keeping an import alive in
tests/prefix.rs goes with it.

Found by the design review of PR #17; reproduced first in
tests/prefix.rs (`a_lookup_that_uses_nothing_touches_nothing`: after two
lookups sharing nothing and one token with `b`, `b` is still the coldest).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
xiaguan added a commit that referenced this pull request Sep 15, 2026
gen.py handed prefill and decode the same `fold` call, its rows bound to
the `tokens` var: in a decode batch that is the batch's row count, so
every group folded every other group's row into its own line. One
request at a time never showed it (one row is one group); twelve at once
gave twelve wrong answers, which the e2e reported as "12 finished, 0
identical to conc1" and did not gate.

Found by the design review of PR #17; reproduced first by gating
`concurrent` under the exact oracle (tray06, `FAIL: concurrent`,
0/12 on toy-stateful), passing with the fold taking a literal 1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
xiaguan added a commit that referenced this pull request Sep 15, 2026
…act oracle can

The design review of PR #17 read the driver's verdicts and found what
each let through; every one is now a pure function with a test in
tools/e2e/test_e2e.py (run by CI) that failed before the fix:

- A near-tie excused the whole answer: nothing after the first divergence
  was compared. `Same` now excuses that token alone, takes the oracle's
  continuation from the server's choice and goes on comparing, at most
  EXCUSED_MAX times per answer. On qwen3-4b a repeat answer's token 46
  (two logits equal at 21.5) is excused and the remaining 18 match.
- The bf16 ulp was `int(x).bit_length()`, wrong below 0.5; `ulp_of` is
  `math.frexp`'s exponent.
- `concurrent` reported identity and gated only completion: under an
  exact oracle it is identity (which is what showed the toy's fold bug);
  under `kern run` the count stays a report.
- `turn2_hit` excused as many misses as there were `not kept` lines in
  the whole log; a miss now pairs with its own first turns' request ids
  (the frontend numbers them in arrival order, the driver counts along).
- `spec_acceptance` was a fixed 20%; it is half of the rate one request
  at a time, inside the bounds an exact oracle states.
- A single-rank target without an oracle degraded to a report; it fails.
  With `--reference`, `kern run` on the spare GPU is itself held to the
  reference (`run_equals_reference`).
- A run in which every target was skipped exited 0; `summarize` returns
  nonzero when nothing ran.

Gates: toy (tray06, GPUs 0,1) five of five; qwen3-4b (tray06, GPUs 2,3)
nine of nine.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
xiaguan and others added 19 commits September 15, 2026 07:24
A review found that a checkpoint taken inside a page shared that page
with the lease that kept writing it, and the host tier, deduplicating
parked pages by device node, reused the stale copy. That was a missing
invariant, not a typo: nothing in the types said a chain's page is
frozen. Now it is. A `Node`'s page is never written after the node
exists: whole pages are shared through the chain, the page a
checkpoint ends inside is copied for it, and the only writer of any
page is a live lease into its own pages (`docs/pool.md`).

The accounting is one set of types over both tiers. `Storage` is the
sealed trait `Pool` and `Host` implement, `Store<T>` a prefix's bytes
in one of them (`Checkpoint = Store<Pool>`, `Parked = Store<Host>`),
held by value: a clone is another holder, the last drop returns what
nobody else holds. A device node keeps a `Weak` twin of its host copy,
so parking a session again copies only the new pages, and a woken
checkpoint is born twinned and parks again for free. `wake` gives a
resident `Checkpoint` back rather than a lease: the scheduler indexes
it and continues from it like any other hit, so several requests can
share a woken prefix. Nothing crosses a module boundary as an id;
`Chain`, `Checkpoint::nodes()`, `Park` and `Pool::wake` are gone.

The prefix index is a radix tree over tokens. Entries own their
stores, so the tree branches at any token while no page is ever split;
a `Hit` holds a clone of what it found and survives whatever
`make_room` does next. A lookup takes the longest usable entry, a
resident one first at equal length, counting stateless entries off the
prompt's path through per-node subtree counts.

kern-serve's stats line and the AgentX replay report hits by tier
(`resident_hits`, `host_hits` and their tokens), so a host tier that is
never hit is visible.

Gate: kern-pool's property tests (byte-level lease / checkpoint /
restore / fork / retire against a position model, park / wake bytes
and host partition, the index against a linear scan run twice) pass;
the old `checkpoint` fails the first one. AgentX replay in the
qwen3.8-27b shape (page 784, 147 MiB slot, 250 GiB, concurrency 32):
93.1% without a host tier, unchanged; with 512 GiB 95.6% -> 96.4%,
p99 extend 208k -> 130k, 1,276 requests woken from the host
(~/bench_results/2026-09-14-pool-radix-replay).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
…hat it hands out

Trimmed against the one consumer, kern-serve: the make-room loop the
scheduler and agentx_replay each carried (coldest resident → park, drop
the coldest parked until it fits, else drop) is `Prefix::evict`, which
names the entry it took (`Evicted::Parked(key)` / `Dropped { key,
parked }`); `coldest`, `park` and `remove` are private, `Hit::tier /
resident / parked`, `Prefix::resident / parked / is_empty`, `Store::pages
/ paged`, `Host::bytes / pages / page_bytes / slot_bytes`,
`Parked::slot_offset`, `Remap::is_empty`, `Waking::tokens` and
`Tray::fork` had no caller outside tests and are gone; `Pooled::objects`
is private.

The three `mod tests` that read the ledger (chunks, the pool's statuses,
the tree's edges) are gone with the `cfg(test)` accessors they needed.
What they checked is checked one level up, through the public API: the
pool property tests land every planned `Remap` into a model of the
chunks (a chunk is free or at one position, whatever a handle names is
mapped whole, the counters say what the plans said), the prefix model
learns what `evict` took from the key it names, and `runs` is an
integration test. kern-pool: 40 tests, no GPU.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
The e2e driver compares a served answer with kern run's on the same
tokens; a prompt given as text tokenizes the same on both sides only
until an answer's text is fed back (it need not tokenize to its ids).
Given ids, the oracle runs exactly what the server ran.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
A speculative round accepts tokens the client never gets: past
max_tokens, or past the stop token. With a recurrent state they are in
the sequence's state, and the request-end snapshot keyed by them is a
prefix no next turn sends; it held a slot until evicted and never hit
(e2e turn2 on DSv4.1: 2 of 4 misses, dflash2: 1 of 4). Such a state now
returns to the pool at once, with a debug line; a state ending exactly
at the stop token is kept, a chat's next turn ends the answer with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
The scheduler thread unwinding left the port open and every request
hanging until the client's timeout; the e2e driver sat 15 minutes on
one. The process exits 101 instead, the panic message already printed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
One driver, one kern.toml, every target through the same scenarios over
the binaries a user has: kern test, conc1 against kern run, the hits
the checkpoints allow (repeat, turn2 as ids), warm against kern run and
against a cold server, twelve at once, a client hanging up mid-stream,
a small pool with a host tier filled until the server parks, a
stateful manifest growing its slots from two, a speculative manifest
at --rows 1. Byte identity gates; a divergence on a single-rank target
is excused only by the logits of that very step (kern run --prompt-ids
over the shared context, both tokens within 4 ULPs of the top), and a
tray target reports what crossed numerical paths instead of gating it.

Gate on 2026-09-14 (tray06 / tray07 / tray09): qwen3-4b, its dspark
draft, qwen3.8-27b, its dflash2 draft, DeepSeek-V4.1-Flash EP4 and
Kimi-K3 4 layers EP4 all pass; numbers in docs/serve.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
A manifest that is not a model: each token slot holds 64 marks of its
token and position, the next token is decided by the sum over every
word of every slot the sequence holds (plus a line's ordered fold for a
per-sequence state), a speculative round chains its own rows and takes
1 + S % rows of them. tools/toy/model.py is the same arithmetic, so a
served answer has one right value and a page served stale, a slot
remapped wrong or a line restored from the wrong copy is a wrong token,
not a near-tie. gen.py builds the cubin (256 and 128 threads, the pair
kern test A/Bs), six manifests (paged, stateful, spec, stateful spec,
a 64 KiB/token big one), a byte-level tokenizer, the salt weight and a
kern.toml. kern run agrees with the reference on every one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
--reference <module.py> makes a Python generate(ids, max_tokens,
manifest) the oracle for every target whatever its ranks; it excuses
no divergence. The kern run oracle now replays ids (--prompt-ids) for
every answer, as it already did for turn2 and the probe, so one code
path feeds both oracles.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
`--capacity <tokens>` became chunks (one per state at least) and the
pool made every page the chunks hold: with the 64 MiB chunk of bb50189,
`--capacity 2832` on a 4 KiB/token manifest gave 1023 pages, not 177,
and the toy gate's small pool parked nothing. Pool::new takes the tokens
asked for and makes that many pages at most; the chunk's tail stays
empty, as a chunk is whole and a capacity is not.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
Five toy targets through the whole scenario set on one GB300 in under
three minutes, every gate exact, including the ones a real model can
only report; the page-cap regression it caught.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
A node's page is never written after the node exists. Every other
holder was already kept out by the types (a node's page id is private,
a restored lease starts past its prefix); the lease that moved the page
into a chain was kept out by convention only, `slot(pos)` checking the
prefix and not the pages shared since. It now refuses those too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
`Prefix::evict` dropped the coldest parked entries until the host had
room, then reported that count only when the resident entry was dropped
too; a park that succeeded after dropping lost it, and the scheduler's
`host_evictions` counted none of those. Both variants of `Evicted` now
carry `dropped`, and the scheduler and the replay add it whichever way
the eviction went. `Prefix::remove`'s unread result goes.

Found by the design review of PR #17; reproduced first in
tests/prefix.rs (`a_full_host_drops_its_coldest…`: the park of `c`
after `a` is dropped from the host says `dropped: 1`).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
An off-path candidate sharing fewer whole pages than one with the prompt
contributes no hit, yet `lookup` restamped it as the most recently used:
a query that found nothing made the coldest entry warm, and the next
eviction took the wrong one. Only a candidate sharing at least a page is
touched now. The stray `pool_of` call keeping an import alive in
tests/prefix.rs goes with it.

Found by the design review of PR #17; reproduced first in
tests/prefix.rs (`a_lookup_that_uses_nothing_touches_nothing`: after two
lookups sharing nothing and one token with `b`, `b` is still the coldest).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
`lease.slots(0..pos)` names positions inside pages the lease sealed by
checkpointing, which `Lease::slot` refuses since a377c67; the harness only
wants the pages' bytes, and `page_ids()` is the read-side accessor for
exactly that.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
The perf sweep's points went up to the manifest's `tokens.max`; the test
leases `--capacity` positions (4096 by default), so a manifest allowing
wider chunks (toy-big: 8192) staged past the lease and panicked. No real
model's `tokens.max` exceeded 4096, which is why the toy found it the day
toy-big got a `-ref` manifest and `kern test` ran on it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
gen.py handed prefill and decode the same `fold` call, its rows bound to
the `tokens` var: in a decode batch that is the batch's row count, so
every group folded every other group's row into its own line. One
request at a time never showed it (one row is one group); twelve at once
gave twelve wrong answers, which the e2e reported as "12 finished, 0
identical to conc1" and did not gate.

Found by the design review of PR #17; reproduced first by gating
`concurrent` under the exact oracle (tray06, `FAIL: concurrent`,
0/12 on toy-stateful), passing with the fold taking a literal 1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
… order

The kernels touched 512 B of a 4 KiB (or 64 KiB) slot and 16 B of a
1 MiB line, so a page copied short, a line restored at the wrong stride
or a slot remapped by half went unseen; and the sum over positions was
commutative, so a page table listing two pages swapped read the same. A
slot's tail is now a function of its head and a line's of its carry,
written whole and checked whole by the reader (a mismatch poisons the
sum), and every head word is rotated by its position before summing, so
the order of pages is in the answer. The reference computes the head sum
only; the tails are the kernels' own check and cost it nothing.

Every shape now has a `-ref` manifest (the 128-thread build), so `kern
test` holds every toy kernel to its twin, not only the paged one; this
is what found kern test's sweep past the lease on toy-big. The model
states its expected acceptance rate (`ACCEPT_PCT`, half the rows) for
the e2e to hold the count path to.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
xiaguan and others added 3 commits September 15, 2026 07:24
…act oracle can

The design review of PR #17 read the driver's verdicts and found what
each let through; every one is now a pure function with a test in
tools/e2e/test_e2e.py (run by CI) that failed before the fix:

- A near-tie excused the whole answer: nothing after the first divergence
  was compared. `Same` now excuses that token alone, takes the oracle's
  continuation from the server's choice and goes on comparing, at most
  EXCUSED_MAX times per answer. On qwen3-4b a repeat answer's token 46
  (two logits equal at 21.5) is excused and the remaining 18 match.
- The bf16 ulp was `int(x).bit_length()`, wrong below 0.5; `ulp_of` is
  `math.frexp`'s exponent.
- `concurrent` reported identity and gated only completion: under an
  exact oracle it is identity (which is what showed the toy's fold bug);
  under `kern run` the count stays a report.
- `turn2_hit` excused as many misses as there were `not kept` lines in
  the whole log; a miss now pairs with its own first turns' request ids
  (the frontend numbers them in arrival order, the driver counts along).
- `spec_acceptance` was a fixed 20%; it is half of the rate one request
  at a time, inside the bounds an exact oracle states.
- A single-rank target without an oracle degraded to a report; it fails.
  With `--reference`, `kern run` on the spare GPU is itself held to the
  reference (`run_equals_reference`).
- A run in which every target was skipped exited 0; `summarize` returns
  nonzero when nothing ran.

Gates: toy (tray06, GPUs 0,1) five of five; qwen3-4b (tray06, GPUs 2,3)
nine of nine.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
… when its port answers

Two races in the driver, both found by running the gate twice: the
small-pool fill stopped at "four `parked` lines in the log", and whether
the fifth (the checkpoint turn2 hits) had been written by the time the
log was read decided wakes=1 or wakes=0; and `serving` is logged before
pegainfer binds the port, so a fast-loading model (qwen3-4b, 0.7 s) got
its first request refused once. The fill now runs until every checkpoint
turn2 hits (the lengths the warm turn2 got) has parked, or the cap; a
server is ready when `/v1/models` answers.

Gates: toy five of five (tray06, 189 s), qwen3-4b nine of nine.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
… serve.md records the TDD round

pool.md: `slot(pos)` refuses the sealed pages, `Pool::new`'s `tokens`
caps the pages whatever the chunks hold. runtime.md: the same rule at
the lease. serve.md: the toy section's first table claimed five targets
identical while `concurrent` was not gated and toy-stateful's twelve
concurrent answers were wrong; the section now says what the review
found, what each test reproduced, and the round of 2026-09-15 on tray06.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
xiaguan and others added 5 commits September 15, 2026 07:42
…e over the same prompts

Half was slack for the wrong reason: the serial baseline was one 5 s
window, whichever held the most steps, so it covered a few prompts
while the burst covered all twelve, and acceptance differs by prompt
more than by load. The baseline is now every serial window weighted by
its steps (the same twelve prompts), and a batch only changes reduction
order, not what a sequence accepts, so the burst must reach 0.9 of it.

Measured: toy-spec 50 vs 49.3, toy-stateful-spec 50 vs 51.6,
qwen3-4b-dspark (7 rows) 24 vs 25.0; all pass (tray06, 2026-09-15).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
…ke to one park

A pool holding exactly one worst-case second turn (plus the pad's page),
`--host-gib 2`: the first turn runs, its second turn follows at once. A
stateful manifest's checkpoint ends inside a page, so the continuation
is one page more than the pool: the checkpoint must park, and the turn
must wake it into its own row and finish within 120 s with the warm
answer, at most 4 `parked` lines. A paged-only checkpoint ends at a page
and continues in place.

Red on the previous kern-serve: DSv4.1 EP4's host session on tray06
(`--chunk 128 --max-seqs 16`) hung in park_wake's second turn with 2.36M
`parked tokens=86` lines; toy-stateful under this scenario, 6,075,401
lines (`~/bench_results/2026-09-15-toy-e2e/results/tray06-r12-wake-red`).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
`Host::restore(&Parked, &Pool, len, tokens)`, the shape of
`Pool::restore`: the pages `tokens` need taken once, the first
ceil(len/unit) copied in from the host, the whole ones sealed as nodes
twinned with their host pages (the next checkpoint parks them for
free), the half page the lease's own. `Runtime::wake(p, len, tokens)`
and `awake` carry a `Lease`; `Tray::wake` a `Rising` that lands as a
`Row`; the scheduler admits a landed wake at once. The parked entry
stays in the index; the request's own finish indexes the longer
context. Gone: `Host::wake`, the woken snapshot's insert and re-queue,
the scheduler's `woken` list.

Why: waking a checkpoint and then leasing from it decided room twice.
DSv4.1 EP4's second turn hit a parked entry whose woken pages, half-page
copy and continuation were one page more than the pool; the only
resident `make_room` could park was the one just woken, so wake and
park alternated without end (2.36M `parked` lines; toy-stateful under
e2e's wake_room, 6.08M).

Gate: kern-pool tests (host.rs rewritten around restore, the property
test restores from either tier); toy gate five of five, qwen3-4b and
-dspark all pass, DSv4.1 EP4 on tray06 8 gated all pass — wake_room
parks once and wakes into a 2-page lease (`~/bench_results/2026-09-15-toy-e2e/results/tray03-r14-wake-green`,
`2026-09-14-e2e-gate/results/tray06-r9-dsv41-wake`).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
`Tray::max_seq_tokens` is the pool's row cap within the pages beside
the rank's pad, and a request whose worst case is over it is rejected
(ContextLength) before any lease. Before, `max_request_tokens` counted
the pad's page: a request filling the pool exactly was neither rejected
nor ever seated — wake_room on every target waited its 120 s out with
one `parked` line (`~/bench_results/2026-09-15-toy-e2e/results/tray03-r13-wake-green`).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6hPjvcgRZcdFcymLwFchB
Signed-off-by: JinYan Su <751080330@qq.com>
@xiaguan
xiaguan merged commit 24576d8 into master Sep 15, 2026
3 checks passed
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.

1 participant