1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
//! Per-page GPU scratch for one tower run (ROADMAP M-V4).
//!
//! # Sized by the page, allocated per image, dropped after the readback
//!
//! Unlike `DecodeScratch`, which is allocated once at open because every
//! decode step is the same shape, a tower run's shape is the image's: a
//! 1024x1280 OCR page is 5,120 patches and the largest page the processor
//! accepts is 64,516. So this is built per `encode_image` and dropped with
//! the embedding, which is the bulk-OCR constant-memory story -- peak vision
//! residency is `2 x block_stride` of pinned slots plus one page's scratch,
//! and the scratch goes away between pages.
//!
//! At the real widths that was about 152 MB for a 5,120-patch page and about
//! 1.9 GB for the 64,516-patch extreme, before [`VISION_MLP_TILE_ROWS`]
//! (Part B1): `fc1 -> gelu -> fc2` is row-independent, so a fixed row tile
//! caps [`VisionScratch::h1`] (the 555 MB term at the extreme page) with
//! identical arithmetic -- the loop runs the same three dispatches once per
//! tile instead of once for the whole page. Attention cannot be tiled the
//! same way -- it is bidirectional over the whole page -- so that lever
//! bounds the MLP term alone and nothing else. The tiling is unconditional
//! rather than gated on page size: an ordinary 1024x1280 OCR page is 5,120
//! patches, already more than one [`VISION_MLP_TILE_ROWS`]-row tile, so the
//! byte saving and the loop both apply to it too, not only to the extreme
//! case that motivated the lever.
//!
//! # What is reused and what is not
//!
//! `normed` serves BOTH the block norms and the merger's norm, and that is
//! not a saving so much as the merger's own shape: its norm is over `hidden`
//! per patch row, and the reshape to `[merged, hidden * merge^2]` that
//! follows is a pure VIEW of the same bytes, because M-V1 already emits patch
//! rows in merge-window order. There is no permutation to do and therefore no
//! second buffer to do it into.
//!
//! `proj` serves both the attention projection and the MLP's `fc2`, which are
//! never live at once. `q`/`k`/`v` are separate because the fused `qkv`
//! weight is run as three matmuls at three row offsets (the shape
//! `crates/gpu/tests/vision_block_parity.rs` established) and each writes a
//! contiguous `[seq, hidden]` the rope and attention kernels can read
//! directly.
//!
//! # Buffer aliasing (Part B2)
//!
//! `attn`, `proj` and `m1` are ALIASES rather than separate allocations --
//! `metal::Buffer::clone()` retains the same underlying `MTLBuffer`, so
//! `s.attn`, `s.proj` and `s.m1` are each a second Rust handle onto bytes
//! `s.normed`, `s.q` and `s.k` already own. This is sound under the SAME
//! commit-order guarantee `crate::vision::mod`'s block loop already states
//! for the two streamer slots (`crates/gpu/CLAUDE.md` Gotcha 8: dispatches on
//! one command buffer execute in commit order), applied at a finer grain --
//! every read of an aliased buffer under its old name is dispatched, in this
//! pass, strictly before the next write under its new name:
//!
//! - **`attn` aliases `normed`.** `normed`'s three reads (the qkv matmuls)
//! all happen before `attn` is first written (the attention kernel);
//! `attn`'s one read (the output projection) happens before `normed` is
//! next written (block N's `norm2`, or the merger's own norm after the
//! last block).
//! - **`proj` aliases `q`.** `q`'s last read (the attention kernel) happens
//! before `proj` is first written (the output projection); `proj` is then
//! read (the first residual add) and overwritten (the MLP's `fc2`) and
//! read again (the second residual add) entirely within the SAME block,
//! before the next block's `q` write.
//! - **`m1` aliases `k`.** `k` is read by every block's rope-and-attention
//! pair and never touched by the merger; `m1` is written and read ONLY by
//! the merger, which runs once, after the last block. The two are also the
//! SAME byte count for any valid page: `merged * merger_input ==
//! (seq / merge^2) * (hidden * merge^2) == seq * hidden`, an algebraic
//! identity rather than a coincidence of one shape, since `allocate`
//! already refuses a `seq` that is not a whole number of merge windows.
//!
//! `v` is NOT aliased: it has no dead point before the block loop ends (every
//! block's attention kernel reads it), so there is no free buffer to fold it
//! into. Five physical wide (`seq * hidden`) allocations remain: `x`,
//! `normed`(+`attn`), `q`(+`proj`), `k`(+`m1`), `v` -- against seven before
//! this change, before counting `m1`'s own separate allocation as an eighth
//! it no longer needs.
//!
//! This rests on the SERIAL encoder: a future prefetch or overlap arm that
//! lets two blocks' dispatches interleave on different command buffers would
//! have to un-alias first, since the ordering argument above assumes one
//! command buffer's dispatches never race another's.
use crateRealForwardError;
use crateVisionShape;
/// Every buffer one tower run needs, plus what they cost.
pub
/// FP16 bytes for `elems` elements.
/// F32 bytes for `elems` elements. Only `freqs` is F32 (AGENTS.md/CLAUDE.md
/// B7); every other scratch buffer here is FP16.
/// Row tile for the MLP's `fc1 -> gelu -> fc2` (`crate::vision::block`'s
/// "--- MLP half ---" loop). `fc1 -> gelu -> fc2` is row-independent, so a
/// fixed tile caps [`VisionScratch::h1`] at
/// `min(seq, VISION_MLP_TILE_ROWS) * intermediate` elements regardless of
/// how many patches the page has, with identical arithmetic to running
/// every row at once.
///
/// Named exactly this because [`scratch_bytes`] and the tiling loop in
/// `block.rs` both reference it, and the two must never drift apart about
/// what "the tile" means.
pub const VISION_MLP_TILE_ROWS: usize = 2048;
/// Bytes one page of `seq` patches costs in scratch, at a given MLP row
/// tile.
///
/// The single formula both [`VisionScratch::allocate`] (which actually
/// allocates at this size) and `VisionTower::scratch_bytes` (which predicts
/// it for a caller sizing a page budget, and which passes its own current
/// [`VisionTower::mlp_tile_rows`] here) call, so the two can never
/// independently drift -- only whether either one is applied at all, which
/// is exactly the failure mode `VisionTower::scratch_bytes`'s own doc comment
/// exists to let a gate assert against.
///
/// [`VisionTower::mlp_tile_rows`]: super::VisionTower
pub