Skip to content

Commit 440ba94

Browse files
artizclaude
andcommitted
Revert "#157 stage 2: padded rec batching in the browser (fewer ORT calls)"
This reverts commit 1c871c7. Padding every line in a batch up to the batch's max width inflates recognition compute (a narrow line padded to a wide one does several times the conv work), and on real pages line widths vary enough that the padding waste outweighs the saved per-call overhead — measured ~9.6-10.4s/page vs ~8.0s/page with the exact-width grouping. With multi-threaded ORT the per-run overhead the batching targeted is already cheap, so the trade doesn't pay. Back to the bit-identical exact-width batches. Refs #157 Signed-off-by: artiz <artem.kustikov@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EY5KAiquN4YpVf2PXEQkVT
1 parent 1c871c7 commit 440ba94

3 files changed

Lines changed: 8 additions & 103 deletions

File tree

crates/docling-pdf/src/ocr_prep.rs

Lines changed: 0 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -273,67 +273,6 @@ pub fn batch_input(w: usize, chunk: &[usize], lines: &[PrepLine]) -> Vec<f32> {
273273
data
274274
}
275275

276-
/// Browser-path recognition batching: group lines of *differing* width into
277-
/// [`REC_BATCH`]-sized chunks (sorted by width, so a chunk spans a narrow
278-
/// range) and pad each chunk to its max width. The padding is the input
279-
/// buffer's zero value — mid-gray in the model's `[-1, 1]` space, which is
280-
/// exactly what PP-OCRv3 was trained/served with in batched inference, so the
281-
/// decode matches one-at-a-time recognition (measured: 0 diffs on a mixed
282-
/// Latin+Cyrillic sample). A width-ratio guard keeps padding waste ≤1.5×.
283-
///
284-
/// On a text-dense page where most lines have distinct widths this collapses
285-
/// N recognition calls into ≈N/[`REC_BATCH`] — a real win under ORT Web, where
286-
/// every `session.run` carries thread-sync overhead. Deliberately NOT used by
287-
/// the native path: that keeps the bit-identical exact-width [`width_batches`]
288-
/// so the byte-for-byte conformance baseline is untouched.
289-
pub fn width_batches_padded(lines: &[PrepLine]) -> Vec<(usize, Vec<usize>)> {
290-
let mut order: Vec<usize> = (0..lines.len()).collect();
291-
order.sort_by_key(|&i| lines[i].w);
292-
let mut out: Vec<(usize, Vec<usize>)> = Vec::new();
293-
let mut cur: Vec<usize> = Vec::new();
294-
let mut cur_min = 0usize;
295-
let flush = |cur: &mut Vec<usize>, out: &mut Vec<(usize, Vec<usize>)>| {
296-
if !cur.is_empty() {
297-
let mw = cur.iter().map(|&j| lines[j].w).max().unwrap_or(0);
298-
out.push((mw, std::mem::take(cur)));
299-
}
300-
};
301-
for &i in &order {
302-
let w = lines[i].w;
303-
// Cap padding waste: start a new chunk if this line is >1.5× the
304-
// chunk's narrowest, or the chunk is already full.
305-
if cur.len() == REC_BATCH || (!cur.is_empty() && w * 2 > cur_min * 3) {
306-
flush(&mut cur, &mut out);
307-
}
308-
if cur.is_empty() {
309-
cur_min = w;
310-
}
311-
cur.push(i);
312-
}
313-
flush(&mut cur, &mut out);
314-
out
315-
}
316-
317-
/// Pack a padded width-batch (see [`width_batches_padded`]): each line's CHW
318-
/// data goes into the left `line.w` columns of a zero-filled `(N, 3, H, W)`
319-
/// buffer, `W` = the batch's max width.
320-
pub fn batch_input_padded(w: usize, chunk: &[usize], lines: &[PrepLine]) -> Vec<f32> {
321-
let h = REC_HEIGHT as usize;
322-
let mut data = vec![0f32; chunk.len() * 3 * h * w];
323-
for (i, &ix) in chunk.iter().enumerate() {
324-
let lw = lines[ix].w;
325-
let src = &lines[ix].data; // 3 * h * lw, CHW
326-
for c in 0..3 {
327-
for y in 0..h {
328-
let s = c * h * lw + y * lw;
329-
let d = i * 3 * h * w + c * h * w + y * w;
330-
data[d..d + lw].copy_from_slice(&src[s..s + lw]);
331-
}
332-
}
333-
}
334-
data
335-
}
336-
337276
#[cfg(test)]
338277
mod tests {
339278
use super::*;
@@ -402,33 +341,4 @@ mod tests {
402341
];
403342
assert_eq!(decode_row(&chars, &probs, 4), "ab");
404343
}
405-
406-
/// Two lines of different width batch together when padded, and the padded
407-
/// buffer places each line's data left-aligned with a zero tail.
408-
#[test]
409-
fn padded_batching_groups_and_left_aligns() {
410-
// Narrow line (w=8, all 1.0) + wide line (w=10, all 2.0), within the
411-
// 1.5x width-ratio guard so they share one padded batch.
412-
let lines = vec![
413-
PrepLine { w: 8, data: vec![1.0; 3 * REC_HEIGHT as usize * 8] },
414-
PrepLine { w: 10, data: vec![2.0; 3 * REC_HEIGHT as usize * 10] },
415-
];
416-
let batches = width_batches_padded(&lines);
417-
assert_eq!(batches.len(), 1, "same-ratio widths share a padded batch");
418-
let (w, chunk) = &batches[0];
419-
assert_eq!(*w, 10, "batch width is the max in the chunk");
420-
let buf = batch_input_padded(*w, chunk, &lines);
421-
assert_eq!(buf.len(), chunk.len() * 3 * REC_HEIGHT as usize * 10);
422-
// First row of the narrow line: 8 ones then 2 zero-pad columns.
423-
let narrow_ix = chunk.iter().position(|&i| lines[i].w == 8).unwrap();
424-
let row = &buf[narrow_ix * 3 * REC_HEIGHT as usize * 10..][..10];
425-
assert_eq!(row, &[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0]);
426-
427-
// A line >1.5x the chunk minimum starts a fresh batch (bounds waste).
428-
let spread = vec![
429-
PrepLine { w: 8, data: vec![0.0; 3 * REC_HEIGHT as usize * 8] },
430-
PrepLine { w: 40, data: vec![0.0; 3 * REC_HEIGHT as usize * 40] },
431-
];
432-
assert_eq!(width_batches_padded(&spread).len(), 2);
433-
}
434344
}

crates/docling-wasm/src/ocr.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232
3333
use docling_core::{DoclingDocument, Node};
3434
use docling_pdf::ocr_prep::{
35-
batch_input_padded, decode_row, dict_chars, normalize_polarity, prep_page_lines,
36-
width_batches_padded, REC_HEIGHT,
35+
batch_input, decode_row, dict_chars, normalize_polarity, prep_page_lines, width_batches,
36+
REC_HEIGHT,
3737
};
3838
use wasm_bindgen::prelude::*;
3939

@@ -79,11 +79,8 @@ pub async fn ocr_image(
7979
let lines = prep_page_lines(&img);
8080
let chars = dict_chars(dict);
8181
let mut texts = vec![String::new(); lines.len()];
82-
// Browser path batches lines of differing width (padded to the batch max)
83-
// — far fewer ORT calls than exact-width grouping, output-equivalent under
84-
// the model's zero padding. Native keeps the bit-identical exact-width path.
85-
for (w, chunk) in width_batches_padded(&lines) {
86-
let input = batch_input_padded(w, &chunk, &lines);
82+
for (w, chunk) in width_batches(&lines) {
83+
let input = batch_input(w, &chunk, &lines);
8784
let out = session
8885
.run(
8986
chunk.len() as u32,

crates/docling-wasm/src/scanned.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
2323
use docling_pdf::layout::{decode_layout, layout_input, SIDE};
2424
use docling_pdf::ocr_prep::{
25-
batch_input_padded, decode_row, dict_chars, normalize_polarity, prep_region_lines,
26-
width_batches_padded, REC_HEIGHT,
25+
batch_input, decode_row, dict_chars, normalize_polarity, prep_region_lines, width_batches,
26+
REC_HEIGHT,
2727
};
2828
use docling_pdf::pdfium_backend::{PdfPage, TextCell};
2929
use docling_pdf::scanned::{assemble_page, finish_document, refine_regions};
@@ -110,10 +110,8 @@ impl ScannedConverter {
110110
// OCR the text regions (same gather/batch/decode as native ocr_page).
111111
let (bboxes, lines) = prep_region_lines(&img, &regions, scale);
112112
let mut texts = vec![String::new(); lines.len()];
113-
// Padded batching across differing widths (browser-only): far fewer ORT
114-
// calls than exact-width grouping, output-equivalent under zero padding.
115-
for (w, chunk) in width_batches_padded(&lines) {
116-
let data = batch_input_padded(w, &chunk, &lines);
113+
for (w, chunk) in width_batches(&lines) {
114+
let data = batch_input(w, &chunk, &lines);
117115
let out = rec
118116
.run(
119117
chunk.len() as u32,

0 commit comments

Comments
 (0)