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 from_vec(w: Vec<f32>, rows: usize, cols: usize) -> Self {
248 Mat { w, rows, cols }
249 }
250
251 pub(crate) fn weights(&self) -> &[f32] {
252 &self.w
253 }
254
255 pub(crate) fn matmat(&self, x: &[f32], n: usize, bias: Option<&[f32]>, y: &mut [f32], pool: Option<&Pool>) {
257 let (k, m) = (self.cols, self.rows);
258 if device_gemm_on() && crate::gpu::vision_gemm_nt(x, &self.w, y, n, k, m) {
259 GEMM_DEV.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
260 } else {
261 GEMM_HOST.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
262 crate::fcd_ops::gemm_nt_host(x, &self.w, y, n, k, m, pool);
263 }
264 if let Some(b) = bias {
265 add_bias(y, b, pool);
266 }
267 }
268}
269
270pub(crate) static GEMM_DEV: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
272pub(crate) static GEMM_HOST: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
273
274fn device_gemm_on() -> bool {
276 std::env::var("CMF_OV2_GPU").as_deref() != Ok("0") && crate::gpu::enabled_here()
277}
278
279fn add_bias(y: &mut [f32], b: &[f32], pool: Option<&Pool>) {
280 let d = b.len();
281 let rows = y.len() / d;
282 let dst = crate::pool::SendMut::new(y.as_mut_ptr());
283 let f = |lo: usize, hi: usize| {
284 for r in lo..hi {
285 let row = unsafe { std::slice::from_raw_parts_mut(dst.at(r * d), d) };
287 for (v, &bb) in row.iter_mut().zip(b) {
288 *v += bb;
289 }
290 }
291 };
292 match pool {
293 Some(p) => p.run_rows(rows, &f),
294 None => f(0, rows),
295 }
296}
297
298pub(crate) fn layer_norm_rows(x: &[f32], w: &[f32], b: &[f32], eps: f64, out: &mut [f32], pool: Option<&Pool>) {
300 let d = w.len();
301 let rows = x.len() / d;
302 let dst = crate::pool::SendMut::new(out.as_mut_ptr());
303 let f = |lo: usize, hi: usize| {
304 for r in lo..hi {
305 let src = &x[r * d..(r + 1) * d];
306 let o = unsafe { std::slice::from_raw_parts_mut(dst.at(r * d), d) };
308 let mean = src.iter().map(|&v| v as f64).sum::<f64>() / d as f64;
309 let var = src.iter().map(|&v| (v as f64 - mean).powi(2)).sum::<f64>() / d as f64;
310 let inv = 1.0 / (var + eps).sqrt();
311 for i in 0..d {
312 o[i] = ((src[i] as f64 - mean) * inv) as f32 * w[i] + b[i];
313 }
314 }
315 };
316 match pool {
317 Some(p) => p.run_rows(rows, &f),
318 None => f(0, rows),
319 }
320}
321
322fn erf(x: f32) -> f32 {
323 crate::lfm3_vl::erf(x as f64) as f32
324}
325
326pub(crate) fn gelu_rows(y: &mut [f32], pool: Option<&Pool>) {
328 let n = y.len();
329 let chunk = 1 << 14;
330 let dst = crate::pool::SendMut::new(y.as_mut_ptr());
331 let f = |lo: usize, hi: usize| {
332 for c in lo..hi {
333 let a = c * chunk;
334 let b = (a + chunk).min(n);
335 let s = unsafe { std::slice::from_raw_parts_mut(dst.at(a), b - a) };
337 for v in s {
338 *v = 0.5 * *v * (1.0 + erf(*v * std::f32::consts::FRAC_1_SQRT_2));
339 }
340 }
341 };
342 let chunks = n.div_ceil(chunk);
343 match pool {
344 Some(p) => p.run_rows(chunks, &f),
345 None => f(0, chunks),
346 }
347}
348
349struct Layer {
350 ln1_w: Vec<f32>,
351 ln1_b: Vec<f32>,
352 qkv: Mat,
353 qkv_b: Vec<f32>,
354 proj: Mat,
355 proj_b: Vec<f32>,
356 ln2_w: Vec<f32>,
357 ln2_b: Vec<f32>,
358 fc1: Mat,
359 fc1_b: Vec<f32>,
360 fc2: Mat,
361 fc2_b: Vec<f32>,
362}
363
364pub struct Ov2Tower {
366 pub cfg: Ov2Config,
367 patch: Mat,
368 ln_pre_w: Vec<f32>,
369 ln_pre_b: Vec<f32>,
370 layers: Vec<Layer>,
371 ln_q_w: Vec<f32>,
372 ln_q_b: Vec<f32>,
373 mlp0: Mat,
374 mlp0_b: Vec<f32>,
375 mlp2: Mat,
376 mlp2_b: Vec<f32>,
377 pool: Option<Arc<Pool>>,
378}
379
380fn vec_of(model: &Arc<CmfModel>, name: &str, len: usize) -> Result<Vec<f32>, String> {
381 let v = crate::dit::cmf_f32(model, name)?;
382 if v.len() != len {
383 return Err(format!("tensor '{name}' has {} values, expected {len}", v.len()));
384 }
385 Ok(v)
386}
387
388impl Ov2Tower {
389 pub fn from_model(model: &Arc<CmfModel>, pool: Option<Arc<Pool>>) -> Result<Self, String> {
390 let cfg = Ov2Config::from_model(model)?;
391 let (d, inter) = (cfg.hidden, cfg.intermediate);
392 let merged = d * cfg.merge_size * cfg.merge_size;
393 let mut layers = Vec::with_capacity(cfg.layers);
394 for i in 0..cfg.layers {
395 let p = format!("visual.encoder.layers.{i}");
396 layers.push(Layer {
397 ln1_w: vec_of(model, &format!("{p}.layer_norm1.weight"), d)?,
398 ln1_b: vec_of(model, &format!("{p}.layer_norm1.bias"), d)?,
399 qkv: Mat::load(model, &format!("{p}.self_attn.qkv.weight"), 3 * d, d)?,
400 qkv_b: vec_of(model, &format!("{p}.self_attn.qkv.bias"), 3 * d)?,
401 proj: Mat::load(model, &format!("{p}.self_attn.proj.weight"), d, d)?,
402 proj_b: vec_of(model, &format!("{p}.self_attn.proj.bias"), d)?,
403 ln2_w: vec_of(model, &format!("{p}.layer_norm2.weight"), d)?,
404 ln2_b: vec_of(model, &format!("{p}.layer_norm2.bias"), d)?,
405 fc1: Mat::load(model, &format!("{p}.mlp.fc1.weight"), inter, d)?,
406 fc1_b: vec_of(model, &format!("{p}.mlp.fc1.bias"), inter)?,
407 fc2: Mat::load(model, &format!("{p}.mlp.fc2.weight"), d, inter)?,
408 fc2_b: vec_of(model, &format!("{p}.mlp.fc2.bias"), d)?,
409 });
410 }
411 Ok(Ov2Tower {
412 patch: Mat::load(model, PATCH, d, cfg.patch_dim())?,
413 ln_pre_w: vec_of(model, "visual.layernorm_pre.weight", d)?,
414 ln_pre_b: vec_of(model, "visual.layernorm_pre.bias", d)?,
415 layers,
416 ln_q_w: vec_of(model, "visual.merger.ln_q.weight", d)?,
417 ln_q_b: vec_of(model, "visual.merger.ln_q.bias", d)?,
418 mlp0: Mat::load(model, "visual.merger.mlp.0.weight", merged, merged)?,
419 mlp0_b: vec_of(model, "visual.merger.mlp.0.bias", merged)?,
420 mlp2: Mat::load(model, "visual.merger.mlp.2.weight", cfg.text_hidden, merged)?,
421 mlp2_b: vec_of(model, "visual.merger.mlp.2.bias", cfg.text_hidden)?,
422 cfg,
423 pool,
424 })
425 }
426
427 fn rope_tables(&self, gt: usize, gh: usize, gw: usize) -> (Vec<f32>, Vec<f32>) {
430 let c = &self.cfg;
431 let hd = c.head_dim;
432 let half = hd / 2;
433 let unit = half / 16;
434 let (nt, nh, nw) = (4 * unit, 6 * unit, 6 * unit);
435 let inv = |n: usize| -> Vec<f32> {
436 (0..n)
437 .map(|i| (1.0 / c.rope_theta.powf(i as f64 / n as f64)) as f32)
438 .collect()
439 };
440 let (it, ih, iw) = (inv(nt), inv(nh), inv(nw));
441 let m = c.merge_size;
442 let n = gt * gh * gw;
443 let (mut cos, mut sin) = (vec![0f32; n * hd], vec![0f32; n * hd]);
444 let mut k = 0usize;
445 let mut ang = vec![0f32; half];
446 for t in 0..gt {
447 for a in 0..gh / m {
448 for b in 0..gw / m {
449 for mh in 0..m {
450 for mw in 0..m {
451 let (r, col) = ((a * m + mh) as f32, (b * m + mw) as f32);
452 for (j, &f) in it.iter().enumerate() {
454 ang[j] = t as f32 * f;
455 }
456 for (j, &f) in ih.iter().enumerate() {
457 ang[nt + j] = r * f;
458 }
459 for (j, &f) in iw.iter().enumerate() {
460 ang[nt + nh + j] = col * f;
461 }
462 for j in 0..hd {
463 let (s, co) = ang[j % half].sin_cos();
464 cos[k * hd + j] = co;
465 sin[k * hd + j] = s;
466 }
467 k += 1;
468 }
469 }
470 }
471 }
472 }
473 (cos, sin)
474 }
475
476 fn split_rope(&self, qkv: &[f32], n: usize, cos: &[f32], sin: &[f32], qh: &mut [f32], kh: &mut [f32], vh: &mut [f32]) {
478 let (nh, hd) = (self.cfg.heads, self.cfg.head_dim);
479 let d = nh * hd;
480 let (qp, kp, vp) = (
481 crate::pool::SendMut::new(qh.as_mut_ptr()),
482 crate::pool::SendMut::new(kh.as_mut_ptr()),
483 crate::pool::SendMut::new(vh.as_mut_ptr()),
484 );
485 let f = |lo: usize, hi: usize| {
486 for p in lo..hi {
487 let row = &qkv[p * 3 * d..(p + 1) * 3 * d];
488 let (cs, sn) = (&cos[p * hd..(p + 1) * hd], &sin[p * hd..(p + 1) * hd]);
489 for h in 0..nh {
490 let dst = (h * n + p) * hd;
491 let (q, k, v) = unsafe {
493 (
494 std::slice::from_raw_parts_mut(qp.at(dst), hd),
495 std::slice::from_raw_parts_mut(kp.at(dst), hd),
496 std::slice::from_raw_parts_mut(vp.at(dst), hd),
497 )
498 };
499 let (qs, ks, vs) = (
500 &row[h * hd..(h + 1) * hd],
501 &row[d + h * hd..d + (h + 1) * hd],
502 &row[2 * d + h * hd..2 * d + (h + 1) * hd],
503 );
504 for i in (0..hd).step_by(2) {
505 q[i] = qs[i] * cs[i] - qs[i + 1] * sn[i];
506 q[i + 1] = qs[i + 1] * cs[i + 1] + qs[i] * sn[i + 1];
507 k[i] = ks[i] * cs[i] - ks[i + 1] * sn[i];
508 k[i + 1] = ks[i + 1] * cs[i + 1] + ks[i] * sn[i + 1];
509 }
510 v.copy_from_slice(vs);
511 }
512 }
513 };
514 match self.pool.as_deref() {
515 Some(pool) => pool.run_rows(n, &f),
516 None => f(0, n),
517 }
518 }
519
520 fn attend(&self, qh: &[f32], kh: &[f32], vh: &[f32], n: usize, out: &mut [f32]) {
522 let (nh, hd) = (self.cfg.heads, self.cfg.head_dim);
523 let scale = (hd as f32).powf(-0.5);
524 if device_gemm_on() && n >= 64 && crate::gpu::dit_attention(qh, kh, vh, nh, nh, n, hd, scale, out) {
525 return;
526 }
527 attend_host(qh, kh, vh, nh, hd, n, scale, out, self.pool.as_deref());
528 }
529
530 pub fn forward(&self, input: &VisualInput) -> Result<Vec<f32>, String> {
533 let c = &self.cfg;
534 let (gt, gh, gw) = (input.grid_t, input.grid_h, input.grid_w);
535 let n = gt * gh * gw;
536 let m2 = c.merge_size * c.merge_size;
537 if input.rows.len() != n * c.patch_dim() || n % m2 != 0 {
538 return Err("onevision2: patch rows do not match the grid".into());
539 }
540 let pool = self.pool.as_deref();
541 let (d, inter) = (c.hidden, c.intermediate);
542 let prof = std::env::var("CMF_OV2_PROF").is_ok();
543 let t0 = std::time::Instant::now();
544 let mut stamps: Vec<(&str, f64)> = Vec::new();
545 let mut stamp = |what: &'static str, t: &mut std::time::Instant| {
546 stamps.push((what, t.elapsed().as_secs_f64()));
547 *t = std::time::Instant::now();
548 };
549 let mut ts = std::time::Instant::now();
550 let mut x = vec![0f32; n * d];
551 self.patch.matmat(&input.rows, n, None, &mut x, pool);
552 stamp("patch", &mut ts);
553 let mut xn = vec![0f32; n * d];
554 layer_norm_rows(&x, &self.ln_pre_w, &self.ln_pre_b, c.eps, &mut xn, pool);
555 std::mem::swap(&mut x, &mut xn);
556 let (cos, sin) = self.rope_tables(gt, gh, gw);
557 stamp("ln_pre+rope", &mut ts);
558 let mut qkv = vec![0f32; n * 3 * d];
559 let (mut qh, mut kh, mut vh) = (vec![0f32; n * d], vec![0f32; n * d], vec![0f32; n * d]);
560 let mut att = vec![0f32; n * d];
561 let mut tmp = vec![0f32; n * d];
562 let mut ff = vec![0f32; n * inter];
563 let (mut t_mm, mut t_att) = (0f64, 0f64);
564 let resident = device_gemm_on() && {
566 let layers = self
567 .layers
568 .iter()
569 .map(|l| crate::gpu::VitLayer {
570 ln1_w: &l.ln1_w,
571 ln1_b: &l.ln1_b,
572 qkv: &l.qkv.w,
573 qkv_b: &l.qkv_b,
574 proj: &l.proj.w,
575 proj_b: &l.proj_b,
576 ln2_w: &l.ln2_w,
577 ln2_b: &l.ln2_b,
578 fc1: &l.fc1.w,
579 fc1_b: &l.fc1_b,
580 fc2: &l.fc2.w,
581 fc2_b: &l.fc2_b,
582 })
583 .collect();
584 let args = crate::gpu::VitArgs {
585 key: self as *const Self as u64,
586 layers,
587 n,
588 d,
589 heads: c.heads,
590 head_dim: c.head_dim,
591 inter,
592 eps: c.eps,
593 gelu_tanh: false,
594 quick_gelu: false,
595 rope_halves: false,
596 cos: &cos,
597 sin: &sin,
598 prof,
599 };
600 crate::gpu::vit_encode(&args, &mut x)
601 };
602 stamp("encoder", &mut ts);
603 for l in self.layers.iter().filter(|_| !resident) {
604 layer_norm_rows(&x, &l.ln1_w, &l.ln1_b, c.eps, &mut xn, pool);
605 let tm = std::time::Instant::now();
606 l.qkv.matmat(&xn, n, Some(&l.qkv_b), &mut qkv, pool);
607 t_mm += tm.elapsed().as_secs_f64();
608 self.split_rope(&qkv, n, &cos, &sin, &mut qh, &mut kh, &mut vh);
609 let ta = std::time::Instant::now();
610 self.attend(&qh, &kh, &vh, n, &mut att);
611 t_att += ta.elapsed().as_secs_f64();
612 let tm = std::time::Instant::now();
613 l.proj.matmat(&att, n, Some(&l.proj_b), &mut tmp, pool);
614 for (a, &b) in x.iter_mut().zip(&tmp) {
615 *a += b;
616 }
617 layer_norm_rows(&x, &l.ln2_w, &l.ln2_b, c.eps, &mut xn, pool);
618 l.fc1.matmat(&xn, n, Some(&l.fc1_b), &mut ff, pool);
619 gelu_rows(&mut ff, pool);
620 l.fc2.matmat(&ff, n, Some(&l.fc2_b), &mut tmp, pool);
621 t_mm += tm.elapsed().as_secs_f64();
622 for (a, &b) in x.iter_mut().zip(&tmp) {
623 *a += b;
624 }
625 }
626 layer_norm_rows(&x, &self.ln_q_w, &self.ln_q_b, c.eps, &mut xn, pool);
627 let nm = n / m2;
628 let mut h1 = vec![0f32; nm * d * m2];
629 self.mlp0.matmat(&xn, nm, Some(&self.mlp0_b), &mut h1, pool);
630 gelu_rows(&mut h1, pool);
631 let mut out = vec![0f32; nm * c.text_hidden];
632 self.mlp2.matmat(&h1, nm, Some(&self.mlp2_b), &mut out, pool);
633 stamp("merger", &mut ts);
634 if prof {
635 eprintln!("onevision2 stages: {}", stamps.iter().map(|(w, s)| format!("{w} {s:.3} s")).collect::<Vec<_>>().join(", "));
636 eprintln!(
637 "onevision2: {n} patches, tower {:.3} s (gemm {:.3} s: {} on the device, {} on the host; attention {:.3} s)",
638 t0.elapsed().as_secs_f64(),
639 t_mm,
640 GEMM_DEV.load(std::sync::atomic::Ordering::Relaxed),
641 GEMM_HOST.load(std::sync::atomic::Ordering::Relaxed),
642 t_att
643 );
644 }
645 Ok(out)
646 }
647}
648
649#[allow(clippy::too_many_arguments)]
651pub(crate) fn attend_host(qh: &[f32], kh: &[f32], vh: &[f32], nh: usize, hd: usize, n: usize, scale: f32, out: &mut [f32], pool: Option<&Pool>) {
652 let tile = n.min(1024);
653 let mut vt = vec![0f32; hd * n];
654 let mut qs = vec![0f32; tile * hd];
655 let mut sc = vec![0f32; tile * n];
656 let mut oh = vec![0f32; tile * hd];
657 for h in 0..nh {
658 let kp = &kh[h * n * hd..(h + 1) * n * hd];
659 for j in 0..n {
660 for e in 0..hd {
661 vt[e * n + j] = vh[(h * n + j) * hd + e];
662 }
663 }
664 for t0 in (0..n).step_by(tile) {
665 let tq = tile.min(n - t0);
666 for i in 0..tq {
667 for e in 0..hd {
668 qs[i * hd + e] = qh[(h * n + t0 + i) * hd + e] * scale;
669 }
670 }
671 crate::fcd_ops::gemm_nt_host(&qs[..tq * hd], kp, &mut sc[..tq * n], tq, hd, n, pool);
672 let sp = crate::pool::SendMut::new(sc.as_mut_ptr());
673 let f = |lo: usize, hi: usize| {
674 for i in lo..hi {
675 let row = unsafe { std::slice::from_raw_parts_mut(sp.at(i * n), n) };
677 let mx = row.iter().fold(f32::NEG_INFINITY, |m, &x| m.max(x));
678 let mut sum = 0f32;
679 for x in row.iter_mut() {
680 *x = (*x - mx).exp();
681 sum += *x;
682 }
683 let inv = 1.0 / sum;
684 for x in row.iter_mut() {
685 *x *= inv;
686 }
687 }
688 };
689 match pool {
690 Some(p) => p.run_rows(tq, &f),
691 None => f(0, tq),
692 }
693 crate::fcd_ops::gemm_nt_host(&sc[..tq * n], &vt, &mut oh[..tq * hd], tq, n, hd, pool);
694 for i in 0..tq {
695 let r = t0 + i;
696 out[r * nh * hd + h * hd..r * nh * hd + (h + 1) * hd].copy_from_slice(&oh[i * hd..(i + 1) * hd]);
697 }
698 }
699 }
700}
701
702fn tower_for(model: &Arc<CmfModel>) -> Result<Arc<Ov2Tower>, String> {
706 static TOWER: std::sync::Mutex<Option<(usize, Arc<Ov2Tower>)>> = std::sync::Mutex::new(None);
707 let key = Arc::as_ptr(model) as usize;
708 let mut g = TOWER.lock().unwrap();
709 if let Some((k, t)) = g.as_ref() {
710 if *k == key {
711 return Ok(t.clone());
712 }
713 }
714 let t = Arc::new(Ov2Tower::from_model(model, crate::pool::Pool::from_env())?);
715 *g = Some((key, t.clone()));
716 Ok(t)
717}
718
719pub struct PreparedOv2Prompt {
721 pub token_ids: Vec<u32>,
722 pub rows: Option<Vec<f32>>,
723}
724
725fn normalize(messages: &[Value]) -> Result<(Vec<Value>, Vec<Value>), String> {
729 let mut messages = messages.to_vec();
730 let mut items = Vec::new();
731 for message in &mut messages {
732 let Some(parts) = message.get_mut("content").and_then(Value::as_array_mut) else {
733 continue;
734 };
735 for part in parts {
736 if part.is_string() {
737 continue;
738 }
739 let typ = part.get("type").and_then(Value::as_str).unwrap_or("");
740 if typ == "image" || typ == "image_url" || part.get("image").is_some() || part.get("image_url").is_some() {
741 let source = part.get("image_url").or_else(|| part.get("image")).unwrap_or(part);
742 items.push(match source.as_str() {
743 Some(url) => json!({"url": url}),
744 None => source.clone(),
745 });
746 *part = json!({"type": "image"});
747 } else if typ != "text" && part.get("text").is_none() {
748 return Err(format!("Mage-VL: unsupported content part '{typ}' (text and images only)"));
749 }
750 }
751 }
752 Ok((messages, items))
753}
754
755pub fn has_media(messages: &[Value]) -> Result<bool, String> {
757 Ok(!normalize(messages)?.1.is_empty())
758}
759
760pub fn prepare_for_pipeline(
762 pipeline: &Pipeline,
763 messages: &[Value],
764 tools: Option<&[Value]>,
765 thinking: Option<bool>,
766 image_max_pixels: Option<usize>,
767) -> Result<PreparedOv2Prompt, String> {
768 let model = pipeline.model.as_ref().ok_or("Mage-VL images require a file-backed pipeline")?;
769 prepare(model, pipeline, messages, tools, thinking, image_max_pixels)
770}
771
772pub fn prepare(
776 model: &Arc<CmfModel>,
777 pipeline: &Pipeline,
778 messages: &[Value],
779 tools: Option<&[Value]>,
780 thinking: Option<bool>,
781 image_max_pixels: Option<usize>,
782) -> Result<PreparedOv2Prompt, String> {
783 let (messages, items) = normalize(messages)?;
784 let tok = &pipeline.tokenizer;
785 if items.is_empty() {
786 let ids = tok.try_apply_chat_template_json(&messages, tools, thinking)?;
787 return Ok(PreparedOv2Prompt { token_ids: ids, rows: None });
788 }
789 let text = tok
790 .render_chat_json(&messages, tools, thinking)
791 .ok_or("Mage-VL: the chat template failed to render")?;
792 let tower = tower_for(model)?;
793 let cfg = &tower.cfg;
794 if cfg.text_hidden != pipeline.hidden_size {
795 return Err("Mage-VL: merger width differs from the text model".into());
796 }
797 let dump = std::env::var_os("CMF_OV2_DUMP").map(std::path::PathBuf::from);
800 const PAD: &str = "<|image_pad|>";
801 let mut sources = items.into_iter();
802 let mut expanded = String::with_capacity(text.len());
803 let mut media_rows: Vec<f32> = Vec::new();
804 let mut rest = text.as_str();
805 let mut k = 0usize;
806 while let Some(at) = rest.find(PAD) {
807 expanded.push_str(&rest[..at]);
808 rest = &rest[at + PAD.len()..];
809 let source = sources.next().ok_or("Mage-VL: more <|image_pad|> placeholders than images")?;
810 let t0 = std::time::Instant::now();
811 let frame = media::decode_rgb(&media::load_image_bytes(&source)?)?;
812 let input = prepare_image(&frame, cfg, image_max_pixels)?;
813 let rows = tower.forward(&input)?;
814 let tokens = input.patches() / (cfg.merge_size * cfg.merge_size);
815 if let Some(dir) = &dump {
816 let write = |name: String, v: &[f32]| {
817 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
818 std::fs::write(dir.join(name), bytes).map_err(|e| format!("CMF_OV2_DUMP: {e}"))
819 };
820 write(format!("pixels_{k}.f32"), &input.rows)?;
821 write(format!("rows_{k}.f32"), &rows)?;
822 }
823 if dump.is_some() || std::env::var_os("CMF_OV2_PROF").is_some() {
824 eprintln!(
825 "mage-vl: image {k} {}x{} resized to {}x{}, grid {}x{}, {tokens} tokens, tower {:.2} s",
826 frame.width,
827 frame.height,
828 input.resized.1,
829 input.resized.0,
830 input.grid_h,
831 input.grid_w,
832 t0.elapsed().as_secs_f64()
833 );
834 }
835 expanded.push_str(&PAD.repeat(tokens));
836 media_rows.extend(rows);
837 k += 1;
838 }
839 expanded.push_str(rest);
840 if sources.next().is_some() {
841 return Err("Mage-VL: the template rendered fewer placeholders than images".into());
842 }
843 let ids = tok.with_bos(tok.encode(&expanded));
844 let h = pipeline.hidden_size;
845 let mut rows = Vec::with_capacity(ids.len() * h);
846 let mut at = 0usize;
847 for &id in &ids {
848 if id == cfg.image_token_id {
849 let row = media_rows.get(at..at + h).ok_or("Mage-VL: placeholder and image row counts differ")?;
850 rows.extend_from_slice(row);
851 at += h;
852 } else {
853 rows.extend(pipeline.embed_id(id));
854 }
855 }
856 if at != media_rows.len() {
857 return Err("Mage-VL: unused image rows".into());
858 }
859 Ok(PreparedOv2Prompt { token_ids: ids, rows: Some(rows) })
860}
861
862#[cfg(test)]
863mod tests {
864 use super::*;
865
866 fn cfg() -> Ov2Config {
867 let config = json!({
868 "image_token_id": 151655,
869 "text_config": {"hidden_size": 2560},
870 "vision_config": {
871 "hidden_size": 1024, "num_attention_heads": 16, "num_hidden_layers": 24,
872 "intermediate_size": 4096, "patch_size": 16, "spatial_merge_size": 2,
873 "temporal_patch_size": 1, "layer_norm_eps": 1e-6, "rope_theta": 10000.0,
874 "hidden_act": "gelu", "layer_norm_type": "layer_norm", "use_head": false
875 }
876 });
877 let processor = json!({
878 "image_mean": [0.48145466, 0.4578275, 0.40821073],
879 "image_std": [0.26862954, 0.26130258, 0.27577711],
880 "min_pixels": 3136, "max_pixels": 4000000, "patch_size": 16, "merge_size": 2,
881 "rescale_factor": 0.00392156862745098
882 });
883 Ov2Config::from_json(&config, &processor).unwrap()
884 }
885
886 #[test]
887 fn config_reads_the_checkpoint_geometry() {
888 let c = cfg();
889 assert_eq!((c.head_dim, c.patch_dim(), c.text_hidden), (64, 768, 2560));
890 assert_eq!((c.min_pixels, c.max_pixels), (3136, 4_000_000));
891 }
892
893 #[test]
894 fn image_parts_become_template_images_in_order() {
895 let msgs = vec![json!({"role": "user", "content": [
896 {"type": "image_url", "image_url": {"url": "a.png"}},
897 {"type": "text", "text": "hi"},
898 {"type": "image", "image": "b.png"}
899 ]})];
900 let (out, items) = normalize(&msgs).unwrap();
901 assert_eq!(items, vec![json!({"url": "a.png"}), json!({"url": "b.png"})]);
902 assert_eq!(out[0]["content"][0], json!({"type": "image"}));
903 assert!(normalize(&[json!({"role": "user", "content": [{"type": "audio", "audio": "x"}]})]).is_err());
904 }
905}