HiramKHackenbacker commited on
Commit
0d7b6cb
Β·
verified Β·
1 Parent(s): 15c2afa

model card: standard structure, measured facts, verified license declarations

Browse files
Files changed (1) hide show
  1. README.md +286 -346
README.md CHANGED
@@ -1,7 +1,8 @@
1
  ---
2
- license: gemma
3
- license_link: https://ai.google.dev/gemma/terms
4
  base_model: google/gemma-4-E2B-it-qat-q4_0-unquantized
 
5
  library_name: coreai
6
  pipeline_tag: text-generation
7
  tags:
@@ -9,6 +10,7 @@ tags:
9
  - aimodel
10
  - apple-silicon
11
  - on-device
 
12
  - quantized
13
  - int4
14
  - qat
@@ -17,70 +19,46 @@ tags:
17
 
18
  # Gemma 4 E2B β€” Core AI (.aimodel)
19
 
20
- `google/gemma-4-E2B-it-qat-q4_0-unquantized` converted to Core AI `.aimodel` bundles for
21
- Apple silicon by [visible-cx](https://huggingface.co/visible-cx). These are derivative
22
- artifacts: Google's QAT-trained weights rounded onto the int4 grid they were trained for
23
- and re-expressed as a Core AI graph. They load through Core AI on macOS and are not usable
24
- by PyTorch, GGUF or MLX.
25
 
26
- Gemma 4 E2B uses **Per-Layer Embeddings**, so these bundles take a large embedding gather
27
- table as a *static graph input* rather than carrying it in the graph. That table ships in
28
  `ios-frontend/` and **the bundles do not load without it**; a missing table produces a bare
29
- input-arity error naming `ple_table`/`ple_scale`.
30
-
31
- > ⚠️ **Known issue β€” memory growth on long generations.** The Core AI runtime caches a
32
- > graph specialization per input-shape signature, and this export makes the sequence
33
- > length load-bearing: every generated token adds one token to `position_ids` and thus
34
- > mints a new signature, retaining roughly **80 MB of GPU allocations per generated
35
- > token** (both engines, guided and unguided alike, at equal context). Workloads whose
36
- > prompt/output lengths repeat β€” e.g. short constrained extraction rows β€” stay bounded,
37
- > because previously seen lengths replay from cache at no cost. Long free-form
38
- > generations grow without bound until the process exits; no in-process mitigation
39
- > exists (token caps only postpone it, and shape-length reuse is the only stable state).
40
- > A re-export onto a shape-stable contract that removes this growth is available in this
41
- > repository under `stable/` β€” see its section below. It requires updated host support
42
- > (in progress); on current hosts, use the bundles above for guided generation only.
43
-
44
- ## Provenance
45
-
46
- | | |
47
- |---|---|
48
- | Base checkpoint | `google/gemma-4-E2B-it-qat-q4_0-unquantized` (ungated) |
49
- | Zoo recipe | `gemma-4-e2b`, `status = "verified"` β€” `int4lin --tbl` |
50
- | Recipe (pf64) | `export_gemma4_pf_pipelined.py --pf 64` with `--tbl` and `--raw-dir` pointed at the gather table below |
51
- | Toolchain base | `apple/coreai-models` @ `b1cb71b8522d99408059fa0b98b8742171bcb0b8` + the [coreai-model-zoo](https://github.com/john-rocky/coreai-model-zoo) python overlay |
52
- | Toolchain | `coreai-torch 0.4.1`, `coreai-core 1.0.0b2`, `coreai-opt 0.2.1`, `torch 2.9.0` |
53
- | Producer fingerprint | `coreai-core 1.0.0b2` on every inner asset `metadata.json` |
54
- | Weight format | **int4, per-block-32** (`int4lin`, symmetric-with-clipping) β€” the ggml q4_0 grid the QAT checkpoint was trained on |
55
- | Vocab | 262,144 |
56
- | Export functions | `main` (S=1 decode) and, in `_pf64` bundles, `prefill` (S=64 chunked prefill) |
57
-
58
- "QAT-unquantized" means QAT-*trained*, stored at full width; the int4 rounding happens at
59
- export, onto the grid training already targeted.
60
-
61
- `_tbl` = the PLE gather table is bound as a static graph input. `_pf64` = a second
62
- entrypoint, `prefill`, with a static query width of 64
63
- (`function_map: {"main": ["main", "prefill"]}`).
64
 
65
  ## Contents
66
 
67
- ### Bundles
68
 
69
- | Path | Bytes | Context | Functions |
70
- |---|---:|---|---|
71
- | `gpu-pipelined/gemma4_e2b_qat_decode_int4lin_tbl_pf64` | 2,122,089,973 | 4096 | main + prefill |
72
- | `w4a8/gemma4_e2b_qat_decode_int4lin_a8_tbl_pf64` | 2,122,679,604 | 16384 | main + prefill |
73
 
74
  Each bundle folder holds `<name>.aimodel/` (`main.mlirb` β‰ˆ 2.09 GB, `main.hash`, asset
75
  `metadata.json`), a bundle-level `metadata.json`, and `tokenizer/` (`tokenizer.json`
76
  32,169,626 B, `tokenizer_config.json`, `generation_config.json`, `chat_template.jinja`
77
  18,569 B). The `w4a8` folder additionally ships its `calibration_corpus.jsonl` (35,045 B).
78
 
79
- **Stop token:** every bundle declares `eos_token = "<turn|>"` (id 106), which is the
80
- turn terminator Gemma 4 emits. `generation_config.json` independently lists
81
- `eos_token_id: [1, 106, 50]`. A host that stops on the raw upstream `<eos>` instead will
82
- overrun every reply.
83
-
84
  ### The PLE gather-table sidecar β€” required, not optional
85
 
86
  | Path | Files | Bytes |
@@ -99,58 +77,138 @@ overrun every reply.
99
 
100
  `meta.json` records the shape and the dequant convention:
101
  `V 262144, D 1536, PLD 8960, L 35, ld 256, embed_scale_pl 16.0` (= √256, which is what the
102
- exporter assumes).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
- Every `_tbl` bundle binds `ple_table` (from `embed_per_layer.i8`, 2.35 GB) and `ple_scale`
105
- (from `embed_per_layer.scale.f32`) as **static** graph inputs.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
  ## Requirements
108
 
109
  - **Apple silicon Mac**, Core AI runtime.
110
  - **Engine contract: 4 inputs** β€” `input_ids`, `position_ids`, plus static `ple_table` and
111
- `ple_scale`. Two engines accept that:
112
- - **Pipelined engine** β€” binds the statics zero-copy over the caller's buffer, but does
113
- not support logits, so no grammar-constrained decoding.
 
114
  - **Sequential engine** β€” the only logits-capable engine, and therefore the only path for
115
  guided decoding. It accepts `>= 2` inputs and binds everything beyond
116
- `input_ids`/`position_ids` from `EngineOptions.staticInputBuffers`; an extra input with
117
- no buffer is rejected by name. It pays a **one-time copy of every constant input at
118
- load β€” 2.19 GB for E2B**, because its submission path otherwise materialises a foreign
119
- buffer-backed view on every forward pass. Binding the table as a raw view instead costs
120
- ~0.30 s per pass (3.4 tok/s); the load-time copy takes the same bundle to 8.6 s/row.
121
- - **States:** `keyCache` / `valueCache` `Float16, 15 Γ— 1 Γ— 1 Γ— ? Γ— 512`. Dynamic sequence
122
- dim β†’ `GrowingKVCache` (initial 256, doubling), not a static allocation at the manifest
123
- maximum.
124
- - **KV cost: 30,720 bytes per token** (fp16) β€” 126 MB at 4096, 252 MB at 8192, 503 MB at
125
- 16384.
126
- - **Memory envelope, 16 GB Mac** (Metal `recommendedMaxWorkingSetSize` β‰ˆ 10.7 GB):
127
-
128
- | | |
129
- |---|---|
130
- | Weights (`main.mlirb`) | 2.09 GB |
131
- | PLE table bound as static input | 2.35 GB |
132
- | Base | **4.44 GB** |
133
- | Affordable context, fp16 KV | ~204k tokens β†’ **131,072, the model cap, binds first** |
134
-
135
- KV is not the ceiling at this tier; the PLE table is. Measured peak footprint on a guided
136
- run: **8.26 GB**, max RSS 5.40 GB. **Minimum practical machine memory: 16 GB**, for
137
- guided decoding only. Free-form generation does not fit this envelope on any machine β€”
138
- see the known issue above.
139
- - Sliding-window note: E2B interleaves 12 sliding and 3 full attention layers across 35
140
- layers collapsed to 15 KV slots. The export models the window in the *mask*, not in
141
- memory β€” sliding layers occupy full-length slots and zero-pad head_dim 256β†’512, so 80% of
142
- KV bytes sit in windowed slots and 40% is dead padding.
143
- - The bundle manifest declares `runtime_env COREAI_CHUNK_THRESHOLD=1`. Both engines read
144
- the static query width off the graph.
145
-
146
- ## Performance
147
-
148
- Measured on a **16 GB Apple silicon Mac (M2 Pro, macOS 27 beta)**: 10-sample harness,
149
- guided JSON-constrained decoding against a fixed schema, greedy, sequential engine with the
150
- PLE tables bound as static inputs, `reset()` between samples, 128-token cap. Load excluded
151
- from `s/row`; sample 1 excluded as a cache-warm outlier.
152
-
153
- | | `_tbl_pf64` |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  |---|---|
155
  | Load | 12.1 s |
156
  | Guided JSON parse | **10/10** |
@@ -160,291 +218,173 @@ from `s/row`; sample 1 excluded as a cache-warm outlier.
160
  | Decode | **22.7–32.7 tok/s** |
161
  | TTFT | **0.59–4.20 s** |
162
  | Peak footprint | 8.26 GB |
163
- | Max RSS | 5.40 GB |
164
  | Outcome | completed all ten samples |
165
 
166
- **The S=64 `prefill` function carries the time-to-first-token.** Measured head to head on
167
- a 942-token prompt against a decode-only export of the same weights, exporting `prefill`
168
- moved TTFT 35.74 s β†’ 4.65 s and s/row 45.04 β†’ 8.83, a **7.7Γ—**. Decode is untouched β€” the
169
- prefill function only serves full-width chunks. The decode-only bundle is no longer
170
- published; every bundle in this repo now carries `prefill`.
171
-
172
- **Enum conformance is the grammar's doing.** Unguided, the model emits an off-schema enum
173
- value on essentially every sample. Guided, all ten are correct, because an off-enum token
174
- is unsamplable. Decode throughput is essentially unchanged by the constraint; the ~25%
175
- extra cost of a guided sample is the sequential engine's step-synchronous prefill.
176
-
177
- For reference, upstream measured E2B at 77.0/87.1 tok/s on an M4 Max, and an unguided
178
- pipelined run on the 16 GB machine reached 44.3 tok/s. The 22.7–32.7 tok/s above is the
179
- guided, sequential-engine figure.
180
-
181
- ### Unguided workload β€” memory-capped
182
 
183
- Free-form generation from a fixed prompt. **Every generation length below is a memory cap,
184
- not a model stop** β€” see the known issue.
 
 
185
 
186
- | depth | prompt tokens | TTFT | decode | generated | wall | peak footprint |
187
- |---|---|---|---|---|---|---|
188
- | 3.4k | 3314 | 12.21 s | 7.2 tok/s | 128 (capped) | 30.4 s | **19.97 GB** |
189
- | 8k | 7972 | 54.01 s | 2.1 tok/s | 64 (capped) | 84.3 s | **36.71 GB** |
190
 
191
- Output quality up to the cap is sound; the limit is memory, not capability.
 
192
 
193
- **Needle-in-haystack recall: 3/3 verbatim at 8k**, within a 64-token cap (three distinctive
194
- facts planted at 10/50/90% of the filler, strict scoring). 15k was not attempted: 8k
195
- already cost 36.71 GB of footprint.
196
 
197
- ## Status
198
-
199
- | Artifact | Status |
200
- |---|---|
201
- | `gpu-pipelined/gemma4_e2b_qat_decode_int4lin_tbl_pf64` (ctx 4096) | **QUALIFIED FOR GUIDED GENERATION** β€” measured: 10/10 parse, 10/10 enum-clean, 6.12/4.23 s/row, 22.7–32.7 tok/s, 8.26 GB peak footprint. Not qualified for free-form generation on the current reference engine implementation. |
202
- | `w4a8/gemma4_e2b_qat_decode_int4lin_a8_tbl_pf64` | **EXPERIMENTAL** β€” built, unmeasured. Gate: a Mac-side oracle/parity check plus a device benchmark. |
203
- | `stable/…_tbl_pf64_stable_c16384` | **EXPERIMENTAL** β€” shape-stable decode contract; removes the per-token memory growth by design; host support exists; measured E2B: memory fix confirmed but ~1 tok/s decode at capacity 16384 β€” awaiting a smaller-capacity export before any use. |
204
- | `stable-smoke/…_stable_c16384_l5` | **NOT A MODEL** β€” 5-layer truncated proving asset for host development; produces low-quality text by design. |
205
-
206
- ### About the `w4a8` bundle
207
 
208
- Same QAT int4 per-block-32 weights and the same graph as
209
- `gpu-pipelined/gemma4_e2b_qat_decode_int4lin_tbl_pf64`; the difference is an int8
210
- quantize/dequantize pair on the inputs of every `F.linear`, calibrated on 128 synthetic
211
- samples (the corpus ships in the folder). It was built as a prefill/TTFT lever.
212
 
213
- The caveat comes from the compression toolchain's own documentation: the runtime fast path
214
- is `W_INT8 Γ— A_INT8`, and a float weight path executes in floating point *regardless* of
215
- activation quantization. These weights use int4 scale-multiply dequant, not an int8 LUT, so
216
- the expected TTFT gain may be zero or negative. This bundle exists to be measured, not
217
- because a win is predicted.
218
-
219
- **No numerics gate has been run on any bundle in this repo.** The 10/10 results are
220
- behavioural (parse rate, enum conformance, clean stop); a decode oracle against an fp32
221
- reference has not been run.
222
-
223
- ## License
224
 
225
- The upstream QAT checkpoint carries Apache-2.0 metadata and is ungated, but it is a Gemma
226
- model and **the Gemma terms apply downstream**, which is why this repo declares
227
- `license: gemma`. Use is subject to the
228
- [Gemma Terms of Use](https://ai.google.dev/gemma/terms), the
229
- [Gemma Prohibited Use Policy](https://ai.google.dev/gemma/prohibited_use_policy) and the
230
- [Gemma 4 license](https://ai.google.dev/gemma/docs/gemma_4_license). Those obligations
231
- travel with any redistribution of these bundles, including the gather-table sidecar, which
232
- is derived from the same weights. The contribution here is the conversion, not the weights.
233
 
 
 
 
 
234
 
235
- ## `stable-smoke/` β€” truncated proving asset
 
 
236
 
237
- > ⚠️ **EXPERIMENTAL β€” not a usable model.** A 5-layer truncation of the E2B decoder,
238
- > published only so the shape-stable engine contract below can be developed against a
239
- > small download. It carries real weights for the layers it keeps and produces
240
- > low-quality text; do not evaluate quality from it.
241
 
242
- | Bundle | Bytes | Context | Functions | Producer | Created |
243
- |---|---:|---|---|---|---|
244
- | `stable-smoke/gpu-pipelined/gemma4_e2b_qat_decode_int4lin_tbl_pf64_stable_c16384_l5` | 1,171,287,831 | 16384 | main+prefill | coreai-core 1.0.0b2 | 20260818T003645Z |
245
-
246
-
247
- ## `stable/` β€” shape-stable decode contract
248
-
249
- > ⚠️ **EXPERIMENTAL β€” built, not yet measured on a Mac.** A re-export of the same weights
250
- > and the same quantization onto a decode/prefill contract in which no input shape moves
251
- > between steps, which removes the per-generated-token memory growth described in the
252
- > known issue above. It requires a host that feeds `position_ids` as the **absolute
253
- > positions of the S new tokens only** (host support in progress); a host that feeds the
254
- > growing `0..N` prefix will write the KV cache at the wrong offset. Measured on host support (16 GB M2 Pro): the memory defect is
255
- > confirmed fixed β€” 6.53 GB flat peak across a 664-token free-form generation
256
- > (+0.011 MB/token, versus ~81 MB/token and a killed process on the default bundle) and
257
- > guided output byte-identical to the default bundle β€” but decode runs at ~1.0 tok/s
258
- > versus ~27–31, because every step reads the full 16,384-slot cache. Not usable as
259
- > shipped; a smaller-capacity export would trade window for speed. Treat as a working
260
- > proof of the contract, not a deployable bundle.
261
-
262
- | Bundle | Bytes | Context | Functions | Producer | Created |
263
- |---|---:|---|---|---|---|
264
- | `stable/gpu-pipelined/gemma4_e2b_qat_decode_int4lin_tbl_pf64_stable_c16384` | 2,122,071,656 | 16384 | main+prefill | coreai-core 1.0.0b2 | 20260818T003828Z |
265
-
266
- **Contract.** Both entrypoints take four inputs and two states, all statically shaped:
267
 
 
 
 
268
  ```
269
- main IN input_ids Int32 1x1 | position_ids Int32 1x1 | ple_table Int8 V x (L*ld) | ple_scale Float32 V
270
- prefill IN input_ids Int32 1x64 | position_ids Int32 1x64 | (same statics)
271
- ST keyCache / valueCache Float16 [slots, 1, n_kv, 16384, 512]
272
- OUT logits Float16 1 x S x 262144
273
- ```
274
-
275
- `position_ids` carries the absolute position of each of the S tokens in the call, and
276
- `position_ids[0,0]` is also the cache slot the K/V for those tokens is written at. The
277
- attention mask (causality, unwritten slots, and the 512-token sliding window) is derived
278
- inside the graph from those positions, so there is no mask input to bind. The KV state's
279
- sequence extent is a literal 16,384 rather than a dynamic dimension, so a host that
280
- resolves its cache strategy from the state descriptor will allocate the full cache up
281
- front (0.50 GB for E2B, 1.61 GB for E4B) instead of growing it β€” that fixed cost is the
282
- trade for the unbounded growth it replaces.
283
 
284
- The context ceiling here is **encoded in the graph**: a different window needs a different
285
- export, not a manifest edit.
286
 
287
- **Verification.** Before conversion, the stable graph was checked against the shipped
288
- graph in eager torch on the same weights: greedy argmax identical on all tested decode
289
- steps (including positions past the 512-token sliding window, with 384 cache slots
290
- unwritten-and-masked), worst logits cosine 0.99998 (fp16), and the prefill path
291
- bit-identical in fp32. On-device behavior is unmeasured until host support lands.
292
 
 
 
 
 
 
 
 
293
 
 
 
 
 
294
 
295
- ## `ring-smoke/` β€” truncated proving asset
296
 
297
- > ⚠️ **EXPERIMENTAL β€” not a usable model.** A 5-layer truncation of the E2B decoder,
298
- > published only so the shape-stable engine contract can be developed against a small
299
- > download. It carries real weights for the layers it keeps and produces low-quality
300
- > text; do not evaluate quality from it.
 
 
 
301
 
302
- | Bundle | Bytes | Context | Functions | Producer | Created |
303
- |---|---:|---|---|---|---|
304
- | `ring-smoke/gpu-pipelined/gemma4_e2b_qat_decode_int4lin_tbl_pf64_ring_c16384_l5` | 1,171,301,346 | 16384 | main+prefill | coreai-core 1.0.0b2 | 20260818T085021Z |
305
 
 
 
 
 
 
 
306
 
307
- ## `ring/` β€” shape-stable decode, ring sliding caches
308
 
309
- > ⚠️ **EXPERIMENTAL β€” built and gated in torch, on-device measurements pending.** A
310
- > re-export of the same weights and the same quantization as `stable/`, onto the same
311
- > host contract, with the KV layout reworked. No throughput or footprint numbers have
312
- > been taken on a Mac yet; the structural facts below are read off the converted program,
313
- > not inferred.
 
 
314
 
315
- | Bundle | Bytes | Context | Functions | Producer | Created |
316
- |---|---:|---|---|---|---|
317
- | `ring/gpu-pipelined/gemma4_e2b_qat_decode_int4lin_tbl_pf64_ring_c16384` | 2,122,101,099 | 16384 | main+prefill | coreai-core 1.0.0b2 | 20260818T085141Z |
318
 
319
- **What this is.** `stable/` removed the per-generated-token memory growth by making every
320
- input shape a literal, and it worked β€” but it gave every layer a full 16,384-slot KV cache
321
- and read all of it every step. `ring/` keeps that contract byte-for-byte and changes what
322
- sits behind it:
323
 
324
- * **sliding layers read a ring buffer**, 576 slots (the model's own 512-token sliding
325
- window plus one 64-token prefill chunk) instead of 16,384. Ring slot `r` holds the
326
- newest written position with `pos % 576 == r`, and the mask that selects the written,
327
- causal, in-window slots is derived inside the graph from the absolute positions.
328
- * **the grouped-query head expansion is folded into the query** rather than materialised.
329
- The stock lowering broadcasts an index tensor to the full `[1, heads, K, head_dim]` and
330
- gathers K and V through it; reshaping the query so the head counts already match
331
- produces the same dot products with no index tensor and no gathered copies.
332
- * **both regions are packed into the same two states**, so the host still binds exactly
333
- two KV states, positionally, at whatever literal extent the descriptor declares.
334
 
335
- **Contract β€” unchanged from `stable/`.** Both entrypoints take four inputs and two states,
336
- all statically shaped:
337
 
338
  ```
339
  main IN input_ids Int32 1x1 | position_ids Int32 1x1 | ple_table Int8 V x (L*ld) | ple_scale Float32 V
340
  prefill IN input_ids Int32 1x64 | position_ids Int32 1x64 | (same statics)
341
- ST keyCache / valueCache Float16 [1, 1, n_kv, TOTAL, 512]
342
  OUT logits Float16 1 x S x 262144
343
  ```
344
 
345
- `position_ids` carries the absolute position of each of the S tokens in the call, and
346
- `position_ids[0,0]` is the write position. The mask is derived in the graph, so there is
347
- no mask input to bind. The KV states have literal extents, so a host that resolves its
348
- cache strategy from the state descriptor allocates them up front rather than growing them.
349
 
350
- **Structural facts** (dumped from the converted program):
 
 
 
 
351
 
352
- | | E2B | E4B |
353
  |---|---|---|
354
- | KV state shape | `[1, 1, 1, 56064, 512]` x2 | `[1, 1, 2, 77056, 512]` x2 |
355
- | KV bytes, both states | 114.8 MB | 315.6 MB |
356
- | same for `stable/` | 503.3 MB | 1.61 GB |
357
- | cache slots read per decode step | 56,064 | 77,056 |
358
- | same for `stable/` | 245,760 | 393,216 |
359
- | sliding / full layers | 12 x 576 + 3 x 16,384 | 20 x 576 + 4 x 16,384 |
360
- | dynamic dimensions anywhere | none | none |
361
-
362
- **One new host precondition.** A `prefill` call's first position must be a multiple of 64.
363
- The ring write is a fixed-width store at `p0 % 576`, and 576 is nine 64-token chunks, so a
364
- 64-aligned chunk can never straddle the wrap; an unaligned one would write past the end of
365
- its own ring region. Feeding whole 64-token chunks from a 64-aligned position β€” and the
366
- remainder one token at a time through `main` β€” was already what the contract asked for.
367
- `stable/` tolerated an unaligned chunk; `ring/` does not.
368
-
369
- Unlike the `ctx*/` folders, the context ceiling is **encoded in the graph**: a different
370
- window needs a different export, not a manifest edit.
371
-
372
-
373
- ## `ring2/` β€” the same graph at three KV capacities
374
-
375
- > **EXPERIMENTAL β€” diagnostic assets, not products.** These are the `ring/` recipe, byte
376
- > for byte, exported at a smaller full-attention capacity. They exist to measure how the
377
- > per-step cost of the shape-stable graph depends on that capacity. No throughput numbers
378
- > have been taken on device for either of them. Their context ceilings (2,048 and 8,192)
379
- > are smaller than the 16,384 of `ring/` and are encoded in the graph, so neither is a
380
- > drop-in replacement for anything already published here.
381
-
382
- | Bundle | Context | KV state shape | KV bytes, both states | Sliding / full layers |
383
- |---|---:|---|---:|---|
384
- | `ring2/gpu-pipelined/gemma4_e2b_qat_decode_int4lin_tbl_pf64_ring_c2048` | 2,048 | `[1, 1, 1, 13056, 512]` x2 | 26.7 MB | 12 x 576 + 3 x 2,048 |
385
- | `ring2/gpu-pipelined/gemma4_e2b_qat_decode_int4lin_tbl_pf64_ring_c8192` | 8,192 | `[1, 1, 1, 31488, 512]` x2 | 64.5 MB | 12 x 576 + 3 x 8,192 |
386
- | `ring/gpu-pipelined/..._ring_c16384` (for comparison) | 16,384 | `[1, 1, 1, 56064, 512]` x2 | 114.8 MB | 12 x 576 + 3 x 16,384 |
387
-
388
- **Contract: identical to `ring/`.** Same four inputs in the same order, same two states in
389
- the same positional order, `position_ids` as the absolute positions of the S tokens in the
390
- call, the mask derived in the graph, the same 64-alignment precondition on `prefill`. The
391
- only thing that differs is the literal extent of the full-attention region, and a host that
392
- reads its cache strategy from the state descriptor needs no change to drive them.
393
-
394
- **Equivalence.** Both were gated in eager torch against the shipped graph before conversion,
395
- at their own capacity: 1,600 prompt tokens as 25 chunks of 64 then 32 greedy decode steps,
396
- final position 1,631, so the sliding ring wraps roughly three times. fp32, 57 comparisons
397
- per run, **0 argmax mismatches** and worst cosine 0.999999999987 for both. No dynamic
398
- dimension appears in either entrypoint of either bundle.
399
-
400
- ## `ring2-smoke/` β€” truncated proving asset
401
-
402
- > **EXPERIMENTAL β€” not a usable model.** A 5-layer truncation of the E2B decoder at the
403
- > 2,048 capacity (`[1, 1, 1, 4352, 512]` states, 4 x 576 + 1 x 2,048), published only so
404
- > host-side work can be developed against the contract over a smaller download. It carries
405
- > real weights for the layers it keeps and produces low-quality text; do not evaluate
406
- > quality from it.
407
-
408
-
409
- ## `ring3/` β€” per-layer-type SDPA form
410
-
411
- > **EXPERIMENTAL β€” gated in torch, no on-device numbers yet.** Same weights, same
412
- > quantization, same host contract as `ring/`. One thing changes inside the graph: the
413
- > grouped-query expansion is folded into the query **only in the full-attention layers**,
414
- > and the sliding layers keep the stock gathered form.
415
-
416
- **Why the split.** Folding the GQA expansion into the query removes the gathered copies and
417
- the broadcast index tensor, but it also turns G independent per-head matmuls into a single
418
- batch-1 matmul with G times the rows. That trade is worth taking when the key length is long
419
- (the full-attention layers read the whole capacity) and not worth taking when it is short
420
- (the sliding layers read a fixed 576-slot ring, where the operation is short enough that
421
- losing the per-head parallelism costs more than the bytes it saves). `ring/` applied the fold
422
- everywhere; `ring3/` applies it only where the key length is long.
423
-
424
- Both forms compute the same dot products in the same order. The equality gate below is run
425
- against the shipped graph, not against `ring/`.
426
-
427
- **Contract: unchanged.** Same four inputs in the same order, same two KV states in the same
428
- positional order, `position_ids` carrying the absolute position of each of the S tokens in
429
- the call, the mask derived inside the graph, the same requirement that a `prefill` call start
430
- at a multiple of 64. A host driving `ring/` drives these with no change.
431
-
432
- **Equality.** Gated in eager torch against the shipped graph before conversion: 1,600 prompt
433
- tokens as 25 chunks of 64 then 32 greedy decode steps, final position 1,631, so the sliding
434
- ring wraps about three times. fp32, 57 comparisons per run, **0 argmax mismatches** on every
435
- bundle below. No dynamic dimension appears in either entrypoint of any of them.
436
-
437
- | Bundle | Context | KV state shape | KV bytes, both states | Sliding / full slots |
438
- |---|---:|---|---:|---|
439
- | `ring3/gpu-pipelined/gemma4_e2b_qat_decode_int4lin_tbl_pf64_ring_c4096_gqafull` | 4,096 | `[1, 1, 1, 19200, 512]` x2 | 39.3 MB | 12 x 576 + 3 x 4,096 |
440
- | `ring3/gpu-pipelined/gemma4_e2b_qat_decode_int4lin_tbl_pf64_ring_c2048_gqafull` | 2,048 | `[1, 1, 1, 13056, 512]` x2 | 26.7 MB | 12 x 576 + 3 x 2,048 |
441
-
442
- E2B runs 28 sliding and 7 full attention layers; in these bundles the 28 keep the gathered
443
- form and the 7 use the folded one.
444
-
445
- ## `ring3-smoke/` β€” truncated proving asset
446
-
447
- > **EXPERIMENTAL β€” not a usable model.** A 5-layer truncation of the E2B decoder at the 4,096
448
- > capacity (`[1, 1, 1, 6400, 512]` states, 4 x 576 + 1 x 4,096), published only so host-side
449
- > work can be developed against the contract over a smaller download. It carries real weights
450
- > for the layers it keeps and produces low-quality text; do not evaluate quality from it.
 
1
  ---
2
+ license: apache-2.0
3
+ license_link: https://ai.google.dev/gemma/docs/gemma_4_license
4
  base_model: google/gemma-4-E2B-it-qat-q4_0-unquantized
5
+ base_model_relation: quantized
6
  library_name: coreai
7
  pipeline_tag: text-generation
8
  tags:
 
10
  - aimodel
11
  - apple-silicon
12
  - on-device
13
+ - coreai-kit
14
  - quantized
15
  - int4
16
  - qat
 
19
 
20
  # Gemma 4 E2B β€” Core AI (.aimodel)
21
 
22
+ `google/gemma-4-E2B-it-qat-q4_0-unquantized` converted to Core AI `.aimodel` bundles for Apple
23
+ silicon by [visible-cx](https://huggingface.co/visible-cx). These are derivative artifacts:
24
+ Google's QAT-trained weights rounded onto the int4 grid they were trained for and re-expressed
25
+ as a Core AI graph. They load through Core AI on macOS and are not usable by PyTorch, GGUF or
26
+ MLX.
27
 
28
+ Gemma 4 E2B uses **Per-Layer Embeddings**, so these bundles take a large embedding gather table
29
+ as a *static graph input* rather than carrying it in the graph. That table ships in
30
  `ios-frontend/` and **the bundles do not load without it**; a missing table produces a bare
31
+ input-arity error naming `ple_table`/`ple_scale`. It is 2.81 GB and it is part of the model's
32
+ memory cost, not a sidecar you can ignore.
33
+
34
+ > ⚠️ **Known issue β€” memory growth per generated token.** The Core AI runtime caches one
35
+ > compiled specialization per input-shape signature, and this export makes sequence length
36
+ > load-bearing: every generated token adds one token to `position_ids` and mints a new
37
+ > signature, retaining roughly **81 MB of GPU allocations per generated token** until the
38
+ > process exits.
39
+ >
40
+ > **Guided decoding does not protect you β€” shape reuse does.** At 64 generated tokens the same
41
+ > prompt costs **9.84 GB free-form and 9.88 GB guided**; the grammar loop is not a defence. What
42
+ > *is* a defence is repeating lengths you have already run, which replay from cache at no cost:
43
+ > a fixed-schema, fixed-cap extraction workload stays bounded, and that is the shape measured
44
+ > at 10/10 below. Long or variable-length generation grows without bound; no in-process
45
+ > mitigation exists, and it is not fixable below a re-export. `stable/` and `ring/` in this repo
46
+ > are those re-exports β€” see [Shape-stable re-exports](#shape-stable-re-exports).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  ## Contents
49
 
50
+ ### Production bundles
51
 
52
+ | Path | Bytes | Context | Functions | Status |
53
+ |---|---:|---|---|---|
54
+ | `gpu-pipelined/gemma4_e2b_qat_decode_int4lin_tbl_pf64` | 2,122,089,973 | 4096 | main + prefill | **QUALIFIED (guided / bounded shapes)** |
55
+ | `w4a8/gemma4_e2b_qat_decode_int4lin_a8_tbl_pf64` | 2,122,679,604 | 16384 | main + prefill | EXPERIMENTAL |
56
 
57
  Each bundle folder holds `<name>.aimodel/` (`main.mlirb` β‰ˆ 2.09 GB, `main.hash`, asset
58
  `metadata.json`), a bundle-level `metadata.json`, and `tokenizer/` (`tokenizer.json`
59
  32,169,626 B, `tokenizer_config.json`, `generation_config.json`, `chat_template.jinja`
60
  18,569 B). The `w4a8` folder additionally ships its `calibration_corpus.jsonl` (35,045 B).
61
 
 
 
 
 
 
62
  ### The PLE gather-table sidecar β€” required, not optional
63
 
64
  | Path | Files | Bytes |
 
77
 
78
  `meta.json` records the shape and the dequant convention:
79
  `V 262144, D 1536, PLD 8960, L 35, ld 256, embed_scale_pl 16.0` (= √256, which is what the
80
+ exporter assumes). Every `_tbl` bundle binds `ple_table` (from `embed_per_layer.i8`) and
81
+ `ple_scale` (from `embed_per_layer.scale.f32`) as **static** graph inputs. **A QAT bundle must
82
+ be paired with the QAT tables.**
83
+
84
+ ### Diagnostic and proving assets
85
+
86
+ | Path | Bundle | Bytes | Context |
87
+ |---|---|---:|---|
88
+ | `stable/gpu-pipelined/` | `…_tbl_pf64_stable_c16384` | 2,122,071,656 | 16384 |
89
+ | `ring/gpu-pipelined/` | `…_tbl_pf64_ring_c16384` | 2,122,101,099 | 16384 |
90
+ | `ring2/gpu-pipelined/` | `…_tbl_pf64_ring_c2048` | 2,122,043,748 | 2048 |
91
+ | `ring2/gpu-pipelined/` | `…_tbl_pf64_ring_c8192` | 2,122,068,315 | 8192 |
92
+ | `ring3/gpu-pipelined/` | `…_tbl_pf64_ring_c2048_gqafull` | 2,122,042,254 | 2048 |
93
+ | `ring3/gpu-pipelined/` | `…_tbl_pf64_ring_c4096_gqafull` | 2,122,050,478 | 4096 |
94
+ | `stable-smoke/` `ring-smoke/` `ring2-smoke/` `ring3-smoke/` | 5-layer truncations | ~1.17 GB each | β€” |
95
+
96
+ The `*-smoke` folders are **not models**: 5-layer truncations of the decoder, published only so
97
+ host-side work can be developed against a small download. They carry real weights for the
98
+ layers they keep and produce low-quality text by design.
99
+
100
+ **Stop token:** every bundle declares `eos_token = "<turn|>"` (id 106), the turn terminator
101
+ Gemma 4 emits. `generation_config.json` independently lists `eos_token_id: [1, 106, 50]`. A
102
+ host that stops on the raw upstream `<eos>` will overrun every reply.
103
 
104
+ ## Provenance
105
+
106
+ | | |
107
+ |---|---|
108
+ | Base checkpoint | `google/gemma-4-E2B-it-qat-q4_0-unquantized` (ungated) |
109
+ | Zoo recipe | `gemma-4-e2b`, `status = "verified"` β€” `int4lin --tbl` |
110
+ | Recipe (pf64) | `export_gemma4_pf_pipelined.py --pf 64` with `--tbl` and `--raw-dir` pointed at the gather table above |
111
+ | Toolchain base | `apple/coreai-models` @ `b1cb71b8522d99408059fa0b98b8742171bcb0b8` + the [coreai-model-zoo](https://github.com/john-rocky/coreai-model-zoo) python overlay |
112
+ | Toolchain | `coreai-torch 0.4.1`, `coreai-core 1.0.0b2`, `coreai-opt 0.2.1`, `torch 2.9.0` |
113
+ | Producer fingerprint | `coreai-core 1.0.0b2` on every inner asset `metadata.json` |
114
+ | Weight format | **int4, per-block-32** (`int4lin`, symmetric-with-clipping) β€” the ggml q4_0 grid the QAT checkpoint was trained on |
115
+ | Vocab | 262,144 |
116
+ | Export functions | `main` (S=1 decode) and, in `_pf64` bundles, `prefill` (S=64 chunked prefill) |
117
+
118
+ "QAT-unquantized" means QAT-*trained*, stored at full width; the int4 rounding happens at
119
+ export, onto the grid training already targeted.
120
+
121
+ `_tbl` = the PLE gather table is bound as a static graph input. `_pf64` = a second entrypoint,
122
+ `prefill`, with a static query width of 64 (`function_map: {"main": ["main", "prefill"]}`).
123
+
124
+ The `gpu-pipelined/` bundle mirrors the zoo's verified `gemma-4-e2b` recipe with the `pf64`
125
+ multifunction addition; the gather table is the zoo's own
126
+ [mlboydaisuke](https://huggingface.co/mlboydaisuke) artifact layout, and the zoo's
127
+ `gemma-4-E2B-CoreAI` repo is the upstream this one is a sibling of.
128
 
129
  ## Requirements
130
 
131
  - **Apple silicon Mac**, Core AI runtime.
132
  - **Engine contract: 4 inputs** β€” `input_ids`, `position_ids`, plus static `ple_table` and
133
+ `ple_scale`. Two engines accept that, and the difference matters:
134
+ - **Pipelined engine** β€” binds the statics zero-copy over the caller's buffer, but does not
135
+ expose logits, so no grammar-constrained decoding. This is the default path in CoreAIKit's
136
+ `GemmaRuntime` and the path the throughput numbers below were measured on.
137
  - **Sequential engine** β€” the only logits-capable engine, and therefore the only path for
138
  guided decoding. It accepts `>= 2` inputs and binds everything beyond
139
+ `input_ids`/`position_ids` from `EngineOptions.staticInputBuffers`. **A caller that does not
140
+ supply those buffers gets a load failure by name**, not a fallback:
141
+ `invalidInputType("Inputs beyond input_ids/position_ids must be bound as constant buffers
142
+ on this engine, but ["ple_table", "ple_scale"] have none.")`. It pays a **one-time copy of
143
+ every constant input at load β€” 2.19 GB for E2B** β€” because its submission path otherwise
144
+ materialises a foreign buffer-backed view on every forward pass; binding the table as a raw
145
+ view instead costs ~0.30 s per pass (3.4 tok/s).
146
+ - **States:** `keyCache` / `valueCache` `Float16, 15 Γ— 1 Γ— 1 Γ— ? Γ— 512`. Dynamic sequence dim β†’
147
+ `GrowingKVCache` (initial 256, doubling), not a static allocation at the manifest maximum.
148
+ - **KV cost: 30,720 bytes per token** (fp16) β€” 126 MB at 4096, 503 MB at 16384. **KV is not the
149
+ ceiling at this tier; the PLE table is.**
150
+ - **Minimum practical machine memory: 16 GB**, for bounded-shape guided work only.
151
+ - Sliding-window note: E2B interleaves sliding and full attention layers across 35 layers
152
+ collapsed to 15 KV slots. The export models the window in the *mask*, not in memory β€”
153
+ sliding layers occupy full-length slots and zero-pad head_dim 256β†’512, so 80% of KV bytes sit
154
+ in windowed slots and 40% is dead padding.
155
+ - The bundle manifest declares `runtime_env COREAI_CHUNK_THRESHOLD=1`.
156
+
157
+ ## Measurements
158
+
159
+ Measured on a **16 GB Apple silicon Mac (M2 Pro, macOS 27 beta)**.
160
+
161
+ ### Throughput and memory β€” n=2, watchdogged, both runs completed
162
+
163
+ The published `gpu-pipelined/` bundle at its shipped c4096, with the external PLE tables, on the
164
+ pipelined engine (both mandatory), 73-token prompt, 8 calls of 83 generated tokens, watchdog
165
+ sampling at 0.5 s. **Both runs completed 8/8 and neither tripped.**
166
+
167
+ | | run 1 | run 2 |
168
+ |---|---:|---:|
169
+ | reclaimable at start | 4.88 | 6.20 |
170
+ | wired baseline β†’ peak | 3.15 β†’ 8.15 | 2.97 β†’ 8.01 |
171
+ | **wired growth** | **5.00** | **5.04** |
172
+ | process footprint peak | 4.64 | 4.58 |
173
+ | compressor | 2.34 β†’ 5.48 | 2.56 β†’ 5.33 |
174
+ | reclaimable trough | **1.35** | 1.78 |
175
+ | **decode** | **41.35 tok/s** | **41.40 tok/s** |
176
+
177
+ GiB unless stated. n=2 agreeing to **0.8%**.
178
+
179
+ **Charge this model for two artifacts, not one.** The E-series loads a compiled program *and* a
180
+ PLE table, and a memory law told only about the first is wrong by 2.87 GiB:
181
+
182
+ ```
183
+ compiled blob resources.bin 2,088,055,648 B = 1.9447 GiB
184
+ PLE tables gemma4_qat_gather_raw 2,808,611,840 B = 2.6157 GiB
185
+ ------------------------------------------------------------------------
186
+ what the runtime actually loads = 4.5603 GiB
187
+ ```
188
+
189
+ (The PLE figure is the binary payload the runtime binds; the published folder is 231 bytes
190
+ larger because it also carries `meta.json`.)
191
+
192
+ | denominator | wired Γ· denominator |
193
+ |---|---:|
194
+ | compiled blob alone | **2.581** β€” absurd |
195
+ | **blob + PLE tables** | **1.101** |
196
+
197
+ **1.101 sits alongside the LFM MoE's 1.092 and below the dense 12B's 1.157** β€” the E-series is
198
+ not architecture-exceptional at all; it was being charged for one of the two files it opens. The
199
+ honest requirement on this machine is `4.5603 Γ— 1.10 + 1.25 GiB in-flight floor` = **6.27 GiB**.
200
+
201
+ A related catalog error, recorded because it points the other way: `approximateBytes` for E2B
202
+ had been taken from the published **LiteRT `.litertlm`** artifact (β‰ˆ 2.41 GB), a file the Core AI
203
+ backend never opens. One number over-charged the download size and the other under-charged the
204
+ memory gate, from the same root cause β€” pricing the wrong artifact.
205
+
206
+ ### Guided structured output
207
+
208
+ 10-sample harness, guided JSON-constrained decoding against a fixed schema, greedy, **sequential
209
+ engine** with the PLE tables bound as static inputs, `reset()` between samples, 128-token cap.
210
+
211
+ | | `gpu-pipelined/…_tbl_pf64` |
212
  |---|---|
213
  | Load | 12.1 s |
214
  | Guided JSON parse | **10/10** |
 
218
  | Decode | **22.7–32.7 tok/s** |
219
  | TTFT | **0.59–4.20 s** |
220
  | Peak footprint | 8.26 GB |
 
221
  | Outcome | completed all ten samples |
222
 
223
+ The 41.4 tok/s figure above and the 22.7–32.7 here are not in conflict: the first is the
224
+ pipelined engine unguided, the second is the sequential engine under a grammar mask, whose
225
+ step-synchronous prefill costs roughly 25% per sample. An earlier unguided pipelined run on the
226
+ same machine reached 44.3 tok/s; upstream measured E2B at 77.0/87.1 tok/s on an M4 Max.
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
+ **The S=64 `prefill` function carries the time-to-first-token.** Head to head on a 942-token
229
+ prompt against a decode-only export of the same weights, exporting `prefill` moved TTFT
230
+ 35.74 s β†’ 4.65 s and s/row 45.04 β†’ 8.83, a **7.7Γ—**. Decode is untouched. The decode-only
231
+ bundle is no longer published.
232
 
233
+ **Enum conformance is the grammar's doing.** Unguided, the model emits an off-schema enum value
234
+ on essentially every sample. Guided, all ten are correct, because an off-enum token is
235
+ unsamplable.
 
236
 
237
+ **Needle-in-haystack recall: 3/3 verbatim at 8k**, within a 64-token cap. 15k was not attempted:
238
+ 8k already cost 36.71 GB of footprint under the shape-signature defect.
239
 
240
+ ### What this model is and is not, on this stack
 
 
241
 
242
+ The shape-signature growth sets a hard ceiling that no cap works around: at ~81 MB/token over a
243
+ ~9.6 GB intercept, a 900-token report costs ~54 GB (measured killed) and a 600-token insight
244
+ card ~58 GB. The 128-token ceiling that does survive is shorter than a single card. **Gemma 4 is
245
+ an enrichment model on this stack β€” bounded, repeating shapes β€” and it is not a report, RAG or
246
+ long-insight model. The blocker is the export, not the weights.**
 
 
 
 
 
247
 
248
+ ### The `staticInputBuffers` residency question, settled
 
 
 
249
 
250
+ The PLE table is bound through `EngineOptions.staticInputBuffers`, and it had been assumed those
251
+ pages stay clean and evictable. **They do not.** Forcing the mapped path
252
+ (`COREAI_GEMMA_TABLES=mapped`) against the owned one, on this bundle:
 
 
 
 
 
 
 
 
253
 
254
+ | | `owned` (`makeBuffer` + read) | `mapped` (`mmap` + `bytesNoCopy`) |
255
+ |---|---:|---:|
256
+ | `phys_footprint` after load | **2.39 GiB** | **0.19 GiB** |
257
+ | `vmmap` region holding the table | `IOAccelerator`, 2.2 G resident, dirty 64K | `mapped file`, dirty 0K |
258
+ | **wired growth** | **4.67 GiB** | **4.65 GiB** |
 
 
 
259
 
260
+ The footprint column is real and matters on iOS jetsam accounting. **The wired column is what
261
+ the machine is about, and the two paths are identical to within 0.02 GiB.** Under pressure β€”
262
+ reclaimable falling to 2.52 GiB with the compressor climbing β€” not one byte came back. An
263
+ `mmap`-backed static input gets wired at the first forward pass exactly like a program constant.
264
 
265
+ One qualification, added later: that test measured whether the *machine* can take the pages
266
+ back, and it cannot. Releasing the `InferenceFunction` does give them back β€” the process can,
267
+ even though the kernel cannot.
268
 
269
+ ## Usage
 
 
 
270
 
271
+ Swift Package Manager, via [CoreAIKit](https://github.com/john-rocky/coreai-kit) β€” a community
272
+ package, not affiliated with Apple, requiring macOS 27 beta:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
 
274
+ ```swift
275
+ .package(url: "https://github.com/john-rocky/coreai-kit", branch: "main")
276
+ // target dependency: .product(name: "CoreAIKit", package: "coreai-kit")
277
  ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
 
279
+ An E-series bundle is **two downloads**, the decoder and its paired PLE tables, addressed as two
280
+ paths inside this repo:
281
 
282
+ ```swift
283
+ import CoreAIKit
 
 
 
284
 
285
+ let store = ModelStore.default
286
+ let decoderURL = try await store.download(
287
+ ModelID("visible-cx/gemma-4-E2B-CoreAI",
288
+ path: "gpu-pipelined/gemma4_e2b_qat_decode_int4lin_tbl_pf64"))
289
+ let tablesURL = try await store.download(
290
+ ModelID("visible-cx/gemma-4-E2B-CoreAI",
291
+ path: "ios-frontend/gemma4_qat_gather_raw"))
292
 
293
+ // engineVariant defaults to .pipelined here, which is the supported path for _tbl bundles.
294
+ let runtime = try await GemmaRuntime(
295
+ decoderBundleAt: decoderURL, tablesAt: tablesURL, arch: .gemma4)
296
+ ```
297
 
298
+ Notes that are not optional:
299
 
300
+ - **Do not pair a QAT bundle with non-QAT tables.**
301
+ - **Do not enable chunked prefill.** The `…_tbl` graph is S=1 on `main`; `GemmaRuntime` sets
302
+ `COREAI_CHUNK_THRESHOLD=1` for you if it is unset. Leave it.
303
+ - **Guided decoding needs the sequential engine**, and the sequential engine needs
304
+ `ple_table`/`ple_scale` supplied through `EngineOptions.staticInputBuffers` β€” it will refuse
305
+ by name otherwise.
306
+ - Pass `revision:` a Hub commit hash to pin immutable bytes.
307
 
308
+ ## Integrity
 
 
309
 
310
+ Core AI `.aimodel` bundles are **not byte-reproducible**: the exporter is not deterministic even
311
+ against itself. Verify by digesting the exact published bytes rather than by rebuilding. Every
312
+ bundle carries `main.hash`, the raw 32 bytes of `sha256(main.mlirb)`; on the Hub the same value
313
+ is recoverable from the LFS oid without fetching the file. The compiled-blob identity this
314
+ project keys its measurements on for the published bundle is
315
+ `73bef8155c41d512e9c6b4ab1788b7547bade134`.
316
 
317
+ ## Status
318
 
319
+ | Artifact | Status |
320
+ |---|---|
321
+ | `gpu-pipelined/…_tbl_pf64` (ctx 4096) | **QUALIFIED FOR BOUNDED-SHAPE GENERATION** β€” measured: 10/10 guided parse and enum-clean, 6.12/4.23 s/row, 41.4 tok/s unguided on n=2 completed watchdogged runs, 5.00–5.04 GiB wired growth, honest requirement 6.27 GiB. **Not qualified for free-form or variable-length generation** β€” see the known issue. |
322
+ | `w4a8/…_a8_tbl_pf64` | **EXPERIMENTAL** β€” built, unmeasured. Same int4 weights and graph; the difference is an int8 quantize/dequantize pair on the inputs of every `F.linear`, calibrated on 128 synthetic samples (corpus ships in the folder). Built as a prefill/TTFT lever, with the toolchain's own caveat that the runtime fast path is `W_INT8 Γ— A_INT8` and these weights use int4 scale-multiply dequant β€” so the expected gain may be zero or negative. It exists to be measured, not because a win is predicted. |
323
+ | `stable/…_stable_c16384` | **EXPERIMENTAL** β€” the memory fix is confirmed (6.53 GB flat peak across a 664-token generation, +0.011 MB/token, guided output byte-identical to the default bundle) but decode runs at **~1.0 tok/s** because every step reads the full 16,384-slot cache. A working proof of the contract, not a deployable bundle. |
324
+ | `ring/`, `ring2/`, `ring3/` | **EXPERIMENTAL β€” gated in torch, no on-device numbers.** Diagnostic assets for the capacity-vs-cost question. |
325
+ | `*-smoke/` | **NOT MODELS** β€” 5-layer truncations for host development. |
326
 
327
+ **No numerics gate has been run on device for any bundle in this repo.** The 10/10 results are
328
+ behavioural (parse rate, enum conformance, clean stop); a decode oracle against an fp32
329
+ reference has not been run.
330
 
331
+ ## Shape-stable re-exports
 
 
 
332
 
333
+ `stable/`, `ring/`, `ring2/` and `ring3/` are re-exports of the same weights and the same
334
+ quantization onto a contract in which **no input shape moves between steps**, which removes the
335
+ per-generated-token growth by design. They need a host that feeds `position_ids` as the
336
+ **absolute positions of the S new tokens only**; a host that feeds the growing `0..N` prefix
337
+ will write the KV cache at the wrong offset.
 
 
 
 
 
338
 
339
+ **Contract** (identical across all four families):
 
340
 
341
  ```
342
  main IN input_ids Int32 1x1 | position_ids Int32 1x1 | ple_table Int8 V x (L*ld) | ple_scale Float32 V
343
  prefill IN input_ids Int32 1x64 | position_ids Int32 1x64 | (same statics)
344
+ ST keyCache / valueCache Float16, literal extents
345
  OUT logits Float16 1 x S x 262144
346
  ```
347
 
348
+ `position_ids[0,0]` is also the cache slot the K/V for those tokens is written at. The mask β€”
349
+ causality, unwritten slots and the sliding window β€” is derived inside the graph from those
350
+ positions, so there is no mask input to bind. The context ceiling is **encoded in the graph**: a
351
+ different window needs a different export, not a manifest edit.
352
 
353
+ **`stable/` gave every layer a full 16,384-slot cache and read all of it every step**, which is
354
+ the 1 tok/s. `ring/` keeps the contract byte for byte and changes what sits behind it: sliding
355
+ layers read a **576-slot ring** (the model's 512-token window plus one 64-token prefill chunk)
356
+ instead of 16,384, and the grouped-query head expansion is folded into the query rather than
357
+ materialised.
358
 
359
+ | | `stable/` | `ring/` |
360
  |---|---|---|
361
+ | KV state shape | `[slots, 1, n_kv, 16384, 512]` Γ—2 | `[1, 1, 1, 56064, 512]` Γ—2 |
362
+ | KV bytes, both states | 503.3 MB | **114.8 MB** |
363
+ | cache slots read per decode step | 245,760 | **56,064** |
364
+ | dynamic dimensions | none | none |
365
+
366
+ `ring2/` is the same recipe at 2,048 and 8,192 (26.7 MB and 64.5 MB of KV); `ring3/` folds the
367
+ GQA expansion **only in the full-attention layers**, where the key length is long enough for the
368
+ trade to pay, and keeps the stock gathered form in the short-key sliding layers.
369
+
370
+ **One host precondition new in `ring/`:** a `prefill` call's first position must be a multiple
371
+ of 64. The ring write is a fixed-width store at `p0 % 576`, and 576 is nine 64-token chunks, so
372
+ a 64-aligned chunk can never straddle the wrap. `stable/` tolerated an unaligned chunk; `ring/`
373
+ does not.
374
+
375
+ **Equivalence gates**, all run in eager torch against the shipped graph before conversion:
376
+ `stable/` β€” greedy argmax identical on every tested decode step including past the sliding
377
+ window, worst logits cosine 0.99998 (fp16), prefill bit-identical in fp32. `ring*/` β€” 1,600
378
+ prompt tokens as 25 chunks of 64 then 32 greedy decode steps, final position 1,631 so the ring
379
+ wraps about three times, fp32, 57 comparisons per run: **0 argmax mismatches** on every bundle,
380
+ worst cosine 0.999999999987. No dynamic dimension appears in either entrypoint of any of them.
381
+
382
+ ## License
383
+
384
+ Google publishes the upstream QAT checkpoint under **Apache-2.0** with a `license_link` to the
385
+ [Gemma 4 license](https://ai.google.dev/gemma/docs/gemma_4_license), and this repo mirrors that
386
+ declaration. Use is governed by those terms and by the
387
+ [Gemma Prohibited Use Policy](https://ai.google.dev/gemma/prohibited_use_policy); the
388
+ obligations travel with any redistribution of these bundles, **including the gather-table
389
+ sidecar**, which is derived from the same weights. The contribution here is the conversion, not
390
+ the weights.