Skip to content

feat: raise the Gemma 4 server context ceiling to 256K, with measured admission limits - #156

Open
ulises-c wants to merge 63 commits into
drumih:mainfrom
ulises-c:docs/record-256k-context-ladder
Open

feat: raise the Gemma 4 server context ceiling to 256K, with measured admission limits#156
ulises-c wants to merge 63 commits into
drumih:mainfrom
ulises-c:docs/record-256k-context-ladder

Conversation

@ulises-c

@ulises-c ulises-c commented Aug 25, 2026

Copy link
Copy Markdown

Summary

  • Raises the TurboFieldfare OpenAI server's --max-context allowlist from a 64K maximum to the Gemma 4 26B-A4B native 256K.
  • Rejects --prefill off above 64K, which this widening would otherwise have made an unguarded out-of-memory combination.
  • Adds prompt/decode timing diagnostics, a portable admission-ladder harness, a retrieval harness, and focused tests.
  • Records what happens as context grows from 64K through 256K, and what those measurements do not establish.

What is and is not established

Established on the test machine: prompts up to 253,952 tokens are admitted, prefilled to completion, and memory stable, with no swap, OOM, rejection, or cancellation at any rung.

Not established: that the model still attends across that span. The ladder sends repeated filler and takes a one-token completion, so a level passes when the server accepts and prefills the prompt. Gemma 4 sets slidingWindow: 1024 and marks only 5 of 30 layers as full attention, so all long-range signal rests on those 5 layers, and a degraded long context would return HTTP 200 here exactly like a healthy one.

Retrieval is verified at 1,543 and 14,043 prompt tokens (needle recalled at depths 0.1/0.5/0.9). 14,043 clears the 1024-token window by more than 13x, so the full-attention path demonstrably carries information well past the local window — but nothing is measured between 57K and 254K, which is the range this cap adds. See blocking question 2.

Blocking questions

These need a decision or a measurement before merge. They are recorded here rather than resolved silently in code.

1. The timing columns predate the code this branch now ships. Every rung was measured before this branch merged main, which brought in #159 ("Speed up prefill on pre-Apple10 Macs"). That change rewrote prefill attention pipeline selection and the prefill command-buffer structure — precisely the path the timing columns measure. The rungs were also captured across more than one commit with the 64K baseline recorded last, so they are not a matched set even among themselves. Re-run the full ladder on a single post-merge binary, baseline first? The memory columns are structural and are expected to survive (see the KV cross-check below); the timing columns are not.

2. Retrieval is unmeasured across the range this PR adds. Scripts/context_retrieval.py is committed and validated, but the model was unavailable when this branch was finished. Run --max-context 262144 --filler-words 250000 --depths 0.1,0.5,0.9 before merge, or merge with the scope explicitly limited to admission and open a follow-up? A miss would be a more valuable finding than the timing table.

3. Should the ladder rungs be exposed to clients at all? The client examples now advertise 32K rather than 262144, because agent clients size their history to the advertised window and one 254K request cost ~32 minutes of prefill. The 96K/128K/192K/256K caps remain available deliberately, as diagnostics. Confirm that split is what you want.

Measured findings

Apple M5 Max MacBook Pro, 18 CPU cores, 32-core GPU, 36 GB unified memory, macOS 26.5.2 (25F84), Apple Swift 6.3.3. Uniform 8,192-token reserve at every rung. Timing columns are pre-#159 — see blocking question 1.

Context cap Actual prompt E2E server time Prefill Prefill tok/s Peak footprint Peak Metal Minimum free memory Swap Result
64K baseline 57,344 236.158s 236.000s 242.983 3,441 MB 1,537 MB 76% 0 MB completed
96K 90,112 464.218s 463.937s 194.233 4,082 MB 2,177 MB 76% 0 MB completed
128K 122,880 701.019s 700.724s 175.361 4,714 MB 2,817 MB 75% 0 MB completed
192K 188,416 1,291.620s 1,290.896s 145.958 6,032 MB 4,097 MB 68% 0 MB completed
256K 253,952 1,920.565s 1,919.868s 132.276 7,320 MB 5,377 MB 62% 0 MB completed

Prompt length grows 4.43x while end-to-end time grows 8.13x. Time per prompt token rises from 4.118 ms to 7.563 ms and prefill throughput falls from 242.983 to 132.276 tok/s, so cost is superlinear in length even though a least-squares fit over these five points is near-linear (R² = 0.996).

Retrieval

Context cap Actual prompt Depths Recall
4K 1,543 0.1 / 0.5 / 0.9 3/3
16K 14,043 0.1 / 0.5 / 0.9 3/3
57K–254K not measured

KV memory cross-check

FP16 KV allocation follows from KVCacheManager: with the ring enabled each of the 25 sliding-window layers holds slidingWindow + prefillChunkTokens = 1,152 tokens, while each of the 5 full-attention layers holds maxContext, doubled for K and V.

KV(ctx) = 25 * 1152 * 4096 * 2  +  5 * ctx * 2048 * 2
Context Predicted KV Observed peak Metal Difference
64K 1,505 MB 1,537 MB +32 MB
96K 2,145 MB 2,177 MB +32 MB
128K 2,785 MB 2,817 MB +32 MB
192K 4,065 MB 4,097 MB +32 MB
256K 5,345 MB 5,377 MB +32 MB

A constant 32 MB offset across a 4.43x span means the model accounts for the scaling term and the residual is fixed overhead. This is the structural reason the memory columns should survive re-measurement, and it is where the --prefill off guard comes from: without the ring every layer allocates at maxContext, giving ~55 GiB at 256K on a 36 GB machine.

Implementation

  • ServerArguments accepts the 96K/128K/192K/256K caps, and rejects --prefill off above 64K with a message naming the resulting KV requirement. The 64K bound is the largest previously reachable context, so every previously valid invocation still resolves.
  • ServerInference exposes prefill and decode durations; ServerLog reports pp, pp_tok_s, tg, tg_tok_s alongside prompt/cached/completion counts.
  • Scripts/context_ladder.py takes --model/--server/--port/--levels/--out, defaults to this checkout, validates inputs up front, records the source commit in every result, and merges a single rung into the existing aggregate instead of overwriting it.
  • Scripts/context_retrieval.py runs needle-in-a-haystack with a real multi-token completion, sweeping depth because sliding-window degradation is position dependent. Exits non-zero on a miss.
  • docs/experiments/summaries/10-long-context.md holds the measurements and their caveats, since benchmark-results/ is gitignored and the raw artifacts cannot travel with the PR.

Testing

  • swift build -c release: clean, only pre-existing warnings.
  • Full Scripts/test.sh: 1,262 tests in 195 suites passed (152s). The 149 tokenizer .authorizationRequired failures reported earlier were environmental and do not reproduce.
  • ServerArgumentTests and ServerLogTests: 19 passed, including 13 new argument cases and 3 new log cases.
  • New argument coverage asserts rejection at each ladder rung under --prefill off, plus matched controls proving pre-ladder --prefill off and ladder --prefill on still resolve.
  • New log coverage exercises the pp_tok_s cached-token subtraction, which every ladder rung left untested at cached=0 despite prompt reuse being the server default.
  • Guard verified end to end: --max-context 262144 --prefill off is refused with the 55 GiB message; --prefill on passes validation.
  • git diff --check: clean.

Scope and limitations

This validates native 256K admission, full prefill completion, timing behavior, and memory stability on the stated machine. It does not establish long-context answer quality, semantic robustness, or useful multi-token decode throughput — the one-token completion makes tg/tg_tok_s unusable as general decode measurements. Repetitive filler is also the easy case for retrieval, so recall figures are an upper bound relative to real agent history.

Related issue

NeelM0906 and others added 30 commits July 30, 2026 23:37
- ArchConfig: ModelFamily (gemma4/qwen36), LinearAttentionConfig, qwen36_35B_A3B
  baseline, layer-mask value 2 for gated-DeltaNet linear attention layers
- ManifestReader: optional family-extension arch fields, family peek for
  auto-detection, arch-registry production sniff
- Model: family-aware tensor accessors (router, shared expert, scalar gate,
  linear_attn.*), untied lm_head, auto-detecting load overload
- KVCacheManager: LayerKind.linear (no per-token KV); GDNStateManager for
  fp32 delta state + conv tails
- Sampler/logit path: finalLogitSoftcap plumbed through, softcap <= 0 disables
- New gdn.metal module: causal depthwise conv + SiLU (decode/prefill/tail),
  per-head q/k norm with folded delta scales, gated delta rule recurrence
  (decode + chunked prefill), gated output norm; validated against a CPU
  reference incl. prefill == sequential decode equivalence
- rope_neox_subdim (decode + prefill): Qwen partial-rotary convention
- silu activation function constants for routed MoE, prefill MoE, INT8 shared
  expert; silu_mul_fp16; sigmoid gate/scalar and residual-add elementwise
  kernels; MoE/SharedExpert/PrefillGroupedRoutedMoE wrappers parameterized
…source)

- SupportedModelSource: selectable gemma4/qwen36 pinned sources with
  per-source modelID, download/installed byte estimates
- SourceFingerprint: qwen index sha -> qwen3.6-35b-a3b-4bit
- CLI: --model {gemma4|qwen36} selector (default gemma4)
- ArchInfo: qwen3_5_moe parse path (layer mask 2=linear/1=full, GDN dims,
  attn output gate, silu, untied head) + production baseline cross-check
- RepackPlanner: per-family tensor classification (.mlp.switch_mlp routed
  experts) and resident ordering (embed first, lm_head last)
- GTurboJSON/RemoteStreamingRepacker: qwen36 family arch fields in
  manifest.json (gemma output unchanged), bit-width sniffing for qwen names
- SyntheticSnapshot.buildQwen + planner/CLI/end-to-end install tests
…ecoder support

- Rename GemmaToolCallParserError to neutral ToolCallParserError with a
  back-compat typealias
- Resolve ChatDialect (gemma/chatml) at GFTokenizer init by sniffing the
  <|im_end|> special token; chatml requires the nine ChatML specials by
  string, gemma keeps its ten-token contract
- ChatML: no BOS (encode addBOS is a no-op), stop tokens {im_end,
  endoftext}, padded logits vocab 248320, hand-rolled ChatML chat template
  with enable_thinking=false generation prompt, text continuation bridge
- encodeToolResultContinuation throws unsupportedForDialect for chatml so
  the server prompt cache falls back to prefix matching
- New QwenToolCallParser for the <function=/<parameter= XML-ish body with
  JSON/string value inference, fail-closed tool allow-list, 256 KiB guard
- StructuredAssistantDecoder chatml mode: <think> suppression and
  <tool_call> buffering via special-token IDs
- Synthetic ChatML tokenizer fixture + template/parser/decoder test suites
Gemma's tokenizer maps unknown strings to <unk> instead of nil, so probing
for <|im_end|> misclassified Gemma as ChatML. Verify resolution by
round-tripping the ID back to the token string.
…t + model-id)

- AppModelInstallDescriptor.qwen36 (pinned repo/revision/index sha, byte
  estimates from the range plan) + TURBO_FIELDFARE_MODEL=qwen36 selection;
  install dirs gemma4.gturbo / qwen36.gturbo; badge shows descriptor name
- AppModelInstallationProbe validates against the manifest's own family
- Server: ChatDialect threaded into request validation (Gemma DSL key/args
  checks relaxed for ChatML), --model-id default derived from the loaded
  model family, backend exposes chatDialect/defaultModelID
- CLI usage banner covers both models
…refill

Decode: three-way per-layer dispatch (gated-DeltaNet linear / gated full
attention with NeoX sub-dim RoPE / existing Gemma path), plain pre-norm
post-attention block with single moeX norm, sigmoid-gated shared expert,
phase-2 residual fusing the shared branch, residual-add tail, untied lm_head.
Prefill: chunked GDN path (conv + tail carry + delta recurrence), packed
q_proj split, prefill_rope_neox_subdim_block Swift encode, no V norm, gate
multiply, ones router scale buffers, residual-add tail; linear layers skip
KV staging/blit/attention. Init: family-neutral kernel construction (SiLU
MoE/shared/grouped, specialized dims from cfg), GDN + state manager +
Elementwise + scalar-gate GEMV gated on data flags, ones effective/per-expert
scale buffers when routerScaled is false, embed outScale and attention scale
from config. Gemma path byte-identical; all 134 existing tests pass.
QwenToySynthetic writes a tiny runnable .gturbo with the qwen36 tensor
contract (linear_attn.* GDN tensors, packed [query;gate] q_proj, mlp.gate
router, gated shared expert, untied lm_head, arch-v2 manifest, no sandwich
tensors). Tests: runner init without touching sandwich tensors, hybrid
linear/full decode smoke with reset determinism, chunked prefill smoke +
decode continuation, prefill-vs-decode argmax consistency (KV + GDN state
carry), KV/GDN-state reset interplay, prefill scratch layout sizing for
qwen vs unchanged gemma.
…phase counters

Specialization measured on M5: unspecialized 4096x2048 INT4 GEMV runs at
101.6 GB/s, the constant-folded variant at 141.0 GB/s. Shapes now come from
ArchConfig rather than a hardcoded Gemma list, and the LM head specializes on
the loaded model's D/vocab instead of 2816/262144.

Also adds --expert-cache-slots, --rdadvise, and TURBO_FIELDFARE_PHASES=1
decode phase reporting so the I/O-vs-GPU split is measurable.
The probe now derives the expected checkpoint from the family the manifest
declares when the caller does not pin a descriptor, so a Gemma install is
still recognized while Qwen is the selected model (and vice versa). The
post-install verification still pins to the descriptor that was downloaded.

Also covers the two new CLI options in the help-options test.
Decode issued four separate INT4 GEMVs per gated-DeltaNet layer, all
reading the same hidden vector: in_proj_qkv (8192 rows), in_proj_z
(4096), in_proj_a (32) and in_proj_b (32). The last two were four
threadgroups apiece — near-empty launches.

gdn_in_proj_gemv_simd dispatches over the concatenated 12352-row space
(1544 threadgroups at 8 rows/TG) and routes each row to its own
weight/scale/bias base and output buffer with a 4-way compare, reusing
dequant_int4_gemv_simd_body verbatim. The per-row math and operand order
are unchanged, so the result is bit-identical to the four dispatches;
greedy decode output on scratch/qwen36.gturbo is byte-for-byte the same.
The body's ushort-pair weight loads are kept: packed sub-tensor offsets
are only 2-byte aligned.

Qwen-only (the GDN path); Gemma 4 is untouched. No new buffers.

Measured (M5, 30 layers = 1 decode token, median GPU time):
  separate x4  3.66 ms  ->  fused x1  3.42 ms   (-6.6%)
That stage is ~3.4 ms of a ~59 ms/token decode, so end-to-end tok/s does
not move outside run-to-run noise (13.8-18.2 tok/s on an idle baseline).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CONTRIBUTING requires public runtime controls to appear in
docs/RUNTIME_CONTROLS.md; --expert-cache-slots and --rdadvise now do, along
with the TURBO_FIELDFARE_PHASES diagnostic. SYSTEM_DESIGN's scope section
described a Gemma-only runtime while the branch also ships Qwen 3.6, and now
states what that support covers and what it has not been validated against.
Pinning 16 GB of a 24 GB host to leave an ~8 GB working set changes neither
throughput (19.7 vs 19.8 tok/s) nor footprint (1.52 vs 1.49 GB peak). The
18.1 GB expert pool never fit the page cache on either configuration, so
decode already streams from SSD and constraining memory does not change what
it reads.

Every resident component is equal to or smaller than the Gemma path's:
expert slots 1.13 GB vs 1.61 GB, KV at 4K 84 MB vs 320 MB. Only the on-disk
install is larger (19.6 GB vs 14.3 GB). Replaces the earlier claim that the
8 GB configuration was unvalidated.
Ran the three frozen real-generation-v1 prompts with their fixed seeds, app
sampling defaults, 4K context and 16 slots — one discarded warmup then one
measured run per case in a fresh process. Every measured footer reported
stop=endOfTurn.

  short-explanation   62/493    23.05 tok/s   1,447 MiB footprint
  medium-review      426/697    21.20 tok/s   1,448 MiB
  long-synthesis   2,940/700    18.84 tok/s   1,464 MiB

Rerunning the short case with 16 GB pinned elsewhere (~8 GB working set) gave
23.36 tok/s at 1,448 MiB with byte-identical output.

The CLI footer now also reports prefill seconds, which the protocol's report
format asks for.
Qwen 3.6 is presented alongside Gemma 4 rather than as an experiment: the
README at-a-glance covers both models' memory, storage and measured decode,
and SYSTEM_DESIGN's scope section describes two explicitly enumerated
architectures, each with its own pinned checkpoint, compile-time baseline and
manifest contract.

Completes the 8 GB validation across all three community benchmark cases with
16 GB pinned elsewhere. Every case reported stop=endOfTurn and produced output
byte-identical to its unconstrained counterpart:

  short-explanation  23.05 -> 22.95 tok/s   1,447 -> 1,464 MiB
  medium-review      21.20 -> 21.35 tok/s   1,448 -> 1,448 MiB
  long-synthesis     18.84 -> 18.62 tok/s   1,464 -> 1,388 MiB

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The comparison quoted 41.5 tok/s for Gemma 4 from an app HUD reading rather
than the repository's own published 31-35 tok/s protocol rows, and derived
per-model read bandwidths from an assumed 100% expert cache-miss rate that was
never instrumented.

Replaced with what is measured — Qwen reads fewer expert bytes per token than
Gemma, has half the slot coverage, and its throughput is page-cache
independent (repeated runs do not warm up: 21.4, 22.1, 20.8 tok/s; an ~8 GB
working set does not slow it down) — and labels the page-cache explanation for
the gap as an untested hypothesis, since Gemma was never run under the same
protocol on this host.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@ulises-c ulises-c left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Verdict: Changes Requested — 3 blocking, 5 warnings.
(Posted as a COMMENT review: GitHub does not allow requesting changes on one's own PR.)

Adversarial review — not ready to merge

Reviewed at merged HEAD 40428a5. Rebuilt release (Build complete!), reproduced both cited suites (ServerArgumentTests 11 passed, ServerLogTests 2 passed), independently re-derived the KV memory model from KVCacheManager.swift, and ran a needle-in-a-haystack retrieval probe.

The engineering is clean and Scope and limitations is honestly written. But the title claims a capability the evidence does not establish, and the headline numbers now predate the code this PR ships.

Blocking

1. The ladder proves admission, not comprehension.
ArchConfig.gemma4_26B_A4B sets slidingWindow: 1024 with fullAttentionLayerMask = layers 5/11/17/23/29. 25 of 30 layers see only 1024 tokens; 5 full-attention layers carry all long-range signal. A "word " * N prompt with max_completion_tokens=1 cannot distinguish a working 256K context from one where those 5 layers degrade into noise — both return HTTP 200 finish=length.

I verified retrieval genuinely works below the ladder with a needle probe:

Context Prompt tokens Depths Result
4K 1,543 0.1 / 0.5 / 0.9 3/3 recalled
16K 14,043 0.1 / 0.5 / 0.9 3/3 recalled
57K–254K untested

14K already clears the window by 13x, so the full-attention path works. The untested range is exactly the range this PR adds.

2. Every timing number predates the merged code.
Rungs were measured 08-24/08-25 from c4f9442/4b3bd87. 40428a5 then merged #159 "Speed up prefill on pre-Apple10 Macs", which rewrote PrefillAttention.swift pipeline selection (new layerKind == .full + windowNeverClips guards) and RealForwardRunner prefill command buffers. The table's pp, pp_tok_s, the 242.983 -> 132.276 decline and the R^2=0.996 fit all describe a different prefill implementation. #159's own message states the prior TensorOps path "produced wrong output" for clipping windows.

3. The 64K baseline is not a matched control.
Run order: 96K/128K/192K (08-24 16:00-16:33) -> 256K (08-25 13:18) -> 64K last (08-25 14:55), spanning at least two source commits. A baseline run after the experiment on a different build is not a baseline.

Warnings

4. --prefill off --max-context 262144 is an unguarded OOM. Derived from KVCacheManager.swift:79-101, KV = 25 x 1152 x 4096 x 2 + 5 x ctx x 2048 x 2. This predicts every observed Metal peak to a constant +32 MB across a 4.4x span:

Context Predicted Observed delta
64K 1,505 MB 1,537 MB +32
96K 2,145 MB 2,177 MB +32
128K 2,785 MB 2,817 MB +32
192K 4,065 MB 4,097 MB +32
256K 5,345 MB 5,377 MB +32

256K only fits because the FP16 ring caps SWA layers at 1,152 tokens each. --prefill off disables that ring -> all 30 layers allocate at maxContext -> ~55 GB at 256K on a 36 GB machine, accepted with zero validation.

5. The harness cannot be run by any reviewer. MODEL is hardcoded to a different checkout, PORT is fixed, and benchmark-results/ is gitignored — so none of the cited raw artifacts are in the PR.

6. "Server-side context-capability validation" is overstated. ServerArguments only widens an integer allowlist; nothing validates the model supports 256K (no RoPE-scaling or manifest check).

7. Prompt cache untested at long context. Every rung reports cached=0, yet single-prefix is the default and is the real agent workload.

8. Docs recommend 262144 as the routine client budget — measured cost ~32 min of prefill per request. A routine budget should sit materially below the measured edge.

Good

  • Memory instrumentation is well designed — it is what let me validate the KV model independently.
  • Zero swap and no OOM across five rungs is a real, credible result.
  • completedMessage extracted as a pure function for testability is the right call.
  • Uniform 8,192-token reserve across rungs is a correct control.

To make this mergeable

  1. Run needle retrieval at 64K/128K/256K, depths 0.1/0.5/0.9 — this is the missing evidence.
  2. Re-run the ladder on merged HEAD, all rungs same binary, baseline first.
  3. Guard --prefill off above ~64K, with a test.
  4. Retitle toward what is proven, or hold the title until (1) lands.
  5. Make the harness portable; commit or un-ignore the artifacts.
  6. Walk doc examples back to a conservative budget; fix the CLI validation claim; drop unused os/select imports.

Adversarial review by Hermes Agent. Build/test/memory-model claims above were reproduced locally; the 57K+ retrieval gap is stated as untested rather than assumed.

Comment thread Scripts/context_ladder.py Outdated
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
MODEL = Path("/Users/ulises/github/turbo-fieldfare/scratch/gemma4.gturbo")

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Blocking (harness portability). MODEL is hardcoded to /Users/ulises/github/turbo-fieldfare/scratch/gemma4.gturbo — a different checkout from the one this PR lives in. No reviewer can run this, and it silently couples the harness to one machine's layout.

Combined with benchmark-results/ being gitignored (.gitignore:43), a reviewer gets a script that cannot run and cannot see the data it produced.

Suggest argparse with --model / --port, defaulting to ROOT / "scratch/gemma4.gturbo" and falling back to an env var.

Comment thread Scripts/context_ladder.py
"model": MODEL_ID,
"messages": [{"role": "user", "content": text}],
"temperature": 0,
"max_completion_tokens": 1,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Blocking (this is the core issue). max_completion_tokens=1 combined with the "word " * words prompt at line 134 means a level passes if the server admits and prefills the prompt — never that the model can use it.

Gemma 4 runs slidingWindow: 1024 on 25 of 30 layers; only layers 5/11/17/23/29 are full-attention. So a 254K context where those 5 layers degrade into noise returns HTTP 200 finish=lengthidentical to a healthy run.

I confirmed retrieval works at 1,543 and 14,043 prompt tokens (needle recalled at depths 0.1/0.5/0.9), so the mechanism is sound below the ladder. The untested band is 57K-254K, which is precisely what this PR adds.

Suggest a second pass: plant a fact at fractional depth in the filler and ask for it back with a real multi-token completion.

guard let parsed = Int(value),
[4_096, 8_192, 16_384, 32_768, 65_536].contains(parsed) else {
[4_096, 8_192, 16_384, 32_768, 65_536, 98_304, 131_072,
196_608, 262_144]

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Warning (unguarded OOM introduced by this change). This widens the allowlist to 262144, but --prefill off remains independently settable.

From KVCacheManager.swift:79-101, the ring only caps SWA layers when fp16RingEnabled; with prefill off, all 30 layers allocate at maxContext:

  • ring on @ 256K: ~5.2 GB (matches the observed 5,377 MB peak to +32 MB)
  • ring off @ 256K: ~55 GB — unallocatable on the 36 GB test machine

Before this PR the cap was 65536, which bounded the damage. Now the combination is reachable and accepted with no validation. Suggest rejecting --prefill off above ~64K in validate(), with a test.

duration: .seconds(676),
completion: completion)

#expect(line.contains("pp=675.595s"))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fine as a formatting unit test, but worth noting it asserts values no real run produces: decodeSeconds: 0.250 -> tg_tok_s=4.000, whereas every actual ladder rung logs tg=0.001s with tg_tok_s of 785-1094.

The PR describes this as covering "prefill/decode timing" behavior; it covers string formatting of hypothetical values. The pp_tok_s cached-token subtraction path (ServerLog.swift:32-35) is never exercised with cached > 0 — and every ladder rung reports cached=0, so prompt-cache behavior at long context is untested in both the suite and the benchmark.

Comment thread docs/OPENAI_SERVER.md Outdated
--model scratch/gemma4.gturbo \
--port 8080 \
--max-context 16384
--max-context 262144

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Warning. This flips the primary launch example from 16384 to 262144, and lines 115/153 do the same for the OpenCode and Pi client configs — making 256K the recommended default for every reader.

Measured cost of one such request on the test machine is 1,920s (~32 min) of prefill. Per the project's own methodology, a routine budget should sit materially below the measured edge, not at it.

Suggest keeping the example at a practical value and documenting 262144 as the tested maximum with its latency cost stated inline.

Comment thread docs/RUNTIME_CONTROLS.md Outdated
| --- | --- | --- | --- | --- |
| Maximum response | Automatic | `--max-new` | App: remaining context; CLI: 1,024 tokens | The app can use the context space left after formatting the prompt. The CLI uses its explicit or default `--max-new` limit. |
| Maximum context | 4K, 8K, 16K, 32K, 64K | `--max-context` | CLI and app: 8K; server: 16K | Sets prompt plus response capacity, and 8K is what leaves room for an image and its prompt. The app shows the FP16 KV-memory delta. The server defaults higher still because agent clients routinely send prompts near 8K on their own. |
| Maximum context | App: 4K, 8K, 16K, 32K, 64K; CLI/server: 4K through 256K, including 96K and 192K ladder points | `--max-context` | CLI and app: 8K; server: 16K | Sets prompt plus response capacity, and 8K is what leaves room for an image and its prompt. The app shows the FP16 KV-memory delta. The server defaults higher still because agent clients routinely send prompts near 8K on their own. |

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Inaccurate. This says CLI supports "4K through 256K, including 96K and 192K ladder points", implying enumerated CLI validation.

Sources/TurboFieldfareCLI/Args.swift:241-246 accepts any positive integer for --max-context — there is no allowlist on the CLI at all. Only the server enumerates. As written this documents validation that does not exist, in the same row that describes server behavior which does.

Widening --max-context to the Gemma 4 ladder rungs made an unguarded
out-of-memory combination reachable. Chunked prefill is what enables the
FP16 sliding-window ring in KVCacheManager, and the ring is the only
reason a long context fits: it caps each of the 25 sliding-window layers
at slidingWindow + chunkTokens tokens instead of maxContext.

With --prefill off the ring is disabled and every layer allocates KV at
the full context. Measured against the five recorded ladder rungs, the
ring-backed footprint matches observed peak Metal allocation to a
constant 32 MB offset; without the ring the same 256K cap needs about
55 GiB of KV alone, which no supported machine can satisfy.

Reject the combination during argument resolution, where it produces an
actionable message, rather than at model load. The bound is 65536, the
largest context reachable before the ladder rungs were added, so every
previously valid invocation still resolves.

Adds 13 cases: rejection at each ladder rung, and matched controls
proving both pre-ladder --prefill off and ladder --prefill on still pass.
completedMessage divides by prompt tokens minus cached tokens, but every
context-ladder rung ran against an empty cache and reported cached=0, so
the subtraction was never exercised by either the suite or the benchmark.
Prompt reuse is the server default, which makes a partial cache hit the
common case rather than an edge case.

Adds three cases: a partial hit where the rate must reflect only the
computed tokens, a fully cached prompt that must read as zero rather
than dividing by zero elapsed time, and a cached count exceeding the
prompt, which the existing max(..., 0) clamps but nothing asserted.
The ladder hardcoded a model path pointing at a different checkout, a
fixed port, and a fixed rung list, so no reviewer could run it and a
single rung could not be re-measured without repeating the hours-long
rungs beside it. It now takes --model/--server/--port/--levels/--out,
defaults the model to this checkout's scratch directory, validates the
binary and rung labels up front, records the source commit in every
result, and merges a partial run into the existing aggregate instead of
overwriting it. The 64K baseline joins LEVEL_CONTEXTS so it is produced
by the same protocol as the rungs it is compared against. Drops the
unused os and select imports.

Adds Scripts/context_retrieval.py for the question the ladder cannot
answer. A one-token completion on repeated filler proves admission, not
comprehension: Gemma 4 runs sliding-window attention on 25 of 30 layers,
so long-range signal rests on 5 full-attention layers and a degraded
long context still returns HTTP 200. The new harness plants a
distinctive fact at a fractional depth, asks for it back with a real
multi-token completion, and sweeps depth because sliding-window
degradation is position dependent. It exits non-zero on a miss so a
sweep can be scripted, and its docstring states that repetitive filler
makes recall an upper bound rather than a guarantee.
The launch example and both client configs advertised 262144, making a
256K window the recommended default for every reader. One such request
measured about 32 minutes of prefill on the test machine, and agent
clients size their history to the advertised window, so that setting
commits every request to that cost. The examples move to practical caps
and the reasoning is stated inline.

Corrects three claims. Context prose now records the --prefill on
requirement above 64K and why the sliding-window ring makes it
necessary. It states plainly that the capability check is a
supported-value list rather than a probe of the weights, and that the
ladder measures admission rather than comprehension, pointing at the
retrieval harness for the latter.

The runtime-controls row claimed the CLI validates an enumerated set
including the ladder points. Args.swift accepts any positive integer for
--max-context; only the server enumerates. The row now describes each
binary's real behavior.
The pull request cited raw artifacts under benchmark-results/, which is
gitignored, so none of the data backing the table was actually in the
change. This moves the measurements into a durable experiment note
alongside the conditions needed to read them correctly.

Records both questions separately. The admission ladder is reported with
its recorded run, and the note states plainly that the timing columns
predate the merge of drumih#159, which rewrote prefill attention selection and
the prefill command buffers, so those columns describe a superseded
implementation and are marked for re-measurement. It also notes the
rungs were captured across more than one commit with the baseline last,
so they are not a matched set.

Adds the KV memory model derived from KVCacheManager and shows it
predicts observed peak Metal allocation at all five rungs to a constant
32 MB offset. That is the structural reason the memory columns should
survive re-measurement, and the same formula gives the ~55 GiB figure
behind the new --prefill guard.

Records the retrieval results that do exist, at 1,543 and 14,043 prompt
tokens, and states that nothing is measured between 57K and 254K, which
is the range the 256K cap adds.
@ulises-c ulises-c changed the title feat: extend Gemma 4 server context to native 256K feat: raise the Gemma 4 server context ceiling to 256K, with measured admission limits Aug 26, 2026
@ulises-c

Copy link
Copy Markdown
Author

Review items addressed

Pushed five commits (80c1c82..4dd7dc1) responding to the adversarial review above. Summary of what changed and what deliberately did not.

Fixed

# Item Resolution
4 --prefill off unguarded OOM 80c1c82 — rejected above 64K during argument resolution, with the KV requirement named in the message. 13 test cases incl. matched controls.
5 Harness unrunnable a1acfc5--model/--server/--port/--levels/--out, defaults to this checkout, validates binary and rung labels, records source commit, merges single rungs into the aggregate. Unused os/select dropped.
6 "Capability validation" overstated f025a02 — docs now state plainly it is a supported-value list, not a probe of the weights.
7 Prompt cache untested a74d421 — three cases covering partial hit, fully cached, and cached-exceeds-prompt clamp.
8 Doc budget recommendation f025a02 — launch example back to 16K, client examples to 32K, with the ~32-minute prefill cost stated inline.
10 CLI validation claim f025a02 — row now describes each binary's real behavior; the CLI accepts any positive integer.
Artifacts absent from PR 4dd7dc1 — measurements moved into docs/experiments/summaries/10-long-context.md, since benchmark-results/ is gitignored.

Partially addressed

#1 (admission vs comprehension)Scripts/context_retrieval.py is committed, validated, and documented. It is not yet run across 57K–254K: the model directory became unavailable mid-session and I did not reinstall it. Now blocking question 2 in the description, with the untested range stated explicitly rather than implied.

#2 / #3 (stale timings, unmatched baseline) — cannot be fixed by editing; they need a re-run on one post-merge binary. Now blocking question 1, and the table and experiment note both carry a pre-#159 marker. The 64K baseline joined LEVEL_CONTEXTS so a matched re-run is one command.

Note on the KV model

The --prefill off guard derives its threshold from ArchConfig at runtime rather than a hardcoded constant. The predicted figure it reports (55 GiB at 256K) is the same formula that matches observed peak Metal allocation at all five rungs to a constant 32 MB — so the guard and the memory table are backed by the same verified model.

Verification

Full Scripts/test.sh: 1,262 tests in 195 suites passed (152s). The 149 tokenizer .authorizationRequired failures previously reported did not reproduce — that was environmental. swift build -c release clean; git diff --check clean.


The title has been changed from "extend Gemma 4 server context to native 256K" to one that claims the measured result rather than the aspirational one.

chore: sync fork-main with upstream through drumih#159
Merges upstream PR drumih#29 (NeelM0906, "Add Qwen 3.6 35B-A3B as a second
supported architecture") onto fork-main synced through upstream drumih#159.

Stage 1 of the MoE generalization: correctness only, no optimization work.
Perf cherry-picks land separately in Stage 2, one lever at a time.

PR drumih#29 branched from a stale base (add22ff / drumih#49) and predates several
upstream changes that touch the exact seams it rewires:

  fd3c0b8  optional Gemma 4 image support (drumih#144)
  ec8ad21  prefill speedup on pre-Apple10 Macs (drumih#159)
  417f389  CLI runtime controls / RuntimeConfiguration (drumih#100)
  acefaf1  incremental lossless detokenization (drumih#118)
  d56c808  typed Codable manifest contract (drumih#87)

Both sides generalize the same seams in different directions: upstream adds
a MODALITY (text|image), drumih#29 adds a FAMILY (gemma4|qwen36). These are treated
as two independent axes rather than one flat enum. 20 files conflicted
(~1,090 lines); the rule applied throughout is that upstream wins on
pre-existing machinery, while drumih#29's genuinely new work is preserved.

Resolutions a mechanical -X ours/-X theirs would have silently broken:

* RealForwardRunner prefill. drumih#29 wraps attention in a linear/full family
  dispatch; upstream added the FP16-KV guard and bidirectional-block params
  inside that same region. drumih#29's call had dropped layerKind:,
  bidirectionalBlockStart/End: and fp16KV: — a Gemma image regression.
  Kept drumih#29's outer dispatch with upstream's guards restored in the else branch.

* Logit softcap. RawCompletionScratch defaults logitSoftcap to Gemma's 30.0.
  Now threaded from model.config.finalLogitSoftcap, so Qwen (which disables
  softcap with 0) is not silently softcapped into wrong logits.

* Manifest wire contract. Extended upstream's typed Codable struct with 13
  optional family fields instead of taking drumih#29's hand-rolled dictionary.
  JSONEncoder omits nil optionals and .sortedKeys fixes ordering, so Gemma
  manifests still encode byte-identically. The per-field emit is gated by one
  writesFamilyExtensions flag, so adding a third family is a one-line change.

* Gemma-specific tool encodings. GemmaToolSchema.adapted() and
  gemmaToolArgumentBody() are Gemma prompt encodings and are now skipped for
  ChatML, which emits <tool_call> JSON directly.

* ByteLevel detokenization. Upstream drumih#118 replaced drumih#29's library-delegating
  detokenizer with a Gemma-pinned one, leaving ChatML's ByteLevel
  (GPT-2 byte<->unicode) decoding with no path. Added ByteLevelDecoding.swift;
  neither side carried this alone.

Fail-closed image path, per AGENTS.md:

* visionCompanion(for: .qwen36) returns nil rather than falling back to
  Gemma's pack, which would pair one family's vision tower with another
  family's text model.
* TurboFieldfareRepack now rejects --model combined with vision install flags
  (previously silently ignored) and pins the image pack to
  SupportedModelSource.gemma4 explicitly.

Runtime/server-side rejection of image input for Qwen is not yet implemented
and lands in a follow-up commit.

Docs state the two-family reality rather than either side's stale claim:
drumih#29's "TurboFieldfare omits both vision towers" is false now that Gemma image
support shipped. Qwen's published figures are labeled as upstream's, measured
on an M5 24 GB host at 4K, not reproduced on this fork.

Verified on this checkout:

  swift build -c release   green
  Scripts/test.sh          1309 tests in 201 suites passed, exit 0

Gemma 4 byte-identical output non-regression at fixed seeds is NOT yet run;
it gates the Stage 1 PR, not this commit.
The image tower is Gemma 4's. `VisionRuntime.open` is the only door into
it, and it loads the *text* model's manifest with
`expecting: .gemma4_26B_A4B`, so `ManifestReader.validateArch` refuses a
Qwen 3.6 text model on the `family` field before any pack is opened.

That invariant held already — this commit adds no production change, only
the tests that keep it holding. Investigating it traced every image entry
point and found no bypass:

  - streaming parser gates on `visionCapability == "ready"` before staging
  - `multimodalMessages` is only produced when `imageFiles` is non-empty
  - a forged `turbofieldfare-attachment:` token without a matching lease
    is rejected as `invalid_image`
  - `imageSoftTokenCounts`, `renderMultimodal`, and `generate` each throw
    `vision_unavailable` when `visionRuntime == nil`
  - all three `VisionRuntime.open` call sites (server, CLI, app) funnel
    through the same family check

No path accepts an image and then answers as though none was sent, which
is the failure AGENTS.md names explicitly.

The cases assert `ModelError.archMismatch` on `family` specifically, not
merely that something threw. A bare "it threw" assertion would keep
passing if the family check were dropped and the throw came from an
incidental shape mismatch instead, and would also keep passing if a
future Qwen model happened to share Gemma's dimensions.

Verified non-vacuous by mutation: deleting the `try check("family", ...)`
line makes the guard tests fail (one falls through to
`VisionPackError.packNotFound`); the line was then restored byte-for-byte
and `git diff Sources/` is empty.

The fixtures write `manifest.json` only, into a temporary `.gturbo`
directory — no weights, no vision pack, no network — so they run on a
checkout with no model installed. The two `VisionRuntime.open` cases are
gated on device support per the existing convention.

Suite: 1309 -> 1318 tests in 202 suites, exit 0.
Stage 1, correctness only. Gate A: 1318 tests / 202 suites, exit 0. Gate B: Gemma 4 output byte-identical vs fork-main at fixed seeds across all three real-generation-v1 cases (7b95132718b7f376 / 6d0c753cb242b15a / 9e924af0585350df).
The Gemma 4 KV formula was inlined at three sites: the server's
--prefill off guard, the Mac app's context menu, and the long-context
experiment notes. Each computed a Gemma-shaped number for any model,
which is wrong for Qwen 3.6 -- its 30 gated-DeltaNet layers hold a
fixed recurrent state instead of per-token K/V rows, so the estimate
overstated its unringed 256K cost by 11x (55 GiB predicted vs 5 GiB
actual).

Replace all three with ArchConfig.kvFootprint, which mirrors
KVCacheManager.init exactly and is cross-checked against the real
allocator by test at 4K/16K/64K for both families.

The --prefill off bound becomes a 16 GiB KV budget rather than a 65536
context constant. The budget reproduces the previous Gemma behavior
exactly (64K = 13.75 GiB allowed, 96K = 20.62 GiB rejected) while
letting Qwen reach every ladder rung, which it can serve unringed.
The server reads the family from the manifest before resolving, and
falls back to Gemma -- the stricter bound -- when it cannot.

The app's context menu gains the 96K/128K/192K/256K options and
computes its labels instead of carrying hand-rounded literals.

1,339 tests in 203 suites pass.
Scripts/memory_matrix.py projects resident memory from the same KV model
the runtime uses, with the non-KV terms calibrated against the five
measured Gemma rungs. --validate re-checks the projection and fails if
it ever under-predicts.

Two corrections that the obvious formulation gets wrong:

The installed pack is not resident. Routed experts stream through a
fixed slot cache, so Gemma holds 1,322 MiB + 51 MiB at 16 slots, not
13.31 GiB. Assuming otherwise overstates it by ~12 GiB and wrongly
reports that nothing fits in 16 GB.

KV is allocated at the context cap, not the prompt length. Measured on
the M5 Max: --max-context 262144 with a 14-token prompt reached 7,268
MB, within 52 MB of the 253,952-token rung. --max-context is the memory
dial.

Result: every Gemma rung through 256K fits on both a 16 GB M4 and a
36 GB M5 Max. Memory is not the long-context limit; prefill time is.

Qwen's KV column is exact but its total is left blank on purpose --
the pack is not installed here, so its resident split and runtime
overhead are unmeasured.
The --prefill off rejection is a KV budget against the installed
architecture, not a fixed context limit, and Qwen 3.6 is unaffected by
the ring. Also state that KV is allocated at the cap rather than the
prompt length, and link the per-device memory table.
The CLI, the server, and the app each decided independently whether a
--max-context was legal, and they disagreed. The CLI accepted any positive
integer, so --max-context 1000000 was refused by the server and accepted by
the CLI, failing later inside the allocator. The server's parser carried a
fourth hardcoded copy of the ladder and rejected an out-of-range value with
'--max-context is not supported', naming neither the value nor the legal set.

ContextAdmission holds the rule once: the native maximum, the ladder, and the
16 GiB unchunked-KV budget, all derived from the loaded ArchConfig. Every
surface calls it and renders the same rejection in its own vocabulary.

A nil family means 'not yet identified', which admits whatever any supported
model could run. Argument parsing happens before the manifest is read, so
assuming Gemma there would reject Qwen-legal commands before looking at the
model; the strict per-family check runs afterwards in the run path. The CLI
now reads the family from the manifest the way the server already did.

Tests pin the surfaces to each other through their real parsers, including a
matched control at 256K prefill off where Qwen is admissible and Gemma is not.
The app menu test now derives from ContextAdmission.ladder instead of
repeating it, so a rung added to one and not the other fails.
The recorded ladder measured admission but probed recall only at 1,543 and
14,043 tokens, so 57K-254K was unmeasured -- and a degraded long context
returns HTTP 200 exactly like a healthy one. The recorded prefill columns
also predate the merge of drumih#159, which rewrote the path they measure, and were
taken across several commits with the 64K baseline last.

One sweep answers both: every probe runs on a single binary and records
pp_seconds and pp_tokens_per_second alongside the needle result, so the
matched timing set falls out of the recall run instead of costing a second
full prefill of the ladder. Each rung starts and stops its own server, and
results append to JSONL after every probe so an interrupted sweep keeps what
it measured.
Retrieval is measured across the ladder for the first time: 15/15 at depths
0.1/0.5/0.9 from 57,043 to 253,143 tokens, every probe with cached_tokens 0.
Depth 0.5 at 256K puts the needle ~126,000 tokens from either end, outside the
1,024-token window of all 25 sliding-window layers, so it is reachable only
through the 5 full-attention layers -- and it was recovered verbatim. The cap
extends what the model can retrieve, not just what it will accept.

Filler is repetitive by construction, so this is an upper bound on recall and
evidence against catastrophic sliding-window failure, not a guarantee for
arbitrary content. Recorded as such.

The prefill columns are re-measured on one binary, three probes per rung,
replacing numbers taken across several commits before drumih#159. Every rung is
slower, median -6.5%. That is NOT called a regression: the old column is not
an internally matched set, drumih#159 targeted pre-Apple10 Macs while this host is
an M5 Max, and no A/B against the pre-drumih#159 commit has been run. The new table
is a matched baseline for future comparison; the old one is kept as a dated
lab record.

Raw sweep output is committed under docs/experiments/data/ since
benchmark-results/ is gitignored.
GTurboLayoutValidator kept its own hardcoded 16 MiB cap after the verifier and
the runtime both moved to 64 MiB for Qwen 3.6, whose 40 layers x 256 experts
produce a 22,493,846-byte layout.json. Installing Qwen downloaded all 18.8 GiB,
verified every weight file, and then failed at the last step:

  install failed: install state at .../packed_experts/layout.json is corrupt:
  size 22493846 exceeds 16777216-byte cap

The validator now uses VerifiedInstallTool.layoutMaxBytes, the same constant
the verifier applies. Nothing was re-downloaded; --resume finished from the
preserved .partial.

The verifier had cap tests and the validator had none, which is why the drift
went unnoticed. LayoutBoundParityTests now asserts the installer and runtime
bounds are the same number, that a Qwen-sized layout fits both, and that the
layout bound stays above the generic metadata bound so the three cannot be
collapsed into one.
Installed scratch/qwen36.gturbo and measured it at a 256K cap: a 17-token
prompt reaches 6,641 MB phys_footprint against a projected 8,347 MB. The
projection stays conservative, as required, but by 1,706 MB (20%) rather than
Gemma's 0-46 MB -- the borrowed overhead constant and on-demand expert slots
account for the slack. --validate now fails if it ever inverts.

Also confirmed on the real binary that Qwen starts at a 256K cap with
--prefill off, which Gemma is refused at identical arguments, and that Qwen
allocates KV at the cap exactly as Gemma does.

Records the measurement instrument. phys_footprint and RSS differ by 6x on
this workload (6,641 MB vs 1,075 MB) because the Metal heaps backing KV are
not counted in RSS; an RSS reading against these tables shows a large fake
overestimate.
context_sweep.py hardcoded scratch/gemma4.gturbo, so the Qwen ladder could not
run through it. Adds --model and writes per-model JSONL (sweep-<pack>.jsonl)
so a Gemma sweep and a Qwen sweep cannot land in one file and be averaged
across two architectures by a later aggregate.
context_retrieval.py hardcoded --model-id gemma-4-26b-a4b-it. The server does
validate that field, so every Qwen probe returned HTTP 404 model_not_found in
8ms and was recorded as found=false. The sweep summary rendered that as
'RECALL 0/3' at every rung -- a total-retrieval-failure shape for what was
really a wrong-name error, and the kind of result that would have been
alarming to read as a finding.

The id now defaults to whatever the server prints in its ready line, so it
follows the pack. A 404 aborts the run with an explicit naming error instead
of emitting rows that are indistinguishable from genuine recall misses.

Verified both families at 16K: Qwen 1/1 (id qwen3.6-35b-a3b, auto-detected),
Gemma 1/1 (id gemma-4-26b-a4b-it, same value that was previously hardcoded).

Also stops context_sweep.py crashing in its own summary when a rung produced
no usable rows -- it formatted None with ',' and lost the whole table.
Both families were fully wired for install -- descriptor, repo, revision,
sizes, and a qwen36.gturbo directory -- but nothing in the app could select
one. AppModelInstallDescriptor.selected read only TURBO_FIELDFARE_MODEL or a
defaults key, so a GUI user could reach Gemma and nothing else.

Adds a picker in two places: the Model menu, and the install screen itself,
where the decision actually gets made. In the menu alone the choice would be
invisible at the one moment it matters -- before committing to a 14.6 GB or
19.5 GB transfer.

Selecting a family persists the choice, swaps the installer's descriptor, and
moves the model directory to that family's pack, reusing setModelURL for the
teardown (cancel loads and transfers, clear staged images, unload the
runtime). Switching is refused while a transfer is in flight, which would
otherwise strand a partial download owned by a descriptor no longer selected.
The environment variable still wins on read, so a launch with
TURBO_FIELDFARE_MODEL set is not silently overridden by a click.

selectableFamilies sits on the descriptor rather than AppModel so it is
reachable without main-actor isolation.

Backfills the selection path, which had no coverage at all: nothing
referenced TURBO_FIELDFARE_MODEL, .selected, or installDirectoryName. Eight
tests cover the descriptor/family round-trip, per-family install directories,
Gemma-only vision companions, the switch, the no-op re-select, and the
in-flight refusal. 1,358 tests pass.
The picker went into the Model menu and the install screen, and a user with
Gemma already installed saw neither: the menu bar is not where anyone looks
for a model setting, and the install screen only renders when a model is
missing. The reported symptom was exactly that -- no dropdown anywhere, so
only Gemma is reachable.

The inspector is a permanent 320pt pane with a Model section already at the
top of it, next to Context and Slots. That is where a model setting belongs,
so the picker goes there, styled to match the pickers beside it. The menu and
install-screen entries stay; this adds the one placement that is visible
during normal use.
The .railguard/ session-state files are Hermes agent tooling, not project
content; they were committed by accident. Untrack them and add .railguard/
and .hermes/ to .gitignore so they stay out.
context_session.py grows context in --step-tokens increments to a target,
replaying each turn's answer so the server KV cache keys on it -- this
exposes the real per-turn prefill cost (new tokens only) that the cold
ladder hides. Needle-in-haystack recall is checked at --checkpoints.

The final step is clamped in token-space against --max-context (approx_depth
is real tokens but step is a filler WORD count; filler_block + chat template
emit more tokens than words) so the last turn can't overshoot the cap and
draw an HTTP 400, as it did at the 256K rung on the first run.

ladder_table.py renders the cold-ladder results.json as a markdown table.
context_sweep.py takes the model id from the server, not a Gemma constant.
Growing context in ~16K steps with a warm KV cache reprefills only the new
tokens, but the per-turn cost of that constant delta scales with session
depth (13.6 -> 106.4 ms/new-token from 33K to 246K), so reaching 246K in
steps cost 4.03 h -- essentially the cold 256K prefill. Incremental context
amortizes the O(n^2) work into per-turn responses; it does not reduce it.

Needle recall HIT at 64K/96K/128K/192K; memory flat from turn 1 because the
full max-context KV ring is pre-allocated. Notes the missing 256K agentic
checkpoint (a now-fixed harness clamp off-by-one) and why it is not re-run.
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