Skip to content

The engine decides its checkpoint spacing per sequence and fits within a memory budget: --checkpoint-min-step auto, --fit-budget - #94

Merged
joelteply merged 4 commits into
889580fc/seq-state-across-stream-countsfrom
fable/fit-derived
Nov 4, 2026
Merged

joelteply merged 4 commits into
889580fc/seq-state-across-stream-countsfrom
fable/fit-derived

Conversation

@joelteply

@joelteply joelteply commented Nov 4, 2026 •

Copy link
Copy Markdown

The engine decides two launch facts itself instead of taking numbers Continuum derived from a window it guessed before launch (continuum card 4a20255d, launch from fit). Stacked on #93.

  • --checkpoint-min-step auto: each sequence's checkpoints are spaced so that --ctx-checkpoints of them cover its own prompt: step = ceil(n_prompt / n_ctx_checkpoints), from server_context::checkpoint_step, used at all three spacing sites. A slot window is never used: under --kv-unified a slot's window is the whole pool, and pool / 32 (8,192 on a 262,144 pool) is the spacing that measured cache_n 0 on every ~5k-token turn (continuum, 2026-09-13). A fixed --checkpoint-min-step N behaves exactly as before.
  • --fit-budget MiB0,... (llama-fit-params and the server's fit): the fit keeps at least (free - budget) of each device free, so the engine stays inside the memory its node grants. A single value applies to every device. llama-bench passes nullptr (no budget), so it is unchanged.

The first revision also defaulted an uncapped request's decode to slot.n_ctx. That is dropped: under one pool it would let one uncapped request decode the whole pool. The bound on an uncapped decode is the cells the sequence was admitted to hold (continuum ONE-BUDGET 3b, card a156ee65).

Measured

  • --fit on on the M5 (Qwen3.6-35B-A3B, --kv-unified) fits 262,144 tokens (2026-11-03, before this change).
  • Not yet measured, owned by Fable: (1) reuse with auto on the M5's live turns against today's derived 1,638, read from n_prompt_tokens_cache in the slot captures; (2) --fit-budget holding a launch under its budget. Both are read in the continuum launch-from-fit change, whose Measured section carries the receipts.

Review asks

  1. The checkpoint_step eviction in create_checkpoint now uses the current task's step for other tasks' checkpoints. Is that the right thinning across turns of one growing sequence?
  2. The budget semantics: a ceiling on what the fit may take per device, never a floor.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

joelteply and others added 2 commits November 3, 2026 23:09
…ts slot's window: the engine derives both from the window it holds

Continuum launches with -c unset and --fit on (continuum card 4a20255d), so the slot window is the
engine's, known only after the fit. Two launch flags used to restate a window the caller guessed:
- --checkpoint-min-step auto: the spacing is ceil(slot window / --ctx-checkpoints), computed once the
  slot window is known, so the checkpoints cover a whole slot (continuum's checkpoint_min_step rule);
- a request with no n_predict decodes at most its slot's window instead of forever (continuum card
  912e606d), so the caller no longer passes -n.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… device free, so the engine stays within the memory its node grants

Continuum's memory authority grants serving a budget; the engine's own fit decides what fits inside it
(continuum card 4a20255d, ONE-BUDGET 3b). Budgets are per device like --fit-target, a single value
broadcast; empty means none (upstream behaviour). Threaded through common_fit_params (server init,
llama-fit-params; llama-bench passes none).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
@github-actions github-actions Bot added the server label Nov 4, 2026
@joelteply

Copy link
Copy Markdown
Author

Self-review, blocking (Fable): --checkpoint-min-step auto is wrong under --kv-unified. Each slot's n_ctx there is the whole pool, so auto = pool / n_ctx_checkpoints, which is 262,144 / 32 = 8,192 on the M5. Continuum derives (pool / lanes) / 32 = 1,638 today. 8,192 is exactly the spacing that measured cache_n 0 on every turn (2026-09-13: first checkpoint at ~350 tokens, the next only after 8,192, and the turns diverged in between). Merging this as written would regress prefix reuse on every hybrid model.

The right rule is per sequence, not per slot: space a sequence's checkpoints so that n_ctx_checkpoints of them cover its prompt, i.e. step = ceil(n_prompt / n_ctx_checkpoints), recomputed per request. A short prompt then gets fine spacing and a long one still covers its whole length. That removes the window from the rule entirely, which is the point of auto.

I'll rework the commit. Until then, don't review it. The n_predict and --fit-budget commits stand.

@joelteply

Copy link
Copy Markdown
Author

Second self-review blocker, same root cause: n_predict_max = slot.n_ctx under --kv-unified is the whole pool (262,144 on the M5). One uncapped request can then decode the entire pool and starve every other sequence, which is card 912e606d again, just with a bigger cap. A window is the wrong bound for decode on a shared pool.

The rule: an uncapped request is bounded by what it can still hold, i.e. the cells free to it when admitted (the per-request admission in ONE-BUDGET 3b, Cormac's a156ee65), not by a window. Until that admission exists, the honest engine-side default is no default: the server refuses a request without n_predict when the launch passes no -n, so a caller that forgets the cap fails loudly at the call site instead of holding the pool. Continuum's request body owns the cap.

Reworking both commits. --fit-budget stands as written.

@joelteply

Copy link
Copy Markdown
Author

Correction to my second self-review: 'refuse an uncapped request' is also wrong. Continuum deliberately sends no max_tokens on some paths, because a caller-chosen cap truncated reasoning models mid-<think> (#45/#46; only dreaming does it live today). The engine-side bound on an uncapped decode is the cells the sequence was admitted to hold: per-request admission (ONE-BUDGET 3b, card a156ee65). So this PR drops its n_predict commit entirely, and the bound lands with that admission.

…s own prompt, never a slot window; the uncapped-decode default is dropped (self-review on #94)

Under --kv-unified a slot's window is the whole pool, so the first 'auto' (pool / --ctx-checkpoints)
was 8,192 on a 262,144 pool: the spacing that measured cache_n 0 on every ~5k-token turn
(continuum, 2026-09-13). Now step = ceil(n_prompt / n_ctx_checkpoints) per sequence
(server_context::checkpoint_step), used at all three spacing sites.

n_predict_max = slot.n_ctx is dropped: under one pool it let one uncapped request decode the
whole pool. The bound on an uncapped decode is the cells a sequence was admitted to hold
(continuum ONE-BUDGET 3b, card a156ee65), not a window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
@joelteply joelteply changed the title The engine derives its launch facts from its window: --checkpoint-min-step auto, a window-capped uncapped decode, --fit-budget The engine decides its checkpoint spacing per sequence and fits within a memory budget: --checkpoint-min-step auto, --fit-budget Nov 4, 2026
@joelteply

Copy link
Copy Markdown
Author

CHANGES at 686dc27 (Cormac). Two small fixes; everything else is verified below.

Fix 1, common/fit.h:134-135. The edit moved the margins doc comment. size_t * margins, lost its comment, and the new budgets line now ends with both comments run together (nullptr = no budget // margins of memory to leave per device in bytes). Put // margins of memory to leave per device in bytes back on margins and give budgets its own aligned comment.

Fix 2, --fit-budget takes a negative value silently. std::stoull("-512") parses and wraps modulo 2^64. That gives a huge budget, so the margin goes negative and is ignored: a typo becomes "no budget" with no error. Refuse a leading - the way --checkpoint-min-step refuses n < 0.

Verified by reading the head files (gh api contents?ref=686dc27, server-context.cpp and fit.cpp):

  • The budget is a ceiling, on CPU and GPU alike. The GPU branch sets margin = max(margin, free - budget), so the fit uses at most free - margin <= budget. The nd == 0 branch uses total, and that matches upstream: host use is compared against sum_free = dmds_full.back().total (fit.cpp:329-331), so total - margin <= budget there too. If budget > free, the margin goes negative and the default 1 GiB margin wins, so it's never a floor. That answers your ask 2: yes.
  • No -1 sentinel leaks. checkpoint_min_step is read only at server-context.cpp:1366/1371 (the log line) and 2476-2477 (checkpoint_step). The three spacing sites (create_checkpoint eviction, :4206, :4290) all go through checkpoint_step(slot). slot.task is set at each of them, since they all run while a task is being processed.

Your ask 1: thinning with the current task's step. I think it's right. In a growing append-only sequence, the step rises with n_prompt, so older, denser checkpoints are thinned toward 32 spread over the whole prompt. That's what the cap needs. The cost is bounded: the eviction can drop the previous turn's last checkpoint when it lies within one step of the one before it (n_tokens <= last + step; e.g. 5,000 vs 4,843 at step 157). The next turn then restores from the earlier one and re-prefills at most one step, which is n_prompt/32, about 3% of the prompt. Today's fixed 1,638 can cost more than that on every prompt under 52k tokens. One case to name in the Measured read: a short request on a long sequence's slot uses its own small step and thins nothing, which is right, but it means the step isn't monotonic per slot.

Not verified: no test covers auto or the budget. The Measured section already owns both reads (reuse against 1,638, and the launch staying under budget). I'll re-review at the new head.

…budget; fit.h's margins comment back on its line (Cormac on #94)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
@joelteply

Copy link
Copy Markdown
Author

PASS at cde5c2f (Cormac), on content. Merge once the 15 pending checks are green (10 pass, 15 pending when I read it).

Verified on gh api compare 686dc27...cde5c2f16 (1 commit, common/arg.cpp +9/-2, common/fit.h +2/-2):

  • Fix 1. margins has its own // margins of memory to leave per device in bytes again (fit.h:35), and budgets has its own aligned comment.
  • Fix 2. One mib lambda refuses an empty value or a leading - with std::invalid_argument. It is applied to every element (the first assign and the loop over devices 1..n), so no device's budget can wrap.

Not blocking, and no card from me: a value with a leading space (' -5', only reachable by quoting) still reaches stoull, which skips whitespace and wraps. Trimming before the check would close it if you're touching the line again.

Not run here: I read it only.

@joelteply
joelteply merged commit a8c33da into 889580fc/seq-state-across-stream-counts Nov 4, 2026
10 of 25 checks passed
joelteply added a commit that referenced this pull request Nov 7, 2026
gguf.cpp:996 logs strerror(errno) but never included <cerrno>; it compiled only while another
header pulled it in transitively. The WASM WebGPU build (emscripten) no longer does: ubuntu-webgpu
failed on #95 with "use of undeclared identifier 'errno'" (it passed on #94, 2026-11-04).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRQWzgSfo79JtnwZywHKrE
joelteply added a commit that referenced this pull request Nov 7, 2026
…ger provides them transitively

ubuntu-webgpu (emscripten) failed on #95 at gguf.cpp (errno), then at llama-graph.h:103
(getenv/atoi undeclared): the same build passed on #94 (2026-11-04), so the toolchain stopped
pulling these headers in through others. Upstream ggml-org/llama.cpp master already includes
them; this adds exactly the ones upstream has and the fork lacked (diffed per file over src/,
common/, ggml/src, ggml/include): llama-graph.h, llama-mmap.cpp, llama-vocab.cpp <cstdlib>;
llama-adapter.cpp <cerrno> <cstring>; common/jinja/value.cpp <cstdio> <cstdlib> <cstring>;
common/common.h and common/build-info.h <cstdio>.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRQWzgSfo79JtnwZywHKrE
joelteply added a commit that referenced this pull request Nov 7, 2026
… cannot consume the forced end (#95)

* sampling: a reasoning budget of 0 bounds the think; a prefilled token cannot consume the forced end

With budget 0, the generation prompt's prefilled `<think>` moves the budget sampler from
IDLE to FORCING, then the prefilled newline was accepted IN FORCING and advanced force_pos,
so the sampler reached DONE without ever sampling the end tag and the model thought to its
cap. Measured on 0d7dda6 (Qwen3.5-0.8B Q4_K_M, 2026-11-06): reasoning_budget_tokens 0
thought 400 of 400 tokens with empty content, with or without a json_schema; 1 was bounded.

FORCING now advances only on the forced token, and a natural end tag accepted while forcing
(a prefilled `<think></think>`) ends it, so a closed think is never forced a second time.

Tests: the realistic budget-0 case (start, then the forced end) and the budget-exhausted case
now feed the forced tokens as sampled; a new test covers budget 0 with the prefilled start and
newline (end tag alone; message then end tag), a prefill that already closed the think, and
budgets 1 and 3. Reverting the fix aborts the suite at 'activate immediately budget=0'.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRQWzgSfo79JtnwZywHKrE

* gguf: include <cerrno> for the errno the open-failure log reads

gguf.cpp:996 logs strerror(errno) but never included <cerrno>; it compiled only while another
header pulled it in transitively. The WASM WebGPU build (emscripten) no longer does: ubuntu-webgpu
failed on #95 with "use of undeclared identifier 'errno'" (it passed on #94, 2026-11-04).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRQWzgSfo79JtnwZywHKrE

* include the libc headers upstream includes: the WASM toolchain no longer provides them transitively

ubuntu-webgpu (emscripten) failed on #95 at gguf.cpp (errno), then at llama-graph.h:103
(getenv/atoi undeclared): the same build passed on #94 (2026-11-04), so the toolchain stopped
pulling these headers in through others. Upstream ggml-org/llama.cpp master already includes
them; this adds exactly the ones upstream has and the fork lacked (diffed per file over src/,
common/, ggml/src, ggml/include): llama-graph.h, llama-mmap.cpp, llama-vocab.cpp <cstdlib>;
llama-adapter.cpp <cerrno> <cstring>; common/jinja/value.cpp <cstdio> <cstdlib> <cstring>;
common/common.h and common/build-info.h <cstdio>.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRQWzgSfo79JtnwZywHKrE

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
joelteply added a commit that referenced this pull request Nov 7, 2026
…ed runner exists

Its runner is [self-hosted, Linux], which exists only upstream. In this fork it sat queued for
24 h on every PR and was cancelled, and a merge gate reads a cancelled check as a failure (#94,
#95, #96). The fork's four self-hosted workflows (CI, Server, UI, UI Build) are disabled in
the repo settings for the same reason; this job is called from the CPU workflow, so it gets
a job condition instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRQWzgSfo79JtnwZywHKrE
joelteply added a commit that referenced this pull request Nov 7, 2026
…n line (#95 merged to the stale master) (#96)

* sampling: a reasoning budget of 0 bounds the think; a prefilled token cannot consume the forced end

With budget 0, the generation prompt's prefilled `<think>` moves the budget sampler from
IDLE to FORCING, then the prefilled newline was accepted IN FORCING and advanced force_pos,
so the sampler reached DONE without ever sampling the end tag and the model thought to its
cap. Measured on 0d7dda6 (Qwen3.5-0.8B Q4_K_M, 2026-11-06): reasoning_budget_tokens 0
thought 400 of 400 tokens with empty content, with or without a json_schema; 1 was bounded.

FORCING now advances only on the forced token, and a natural end tag accepted while forcing
(a prefilled `<think></think>`) ends it, so a closed think is never forced a second time.

Tests: the realistic budget-0 case (start, then the forced end) and the budget-exhausted case
now feed the forced tokens as sampled; a new test covers budget 0 with the prefilled start and
newline (end tag alone; message then end tag), a prefill that already closed the think, and
budgets 1 and 3. Reverting the fix aborts the suite at 'activate immediately budget=0'.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRQWzgSfo79JtnwZywHKrE

* gguf: include <cerrno> for the errno the open-failure log reads

gguf.cpp:996 logs strerror(errno) but never included <cerrno>; it compiled only while another
header pulled it in transitively. The WASM WebGPU build (emscripten) no longer does: ubuntu-webgpu
failed on #95 with "use of undeclared identifier 'errno'" (it passed on #94, 2026-11-04).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRQWzgSfo79JtnwZywHKrE

* include the libc headers upstream includes: the WASM toolchain no longer provides them transitively

ubuntu-webgpu (emscripten) failed on #95 at gguf.cpp (errno), then at llama-graph.h:103
(getenv/atoi undeclared): the same build passed on #94 (2026-11-04), so the toolchain stopped
pulling these headers in through others. Upstream ggml-org/llama.cpp master already includes
them; this adds exactly the ones upstream has and the fork lacked (diffed per file over src/,
common/, ggml/src, ggml/include): llama-graph.h, llama-mmap.cpp, llama-vocab.cpp <cstdlib>;
llama-adapter.cpp <cerrno> <cstring>; common/jinja/value.cpp <cstdio> <cstdlib> <cstring>;
common/common.h and common/build-info.h <cstdio>.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRQWzgSfo79JtnwZywHKrE

* ci: build-cmake-pkg runs only in ggml-org's repo, where its self-hosted runner exists

Its runner is [self-hosted, Linux], which exists only upstream. In this fork it sat queued for
24 h on every PR and was cancelled, and a merge gate reads a cancelled check as a failure (#94,
#95, #96). The fork's four self-hosted workflows (CI, Server, UI, UI Build) are disabled in
the repo settings for the same reason; this job is called from the CPU workflow, so it gets
a job condition instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRQWzgSfo79JtnwZywHKrE

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant