1use crate::Pipeline;
48use crate::media::{self, RgbFrame};
49use crate::mimo_vision::{MimoProcessorConfig, VisualInput, VisualKind};
50use crate::pool::Pool;
51use cortiq_core::CmfModel;
52use serde_json::{Value, json};
53use std::sync::Arc;
54
55pub const PATCH: &str = "visual.embeddings.patch_embedding.weight";
57
58#[derive(Clone, Debug)]
60pub struct Ov2Config {
61 pub hidden: usize,
62 pub heads: usize,
63 pub head_dim: usize,
64 pub layers: usize,
65 pub intermediate: usize,
66 pub patch_size: usize,
67 pub merge_size: usize,
68 pub temporal_patch_size: usize,
69 pub eps: f64,
70 pub rope_theta: f64,
71 pub text_hidden: usize,
72 pub image_token_id: u32,
73 pub min_pixels: usize,
74 pub max_pixels: usize,
75 pub mean: [f32; 3],
76 pub std: [f32; 3],
77 pub rescale: f32,
78}
79
80pub fn has_vision(model: &CmfModel) -> bool {
82 model.header.arch.arch_name == "llava_onevision2" && model.tensor(PATCH).is_some()
83}
84
85fn need_usize(v: &Value, key: &str) -> Result<usize, String> {
86 v.get(key)
87 .and_then(Value::as_u64)
88 .map(|n| n as usize)
89 .ok_or_else(|| format!("onevision2 config: missing '{key}'"))
90}
91
92fn triple(v: &Value, key: &str) -> Result<[f32; 3], String> {
93 let a = v
94 .get(key)
95 .and_then(Value::as_array)
96 .filter(|a| a.len() == 3)
97 .ok_or_else(|| format!("onevision2 processor: '{key}' must hold 3 numbers"))?;
98 let mut out = [0f32; 3];
99 for (o, x) in out.iter_mut().zip(a) {
100 *o = x
101 .as_f64()
102 .ok_or_else(|| format!("onevision2 processor: '{key}' must hold numbers"))? as f32;
103 }
104 Ok(out)
105}
106
107impl Ov2Config {
108 pub fn from_model(model: &CmfModel) -> Result<Self, String> {
109 let p = model
110 .header
111 .provenance
112 .as_ref()
113 .and_then(|p| p.get("onevision2"))
114 .ok_or("onevision2: the file carries no provenance.onevision2 (reconvert it)")?;
115 let cfg = p.get("config").ok_or("onevision2: provenance has no config")?;
116 let processor = p.get("processor").ok_or("onevision2: provenance has no processor")?;
117 Self::from_json(cfg, processor)
118 }
119
120 pub fn from_json(cfg: &Value, processor: &Value) -> Result<Self, String> {
121 let vc = cfg.get("vision_config").ok_or("onevision2: config has no vision_config")?;
122 let tc = cfg.get("text_config").ok_or("onevision2: config has no text_config")?;
123 let hidden = need_usize(vc, "hidden_size")?;
124 let heads = need_usize(vc, "num_attention_heads")?;
125 if heads == 0 || hidden % heads != 0 || (hidden / heads) % 32 != 0 {
126 return Err("onevision2: head_dim must split 4:6:6 into whole frequencies".into());
127 }
128 let norm = vc.get("layer_norm_type").and_then(Value::as_str).unwrap_or("layer_norm");
129 if norm != "layer_norm" || vc.get("use_head").and_then(Value::as_bool).unwrap_or(false) {
130 return Err("onevision2: only the LayerNorm tower without a pooling head is supported".into());
131 }
132 let act = vc.get("hidden_act").and_then(Value::as_str).unwrap_or("gelu");
133 if act != "gelu" {
134 return Err(format!("onevision2: unsupported tower activation '{act}'"));
135 }
136 let c = Ov2Config {
137 hidden,
138 heads,
139 head_dim: hidden / heads,
140 layers: need_usize(vc, "num_hidden_layers")?,
141 intermediate: need_usize(vc, "intermediate_size")?,
142 patch_size: need_usize(vc, "patch_size")?,
143 merge_size: need_usize(vc, "spatial_merge_size")?,
144 temporal_patch_size: need_usize(vc, "temporal_patch_size")?,
145 eps: vc.get("layer_norm_eps").and_then(Value::as_f64).unwrap_or(1e-6),
146 rope_theta: vc.get("rope_theta").and_then(Value::as_f64).unwrap_or(10000.0),
147 text_hidden: need_usize(tc, "hidden_size")?,
148 image_token_id: need_usize(cfg, "image_token_id")? as u32,
149 min_pixels: processor
150 .get("min_pixels")
151 .and_then(Value::as_u64)
152 .or_else(|| processor.pointer("/size/shortest_edge").and_then(Value::as_u64))
153 .ok_or("onevision2 processor: missing min_pixels")? as usize,
154 max_pixels: processor
155 .get("max_pixels")
156 .and_then(Value::as_u64)
157 .or_else(|| processor.pointer("/size/longest_edge").and_then(Value::as_u64))
158 .ok_or("onevision2 processor: missing max_pixels")? as usize,
159 mean: triple(processor, "image_mean")?,
160 std: triple(processor, "image_std")?,
161 rescale: processor
162 .get("rescale_factor")
163 .and_then(Value::as_f64)
164 .unwrap_or(1.0 / 255.0) as f32,
165 };
166 if c.temporal_patch_size != 1 {
167 return Err("onevision2: only temporal_patch_size 1 (still images) is supported".into());
168 }
169 if need_usize(processor, "patch_size")? != c.patch_size
170 || need_usize(processor, "merge_size")? != c.merge_size
171 {
172 return Err("onevision2: processor and tower patch geometry disagree".into());
173 }
174 Ok(c)
175 }
176
177 fn patch_cfg(&self) -> MimoProcessorConfig {
178 MimoProcessorConfig {
179 patch_size: self.patch_size,
180 merge_size: self.merge_size,
181 temporal_patch_size: self.temporal_patch_size,
182 ..Default::default()
183 }
184 }
185
186 fn patch_dim(&self) -> usize {
187 3 * self.temporal_patch_size * self.patch_size * self.patch_size
188 }
189}
190
191pub fn prepare_image(
194 frame: &RgbFrame,
195 cfg: &Ov2Config,
196 max_pixels: Option<usize>,
197) -> Result<VisualInput, String> {
198 let factor = cfg.patch_size * cfg.merge_size;
199 let max_px = max_pixels.map_or(cfg.max_pixels, |m| m.min(cfg.max_pixels));
200 let (hb, wb) = crate::lfm3_vl::smart_resize(frame.height, frame.width, factor, cfg.min_pixels, max_px)?;
201 let resized = if (hb, wb) == (frame.height, frame.width) {
202 frame.clone()
203 } else {
204 crate::egemma2_vision::resize_bicubic_aa(frame, hb, wb)
205 };
206 let plane = hb * wb;
207 let mut chw = vec![0f32; 3 * plane];
208 for c in 0..3 {
209 let m = cfg.mean[c] / cfg.rescale;
210 let s = cfg.std[c] / cfg.rescale;
211 for (i, px) in resized.data.chunks_exact(3).enumerate() {
212 chw[c * plane + i] = (px[c] as f32 - m) / s;
213 }
214 }
215 let (rows, grid_t, grid_h, grid_w) = crate::mimo_vision::patchify(&[chw.as_slice()], hb, wb, &cfg.patch_cfg())?;
216 Ok(VisualInput {
217 kind: VisualKind::Image,
218 rows,
219 grid_t,
220 grid_h,
221 grid_w,
222 patch_dim: cfg.patch_dim(),
223 merge_size: cfg.merge_size,
224 timestamps: Vec::new(),
225 frame_indices: Vec::new(),
226 resized: (hb, wb),
227 })
228}
229
230pub(crate) struct Mat {
233 w: Vec<f32>,
234 rows: usize,
235 cols: usize,
236}
237
238impl Mat {
239 pub(crate) fn load(model: &Arc<CmfModel>, name: &str, rows: usize, cols: usize) -> Result<Self, String> {
240 let w = crate::dit::cmf_f32(model, name)?;
241 if w.len() != rows * cols {
242 return Err(format!("tensor '{name}' has {} values, expected [{rows}, {cols}]", w.len()));
243 }
244 Ok(Mat { w, rows, cols })
245 }
246
247 pub(crate) fn matmat(&self, x: &[f32], n: usize, bias: Option<&[f32]>, y: &mut [f32], pool: Option<&Pool>) {
249 let (k, m) = (self.cols, self.rows);
250 if device_gemm_on() && crate::gpu::vision_gemm_nt(x, &self.w, y, n, k, m) {
251 GEMM_DEV.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
252 } else {
253 GEMM_HOST.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
254 crate::fcd_ops::gemm_nt_host(x, &self.w, y, n, k, m, pool);
255 }
256 if let Some(b) = bias {
257 add_bias(y, b, pool);
258 }
259 }
260}
261
262pub(crate) static GEMM_DEV: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
264pub(crate) static GEMM_HOST: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
265
266fn device_gemm_on() -> bool {
268 std::env::var("CMF_OV2_GPU").as_deref() != Ok("0") && crate::gpu::enabled_here()
269}
270
271fn add_bias(y: &mut [f32], b: &[f32], pool: Option<&Pool>) {
272 let d = b.len();
273 let rows = y.len() / d;
274 let dst = crate::pool::SendMut::new(y.as_mut_ptr());
275 let f = |lo: usize, hi: usize| {
276 for r in lo..hi {
277 let row = unsafe { std::slice::from_raw_parts_mut(dst.at(r * d), d) };
279 for (v, &bb) in row.iter_mut().zip(b) {
280 *v += bb;
281 }
282 }
283 };
284 match pool {
285 Some(p) => p.run_rows(rows, &f),
286 None => f(0, rows),
287 }
288}
289
290pub(crate) fn layer_norm_rows(x: &[f32], w: &[f32], b: &[f32], eps: f64, out: &mut [f32], pool: Option<&Pool>) {
292 let d = w.len();
293 let rows = x.len() / d;
294 let dst = crate::pool::SendMut::new(out.as_mut_ptr());
295 let f = |lo: usize, hi: usize| {
296 for r in lo..hi {
297 let src = &x[r * d..(r + 1) * d];
298 let o = unsafe { std::slice::from_raw_parts_mut(dst.at(r * d), d) };
300 let mean = src.iter().map(|&v| v as f64).sum::<f64>() / d as f64;
301 let var = src.iter().map(|&v| (v as f64 - mean).powi(2)).sum::<f64>() / d as f64;
302 let inv = 1.0 / (var + eps).sqrt();
303 for i in 0..d {
304 o[i] = ((src[i] as f64 - mean) * inv) as f32 * w[i] + b[i];
305 }
306 }
307 };
308 match pool {
309 Some(p) => p.run_rows(rows, &f),
310 None => f(0, rows),
311 }
312}
313
314fn erf(x: f32) -> f32 {
315 crate::lfm3_vl::erf(x as f64) as f32
316}
317
318pub(crate) fn gelu_rows(y: &mut [f32], pool: Option<&Pool>) {
320 let n = y.len();
321 let chunk = 1 << 14;
322 let dst = crate::pool::SendMut::new(y.as_mut_ptr());
323 let f = |lo: usize, hi: usize| {
324 for c in lo..hi {
325 let a = c * chunk;
326 let b = (a + chunk).min(n);
327 let s = unsafe { std::slice::from_raw_parts_mut(dst.at(a), b - a) };
329 for v in s {
330 *v = 0.5 * *v * (1.0 + erf(*v * std::f32::consts::FRAC_1_SQRT_2));
331 }
332 }
333 };
334 let chunks = n.div_ceil(chunk);
335 match pool {
336 Some(p) => p.run_rows(chunks, &f),
337 None => f(0, chunks),
338 }
339}
340
341struct Layer {
342 ln1_w: Vec<f32>,
343 ln1_b: Vec<f32>,
344 qkv: Mat,
345 qkv_b: Vec<f32>,
346 proj: Mat,
347 proj_b: Vec<f32>,
348 ln2_w: Vec<f32>,
349 ln2_b: Vec<f32>,
350 fc1: Mat,
351 fc1_b: Vec<f32>,
352 fc2: Mat,
353 fc2_b: Vec<f32>,
354}
355
356pub struct Ov2Tower {
358 pub cfg: Ov2Config,
359 patch: Mat,
360 ln_pre_w: Vec<f32>,
361 ln_pre_b: Vec<f32>,
362 layers: Vec<Layer>,
363 ln_q_w: Vec<f32>,
364 ln_q_b: Vec<f32>,
365 mlp0: Mat,
366 mlp0_b: Vec<f32>,
367 mlp2: Mat,
368 mlp2_b: Vec<f32>,
369 pool: Option<Arc<Pool>>,
370}
371
372fn vec_of(model: &Arc<CmfModel>, name: &str, len: usize) -> Result<Vec<f32>, String> {
373 let v = crate::dit::cmf_f32(model, name)?;
374 if v.len() != len {
375 return Err(format!("tensor '{name}' has {} values, expected {len}", v.len()));
376 }
377 Ok(v)
378}
379
380impl Ov2Tower {
381 pub fn from_model(model: &Arc<CmfModel>, pool: Option<Arc<Pool>>) -> Result<Self, String> {
382 let cfg = Ov2Config::from_model(model)?;
383 let (d, inter) = (cfg.hidden, cfg.intermediate);
384 let merged = d * cfg.merge_size * cfg.merge_size;
385 let mut layers = Vec::with_capacity(cfg.layers);
386 for i in 0..cfg.layers {
387 let p = format!("visual.encoder.layers.{i}");
388 layers.push(Layer {
389 ln1_w: vec_of(model, &format!("{p}.layer_norm1.weight"), d)?,
390 ln1_b: vec_of(model, &format!("{p}.layer_norm1.bias"), d)?,
391 qkv: Mat::load(model, &format!("{p}.self_attn.qkv.weight"), 3 * d, d)?,
392 qkv_b: vec_of(model, &format!("{p}.self_attn.qkv.bias"), 3 * d)?,
393 proj: Mat::load(model, &format!("{p}.self_attn.proj.weight"), d, d)?,
394 proj_b: vec_of(model, &format!("{p}.self_attn.proj.bias"), d)?,
395 ln2_w: vec_of(model, &format!("{p}.layer_norm2.weight"), d)?,
396 ln2_b: vec_of(model, &format!("{p}.layer_norm2.bias"), d)?,
397 fc1: Mat::load(model, &format!("{p}.mlp.fc1.weight"), inter, d)?,
398 fc1_b: vec_of(model, &format!("{p}.mlp.fc1.bias"), inter)?,
399 fc2: Mat::load(model, &format!("{p}.mlp.fc2.weight"), d, inter)?,
400 fc2_b: vec_of(model, &format!("{p}.mlp.fc2.bias"), d)?,
401 });
402 }
403 Ok(Ov2Tower {
404 patch: Mat::load(model, PATCH, d, cfg.patch_dim())?,
405 ln_pre_w: vec_of(model, "visual.layernorm_pre.weight", d)?,
406 ln_pre_b: vec_of(model, "visual.layernorm_pre.bias", d)?,
407 layers,
408 ln_q_w: vec_of(model, "visual.merger.ln_q.weight", d)?,
409 ln_q_b: vec_of(model, "visual.merger.ln_q.bias", d)?,
410 mlp0: Mat::load(model, "visual.merger.mlp.0.weight", merged, merged)?,
411 mlp0_b: vec_of(model, "visual.merger.mlp.0.bias", merged)?,
412 mlp2: Mat::load(model, "visual.merger.mlp.2.weight", cfg.text_hidden, merged)?,
413 mlp2_b: vec_of(model, "visual.merger.mlp.2.bias", cfg.text_hidden)?,
414 cfg,
415 pool,
416 })
417 }
418
419 fn rope_tables(&self, gt: usize, gh: usize, gw: usize) -> (Vec<f32>, Vec<f32>) {
422 let c = &self.cfg;
423 let hd = c.head_dim;
424 let half = hd / 2;
425 let unit = half / 16;
426 let (nt, nh, nw) = (4 * unit, 6 * unit, 6 * unit);
427 let inv = |n: usize| -> Vec<f32> {
428 (0..n)
429 .map(|i| (1.0 / c.rope_theta.powf(i as f64 / n as f64)) as f32)
430 .collect()
431 };
432 let (it, ih, iw) = (inv(nt), inv(nh), inv(nw));
433 let m = c.merge_size;
434 let n = gt * gh * gw;
435 let (mut cos, mut sin) = (vec![0f32; n * hd], vec![0f32; n * hd]);
436 let mut k = 0usize;
437 let mut ang = vec![0f32; half];
438 for t in 0..gt {
439 for a in 0..gh / m {
440 for b in 0..gw / m {
441 for mh in 0..m {
442 for mw in 0..m {
443 let (r, col) = ((a * m + mh) as f32, (b * m + mw) as f32);
444 for (j, &f) in it.iter().enumerate() {
446 ang[j] = t as f32 * f;
447 }
448 for (j, &f) in ih.iter().enumerate() {
449 ang[nt + j] = r * f;
450 }
451 for (j, &f) in iw.iter().enumerate() {
452 ang[nt + nh + j] = col * f;
453 }
454 for j in 0..hd {
455 let (s, co) = ang[j % half].sin_cos();
456 cos[k * hd + j] = co;
457 sin[k * hd + j] = s;
458 }
459 k += 1;
460 }
461 }
462 }
463 }
464 }
465 (cos, sin)
466 }
467
468 fn split_rope(&self, qkv: &[f32], n: usize, cos: &[f32], sin: &[f32], qh: &mut [f32], kh: &mut [f32], vh: &mut [f32]) {
470 let (nh, hd) = (self.cfg.heads, self.cfg.head_dim);
471 let d = nh * hd;
472 let (qp, kp, vp) = (
473 crate::pool::SendMut::new(qh.as_mut_ptr()),
474 crate::pool::SendMut::new(kh.as_mut_ptr()),
475 crate::pool::SendMut::new(vh.as_mut_ptr()),
476 );
477 let f = |lo: usize, hi: usize| {
478 for p in lo..hi {
479 let row = &qkv[p * 3 * d..(p + 1) * 3 * d];
480 let (cs, sn) = (&cos[p * hd..(p + 1) * hd], &sin[p * hd..(p + 1) * hd]);
481 for h in 0..nh {
482 let dst = (h * n + p) * hd;
483 let (q, k, v) = unsafe {
485 (
486 std::slice::from_raw_parts_mut(qp.at(dst), hd),
487 std::slice::from_raw_parts_mut(kp.at(dst), hd),
488 std::slice::from_raw_parts_mut(vp.at(dst), hd),
489 )
490 };
491 let (qs, ks, vs) = (
492 &row[h * hd..(h + 1) * hd],
493 &row[d + h * hd..d + (h + 1) * hd],
494 &row[2 * d + h * hd..2 * d + (h + 1) * hd],
495 );
496 for i in (0..hd).step_by(2) {
497 q[i] = qs[i] * cs[i] - qs[i + 1] * sn[i];
498 q[i + 1] = qs[i + 1] * cs[i + 1] + qs[i] * sn[i + 1];
499 k[i] = ks[i] * cs[i] - ks[i + 1] * sn[i];
500 k[i + 1] = ks[i + 1] * cs[i + 1] + ks[i] * sn[i + 1];
501 }
502 v.copy_from_slice(vs);
503 }
504 }
505 };
506 match self.pool.as_deref() {
507 Some(pool) => pool.run_rows(n, &f),
508 None => f(0, n),
509 }
510 }
511
512 fn attend(&self, qh: &[f32], kh: &[f32], vh: &[f32], n: usize, out: &mut [f32]) {
514 let (nh, hd) = (self.cfg.heads, self.cfg.head_dim);
515 let scale = (hd as f32).powf(-0.5);
516 if device_gemm_on() && n >= 64 && crate::gpu::dit_attention(qh, kh, vh, nh, nh, n, hd, scale, out) {
517 return;
518 }
519 let pool = self.pool.as_deref();
520 let tile = n.min(1024);
521 let mut vt = vec![0f32; hd * n];
522 let mut qs = vec![0f32; tile * hd];
523 let mut sc = vec![0f32; tile * n];
524 let mut oh = vec![0f32; tile * hd];
525 for h in 0..nh {
526 let kp = &kh[h * n * hd..(h + 1) * n * hd];
527 for j in 0..n {
528 for e in 0..hd {
529 vt[e * n + j] = vh[(h * n + j) * hd + e];
530 }
531 }
532 for t0 in (0..n).step_by(tile) {
533 let tq = tile.min(n - t0);
534 for i in 0..tq {
535 for e in 0..hd {
536 qs[i * hd + e] = qh[(h * n + t0 + i) * hd + e] * scale;
537 }
538 }
539 crate::fcd_ops::gemm_nt_host(&qs[..tq * hd], kp, &mut sc[..tq * n], tq, hd, n, pool);
540 let sp = crate::pool::SendMut::new(sc.as_mut_ptr());
541 let f = |lo: usize, hi: usize| {
542 for i in lo..hi {
543 let row = unsafe { std::slice::from_raw_parts_mut(sp.at(i * n), n) };
545 let mx = row.iter().fold(f32::NEG_INFINITY, |m, &x| m.max(x));
546 let mut sum = 0f32;
547 for x in row.iter_mut() {
548 *x = (*x - mx).exp();
549 sum += *x;
550 }
551 let inv = 1.0 / sum;
552 for x in row.iter_mut() {
553 *x *= inv;
554 }
555 }
556 };
557 match pool {
558 Some(p) => p.run_rows(tq, &f),
559 None => f(0, tq),
560 }
561 crate::fcd_ops::gemm_nt_host(&sc[..tq * n], &vt, &mut oh[..tq * hd], tq, n, hd, pool);
562 for i in 0..tq {
563 let r = t0 + i;
564 out[r * nh * hd + h * hd..r * nh * hd + (h + 1) * hd].copy_from_slice(&oh[i * hd..(i + 1) * hd]);
565 }
566 }
567 }
568 }
569
570 pub fn forward(&self, input: &VisualInput) -> Result<Vec<f32>, String> {
573 let c = &self.cfg;
574 let (gt, gh, gw) = (input.grid_t, input.grid_h, input.grid_w);
575 let n = gt * gh * gw;
576 let m2 = c.merge_size * c.merge_size;
577 if input.rows.len() != n * c.patch_dim() || n % m2 != 0 {
578 return Err("onevision2: patch rows do not match the grid".into());
579 }
580 let pool = self.pool.as_deref();
581 let (d, inter) = (c.hidden, c.intermediate);
582 let prof = std::env::var("CMF_OV2_PROF").is_ok();
583 let t0 = std::time::Instant::now();
584 let mut stamps: Vec<(&str, f64)> = Vec::new();
585 let mut stamp = |what: &'static str, t: &mut std::time::Instant| {
586 stamps.push((what, t.elapsed().as_secs_f64()));
587 *t = std::time::Instant::now();
588 };
589 let mut ts = std::time::Instant::now();
590 let mut x = vec![0f32; n * d];
591 self.patch.matmat(&input.rows, n, None, &mut x, pool);
592 stamp("patch", &mut ts);
593 let mut xn = vec![0f32; n * d];
594 layer_norm_rows(&x, &self.ln_pre_w, &self.ln_pre_b, c.eps, &mut xn, pool);
595 std::mem::swap(&mut x, &mut xn);
596 let (cos, sin) = self.rope_tables(gt, gh, gw);
597 stamp("ln_pre+rope", &mut ts);
598 let mut qkv = vec![0f32; n * 3 * d];
599 let (mut qh, mut kh, mut vh) = (vec![0f32; n * d], vec![0f32; n * d], vec![0f32; n * d]);
600 let mut att = vec![0f32; n * d];
601 let mut tmp = vec![0f32; n * d];
602 let mut ff = vec![0f32; n * inter];
603 let (mut t_mm, mut t_att) = (0f64, 0f64);
604 let resident = device_gemm_on() && {
606 let layers = self
607 .layers
608 .iter()
609 .map(|l| crate::gpu::VitLayer {
610 ln1_w: &l.ln1_w,
611 ln1_b: &l.ln1_b,
612 qkv: &l.qkv.w,
613 qkv_b: &l.qkv_b,
614 proj: &l.proj.w,
615 proj_b: &l.proj_b,
616 ln2_w: &l.ln2_w,
617 ln2_b: &l.ln2_b,
618 fc1: &l.fc1.w,
619 fc1_b: &l.fc1_b,
620 fc2: &l.fc2.w,
621 fc2_b: &l.fc2_b,
622 })
623 .collect();
624 let args = crate::gpu::VitArgs {
625 key: self as *const Self as u64,
626 layers,
627 n,
628 d,
629 heads: c.heads,
630 head_dim: c.head_dim,
631 inter,
632 eps: c.eps,
633 gelu_tanh: false,
634 rope_halves: false,
635 cos: &cos,
636 sin: &sin,
637 prof,
638 };
639 crate::gpu::vit_encode(&args, &mut x)
640 };
641 stamp("encoder", &mut ts);
642 for l in self.layers.iter().filter(|_| !resident) {
643 layer_norm_rows(&x, &l.ln1_w, &l.ln1_b, c.eps, &mut xn, pool);
644 let tm = std::time::Instant::now();
645 l.qkv.matmat(&xn, n, Some(&l.qkv_b), &mut qkv, pool);
646 t_mm += tm.elapsed().as_secs_f64();
647 self.split_rope(&qkv, n, &cos, &sin, &mut qh, &mut kh, &mut vh);
648 let ta = std::time::Instant::now();
649 self.attend(&qh, &kh, &vh, n, &mut att);
650 t_att += ta.elapsed().as_secs_f64();
651 let tm = std::time::Instant::now();
652 l.proj.matmat(&att, n, Some(&l.proj_b), &mut tmp, pool);
653 for (a, &b) in x.iter_mut().zip(&tmp) {
654 *a += b;
655 }
656 layer_norm_rows(&x, &l.ln2_w, &l.ln2_b, c.eps, &mut xn, pool);
657 l.fc1.matmat(&xn, n, Some(&l.fc1_b), &mut ff, pool);
658 gelu_rows(&mut ff, pool);
659 l.fc2.matmat(&ff, n, Some(&l.fc2_b), &mut tmp, pool);
660 t_mm += tm.elapsed().as_secs_f64();
661 for (a, &b) in x.iter_mut().zip(&tmp) {
662 *a += b;
663 }
664 }
665 layer_norm_rows(&x, &self.ln_q_w, &self.ln_q_b, c.eps, &mut xn, pool);
666 let nm = n / m2;
667 let mut h1 = vec![0f32; nm * d * m2];
668 self.mlp0.matmat(&xn, nm, Some(&self.mlp0_b), &mut h1, pool);
669 gelu_rows(&mut h1, pool);
670 let mut out = vec![0f32; nm * c.text_hidden];
671 self.mlp2.matmat(&h1, nm, Some(&self.mlp2_b), &mut out, pool);
672 stamp("merger", &mut ts);
673 if prof {
674 eprintln!("onevision2 stages: {}", stamps.iter().map(|(w, s)| format!("{w} {s:.3} s")).collect::<Vec<_>>().join(", "));
675 eprintln!(
676 "onevision2: {n} patches, tower {:.3} s (gemm {:.3} s: {} on the device, {} on the host; attention {:.3} s)",
677 t0.elapsed().as_secs_f64(),
678 t_mm,
679 GEMM_DEV.load(std::sync::atomic::Ordering::Relaxed),
680 GEMM_HOST.load(std::sync::atomic::Ordering::Relaxed),
681 t_att
682 );
683 }
684 Ok(out)
685 }
686}
687
688fn tower_for(model: &Arc<CmfModel>) -> Result<Arc<Ov2Tower>, String> {
692 static TOWER: std::sync::Mutex<Option<(usize, Arc<Ov2Tower>)>> = std::sync::Mutex::new(None);
693 let key = Arc::as_ptr(model) as usize;
694 let mut g = TOWER.lock().unwrap();
695 if let Some((k, t)) = g.as_ref() {
696 if *k == key {
697 return Ok(t.clone());
698 }
699 }
700 let t = Arc::new(Ov2Tower::from_model(model, crate::pool::Pool::from_env())?);
701 *g = Some((key, t.clone()));
702 Ok(t)
703}
704
705pub struct PreparedOv2Prompt {
707 pub token_ids: Vec<u32>,
708 pub rows: Option<Vec<f32>>,
709}
710
711fn normalize(messages: &[Value]) -> Result<(Vec<Value>, Vec<Value>), String> {
715 let mut messages = messages.to_vec();
716 let mut items = Vec::new();
717 for message in &mut messages {
718 let Some(parts) = message.get_mut("content").and_then(Value::as_array_mut) else {
719 continue;
720 };
721 for part in parts {
722 if part.is_string() {
723 continue;
724 }
725 let typ = part.get("type").and_then(Value::as_str).unwrap_or("");
726 if typ == "image" || typ == "image_url" || part.get("image").is_some() || part.get("image_url").is_some() {
727 let source = part.get("image_url").or_else(|| part.get("image")).unwrap_or(part);
728 items.push(match source.as_str() {
729 Some(url) => json!({"url": url}),
730 None => source.clone(),
731 });
732 *part = json!({"type": "image"});
733 } else if typ != "text" && part.get("text").is_none() {
734 return Err(format!("Mage-VL: unsupported content part '{typ}' (text and images only)"));
735 }
736 }
737 }
738 Ok((messages, items))
739}
740
741pub fn has_media(messages: &[Value]) -> Result<bool, String> {
743 Ok(!normalize(messages)?.1.is_empty())
744}
745
746pub fn prepare_for_pipeline(
748 pipeline: &Pipeline,
749 messages: &[Value],
750 tools: Option<&[Value]>,
751 thinking: Option<bool>,
752 image_max_pixels: Option<usize>,
753) -> Result<PreparedOv2Prompt, String> {
754 let model = pipeline.model.as_ref().ok_or("Mage-VL images require a file-backed pipeline")?;
755 prepare(model, pipeline, messages, tools, thinking, image_max_pixels)
756}
757
758pub fn prepare(
762 model: &Arc<CmfModel>,
763 pipeline: &Pipeline,
764 messages: &[Value],
765 tools: Option<&[Value]>,
766 thinking: Option<bool>,
767 image_max_pixels: Option<usize>,
768) -> Result<PreparedOv2Prompt, String> {
769 let (messages, items) = normalize(messages)?;
770 let tok = &pipeline.tokenizer;
771 if items.is_empty() {
772 let ids = tok.try_apply_chat_template_json(&messages, tools, thinking)?;
773 return Ok(PreparedOv2Prompt { token_ids: ids, rows: None });
774 }
775 let text = tok
776 .render_chat_json(&messages, tools, thinking)
777 .ok_or("Mage-VL: the chat template failed to render")?;
778 let tower = tower_for(model)?;
779 let cfg = &tower.cfg;
780 if cfg.text_hidden != pipeline.hidden_size {
781 return Err("Mage-VL: merger width differs from the text model".into());
782 }
783 let dump = std::env::var_os("CMF_OV2_DUMP").map(std::path::PathBuf::from);
786 const PAD: &str = "<|image_pad|>";
787 let mut sources = items.into_iter();
788 let mut expanded = String::with_capacity(text.len());
789 let mut media_rows: Vec<f32> = Vec::new();
790 let mut rest = text.as_str();
791 let mut k = 0usize;
792 while let Some(at) = rest.find(PAD) {
793 expanded.push_str(&rest[..at]);
794 rest = &rest[at + PAD.len()..];
795 let source = sources.next().ok_or("Mage-VL: more <|image_pad|> placeholders than images")?;
796 let t0 = std::time::Instant::now();
797 let frame = media::decode_rgb(&media::load_image_bytes(&source)?)?;
798 let input = prepare_image(&frame, cfg, image_max_pixels)?;
799 let rows = tower.forward(&input)?;
800 let tokens = input.patches() / (cfg.merge_size * cfg.merge_size);
801 if let Some(dir) = &dump {
802 let write = |name: String, v: &[f32]| {
803 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
804 std::fs::write(dir.join(name), bytes).map_err(|e| format!("CMF_OV2_DUMP: {e}"))
805 };
806 write(format!("pixels_{k}.f32"), &input.rows)?;
807 write(format!("rows_{k}.f32"), &rows)?;
808 }
809 if dump.is_some() || std::env::var_os("CMF_OV2_PROF").is_some() {
810 eprintln!(
811 "mage-vl: image {k} {}x{} resized to {}x{}, grid {}x{}, {tokens} tokens, tower {:.2} s",
812 frame.width,
813 frame.height,
814 input.resized.1,
815 input.resized.0,
816 input.grid_h,
817 input.grid_w,
818 t0.elapsed().as_secs_f64()
819 );
820 }
821 expanded.push_str(&PAD.repeat(tokens));
822 media_rows.extend(rows);
823 k += 1;
824 }
825 expanded.push_str(rest);
826 if sources.next().is_some() {
827 return Err("Mage-VL: the template rendered fewer placeholders than images".into());
828 }
829 let ids = tok.with_bos(tok.encode(&expanded));
830 let h = pipeline.hidden_size;
831 let mut rows = Vec::with_capacity(ids.len() * h);
832 let mut at = 0usize;
833 for &id in &ids {
834 if id == cfg.image_token_id {
835 let row = media_rows.get(at..at + h).ok_or("Mage-VL: placeholder and image row counts differ")?;
836 rows.extend_from_slice(row);
837 at += h;
838 } else {
839 rows.extend(pipeline.embed_id(id));
840 }
841 }
842 if at != media_rows.len() {
843 return Err("Mage-VL: unused image rows".into());
844 }
845 Ok(PreparedOv2Prompt { token_ids: ids, rows: Some(rows) })
846}
847
848#[cfg(test)]
849mod tests {
850 use super::*;
851
852 fn cfg() -> Ov2Config {
853 let config = json!({
854 "image_token_id": 151655,
855 "text_config": {"hidden_size": 2560},
856 "vision_config": {
857 "hidden_size": 1024, "num_attention_heads": 16, "num_hidden_layers": 24,
858 "intermediate_size": 4096, "patch_size": 16, "spatial_merge_size": 2,
859 "temporal_patch_size": 1, "layer_norm_eps": 1e-6, "rope_theta": 10000.0,
860 "hidden_act": "gelu", "layer_norm_type": "layer_norm", "use_head": false
861 }
862 });
863 let processor = json!({
864 "image_mean": [0.48145466, 0.4578275, 0.40821073],
865 "image_std": [0.26862954, 0.26130258, 0.27577711],
866 "min_pixels": 3136, "max_pixels": 4000000, "patch_size": 16, "merge_size": 2,
867 "rescale_factor": 0.00392156862745098
868 });
869 Ov2Config::from_json(&config, &processor).unwrap()
870 }
871
872 #[test]
873 fn config_reads_the_checkpoint_geometry() {
874 let c = cfg();
875 assert_eq!((c.head_dim, c.patch_dim(), c.text_hidden), (64, 768, 2560));
876 assert_eq!((c.min_pixels, c.max_pixels), (3136, 4_000_000));
877 }
878
879 #[test]
880 fn image_parts_become_template_images_in_order() {
881 let msgs = vec![json!({"role": "user", "content": [
882 {"type": "image_url", "image_url": {"url": "a.png"}},
883 {"type": "text", "text": "hi"},
884 {"type": "image", "image": "b.png"}
885 ]})];
886 let (out, items) = normalize(&msgs).unwrap();
887 assert_eq!(items, vec![json!({"url": "a.png"}), json!({"url": "b.png"})]);
888 assert_eq!(out[0]["content"][0], json!({"type": "image"}));
889 assert!(normalize(&[json!({"role": "user", "content": [{"type": "audio", "audio": "x"}]})]).is_err());
890 }
891}