1use 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; pub const GV_INTER: usize = 4304;
47pub const GV_DEPTH: usize = 27;
48pub const GV_PATCH: usize = 16;
49pub const GV_MERGE: usize = 3; pub const GV_POS_ROWS: usize = 10240; pub const GV_OUT: usize = 5376; pub const GV_PATCH_IN: usize = 3 * GV_PATCH * GV_PATCH; pub const GV_ALIGN: usize = GV_PATCH * GV_MERGE; pub 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
61pub const GV_TOK_BEGIN: u32 = 255999; pub const GV_TOK_SOFT: u32 = 258880; pub const GV_TOK_END: u32 = 258882; struct 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, 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, }
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
134pub 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
160pub 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
176pub fn n_soft_for_grid(gw: usize, gh: usize) -> usize {
179 (gw / GV_MERGE) * (gh / GV_MERGE)
180}
181
182pub 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
201pub 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
208pub 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
224pub 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 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 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 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 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 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 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 let half = GV_HEAD_DIM / 2; let quarter = half / 2; 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 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 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 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 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 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 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 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 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}