Skip to main content

memra_engine/
vision_gemma.rs

1//! Vision tower for the gemma-4 family (lane/gemma-vision, 2026-08-16).
2//!
3//! Gemma-4 is its OWN semantic program — nothing here is inherited from the qwen3_5
4//! tower by analogy; every law below was derived from the family's reference
5//! implementation (llama.cpp `clip_graph_gemma4v`, the vendor-blessed consumer of the
6//! official `google/gemma-4-*-qat-q4_0-gguf` mmproj packaging) and the mmproj tensor
7//! census (`research/gemma-vision-20260816/REPORT.md` carries the receipts):
8//!
9//! - ViT: 27 blocks, hidden 1152, 16 heads (head_dim 72), ffn 4304 — but RMS norms
10//!   (not LayerNorm), NO biases anywhere, per-head RMS q/k norms (72), a WEIGHTLESS
11//!   RMS on V before attention, and sandwich post-norms (attn_post / ffn_post) applied
12//!   BEFORE each residual add. Attention runs UNSCALED: kq_scale = 1.0, not 1/sqrt(d).
13//! - FFN is GEGLU-quick: gelu_quick(gate(x)) * up(x), gelu_quick(x) = x*sigmoid(1.702x)
14//!   (llama.cpp default when the mmproj carries neither use_gelu nor use_silu).
15//! - Positions: FACTORED ADDITIVE tables (one x-table, one y-table, 10240 rows each,
16//!   `v.position_embd.weight` logical [2, 10240, 1152]) added to the patch embeddings,
17//!   PLUS a per-layer 2D rope on q/k: first 36 dims rotate by pos_x, last 36 by pos_y,
18//!   neox pairing (d, d+18) inside each half, theta = 100.0 (hardcoded in the
19//!   reference, not a metadata key).
20//! - Input: pixels in [0,1], NO mean/std normalization (image_mean 0 / std 1 in the
21//!   census); the graph applies 2x-1. Patch embed is a bias-less conv16, flattened
22//!   here to a Linear over (c, ky, kx)-ordered 768-float patch rows.
23//! - Head: 3x3 avg-pool over the patch grid (n_merge 3), scale by sqrt(1152),
24//!   (x - std_bias) * std_scale, WEIGHTLESS RMS, then a single 1152 -> 5376 projection
25//!   (`mm.input_projection`; this file ships no ClippableLinear clamp scalars).
26//! - Preprocessing: native resolution — smart-resize to a 48-aligned grid with the
27//!   token budget 40..280 (token = 48x48 px block), bilinear.
28//!
29//! Serving-law note (derived, NOT wired here): gemma-4 image spans decode with
30//! NON-CAUSAL attention inside the LM (`mtmd_decode_use_non_causal` = true for this
31//! family). memra's prime path is causal, so serving gemma vision through it would be
32//! silently wrong — the serving gate must refuse until a masked-prefill arm exists.
33//!
34//! v1 posture matches the qwen tower: correctness-first — f32 GEMMs, `sdpa_naive`,
35//! host-side permutes/rope/geglu; parity gate before any serving path.
36
37use crate::Engine;
38use cudarc::driver::CudaSlice;
39use memra_gguf::dequant::bf16_to_f32;
40use memra_gguf::{GgmlType, GgufFile};
41use std::path::Path;
42
43pub const GV_HIDDEN: usize = 1152;
44pub const GV_HEADS: usize = 16;
45pub const GV_HEAD_DIM: usize = GV_HIDDEN / GV_HEADS; // 72
46pub const GV_INTER: usize = 4304;
47pub const GV_DEPTH: usize = 27;
48pub const GV_PATCH: usize = 16;
49pub const GV_MERGE: usize = 3; // pooling kernel (n_merge)
50pub const GV_POS_ROWS: usize = 10240; // per-axis position table rows
51pub const GV_OUT: usize = 5376; // gemma-4-31B n_embd
52pub const GV_PATCH_IN: usize = 3 * GV_PATCH * GV_PATCH; // 768
53pub const GV_ALIGN: usize = GV_PATCH * GV_MERGE; // 48
54/// Output-token budget (llama.cpp gemma4v: set_limit_image_tokens(40, 280)); a token
55/// is one pooled 48x48-pixel block, so the pixel budget is tokens * 48*48.
56pub const GV_MIN_TOKENS: usize = 40;
57pub const GV_MAX_TOKENS: usize = 280;
58const RMS_EPS: f32 = 1e-6;
59const ROPE_THETA: f32 = 100.0;
60
61/// Begin/end delimiters + the soft token that occupies image positions in the token
62/// stream (gemma-4-31B vocab; the soft token embedding is REPLACED by tower rows).
63pub const GV_TOK_BEGIN: u32 = 255999; // <|image>
64pub const GV_TOK_SOFT: u32 = 258880; // <|image|>
65pub const GV_TOK_END: u32 = 258882; // <image|>
66
67struct GLin {
68    w: CudaSlice<f32>,
69    in_f: usize,
70    out_f: usize,
71}
72
73struct GBlock {
74    ln1: CudaSlice<f32>,
75    ln2: CudaSlice<f32>,
76    attn_post: CudaSlice<f32>,
77    ffn_post: CudaSlice<f32>,
78    q_norm: Vec<f32>,
79    k_norm: Vec<f32>,
80    wq: GLin,
81    wk: GLin,
82    wv: GLin,
83    wo: GLin,
84    gate: GLin,
85    up: GLin,
86    down: GLin,
87}
88
89pub struct GemmaVisionTower {
90    patch_w: GLin, // conv16 flattened: 768 -> 1152, no bias
91    /// Host copies of the factored position tables, [10240, 1152] each.
92    pos_x: Vec<f32>,
93    pos_y: Vec<f32>,
94    blocks: Vec<GBlock>,
95    std_bias: Vec<f32>,
96    std_scale: Vec<f32>,
97    proj: GLin, // 1152 -> 5376
98}
99
100fn read_f32(g: &GgufFile, name: &str) -> Result<Vec<f32>, Box<dyn std::error::Error>> {
101    let t = g
102        .find(name)
103        .ok_or_else(|| format!("gemma vision tensor missing: {name}"))?;
104    let raw = g.tensor_data(t);
105    match t.ggml_type {
106        GgmlType::BF16 => Ok(raw
107            .chunks_exact(2)
108            .map(|c| bf16_to_f32(u16::from_le_bytes([c[0], c[1]])))
109            .collect()),
110        GgmlType::F32 => Ok(raw
111            .chunks_exact(4)
112            .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
113            .collect()),
114        other => Err(format!("gemma vision tensor {name}: unsupported type {other:?}").into()),
115    }
116}
117
118fn load_lin(
119    e: &Engine,
120    g: &GgufFile,
121    name: &str,
122    in_f: usize,
123    out_f: usize,
124) -> Result<GLin, Box<dyn std::error::Error>> {
125    let w = read_f32(g, name)?;
126    assert_eq!(w.len(), in_f * out_f, "{name} shape");
127    Ok(GLin {
128        w: e.htod(&w)?,
129        in_f,
130        out_f,
131    })
132}
133
134/// The dyn-size resize law (llama.cpp `calc_size_preserved_ratio`, "smart_resize"):
135/// round each side to the 48 grid, then rescale into the [min,max] pixel budget with
136/// floor/ceil-to-48 — identical arithmetic, so the grid memra feeds the tower is the
137/// grid the reference implementation would feed it.
138pub fn gemma_target_size(w: u32, h: u32) -> (u32, u32) {
139    let align = GV_ALIGN as f32;
140    let min_px = (GV_MIN_TOKENS * GV_ALIGN * GV_ALIGN) as f32;
141    let max_px = (GV_MAX_TOKENS * GV_ALIGN * GV_ALIGN) as f32;
142    let round = |x: f32| ((x / align).round() * align).max(align) as u32;
143    let ceilf = |x: f32| ((x / align).ceil() * align).max(align) as u32;
144    let floorf = |x: f32| ((x / align).floor() * align).max(align) as u32;
145    let (wf, hf) = (w as f32, h as f32);
146    let mut w_bar = round(wf);
147    let mut h_bar = round(hf);
148    if (w_bar * h_bar) as f32 > max_px {
149        let beta = (wf * hf / max_px).sqrt();
150        w_bar = floorf(wf / beta);
151        h_bar = floorf(hf / beta);
152    } else if ((w_bar * h_bar) as f32) < min_px {
153        let beta = (min_px / (wf * hf)).sqrt();
154        w_bar = ceilf(wf * beta);
155        h_bar = ceilf(hf * beta);
156    }
157    (w_bar, h_bar)
158}
159
160/// One preprocessed gemma image, server-carried from the HTTP layer to the GPU worker.
161/// `patches` is the tower input (dropped after the tower forward); `n_soft` is the pooled
162/// token count = (gw/3)(gh/3), i.e. the number of `<|image|>` soft tokens the prompt run
163/// must carry for this unit.
164pub struct GemmaVisionUnit {
165    pub patches: Vec<f32>,
166    pub gw: usize,
167    pub gh: usize,
168}
169
170impl GemmaVisionUnit {
171    pub fn n_soft(&self) -> usize {
172        n_soft_for_grid(self.gw, self.gh)
173    }
174}
175
176/// Soft tokens a `(gw, gh)` grid pools to — the planned twin of
177/// `GemmaVisionUnit::n_soft`, usable from `gemma_plan_image` BEFORE any decode.
178pub fn n_soft_for_grid(gw: usize, gh: usize) -> usize {
179    (gw / GV_MERGE) * (gh / GV_MERGE)
180}
181
182/// Decode a base64 `data:` URI to raw image bytes (mirrors vision_pre::decode_data_uri;
183/// http(s) fetch stays off for SSRF). Carries the same per-image raw cap
184/// (`vision_pre::IMG_MAX_RAW_BYTES`), refused by encoded length before any allocation.
185pub fn gemma_decode_data_uri(uri: &str) -> Result<Vec<u8>, String> {
186    let comma = uri.find(',').ok_or("data URI has no comma")?;
187    let meta = &uri[..comma];
188    let body = &uri[comma + 1..];
189    if !meta.contains(";base64") {
190        return Err("only base64 data URIs are supported".into());
191    }
192    if let Some(err) = crate::vision_pre::data_uri_payload_over_cap(body) {
193        return Err(err);
194    }
195    use base64::Engine as _;
196    base64::engine::general_purpose::STANDARD
197        .decode(body.as_bytes())
198        .map_err(|e| format!("base64 decode: {e}"))
199}
200
201/// Prep a data-URI image into a GemmaVisionUnit (decode + smart-resize + patchify).
202pub fn gemma_prep_data_uri(uri: &str) -> Result<GemmaVisionUnit, String> {
203    let bytes = gemma_decode_data_uri(uri)?;
204    let (patches, gw, gh) = gemma_prep_image(&bytes).map_err(|e| e.to_string())?;
205    Ok(GemmaVisionUnit { patches, gw, gh })
206}
207
208/// PRE-DECODE admission (hermes decode-bomb finding, fixed 2026-08-23 — same law as
209/// `vision_pre::plan_image_bytes`): header dims -> decode-budget check -> target grid.
210/// Returns `(gw, gh)` so `n_soft` (thus the pad run and the request's token price) is
211/// known before any canvas expands.
212pub fn gemma_plan_image(bytes: &[u8]) -> Result<(usize, usize), String> {
213    let (w, h) = crate::vision_pre::image_header_dims(bytes)?;
214    if w.saturating_mul(h) > crate::vision_pre::IMG_MAX_DECODE_PIXELS {
215        return Err(format!(
216            "image {w}x{h} exceeds the decode budget ({} px) — refused before decode",
217            crate::vision_pre::IMG_MAX_DECODE_PIXELS
218        ));
219    }
220    let (tw, th) = gemma_target_size(w as u32, h as u32);
221    Ok(((tw as usize) / GV_PATCH, (th as usize) / GV_PATCH))
222}
223
224/// Decode + resize + patchify one image: bytes -> (patch rows [n, 768] in the conv's
225/// (c, ky, kx) flat order with the graph's 2x-1 scaling baked in, grid_w, grid_h).
226/// Admission runs FIRST (`gemma_plan_image`, header-only); the decoder is capped to the
227/// admitted dimensions so a header lying small cannot expand past them.
228pub fn gemma_prep_image(
229    bytes: &[u8],
230) -> Result<(Vec<f32>, usize, usize), Box<dyn std::error::Error>> {
231    gemma_plan_image(bytes)?;
232    let (hw, hh) = crate::vision_pre::image_header_dims(bytes)?;
233    let mut reader = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format()?;
234    let mut limits = image::Limits::default();
235    limits.max_image_width = Some(hw as u32);
236    limits.max_image_height = Some(hh as u32);
237    reader.limits(limits);
238    let img = reader.decode()?.to_rgb8();
239    let (w0, h0) = img.dimensions();
240    let (tw, th) = gemma_target_size(w0, h0);
241    // Bilinear per the reference (RESIZE_ALGO_BILINEAR); image's Triangle filter is the
242    // bilinear kernel. Resize kernels are allowed to differ by a hair from llama.cpp's
243    // own bilinear — the parity oracle feeds FIXED pixels so the tower is gated
244    // independently of resampling.
245    let resized = image::imageops::resize(&img, tw, th, image::imageops::FilterType::Triangle);
246    let (gw, gh) = ((tw as usize) / GV_PATCH, (th as usize) / GV_PATCH);
247    let mut patches = vec![0f32; gw * gh * GV_PATCH_IN];
248    for py in 0..gh {
249        for px in 0..gw {
250            let dst = &mut patches[(py * gw + px) * GV_PATCH_IN..(py * gw + px + 1) * GV_PATCH_IN];
251            for c in 0..3 {
252                for ky in 0..GV_PATCH {
253                    for kx in 0..GV_PATCH {
254                        let p = resized
255                            .get_pixel((px * GV_PATCH + kx) as u32, (py * GV_PATCH + ky) as u32);
256                        // [0,1] then the graph's 2x-1
257                        dst[(c * GV_PATCH + ky) * GV_PATCH + kx] =
258                            (p[c] as f32) / 255.0 * 2.0 - 1.0;
259                    }
260                }
261            }
262        }
263    }
264    Ok((patches, gw, gh))
265}
266
267impl GemmaVisionTower {
268    /// Load the tower from a gemma-4 mmproj GGUF (`general.type = mmproj`,
269    /// `clip.vision.projector_type = gemma4v`).
270    pub fn load(e: &Engine, path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
271        let g = GgufFile::open(path)?;
272        let proj_type = g
273            .metadata
274            .get("clip.vision.projector_type")
275            .and_then(|v| v.as_str())
276            .unwrap_or_default();
277        if proj_type != "gemma4v" {
278            return Err(format!(
279                "mmproj {path:?} projector_type {proj_type:?} is not gemma4v — this loader \
280                 refuses other families by design (no generic support claims)",
281                path = path
282            )
283            .into());
284        }
285        let patch_w = {
286            // conv weight logical [1152, 3, 16, 16] row-major == Linear rows over the
287            // (c, ky, kx) patch order gemma_prep_image emits.
288            let w = read_f32(&g, "v.patch_embd.weight")?;
289            assert_eq!(
290                w.len(),
291                GV_HIDDEN * GV_PATCH_IN,
292                "v.patch_embd.weight shape"
293            );
294            GLin {
295                w: e.htod(&w)?,
296                in_f: GV_PATCH_IN,
297                out_f: GV_HIDDEN,
298            }
299        };
300        let pos = read_f32(&g, "v.position_embd.weight")?;
301        assert_eq!(
302            pos.len(),
303            2 * GV_POS_ROWS * GV_HIDDEN,
304            "position table shape"
305        );
306        let (pos_x, pos_y) = {
307            let half = GV_POS_ROWS * GV_HIDDEN;
308            (pos[..half].to_vec(), pos[half..].to_vec())
309        };
310        let mut blocks = Vec::with_capacity(GV_DEPTH);
311        for il in 0..GV_DEPTH {
312            let bp = format!("v.blk.{il}");
313            blocks.push(GBlock {
314                ln1: e.htod(&read_f32(&g, &format!("{bp}.ln1.weight"))?)?,
315                ln2: e.htod(&read_f32(&g, &format!("{bp}.ln2.weight"))?)?,
316                attn_post: e.htod(&read_f32(&g, &format!("{bp}.attn_post_norm.weight"))?)?,
317                ffn_post: e.htod(&read_f32(&g, &format!("{bp}.ffn_post_norm.weight"))?)?,
318                q_norm: read_f32(&g, &format!("{bp}.attn_q_norm.weight"))?,
319                k_norm: read_f32(&g, &format!("{bp}.attn_k_norm.weight"))?,
320                wq: load_lin(e, &g, &format!("{bp}.attn_q.weight"), GV_HIDDEN, GV_HIDDEN)?,
321                wk: load_lin(e, &g, &format!("{bp}.attn_k.weight"), GV_HIDDEN, GV_HIDDEN)?,
322                wv: load_lin(e, &g, &format!("{bp}.attn_v.weight"), GV_HIDDEN, GV_HIDDEN)?,
323                wo: load_lin(
324                    e,
325                    &g,
326                    &format!("{bp}.attn_out.weight"),
327                    GV_HIDDEN,
328                    GV_HIDDEN,
329                )?,
330                gate: load_lin(e, &g, &format!("{bp}.ffn_gate.weight"), GV_HIDDEN, GV_INTER)?,
331                up: load_lin(e, &g, &format!("{bp}.ffn_up.weight"), GV_HIDDEN, GV_INTER)?,
332                down: load_lin(e, &g, &format!("{bp}.ffn_down.weight"), GV_INTER, GV_HIDDEN)?,
333            });
334        }
335        let std_bias = read_f32(&g, "v.std_bias")?;
336        let std_scale = read_f32(&g, "v.std_scale")?;
337        assert_eq!(std_bias.len(), GV_HIDDEN);
338        assert_eq!(std_scale.len(), GV_HIDDEN);
339        let proj = load_lin(e, &g, "mm.input_projection.weight", GV_HIDDEN, GV_OUT)?;
340        eprintln!(
341            "[gemma-vision] tower loaded from {} ({GV_DEPTH} blocks, f32-resident)",
342            path.display()
343        );
344        Ok(Self {
345            patch_w,
346            pos_x,
347            pos_y,
348            blocks,
349            std_bias,
350            std_scale,
351            proj,
352        })
353    }
354
355    fn linear(
356        &self,
357        e: &Engine,
358        x: &CudaSlice<f32>,
359        l: &GLin,
360        m: usize,
361    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
362        e.linear(x, &l.w, m, l.in_f, l.out_f)
363    }
364
365    /// Forward one image's patch rows -> [gh*gw/9, 5376] embeddings (device).
366    pub fn forward(
367        &self,
368        e: &Engine,
369        patches: &[f32],
370        gw: usize,
371        gh: usize,
372    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
373        let n = gw * gh;
374        assert_eq!(patches.len(), n * GV_PATCH_IN, "patch buffer shape");
375        assert_eq!(gw % GV_MERGE, 0, "grid width must be 3-aligned");
376        assert_eq!(gh % GV_MERGE, 0, "grid height must be 3-aligned");
377        if n > 12288 {
378            return Err(format!(
379                "gemma vision segment {n} patches exceeds the sdpa shared-memory ceiling (12288)"
380            )
381            .into());
382        }
383        let dbg = std::env::var("MEMRA_VISION_DEBUG").ok();
384        let dump = |tag: &str, buf: &[f32]| {
385            if let Some(dir) = dbg.as_deref() {
386                let raw: Vec<u8> = buf.iter().flat_map(|v| v.to_le_bytes()).collect();
387                let _ = std::fs::write(format!("{dir}/rust_{tag}.bin"), raw);
388            }
389        };
390
391        // patch embed + factored additive position tables (row-major grid: x = col, y = row)
392        let xd = e.htod(patches)?;
393        let embedded = self.linear(e, &xd, &self.patch_w, n)?;
394        let mut pos = vec![0f32; n * GV_HIDDEN];
395        for t in 0..n {
396            let (py, px) = (t / gw, t % gw);
397            let dst = &mut pos[t * GV_HIDDEN..(t + 1) * GV_HIDDEN];
398            let tx = &self.pos_x[px * GV_HIDDEN..(px + 1) * GV_HIDDEN];
399            let ty = &self.pos_y[py * GV_HIDDEN..(py + 1) * GV_HIDDEN];
400            for c in 0..GV_HIDDEN {
401                dst[c] = tx[c] + ty[c];
402            }
403        }
404        let pos_d = e.htod(&pos)?;
405        let mut x = e.zeros(n * GV_HIDDEN)?;
406        e.add(&embedded, &pos_d, &mut x, n * GV_HIDDEN)?;
407        if dbg.is_some() {
408            dump("pre_blocks", &e.dtoh(&x)?);
409        }
410
411        // 2D rope tables: FIRST 36 dims rotate by pos_x, LAST 36 by pos_y (the reference
412        // order — opposite of qwen3_5's y-first); neox pairing (d, d+18) inside each
413        // half; inv_freq[i] = theta^(-2i/36), theta 100.
414        let half = GV_HEAD_DIM / 2; // 36
415        let quarter = half / 2; // 18
416        let inv_freq: Vec<f32> = (0..quarter)
417            .map(|i| ROPE_THETA.powf(-2.0 * (i as f32) / half as f32))
418            .collect();
419        let mut cos_x = vec![0f32; n * quarter];
420        let mut sin_x = vec![0f32; n * quarter];
421        let mut cos_y = vec![0f32; n * quarter];
422        let mut sin_y = vec![0f32; n * quarter];
423        for t in 0..n {
424            let (py, px) = (t / gw, t % gw);
425            for i in 0..quarter {
426                let ax = px as f32 * inv_freq[i];
427                let ay = py as f32 * inv_freq[i];
428                cos_x[t * quarter + i] = ax.cos();
429                sin_x[t * quarter + i] = ax.sin();
430                cos_y[t * quarter + i] = ay.cos();
431                sin_y[t * quarter + i] = ay.sin();
432            }
433        }
434        // per-head RMS over head_dim with weight (q/k) or weightless (v)
435        let head_rms = |row: &mut [f32], w: Option<&[f32]>| {
436            let mut ss = 0f32;
437            for v in row.iter() {
438                ss += v * v;
439            }
440            let inv = 1.0 / (ss / GV_HEAD_DIM as f32 + RMS_EPS).sqrt();
441            for (d, v) in row.iter_mut().enumerate() {
442                *v *= inv * w.map_or(1.0, |w| w[d]);
443            }
444        };
445
446        for (ib, blk) in self.blocks.iter().enumerate() {
447            // attn: rms(ln1) -> q/k/v -> per-head norms -> 2D rope -> sdpa(scale=1) ->
448            //       o_proj -> rms(attn_post) -> +residual
449            let mut h = e.zeros(n * GV_HIDDEN)?;
450            e.rms_norm(&x, &blk.ln1, &mut h, GV_HIDDEN, n, RMS_EPS)?;
451            let q = self.linear(e, &h, &blk.wq, n)?;
452            let k = self.linear(e, &h, &blk.wk, n)?;
453            let v = self.linear(e, &h, &blk.wv, n)?;
454            let (mut qh, mut kh, mut vh) = (e.dtoh(&q)?, e.dtoh(&k)?, e.dtoh(&v)?);
455            for t in 0..n {
456                for hd in 0..GV_HEADS {
457                    let o = t * GV_HIDDEN + hd * GV_HEAD_DIM;
458                    head_rms(&mut qh[o..o + GV_HEAD_DIM], Some(&blk.q_norm));
459                    head_rms(&mut kh[o..o + GV_HEAD_DIM], Some(&blk.k_norm));
460                    head_rms(&mut vh[o..o + GV_HEAD_DIM], None);
461                    // rope: first half by x, second half by y, pairs (d, d+18) per half
462                    for (base, cos, sin) in [(0, &cos_x, &sin_x), (half, &cos_y, &sin_y)] {
463                        for i in 0..quarter {
464                            let (c, s) = (cos[t * quarter + i], sin[t * quarter + i]);
465                            for buf in [&mut qh, &mut kh] {
466                                let a = buf[o + base + i];
467                                let b = buf[o + base + i + quarter];
468                                buf[o + base + i] = a * c - b * s;
469                                buf[o + base + i + quarter] = b * c + a * s;
470                            }
471                        }
472                    }
473                }
474            }
475            let (qd, kd, vd) = (e.htod(&qh)?, e.htod(&kh)?, e.htod(&vh)?);
476            let mut od = e.zeros(n * GV_HIDDEN)?;
477            // UNSCALED attention (kq_scale = 1.0 in the reference graph), full/non-causal.
478            e.sdpa_naive(
479                &qd,
480                &kd,
481                &vd,
482                &mut od,
483                GV_HEAD_DIM,
484                GV_HEADS,
485                GV_HEADS,
486                n,
487                n,
488                1.0,
489                false,
490            )?;
491            let attn = self.linear(e, &od, &blk.wo, n)?;
492            let mut post = e.zeros(n * GV_HIDDEN)?;
493            e.rms_norm(&attn, &blk.attn_post, &mut post, GV_HIDDEN, n, RMS_EPS)?;
494            let mut xr = e.zeros(n * GV_HIDDEN)?;
495            e.add(&x, &post, &mut xr, n * GV_HIDDEN)?;
496
497            // ffn: rms(ln2) -> gelu_quick(gate) * up -> down -> rms(ffn_post) -> +residual
498            let mut h2 = e.zeros(n * GV_HIDDEN)?;
499            e.rms_norm(&xr, &blk.ln2, &mut h2, GV_HIDDEN, n, RMS_EPS)?;
500            let gate = self.linear(e, &h2, &blk.gate, n)?;
501            let up = self.linear(e, &h2, &blk.up, n)?;
502            let (gh_, uh) = (e.dtoh(&gate)?, e.dtoh(&up)?);
503            let mut act = vec![0f32; n * GV_INTER];
504            for i in 0..n * GV_INTER {
505                let g = gh_[i];
506                // gelu_quick(x) = x * sigmoid(1.702 x) — NOT the tanh approximation.
507                act[i] = g / (1.0 + (-1.702 * g).exp()) * uh[i];
508            }
509            let ad = e.htod(&act)?;
510            let down = self.linear(e, &ad, &blk.down, n)?;
511            let mut fpost = e.zeros(n * GV_HIDDEN)?;
512            e.rms_norm(&down, &blk.ffn_post, &mut fpost, GV_HIDDEN, n, RMS_EPS)?;
513            let mut xn = e.zeros(n * GV_HIDDEN)?;
514            e.add(&xr, &fpost, &mut xn, n * GV_HIDDEN)?;
515            x = xn;
516            if dbg.is_some() && ib == 0 {
517                dump("blk0", &e.dtoh(&x)?);
518            }
519        }
520        if dbg.is_some() {
521            dump("post_blocks", &e.dtoh(&x)?);
522        }
523
524        // head: 3x3 avg-pool over the grid -> *sqrt(1152) -> (x - std_bias)*std_scale
525        //       -> weightless RMS -> project 1152 -> 5376
526        let xh = e.dtoh(&x)?;
527        let (mw, mh) = (gw / GV_MERGE, gh / GV_MERGE);
528        let nm = mw * mh;
529        let scale = (GV_HIDDEN as f32).sqrt();
530        let mut pooled = vec![0f32; nm * GV_HIDDEN];
531        for my in 0..mh {
532            for mx in 0..mw {
533                let dst = &mut pooled[(my * mw + mx) * GV_HIDDEN..(my * mw + mx + 1) * GV_HIDDEN];
534                for sy in 0..GV_MERGE {
535                    for sx in 0..GV_MERGE {
536                        let t = (my * GV_MERGE + sy) * gw + (mx * GV_MERGE + sx);
537                        for c in 0..GV_HIDDEN {
538                            dst[c] += xh[t * GV_HIDDEN + c];
539                        }
540                    }
541                }
542                for (c, d) in dst.iter_mut().enumerate() {
543                    *d = (*d / (GV_MERGE * GV_MERGE) as f32 * scale - self.std_bias[c])
544                        * self.std_scale[c];
545                }
546            }
547        }
548        // weightless RMS then projection
549        for row in pooled.chunks_exact_mut(GV_HIDDEN) {
550            let mut ss = 0f32;
551            for v in row.iter() {
552                ss += v * v;
553            }
554            let inv = 1.0 / (ss / GV_HIDDEN as f32 + RMS_EPS).sqrt();
555            for v in row.iter_mut() {
556                *v *= inv;
557            }
558        }
559        if dbg.is_some() {
560            dump("pre_proj", &pooled);
561        }
562        let pd = e.htod(&pooled)?;
563        let out = self.linear(e, &pd, &self.proj, nm)?;
564        if dbg.is_some() {
565            dump("projected", &e.dtoh(&out)?);
566        }
567        Ok(out)
568    }
569}