Skip to content

Aligned weight loading and ring KV cache for sliding-window layers (1.2.1) - #7

Merged
jkyberneees merged 3 commits into
mainfrom
fix/aligned-weights
Nov 1, 2026
Merged

jkyberneees merged 3 commits into
mainfrom
fix/aligned-weights

Conversation

@jkyberneees

@jkyberneees jkyberneees commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes "GPU error: Insufficient Memory" when running Gemma 4 12B (8.96 GB) in the app on a 16 GB Mac, and makes long contexts cheap on every Gemma.

  • One copy of the weights. MLX-written safetensors are never 16-byte aligned (8-byte header padding; Gemma 4's 2-byte layer_scalar tensors shift everything after them), so SafeTensorFile copied every tensor into its own buffer while the mapped file stayed referenced by the command buffer — the GPU working set held the weights twice. Shards are now copied once, in file order, into a page-aligned arena and the mapping is released. Load times: Gemma 4 12B 156 s → 25 s from disk / 11–16 s from the page cache; Mistral 7B 3.3 → 2.9 s.
  • Ring KV cache for sliding-window layers (Gemma 2/3/4): window + one batch of rows instead of the whole context. The 12B at 8K context needs 0.5 GB instead of 2.7 GB and runs at 32K on 16 GB. The model tracks its KV write high-water mark; prefix reuse is refused when writing went on for more than a batch past the divergence point (including a cancelled prefill or rejected drafts), and the prompt is processed from the start.
  • GPU out-of-memory is reported as such (by MTLCommandBufferError code) with advice to lower the context length; the load-time budget uses Transformer.scratchBytes(config:), which a test keeps above what init allocates.
  • README and landing page: the "no copy of your weights" / zero-copy wording, load times, the sliding-window limitation, Gemma 3 1B / 4B rows in the performance tables.

Reviewed with /code-review (6 findings, all addressed in the second commit).

Test plan

  • swift test: 135 tests — decode and split decode over a ring cache, a 1,200-token Gemma 3 1B prompt prefilled vs decoded incrementally with the ring active, the prefix-reuse and write-tracking rules, the scratch estimate on every installed checkpoint
  • Gemma 4 12B runs at 8K (18–21 tok/s) and 32K context on a 16 GB M2 Pro; tested in the app
  • Greedy output token-identical on the five existing architectures
  • Prefill and decode A/B, 3 alternating pairs: Mistral 7B within 0.5%, Gemma 3 1B within 1% of the 1.2.0 build; prefill unchanged on Qwen3/Mistral/Gemma

🤖 Generated with Claude Code

…w layers (1.2.1)

MLX writes safetensors without the 16-byte alignment the kernels' vector
loads need, so every tensor was copied into its own Metal buffer while the
mapped file stayed referenced by the command buffer: the GPU held two copies
of the weights, and a 9 GB Gemma 4 12B ran out of GPU memory on a 16 GB Mac.
Shards are now copied once, in file order, into a page-aligned buffer and
the mapping is released. Loading is faster (12B: 156 s -> 25 s from disk,
11 s from the page cache).

Sliding-window layers keep a ring of window + 512 K/V rows (position p at
row p mod rows) instead of the whole context: the 12B at 8K context needs
0.5 GB of cache instead of 2.7 GB and runs at 32K. Prefix reuse is refused
when the previous run went on for more than a batch past the divergence
point, since those rows are gone.

A GPU out-of-memory failure is reported as such, with advice; the load-time
budget includes the scratch buffers. README and the landing page: load-time
and no-copy wording, Gemma rows in the performance tables.

Existing models: greedy output token-identical; Qwen3 0.6B and Mistral 7B
decode within 1% (3 alternating pairs); Gemma 3 1B and Gemma 4 E2B unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Failed ❌

View logs ↗
2cf0c82 2026-11-01T18:09:52.432Z View logs ↗
  • Build: Failed ❌

View logs ↗
b6507d2 2026-11-01T18:02:53.815Z View logs ↗
  • Build: Failed ❌

View logs ↗
f428070 2026-11-01T17:17:20.227Z View logs ↗

jkyberneees and others added 2 commits November 1, 2026 19:01
The model tracks its own KV write high-water mark, so prefix reuse accounts
for writes the engine's bookkeeping dropped (a cancelled prefill, rejected
speculative drafts, a failed in-flight step); the engine resets it when a
run starts from position 0. The ring size is derived from the one maxBatch
constant and checked at load. GPU out-of-memory is recognised by the
MTLCommandBufferError code, with the text match as fallback. The load-time
scratch estimate is Transformer.scratchBytes(config:), and a test checks it
covers what init allocates on every installed checkpoint. flash_prefill
computes the ring slot per row.

Prefill and decode A/B (3 alternating pairs): Mistral 7B and Gemma 3 1B
within 1% of the previous build; prefill unchanged everywhere.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A single arena for all of a shard's tensors measured 6% slower at 1K
context on Qwen3 0.6B (3 alternating pairs, repeatedly); separate
page-aligned buffers, the layout the engine always had, are within 1%.
The mapping is still released after the copy, so the GPU holds one copy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@jkyberneees
jkyberneees merged commit f94f0e1 into main Nov 1, 2026
4 of 5 checks passed
@jkyberneees
jkyberneees deleted the fix/aligned-weights branch November 1, 2026 18:25
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