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}
102
103/// The three decoder-graph generations the loop supports.
104#[derive(Clone, Copy, PartialEq, Eq)]
105enum DecoderStyle {
106 /// `decoder.onnx`: layer-output cache; feeds the full `tags` prefix and a
107 /// single `cache` every step.
108 Legacy,
109 /// The pre-#97 `decoder_kv.onnx`: one tag per step, `cache_k`/`cache_v`,
110 /// with the stacked `cross_k`/`cross_v` re-split inside every step.
111 KvStacked,
112 /// The #97 `decoder_kv.onnx`: one tag per step, and the constant cross
113 /// tensors arrive as 2×`N_LAYERS` per-layer inputs (`cross_kt_i` already
114 /// transposed for q·Kᵀ, `cross_v_i`), computed once per table by the
115 /// encoder — the step graph does no work proportional to their size.
116 KvHoisted,
117 /// `decoder_kv_layers.onnx` (#871, `scripts/install/kv_layers.py`): the
118 /// `KvHoisted` graph with the self-attention cache per layer — `Kᵀ` and
119 /// `V` in and out for each layer instead of two stacked tensors — whose
120 /// outputs land in buffers the loop owns ([`LayerCache`]).
121 KvLayered,
122}
123
124impl DecoderStyle {
125 /// One tag per step with the per-layer cross tensors the encoder emits.
126 fn hoisted(self) -> bool {
127 matches!(self, DecoderStyle::KvHoisted | DecoderStyle::KvLayered)
128 }
129}
130
131/// KV-cache geometry fixed by the `decoder_kv.onnx` export
132/// (`[N_LAYERS, 1, KV_HEADS, past, KV_HEAD_DIM]`, `KV_HEADS × KV_HEAD_DIM = EMBED_DIM`).
133const KV_HEADS: usize = 8;
134const KV_HEAD_DIM: usize = 64;
135
136/// The autoregressive decode state: `a` is the legacy layer-output cache, or
137/// `cache_k` for the KV graph; `b` is `cache_v` (KV graph only). `None` = first
138/// step (the zero-`past` empties are allocated per table by [`TableFormer::empty_cache`]).
139#[derive(Default)]
140struct DecodeCache {
141 a: Option<DynValue>,
142 b: Option<DynValue>,
143 /// The `KvLayered` caches, set up on the first step.
144 layers: Option<LayerCache>,
145}
146
147/// The `KvLayered` decoder's self-attention caches, in memory docling owns
148/// (#871). With ONNX Runtime allocating them, every step of a table made a
149/// cache one token longer than the last and freed the previous one —
150/// hundreds of ever-larger blocks per table, which on linux/arm64 with the
151/// CPU arena off fragmented glibc's heap until a 6 GiB container ran out of
152/// memory. Here each step reads its caches from one of two buffers and ONNX
153/// Runtime writes the one-token-longer caches straight into the other (the
154/// outputs are preallocated over it, [`docling_onnx::borrowed_f32`]); the
155/// next step swaps them. Each buffer holds the `2 × N_LAYERS` tensors in
156/// regions of `cap` floats — per layer `Kᵀ` `[rows, H, hd, past]`, then `V`
157/// `[rows, H, past, hd]` — sized for [`MAX_STEPS`] tokens: zeroed (`calloc`,
158/// so mapped lazily), only the prefix a table reaches becomes resident, and
159/// they are released when the table (or the page's batch) is done.
160struct LayerCache {
161 rows: usize,
162 past: usize,
163 cap: usize,
164 bufs: [Vec<f32>; 2],
165 /// Which of `bufs` holds the current caches.
166 cur: usize,
167 /// Zero-`past` first-step inputs (ort's slice constructors reject a
168 /// 0-length dim, the session allocator does not).
169 empty: Vec<Tensor<f32>>,
170}
171
172impl LayerCache {
173 fn new(alloc: &ort::memory::Allocator, rows: usize) -> Result<Self, String> {
174 let cap = rows * KV_HEADS * MAX_STEPS * KV_HEAD_DIM;
175 let mut empty = Vec::with_capacity(2 * N_LAYERS);
176 for j in 0..2 * N_LAYERS {
177 empty.push(
178 Tensor::<f32>::new(alloc, Self::shape(j, rows, 0))
179 .map_err(|e| format!("tableformer: empty kv cache: {e}"))?,
180 );
181 }
182 Ok(Self {
183 rows,
184 past: 0,
185 cap,
186 bufs: [vec![0.0; 2 * N_LAYERS * cap], vec![0.0; 2 * N_LAYERS * cap]],
187 cur: 0,
188 empty,
189 })
190 }
191
192 /// Tensor `j`'s shape at `past` tokens: `Kᵀ` for the first `N_LAYERS`.
193 fn shape(j: usize, rows: usize, past: usize) -> [usize; 4] {
194 if j < N_LAYERS {
195 [rows, KV_HEADS, KV_HEAD_DIM, past]
196 } else {
197 [rows, KV_HEADS, past, KV_HEAD_DIM]
198 }
199 }
200
201 fn input_name(j: usize) -> String {
202 if j < N_LAYERS {
203 format!("cache_kt_{j}")
204 } else {
205 format!("cache_v_{}", j - N_LAYERS)
206 }
207 }
208}
209
210/// Zero-`past` first-step cache tensors: `(cache, None)` for the legacy graph,
211/// `(cache_k, Some(cache_v))` for the KV graph.
212type EmptyCache = (Tensor<f32>, Option<Tensor<f32>>);
213
214/// Encoder outputs that drive the cached decode loop: the per-layer cross-attention
215/// K/V (projected from the image memory once, constant across decode steps) and
216/// `enc_out` for the bbox decoder. Kept as owned `ort` values so each decode step
217/// (and the bbox run) borrows them directly — no per-step extract/copy/re-wrap.
218struct EncodeOut {
219 /// Stacked `[N_LAYERS,1,H,S,hd]` cross K/V — the `Legacy`/`KvStacked`
220 /// decoders' inputs. `None` for `KvHoisted`, which reads the per-layer
221 /// tensors instead: the stacked pair is 2×9.6 MB per table, and a page's
222 /// tables are now all held encoded at once for the batched loop.
223 ck: Option<DynValue>,
224 cv: Option<DynValue>,
225 eo: DynValue,
226 /// `KvHoisted` only: per-layer `[cross_kt_0..N, cross_v_0..N]`, index-aligned
227 /// with the decoder's input names, borrowed by every decode step.
228 per_layer: Vec<(String, DynValue)>,
229}
230
231impl TableFormer {
232 /// Load the exported encoder/decoder/bbox ONNX graphs (env overrides, else
233 /// `.models/tableformer/{encoder,decoder,bbox}.onnx`). Returns `None` if any is
234 /// absent, so the pipeline falls back to geometric reconstruction.
235 pub fn load() -> Option<Self> {
236 Self::load_with(crate::intra_threads())
237 }
238
239 /// Like [`load`](Self::load) but with an explicit intra-op thread count, so a
240 /// parallel page-worker pool can run each table model on fewer threads (the
241 /// throughput comes from running pages concurrently, not from one fat model).
242 ///
243 /// See [`resolved_paths`] for the encoder/decoder/bbox file selection.
244 pub fn load_with(intra: usize) -> Option<Self> {
245 // (resolution shared with the model inventory — see resolved_paths)
246 let (enc, dec, bbx) = resolved_paths();
247 if crate::timing::enabled() {
248 eprintln!("docling-pdf: tableformer decoder: {dec}");
249 }
250 if [&enc, &dec, &bbx]
251 .iter()
252 .any(|p| !std::path::Path::new(p).exists())
253 {
254 // The geometric fallback is a supported, intentional configuration
255 // (docling has no ML table-structure equivalent baked in either), so
256 // this stays a single quiet stderr note rather than an error — but it
257 // fires every process (not per-worker) so a CWD-relative default that
258 // silently misses its files (a very easy mistake for anything not run
259 // from the repo root, e.g. an embedding app) is at least visible once.
260 warn_missing_once(&enc, &dec, &bbx);
261 return None;
262 }
263 // The decoder's KV-cache grows by one entry every autoregressive step, so
264 // its input shapes differ on every `run()` call. ONNX Runtime's memory
265 // pattern optimizer assumes stable shapes to plan buffer reuse; disabling
266 // it for this session avoids repeatedly re-validating/re-touching that
267 // plan (and the external-weights file) on each step. The bbox head has
268 // the same problem one level up: its `tag_h` input is `[ncells, 512]`
269 // and every table has a different cell count, so with the pattern
270 // planner on each run re-plans — and on this graph the plan is *worse*
271 // than none: 290 ms vs 54 ms for a 100-cell table, 560 vs 94 ms for
272 // 200 cells (ORT 1.22, 4 threads). It was 0.26 s per table on the
273 // corpus, more than the encoder.
274 //
275 // The decoder runs on ONE intra-op thread. A step is 49 small GEMMs
276 // over a single token — it streams the layer weights, it does not
277 // compute — so extra threads only add synchronisation: measured 4.1 ms
278 // per step on 1 thread vs 5.5 on 4 (7.1 vs 4.9 once the cache is 100+
279 // long). In the pool it also stops a table decode from taking all the
280 // cores away from the other workers' layout inference. And a
281 // single-thread session has a fixed reduction order, so table
282 // structure no longer varies run-to-run on near-tie tokens the way
283 // multi-threaded float sums let it (the conformance scripts pin one
284 // thread for exactly that reason; the default now matches them). The
285 // encoder keeps the shared budget: one 448×448 CNN + transformer pass
286 // per table, 680 ms single-threaded vs 165 on four.
287 let build = |path: &str, mem_pattern: bool, threads: usize| -> Result<Session, String> {
288 let builder = docling_onnx::session_builder()?
289 .with_intra_threads(threads)
290 .map_err(|e| e.to_string())?
291 .with_memory_pattern(mem_pattern)
292 .map_err(|e| e.to_string())?;
293 let variant = if mem_pattern {
294 "mem_pattern"
295 } else {
296 "no_mem_pattern"
297 };
298 docling_onnx::commit(docling_onnx::apply(builder)?, path, variant)
299 .map_err(|e| format!("tableformer load {path}: {e}"))
300 };
301 match (
302 build(&enc, true, intra),
303 build(&dec, false, 1),
304 build(&bbx, false, intra),
305 ) {
306 (Ok(encoder), Ok(decoder), Ok(bbox)) => {
307 let has = |n: &str| decoder.inputs().iter().any(|i| i.name() == n);
308 let style = if has("cache_kt_0") {
309 DecoderStyle::KvLayered
310 } else if has("cross_kt_0") {
311 DecoderStyle::KvHoisted
312 } else if has("cache_k") {
313 DecoderStyle::KvStacked
314 } else {
315 DecoderStyle::Legacy
316 };
317 if style.hoisted() && !encoder.outputs().iter().any(|o| o.name() == "cross_kt_0") {
318 eprintln!(
319 "docling-pdf: tableformer decoder needs per-layer cross tensors \
320 (cross_kt_*) the encoder doesn't emit — re-download or re-export \
321 the model set (scripts/install/export_tableformer.py); \
322 falling back to geometric tables"
323 );
324 return None;
325 }
326 // Dynamic batch axis on `tag` ⇒ the export batches decode
327 // steps across tables (ort reports a symbolic dim as -1).
328 let batched = style.hoisted()
329 && decoder.inputs().iter().any(|i| {
330 i.name() == "tag"
331 && matches!(i.dtype(), ort::value::ValueType::Tensor { shape, .. }
332 if shape.first().is_some_and(|d| *d < 0))
333 });
334 if crate::timing::enabled() && batched {
335 eprintln!("docling-pdf: tableformer decoder batches a page's tables per step");
336 }
337 Some(Self {
338 encoder,
339 decoder,
340 bbox,
341 style,
342 batched,
343 })
344 }
345 _ => None,
346 }
347 }
348
349 /// Run the image encoder and capture what the cached decoder loop needs: each
350 /// decoder layer's cross-attention K/V (projected from the image memory once,
351 /// shape `[N_LAYERS,1,H,S,head_dim]`) and `enc_out` for the bbox decoder.
352 fn encode(&mut self, img: &RgbImage) -> Result<EncodeOut, String> {
353 let input = crate::timing::timed("tf.preprocess", || preprocess(img))?;
354 let mut enc_out = crate::timing::timed("tf.encoder", || {
355 docling_onnx::run(&mut self.encoder, ort::inputs!["image" => input])
356 .map_err(|e| format!("tableformer: encode: {e}"))
357 })?;
358 let mut per_layer = Vec::new();
359 if self.style.hoisted() {
360 for prefix in ["cross_kt_", "cross_v_"] {
361 for i in 0.. {
362 let name = format!("{prefix}{i}");
363 match enc_out.remove(&name) {
364 Some(v) => per_layer.push((name, v)),
365 None => break,
366 }
367 }
368 }
369 if per_layer.is_empty() {
370 return Err("tableformer: encoder emitted no cross_kt_* outputs".into());
371 }
372 }
373 let mut grab = |name: &str| -> Result<DynValue, String> {
374 enc_out
375 .remove(name)
376 .ok_or_else(|| format!("tableformer: encoder output {name} missing"))
377 };
378 let hoisted = self.style.hoisted();
379 Ok(EncodeOut {
380 ck: if hoisted {
381 None
382 } else {
383 Some(grab("cross_k")?)
384 },
385 cv: if hoisted {
386 None
387 } else {
388 Some(grab("cross_v")?)
389 },
390 eo: grab("enc_out")?,
391 per_layer,
392 })
393 }
394
395 /// One doubly-cached decode step: feed the current `tags`, the constant cross
396 /// K/V, and the growing self-attention `cache`; return the raw argmax tag and
397 /// the last token's hidden state, advancing the cache. The cache stays an owned
398 /// `ort` value — the previous step's `out_cache` output is fed back directly,
399 /// never extracted or copied (it grows every step, so per-step copies were
400 /// O(steps²) float traffic). `empty_cache` is the zero-`past` value used on the
401 /// first step (ort's array constructors reject a 0-length dim, so it is
402 /// allocated through the session allocator by the caller).
403 fn decode_step(
404 &mut self,
405 tags: &[i64],
406 enc: &EncodeOut,
407 cache: &mut DecodeCache,
408 empty: &EmptyCache,
409 ) -> Result<(i64, Vec<f32>), String> {
410 crate::timing::timed("tf.decode_step", || {
411 self.decode_step_inner(tags, enc, cache, empty)
412 })
413 }
414
415 fn decode_step_inner(
416 &mut self,
417 tags: &[i64],
418 enc: &EncodeOut,
419 cache: &mut DecodeCache,
420 empty: &EmptyCache,
421 ) -> Result<(i64, Vec<f32>), String> {
422 if self.style.hoisted() {
423 // #97 graph: one tag; the constant per-layer cross tensors are
424 // borrowed views — the step pays nothing proportional to them.
425 let last = *tags.last().expect("decode starts from <start>");
426 let (raws, hidden) = self.step_kv(&[last], &enc.per_layer, cache, empty)?;
427 return Ok((raws[0], hidden));
428 }
429 let (ck, cv) = match (enc.ck.as_ref(), enc.cv.as_ref()) {
430 (Some(k), Some(v)) => (k, v),
431 _ => return Err("tableformer: stacked cross K/V missing".into()),
432 };
433 let mut dout = match self.style {
434 DecoderStyle::KvHoisted | DecoderStyle::KvLayered => unreachable!("handled above"),
435 DecoderStyle::KvStacked => {
436 // Pre-#97 KV graph: feed only the newly emitted tag; the projected
437 // K/V for the whole prefix live in cache_k/cache_v and are fed
438 // back as-is.
439 let last = *tags.last().expect("decode starts from <start>");
440 let tag_t = Tensor::from_array(([1usize, 1usize], vec![last]))
441 .map_err(|e| format!("tableformer: tag: {e}"))?;
442 match (cache.a.as_ref(), cache.b.as_ref()) {
443 (Some(k), Some(v)) => docling_onnx::run(
444 &mut self.decoder,
445 ort::inputs![
446 "tag" => tag_t, "cross_k" => ck, "cross_v" => cv,
447 "cache_k" => k, "cache_v" => v],
448 ),
449 _ => docling_onnx::run(
450 &mut self.decoder,
451 ort::inputs![
452 "tag" => tag_t, "cross_k" => ck, "cross_v" => cv,
453 "cache_k" => &empty.0,
454 "cache_v" => empty.1.as_ref().expect("kv empty cache has both halves")],
455 ),
456 }
457 }
458 DecoderStyle::Legacy => {
459 let tags_t = Tensor::from_array(([tags.len(), 1usize], tags.to_vec()))
460 .map_err(|e| format!("tableformer: tags: {e}"))?;
461 match cache.a.as_ref() {
462 None => docling_onnx::run(
463 &mut self.decoder,
464 ort::inputs![
465 "tags" => tags_t, "cross_k" => ck, "cross_v" => cv,
466 "cache" => &empty.0],
467 ),
468 Some(c) => docling_onnx::run(
469 &mut self.decoder,
470 ort::inputs![
471 "tags" => tags_t, "cross_k" => ck, "cross_v" => cv,
472 "cache" => c],
473 ),
474 }
475 }
476 }
477 .map_err(|e| format!("tableformer: decode: {e}"))?;
478 let (_, logits) = dout["logits"]
479 .try_extract_tensor::<f32>()
480 .map_err(|e| format!("tableformer: logits: {e}"))?;
481 let raw = argmax(logits) as i64;
482 let (_, hidden) = dout["hidden"]
483 .try_extract_tensor::<f32>()
484 .map_err(|e| format!("tableformer: hidden: {e}"))?;
485 let hidden = hidden.to_vec();
486 if self.style != DecoderStyle::Legacy {
487 cache.a = Some(
488 dout.remove("out_cache_k")
489 .ok_or_else(|| "tableformer: out_cache_k missing".to_string())?,
490 );
491 cache.b = Some(
492 dout.remove("out_cache_v")
493 .ok_or_else(|| "tableformer: out_cache_v missing".to_string())?,
494 );
495 } else {
496 cache.a = Some(
497 dout.remove("out_cache")
498 .ok_or_else(|| "tableformer: decoder output out_cache missing".to_string())?,
499 );
500 }
501 Ok((raw, hidden))
502 }
503
504 /// One step of a one-tag-per-step graph over `tags.len()` rows, through
505 /// whichever cache layout the loaded decoder has.
506 fn step_kv(
507 &mut self,
508 tags: &[i64],
509 per_layer: &[(String, DynValue)],
510 cache: &mut DecodeCache,
511 empty: &EmptyCache,
512 ) -> Result<(Vec<i64>, Vec<f32>), String> {
513 if self.style == DecoderStyle::KvLayered {
514 self.step_kv_layered(tags, per_layer, cache)
515 } else {
516 self.step_kv_hoisted(tags, per_layer, cache, empty)
517 }
518 }
519
520 /// One `KvLayered` step: [`Self::step_kv_hoisted`] with the caches read
521 /// from and written into the [`LayerCache`] buffers — ONNX Runtime
522 /// allocates nothing that grows with the table (#871).
523 fn step_kv_layered(
524 &mut self,
525 tags: &[i64],
526 per_layer: &[(String, DynValue)],
527 cache: &mut DecodeCache,
528 ) -> Result<(Vec<i64>, Vec<f32>), String> {
529 let rows = tags.len();
530 let lc = match &mut cache.layers {
531 Some(lc) => lc,
532 None => cache
533 .layers
534 .insert(LayerCache::new(self.decoder.allocator(), rows)?),
535 };
536 if lc.rows != rows || lc.past >= MAX_STEPS {
537 return Err(format!(
538 "tableformer: kv cache for {} rows at {} tokens, asked for {rows} rows one more",
539 lc.rows, lc.past
540 ));
541 }
542 let (past, cap) = (lc.past, lc.cap);
543 let tag_t = Tensor::from_array(([rows, 1usize], tags.to_vec()))
544 .map_err(|e| format!("tableformer: tag: {e}"))?;
545 let [first, second] = &mut lc.bufs;
546 let (src, dst) = if lc.cur == 0 {
547 (&*first, second)
548 } else {
549 (&*second, first)
550 };
551 let mut inputs: Vec<(
552 std::borrow::Cow<'_, str>,
553 ort::session::SessionInputValue<'_>,
554 )> = Vec::with_capacity(1 + 2 * N_LAYERS + per_layer.len());
555 inputs.push(("tag".into(), tag_t.into()));
556 let in_len = rows * KV_HEADS * past * KV_HEAD_DIM;
557 for j in 0..2 * N_LAYERS {
558 let name = LayerCache::input_name(j);
559 if past == 0 {
560 inputs.push((name.into(), (&lc.empty[j]).into()));
561 } else {
562 let view = TensorRef::from_array_view((
563 LayerCache::shape(j, rows, past),
564 &src[j * cap..j * cap + in_len],
565 ))
566 .map_err(|e| format!("tableformer: {name}: {e}"))?;
567 inputs.push((name.into(), view.into()));
568 }
569 }
570 for (name, v) in per_layer {
571 inputs.push((name.as_str().into(), v.into()));
572 }
573 let alloc = self.decoder.allocator();
574 let mut outputs = ort::session::OutputSelector::default();
575 for (j, region) in dst.chunks_exact_mut(cap).enumerate() {
576 // SAFETY: `dst` is borrowed mutably for this whole step and
577 // nothing else touches it; the values over it live in `opts` and
578 // `dout`, both dropped at the end of this function, before the
579 // next step reads the buffer (or the cache frees it).
580 let out = unsafe {
581 docling_onnx::borrowed_f32(region, &LayerCache::shape(j, rows, past + 1), alloc)
582 }
583 .map_err(|e| format!("tableformer: kv cache output: {e}"))?;
584 outputs = outputs.preallocate(format!("out_{}", LayerCache::input_name(j)), out);
585 }
586 let opts = docling_onnx::run_options_with_outputs(outputs)?;
587 let dout = self
588 .decoder
589 .run_with_options(inputs, &opts)
590 .map_err(|e| format!("tableformer: decode: {e}"))?;
591 let (_, logits) = dout["logits"]
592 .try_extract_tensor::<f32>()
593 .map_err(|e| format!("tableformer: logits: {e}"))?;
594 let vocab = logits.len() / rows;
595 let raws: Vec<i64> = logits
596 .chunks_exact(vocab)
597 .map(|row| argmax(row) as i64)
598 .collect();
599 let (_, hidden) = dout["hidden"]
600 .try_extract_tensor::<f32>()
601 .map_err(|e| format!("tableformer: hidden: {e}"))?;
602 let hidden = hidden.to_vec();
603 drop(dout);
604 drop(opts);
605 lc.past += 1;
606 lc.cur ^= 1;
607 Ok((raws, hidden))
608 }
609
610 /// One `KvHoisted` step over `tags.len()` rows — one table per row. `tags`
611 /// holds each row's last emitted tag, `per_layer` the cross tensors with a
612 /// matching leading batch axis (the encoder's own `[1,…]` outputs for a
613 /// single table, or [`Self::batch_cross`]'s concatenation), and the cache
614 /// grows `[N_LAYERS, rows, H, past, hd]` in lockstep. Returns each row's raw
615 /// argmax tag and the `[rows, EMBED_DIM]` hidden states, flattened.
616 fn step_kv_hoisted(
617 &mut self,
618 tags: &[i64],
619 per_layer: &[(String, DynValue)],
620 cache: &mut DecodeCache,
621 empty: &EmptyCache,
622 ) -> Result<(Vec<i64>, Vec<f32>), String> {
623 let rows = tags.len();
624 let tag_t = Tensor::from_array(([rows, 1usize], tags.to_vec()))
625 .map_err(|e| format!("tableformer: tag: {e}"))?;
626 let mut inputs: Vec<(
627 std::borrow::Cow<'_, str>,
628 ort::session::SessionInputValue<'_>,
629 )> = Vec::with_capacity(3 + per_layer.len());
630 inputs.push(("tag".into(), tag_t.into()));
631 match (cache.a.as_ref(), cache.b.as_ref()) {
632 (Some(k), Some(v)) => {
633 inputs.push(("cache_k".into(), k.into()));
634 inputs.push(("cache_v".into(), v.into()));
635 }
636 _ => {
637 inputs.push(("cache_k".into(), (&empty.0).into()));
638 inputs.push((
639 "cache_v".into(),
640 empty
641 .1
642 .as_ref()
643 .expect("kv empty cache has both halves")
644 .into(),
645 ));
646 }
647 }
648 for (name, v) in per_layer {
649 inputs.push((name.as_str().into(), v.into()));
650 }
651 let mut dout = docling_onnx::run(&mut self.decoder, inputs)
652 .map_err(|e| format!("tableformer: decode: {e}"))?;
653 let (_, logits) = dout["logits"]
654 .try_extract_tensor::<f32>()
655 .map_err(|e| format!("tableformer: logits: {e}"))?;
656 let vocab = logits.len() / rows;
657 let raws: Vec<i64> = logits
658 .chunks_exact(vocab)
659 .map(|row| argmax(row) as i64)
660 .collect();
661 let (_, hidden) = dout["hidden"]
662 .try_extract_tensor::<f32>()
663 .map_err(|e| format!("tableformer: hidden: {e}"))?;
664 let hidden = hidden.to_vec();
665 cache.a = Some(
666 dout.remove("out_cache_k")
667 .ok_or_else(|| "tableformer: out_cache_k missing".to_string())?,
668 );
669 cache.b = Some(
670 dout.remove("out_cache_v")
671 .ok_or_else(|| "tableformer: out_cache_v missing".to_string())?,
672 );
673 Ok((raws, hidden))
674 }
675
676 /// Stack the per-layer cross tensors of several encoded tables along the
677 /// batch axis (`[1,H,hd,S]` × B → `[B,H,hd,S]`, same for `cross_v`), index-
678 /// aligned with the decoder's input names. One copy per page — ~20 MB per
679 /// table, nothing next to the decode steps it lets the tables share.
680 fn batch_cross(encs: &[EncodeOut]) -> Result<Vec<(String, DynValue)>, String> {
681 let b = encs.len();
682 let mut out = Vec::with_capacity(encs[0].per_layer.len());
683 for j in 0..encs[0].per_layer.len() {
684 let name = encs[0].per_layer[j].0.clone();
685 let mut data: Vec<f32> = Vec::new();
686 let mut dims = [b, 0, 0, 0];
687 for enc in encs {
688 let (shape, v) = enc.per_layer[j]
689 .1
690 .try_extract_tensor::<f32>()
691 .map_err(|e| format!("tableformer: {name}: {e}"))?;
692 if shape.len() != 4 || shape[0] != 1 {
693 return Err(format!("tableformer: {name}: unexpected shape {shape:?}"));
694 }
695 dims[1..].copy_from_slice(&[
696 shape[1] as usize,
697 shape[2] as usize,
698 shape[3] as usize,
699 ]);
700 data.reserve(v.len() * b);
701 data.extend_from_slice(v);
702 }
703 let t = Tensor::from_array((dims, data))
704 .map_err(|e| format!("tableformer: {name}: {e}"))?;
705 out.push((name, t.into_dyn()));
706 }
707 Ok(out)
708 }
709
710 /// Decode `encs.len()` tables in lockstep: every step runs the decoder once
711 /// over all of them (a step is 49 weight-streaming GEMMs over one token per
712 /// row — B rows cost about what one does). The caches start empty for
713 /// every row and grow together, so nothing is ever padded or masked; a
714 /// table that emits `<end>` simply keeps its row (fed `END`, output
715 /// ignored) until the last one finishes. Row b of every op is exactly the
716 /// single-table computation, so each table's tokens and hidden states are
717 /// bit-identical to decoding it alone (asserted by the export script's
718 /// batching gate; the corpus snapshots pin it end-to-end).
719 fn decode_batch(&mut self, encs: &[EncodeOut]) -> Result<Vec<BboxBook>, String> {
720 let b = encs.len();
721 let cross = Self::batch_cross(encs)?;
722 let mut books: Vec<BboxBook> = (0..b).map(|_| BboxBook::new()).collect();
723 let mut active = vec![true; b];
724 let mut last = vec![START; b];
725 let mut cache = DecodeCache::default();
726 let empty = self.empty_cache(b)?;
727 crate::timing::timed("tf.decode_loop", || -> Result<(), String> {
728 // Each active table's `otsl` grows by one per step, so a shared
729 // step counter is the per-table `otsl.len() < MAX_STEPS` bound.
730 for _ in 0..MAX_STEPS {
731 if !active.iter().any(|a| *a) {
732 break;
733 }
734 let (raws, hidden) = crate::timing::timed("tf.decode_step", || {
735 self.step_kv(&last, &cross, &mut cache, &empty)
736 })?;
737 for t in 0..b {
738 if !active[t] {
739 continue;
740 }
741 let h = &hidden[t * EMBED_DIM..(t + 1) * EMBED_DIM];
742 if books[t].step(raws[t], h) {
743 last[t] = *books[t].tags.last().expect("step pushed a tag");
744 } else {
745 active[t] = false;
746 last[t] = END;
747 }
748 }
749 }
750 Ok(())
751 })?;
752 Ok(books)
753 }
754
755 /// The zero-`past` first-step cache(s) for `rows` tables, allocated through
756 /// the session allocator (ort's array constructors reject a 0-length dim;
757 /// the C API does allow it).
758 fn empty_cache(&self, rows: usize) -> Result<EmptyCache, String> {
759 let alloc = self.decoder.allocator();
760 if self.style != DecoderStyle::Legacy {
761 let mk = || {
762 Tensor::<f32>::new(alloc, [N_LAYERS, rows, KV_HEADS, 0usize, KV_HEAD_DIM])
763 .map_err(|e| format!("tableformer: empty kv cache: {e}"))
764 };
765 Ok((mk()?, Some(mk()?)))
766 } else {
767 let c = Tensor::<f32>::new(alloc, [N_LAYERS, 0usize, 1, EMBED_DIM])
768 .map_err(|e| format!("tableformer: empty cache: {e}"))?;
769 Ok((c, None))
770 }
771 }
772
773 /// Predict the OTSL structure-token sequence for a table-region image.
774 pub fn predict_otsl(&mut self, img: &RgbImage) -> Result<Vec<i64>, String> {
775 let enc = self.encode(img)?;
776 // Structure corrections live in tf_core::correct (shared with the wasm
777 // path); docling's line_num is never incremented, so xcel→lcel fires on
778 // every row.
779 let mut tags: Vec<i64> = vec![START];
780 let mut out: Vec<i64> = Vec::new();
781 let mut prev_ucel = false;
782 let mut cache = DecodeCache::default();
783 let empty = self.empty_cache(1)?;
784 while out.len() < MAX_STEPS {
785 let (raw, _hidden) = self.decode_step(&tags, &enc, &mut cache, &empty)?;
786 let tag = correct(raw, prev_ucel);
787 if tag == END {
788 break;
789 }
790 out.push(tag);
791 tags.push(tag);
792 prev_ucel = tag == UCEL;
793 }
794 Ok(out)
795 }
796
797 /// Full structure prediction: OTSL grid cells with per-cell boxes (in the 448
798 /// image, normalized cxcywh). Collects per-cell decoder hidden states using
799 /// docling's exact bbox bookkeeping (skip-after-row-break, first-lcel of a
800 /// horizontal span), runs the bbox decoder, merges span boxes, then lays the
801 /// cells onto the OTSL grid with row/col spans.
802 pub fn predict_table_structure(&mut self, img: &RgbImage) -> Result<Vec<TableCell>, String> {
803 let enc = self.encode(img)?;
804
805 // The autoregressive loop's bbox bookkeeping lives in tf_core::BboxBook
806 // (shared with the wasm path); this loop only steps the decoder.
807 let mut book = BboxBook::new();
808 let mut cache = DecodeCache::default();
809 let empty = self.empty_cache(1)?;
810 crate::timing::timed("tf.decode_loop", || -> Result<(), String> {
811 while book.otsl.len() < MAX_STEPS {
812 let (raw, hidden) = self.decode_step(&book.tags, &enc, &mut cache, &empty)?;
813 if !book.step(raw, &hidden) {
814 break;
815 }
816 }
817 Ok(())
818 })?;
819 self.finish_table(book, &enc.eo)
820 }
821
822 /// The bbox stage after a table's decode loop: run the bbox decoder over
823 /// the collected per-cell hidden states, merge span boxes, lay the cells
824 /// onto the OTSL grid.
825 fn finish_table(
826 &mut self,
827 mut book: BboxBook,
828 eo: &DynValue,
829 ) -> Result<Vec<TableCell>, String> {
830 if book.runaway() {
831 docling_core::debug_log!(
832 "docling-pdf: tableformer: no row break in {MAX_ROW_TAGS} tags; \
833 geometric table fallback"
834 );
835 return Ok(Vec::new());
836 }
837 if book.n == 0 {
838 return Ok(Vec::new());
839 }
840 let tag_h = Tensor::from_array(([book.n, EMBED_DIM], std::mem::take(&mut book.hiddens)))
841 .map_err(|e| format!("tableformer: tag_h: {e}"))?;
842 let bout = crate::timing::timed("tf.bbox", || {
843 docling_onnx::run(
844 &mut self.bbox,
845 ort::inputs!["enc_out" => eo, "tag_h" => tag_h],
846 )
847 .map_err(|e| format!("tableformer: bbox: {e}"))
848 })?;
849 let (_, raw) = bout["boxes"]
850 .try_extract_tensor::<f32>()
851 .map_err(|e| format!("tableformer: boxes: {e}"))?;
852 let boxes: Vec<[f32; 4]> = raw
853 .chunks_exact(4)
854 .map(|c| [c[0], c[1], c[2], c[3]])
855 .collect();
856 // Per-cell class logits [n, 3] → argmax (docling's `outputs_class`).
857 let (_, craw) = bout["classes"]
858 .try_extract_tensor::<f32>()
859 .map_err(|e| format!("tableformer: classes: {e}"))?;
860 let classes: Vec<i64> = craw.chunks_exact(3).map(|c| argmax(c) as i64).collect();
861 let (merged, merged_classes) = merge_spans(&boxes, &classes, &book.merge);
862 Ok(build_table_cells(&book.otsl, &merged, &merged_classes))
863 }
864
865 /// Predict a table region's Markdown grid: crop the region (docling's
866 /// page→1024px box-average then bbox crop), run the structure model, then
867 /// match the page's word cells into the predicted cells with docling's
868 /// matching post-processor ([`crate::tf_match`]) and expand spans into a
869 /// dense `rows × cols` grid. `region` is `(l, t, r, b)` in page points
870 /// (top-left). Returns `None` if no structure is predicted.
871 pub fn predict_table_rows(
872 &mut self,
873 page_image: &RgbImage,
874 region: [f32; 4],
875 words: &[TextCell],
876 ) -> Option<crate::tf_core::TableGrid> {
877 let page1024 = Self::page_1024(page_image);
878 self.predict_table_rows_on(page_image.height(), &page1024, region, words)
879 }
880
881 /// The page rendered at 1024 px height (cv2.INTER_AREA), the frame every
882 /// table crop of that page is cut from. Computed once per page by the
883 /// pipeline and shared across its tables — the resample is a full-page
884 /// f64 box filter, 110–170 ms on the corpus pages, and it used to run
885 /// again for every table on the page.
886 pub fn page_1024(page_image: &RgbImage) -> RgbImage {
887 let sf = 1024.0 / page_image.height() as f32;
888 let pw = (page_image.width() as f32 * sf) as u32;
889 crate::timing::timed("tableformer.inter_area", || {
890 crate::resample::inter_area(page_image, pw, 1024)
891 })
892 }
893
894 /// [`predict_table_rows`](Self::predict_table_rows) with the page's
895 /// 1024-px frame already built ([`page_1024`](Self::page_1024));
896 /// `page_h` is the source page image's pixel height.
897 pub fn predict_table_rows_on(
898 &mut self,
899 page_h: u32,
900 page1024: &RgbImage,
901 region: [f32; 4],
902 words: &[TextCell],
903 ) -> Option<crate::tf_core::TableGrid> {
904 let crop = Self::crop_region(page_h, page1024, region)?;
905 let cells = crate::timing::timed("tableformer.structure", || {
906 self.predict_table_structure(&crop)
907 })
908 .ok()?;
909 if cells.is_empty() {
910 return None;
911 }
912 // The ort-free tail (word matching + grid assembly) is shared with the
913 // browser path in tf_core.
914 crate::tf_core::table_rows(&cells, region, words)
915 }
916
917 /// Every table of a page at once: [`predict_table_rows_on`](Self::predict_table_rows_on)
918 /// per region, except that with the dynamic-batch decoder the tables'
919 /// decode steps are shared — each table is encoded on its own, then one
920 /// decode loop steps all of them together ([`Self::decode_batch`]), then
921 /// each runs its own bbox head. Per table the result is bit-identical to
922 /// the one-at-a-time path; a page with a single table takes exactly that
923 /// path. Should the batched run fail (an ort error), the tables are
924 /// retried one by one so a page never loses all of its tables to one
925 /// shared step.
926 pub fn predict_tables_on(
927 &mut self,
928 page_h: u32,
929 page1024: &RgbImage,
930 regions: &[[f32; 4]],
931 words: &[TextCell],
932 ) -> Vec<Option<crate::tf_core::TableGrid>> {
933 let mut out: Vec<Option<crate::tf_core::TableGrid>> = vec![None; regions.len()];
934 let crops: Vec<(usize, RgbImage)> = regions
935 .iter()
936 .enumerate()
937 .filter_map(|(i, r)| Self::crop_region(page_h, page1024, *r).map(|c| (i, c)))
938 .collect();
939 if self.batched && crops.len() > 1 {
940 let batched = crate::timing::timed("tableformer.structure", || {
941 self.predict_structures_batched(crops.iter().map(|(_, c)| c))
942 });
943 match batched {
944 Ok(cells) => {
945 for ((i, _), cells) in crops.iter().zip(cells) {
946 if !cells.is_empty() {
947 out[*i] = crate::tf_core::table_rows(&cells, regions[*i], words);
948 }
949 }
950 return out;
951 }
952 Err(e) => docling_core::debug_log!(
953 "docling-pdf: tableformer batched decode failed ({e}); decoding tables one by one"
954 ),
955 }
956 }
957 for (i, crop) in &crops {
958 let cells = crate::timing::timed("tableformer.structure", || {
959 self.predict_table_structure(crop)
960 });
961 if let Ok(cells) = cells {
962 if !cells.is_empty() {
963 out[*i] = crate::tf_core::table_rows(&cells, regions[*i], words);
964 }
965 }
966 }
967 out
968 }
969
970 /// [`predict_table_structure`](Self::predict_table_structure) for several
971 /// crops with the decode steps shared across them.
972 fn predict_structures_batched<'a>(
973 &mut self,
974 crops: impl Iterator<Item = &'a RgbImage>,
975 ) -> Result<Vec<Vec<TableCell>>, String> {
976 let mut encs = Vec::new();
977 for crop in crops {
978 encs.push(self.encode(crop)?);
979 }
980 let books = self.decode_batch(&encs)?;
981 books
982 .into_iter()
983 .zip(&encs)
984 .map(|(book, enc)| self.finish_table(book, &enc.eo))
985 .collect()
986 }
987
988 /// Crop the table bbox out of the 1024px frame. docling's coordinate
989 /// chain, rounding included: the cluster bbox is rounded to integer page
990 /// points *first* (`round(cluster.bbox.l) * scale`, banker's rounding),
991 /// scaled by 2 (its table-structure page scale), then by `1024 / <2x
992 /// page-image height>`, and the crop indices round again. Rounding after
993 /// scaling instead shifts some crops by a pixel — enough to change
994 /// TableFormer's cell boxes on tall tables (redp5110's TOC). `None` for a
995 /// region that collapses to an empty crop.
996 fn crop_region(page_h: u32, page1024: &RgbImage, region: [f32; 4]) -> Option<RgbImage> {
997 let k = 2.0 * 1024.0 / page_h as f64;
998 let px = |v: f32| (v as f64).round_ties_even() * k;
999 let x = (px(region[0]).round_ties_even()).max(0.0) as u32;
1000 let y = (px(region[1]).round_ties_even()).max(0.0) as u32;
1001 let x2 = (px(region[2]).round_ties_even() as u32).min(page1024.width());
1002 let y2 = (px(region[3]).round_ties_even() as u32).min(page1024.height());
1003 if x2 <= x || y2 <= y {
1004 return None;
1005 }
1006 Some(image::imageops::crop_imm(page1024, x, y, x2 - x, y2 - y).to_image())
1007 }
1008}
1009
1010/// Note once per process that TableFormer's ONNX graphs weren't found, so tables
1011/// fall back to geometric reconstruction. The default paths are relative
1012/// (`.models/tableformer/*.onnx`), which only resolves when the process's current
1013/// directory happens to be the repo root — a very easy miss for anything else
1014/// (an embedding app, a binding invoked from a different working directory, …),
1015/// and previously failed with no signal at all.
1016fn warn_missing_once(enc: &str, dec: &str, bbx: &str) {
1017 static WARNED: std::sync::Once = std::sync::Once::new();
1018 WARNED.call_once(|| {
1019 eprintln!(
1020 "docling.rs: TableFormer models not found (checked {enc}, {dec}, {bbx}); \
1021 tables will use geometric reconstruction instead of ML table-structure \
1022 recognition. Set DOCLING_TABLEFORMER_ENCODER / DOCLING_TABLEFORMER_DECODER \
1023 / DOCLING_TABLEFORMER_BBOX to enable it (see README.md)."
1024 );
1025 });
1026}
1027
1028/// docling's preprocessing: bilinear (cv2.INTER_LINEAR) resize the crop to 448²,
1029/// normalize `(x/255 − mean)/std`, laid out as (C, W, H) — docling transposes
1030/// (2,1,0), so width is the major spatial axis. The page→1024px box-average
1031/// (cv2.INTER_AREA) is the caller's job.
1032fn preprocess(img: &RgbImage) -> Result<Tensor<f32>, String> {
1033 Tensor::from_array(([1usize, 3, SIDE, SIDE], preprocess_input(img)))
1034 .map_err(|e| format!("tableformer: input: {e}"))
1035}