1use super::weights::{OcrGraphBuilder, assert_weights_drained};
19use anyhow::Result;
20use rlx_core::vision_ops_ir::{avg_pool2d, conv2d_bias, max_pool2d_2x2};
21use rlx_core::weight_map::WeightMap;
22use rlx_ir::hir::{HirMut, HirNodeId};
23use rlx_ir::{DType, HirGraphExt, Shape};
24
25pub const RECOGNITION_HEIGHT: usize = 64;
26pub const NUM_CLASSES: usize = 97;
27const HIDDEN: usize = 256;
28const FEAT: usize = 128;
29
30#[derive(Clone, Copy, Debug)]
31pub struct RecognitionGraphConfig {
32 pub batch: usize,
33 pub width: usize,
34}
35
36#[derive(Clone, Copy, PartialEq, Eq)]
39enum Stage {
40 AfterG1,
41 AfterG2,
42 AfterLogits,
43}
44
45fn build_recognition_conv_front(
46 b: &mut OcrGraphBuilder,
47 wm: &mut WeightMap,
48 image: HirNodeId,
49 batch: usize,
50 mut h: usize,
51 mut w: usize,
52) -> Result<(HirNodeId, usize)> {
53 let mut x = conv_relu(
54 b,
55 wm,
56 image,
57 "conv.0.weight",
58 "conv.0.bias",
59 batch,
60 32,
61 1,
62 h,
63 w,
64 )?;
65 x = max_pool2d_2x2(&mut b.m(), x, batch, 32, h, w);
66 h /= 2;
67 w /= 2;
68
69 x = conv_relu(
70 b,
71 wm,
72 x,
73 "onnx::Conv_367",
74 "onnx::Conv_368",
75 batch,
76 64,
77 32,
78 h,
79 w,
80 )?;
81 x = max_pool2d_2x2(&mut b.m(), x, batch, 64, h, w);
82 h /= 2;
83 w /= 2;
84
85 x = conv_relu(
86 b,
87 wm,
88 x,
89 "conv.7.weight",
90 "conv.7.bias",
91 batch,
92 128,
93 64,
94 h,
95 w,
96 )?;
97 x = conv_relu(
98 b,
99 wm,
100 x,
101 "onnx::Conv_370",
102 "onnx::Conv_371",
103 batch,
104 128,
105 128,
106 h,
107 w,
108 )?;
109 x = pool_2x1(&mut b.m(), x, batch, 128, h, w);
110 h /= 2;
111
112 x = conv_relu(
113 b,
114 wm,
115 x,
116 "conv.13.weight",
117 "conv.13.bias",
118 batch,
119 128,
120 128,
121 h,
122 w,
123 )?;
124 x = conv_relu(
125 b,
126 wm,
127 x,
128 "onnx::Conv_373",
129 "onnx::Conv_374",
130 batch,
131 128,
132 128,
133 h,
134 w,
135 )?;
136 x = pool_2x1(&mut b.m(), x, batch, 128, h, w);
137 h /= 2;
138
139 x = fused_conv2x2(
140 b,
141 wm,
142 x,
143 "onnx::Conv_376",
144 "onnx::Conv_377",
145 batch,
146 128,
147 128,
148 h,
149 w,
150 )?;
151 h += 1;
152 w += 1;
153 x = avg_pool2d(&mut b.m(), x, [4, 1], [4, 1], batch, 128, h, w);
154 let seq = w;
155 let x = b
156 .m()
157 .reshape_(x, vec![batch as i64, FEAT as i64, seq as i64]);
158 let x = b.m().transpose_(x, vec![2, 0, 1]);
159 Ok((x, seq))
160}
161
162pub fn build_recognition_conv_graph(
164 wm: &mut WeightMap,
165 cfg: RecognitionGraphConfig,
166) -> Result<(rlx_ir::Graph, std::collections::HashMap<String, Vec<f32>>)> {
167 let mut b = OcrGraphBuilder::new("ocr_recognition_conv");
168 let batch = cfg.batch;
169 let h = RECOGNITION_HEIGHT;
170 let w = cfg.width;
171 let image = b
172 .m()
173 .input("image", Shape::new(&[batch, 1, h, w], DType::F32));
174 let (x, _seq) = build_recognition_conv_front(&mut b, wm, image, batch, h, w)?;
175 b.m().set_outputs(vec![x]);
176 b.finish()
177}
178
179pub fn build_recognition_after_g1_graph(
181 wm: &mut WeightMap,
182 cfg: RecognitionGraphConfig,
183) -> Result<(rlx_ir::Graph, std::collections::HashMap<String, Vec<f32>>)> {
184 build_recognition_graph_inner(wm, cfg, Some(Stage::AfterG1))
185}
186
187pub fn build_recognition_after_g2_graph(
189 wm: &mut WeightMap,
190 cfg: RecognitionGraphConfig,
191) -> Result<(rlx_ir::Graph, std::collections::HashMap<String, Vec<f32>>)> {
192 build_recognition_graph_inner(wm, cfg, Some(Stage::AfterG2))
193}
194
195pub fn build_recognition_after_logits_graph(
197 wm: &mut WeightMap,
198 cfg: RecognitionGraphConfig,
199) -> Result<(rlx_ir::Graph, std::collections::HashMap<String, Vec<f32>>)> {
200 build_recognition_graph_inner(wm, cfg, Some(Stage::AfterLogits))
201}
202
203pub fn build_recognition_graph(
204 wm: &mut WeightMap,
205 cfg: RecognitionGraphConfig,
206) -> Result<(rlx_ir::Graph, std::collections::HashMap<String, Vec<f32>>)> {
207 build_recognition_graph_inner(wm, cfg, None)
208}
209
210fn build_recognition_graph_inner(
211 wm: &mut WeightMap,
212 cfg: RecognitionGraphConfig,
213 stop: Option<Stage>,
214) -> Result<(rlx_ir::Graph, std::collections::HashMap<String, Vec<f32>>)> {
215 let mut b = OcrGraphBuilder::new("ocr_recognition");
216 let batch = cfg.batch;
217 let h = RECOGNITION_HEIGHT;
218 let w = cfg.width;
219
220 let image = b
221 .m()
222 .input("image", Shape::new(&[batch, 1, h, w], DType::F32));
223
224 let (x, seq) = build_recognition_conv_front(&mut b, wm, image, batch, h, w)?;
225
226 let xb = b.m().transpose_(x, vec![1, 0, 2]); let g1b = gru_layer(
232 &mut b,
233 wm,
234 xb,
235 "onnx::GRU_422",
236 "onnx::GRU_423",
237 "onnx::GRU_421",
238 batch,
239 seq,
240 FEAT,
241 HIDDEN,
242 )?; if stop == Some(Stage::AfterG1) {
244 let g1 = b.m().transpose_(g1b, vec![1, 0, 2]); b.m().set_outputs(vec![g1]);
246 return b.finish();
247 }
248
249 let g2b = gru_layer(
250 &mut b,
251 wm,
252 g1b,
253 "onnx::GRU_465",
254 "onnx::GRU_466",
255 "onnx::GRU_464",
256 batch,
257 seq,
258 2 * HIDDEN,
259 HIDDEN,
260 )?; let g2 = b.m().transpose_(g2b, vec![1, 0, 2]); if stop == Some(Stage::AfterG2) {
263 b.m().set_outputs(vec![g2]);
264 return b.finish();
265 }
266
267 let head_w = b.load_param(wm, "onnx::MatMul_467")?;
268 let head_b = b.load_param(wm, "output.0.bias")?;
269 let logits = b.m().mm(g2, head_w);
270 let logits = add_bias_seq(&mut b, logits, head_b, batch, seq, NUM_CLASSES)?;
271 if stop == Some(Stage::AfterLogits) {
272 b.m().set_outputs(vec![logits]);
273 return b.finish();
274 }
275 let out = b.m().transpose_(logits, vec![1, 0, 2]);
276 b.m().set_outputs(vec![out]);
277
278 assert_weights_drained(wm, "recognition graph")?;
279 b.finish()
280}
281
282fn conv_relu(
283 b: &mut OcrGraphBuilder,
284 wm: &mut WeightMap,
285 x: HirNodeId,
286 w_key: &str,
287 bias_key: &str,
288 batch: usize,
289 out_c: usize,
290 _in_c: usize,
291 h: usize,
292 w: usize,
293) -> Result<HirNodeId> {
294 let weight = b.load_param(wm, w_key)?;
295 let bias = b.load_param(wm, bias_key)?;
296 let y = conv2d_bias(
297 &mut b.m(),
298 x,
299 weight,
300 bias,
301 batch,
302 out_c,
303 3,
304 3,
305 [1, 1],
306 [1, 1],
307 h,
308 w,
309 );
310 Ok(b.m().relu(y))
311}
312
313fn fused_conv2x2(
315 b: &mut OcrGraphBuilder,
316 wm: &mut WeightMap,
317 x: HirNodeId,
318 w_key: &str,
319 bias_key: &str,
320 batch: usize,
321 out_c: usize,
322 _in_c: usize,
323 h: usize,
324 w: usize,
325) -> Result<HirNodeId> {
326 let weight = b.load_param(wm, w_key)?;
327 let bias = b.load_param(wm, bias_key)?;
328 let out_h = h + 1;
329 let out_w = w + 1;
330 Ok(conv2d_bias(
331 &mut b.m(),
332 x,
333 weight,
334 bias,
335 batch,
336 out_c,
337 2,
338 2,
339 [1, 1],
340 [1, 1],
341 out_h,
342 out_w,
343 ))
344}
345
346fn pool_2x1(
347 g: &mut HirMut<'_>,
348 x: HirNodeId,
349 batch: usize,
350 c: usize,
351 h: usize,
352 w: usize,
353) -> HirNodeId {
354 use rlx_ir::op::{Op, ReduceOp};
355 let dt = g.shape(x).dtype();
356 let out_h = (h.saturating_sub(2)) / 2 + 1;
357 let out_w = w;
358 let out_shape = rlx_core::vision_ops_ir::nchw_shape(batch, c, out_h, out_w, dt);
359 g.add_node(
360 Op::Pool {
361 kind: ReduceOp::Max,
362 kernel_size: vec![2, 1],
363 stride: vec![2, 1],
364 padding: vec![0, 0],
365 },
366 vec![x],
367 out_shape,
368 )
369}
370
371fn gru_param(b: &mut OcrGraphBuilder, key: String, len: usize, data: Vec<f32>) -> HirNodeId {
373 debug_assert_eq!(data.len(), len);
374 let id = b.m().param(&key, Shape::new(&[len], DType::F32));
375 b.params.insert(key, data);
376 id
377}
378
379#[allow(clippy::too_many_arguments)]
385fn gru_layer(
386 b: &mut OcrGraphBuilder,
387 wm: &mut WeightMap,
388 x: HirNodeId,
389 w_key: &str,
390 r_key: &str,
391 b_key: &str,
392 batch: usize,
393 seq: usize,
394 in_size: usize,
395 hidden: usize,
396) -> Result<HirNodeId> {
397 use anyhow::Context;
398 const NUM_DIR: usize = 2;
399 const MAP: [usize; 3] = [1, 0, 2];
401 let g3 = 3 * hidden;
402
403 let (w_data, _) = wm
404 .take(w_key)
405 .with_context(|| format!("missing weight {w_key}"))?;
406 let (r_data, _) = wm
407 .take(r_key)
408 .with_context(|| format!("missing weight {r_key}"))?;
409 let (b_data, _) = wm
410 .take(b_key)
411 .with_context(|| format!("missing weight {b_key}"))?;
412
413 let mut w_ih = vec![0f32; NUM_DIR * g3 * in_size];
414 let mut w_hh = vec![0f32; NUM_DIR * g3 * hidden];
415 let mut b_ih = vec![0f32; NUM_DIR * g3];
416 let mut b_hh = vec![0f32; NUM_DIR * g3];
417 for d in 0..NUM_DIR {
418 for rg in 0..3 {
419 let og = MAP[rg];
420 let wblk = hidden * in_size;
421 let (ws, wd) = ((d * 3 + og) * wblk, (d * 3 + rg) * wblk);
422 w_ih[wd..wd + wblk].copy_from_slice(&w_data[ws..ws + wblk]);
423 let rblk = hidden * hidden;
424 let (rs, rd) = ((d * 3 + og) * rblk, (d * 3 + rg) * rblk);
425 w_hh[rd..rd + rblk].copy_from_slice(&r_data[rs..rs + rblk]);
426 let bd = (d * 3 + rg) * hidden;
428 let wb = d * 6 * hidden + og * hidden;
429 let rb = d * 6 * hidden + g3 + og * hidden;
430 b_ih[bd..bd + hidden].copy_from_slice(&b_data[wb..wb + hidden]);
431 b_hh[bd..bd + hidden].copy_from_slice(&b_data[rb..rb + hidden]);
432 }
433 }
434
435 let wih = gru_param(b, format!("{w_key}.rlx_wih"), w_ih.len(), w_ih);
436 let whh = gru_param(b, format!("{r_key}.rlx_whh"), w_hh.len(), w_hh);
437 let bih = gru_param(b, format!("{b_key}.rlx_bih"), b_ih.len(), b_ih);
438 let bhh = gru_param(b, format!("{b_key}.rlx_bhh"), b_hh.len(), b_hh);
439
440 let shape = Shape::new(&[batch, seq, NUM_DIR * hidden], DType::F32);
441 Ok(b.m().0.gru(x, wih, whh, bih, bhh, hidden, 1, true, shape))
443}
444
445pub fn log_softmax_last_axis(data: &mut [f32], classes: usize) {
447 assert!(classes > 0 && data.len().is_multiple_of(classes));
448 for lane in data.chunks_mut(classes) {
449 let max_val = lane.iter().copied().fold(f32::NEG_INFINITY, f32::max);
450 let log_exp_sum = lane.iter().map(|&x| (x - max_val).exp()).sum::<f32>().ln();
451 for el in lane.iter_mut() {
452 *el = (*el - max_val) - log_exp_sum;
453 }
454 }
455}
456
457fn add_bias_seq(
458 b: &mut OcrGraphBuilder,
459 y: HirNodeId,
460 bias: HirNodeId,
461 _batch: usize,
462 _seq: usize,
463 classes: usize,
464) -> Result<HirNodeId> {
465 let bias3 = b.m().reshape_(bias, vec![1, 1, classes as i64]);
466 Ok(b.m().add(y, bias3))
467}