Skip to main content

cortiq_engine/
uocr.rs

1//! Unlimited-OCR-2 (`unlimited-ocr`): the DeepEncoder (SAM ViT-B feeding
2//! CLIP-L), the DeepSeek-V2 style MoE decoder and its DASH block-sparse
3//! attention (SR-SWA), which reads a fixed set of references per query.
4//!
5//! ## The prompt
6//!
7//! `[BOS] (<PAGE> image tokens)×pages instruction`, no chat template. One
8//! page is tiled ("gundam"): 640² tiles and a padded 1024² global view; several
9//! pages take the global view only. See [`prepare`].
10//!
11//! ## The tower
12//!
13//! SAM: 16×16 patches over an absolute position table, 12 blocks at 768 (window
14//! 14, global at 2, 5, 8, 11) with decomposed relative position bias, a conv
15//! neck and two strided convs down to 1024 channels at 1/64. CLIP-L takes those
16//! channels as its patch embeddings: class token + position table, then 24
17//! blocks at 1024. Each token is `clip ‖ sam` projected to the decoder's 1280.
18//!
19//! ## DASH attention (`dash_attention.py`, the vLLM-parity inference path)
20//!
21//! Every layer has a router: `q_proj`, `k_proj` to 128, a Gemma-style RMSNorm
22//! and the same two-axis rotary as the main branch. Keys are summarized per
23//! completed 32-token block through a learned query (`head_cls`). For each
24//! query:
25//!
26//! 1. score every block at least `local_blocks` behind the query,
27//! 2. restrict to the candidates of the query's page, when it has any,
28//! 3. entmax-1.5 over the 16 best scores, keep the 12 best blocks,
29//! 4. bias = centered log prior,
30//! 5. attend over those blocks (with the bias) and the last 128 tokens (none).
31//!
32//! The read set is at most 512 tokens however long the document is.
33//!
34//! The rotary has two axes (`page2d`): frequency pairs 0..63 follow the token's
35//! running count inside its page, pair 63 the page number's parity.
36//! [`PageRope`] keeps the coordinates, prefill and decode alike.
37//!
38//! Parity: `scripts/ocr/ref_unlimited_ocr.py` (Transformers 4.52.1).
39
40use crate::Pipeline;
41use crate::media::{self, RgbFrame};
42use crate::onevision2::{Mat, gelu_rows, layer_norm_rows};
43use crate::pool::Pool;
44use cortiq_core::CmfModel;
45use serde_json::Value;
46use std::sync::Arc;
47
48pub const ARCH: &str = "unlimited-ocr";
49const BASE_SIZE: usize = 1024;
50const IMAGE_SIZE: usize = 640;
51const PATCH: usize = 16;
52const DOWNSAMPLE: usize = 4;
53const MIN_CROPS: usize = 2;
54const MAX_CROPS: usize = 16;
55const BOS_ID: u32 = 0;
56/// `_ACT_CLAMP`, `_ENTMAX_NEG` in the reference.
57const ACT_CLAMP: f32 = 1.0e4;
58const ENTMAX_NEG: f32 = -1.0e4;
59/// `DASH_INFER_TOPM`: the window entmax runs over.
60const TOP_M: usize = 16;
61
62/// Whether `model` is an Unlimited-OCR file.
63pub fn is_uocr(model: &CmfModel) -> bool {
64    model.header.arch.arch_name == ARCH
65}
66
67// ───────────────────────── page coordinates ─────────────────────────
68
69/// The two-axis rotary coordinates and page bookkeeping of one sequence.
70#[derive(Clone, Debug, Default)]
71pub struct PageRope {
72    pub image_id: u32,
73    pub page_id: u32,
74    /// `(tok, page)` of every prompt position.
75    coords: Vec<(u32, u32)>,
76    /// Image page of every prompt position, -1 elsewhere (raw markers − 1).
77    image_page: Vec<i32>,
78    /// The page of every prompt query, -1 for none.
79    query_page: Vec<i32>,
80    /// Prefill rows per page bucket (`pf_cnt`).
81    pf_cnt: Vec<u32>,
82    /// Decode state machine.
83    page: u32,
84    tok: u32,
85    /// Response markers so far plus the prompt's, and the prompt's.
86    mcum: i32,
87    pp: i32,
88    /// Per block `[min, max]` image page of the prompt, -1 without one.
89    bmin: Vec<i32>,
90    bmax: Vec<i32>,
91    block: usize,
92}
93
94impl PageRope {
95    /// `_build_page_rope_pos`, `_build_page_aware_ids` and the prefill half of
96    /// `_uocr_dash_decode_allow` over the whole prompt.
97    pub fn begin(ids: &[u32], image_id: u32, page_id: u32, block: usize) -> Result<Self, String> {
98        let s = ids.len();
99        let last_img = ids.iter().rposition(|&i| i == image_id).ok_or("unlimited-ocr: the prompt has no image")?;
100        let mut cum = vec![0i32; s];
101        let mut c = 0i32;
102        for (i, &id) in ids.iter().enumerate() {
103            c += i32::from(id == page_id);
104            cum[i] = c;
105        }
106        let n_pages = cum[last_img];
107        if n_pages == 0 {
108            return Err("unlimited-ocr: the prompt carries no <PAGE> marker".into());
109        }
110        let page_of = |i: usize| -> u32 {
111            let p = if i <= last_img { cum[i] } else { cum[i] - n_pages };
112            p.max(0) as u32
113        };
114        let n_pg = (0..s).map(page_of).max().unwrap_or(0) as usize + 1;
115        let mut run = vec![0u32; n_pg];
116        let mut pf_cnt = vec![0u32; n_pg];
117        let mut coords = Vec::with_capacity(s);
118        for i in 0..s {
119            let p = page_of(i) as usize;
120            coords.push((run[p], p as u32));
121            run[p] += 1;
122            if i <= last_img {
123                pf_cnt[p] += 1;
124            }
125        }
126        let image_page: Vec<i32> =
127            (0..s).map(|i| if ids[i] == image_id { cum[i] - 1 } else { -1 }).collect();
128        let query_page: Vec<i32> = (0..s)
129            .map(|i| {
130                let p = if i <= last_img { cum[i] - 1 } else { cum[i] - 1 - n_pages };
131                p.max(-1)
132            })
133            .collect();
134        let nblk = s.div_ceil(block);
135        let (mut bmin, mut bmax) = (vec![-1i32; nblk], vec![-1i32; nblk]);
136        for (i, &pg) in image_page.iter().enumerate() {
137            if pg >= 0 {
138                let b = i / block;
139                bmax[b] = bmax[b].max(pg);
140                bmin[b] = if bmin[b] < 0 { pg } else { bmin[b].min(pg) };
141            }
142        }
143        let (tok, page) = coords[s - 1];
144        Ok(Self {
145            image_id,
146            page_id,
147            coords,
148            image_page,
149            query_page,
150            pf_cnt,
151            page,
152            tok,
153            mcum: cum[s - 1],
154            pp: n_pages,
155            bmin,
156            bmax,
157            block,
158        })
159    }
160
161    pub fn prompt_len(&self) -> usize {
162        self.coords.len()
163    }
164
165    /// `(tok, page)` of prompt position `pos`.
166    pub fn prompt_coords(&self, pos: usize) -> (u32, u32) {
167        self.coords[pos]
168    }
169
170    /// Advance the decode state over token `id`; returns its coordinates.
171    pub fn decode_step(&mut self, id: u32) -> (u32, u32) {
172        if id == self.page_id {
173            self.page = (self.page + 1).min(self.pf_cnt.len() as u32 - 1);
174            self.tok = self.pf_cnt[self.page as usize];
175            self.mcum += 1;
176        } else {
177            self.tok += 1;
178        }
179        (self.tok, self.page)
180    }
181
182    /// The candidate blocks of a prompt query (`_build_kl_candidate_mask`):
183    /// the blocks with an image token of its page; everything when it has no
184    /// page or the page has no block.
185    pub fn prefill_allow(&self, pos: usize, nblk: usize, out: &mut Vec<bool>) {
186        out.clear();
187        out.resize(nblk, false);
188        let qp = self.query_page[pos];
189        let mut any = false;
190        if qp >= 0 {
191            // Blocks hold a page's tokens contiguously: `bmin..=bmax` of a
192            // block spans the pages present in it.
193            for b in 0..nblk.min(self.bmax.len()) {
194                let hit = self.bmin[b] >= 0
195                    && self.bmin[b] <= qp
196                    && self.bmax[b] >= qp
197                    && self.block_has_page(b, qp);
198                out[b] = hit;
199                any |= hit;
200            }
201        }
202        if !any {
203            out.iter_mut().for_each(|v| *v = true);
204        }
205    }
206
207    fn block_has_page(&self, b: usize, page: i32) -> bool {
208        let at = b * self.block;
209        let end = (at + self.block).min(self.image_page.len());
210        self.image_page[at..end].iter().any(|&p| p == page)
211    }
212
213    /// The candidate blocks of a decode query (`_uocr_dash_decode_allow`), the
214    /// state already advanced over its token.
215    pub fn decode_allow(&self, nblk: usize, out: &mut Vec<bool>) {
216        out.clear();
217        out.resize(nblk, false);
218        let qp = self.mcum - 1 - self.pp;
219        let qp = if qp >= 0 { qp } else { -1 };
220        let mut any = false;
221        if qp >= 0 {
222            for b in 0..nblk.min(self.bmax.len()) {
223                let hit = self.bmin[b] >= 0 && self.bmin[b] <= qp && self.bmax[b] >= qp;
224                out[b] = hit;
225                any |= hit;
226            }
227        }
228        if !any {
229            out.iter_mut().for_each(|v| *v = true);
230        }
231    }
232}
233
234/// The rotary angles of `(tok, page)` over `half` frequency pairs: all but the
235/// last on the token axis (`1/θ^(j/n)`, θ = 10⁴), the last the page's parity
236/// (`sparse_rope_page_mod = 2`, one pair at θ = 256, whose only frequency is 1).
237pub fn rope_angles(tok: u32, page: u32, inv_tok: &[f32], out: &mut [f32]) {
238    let t = tok as f32;
239    for (o, &f) in out.iter_mut().zip(inv_tok) {
240        *o = t * f;
241    }
242    *out.last_mut().expect("at least one pair") = (page % 2) as f32;
243}
244
245/// `inv_tok` of [`rope_angles`].
246pub fn inv_tok(half: usize, theta: f32) -> Vec<f32> {
247    let n = half - 1;
248    (0..n).map(|j| 1.0 / theta.powf(j as f32 / n as f32)).collect()
249}
250
251/// `rotate_half`: `x[j]`, `x[j+half]` turned by `angles[j]`.
252pub fn rotate(x: &mut [f32], cs: &[(f32, f32)]) {
253    let half = cs.len();
254    let (a, b) = x.split_at_mut(half);
255    for j in 0..half {
256        let (s, c) = cs[j];
257        let (p, q) = (a[j], b[j]);
258        a[j] = p * c - q * s;
259        b[j] = q * c + p * s;
260    }
261}
262
263// ───────────────────────── entmax routing ─────────────────────────
264
265/// `_Entmax15.forward` over `x`, sorted descending, into `y`.
266pub fn entmax15_sorted(x: &[f32], y: &mut [f32]) {
267    let d = x.len();
268    let top = x[0];
269    let (mut sum, mut sum2) = (0f32, 0f32);
270    let mut tau_star = f32::NAN;
271    let mut support = 0usize;
272    let mut taus = [0f32; TOP_M];
273    for i in 0..d {
274        let v = (x[i] - top) / 2.0;
275        sum += v;
276        sum2 += v * v;
277        let rho = (i + 1) as f32;
278        let mean = sum / rho;
279        let mean_sq = sum2 / rho;
280        let ss = rho * (mean_sq - mean * mean);
281        let delta = ((1.0 - ss) / rho).max(0.0);
282        let tau = mean - delta.sqrt();
283        taus[i] = tau;
284        if tau <= v {
285            support += 1;
286        }
287    }
288    if support > 0 {
289        tau_star = taus[support - 1];
290    } else if d > 0 {
291        tau_star = taus[0];
292    }
293    for i in 0..d {
294        let v = (x[i] - top) / 2.0 - tau_star;
295        y[i] = if v > 0.0 { v * v } else { 0.0 };
296    }
297}
298
299/// The routing parameters of one layer.
300#[derive(Clone, Copy, Debug)]
301pub struct RouteParams {
302    /// `dash_scale_per_layer / √index_dim`.
303    pub idx_scale: f32,
304    pub floor: f32,
305    pub sigma: f32,
306    pub k: usize,
307    pub top_m: usize,
308}
309
310/// `_infer_route_select` for one query. `scores[b]` is a block's score, None
311/// when it is not a remote block; `allow` the page candidates. Fills `pick`
312/// with `(block, bias)` of the read set.
313pub fn route_select(
314    scores: &[Option<f32>],
315    allow: &[bool],
316    p: &RouteParams,
317    window: &mut Vec<(f32, u32, bool)>,
318    pick: &mut Vec<(u32, f32)>,
319) {
320    pick.clear();
321    let nblk = scores.len();
322    if nblk == 0 {
323        return;
324    }
325    let any_cand = scores.iter().zip(allow).any(|(s, &a)| s.is_some() && a);
326    let is_cand = |b: usize| scores[b].is_some() && (!any_cand || allow[b]);
327    window.clear();
328    for b in 0..nblk {
329        let c = is_cand(b);
330        let v = if c { scores[b].unwrap_or(ENTMAX_NEG) * p.idx_scale } else { ENTMAX_NEG };
331        let v = if v.is_finite() { v } else { ENTMAX_NEG };
332        window.push((v, b as u32, c));
333    }
334    let m = p.top_m.min(nblk).max(1);
335    // torch.topk: the largest values, descending; ties keep no contract that a
336    // read set depends on (only non-candidates can tie at ENTMAX_NEG).
337    window.select_nth_unstable_by(m - 1, |a, b| b.0.total_cmp(&a.0).then(a.1.cmp(&b.1)));
338    window.truncate(m);
339    window.sort_unstable_by(|a, b| b.0.total_cmp(&a.0).then(a.1.cmp(&b.1)));
340    let xv: Vec<f32> = window.iter().map(|w| w.0).collect();
341    let mut prior = [0f32; TOP_M];
342    entmax15_sorted(&xv, &mut prior[..m]);
343    let k = p.k.min(m).max(1);
344    let mut sum = 0f32;
345    let mut cnt = 0usize;
346    let mut logp = [0f32; TOP_M];
347    for i in 0..k {
348        logp[i] = prior[i].max(p.floor).ln();
349        if window[i].2 {
350            sum += logp[i];
351            cnt += 1;
352        }
353    }
354    let mu = sum / cnt.max(1) as f32;
355    for i in 0..k {
356        if window[i].2 {
357            pick.push((window[i].1, (logp[i] - mu) / p.sigma));
358        }
359    }
360}
361
362// ───────────────────────── the router state ─────────────────────────
363
364/// One layer's router weights.
365pub struct DashLayer {
366    /// `[2·index_dim, hidden]`: the q rows, then the k rows.
367    pub qk_proj: Vec<f32>,
368    pub q_norm: Vec<f32>,
369    pub k_norm: Vec<f32>,
370    pub head_cls: Vec<f32>,
371    pub route: RouteParams,
372}
373
374/// The router: weights, geometry and the sequence state on the host.
375pub struct Dash {
376    pub layers: Vec<DashLayer>,
377    pub block: usize,
378    pub local_blocks: usize,
379    pub local_window: usize,
380    pub index_dim: usize,
381    pub head_dim: usize,
382    pub inv_tok: Vec<f32>,
383    pub image_id: u32,
384    pub page_id: u32,
385    pub rope: Option<PageRope>,
386    /// The answer's page bookkeeping (`_uocr_page_count_guard`).
387    pub guard: Option<PageGuard>,
388    /// `(sin, cos)` of the current position, one per frequency pair.
389    pub cs: Vec<(f32, f32)>,
390    /// The angles of the current rows, one `half` per row.
391    pub angles: Vec<f32>,
392    scratch_angles: Vec<f32>,
393    /// Candidate blocks of the current query.
394    pub allow: Vec<bool>,
395    /// The blocks the device scores per row: remote and candidates, or every
396    /// remote block when none is both. `coff[row]..coff[row + 1]`.
397    pub cands: Vec<u32>,
398    pub coff: Vec<u32>,
399    /// Per layer: completed block summaries `[n, index_dim]`.
400    pub k_bar: Vec<Vec<f32>>,
401    /// Per layer: the keys of the open block.
402    pub open: Vec<Vec<f32>>,
403    /// Whether the graph holds the router state instead of the host.
404    pub on_device: bool,
405    // Scratch.
406    scores: Vec<Option<f32>>,
407    window: Vec<(f32, u32, bool)>,
408    pick: Vec<(u32, f32)>,
409    row: Vec<f32>,
410}
411
412fn sp_usize(sp: &Value, key: &str) -> Result<usize, String> {
413    sp.get(key)
414        .and_then(Value::as_u64)
415        .map(|v| v as usize)
416        .ok_or_else(|| format!("unlimited-ocr: sparse_attention_config has no '{key}'"))
417}
418
419fn sp_list(sp: &Value, key: &str, layers: usize) -> Result<Vec<f32>, String> {
420    let a = sp
421        .get(key)
422        .and_then(Value::as_array)
423        .filter(|a| a.len() == layers)
424        .ok_or_else(|| format!("unlimited-ocr: sparse_attention_config.{key} must list {layers} layers"))?;
425    Ok(a.iter().map(|v| v.as_f64().unwrap_or(0.0) as f32).collect())
426}
427
428impl Dash {
429    pub fn from_model(model: &CmfModel) -> Result<Self, String> {
430        let cfg = model
431            .header
432            .provenance
433            .as_ref()
434            .and_then(|p| p.pointer("/uocr/config"))
435            .ok_or("unlimited-ocr: the file carries no provenance.uocr (reconvert it)")?;
436        let sp = cfg.get("sparse_attention_config").ok_or("unlimited-ocr: config has no sparse_attention_config")?;
437        let layers_n = cfg
438            .get("num_hidden_layers")
439            .and_then(Value::as_u64)
440            .ok_or("unlimited-ocr: config has no num_hidden_layers")? as usize;
441        let block = sp_usize(sp, "sparse_block_size")?;
442        let local_blocks = sp_usize(sp, "sparse_local_block")?;
443        let index_dim = sp_usize(sp, "sparse_index_dim")?;
444        let (hidden, heads) = (model.header.arch.hidden_size, model.header.arch.num_attention_heads);
445        let head_dim = hidden / heads;
446        if index_dim != head_dim {
447            return Err("unlimited-ocr: an index width other than the head width is not supported".into());
448        }
449        let on = |k: &str, want: &str| sp.get(k).and_then(Value::as_str) == Some(want);
450        if !on("training_stage", "dash") || !on("dash_stage0", "head_cls") || !on("sparse_main_rope_mode", "page2d") {
451            return Err("unlimited-ocr: only the dash head_cls page2d router is supported".into());
452        }
453        if sp.get("sparse_rope_page_pairs").and_then(Value::as_u64) != Some(1)
454            || sp.get("sparse_rope_page_mod").and_then(Value::as_u64) != Some(2)
455        {
456            return Err("unlimited-ocr: only one page pair at parity is supported".into());
457        }
458        let scales = sp_list(sp, "dash_scale_per_layer", layers_n)?;
459        let floors = sp_list(sp, "dash_floor_per_layer", layers_n)?;
460        let sigma = sp.get("dash_sigma").and_then(Value::as_f64).unwrap_or(1.0) as f32;
461        let k = sp_usize(sp, "dash_topk_blocks")?;
462        let f = |n: String, len: usize| -> Result<Vec<f32>, String> {
463            let v = crate::dit::cmf_f32(model, &n)?;
464            if v.len() != len {
465                return Err(format!("tensor '{n}' has {} values, expected {len}", v.len()));
466            }
467            Ok(v)
468        };
469        let mut layers = Vec::with_capacity(layers_n);
470        for li in 0..layers_n {
471            let p = format!("model.layers.{li}.self_attn.dash_router");
472            layers.push(DashLayer {
473                qk_proj: {
474                    let mut w = f(format!("{p}.q_proj.weight"), index_dim * hidden)?;
475                    w.extend(f(format!("{p}.k_proj.weight"), index_dim * hidden)?);
476                    w
477                },
478                q_norm: f(format!("{p}.q_norm.weight"), index_dim)?,
479                k_norm: f(format!("{p}.k_norm.weight"), index_dim)?,
480                head_cls: f(format!("{p}.head_cls"), index_dim)?,
481                route: RouteParams {
482                    idx_scale: scales[li] / (index_dim as f32).sqrt(),
483                    floor: floors[li],
484                    sigma,
485                    k,
486                    top_m: TOP_M,
487                },
488            });
489        }
490        let half = head_dim / 2;
491        let theta = sp.get("sparse_rope_tok_theta").and_then(Value::as_f64).unwrap_or(10000.0) as f32;
492        Ok(Self {
493            block,
494            local_blocks,
495            local_window: local_blocks * block,
496            index_dim,
497            head_dim,
498            inv_tok: inv_tok(half, theta),
499            image_id: sp_usize(sp, "image_token_id")? as u32,
500            page_id: sp_usize(sp, "page_token_id")? as u32,
501            rope: None,
502            guard: None,
503            cs: vec![(0.0, 1.0); half],
504            angles: vec![0.0; half],
505            scratch_angles: vec![0.0; half],
506            allow: Vec::new(),
507            cands: Vec::new(),
508            coff: Vec::new(),
509            k_bar: vec![Vec::new(); layers_n],
510            open: vec![Vec::new(); layers_n],
511            on_device: false,
512            scores: Vec::new(),
513            window: Vec::new(),
514            pick: Vec::new(),
515            row: vec![0.0; index_dim],
516            layers,
517        })
518    }
519
520    /// A new sequence over the prompt `ids`.
521    pub fn begin(&mut self, ids: &[u32], eos_id: u32) -> Result<(), String> {
522        self.rope = Some(PageRope::begin(ids, self.image_id, self.page_id, self.block)?);
523        let pages = ids.iter().filter(|&&i| i == self.page_id).count();
524        self.guard = Some(PageGuard::new(pages, self.page_id, eos_id));
525        for v in self.k_bar.iter_mut().chain(self.open.iter_mut()) {
526            v.clear();
527        }
528        Ok(())
529    }
530
531    /// Set the coordinates and candidates of a prompt position.
532    pub fn at_prompt(&mut self, pos: usize) {
533        self.at_prompt_chunk(pos, 1);
534        let rope = self.rope.as_ref().expect("begin ran");
535        let (tok, page) = rope.prompt_coords(pos);
536        self.set_cs(tok, page);
537    }
538
539    /// The angles and candidates of the prompt rows `pos..pos + k`. `allow` is
540    /// left at the last row.
541    pub fn at_prompt_chunk(&mut self, pos: usize, k: usize) {
542        let half = self.head_dim / 2;
543        let mut allow = std::mem::take(&mut self.allow);
544        self.angles.clear();
545        self.angles.resize(k * half, 0.0);
546        self.cands.clear();
547        self.coff.clear();
548        self.coff.push(0);
549        let rope = self.rope.as_ref().expect("begin ran");
550        let nblk = rope.prompt_len().div_ceil(self.block);
551        for i in 0..k {
552            let p = pos + i;
553            let (tok, page) = rope.prompt_coords(p);
554            rope_angles(tok, page, &self.inv_tok, &mut self.angles[i * half..(i + 1) * half]);
555            rope.prefill_allow(p, nblk, &mut allow);
556            Self::push_cands(&mut self.cands, &allow, p, self.block, self.local_blocks);
557            self.coff.push(self.cands.len() as u32);
558        }
559        self.allow = allow;
560    }
561
562    /// Advance over decoded token `id` at `pos` and set its candidates.
563    pub fn at_decode(&mut self, id: u32, pos: usize) {
564        let half = self.head_dim / 2;
565        let rope = self.rope.as_mut().expect("begin ran");
566        let (tok, page) = rope.decode_step(id);
567        let nblk = (pos + 1).div_ceil(self.block);
568        let mut allow = std::mem::take(&mut self.allow);
569        self.rope.as_ref().expect("begin ran").decode_allow(nblk, &mut allow);
570        self.cands.clear();
571        Self::push_cands(&mut self.cands, &allow, pos, self.block, self.local_blocks);
572        self.coff.clear();
573        self.coff.extend([0, self.cands.len() as u32]);
574        self.allow = allow;
575        self.angles.clear();
576        self.angles.resize(half, 0.0);
577        rope_angles(tok, page, &self.inv_tok, &mut self.angles);
578        self.set_cs(tok, page);
579    }
580
581    fn set_cs(&mut self, tok: u32, page: u32) {
582        rope_angles(tok, page, &self.inv_tok, &mut self.scratch_angles);
583        for (o, a) in self.cs.iter_mut().zip(&self.scratch_angles) {
584            *o = a.sin_cos();
585        }
586    }
587
588    /// Row `pos`'s candidates: the remote blocks it may read.
589    fn push_cands(out: &mut Vec<u32>, allow: &[bool], pos: usize, block: usize, local: usize) {
590        let nrem = (pos / block + 1).saturating_sub(local);
591        let at = out.len();
592        out.extend((0..nrem.min(allow.len())).filter(|&b| allow[b]).map(|b| b as u32));
593        if out.len() == at {
594            out.extend(0..nrem as u32);
595        }
596    }
597
598    /// `_GemmaRMSNorm` plus the rotation: a router row in place.
599    fn norm_rotate(x: &mut [f32], w: &[f32], eps: f32, cs: &[(f32, f32)]) {
600        let d = x.len();
601        let mean = x.iter().map(|v| v * v).sum::<f32>() / d as f32;
602        let inv = (mean + eps).sqrt().recip();
603        for (v, &wv) in x.iter_mut().zip(w) {
604            let n = (*v * inv).clamp(-ACT_CLAMP, ACT_CLAMP);
605            *v = (n * (1.0 + wv)).clamp(-ACT_CLAMP, ACT_CLAMP);
606        }
607        rotate(x, cs);
608    }
609
610    /// One position through layer `li`'s router on the host: appends its key
611    /// (closing a block when full) and returns the read set in `self.pick`.
612    fn route_host(&mut self, li: usize, n1: &[f32], pos: usize, eps: f32) {
613        let d = self.index_dim;
614        let hs = n1.len();
615        let l = &self.layers[li];
616        let mut q = vec![0f32; d];
617        let mut k = std::mem::take(&mut self.row);
618        for r in 0..d {
619            let wq = &l.qk_proj[r * hs..(r + 1) * hs];
620            let wk = &l.qk_proj[(d + r) * hs..(d + r + 1) * hs];
621            let (mut a, mut b) = (0f32, 0f32);
622            for c in 0..hs {
623                a += wq[c] * n1[c];
624                b += wk[c] * n1[c];
625            }
626            q[r] = a;
627            k[r] = b;
628        }
629        Self::norm_rotate(&mut q, &l.q_norm, eps, &self.cs);
630        Self::norm_rotate(&mut k, &l.k_norm, eps, &self.cs);
631        // Queries score against the blocks completed before this position's
632        // key: every remote block is complete already.
633        let inv_sqrt = (d as f32).sqrt().recip();
634        let qblk = pos / self.block;
635        let done = self.k_bar[li].len() / d;
636        let nblk = self.allow.len().max(done);
637        self.scores.clear();
638        self.scores.resize(nblk, None);
639        for b in 0..done.min((qblk + 1).saturating_sub(self.local_blocks)) {
640            let kb = &self.k_bar[li][b * d..(b + 1) * d];
641            let z: f32 = q.iter().zip(kb).map(|(x, y)| x * y).sum::<f32>() * inv_sqrt;
642            self.scores[b] = Some(z.clamp(-ACT_CLAMP, ACT_CLAMP));
643        }
644        if self.allow.len() < nblk {
645            self.allow.resize(nblk, false);
646        }
647        let (scores, allow, window, pick) = (&self.scores, &self.allow, &mut self.window, &mut self.pick);
648        route_select(scores, allow, &l.route, window, pick);
649        self.open[li].extend_from_slice(&k);
650        self.row = k;
651        if self.open[li].len() == self.block * d {
652            let mut kb = vec![0f32; d];
653            summarize_block(&self.open[li], &self.layers[li].head_cls, &mut kb);
654            self.k_bar[li].extend_from_slice(&kb);
655            self.open[li].clear();
656        }
657    }
658}
659
660/// The learned chunk summary (`Stage 0`): softmax of the keys against
661/// `head_cls`, the weighted mean key.
662pub fn summarize_block(keys: &[f32], hc: &[f32], out: &mut [f32]) {
663    let d = hc.len();
664    let n = keys.len() / d;
665    let inv_sqrt = (d as f32).sqrt().recip();
666    let mut aw = vec![0f32; n];
667    let mut mx = f32::NEG_INFINITY;
668    for (i, a) in aw.iter_mut().enumerate() {
669        let k = &keys[i * d..(i + 1) * d];
670        *a = (k.iter().zip(hc).map(|(x, y)| x * y).sum::<f32>() * inv_sqrt).clamp(-ACT_CLAMP, ACT_CLAMP);
671        mx = mx.max(*a);
672    }
673    let mut den = 0f32;
674    for a in aw.iter_mut() {
675        *a = (*a - mx).exp();
676        den += *a;
677    }
678    let den = den.max(1e-6);
679    out.iter_mut().for_each(|v| *v = 0.0);
680    for (i, a) in aw.iter().enumerate() {
681        let w = a / den;
682        for (o, &v) in out.iter_mut().zip(&keys[i * d..(i + 1) * d]) {
683            *o += w * v;
684        }
685    }
686}
687
688/// DASH attention for one position on the host: K/V appended to the layer's
689/// cache, the read set from the router. `out` is `[heads·head_dim]`, before
690/// `o_proj`.
691#[allow(clippy::too_many_arguments)]
692pub(crate) fn attention_host(
693    dash: &mut Dash,
694    li: usize,
695    n1: &[f32],
696    wq: &crate::qtensor::QTensor,
697    wk: &crate::qtensor::QTensor,
698    wv: &crate::qtensor::QTensor,
699    cache: &mut crate::kv_cache::LayerKvCache,
700    position: usize,
701    nh: usize,
702    hd: usize,
703    eps: f32,
704    pool: Option<&Pool>,
705    out: &mut [f32],
706) {
707    dash.route_host(li, n1, position, eps);
708    let (mut q, mut k, mut v) = (vec![0f32; nh * hd], vec![0f32; nh * hd], vec![0f32; nh * hd]);
709    wq.matvec(n1, &mut q, pool);
710    wk.matvec(n1, &mut k, pool);
711    wv.matvec(n1, &mut v, pool);
712    for h in 0..nh {
713        rotate(&mut q[h * hd..(h + 1) * hd], &dash.cs);
714        rotate(&mut k[h * hd..(h + 1) * hd], &dash.cs);
715    }
716    cache.append(&k, &v, &[]);
717    let scale = (hd as f32).sqrt().recip();
718    let (block, window) = (dash.block, dash.local_window);
719    let lo = (position + 1).saturating_sub(window);
720    let pick = &dash.pick;
721    let (keys, vals) = (cache.k_heads(), cache.v_heads());
722    let attend = |h: usize, o: &mut [f32]| {
723        let qh = &q[h * hd..(h + 1) * hd];
724        let (kh, vh) = (&keys[h], &vals[h]);
725        let mut sc: Vec<(u32, f32)> = Vec::with_capacity(pick.len() * block + window);
726        let dot = |p: usize| -> f32 {
727            let kr = &kh[p * hd..(p + 1) * hd];
728            qh.iter().zip(kr).map(|(a, b)| a * b).sum::<f32>() * scale
729        };
730        for &(b, bias) in pick {
731            let at = b as usize * block;
732            // The local window wins on overlap.
733            for p in at..(at + block).min(lo) {
734                sc.push((p as u32, dot(p) + bias));
735            }
736        }
737        for p in lo..=position {
738            sc.push((p as u32, dot(p)));
739        }
740        let mx = sc.iter().fold(f32::NEG_INFINITY, |m, s| m.max(s.1));
741        let mut den = 0f32;
742        for s in sc.iter_mut() {
743            s.1 = (s.1 - mx).exp();
744            den += s.1;
745        }
746        let inv = den.recip();
747        o.iter_mut().for_each(|x| *x = 0.0);
748        for &(p, w) in &sc {
749            let w = w * inv;
750            let vr = &vh[p as usize * hd..(p as usize + 1) * hd];
751            for (x, &y) in o.iter_mut().zip(vr) {
752                *x += w * y;
753            }
754        }
755    };
756    let oo = crate::pool::SendMut::new(out.as_mut_ptr());
757    let run = |lo_h: usize, hi_h: usize| {
758        for h in lo_h..hi_h {
759            // SAFETY: each worker owns whole heads of `out`.
760            let o = unsafe { std::slice::from_raw_parts_mut(oo.at(h * hd), hd) };
761            attend(h, o);
762        }
763    };
764    match pool {
765        Some(p) => p.run_rows(nh, &run),
766        None => run(0, nh),
767    }
768}
769
770// ───────────────────────── image layout ─────────────────────────
771
772fn round_half_even(v: f64) -> i64 {
773    let r = v.round();
774    if (v - v.trunc()).abs() == 0.5 && (r as i64) % 2 != 0 { (r - v.signum()) as i64 } else { r as i64 }
775}
776
777/// One axis of PIL's fixed-point bicubic (`precompute_coeffs`): per output
778/// sample its first input and 22-bit weights.
779fn pil_taps(inp: usize, out: usize) -> Vec<(usize, Vec<i32>)> {
780    let bic = |x: f64| -> f64 {
781        const A: f64 = -0.5;
782        let x = x.abs();
783        if x < 1.0 {
784            ((A + 2.0) * x - (A + 3.0)) * x * x + 1.0
785        } else if x < 2.0 {
786            (((x - 5.0) * x + 8.0) * x - 4.0) * A
787        } else {
788            0.0
789        }
790    };
791    let scale = inp as f64 / out as f64;
792    let fs = scale.max(1.0);
793    let support = 2.0 * fs;
794    (0..out)
795        .map(|i| {
796            let center = (i as f64 + 0.5) * scale;
797            let lo = ((center - support + 0.5) as i64).max(0) as usize;
798            let hi = ((center + support + 0.5) as i64).min(inp as i64) as usize;
799            let w: Vec<f64> = (lo..hi).map(|x| bic((x as f64 - center + 0.5) / fs)).collect();
800            let sum: f64 = w.iter().sum();
801            let k = w
802                .iter()
803                .map(|v| {
804                    let v = if sum != 0.0 { v / sum } else { *v } * (1u32 << 22) as f64;
805                    (if v < 0.0 { v - 0.5 } else { v + 0.5 }) as i32
806                })
807                .collect();
808            (lo, k)
809        })
810        .collect()
811}
812
813/// `Image.resize(size)`, whose default is BICUBIC: horizontal, then vertical,
814/// each pass rounded to 8 bits.
815pub fn pil_bicubic(img: &RgbFrame, height: usize, width: usize) -> RgbFrame {
816    let clip = |v: i64| -> u8 { (v >> 22).clamp(0, 255) as u8 };
817    let round = 1i64 << 21;
818    let (h0, w0) = (img.height, img.width);
819    let mut cur = img.data.clone();
820    if width != w0 {
821        let taps = pil_taps(w0, width);
822        let mut out = vec![0u8; h0 * width * 3];
823        for y in 0..h0 {
824            let src = &cur[y * w0 * 3..(y + 1) * w0 * 3];
825            let dst = &mut out[y * width * 3..(y + 1) * width * 3];
826            for (x, (lo, k)) in taps.iter().enumerate() {
827                for c in 0..3 {
828                    let mut ss = round;
829                    for (j, &w) in k.iter().enumerate() {
830                        ss += src[(lo + j) * 3 + c] as i64 * w as i64;
831                    }
832                    dst[x * 3 + c] = clip(ss);
833                }
834            }
835        }
836        cur = out;
837    }
838    if height != h0 {
839        let taps = pil_taps(h0, height);
840        let mut out = vec![0u8; height * width * 3];
841        for (y, (lo, k)) in taps.iter().enumerate() {
842            for x in 0..width * 3 {
843                let mut ss = round;
844                for (j, &w) in k.iter().enumerate() {
845                    ss += cur[(lo + j) * width * 3 + x] as i64 * w as i64;
846                }
847                out[y * width * 3 + x] = clip(ss);
848            }
849        }
850        cur = out;
851    }
852    RgbFrame { width, height, data: cur }
853}
854
855/// `ImageOps.pad(image, (size, size), color=127)`: BICUBIC into the square,
856/// centered on mid gray.
857pub fn pad_square(img: &RgbFrame, size: usize) -> RgbFrame {
858    let (w, h) = (img.width as f64, img.height as f64);
859    let (mut nw, mut nh) = (size, size);
860    if img.width != img.height {
861        if img.width > img.height {
862            nh = round_half_even(h / w * size as f64) as usize;
863        } else {
864            nw = round_half_even(w / h * size as f64) as usize;
865        }
866    }
867    let r = if (nh, nw) == (img.height, img.width) {
868        img.clone()
869    } else {
870        pil_bicubic(img, nh, nw)
871    };
872    if nw == size && nh == size {
873        return r;
874    }
875    let mut data = vec![127u8; size * size * 3];
876    let x0 = round_half_even((size - nw) as f64 * 0.5) as usize;
877    let y0 = round_half_even((size - nh) as f64 * 0.5) as usize;
878    for y in 0..nh {
879        let d = ((y0 + y) * size + x0) * 3;
880        let s = y * nw * 3;
881        data[d..d + nw * 3].copy_from_slice(&r.data[s..s + nw * 3]);
882    }
883    RgbFrame { width: size, height: size, data }
884}
885
886/// `find_closest_aspect_ratio` + the target list of `dynamic_preprocess`:
887/// `(cols, rows)`.
888fn closest_grid(w: usize, h: usize, tile: usize) -> (usize, usize) {
889    let mut ratios: Vec<(usize, usize)> = Vec::new();
890    for i in 1..=MAX_CROPS {
891        for j in 1..=MAX_CROPS {
892            if i * j >= MIN_CROPS && i * j <= MAX_CROPS {
893                ratios.push((i, j));
894            }
895        }
896    }
897    // `sorted(set, key=i·j)`: stable over a set's iteration order, which for
898    // these small int tuples is what CPython's tuple hash gives. Ties only
899    // matter at exactly equal aspect ratios with equal areas, which cannot be
900    // two different (i, j); the order among equal areas is irrelevant.
901    ratios.sort_by_key(|r| r.0 * r.1);
902    let aspect = w as f64 / h as f64;
903    let area = (w * h) as f64;
904    let mut best = (1usize, 1usize);
905    let mut best_diff = f64::INFINITY;
906    for &(i, j) in &ratios {
907        let diff = (aspect - i as f64 / j as f64).abs();
908        if diff < best_diff {
909            best_diff = diff;
910            best = (i, j);
911        } else if diff == best_diff && area > 0.5 * (tile * tile * i * j) as f64 {
912            best = (i, j);
913        }
914    }
915    best
916}
917
918/// One image's views: the global `[3, 1024, 1024]` and the tiles
919/// `[3, 640, 640]` with the grid `(cols, rows)`.
920pub struct PageViews {
921    pub global: Vec<f32>,
922    pub tiles: Vec<Vec<f32>>,
923    pub cols: usize,
924    pub rows: usize,
925}
926
927fn to_chw(img: &RgbFrame, top: usize, left: usize, side: usize) -> Vec<f32> {
928    let mut out = vec![0f32; 3 * side * side];
929    for c in 0..3 {
930        for y in 0..side {
931            let line = ((top + y) * img.width + left) * 3 + c;
932            for x in 0..side {
933                // ToTensor (/255) then Normalize(0.5, 0.5), both f32.
934                let v = img.data[line + x * 3] as f32 / 255.0;
935                out[(c * side + y) * side + x] = (v - 0.5) / 0.5;
936            }
937        }
938    }
939    out
940}
941
942/// `_uocr_build_inputs` for one image.
943pub fn page_views(img: &RgbFrame, tiled: bool) -> PageViews {
944    let global = to_chw(&pad_square(img, BASE_SIZE), 0, 0, BASE_SIZE);
945    if !tiled || (img.width <= IMAGE_SIZE && img.height <= IMAGE_SIZE) {
946        return PageViews { global, tiles: Vec::new(), cols: 1, rows: 1 };
947    }
948    let (cols, rows) = closest_grid(img.width, img.height, IMAGE_SIZE);
949    let resized = pil_bicubic(img, IMAGE_SIZE * rows, IMAGE_SIZE * cols);
950    let mut tiles = Vec::with_capacity(cols * rows);
951    for y in 0..rows {
952        for x in 0..cols {
953            tiles.push(to_chw(&resized, y * IMAGE_SIZE, x * IMAGE_SIZE, IMAGE_SIZE));
954        }
955    }
956    PageViews { global, tiles, cols, rows }
957}
958
959/// Image tokens of a page.
960pub fn page_tokens(cols: usize, rows: usize) -> usize {
961    let nq = (IMAGE_SIZE / PATCH).div_ceil(DOWNSAMPLE);
962    let nqb = (BASE_SIZE / PATCH).div_ceil(DOWNSAMPLE);
963    let mut n = nqb * (nqb + 1) + 1;
964    if cols > 1 || rows > 1 {
965        n += (nq * cols + 1) * (nq * rows);
966    }
967    n
968}
969
970// ───────────────────────── the tower ─────────────────────────
971
972/// The nearest-free antialiased bicubic of a square position table
973/// (`F.interpolate(mode="bicubic", antialias=True)`): `[src², d]` → `[dst², d]`.
974fn resize_table(t: &[f32], src: usize, dst: usize, d: usize) -> Vec<f32> {
975    if src == dst {
976        return t.to_vec();
977    }
978    let weights = |src: usize, dst: usize| -> Vec<(usize, Vec<f32>)> {
979        // torch's antialiased bicubic follows PIL: support 2·scale, a = -0.5.
980        let scale = src as f64 / dst as f64;
981        let fs = scale.max(1.0);
982        let support = 2.0 * fs;
983        (0..dst)
984            .map(|i| {
985                let center = (i as f64 + 0.5) * scale;
986                let lo = ((center - support + 0.5) as i64).max(0) as usize;
987                let hi = ((center + support + 0.5) as i64).min(src as i64) as usize;
988                let bic = |x: f64| -> f64 {
989                    let a = -0.5;
990                    let x = x.abs();
991                    if x < 1.0 {
992                        ((a + 2.0) * x - (a + 3.0)) * x * x + 1.0
993                    } else if x < 2.0 {
994                        (((x - 5.0) * x + 8.0) * x - 4.0) * a
995                    } else {
996                        0.0
997                    }
998                };
999                let w: Vec<f64> = (lo..hi).map(|j| bic((j as f64 + 0.5 - center) / fs)).collect();
1000                let s: f64 = w.iter().sum();
1001                (lo, w.iter().map(|v| (v / s) as f32).collect())
1002            })
1003            .collect()
1004    };
1005    let w = weights(src, dst);
1006    // Horizontal, then vertical.
1007    let mut h = vec![0f32; src * dst * d];
1008    for y in 0..src {
1009        for x in 0..dst {
1010            let (lo, ws) = &w[x];
1011            for c in 0..d {
1012                let mut v = 0f32;
1013                for (j, &wt) in ws.iter().enumerate() {
1014                    v += wt * t[(y * src + lo + j) * d + c];
1015                }
1016                h[(y * dst + x) * d + c] = v;
1017            }
1018        }
1019    }
1020    let mut out = vec![0f32; dst * dst * d];
1021    for y in 0..dst {
1022        let (lo, ws) = &w[y];
1023        for x in 0..dst {
1024            for c in 0..d {
1025                let mut v = 0f32;
1026                for (j, &wt) in ws.iter().enumerate() {
1027                    v += wt * h[((lo + j) * dst + x) * d + c];
1028                }
1029                out[(y * dst + x) * d + c] = v;
1030            }
1031        }
1032    }
1033    out
1034}
1035
1036fn vec_of(model: &Arc<CmfModel>, name: &str, len: usize) -> Result<Vec<f32>, String> {
1037    let v = crate::dit::cmf_f32(model, name)?;
1038    if v.len() != len {
1039        return Err(format!("tensor '{name}' has {} values, expected {len}", v.len()));
1040    }
1041    Ok(v)
1042}
1043
1044struct SamBlock {
1045    n1_w: Vec<f32>,
1046    n1_b: Vec<f32>,
1047    qkv: Mat,
1048    qkv_b: Vec<f32>,
1049    proj: Mat,
1050    proj_b: Vec<f32>,
1051    rel_h: Vec<f32>,
1052    rel_w: Vec<f32>,
1053    /// `rel_h` then `rel_w`, for the device.
1054    rel_cat: Vec<f32>,
1055    rel_len: usize,
1056    n2_w: Vec<f32>,
1057    n2_b: Vec<f32>,
1058    fc1: Mat,
1059    fc1_b: Vec<f32>,
1060    fc2: Mat,
1061    fc2_b: Vec<f32>,
1062    window: usize,
1063}
1064
1065struct ClipBlock {
1066    n1_w: Vec<f32>,
1067    n1_b: Vec<f32>,
1068    qkv: Mat,
1069    qkv_b: Vec<f32>,
1070    out: Mat,
1071    out_b: Vec<f32>,
1072    n2_w: Vec<f32>,
1073    n2_b: Vec<f32>,
1074    fc1: Mat,
1075    fc1_b: Vec<f32>,
1076    fc2: Mat,
1077    fc2_b: Vec<f32>,
1078}
1079
1080/// The DeepEncoder and its projector.
1081pub struct UocrTower {
1082    patch: Mat,
1083    patch_b: Vec<f32>,
1084    pos: Vec<f32>,
1085    sam: Vec<SamBlock>,
1086    neck0: Vec<f32>,
1087    neck1_w: Vec<f32>,
1088    neck1_b: Vec<f32>,
1089    neck2: Vec<f32>,
1090    neck3_w: Vec<f32>,
1091    neck3_b: Vec<f32>,
1092    net2: Vec<f32>,
1093    net3: Vec<f32>,
1094    /// The four convs as `[co, k·k, ci]`, for the device.
1095    conv_w: [Vec<f32>; 4],
1096    cls: Vec<f32>,
1097    clip_pos: Vec<f32>,
1098    pre_w: Vec<f32>,
1099    pre_b: Vec<f32>,
1100    clip: Vec<ClipBlock>,
1101    proj: Mat,
1102    proj_b: Vec<f32>,
1103    pub newline: Vec<f32>,
1104    pub view_sep: Vec<f32>,
1105    pool: Option<Arc<Pool>>,
1106    /// Interpolated tables and relative positions per input grid.
1107    cache: std::sync::Mutex<std::collections::HashMap<usize, Arc<GridTables>>>,
1108}
1109
1110struct GridTables {
1111    pos: Vec<f32>,
1112    clip_pos: Vec<f32>,
1113    /// Per global block: `[2g-1, 64]` relative positions, h and w.
1114    rel_global: Vec<(Vec<f32>, Vec<f32>)>,
1115}
1116
1117const SAM_D: usize = 768;
1118const SAM_HEADS: usize = 12;
1119const SAM_HD: usize = 64;
1120const SAM_INTER: usize = 3072;
1121const SAM_GLOBAL: [usize; 4] = [2, 5, 8, 11];
1122const SAM_WINDOW: usize = 14;
1123const SAM_SIDE: usize = 64;
1124const SAM_EPS: f64 = 1e-6;
1125const CLIP_D: usize = 1024;
1126const CLIP_HEADS: usize = 16;
1127const CLIP_HD: usize = 64;
1128const CLIP_INTER: usize = 4096;
1129const CLIP_LAYERS: usize = 24;
1130const CLIP_SIDE: usize = 16;
1131const CLIP_EPS: f64 = 1e-5;
1132const OUT_D: usize = 1280;
1133
1134/// `F.interpolate(mode="linear")` of a `[l, c]` table to `[n, c]`.
1135fn resize_rel(t: &[f32], c: usize, n: usize) -> Vec<f32> {
1136    let l = t.len() / c;
1137    if l == n {
1138        return t.to_vec();
1139    }
1140    let mut out = vec![0f32; n * c];
1141    let scale = l as f64 / n as f64;
1142    for i in 0..n {
1143        let x = ((i as f64 + 0.5) * scale - 0.5).max(0.0);
1144        let x0 = x.floor() as usize;
1145        let x1 = (x0 + 1).min(l - 1);
1146        let f = (x - x0 as f64) as f32;
1147        for k in 0..c {
1148            out[i * c + k] = t[x0 * c + k] * (1.0 - f) + t[x1 * c + k] * f;
1149        }
1150    }
1151    out
1152}
1153
1154/// A bias-free conv over an HWC grid with weights `[co, ci, k, k]`:
1155/// `(output, output side)`.
1156#[allow(clippy::too_many_arguments)]
1157fn conv_same(x: &[f32], g: usize, ci: usize, w: &[f32], co: usize, k: usize, stride: usize, pad: usize) -> (Vec<f32>, usize) {
1158    let g2 = (g + 2 * pad - k) / stride + 1;
1159    let mut out = vec![0f32; g2 * g2 * co];
1160    let row = |oy: usize, ox: usize, o: &mut [f32]| {
1161        for ky in 0..k {
1162            let Some(iy) = (oy * stride + ky).checked_sub(pad).filter(|&v| v < g) else { continue };
1163            for kx in 0..k {
1164                let Some(ix) = (ox * stride + kx).checked_sub(pad).filter(|&v| v < g) else { continue };
1165                let src = &x[(iy * g + ix) * ci..(iy * g + ix + 1) * ci];
1166                for (co_i, ov) in o.iter_mut().enumerate() {
1167                    let base = co_i * ci * k * k + ky * k + kx;
1168                    let mut acc = 0f32;
1169                    for (ci_i, &s) in src.iter().enumerate() {
1170                        acc += w[base + ci_i * k * k] * s;
1171                    }
1172                    *ov += acc;
1173                }
1174            }
1175        }
1176    };
1177    let p = Pool::from_env();
1178    let oo = crate::pool::SendMut::new(out.as_mut_ptr());
1179    let f = |lo: usize, hi: usize| {
1180        for r in lo..hi {
1181            let (oy, ox) = (r / g2, r % g2);
1182            // SAFETY: disjoint output cells per worker.
1183            let o = unsafe { std::slice::from_raw_parts_mut(oo.at(r * co), co) };
1184            row(oy, ox, o);
1185        }
1186    };
1187    match p.as_deref() {
1188        Some(pool) => pool.run_rows(g2 * g2, &f),
1189        None => f(0, g2 * g2),
1190    }
1191    (out, g2)
1192}
1193
1194/// `LayerNorm2d` over the channels of an HWC grid.
1195fn norm2d(x: &mut [f32], c: usize, w: &[f32], b: &[f32]) {
1196    for row in x.chunks_exact_mut(c) {
1197        let u = row.iter().sum::<f32>() / c as f32;
1198        let s = row.iter().map(|v| (v - u) * (v - u)).sum::<f32>() / c as f32;
1199        let inv = (s + 1e-6).sqrt().recip();
1200        for ((v, &ww), &bb) in row.iter_mut().zip(w).zip(b) {
1201            *v = ww * ((*v - u) * inv) + bb;
1202        }
1203    }
1204}
1205
1206impl UocrTower {
1207    pub fn from_model(model: &Arc<CmfModel>, pool: Option<Arc<Pool>>) -> Result<Self, String> {
1208        let mut sam = Vec::with_capacity(12);
1209        for i in 0..12 {
1210            let p = format!("vis.sam.blocks.{i}");
1211            let global = SAM_GLOBAL.contains(&i);
1212            let rel_len = if global { 2 * SAM_SIDE - 1 } else { 2 * SAM_WINDOW - 1 };
1213            let d = SAM_D;
1214            sam.push(SamBlock {
1215                n1_w: vec_of(model, &format!("{p}.norm1.weight"), d)?,
1216                n1_b: vec_of(model, &format!("{p}.norm1.bias"), d)?,
1217                qkv: Mat::load(model, &format!("{p}.attn.qkv.weight"), 3 * d, d)?,
1218                qkv_b: vec_of(model, &format!("{p}.attn.qkv.bias"), 3 * d)?,
1219                proj: Mat::load(model, &format!("{p}.attn.proj.weight"), d, d)?,
1220                proj_b: vec_of(model, &format!("{p}.attn.proj.bias"), d)?,
1221                rel_h: vec_of(model, &format!("{p}.attn.rel_pos_h"), rel_len * SAM_HD)?,
1222                rel_w: vec_of(model, &format!("{p}.attn.rel_pos_w"), rel_len * SAM_HD)?,
1223                rel_cat: {
1224                    let mut v = vec_of(model, &format!("{p}.attn.rel_pos_h"), rel_len * SAM_HD)?;
1225                    v.extend(vec_of(model, &format!("{p}.attn.rel_pos_w"), rel_len * SAM_HD)?);
1226                    v
1227                },
1228                rel_len,
1229                n2_w: vec_of(model, &format!("{p}.norm2.weight"), d)?,
1230                n2_b: vec_of(model, &format!("{p}.norm2.bias"), d)?,
1231                fc1: Mat::load(model, &format!("{p}.mlp.lin1.weight"), SAM_INTER, d)?,
1232                fc1_b: vec_of(model, &format!("{p}.mlp.lin1.bias"), SAM_INTER)?,
1233                fc2: Mat::load(model, &format!("{p}.mlp.lin2.weight"), d, SAM_INTER)?,
1234                fc2_b: vec_of(model, &format!("{p}.mlp.lin2.bias"), d)?,
1235                window: if global { 0 } else { SAM_WINDOW },
1236            });
1237        }
1238        let mut clip = Vec::with_capacity(CLIP_LAYERS);
1239        for i in 0..CLIP_LAYERS {
1240            let p = format!("vis.clip.transformer.layers.{i}");
1241            let d = CLIP_D;
1242            clip.push(ClipBlock {
1243                n1_w: vec_of(model, &format!("{p}.layer_norm1.weight"), d)?,
1244                n1_b: vec_of(model, &format!("{p}.layer_norm1.bias"), d)?,
1245                qkv: Mat::load(model, &format!("{p}.self_attn.qkv_proj.weight"), 3 * d, d)?,
1246                qkv_b: vec_of(model, &format!("{p}.self_attn.qkv_proj.bias"), 3 * d)?,
1247                out: Mat::load(model, &format!("{p}.self_attn.out_proj.weight"), d, d)?,
1248                out_b: vec_of(model, &format!("{p}.self_attn.out_proj.bias"), d)?,
1249                n2_w: vec_of(model, &format!("{p}.layer_norm2.weight"), d)?,
1250                n2_b: vec_of(model, &format!("{p}.layer_norm2.bias"), d)?,
1251                fc1: Mat::load(model, &format!("{p}.mlp.fc1.weight"), CLIP_INTER, d)?,
1252                fc1_b: vec_of(model, &format!("{p}.mlp.fc1.bias"), CLIP_INTER)?,
1253                fc2: Mat::load(model, &format!("{p}.mlp.fc2.weight"), d, CLIP_INTER)?,
1254                fc2_b: vec_of(model, &format!("{p}.mlp.fc2.bias"), d)?,
1255            });
1256        }
1257        // `[co, ci, k, k]` → `[co, k·k, ci]`.
1258        let reorder = |w: &[f32], ci: usize, co: usize, k: usize| -> Vec<f32> {
1259            let mut out = vec![0f32; w.len()];
1260            for c in 0..co {
1261                for t in 0..k * k {
1262                    for i in 0..ci {
1263                        out[(c * k * k + t) * ci + i] = w[(c * ci + i) * k * k + t];
1264                    }
1265                }
1266            }
1267            out
1268        };
1269        let neck0 = vec_of(model, "vis.sam.neck.0.weight", 256 * SAM_D)?;
1270        let neck2 = vec_of(model, "vis.sam.neck.2.weight", 256 * 256 * 9)?;
1271        let net2 = vec_of(model, "vis.sam.net_2.weight", 512 * 256 * 9)?;
1272        let net3 = vec_of(model, "vis.sam.net_3.weight", 1024 * 512 * 9)?;
1273        let conv_w = [
1274            reorder(&neck0, SAM_D, 256, 1),
1275            reorder(&neck2, 256, 256, 3),
1276            reorder(&net2, 256, 512, 3),
1277            reorder(&net3, 512, 1024, 3),
1278        ];
1279        Ok(Self {
1280            patch: Mat::load(model, "vis.sam.patch_embed.proj.weight", SAM_D, 3 * PATCH * PATCH)?,
1281            patch_b: vec_of(model, "vis.sam.patch_embed.proj.bias", SAM_D)?,
1282            pos: vec_of(model, "vis.sam.pos_embed", SAM_SIDE * SAM_SIDE * SAM_D)?,
1283            sam,
1284            neck0,
1285            neck1_w: vec_of(model, "vis.sam.neck.1.weight", 256)?,
1286            neck1_b: vec_of(model, "vis.sam.neck.1.bias", 256)?,
1287            neck2,
1288            neck3_w: vec_of(model, "vis.sam.neck.3.weight", 256)?,
1289            neck3_b: vec_of(model, "vis.sam.neck.3.bias", 256)?,
1290            net2,
1291            net3,
1292            conv_w,
1293            cls: vec_of(model, "vis.clip.embeddings.class_embedding", CLIP_D)?,
1294            clip_pos: vec_of(model, "vis.clip.embeddings.position_embedding.weight", (CLIP_SIDE * CLIP_SIDE + 1) * CLIP_D)?,
1295            pre_w: vec_of(model, "vis.clip.pre_layrnorm.weight", CLIP_D)?,
1296            pre_b: vec_of(model, "vis.clip.pre_layrnorm.bias", CLIP_D)?,
1297            clip,
1298            proj: Mat::load(model, "vis.projector.layers.weight", OUT_D, 2 * CLIP_D)?,
1299            proj_b: vec_of(model, "vis.projector.layers.bias", OUT_D)?,
1300            newline: vec_of(model, "vis.image_newline", OUT_D)?,
1301            view_sep: vec_of(model, "vis.view_seperator", OUT_D)?,
1302            pool,
1303            cache: std::sync::Mutex::new(std::collections::HashMap::new()),
1304        })
1305    }
1306
1307    fn tables(&self, g: usize) -> Arc<GridTables> {
1308        let mut c = self.cache.lock().unwrap();
1309        if let Some(t) = c.get(&g) {
1310            return t.clone();
1311        }
1312        let cg = g / 4;
1313        let mut clip_pos = self.clip_pos[..CLIP_D].to_vec();
1314        clip_pos.extend(resize_table(&self.clip_pos[CLIP_D..], CLIP_SIDE, cg, CLIP_D));
1315        let t = Arc::new(GridTables {
1316            pos: resize_table(&self.pos, SAM_SIDE, g, SAM_D),
1317            clip_pos,
1318            rel_global: SAM_GLOBAL
1319                .iter()
1320                .map(|&i| {
1321                    let b = &self.sam[i];
1322                    (resize_rel(&b.rel_h, SAM_HD, 2 * g - 1), resize_rel(&b.rel_w, SAM_HD, 2 * g - 1))
1323                })
1324                .collect(),
1325        });
1326        c.insert(g, t.clone());
1327        t
1328    }
1329
1330    /// A SAM attention over groups of `n` tokens on an `s`×`s` grid with
1331    /// decomposed relative positions; `x`/`out` hold `groups·n` rows.
1332    #[allow(clippy::too_many_arguments)]
1333    fn sam_attention(&self, b: &SamBlock, x: &[f32], groups: usize, s: usize, rel_h: &[f32], rel_w: &[f32], out: &mut [f32]) {
1334        let pool = self.pool.as_deref();
1335        let n = s * s;
1336        let rows = groups * n;
1337        let (d, nh, hd) = (SAM_D, SAM_HEADS, SAM_HD);
1338        let mut qkv = vec![0f32; rows * 3 * d];
1339        b.qkv.matmat(x, rows, Some(&b.qkv_b), &mut qkv, pool);
1340        let scale = (hd as f32).sqrt().recip();
1341        let center = s - 1;
1342        let mut att = vec![0f32; rows * d];
1343        let aa = crate::pool::SendMut::new(att.as_mut_ptr());
1344        // qkv is [row, 3, heads, hd]; the score of (qi, kj) is
1345        // q·k·scale + q·Rh[qy-ky] + q·Rw[qx-kx].
1346        let one = |gi: usize, h: usize, qi: usize, sc: &mut [f32], rh: &mut [f32], rw: &mut [f32]| {
1347            let at = |r: usize, part: usize| &qkv[(gi * n + r) * 3 * d + part * d + h * hd..(gi * n + r) * 3 * d + part * d + (h + 1) * hd];
1348            let q = at(qi, 0);
1349            let (qy, qx) = (qi / s, qi % s);
1350            for ky in 0..s {
1351                let r = &rel_h[(qy + center - ky) * hd..(qy + center - ky + 1) * hd];
1352                rh[ky] = q.iter().zip(r).map(|(a, b)| a * b).sum();
1353            }
1354            for kx in 0..s {
1355                let r = &rel_w[(qx + center - kx) * hd..(qx + center - kx + 1) * hd];
1356                rw[kx] = q.iter().zip(r).map(|(a, b)| a * b).sum();
1357            }
1358            let mut mx = f32::NEG_INFINITY;
1359            for (kj, v) in sc.iter_mut().enumerate() {
1360                let k = at(kj, 1);
1361                let dot: f32 = q.iter().zip(k).map(|(a, b)| a * b).sum();
1362                *v = dot * scale + rh[kj / s] + rw[kj % s];
1363                mx = mx.max(*v);
1364            }
1365            let mut den = 0f32;
1366            for v in sc.iter_mut() {
1367                *v = (*v - mx).exp();
1368                den += *v;
1369            }
1370            let inv = den.recip();
1371            // SAFETY: one (row, head) slot per task.
1372            let o = unsafe { std::slice::from_raw_parts_mut(aa.at((gi * n + qi) * d + h * hd), hd) };
1373            o.iter_mut().for_each(|v| *v = 0.0);
1374            for (kj, &w) in sc.iter().enumerate() {
1375                let w = w * inv;
1376                for (a, &v) in o.iter_mut().zip(at(kj, 2)) {
1377                    *a += w * v;
1378                }
1379            }
1380        };
1381        let total = rows * nh;
1382        let f = |lo: usize, hi: usize| {
1383            let (mut sc, mut rh, mut rw) = (vec![0f32; n], vec![0f32; s], vec![0f32; s]);
1384            for t in lo..hi {
1385                let (r, h) = (t / nh, t % nh);
1386                one(r / n, h, r % n, &mut sc, &mut rh, &mut rw);
1387            }
1388        };
1389        match pool {
1390            Some(p) => p.run_rows(total, &f),
1391            None => f(0, total),
1392        }
1393        b.proj.matmat(&att, rows, Some(&b.proj_b), out, pool);
1394    }
1395
1396    /// `[3, side, side]` pixels → the SAM channels `[g/4², 1024]` (HWC) and
1397    /// the CLIP tokens without the class token, `[g/4², 1024]`.
1398    fn encode(&self, px: &[f32], side: usize) -> (Vec<f32>, Vec<f32>) {
1399        let pool = self.pool.as_deref();
1400        let g = side / PATCH;
1401        let n = g * g;
1402        let d = SAM_D;
1403        let tb = self.tables(g);
1404        let prof = std::env::var_os("CMF_OCR_PROF").is_some();
1405        let t0 = std::time::Instant::now();
1406        let mut rows = vec![0f32; n * 3 * PATCH * PATCH];
1407        for y in 0..g {
1408            for x in 0..g {
1409                let o = &mut rows[(y * g + x) * 3 * PATCH * PATCH..(y * g + x + 1) * 3 * PATCH * PATCH];
1410                let mut k = 0;
1411                for c in 0..3 {
1412                    for py in 0..PATCH {
1413                        let s = (c * side + y * PATCH + py) * side + x * PATCH;
1414                        o[k..k + PATCH].copy_from_slice(&px[s..s + PATCH]);
1415                        k += PATCH;
1416                    }
1417                }
1418            }
1419        }
1420        let mut h = vec![0f32; n * d];
1421        self.patch.matmat(&rows, n, Some(&self.patch_b), &mut h, pool);
1422        for (a, &p) in h.iter_mut().zip(&tb.pos) {
1423            *a += p;
1424        }
1425        let mut dev_out = Vec::new();
1426        let device_sam = crate::gpu::enabled_here()
1427            && std::env::var("CMF_OCR_GPU").as_deref() != Ok("0")
1428            && self.sam_device(&h, g, &tb, &mut dev_out);
1429        if device_sam {
1430            let t_sam = t0.elapsed().as_secs_f64();
1431            let clip = self.clip_forward(&dev_out, g / 4, &tb, pool);
1432            if prof {
1433                eprintln!("uocr tower {side}² (device): sam+neck {t_sam:.3} s, clip {:.3} s", t0.elapsed().as_secs_f64() - t_sam);
1434            }
1435            return (dev_out, clip);
1436        }
1437        let mut hn = vec![0f32; n * d];
1438        let mut res = vec![0f32; n * d];
1439        let mut ff = vec![0f32; n * SAM_INTER];
1440        let mut gi = 0usize;
1441        for b in &self.sam {
1442            layer_norm_rows(&h, &b.n1_w, &b.n1_b, SAM_EPS, &mut hn, pool);
1443            if b.window == 0 {
1444                let (rh, rw) = &tb.rel_global[gi];
1445                gi += 1;
1446                self.sam_attention(b, &hn, 1, g, rh, rw, &mut res);
1447            } else {
1448                // window_partition with bottom/right zero padding.
1449                let w = b.window;
1450                let gp = g.div_ceil(w) * w;
1451                let wn = gp / w;
1452                let mut win = vec![0f32; wn * wn * w * w * d];
1453                for wy in 0..wn {
1454                    for wx in 0..wn {
1455                        for iy in 0..w {
1456                            let y = wy * w + iy;
1457                            if y >= g {
1458                                continue;
1459                            }
1460                            for ix in 0..w {
1461                                let x = wx * w + ix;
1462                                if x >= g {
1463                                    continue;
1464                                }
1465                                let dst = ((wy * wn + wx) * w * w + iy * w + ix) * d;
1466                                win[dst..dst + d].copy_from_slice(&hn[(y * g + x) * d..(y * g + x + 1) * d]);
1467                            }
1468                        }
1469                    }
1470                }
1471                let mut wo = vec![0f32; win.len()];
1472                self.sam_attention(b, &win, wn * wn, w, &b.rel_h, &b.rel_w, &mut wo);
1473                debug_assert_eq!(b.rel_len, 2 * w - 1);
1474                for wy in 0..wn {
1475                    for wx in 0..wn {
1476                        for iy in 0..w {
1477                            let y = wy * w + iy;
1478                            if y >= g {
1479                                continue;
1480                            }
1481                            for ix in 0..w {
1482                                let x = wx * w + ix;
1483                                if x >= g {
1484                                    continue;
1485                                }
1486                                let src = ((wy * wn + wx) * w * w + iy * w + ix) * d;
1487                                res[(y * g + x) * d..(y * g + x + 1) * d].copy_from_slice(&wo[src..src + d]);
1488                            }
1489                        }
1490                    }
1491                }
1492            }
1493            for (a, &r) in h.iter_mut().zip(&res) {
1494                *a += r;
1495            }
1496            layer_norm_rows(&h, &b.n2_w, &b.n2_b, SAM_EPS, &mut hn, pool);
1497            b.fc1.matmat(&hn, n, Some(&b.fc1_b), &mut ff, pool);
1498            gelu_rows(&mut ff, pool);
1499            b.fc2.matmat(&ff, n, Some(&b.fc2_b), &mut res, pool);
1500            for (a, &r) in h.iter_mut().zip(&res) {
1501                *a += r;
1502            }
1503        }
1504        let t_sam = t0.elapsed().as_secs_f64();
1505        // The neck: 1×1 and 3×3 convs with LayerNorm2d, then two strided 3×3.
1506        let (mut c1, _) = conv_same(&h, g, d, &self.neck0, 256, 1, 1, 0);
1507        norm2d(&mut c1, 256, &self.neck1_w, &self.neck1_b);
1508        let (mut c2, _) = conv_same(&c1, g, 256, &self.neck2, 256, 3, 1, 1);
1509        norm2d(&mut c2, 256, &self.neck3_w, &self.neck3_b);
1510        let (c3, g2) = conv_same(&c2, g, 256, &self.net2, 512, 3, 2, 1);
1511        let (sam_out, g3) = conv_same(&c3, g2, 512, &self.net3, 1024, 3, 2, 1);
1512        let t_neck = t0.elapsed().as_secs_f64();
1513        let clip = self.clip_forward(&sam_out, g3, &tb, pool);
1514        if prof {
1515            eprintln!(
1516                "uocr tower {side}²: sam {t_sam:.3} s, neck {:.3} s, clip {:.3} s",
1517                t_neck - t_sam,
1518                t0.elapsed().as_secs_f64() - t_neck
1519            );
1520        }
1521        (sam_out, clip)
1522    }
1523
1524    /// SAM on the device: `false` = refused.
1525    fn sam_device(&self, h: &[f32], g: usize, tb: &GridTables, out: &mut Vec<f32>) -> bool {
1526        let layers = self
1527            .sam
1528            .iter()
1529            .map(|b| crate::gpu::SamLayer {
1530                ln1_w: &b.n1_w,
1531                ln1_b: &b.n1_b,
1532                qkv: b.qkv.weights(),
1533                qkv_b: &b.qkv_b,
1534                proj: b.proj.weights(),
1535                proj_b: &b.proj_b,
1536                rel: &b.rel_cat,
1537                ln2_w: &b.n2_w,
1538                ln2_b: &b.n2_b,
1539                fc1: b.fc1.weights(),
1540                fc1_b: &b.fc1_b,
1541                fc2: b.fc2.weights(),
1542                fc2_b: &b.fc2_b,
1543                window: b.window,
1544            })
1545            .collect();
1546        let conv = |w: &'_ [f32], ci: usize, co: usize, k: usize, stride: usize, pad: usize, norm: bool| crate::gpu::SamConv {
1547            w: unsafe { std::slice::from_raw_parts(w.as_ptr(), w.len()) },
1548            ci,
1549            co,
1550            k,
1551            stride,
1552            pad,
1553            norm,
1554        };
1555        let args = crate::gpu::SamArgs {
1556            key: self as *const Self as u64,
1557            layers,
1558            convs: vec![
1559                conv(&self.conv_w[0], SAM_D, 256, 1, 1, 0, true),
1560                conv(&self.conv_w[1], 256, 256, 3, 1, 1, true),
1561                conv(&self.conv_w[2], 256, 512, 3, 2, 1, false),
1562                conv(&self.conv_w[3], 512, 1024, 3, 2, 1, false),
1563            ],
1564            norms: vec![&self.neck1_w, &self.neck1_b, &self.neck3_w, &self.neck3_b],
1565            rel_global: tb
1566                .rel_global
1567                .iter()
1568                .map(|(rh, rw)| {
1569                    let mut v = rh.clone();
1570                    v.extend_from_slice(rw);
1571                    v
1572                })
1573                .collect(),
1574            g,
1575            d: SAM_D,
1576            heads: SAM_HEADS,
1577            head_dim: SAM_HD,
1578            inter: SAM_INTER,
1579            eps: SAM_EPS,
1580        };
1581        crate::gpu::sam_encode(&args, h, out)
1582    }
1583
1584    /// CLIP over SAM's channels `[cg², 1024]`, the tokens without the class.
1585    fn clip_forward(&self, sam_out: &[f32], cg: usize, tb: &GridTables, pool: Option<&Pool>) -> Vec<f32> {
1586        let nc = cg * cg + 1;
1587        let cd = CLIP_D;
1588        let mut x = vec![0f32; nc * cd];
1589        x[..cd].copy_from_slice(&self.cls);
1590        x[cd..].copy_from_slice(sam_out);
1591        for (a, &p) in x.iter_mut().zip(&tb.clip_pos) {
1592            *a += p;
1593        }
1594        let mut xn = vec![0f32; nc * cd];
1595        layer_norm_rows(&x, &self.pre_w, &self.pre_b, CLIP_EPS, &mut xn, pool);
1596        std::mem::swap(&mut x, &mut xn);
1597        let resident = crate::gpu::enabled_here() && std::env::var("CMF_OCR_GPU").as_deref() != Ok("0") && {
1598            let (cos, sin) = (vec![1f32; nc * CLIP_HD], vec![0f32; nc * CLIP_HD]);
1599            let layers = self
1600                .clip
1601                .iter()
1602                .map(|l| crate::gpu::VitLayer {
1603                    ln1_w: &l.n1_w,
1604                    ln1_b: &l.n1_b,
1605                    qkv: l.qkv.weights(),
1606                    qkv_b: &l.qkv_b,
1607                    proj: l.out.weights(),
1608                    proj_b: &l.out_b,
1609                    ln2_w: &l.n2_w,
1610                    ln2_b: &l.n2_b,
1611                    fc1: l.fc1.weights(),
1612                    fc1_b: &l.fc1_b,
1613                    fc2: l.fc2.weights(),
1614                    fc2_b: &l.fc2_b,
1615                })
1616                .collect();
1617            let args = crate::gpu::VitArgs {
1618                key: &self.clip as *const _ as u64,
1619                layers,
1620                n: nc,
1621                d: cd,
1622                heads: CLIP_HEADS,
1623                head_dim: CLIP_HD,
1624                inter: CLIP_INTER,
1625                eps: CLIP_EPS,
1626                gelu_tanh: false,
1627                quick_gelu: true,
1628                rope_halves: true,
1629                cos: &cos,
1630                sin: &sin,
1631                prof: false,
1632            };
1633            crate::gpu::vit_encode(&args, &mut x)
1634        };
1635        if resident {
1636            return x[cd..].to_vec();
1637        }
1638        let mut qkv = vec![0f32; nc * 3 * cd];
1639        let (mut qh, mut kh, mut vh) = (vec![0f32; nc * cd], vec![0f32; nc * cd], vec![0f32; nc * cd]);
1640        let mut att = vec![0f32; nc * cd];
1641        let mut tmp = vec![0f32; nc * cd];
1642        let mut cf = vec![0f32; nc * CLIP_INTER];
1643        let scale = (CLIP_HD as f32).sqrt().recip();
1644        for l in &self.clip {
1645            layer_norm_rows(&x, &l.n1_w, &l.n1_b, CLIP_EPS, &mut xn, pool);
1646            l.qkv.matmat(&xn, nc, Some(&l.qkv_b), &mut qkv, pool);
1647            // `xqkv.view(bsz, seqlen, 3, heads, head)`: q, k, v per token.
1648            for p in 0..nc {
1649                let row = &qkv[p * 3 * cd..(p + 1) * 3 * cd];
1650                for hh in 0..CLIP_HEADS {
1651                    let at = (hh * nc + p) * CLIP_HD;
1652                    qh[at..at + CLIP_HD].copy_from_slice(&row[hh * CLIP_HD..(hh + 1) * CLIP_HD]);
1653                    kh[at..at + CLIP_HD].copy_from_slice(&row[cd + hh * CLIP_HD..cd + (hh + 1) * CLIP_HD]);
1654                    vh[at..at + CLIP_HD].copy_from_slice(&row[2 * cd + hh * CLIP_HD..2 * cd + (hh + 1) * CLIP_HD]);
1655                }
1656            }
1657            crate::onevision2::attend_host(&qh, &kh, &vh, CLIP_HEADS, CLIP_HD, nc, scale, &mut att, pool);
1658            l.out.matmat(&att, nc, Some(&l.out_b), &mut tmp, pool);
1659            for (a, &b) in x.iter_mut().zip(&tmp) {
1660                *a += b;
1661            }
1662            layer_norm_rows(&x, &l.n2_w, &l.n2_b, CLIP_EPS, &mut xn, pool);
1663            l.fc1.matmat(&xn, nc, Some(&l.fc1_b), &mut cf, pool);
1664            // quick_gelu: x·σ(1.702·x)
1665            for v in cf.iter_mut() {
1666                *v /= 1.0 + (-1.702 * *v).exp();
1667            }
1668            l.fc2.matmat(&cf, nc, Some(&l.fc2_b), &mut tmp, pool);
1669            for (a, &b) in x.iter_mut().zip(&tmp) {
1670                *a += b;
1671            }
1672        }
1673        x[cd..].to_vec()
1674    }
1675
1676    /// `[3, side, side]` → projected rows `[g², 1280]`.
1677    fn view_rows(&self, px: &[f32], side: usize) -> Vec<f32> {
1678        let (sam, clip) = self.encode(px, side);
1679        let n = sam.len() / CLIP_D;
1680        let mut joined = vec![0f32; n * 2 * CLIP_D];
1681        for i in 0..n {
1682            joined[i * 2 * CLIP_D..i * 2 * CLIP_D + CLIP_D].copy_from_slice(&clip[i * CLIP_D..(i + 1) * CLIP_D]);
1683            joined[i * 2 * CLIP_D + CLIP_D..(i + 1) * 2 * CLIP_D].copy_from_slice(&sam[i * CLIP_D..(i + 1) * CLIP_D]);
1684        }
1685        let mut out = vec![0f32; n * OUT_D];
1686        self.proj.matmat(&joined, n, Some(&self.proj_b), &mut out, self.pool.as_deref());
1687        out
1688    }
1689
1690    /// One page's feature rows in prompt order: tiles (a newline after every
1691    /// row), the global view (same), the view separator.
1692    pub fn page_rows(&self, v: &PageViews) -> Vec<f32> {
1693        let d = OUT_D;
1694        let mut out = Vec::new();
1695        if !v.tiles.is_empty() {
1696            let tiles: Vec<Vec<f32>> = v.tiles.iter().map(|t| self.view_rows(t, IMAGE_SIZE)).collect();
1697            let tg = IMAGE_SIZE / PATCH / DOWNSAMPLE;
1698            for ty in 0..v.rows {
1699                for iy in 0..tg {
1700                    for tx in 0..v.cols {
1701                        let t = &tiles[ty * v.cols + tx];
1702                        out.extend_from_slice(&t[iy * tg * d..(iy + 1) * tg * d]);
1703                    }
1704                    out.extend_from_slice(&self.newline);
1705                }
1706            }
1707        }
1708        let g = self.view_rows(&v.global, BASE_SIZE);
1709        let gg = BASE_SIZE / PATCH / DOWNSAMPLE;
1710        for y in 0..gg {
1711            out.extend_from_slice(&g[y * gg * d..(y + 1) * gg * d]);
1712            out.extend_from_slice(&self.newline);
1713        }
1714        out.extend_from_slice(&self.view_sep);
1715        out
1716    }
1717}
1718
1719fn tower_for(model: &Arc<CmfModel>) -> Result<Arc<UocrTower>, String> {
1720    static TOWER: std::sync::Mutex<Option<(usize, Arc<UocrTower>)>> = std::sync::Mutex::new(None);
1721    let key = Arc::as_ptr(model) as usize;
1722    let mut g = TOWER.lock().unwrap();
1723    if let Some((k, t)) = g.as_ref() {
1724        if *k == key {
1725            return Ok(t.clone());
1726        }
1727    }
1728    let t = Arc::new(UocrTower::from_model(model, Pool::from_env())?);
1729    *g = Some((key, t.clone()));
1730    Ok(t)
1731}
1732
1733// ───────────────────────── the prompt ─────────────────────────
1734
1735/// The prepared prompt.
1736pub struct PreparedUocrPrompt {
1737    pub token_ids: Vec<u32>,
1738    pub rows: Vec<f32>,
1739    /// `<PAGE>` markers the answer must repeat.
1740    pub pages: usize,
1741}
1742
1743/// [`prepare`] on the pipeline's own file.
1744pub fn prepare_for_pipeline(pipeline: &Pipeline, sources: &[Value], prompt: Option<&str>) -> Result<PreparedUocrPrompt, String> {
1745    let model = pipeline.model.as_ref().ok_or("Unlimited-OCR requires a file-backed pipeline")?;
1746    prepare(model, pipeline, sources, prompt)
1747}
1748
1749/// `_uocr_build_inputs` with the trained layout: `<PAGE><image>` per page,
1750/// then the instruction. One page is tiled, several take the global view.
1751pub fn prepare(
1752    model: &Arc<CmfModel>,
1753    pipeline: &Pipeline,
1754    sources: &[Value],
1755    prompt: Option<&str>,
1756) -> Result<PreparedUocrPrompt, String> {
1757    if sources.is_empty() {
1758        return Err("Unlimited-OCR reads images: pass at least one".into());
1759    }
1760    let tower = tower_for(model)?;
1761    let dash = pipeline.dash.as_ref().ok_or("Unlimited-OCR: the pipeline has no router")?;
1762    let (image_id, page_id) = (dash.image_id, dash.page_id);
1763    let tiled = sources.len() == 1;
1764    let tok = &pipeline.tokenizer;
1765    let mut ids = vec![BOS_ID];
1766    let mut media: Vec<Vec<f32>> = Vec::with_capacity(sources.len());
1767    let dump = std::env::var_os("CMF_OCR_DUMP").map(std::path::PathBuf::from);
1768    for (pi, source) in sources.iter().enumerate() {
1769        let t0 = std::time::Instant::now();
1770        let frame = media::decode_rgb(&media::load_image_bytes(source)?)?;
1771        let v = page_views(&frame, tiled);
1772        let t_pre = t0.elapsed().as_secs_f64();
1773        let rows = tower.page_rows(&v);
1774        let n = page_tokens(v.cols, v.rows);
1775        if rows.len() != n * OUT_D {
1776            return Err(format!("Unlimited-OCR: {} rows for {n} image tokens", rows.len() / OUT_D));
1777        }
1778        if let Some(dir) = &dump {
1779            let write = |name: String, data: &[f32]| {
1780                let bytes: Vec<u8> = data.iter().flat_map(|x| x.to_le_bytes()).collect();
1781                std::fs::write(dir.join(name), bytes).map_err(|e| format!("CMF_OCR_DUMP: {e}"))
1782            };
1783            write(format!("global_{pi}.f32"), &v.global)?;
1784            for (ti, t) in v.tiles.iter().enumerate() {
1785                write(format!("tile_{pi}_{ti}.f32"), t)?;
1786            }
1787            write(format!("rows_{pi}.f32"), &rows)?;
1788        }
1789        if dump.is_some() || std::env::var_os("CMF_OCR_PROF").is_some() {
1790            eprintln!(
1791                "uocr: page {pi} {}x{}, grid {}x{}, {n} tokens, preprocess {t_pre:.3} s, total {:.2} s",
1792                frame.width,
1793                frame.height,
1794                v.cols,
1795                v.rows,
1796                t0.elapsed().as_secs_f64()
1797            );
1798        }
1799        ids.push(page_id);
1800        ids.extend(std::iter::repeat_n(image_id, n));
1801        media.push(rows);
1802    }
1803    let instruction = prompt.filter(|p| !p.trim().is_empty()).unwrap_or("Multi page parsing.");
1804    let tail = tok.encode(instruction);
1805    ids.extend(tail);
1806    let h = pipeline.hidden_size;
1807    let mut rows = Vec::with_capacity(ids.len() * h);
1808    let mut flat = media.iter().flatten().copied();
1809    for &id in &ids {
1810        if id == image_id {
1811            rows.extend((&mut flat).take(h));
1812        } else {
1813            rows.extend(pipeline.embed_id(id));
1814        }
1815    }
1816    if rows.len() != ids.len() * h {
1817        return Err("Unlimited-OCR: placeholder and image row counts differ".into());
1818    }
1819    Ok(PreparedUocrPrompt { token_ids: ids, rows, pages: sources.len() })
1820}
1821
1822/// The images and the instruction of a chat request: every image part in order,
1823/// the last text part. Video and audio are refused.
1824pub fn request_parts(messages: &[Value]) -> Result<(Vec<Value>, Option<String>), String> {
1825    let mut sources = Vec::new();
1826    let mut text = None;
1827    for message in messages {
1828        match message.get("content") {
1829            Some(Value::String(t)) => text = Some(t.clone()),
1830            Some(Value::Array(parts)) => {
1831                for part in parts {
1832                    let typ = part.get("type").and_then(Value::as_str).unwrap_or("");
1833                    if typ == "image" || typ == "image_url" || part.get("image").is_some() || part.get("image_url").is_some() {
1834                        let source = part.get("image_url").or_else(|| part.get("image")).unwrap_or(part);
1835                        sources.push(match source.as_str() {
1836                            Some(url) => serde_json::json!({"url": url}),
1837                            None => source.clone(),
1838                        });
1839                    } else if let Some(t) = part.get("text").and_then(Value::as_str) {
1840                        text = Some(t.to_string());
1841                    } else if typ != "text" {
1842                        return Err(format!("Unlimited-OCR: unsupported content part '{typ}' (text and images only)"));
1843                    }
1844                }
1845            }
1846            _ => {}
1847        }
1848    }
1849    Ok((sources, text))
1850}
1851
1852/// `_uocr_page_count_guard`: in place on the logits, over the generated ids.
1853pub struct PageGuard {
1854    pub expected: usize,
1855    pub page_id: u32,
1856    pub eos_id: u32,
1857    pub max_page_tokens: usize,
1858    marker_cum: usize,
1859    scanned: usize,
1860    page_len: usize,
1861}
1862
1863impl PageGuard {
1864    pub fn new(expected: usize, page_id: u32, eos_id: u32) -> Self {
1865        Self { expected, page_id, eos_id, max_page_tokens: 20000, marker_cum: 0, scanned: 0, page_len: 0 }
1866    }
1867
1868    pub fn apply(&mut self, logits: &mut [f32], generated: &[u32]) {
1869        if self.expected == 0 {
1870            return;
1871        }
1872        if generated.len() > self.scanned {
1873            let tail = &generated[self.scanned..];
1874            let m = tail.iter().filter(|&&t| t == self.page_id).count();
1875            if m > 0 {
1876                self.marker_cum += m;
1877                self.page_len = tail.iter().rev().position(|&t| t == self.page_id).unwrap_or(0);
1878            } else {
1879                self.page_len += tail.len();
1880            }
1881            self.scanned = generated.len();
1882        }
1883        let only = |logits: &mut [f32], t: u32| {
1884            let keep = logits[t as usize];
1885            logits.iter_mut().for_each(|v| *v = f32::NEG_INFINITY);
1886            logits[t as usize] = keep;
1887        };
1888        if self.marker_cum > self.expected {
1889            only(logits, self.eos_id);
1890        } else if self.max_page_tokens > 0 && self.page_len >= self.max_page_tokens {
1891            let t = if self.marker_cum < self.expected { self.page_id } else { self.eos_id };
1892            only(logits, t);
1893        } else if self.marker_cum < self.expected {
1894            logits[self.eos_id as usize] = f32::NEG_INFINITY;
1895        }
1896    }
1897}
1898
1899#[cfg(test)]
1900mod tests {
1901    use super::*;
1902
1903    #[test]
1904    fn page_tokens_match_the_trained_layout() {
1905        assert_eq!(page_tokens(1, 1), 273);
1906        // (10·3 + 1) · (10·4) local rows on top of the global view
1907        assert_eq!(page_tokens(3, 4), 273 + 31 * 40);
1908    }
1909
1910    #[test]
1911    fn page_coordinates_follow_the_reference() {
1912        // BOS, <PAGE>, 3 image, <PAGE>, 2 image, 2 instruction
1913        let (bos, pg, im, t) = (0u32, 9u32, 7u32, 5u32);
1914        let ids = [bos, pg, im, im, im, pg, im, im, t, t];
1915        let mut r = PageRope::begin(&ids, im, pg, 4).unwrap();
1916        let c: Vec<_> = (0..ids.len()).map(|i| r.prompt_coords(i)).collect();
1917        assert_eq!(c[0], (0, 0));
1918        assert_eq!(c[1], (0, 1));
1919        assert_eq!(c[4], (3, 1));
1920        assert_eq!(c[5], (0, 2));
1921        assert_eq!(c[7], (2, 2));
1922        // the instruction shares the reserved page 0 and keeps its count
1923        assert_eq!(c[8], (1, 0));
1924        assert_eq!(c[9], (2, 0));
1925        // first response marker opens page 1 at that page's prefill length
1926        assert_eq!(r.decode_step(pg), (4, 1));
1927        assert_eq!(r.decode_step(t), (5, 1));
1928        assert_eq!(r.decode_step(pg), (3, 2));
1929    }
1930
1931    #[test]
1932    fn decode_candidates_follow_the_response_page() {
1933        let (bos, pg, im, t) = (0u32, 9u32, 7u32, 5u32);
1934        let mut ids = vec![bos, pg];
1935        ids.extend(std::iter::repeat_n(im, 6));
1936        ids.push(pg);
1937        ids.extend(std::iter::repeat_n(im, 7));
1938        ids.push(t);
1939        let mut r = PageRope::begin(&ids, im, pg, 4).unwrap();
1940        let mut allow = Vec::new();
1941        // before the first response marker every block is open
1942        r.decode_allow(5, &mut allow);
1943        assert!(allow.iter().all(|&a| a));
1944        r.decode_step(pg);
1945        r.decode_allow(5, &mut allow);
1946        assert_eq!(allow, vec![true, true, false, false, false]);
1947        r.decode_step(pg);
1948        r.decode_allow(5, &mut allow);
1949        assert_eq!(allow, vec![false, false, true, true, false]);
1950    }
1951
1952    #[test]
1953    fn entmax_matches_a_hand_case() {
1954        // Two equal scores share the mass; a far one gets none.
1955        let mut y = [0f32; 3];
1956        entmax15_sorted(&[0.0, 0.0, -50.0], &mut y);
1957        assert!((y[0] - 0.5).abs() < 1e-6, "{y:?}");
1958        assert!((y[1] - 0.5).abs() < 1e-6);
1959        assert_eq!(y[2], 0.0);
1960        // Sums to one on a mixed row.
1961        let mut z = [0f32; 4];
1962        entmax15_sorted(&[3.0, 2.5, 1.0, 0.0], &mut z);
1963        assert!((z.iter().sum::<f32>() - 1.0).abs() < 1e-5);
1964    }
1965
1966    #[test]
1967    fn route_centers_the_bias_on_the_read_set() {
1968        let p = RouteParams { idx_scale: 1.0, floor: 1e-4, sigma: 1.0, k: 2, top_m: 16 };
1969        let scores = [Some(1.0), Some(5.0), None, Some(4.0)];
1970        let allow = [true, true, true, true];
1971        let (mut w, mut pick) = (Vec::new(), Vec::new());
1972        route_select(&scores, &allow, &p, &mut w, &mut pick);
1973        assert_eq!(pick.len(), 2);
1974        assert_eq!(pick[0].0, 1);
1975        assert_eq!(pick[1].0, 3);
1976        assert!((pick[0].1 + pick[1].1).abs() < 1e-5);
1977        // A page with candidates shuts the rest out.
1978        let allow = [true, false, false, false];
1979        route_select(&scores, &allow, &p, &mut w, &mut pick);
1980        assert_eq!(pick.len(), 1);
1981        assert_eq!(pick[0].0, 0);
1982    }
1983
1984    #[test]
1985    fn closest_grid_picks_the_page_aspect() {
1986        // Reference `dynamic_preprocess` for a 1191×1684 page.
1987        let (c, r) = closest_grid(1191, 1684, 640);
1988        assert!((c as f64 / r as f64 - 1191.0 / 1684.0).abs() < 0.05, "{c}x{r}");
1989    }
1990
1991    #[test]
1992    fn conv_reads_kernels_as_channel_out_in_y_x() {
1993        // 1 in, 1 out, 3×3 kernel: a centre-only kernel is the identity.
1994        let mut w = vec![0f32; 9];
1995        w[4] = 2.0;
1996        let x = [1.0, 2.0, 3.0, 4.0];
1997        let (y, g) = conv_same(&x, 2, 1, &w, 1, 3, 1, 1);
1998        assert_eq!(g, 2);
1999        assert_eq!(y, vec![2.0, 4.0, 6.0, 8.0]);
2000    }
2001}