docling_pdf/tableformer.rs
1//! TableFormer: table-structure recovery via docling-ibm-models, exported to
2//! ONNX by `scripts/install/export_tableformer.py`. The image encoder + tag-transformer
3//! encoder run once to a memory tensor; the decoder is then stepped
4//! autoregressively to emit an OTSL structure-token sequence (the same model
5//! docling runs). See docs/PDF_CONFORMANCE.md.
6
7use crate::pdfium_backend::TextCell;
8// The ONNX-free half (preprocessing, structure corrections, bbox bookkeeping,
9// span merge, OTSL→grid) lives in tf_core so the browser build (#157 stage 3)
10// runs the same logic; this file owns the three `ort` sessions and the
11// owned-value KV-cache fast path.
12use crate::tf_core::{
13 argmax, build_table_cells, correct, merge_spans, preprocess_input, BboxBook, TableCell, END,
14 MAX_ROW_TAGS, MAX_STEPS, START, UCEL,
15};
16use image::RgbImage;
17use ort::session::Session;
18use ort::value::{DynValue, Tensor, TensorRef};
19
20const SIDE: usize = crate::tf_core::SIDE as usize;
21const EMBED_DIM: usize = crate::tf_core::EMBED_DIM;
22/// Decoder geometry, fixed by the exported TableModel04_rs graph: the cached
23/// decoder threads a `[N_LAYERS, past, 1, EMBED_DIM]` per-layer state cache.
24const N_LAYERS: usize = 6;
25
26/// Resolve the encoder / decoder / bbox files exactly as [`TableFormer::load`]
27/// will (shared with `model_inventory`, so diagnostics can never drift from
28/// what actually loads). Explicit `DOCLING_TABLEFORMER_*` overrides win; the
29/// decoder otherwise picks by preference — INT8 variants first unless
30/// `DOCLING_RS_FP32` opts out, and within a precision the true-KV-cache
31/// export (`decoder_kv*`, one token per step, O(past) step cost) ranks ahead
32/// of the legacy layer-output-cache graph it matches byte-for-byte (91/91
33/// snapshot corpus exact with either; the KV graph re-measured ~13–17% faster
34/// warm, so speed wins the default and the legacy file stays as the smaller
35/// fallback). `decoder_kv` ranks ABOVE `decoder_int8`: the #97 hoisted fp32
36/// KV graph is faster than the quantized legacy graph on every machine
37/// measured, and it is byte-exact (its own int8 variant is not produced — see
38/// quantize_models.py). `decoder_kv_layers` ranks just above `decoder_kv`:
39/// the same graph with per-layer caches (#871, [`DecoderStyle::KvLayered`]),
40/// bit-identical and without per-step allocations that grow with the table.
41pub fn resolved_paths() -> (String, String, String) {
42 // The encoder ranks its fp16-weight repack (`encoder_fp16.onnx`, #374 —
43 // the same graph with the weights stored as fp16 and cast back to fp32
44 // at load, ~half the download, fp32 compute) ahead of the fp32 file
45 // unless `DOCLING_RS_FP32` opts out; an explicit override wins.
46 let enc = docling_core::env::nonempty("DOCLING_TABLEFORMER_ENCODER").unwrap_or_else(|| {
47 let candidates: &[&str] = if crate::prefer_fp32() {
48 &[".models/tableformer/encoder.onnx"]
49 } else {
50 &[
51 ".models/tableformer/encoder_fp16.onnx",
52 ".models/tableformer/encoder.onnx",
53 ]
54 };
55 candidates
56 .iter()
57 .map(|p| crate::resolve_asset(p))
58 .find(|p| std::path::Path::new(p).exists())
59 .unwrap_or_else(|| crate::resolve_asset(".models/tableformer/encoder.onnx"))
60 });
61 let dec = docling_core::env::nonempty("DOCLING_TABLEFORMER_DECODER").unwrap_or_else(|| {
62 let candidates: &[&str] = if crate::prefer_fp32() {
63 &[
64 ".models/tableformer/decoder_kv_layers.onnx",
65 ".models/tableformer/decoder_kv.onnx",
66 ".models/tableformer/decoder.onnx",
67 ]
68 } else {
69 &[
70 ".models/tableformer/decoder_kv_int8.onnx",
71 ".models/tableformer/decoder_kv_layers.onnx",
72 ".models/tableformer/decoder_kv.onnx",
73 ".models/tableformer/decoder_int8.onnx",
74 ".models/tableformer/decoder.onnx",
75 ]
76 };
77 candidates
78 .iter()
79 .map(|p| crate::resolve_asset(p))
80 .find(|p| std::path::Path::new(p).exists())
81 .unwrap_or_else(|| ".models/tableformer/decoder.onnx".to_string())
82 });
83 let bbx = docling_core::env::nonempty("DOCLING_TABLEFORMER_BBOX")
84 .unwrap_or_else(|| crate::resolve_asset(".models/tableformer/bbox.onnx"));
85 (enc, dec, bbx)
86}
87
88pub struct TableFormer {
89 encoder: Session,
90 decoder: Session,
91 bbox: Session,
92 /// Which decoder graph flavour is loaded, detected from the session's
93 /// input names (so an explicit `DOCLING_TABLEFORMER_DECODER` override
94 /// works with any of them).
95 style: DecoderStyle,
96 /// The `KvHoisted` decoder's `tag` input has a symbolic batch axis (the
97 /// dynamic-batch `decoder_kv.onnx` export): a page's tables decode
98 /// together, one step for all of them — see [`Self::predict_tables_on`].
99 /// The older fixed-`[1,1]` export decodes the tables one after another.
100 batched: bool,
101 /// The `KvLayered` loop runs through an `IoBinding` that keeps the caches
102 /// and the cross tensors where the provider computes ([`DeviceCache`]):
103 /// on under a CUDA-class provider (CUDA, TensorRT); DirectML only with
104 /// `DOCLING_RS_TF_IOBINDING=1`, which forces it either way (also on CPU,
105 /// which is how it is tested without a GPU; `0` turns it off). Cleared — for good — when a
106 /// table's first bound step fails, so the host-buffer loop takes over.
107 iobinding: bool,
108 /// The decoder session was built without the GPU provider (DirectML):
109 /// its tensors live on the host whatever the other sessions use.
110 decoder_on_host: bool,
111}
112
113/// The three decoder-graph generations the loop supports.
114#[derive(Clone, Copy, PartialEq, Eq)]
115enum DecoderStyle {
116 /// `decoder.onnx`: layer-output cache; feeds the full `tags` prefix and a
117 /// single `cache` every step.
118 Legacy,
119 /// The pre-#97 `decoder_kv.onnx`: one tag per step, `cache_k`/`cache_v`,
120 /// with the stacked `cross_k`/`cross_v` re-split inside every step.
121 KvStacked,
122 /// The #97 `decoder_kv.onnx`: one tag per step, and the constant cross
123 /// tensors arrive as 2×`N_LAYERS` per-layer inputs (`cross_kt_i` already
124 /// transposed for q·Kᵀ, `cross_v_i`), computed once per table by the
125 /// encoder — the step graph does no work proportional to their size.
126 KvHoisted,
127 /// `decoder_kv_layers.onnx` (#871, `scripts/install/kv_layers.py`): the
128 /// `KvHoisted` graph with the self-attention cache per layer — `Kᵀ` and
129 /// `V` in and out for each layer instead of two stacked tensors — whose
130 /// outputs land in buffers the loop owns ([`LayerCache`]).
131 KvLayered,
132}
133
134impl DecoderStyle {
135 /// One tag per step with the per-layer cross tensors the encoder emits.
136 fn hoisted(self) -> bool {
137 matches!(self, DecoderStyle::KvHoisted | DecoderStyle::KvLayered)
138 }
139}
140
141/// KV-cache geometry fixed by the `decoder_kv.onnx` export
142/// (`[N_LAYERS, 1, KV_HEADS, past, KV_HEAD_DIM]`, `KV_HEADS × KV_HEAD_DIM = EMBED_DIM`).
143const KV_HEADS: usize = 8;
144const KV_HEAD_DIM: usize = 64;
145
146/// The autoregressive decode state: `a` is the legacy layer-output cache, or
147/// `cache_k` for the KV graph; `b` is `cache_v` (KV graph only). `None` = first
148/// step (the zero-`past` empties are allocated per table by [`TableFormer::empty_cache`]).
149#[derive(Default)]
150struct DecodeCache {
151 a: Option<DynValue>,
152 b: Option<DynValue>,
153 /// The `KvLayered` caches, set up on the first step.
154 layers: Option<LayerCache>,
155 /// The `KvLayered` caches on the device, when the loop binds them there.
156 device: Option<DeviceCache>,
157}
158
159/// The `KvLayered` decode under a GPU provider (CUDA, TensorRT, DirectML):
160/// an `IoBinding` holding the
161/// step's inputs where the provider computes. With plain runs, every step
162/// handed ONNX Runtime host tensors to copy to the device and got host
163/// tensors back — the 12 constant cross tensors (~19 MB per table, the same
164/// every step) and both directions of the KV cache (~4 MB each way at 160
165/// tokens, growing) — so on CUDA a step cost 12.6 ms against the ~3 ms its
166/// 49 small GEMMs take. Here the cross tensors are bound once per decode
167/// (one copy), the cache outputs stay in device memory and are bound as the
168/// next step's inputs, and only `logits`/`hidden` (kilobytes) come back.
169/// Same graph, same kernels: the output is the plain loop's.
170struct DeviceCache {
171 binding: ort::session::IoBinding,
172 rows: usize,
173 past: usize,
174 /// The last step's cache outputs, where the provider wrote them.
175 cache: Vec<DynValue>,
176 /// Zero-`past` first-step inputs.
177 empty: Vec<Tensor<f32>>,
178 /// Where the cache outputs are bound: the GPU provider's device memory,
179 /// or the CPU when `DOCLING_RS_TF_IOBINDING` forces the loop without one.
180 device: ort::memory::MemoryInfo<'static>,
181}
182
183/// The `KvLayered` decoder's self-attention caches, in memory docling owns
184/// (#871). With ONNX Runtime allocating them, every step of a table made a
185/// cache one token longer than the last and freed the previous one —
186/// hundreds of ever-larger blocks per table, which on linux/arm64 with the
187/// CPU arena off fragmented glibc's heap until a 6 GiB container ran out of
188/// memory. Here each step reads its caches from one of two buffers and ONNX
189/// Runtime writes the one-token-longer caches straight into the other (the
190/// outputs are preallocated over it, [`docling_onnx::borrowed_f32`]); the
191/// next step swaps them. Each buffer holds the `2 × N_LAYERS` tensors in
192/// regions of `cap` floats — per layer `Kᵀ` `[rows, H, hd, past]`, then `V`
193/// `[rows, H, past, hd]` — sized for [`MAX_STEPS`] tokens: zeroed (`calloc`,
194/// so mapped lazily), only the prefix a table reaches becomes resident, and
195/// they are released when the table (or the page's batch) is done.
196struct LayerCache {
197 rows: usize,
198 past: usize,
199 cap: usize,
200 bufs: [Vec<f32>; 2],
201 /// Which of `bufs` holds the current caches.
202 cur: usize,
203 /// Zero-`past` first-step inputs (ort's slice constructors reject a
204 /// 0-length dim, the session allocator does not).
205 empty: Vec<Tensor<f32>>,
206}
207
208impl LayerCache {
209 fn new(alloc: &ort::memory::Allocator, rows: usize) -> Result<Self, String> {
210 let cap = rows * KV_HEADS * MAX_STEPS * KV_HEAD_DIM;
211 let mut empty = Vec::with_capacity(2 * N_LAYERS);
212 for j in 0..2 * N_LAYERS {
213 empty.push(
214 Tensor::<f32>::new(alloc, Self::shape(j, rows, 0))
215 .map_err(|e| format!("tableformer: empty kv cache: {e}"))?,
216 );
217 }
218 Ok(Self {
219 rows,
220 past: 0,
221 cap,
222 bufs: [vec![0.0; 2 * N_LAYERS * cap], vec![0.0; 2 * N_LAYERS * cap]],
223 cur: 0,
224 empty,
225 })
226 }
227
228 /// Tensor `j`'s shape at `past` tokens: `Kᵀ` for the first `N_LAYERS`.
229 fn shape(j: usize, rows: usize, past: usize) -> [usize; 4] {
230 if j < N_LAYERS {
231 [rows, KV_HEADS, KV_HEAD_DIM, past]
232 } else {
233 [rows, KV_HEADS, past, KV_HEAD_DIM]
234 }
235 }
236
237 fn input_name(j: usize) -> String {
238 if j < N_LAYERS {
239 format!("cache_kt_{j}")
240 } else {
241 format!("cache_v_{}", j - N_LAYERS)
242 }
243 }
244}
245
246/// Zero-`past` first-step cache tensors: `(cache, None)` for the legacy graph,
247/// `(cache_k, Some(cache_v))` for the KV graph.
248type EmptyCache = (Tensor<f32>, Option<Tensor<f32>>);
249
250/// Encoder outputs that drive the cached decode loop: the per-layer cross-attention
251/// K/V (projected from the image memory once, constant across decode steps) and
252/// `enc_out` for the bbox decoder. Kept as owned `ort` values so each decode step
253/// (and the bbox run) borrows them directly — no per-step extract/copy/re-wrap.
254struct EncodeOut {
255 /// Stacked `[N_LAYERS,1,H,S,hd]` cross K/V — the `Legacy`/`KvStacked`
256 /// decoders' inputs. `None` for `KvHoisted`, which reads the per-layer
257 /// tensors instead: the stacked pair is 2×9.6 MB per table, and a page's
258 /// tables are now all held encoded at once for the batched loop.
259 ck: Option<DynValue>,
260 cv: Option<DynValue>,
261 eo: DynValue,
262 /// `KvHoisted` only: per-layer `[cross_kt_0..N, cross_v_0..N]`, index-aligned
263 /// with the decoder's input names, borrowed by every decode step.
264 per_layer: Vec<(String, DynValue)>,
265}
266
267impl TableFormer {
268 /// Load the exported encoder/decoder/bbox ONNX graphs (env overrides, else
269 /// `.models/tableformer/{encoder,decoder,bbox}.onnx`). Returns `None` if any is
270 /// absent, so the pipeline falls back to geometric reconstruction.
271 pub fn load() -> Option<Self> {
272 Self::load_with(crate::intra_threads())
273 }
274
275 /// Like [`load`](Self::load) but with an explicit intra-op thread count, so a
276 /// parallel page-worker pool can run each table model on fewer threads (the
277 /// throughput comes from running pages concurrently, not from one fat model).
278 ///
279 /// See [`resolved_paths`] for the encoder/decoder/bbox file selection.
280 pub fn load_with(intra: usize) -> Option<Self> {
281 // (resolution shared with the model inventory — see resolved_paths)
282 let (enc, dec, bbx) = resolved_paths();
283 if crate::timing::enabled() {
284 eprintln!("docling-pdf: tableformer decoder: {dec}");
285 }
286 if [&enc, &dec, &bbx]
287 .iter()
288 .any(|p| !std::path::Path::new(p).exists())
289 {
290 // The geometric fallback is a supported, intentional configuration
291 // (docling has no ML table-structure equivalent baked in either), so
292 // this stays a single quiet stderr note rather than an error — but it
293 // fires every process (not per-worker) so a CWD-relative default that
294 // silently misses its files (a very easy mistake for anything not run
295 // from the repo root, e.g. an embedding app) is at least visible once.
296 warn_missing_once(&enc, &dec, &bbx);
297 return None;
298 }
299 // The decoder's KV-cache grows by one entry every autoregressive step, so
300 // its input shapes differ on every `run()` call. ONNX Runtime's memory
301 // pattern optimizer assumes stable shapes to plan buffer reuse; disabling
302 // it for this session avoids repeatedly re-validating/re-touching that
303 // plan (and the external-weights file) on each step. The bbox head has
304 // the same problem one level up: its `tag_h` input is `[ncells, 512]`
305 // and every table has a different cell count, so with the pattern
306 // planner on each run re-plans — and on this graph the plan is *worse*
307 // than none: 290 ms vs 54 ms for a 100-cell table, 560 vs 94 ms for
308 // 200 cells (ORT 1.22, 4 threads). It was 0.26 s per table on the
309 // corpus, more than the encoder.
310 //
311 // The decoder runs on ONE intra-op thread. A step is 49 small GEMMs
312 // over a single token — it streams the layer weights, it does not
313 // compute — so extra threads only add synchronisation: measured 4.1 ms
314 // per step on 1 thread vs 5.5 on 4 (7.1 vs 4.9 once the cache is 100+
315 // long). In the pool it also stops a table decode from taking all the
316 // cores away from the other workers' layout inference. And a
317 // single-thread session has a fixed reduction order, so table
318 // structure no longer varies run-to-run on near-tie tokens the way
319 // multi-threaded float sums let it (the conformance scripts pin one
320 // thread for exactly that reason; the default now matches them). The
321 // encoder keeps the shared budget: one 448×448 CNN + transformer pass
322 // per table, 680 ms single-threaded vs 165 on four.
323 // The decoder runs on the CPU under DirectML. As exported, every
324 // decoder graph fails inside the provider (`Reshape node_view`,
325 // 0x8007023E, on both adapters of the first DirectML machine that
326 // tried), and every table fell back to geometric structure: DirectML
327 // rejects the `allowzero=1` torch's exporter writes on each `view`
328 // Reshape. With it cleared — equivalent here, no target shape holds a
329 // 0 — the decoder runs there, but slower than on the CPU: 36.9 ms per
330 // step against 13.9 on the same laptop (2203.01017v2, 141 steps). A
331 // step is ~150 tiny ops over one token, kernel launches dominate, so
332 // the encoder and the bbox head stay on the GPU and only the decoder
333 // moves.
334 let decoder_on_host = docling_onnx::is_directml();
335 let build = |path: &str,
336 mem_pattern: bool,
337 threads: usize,
338 host: bool|
339 -> Result<Session, String> {
340 let builder = docling_onnx::session_builder()?
341 .with_intra_threads(threads)
342 .map_err(|e| e.to_string())?
343 .with_memory_pattern(mem_pattern)
344 .map_err(|e| e.to_string())?;
345 let variant = if mem_pattern {
346 "mem_pattern"
347 } else {
348 "no_mem_pattern"
349 };
350 let builder = if host {
351 docling_onnx::apply_host(builder)?
352 } else {
353 docling_onnx::apply(builder)?
354 };
355 docling_onnx::commit(builder, path, variant)
356 .map_err(|e| format!("tableformer load {path}: {e}"))
357 };
358 match (
359 build(&enc, true, intra, false),
360 build(&dec, false, 1, decoder_on_host),
361 build(&bbx, false, intra, false),
362 ) {
363 (Ok(encoder), Ok(decoder), Ok(bbox)) => {
364 let has = |n: &str| decoder.inputs().iter().any(|i| i.name() == n);
365 let style = if has("cache_kt_0") {
366 DecoderStyle::KvLayered
367 } else if has("cross_kt_0") {
368 DecoderStyle::KvHoisted
369 } else if has("cache_k") {
370 DecoderStyle::KvStacked
371 } else {
372 DecoderStyle::Legacy
373 };
374 if style.hoisted() && !encoder.outputs().iter().any(|o| o.name() == "cross_kt_0") {
375 eprintln!(
376 "docling-pdf: tableformer decoder needs per-layer cross tensors \
377 (cross_kt_*) the encoder doesn't emit — re-download or re-export \
378 the model set (scripts/install/export_tableformer.py); \
379 falling back to geometric tables"
380 );
381 return None;
382 }
383 // Dynamic batch axis on `tag` ⇒ the export batches decode
384 // steps across tables (ort reports a symbolic dim as -1).
385 let batched = style.hoisted()
386 && decoder.inputs().iter().any(|i| {
387 i.name() == "tag"
388 && matches!(i.dtype(), ort::value::ValueType::Tensor { shape, .. }
389 if shape.first().is_some_and(|d| *d < 0))
390 });
391 if crate::timing::enabled() && batched {
392 eprintln!("docling-pdf: tableformer decoder batches a page's tables per step");
393 }
394 if crate::timing::enabled() && decoder_on_host {
395 eprintln!("docling-pdf: tableformer decoder runs on the CPU (DirectML)");
396 }
397 let iobinding = style == DecoderStyle::KvLayered
398 && match docling_core::env::nonempty("DOCLING_RS_TF_IOBINDING") {
399 Some(_) => docling_core::env::flag("DOCLING_RS_TF_IOBINDING"),
400 // Not when the decoder was kept on the host
401 // (DirectML, above): nothing to keep on a device.
402 None => !decoder_on_host && docling_onnx::device_memory_info().is_some(),
403 };
404 if crate::timing::enabled() && iobinding {
405 eprintln!("docling-pdf: tableformer decoder keeps its caches on the device");
406 }
407 Some(Self {
408 encoder,
409 decoder,
410 bbox,
411 style,
412 batched,
413 iobinding,
414 decoder_on_host,
415 })
416 }
417 _ => None,
418 }
419 }
420
421 /// Run the image encoder and capture what the cached decoder loop needs: each
422 /// decoder layer's cross-attention K/V (projected from the image memory once,
423 /// shape `[N_LAYERS,1,H,S,head_dim]`) and `enc_out` for the bbox decoder.
424 fn encode(&mut self, img: &RgbImage) -> Result<EncodeOut, String> {
425 let input = crate::timing::timed("tf.preprocess", || preprocess(img))?;
426 let mut enc_out = crate::timing::timed("tf.encoder", || {
427 docling_onnx::run(&mut self.encoder, ort::inputs!["image" => input])
428 .map_err(|e| format!("tableformer: encode: {e}"))
429 })?;
430 let mut per_layer = Vec::new();
431 if self.style.hoisted() {
432 for prefix in ["cross_kt_", "cross_v_"] {
433 for i in 0.. {
434 let name = format!("{prefix}{i}");
435 match enc_out.remove(&name) {
436 Some(v) => per_layer.push((name, v)),
437 None => break,
438 }
439 }
440 }
441 if per_layer.is_empty() {
442 return Err("tableformer: encoder emitted no cross_kt_* outputs".into());
443 }
444 }
445 let mut grab = |name: &str| -> Result<DynValue, String> {
446 enc_out
447 .remove(name)
448 .ok_or_else(|| format!("tableformer: encoder output {name} missing"))
449 };
450 let hoisted = self.style.hoisted();
451 Ok(EncodeOut {
452 ck: if hoisted {
453 None
454 } else {
455 Some(grab("cross_k")?)
456 },
457 cv: if hoisted {
458 None
459 } else {
460 Some(grab("cross_v")?)
461 },
462 eo: grab("enc_out")?,
463 per_layer,
464 })
465 }
466
467 /// One doubly-cached decode step: feed the current `tags`, the constant cross
468 /// K/V, and the growing self-attention `cache`; return the raw argmax tag and
469 /// the last token's hidden state, advancing the cache. The cache stays an owned
470 /// `ort` value — the previous step's `out_cache` output is fed back directly,
471 /// never extracted or copied (it grows every step, so per-step copies were
472 /// O(steps²) float traffic). `empty_cache` is the zero-`past` value used on the
473 /// first step (ort's array constructors reject a 0-length dim, so it is
474 /// allocated through the session allocator by the caller).
475 fn decode_step(
476 &mut self,
477 tags: &[i64],
478 enc: &EncodeOut,
479 cache: &mut DecodeCache,
480 empty: &EmptyCache,
481 ) -> Result<(i64, Vec<f32>), String> {
482 crate::timing::timed("tf.decode_step", || {
483 self.decode_step_inner(tags, enc, cache, empty)
484 })
485 }
486
487 fn decode_step_inner(
488 &mut self,
489 tags: &[i64],
490 enc: &EncodeOut,
491 cache: &mut DecodeCache,
492 empty: &EmptyCache,
493 ) -> Result<(i64, Vec<f32>), String> {
494 if self.style.hoisted() {
495 // #97 graph: one tag; the constant per-layer cross tensors are
496 // borrowed views — the step pays nothing proportional to them.
497 let last = *tags.last().expect("decode starts from <start>");
498 let (raws, hidden) = self.step_kv(&[last], &enc.per_layer, cache, empty)?;
499 return Ok((raws[0], hidden));
500 }
501 let (ck, cv) = match (enc.ck.as_ref(), enc.cv.as_ref()) {
502 (Some(k), Some(v)) => (k, v),
503 _ => return Err("tableformer: stacked cross K/V missing".into()),
504 };
505 let mut dout = match self.style {
506 DecoderStyle::KvHoisted | DecoderStyle::KvLayered => unreachable!("handled above"),
507 DecoderStyle::KvStacked => {
508 // Pre-#97 KV graph: feed only the newly emitted tag; the projected
509 // K/V for the whole prefix live in cache_k/cache_v and are fed
510 // back as-is.
511 let last = *tags.last().expect("decode starts from <start>");
512 let tag_t = Tensor::from_array(([1usize, 1usize], vec![last]))
513 .map_err(|e| format!("tableformer: tag: {e}"))?;
514 match (cache.a.as_ref(), cache.b.as_ref()) {
515 (Some(k), Some(v)) => docling_onnx::run(
516 &mut self.decoder,
517 ort::inputs![
518 "tag" => tag_t, "cross_k" => ck, "cross_v" => cv,
519 "cache_k" => k, "cache_v" => v],
520 ),
521 _ => docling_onnx::run(
522 &mut self.decoder,
523 ort::inputs![
524 "tag" => tag_t, "cross_k" => ck, "cross_v" => cv,
525 "cache_k" => &empty.0,
526 "cache_v" => empty.1.as_ref().expect("kv empty cache has both halves")],
527 ),
528 }
529 }
530 DecoderStyle::Legacy => {
531 let tags_t = Tensor::from_array(([tags.len(), 1usize], tags.to_vec()))
532 .map_err(|e| format!("tableformer: tags: {e}"))?;
533 match cache.a.as_ref() {
534 None => docling_onnx::run(
535 &mut self.decoder,
536 ort::inputs![
537 "tags" => tags_t, "cross_k" => ck, "cross_v" => cv,
538 "cache" => &empty.0],
539 ),
540 Some(c) => docling_onnx::run(
541 &mut self.decoder,
542 ort::inputs![
543 "tags" => tags_t, "cross_k" => ck, "cross_v" => cv,
544 "cache" => c],
545 ),
546 }
547 }
548 }
549 .map_err(|e| format!("tableformer: decode: {e}"))?;
550 let (_, logits) = dout["logits"]
551 .try_extract_tensor::<f32>()
552 .map_err(|e| format!("tableformer: logits: {e}"))?;
553 let raw = argmax(logits) as i64;
554 let (_, hidden) = dout["hidden"]
555 .try_extract_tensor::<f32>()
556 .map_err(|e| format!("tableformer: hidden: {e}"))?;
557 let hidden = hidden.to_vec();
558 if self.style != DecoderStyle::Legacy {
559 cache.a = Some(
560 dout.remove("out_cache_k")
561 .ok_or_else(|| "tableformer: out_cache_k missing".to_string())?,
562 );
563 cache.b = Some(
564 dout.remove("out_cache_v")
565 .ok_or_else(|| "tableformer: out_cache_v missing".to_string())?,
566 );
567 } else {
568 cache.a = Some(
569 dout.remove("out_cache")
570 .ok_or_else(|| "tableformer: decoder output out_cache missing".to_string())?,
571 );
572 }
573 Ok((raw, hidden))
574 }
575
576 /// One step of a one-tag-per-step graph over `tags.len()` rows, through
577 /// whichever cache layout the loaded decoder has.
578 fn step_kv(
579 &mut self,
580 tags: &[i64],
581 per_layer: &[(String, DynValue)],
582 cache: &mut DecodeCache,
583 empty: &EmptyCache,
584 ) -> Result<(Vec<i64>, Vec<f32>), String> {
585 if self.style == DecoderStyle::KvLayered && self.iobinding {
586 match self.step_kv_device(tags, per_layer, cache) {
587 Ok(step) => return Ok(step),
588 // A provider that will not hand out its device memory to a
589 // binding (the device-resident path is only verified on CUDA
590 // and on CPU) fails on a decode's first step, before any
591 // state lives there: decode on with host caches, here and for
592 // every later table. A later failure is a real one.
593 Err(e) if cache.device.as_ref().is_none_or(|d| d.past == 0) => {
594 eprintln!(
595 "docling-pdf: tableformer: device-resident decode unavailable ({e}); \
596 decoding with host caches"
597 );
598 self.iobinding = false;
599 cache.device = None;
600 }
601 Err(e) => return Err(e),
602 }
603 }
604 if self.style == DecoderStyle::KvLayered {
605 self.step_kv_layered(tags, per_layer, cache)
606 } else {
607 self.step_kv_hoisted(tags, per_layer, cache, empty)
608 }
609 }
610
611 /// One `KvLayered` step through the decode's [`DeviceCache`] binding.
612 fn step_kv_device(
613 &mut self,
614 tags: &[i64],
615 per_layer: &[(String, DynValue)],
616 cache: &mut DecodeCache,
617 ) -> Result<(Vec<i64>, Vec<f32>), String> {
618 let rows = tags.len();
619 let err = |what: &str, e: ort::Error| format!("tableformer: {what}: {e}");
620 let dc = match &mut cache.device {
621 Some(dc) => dc,
622 None => {
623 let mut binding = self
624 .decoder
625 .create_binding()
626 .map_err(|e| err("io binding", e))?;
627 // The constant cross tensors: copied to the device once here,
628 // and every step of this decode reads that copy.
629 for (name, v) in per_layer {
630 binding
631 .bind_input(name.as_str(), v)
632 .map_err(|e| err(name, e))?;
633 }
634 binding
635 .synchronize_inputs()
636 .map_err(|e| err("cross inputs", e))?;
637 let alloc = self.decoder.allocator();
638 let mut empty = Vec::with_capacity(2 * N_LAYERS);
639 for j in 0..2 * N_LAYERS {
640 empty.push(
641 Tensor::<f32>::new(alloc, LayerCache::shape(j, rows, 0))
642 .map_err(|e| err("empty kv cache", e))?,
643 );
644 }
645 cache.device.insert(DeviceCache {
646 binding,
647 rows,
648 past: 0,
649 cache: Vec::new(),
650 empty,
651 device: if self.decoder_on_host {
652 ort::memory::MemoryInfo::default()
653 } else {
654 docling_onnx::device_memory_info().unwrap_or_default()
655 },
656 })
657 }
658 };
659 if dc.rows != rows || dc.past >= MAX_STEPS {
660 return Err(format!(
661 "tableformer: kv cache for {} rows at {} tokens, asked for {rows} rows one more",
662 dc.rows, dc.past
663 ));
664 }
665 let tag_t =
666 Tensor::from_array(([rows, 1usize], tags.to_vec())).map_err(|e| err("tag", e))?;
667 dc.binding
668 .bind_input("tag", &tag_t)
669 .map_err(|e| err("tag", e))?;
670 for j in 0..2 * N_LAYERS {
671 let name = LayerCache::input_name(j);
672 if dc.past == 0 {
673 dc.binding.bind_input(name.as_str(), &dc.empty[j])
674 } else {
675 dc.binding.bind_input(name.as_str(), &dc.cache[j])
676 }
677 .map_err(|e| err(&name, e))?;
678 // Re-bound every step: the output is one token longer each time,
679 // so ONNX Runtime must allocate it afresh rather than reuse the
680 // previous step's value.
681 let out = format!("out_{name}");
682 dc.binding
683 .bind_output_to_device(out.as_str(), &dc.device)
684 .map_err(|e| err(&out, e))?;
685 }
686 let host = ort::memory::MemoryInfo::default();
687 for name in ["logits", "hidden"] {
688 dc.binding
689 .bind_output_to_device(name, &host)
690 .map_err(|e| err(name, e))?;
691 }
692 // The CUDA arena is shrunk at a decode's first step only (#842's run
693 // option): shrinking after every step handed the step's working
694 // memory back to the driver and allocated it again on the next one —
695 // a cudaFree + cudaMalloc pair per token. Once per decode still
696 // returns the previous table's chunks before this one grows.
697 let shrink = if dc.past == 0 {
698 docling_onnx::run_options()
699 } else {
700 None
701 };
702 let mut dout = match shrink {
703 Some(opts) => self.decoder.run_binding_with_options(&dc.binding, opts),
704 None => self.decoder.run_binding(&dc.binding),
705 }
706 .map_err(|e| err("decode", e))?;
707 let (_, logits) = dout["logits"]
708 .try_extract_tensor::<f32>()
709 .map_err(|e| err("logits", e))?;
710 let vocab = logits.len() / rows;
711 let raws: Vec<i64> = logits
712 .chunks_exact(vocab)
713 .map(|row| argmax(row) as i64)
714 .collect();
715 let (_, hidden) = dout["hidden"]
716 .try_extract_tensor::<f32>()
717 .map_err(|e| err("hidden", e))?;
718 let hidden = hidden.to_vec();
719 let mut next = Vec::with_capacity(2 * N_LAYERS);
720 for j in 0..2 * N_LAYERS {
721 let out = format!("out_{}", LayerCache::input_name(j));
722 next.push(
723 dout.remove(out.as_str())
724 .ok_or_else(|| format!("tableformer: {out} missing"))?,
725 );
726 }
727 drop(dout);
728 dc.cache = next;
729 dc.past += 1;
730 Ok((raws, hidden))
731 }
732
733 /// One `KvLayered` step: [`Self::step_kv_hoisted`] with the caches read
734 /// from and written into the [`LayerCache`] buffers — ONNX Runtime
735 /// allocates nothing that grows with the table (#871).
736 fn step_kv_layered(
737 &mut self,
738 tags: &[i64],
739 per_layer: &[(String, DynValue)],
740 cache: &mut DecodeCache,
741 ) -> Result<(Vec<i64>, Vec<f32>), String> {
742 let rows = tags.len();
743 let lc = match &mut cache.layers {
744 Some(lc) => lc,
745 None => cache
746 .layers
747 .insert(LayerCache::new(self.decoder.allocator(), rows)?),
748 };
749 if lc.rows != rows || lc.past >= MAX_STEPS {
750 return Err(format!(
751 "tableformer: kv cache for {} rows at {} tokens, asked for {rows} rows one more",
752 lc.rows, lc.past
753 ));
754 }
755 let (past, cap) = (lc.past, lc.cap);
756 let tag_t = Tensor::from_array(([rows, 1usize], tags.to_vec()))
757 .map_err(|e| format!("tableformer: tag: {e}"))?;
758 let [first, second] = &mut lc.bufs;
759 let (src, dst) = if lc.cur == 0 {
760 (&*first, second)
761 } else {
762 (&*second, first)
763 };
764 let mut inputs: Vec<(
765 std::borrow::Cow<'_, str>,
766 ort::session::SessionInputValue<'_>,
767 )> = Vec::with_capacity(1 + 2 * N_LAYERS + per_layer.len());
768 inputs.push(("tag".into(), tag_t.into()));
769 let in_len = rows * KV_HEADS * past * KV_HEAD_DIM;
770 for j in 0..2 * N_LAYERS {
771 let name = LayerCache::input_name(j);
772 if past == 0 {
773 inputs.push((name.into(), (&lc.empty[j]).into()));
774 } else {
775 let view = TensorRef::from_array_view((
776 LayerCache::shape(j, rows, past),
777 &src[j * cap..j * cap + in_len],
778 ))
779 .map_err(|e| format!("tableformer: {name}: {e}"))?;
780 inputs.push((name.into(), view.into()));
781 }
782 }
783 for (name, v) in per_layer {
784 inputs.push((name.as_str().into(), v.into()));
785 }
786 let alloc = self.decoder.allocator();
787 let mut outputs = ort::session::OutputSelector::default();
788 for (j, region) in dst.chunks_exact_mut(cap).enumerate() {
789 // SAFETY: `dst` is borrowed mutably for this whole step and
790 // nothing else touches it; the values over it live in `opts` and
791 // `dout`, both dropped at the end of this function, before the
792 // next step reads the buffer (or the cache frees it).
793 let out = unsafe {
794 docling_onnx::borrowed_f32(region, &LayerCache::shape(j, rows, past + 1), alloc)
795 }
796 .map_err(|e| format!("tableformer: kv cache output: {e}"))?;
797 outputs = outputs.preallocate(format!("out_{}", LayerCache::input_name(j)), out);
798 }
799 let opts = docling_onnx::run_options_with_outputs(outputs)?;
800 let dout = self
801 .decoder
802 .run_with_options(inputs, &opts)
803 .map_err(|e| format!("tableformer: decode: {e}"))?;
804 let (_, logits) = dout["logits"]
805 .try_extract_tensor::<f32>()
806 .map_err(|e| format!("tableformer: logits: {e}"))?;
807 let vocab = logits.len() / rows;
808 let raws: Vec<i64> = logits
809 .chunks_exact(vocab)
810 .map(|row| argmax(row) as i64)
811 .collect();
812 let (_, hidden) = dout["hidden"]
813 .try_extract_tensor::<f32>()
814 .map_err(|e| format!("tableformer: hidden: {e}"))?;
815 let hidden = hidden.to_vec();
816 drop(dout);
817 drop(opts);
818 lc.past += 1;
819 lc.cur ^= 1;
820 Ok((raws, hidden))
821 }
822
823 /// One `KvHoisted` step over `tags.len()` rows — one table per row. `tags`
824 /// holds each row's last emitted tag, `per_layer` the cross tensors with a
825 /// matching leading batch axis (the encoder's own `[1,…]` outputs for a
826 /// single table, or [`Self::batch_cross`]'s concatenation), and the cache
827 /// grows `[N_LAYERS, rows, H, past, hd]` in lockstep. Returns each row's raw
828 /// argmax tag and the `[rows, EMBED_DIM]` hidden states, flattened.
829 fn step_kv_hoisted(
830 &mut self,
831 tags: &[i64],
832 per_layer: &[(String, DynValue)],
833 cache: &mut DecodeCache,
834 empty: &EmptyCache,
835 ) -> Result<(Vec<i64>, Vec<f32>), String> {
836 let rows = tags.len();
837 let tag_t = Tensor::from_array(([rows, 1usize], tags.to_vec()))
838 .map_err(|e| format!("tableformer: tag: {e}"))?;
839 let mut inputs: Vec<(
840 std::borrow::Cow<'_, str>,
841 ort::session::SessionInputValue<'_>,
842 )> = Vec::with_capacity(3 + per_layer.len());
843 inputs.push(("tag".into(), tag_t.into()));
844 match (cache.a.as_ref(), cache.b.as_ref()) {
845 (Some(k), Some(v)) => {
846 inputs.push(("cache_k".into(), k.into()));
847 inputs.push(("cache_v".into(), v.into()));
848 }
849 _ => {
850 inputs.push(("cache_k".into(), (&empty.0).into()));
851 inputs.push((
852 "cache_v".into(),
853 empty
854 .1
855 .as_ref()
856 .expect("kv empty cache has both halves")
857 .into(),
858 ));
859 }
860 }
861 for (name, v) in per_layer {
862 inputs.push((name.as_str().into(), v.into()));
863 }
864 let mut dout = docling_onnx::run(&mut self.decoder, inputs)
865 .map_err(|e| format!("tableformer: decode: {e}"))?;
866 let (_, logits) = dout["logits"]
867 .try_extract_tensor::<f32>()
868 .map_err(|e| format!("tableformer: logits: {e}"))?;
869 let vocab = logits.len() / rows;
870 let raws: Vec<i64> = logits
871 .chunks_exact(vocab)
872 .map(|row| argmax(row) as i64)
873 .collect();
874 let (_, hidden) = dout["hidden"]
875 .try_extract_tensor::<f32>()
876 .map_err(|e| format!("tableformer: hidden: {e}"))?;
877 let hidden = hidden.to_vec();
878 cache.a = Some(
879 dout.remove("out_cache_k")
880 .ok_or_else(|| "tableformer: out_cache_k missing".to_string())?,
881 );
882 cache.b = Some(
883 dout.remove("out_cache_v")
884 .ok_or_else(|| "tableformer: out_cache_v missing".to_string())?,
885 );
886 Ok((raws, hidden))
887 }
888
889 /// Stack the per-layer cross tensors of several encoded tables along the
890 /// batch axis (`[1,H,hd,S]` × B → `[B,H,hd,S]`, same for `cross_v`), index-
891 /// aligned with the decoder's input names. One copy per page — ~20 MB per
892 /// table, nothing next to the decode steps it lets the tables share.
893 fn batch_cross(encs: &[EncodeOut]) -> Result<Vec<(String, DynValue)>, String> {
894 let b = encs.len();
895 let mut out = Vec::with_capacity(encs[0].per_layer.len());
896 for j in 0..encs[0].per_layer.len() {
897 let name = encs[0].per_layer[j].0.clone();
898 let mut data: Vec<f32> = Vec::new();
899 let mut dims = [b, 0, 0, 0];
900 for enc in encs {
901 let (shape, v) = enc.per_layer[j]
902 .1
903 .try_extract_tensor::<f32>()
904 .map_err(|e| format!("tableformer: {name}: {e}"))?;
905 if shape.len() != 4 || shape[0] != 1 {
906 return Err(format!("tableformer: {name}: unexpected shape {shape:?}"));
907 }
908 dims[1..].copy_from_slice(&[
909 shape[1] as usize,
910 shape[2] as usize,
911 shape[3] as usize,
912 ]);
913 data.reserve(v.len() * b);
914 data.extend_from_slice(v);
915 }
916 let t = Tensor::from_array((dims, data))
917 .map_err(|e| format!("tableformer: {name}: {e}"))?;
918 out.push((name, t.into_dyn()));
919 }
920 Ok(out)
921 }
922
923 /// Decode `encs.len()` tables in lockstep: every step runs the decoder once
924 /// over all of them (a step is 49 weight-streaming GEMMs over one token per
925 /// row — B rows cost about what one does). The caches start empty for
926 /// every row and grow together, so nothing is ever padded or masked; a
927 /// table that emits `<end>` simply keeps its row (fed `END`, output
928 /// ignored) until the last one finishes. Row b of every op is exactly the
929 /// single-table computation, so each table's tokens and hidden states are
930 /// bit-identical to decoding it alone (asserted by the export script's
931 /// batching gate; the corpus snapshots pin it end-to-end).
932 fn decode_batch(&mut self, encs: &[EncodeOut]) -> Result<Vec<BboxBook>, String> {
933 let b = encs.len();
934 let cross = Self::batch_cross(encs)?;
935 let mut books: Vec<BboxBook> = (0..b).map(|_| BboxBook::new()).collect();
936 let mut active = vec![true; b];
937 let mut last = vec![START; b];
938 let mut cache = DecodeCache::default();
939 let empty = self.empty_cache(b)?;
940 crate::timing::timed("tf.decode_loop", || -> Result<(), String> {
941 // Each active table's `otsl` grows by one per step, so a shared
942 // step counter is the per-table `otsl.len() < MAX_STEPS` bound.
943 for _ in 0..MAX_STEPS {
944 if !active.iter().any(|a| *a) {
945 break;
946 }
947 let (raws, hidden) = crate::timing::timed("tf.decode_step", || {
948 self.step_kv(&last, &cross, &mut cache, &empty)
949 })?;
950 for t in 0..b {
951 if !active[t] {
952 continue;
953 }
954 let h = &hidden[t * EMBED_DIM..(t + 1) * EMBED_DIM];
955 if books[t].step(raws[t], h) {
956 last[t] = *books[t].tags.last().expect("step pushed a tag");
957 } else {
958 active[t] = false;
959 last[t] = END;
960 }
961 }
962 }
963 Ok(())
964 })?;
965 Ok(books)
966 }
967
968 /// The zero-`past` first-step cache(s) for `rows` tables, allocated through
969 /// the session allocator (ort's array constructors reject a 0-length dim;
970 /// the C API does allow it).
971 fn empty_cache(&self, rows: usize) -> Result<EmptyCache, String> {
972 let alloc = self.decoder.allocator();
973 if self.style != DecoderStyle::Legacy {
974 let mk = || {
975 Tensor::<f32>::new(alloc, [N_LAYERS, rows, KV_HEADS, 0usize, KV_HEAD_DIM])
976 .map_err(|e| format!("tableformer: empty kv cache: {e}"))
977 };
978 Ok((mk()?, Some(mk()?)))
979 } else {
980 let c = Tensor::<f32>::new(alloc, [N_LAYERS, 0usize, 1, EMBED_DIM])
981 .map_err(|e| format!("tableformer: empty cache: {e}"))?;
982 Ok((c, None))
983 }
984 }
985
986 /// Predict the OTSL structure-token sequence for a table-region image.
987 pub fn predict_otsl(&mut self, img: &RgbImage) -> Result<Vec<i64>, String> {
988 let enc = self.encode(img)?;
989 // Structure corrections live in tf_core::correct (shared with the wasm
990 // path); docling's line_num is never incremented, so xcel→lcel fires on
991 // every row.
992 let mut tags: Vec<i64> = vec![START];
993 let mut out: Vec<i64> = Vec::new();
994 let mut prev_ucel = false;
995 let mut cache = DecodeCache::default();
996 let empty = self.empty_cache(1)?;
997 while out.len() < MAX_STEPS {
998 let (raw, _hidden) = self.decode_step(&tags, &enc, &mut cache, &empty)?;
999 let tag = correct(raw, prev_ucel);
1000 if tag == END {
1001 break;
1002 }
1003 out.push(tag);
1004 tags.push(tag);
1005 prev_ucel = tag == UCEL;
1006 }
1007 Ok(out)
1008 }
1009
1010 /// Full structure prediction: OTSL grid cells with per-cell boxes (in the 448
1011 /// image, normalized cxcywh). Collects per-cell decoder hidden states using
1012 /// docling's exact bbox bookkeeping (skip-after-row-break, first-lcel of a
1013 /// horizontal span), runs the bbox decoder, merges span boxes, then lays the
1014 /// cells onto the OTSL grid with row/col spans.
1015 pub fn predict_table_structure(&mut self, img: &RgbImage) -> Result<Vec<TableCell>, String> {
1016 let enc = self.encode(img)?;
1017
1018 // The autoregressive loop's bbox bookkeeping lives in tf_core::BboxBook
1019 // (shared with the wasm path); this loop only steps the decoder.
1020 let mut book = BboxBook::new();
1021 let mut cache = DecodeCache::default();
1022 let empty = self.empty_cache(1)?;
1023 crate::timing::timed("tf.decode_loop", || -> Result<(), String> {
1024 while book.otsl.len() < MAX_STEPS {
1025 let (raw, hidden) = self.decode_step(&book.tags, &enc, &mut cache, &empty)?;
1026 if !book.step(raw, &hidden) {
1027 break;
1028 }
1029 }
1030 Ok(())
1031 })?;
1032 self.finish_table(book, &enc.eo)
1033 }
1034
1035 /// The bbox stage after a table's decode loop: run the bbox decoder over
1036 /// the collected per-cell hidden states, merge span boxes, lay the cells
1037 /// onto the OTSL grid.
1038 fn finish_table(
1039 &mut self,
1040 mut book: BboxBook,
1041 eo: &DynValue,
1042 ) -> Result<Vec<TableCell>, String> {
1043 if book.runaway() {
1044 docling_core::debug_log!(
1045 "docling-pdf: tableformer: no row break in {MAX_ROW_TAGS} tags; \
1046 geometric table fallback"
1047 );
1048 return Ok(Vec::new());
1049 }
1050 if book.n == 0 {
1051 return Ok(Vec::new());
1052 }
1053 let tag_h = Tensor::from_array(([book.n, EMBED_DIM], std::mem::take(&mut book.hiddens)))
1054 .map_err(|e| format!("tableformer: tag_h: {e}"))?;
1055 let bout = crate::timing::timed("tf.bbox", || {
1056 docling_onnx::run(
1057 &mut self.bbox,
1058 ort::inputs!["enc_out" => eo, "tag_h" => tag_h],
1059 )
1060 .map_err(|e| format!("tableformer: bbox: {e}"))
1061 })?;
1062 let (_, raw) = bout["boxes"]
1063 .try_extract_tensor::<f32>()
1064 .map_err(|e| format!("tableformer: boxes: {e}"))?;
1065 let boxes: Vec<[f32; 4]> = raw
1066 .chunks_exact(4)
1067 .map(|c| [c[0], c[1], c[2], c[3]])
1068 .collect();
1069 // Per-cell class logits [n, 3] → argmax (docling's `outputs_class`).
1070 let (_, craw) = bout["classes"]
1071 .try_extract_tensor::<f32>()
1072 .map_err(|e| format!("tableformer: classes: {e}"))?;
1073 let classes: Vec<i64> = craw.chunks_exact(3).map(|c| argmax(c) as i64).collect();
1074 let (merged, merged_classes) = merge_spans(&boxes, &classes, &book.merge);
1075 Ok(build_table_cells(&book.otsl, &merged, &merged_classes))
1076 }
1077
1078 /// Predict a table region's Markdown grid: crop the region (docling's
1079 /// page→1024px box-average then bbox crop), run the structure model, then
1080 /// match the page's word cells into the predicted cells with docling's
1081 /// matching post-processor ([`crate::tf_match`]) and expand spans into a
1082 /// dense `rows × cols` grid. `region` is `(l, t, r, b)` in page points
1083 /// (top-left). Returns `None` if no structure is predicted.
1084 pub fn predict_table_rows(
1085 &mut self,
1086 page_image: &RgbImage,
1087 region: [f32; 4],
1088 words: &[TextCell],
1089 ) -> Option<crate::tf_core::TableGrid> {
1090 let page1024 = Self::page_1024(page_image);
1091 self.predict_table_rows_on(page_image.height(), &page1024, region, words)
1092 }
1093
1094 /// The page rendered at 1024 px height (cv2.INTER_AREA), the frame every
1095 /// table crop of that page is cut from. Computed once per page by the
1096 /// pipeline and shared across its tables — the resample is a full-page
1097 /// f64 box filter, 110–170 ms on the corpus pages, and it used to run
1098 /// again for every table on the page.
1099 pub fn page_1024(page_image: &RgbImage) -> RgbImage {
1100 let sf = 1024.0 / page_image.height() as f32;
1101 let pw = (page_image.width() as f32 * sf) as u32;
1102 crate::timing::timed("tableformer.inter_area", || {
1103 crate::resample::inter_area(page_image, pw, 1024)
1104 })
1105 }
1106
1107 /// [`predict_table_rows`](Self::predict_table_rows) with the page's
1108 /// 1024-px frame already built ([`page_1024`](Self::page_1024));
1109 /// `page_h` is the source page image's pixel height.
1110 pub fn predict_table_rows_on(
1111 &mut self,
1112 page_h: u32,
1113 page1024: &RgbImage,
1114 region: [f32; 4],
1115 words: &[TextCell],
1116 ) -> Option<crate::tf_core::TableGrid> {
1117 let crop = Self::crop_region(page_h, page1024, region)?;
1118 let cells = crate::timing::timed("tableformer.structure", || {
1119 self.predict_table_structure(&crop)
1120 })
1121 .ok()?;
1122 if cells.is_empty() {
1123 return None;
1124 }
1125 // The ort-free tail (word matching + grid assembly) is shared with the
1126 // browser path in tf_core.
1127 crate::tf_core::table_rows(&cells, region, words)
1128 }
1129
1130 /// Every table of a page at once: [`predict_table_rows_on`](Self::predict_table_rows_on)
1131 /// per region, except that with the dynamic-batch decoder the tables'
1132 /// decode steps are shared — each table is encoded on its own, then one
1133 /// decode loop steps all of them together ([`Self::decode_batch`]), then
1134 /// each runs its own bbox head. Per table the result is bit-identical to
1135 /// the one-at-a-time path; a page with a single table takes exactly that
1136 /// path. Should the batched run fail (an ort error), the tables are
1137 /// retried one by one so a page never loses all of its tables to one
1138 /// shared step.
1139 pub fn predict_tables_on(
1140 &mut self,
1141 page_h: u32,
1142 page1024: &RgbImage,
1143 regions: &[[f32; 4]],
1144 words: &[TextCell],
1145 ) -> Vec<Option<crate::tf_core::TableGrid>> {
1146 let mut out: Vec<Option<crate::tf_core::TableGrid>> = vec![None; regions.len()];
1147 let crops: Vec<(usize, RgbImage)> = regions
1148 .iter()
1149 .enumerate()
1150 .filter_map(|(i, r)| Self::crop_region(page_h, page1024, *r).map(|c| (i, c)))
1151 .collect();
1152 if self.batched && crops.len() > 1 {
1153 let batched = crate::timing::timed("tableformer.structure", || {
1154 self.predict_structures_batched(crops.iter().map(|(_, c)| c))
1155 });
1156 match batched {
1157 Ok(cells) => {
1158 for ((i, _), cells) in crops.iter().zip(cells) {
1159 if !cells.is_empty() {
1160 out[*i] = crate::tf_core::table_rows(&cells, regions[*i], words);
1161 }
1162 }
1163 return out;
1164 }
1165 Err(e) => docling_core::debug_log!(
1166 "docling-pdf: tableformer batched decode failed ({e}); decoding tables one by one"
1167 ),
1168 }
1169 }
1170 for (i, crop) in &crops {
1171 let cells = crate::timing::timed("tableformer.structure", || {
1172 self.predict_table_structure(crop)
1173 });
1174 if let Ok(cells) = cells {
1175 if !cells.is_empty() {
1176 out[*i] = crate::tf_core::table_rows(&cells, regions[*i], words);
1177 }
1178 }
1179 }
1180 out
1181 }
1182
1183 /// [`predict_table_structure`](Self::predict_table_structure) for several
1184 /// crops with the decode steps shared across them.
1185 fn predict_structures_batched<'a>(
1186 &mut self,
1187 crops: impl Iterator<Item = &'a RgbImage>,
1188 ) -> Result<Vec<Vec<TableCell>>, String> {
1189 let mut encs = Vec::new();
1190 for crop in crops {
1191 encs.push(self.encode(crop)?);
1192 }
1193 let books = self.decode_batch(&encs)?;
1194 books
1195 .into_iter()
1196 .zip(&encs)
1197 .map(|(book, enc)| self.finish_table(book, &enc.eo))
1198 .collect()
1199 }
1200
1201 /// Crop the table bbox out of the 1024px frame. docling's coordinate
1202 /// chain, rounding included: the cluster bbox is rounded to integer page
1203 /// points *first* (`round(cluster.bbox.l) * scale`, banker's rounding),
1204 /// scaled by 2 (its table-structure page scale), then by `1024 / <2x
1205 /// page-image height>`, and the crop indices round again. Rounding after
1206 /// scaling instead shifts some crops by a pixel — enough to change
1207 /// TableFormer's cell boxes on tall tables (redp5110's TOC). `None` for a
1208 /// region that collapses to an empty crop.
1209 fn crop_region(page_h: u32, page1024: &RgbImage, region: [f32; 4]) -> Option<RgbImage> {
1210 let k = 2.0 * 1024.0 / page_h as f64;
1211 let px = |v: f32| (v as f64).round_ties_even() * k;
1212 let x = (px(region[0]).round_ties_even()).max(0.0) as u32;
1213 let y = (px(region[1]).round_ties_even()).max(0.0) as u32;
1214 let x2 = (px(region[2]).round_ties_even() as u32).min(page1024.width());
1215 let y2 = (px(region[3]).round_ties_even() as u32).min(page1024.height());
1216 if x2 <= x || y2 <= y {
1217 return None;
1218 }
1219 Some(image::imageops::crop_imm(page1024, x, y, x2 - x, y2 - y).to_image())
1220 }
1221}
1222
1223/// Note once per process that TableFormer's ONNX graphs weren't found, so tables
1224/// fall back to geometric reconstruction. The default paths are relative
1225/// (`.models/tableformer/*.onnx`), which only resolves when the process's current
1226/// directory happens to be the repo root — a very easy miss for anything else
1227/// (an embedding app, a binding invoked from a different working directory, …),
1228/// and previously failed with no signal at all.
1229fn warn_missing_once(enc: &str, dec: &str, bbx: &str) {
1230 static WARNED: std::sync::Once = std::sync::Once::new();
1231 WARNED.call_once(|| {
1232 eprintln!(
1233 "docling.rs: TableFormer models not found (checked {enc}, {dec}, {bbx}); \
1234 tables will use geometric reconstruction instead of ML table-structure \
1235 recognition. Set DOCLING_TABLEFORMER_ENCODER / DOCLING_TABLEFORMER_DECODER \
1236 / DOCLING_TABLEFORMER_BBOX to enable it (see README.md)."
1237 );
1238 });
1239}
1240
1241/// docling's preprocessing: bilinear (cv2.INTER_LINEAR) resize the crop to 448²,
1242/// normalize `(x/255 − mean)/std`, laid out as (C, W, H) — docling transposes
1243/// (2,1,0), so width is the major spatial axis. The page→1024px box-average
1244/// (cv2.INTER_AREA) is the caller's job.
1245fn preprocess(img: &RgbImage) -> Result<Tensor<f32>, String> {
1246 Tensor::from_array(([1usize, 3, SIDE, SIDE], preprocess_input(img)))
1247 .map_err(|e| format!("tableformer: input: {e}"))
1248}