Skip to content

Commit 074b820

Browse files
committed
feat(pdf): use the INT8 models by default, FLEISCHWOLF_FP32 opts out
When layout_heron_int8.onnx / tableformer/decoder_int8.onnx sit next to the fp32 models at the default paths, the pipeline now loads them automatically — the quantized stack is conformance-validated (see PDF_PERFORMANCE.md) and ~2x faster on layout, so it is the right default for CPU deployments. Resolution order per model: explicit DOCLING_* env override > int8 default file (unless FLEISCHWOLF_FP32 is set) > fp32 default file. - download_dependencies.sh fetches the int8 assets by default (--no-int8 skips; --int8 kept as a no-op for compatibility). - pdf_setup.sh quantizes locally unless FLEISCHWOLF_FP32=1. - performance.sh mirrors the pipeline default (FLEISCHWOLF_FP32=1 benchmarks fp32); the short-lived FLEISCHWOLF_INT8 toggle is gone. - pdf_conformance.sh / pdf_groundtruth.sh already pin explicit fp32 paths, so snapshot baselines stay byte-deterministic. Verified: default resolves to int8 when present, FLEISCHWOLF_FP32=1 and explicit env paths both produce the fp32 reference output, and the fp32 fallback engages when the int8 files are absent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017fKFbatf55BK5j8vr3dZjj
1 parent de22e2c commit 074b820

8 files changed

Lines changed: 109 additions & 62 deletions

File tree

PDF_PERFORMANCE.md

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ default 2×2 on 4 cores) was re-validated: 2×2 beat both 4×1 and 1×4 on the
4343
`scripts/quantize_models.py` produces two quantized models. Point
4444
`DOCLING_LAYOUT_ONNX` / `DOCLING_TABLEFORMER_DECODER` at them to opt in.
4545

46+
**These are now the default:** when the `*_int8` files sit next to the fp32
47+
models at the default paths, the pipeline loads them automatically.
48+
`FLEISCHWOLF_FP32=1` forces full precision, and an explicit
49+
`DOCLING_LAYOUT_ONNX` / `DOCLING_TABLEFORMER_DECODER` always wins (the
50+
conformance/groundtruth scripts pin fp32 explicitly, so snapshots stay
51+
deterministic).
52+
4653
### Layout: static QDQ INT8, **Conv ops only** (~2.4× faster layout)
4754

4855
Calibrated on 42 real corpus pages preprocessed exactly like
@@ -103,10 +110,12 @@ item 2), which is why quantization helps so little there.
103110

104111
Ordered by expected impact ÷ risk. Items 1–3 attack the 85–95%.
105112

106-
1. **Ship/document the INT8 layout model as the default CPU configuration**
107-
(done here as an opt-in; consider making `download_dependencies.sh` fetch a
108-
pre-quantized `layout_heron_int8.onnx` so users don't need the Python
109-
tooling). Biggest single validated win: ~1.4–2× end-to-end.
113+
1. ~~**Ship/document the INT8 layout model as the default CPU
114+
configuration**~~ **Done on this branch:** the pipeline prefers the int8
115+
models when present (`FLEISCHWOLF_FP32=1` opts out),
116+
`download_dependencies.sh` fetches them by default, and
117+
`publish-models.yml` builds them. Biggest single validated win: ~1.4–2×
118+
end-to-end.
110119
2. **TableFormer decode-loop overhead** (~800 ms/table, ~60–500 steps):
111120
- ~~`decode_step` copies the whole KV cache out (`ocache.to_vec()`) and back
112121
in every step — O(steps²·6·512) float traffic.~~ **Done on this branch:**
@@ -180,18 +189,19 @@ cargo build --release
180189
# stage timing
181190
FLEISCHWOLF_TIMING=1 ./target/release/fleischwolf input.pdf > /dev/null
182191

183-
# quantize + opt in
192+
# build the int8 models (used automatically once present)
184193
uv venv .venv-quant && uv pip install --python .venv-quant/bin/python \
185194
onnx onnxruntime sympy pypdfium2 pillow numpy
186195
.venv-quant/bin/python scripts/quantize_models.py
187-
export DOCLING_LAYOUT_ONNX=$PWD/models/layout_heron_int8.onnx
188-
export DOCLING_TABLEFORMER_DECODER=$PWD/models/tableformer/decoder_int8.onnx
196+
197+
# force full precision for a run
198+
FLEISCHWOLF_FP32=1 ./target/release/fleischwolf input.pdf > /dev/null
189199
```
190200

191-
Integration points: `scripts/download_dependencies.sh --int8` fetches
192-
pre-quantized assets from the models release (published by
201+
Integration points: `scripts/download_dependencies.sh` fetches the
202+
pre-quantized assets by default (`--no-int8` skips; published by
193203
`.github/workflows/publish-models.yml`, which quantizes after export);
194-
`scripts/pdf_setup.sh` quantizes locally with `FLEISCHWOLF_INT8=1`;
195-
`scripts/performance.sh` benchmarks the int8 stack with `FLEISCHWOLF_INT8=1`;
196-
`examples/Dockerfile` bakes both precisions and defaults to int8
197-
(`--build-arg INT8=0` for fp32).
204+
`scripts/pdf_setup.sh` quantizes locally unless `FLEISCHWOLF_FP32=1`;
205+
`scripts/performance.sh` benchmarks whatever the pipeline default resolves to
206+
(int8 when present, `FLEISCHWOLF_FP32=1` for fp32); `examples/Dockerfile`
207+
bakes both precisions and defaults to int8 (`--build-arg INT8=0` for fp32).

README.md

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -290,25 +290,30 @@ come from Hugging Face (`$FLEISCHWOLF_ASR_MODELS_URL` overrides, or point
290290
only for now — other platforms, or building the models from source, need
291291
[`scripts/pdf_setup.sh`](#testing) instead.
292292

293-
### INT8 models (faster PDF conversion on CPU)
293+
### INT8 models (faster PDF conversion on CPU — the default)
294294

295-
The optional `*_int8` assets are post-training quantizations of the same
296-
models: Conv-only static INT8 of the layout detector (calibrated on this
297-
repo's PDF corpus) and dynamic INT8 of the TableFormer decoder. On CPUs with
298-
AVX-512 VNNI they make layout inference — the dominant PDF cost — **~2.4×
299-
faster** (~1.4–1.7× end-to-end) at conformance validated as unchanged against
300-
the corpus groundtruth; the TableFormer output is byte-identical. See
301-
[`PDF_PERFORMANCE.md`](./PDF_PERFORMANCE.md) for the measurements. They are
302-
opt-in — point the pipeline at them explicitly:
295+
The `*_int8` assets are post-training quantizations of the same models:
296+
Conv-only static INT8 of the layout detector (calibrated on this repo's PDF
297+
corpus) and dynamic INT8 of the TableFormer decoder. On CPUs with AVX-512
298+
VNNI they make layout inference — the dominant PDF cost — **~2.4× faster**
299+
(~1.4–1.8× end-to-end) at conformance validated as unchanged against the
300+
corpus groundtruth; the TableFormer output is byte-identical. See
301+
[`PDF_PERFORMANCE.md`](./PDF_PERFORMANCE.md) for the measurements.
302+
303+
**The pipeline uses them automatically** whenever they sit next to the fp32
304+
files at the default paths (`download_dependencies.sh` fetches them by
305+
default; `--no-int8` skips, or build them with `python
306+
scripts/quantize_models.py`). To force full precision:
303307

304308
```bash
305-
scripts/download_dependencies.sh --int8 # or: python scripts/quantize_models.py
306-
export DOCLING_LAYOUT_ONNX=$PWD/models/layout_heron_int8.onnx
307-
export DOCLING_TABLEFORMER_DECODER=$PWD/models/tableformer/decoder_int8.onnx
309+
FLEISCHWOLF_FP32=1 fleischwolf input.pdf # keep the int8 files, use fp32
310+
# or pin a model explicitly — an explicit path always wins:
311+
export DOCLING_LAYOUT_ONNX=$PWD/models/layout_heron.onnx
312+
export DOCLING_TABLEFORMER_DECODER=$PWD/models/tableformer/decoder.onnx
308313
```
309314

310-
(The [example Dockerfile](./examples/Dockerfile) bakes and defaults to the
311-
INT8 models; build with `--build-arg INT8=0` for pure fp32.)
315+
(The [example Dockerfile](./examples/Dockerfile) bakes both precisions and
316+
defaults to INT8; build with `--build-arg INT8=0` for pure fp32.)
312317

313318
Then either:
314319

crates/fleischwolf-pdf/src/layout.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ pub struct LayoutModel {
5252
}
5353

5454
impl LayoutModel {
55-
/// Load the ONNX model from `DOCLING_LAYOUT_ONNX` (or `models/layout_heron.onnx`).
55+
/// Load the ONNX model from `DOCLING_LAYOUT_ONNX`. Without the override,
56+
/// prefers `models/layout_heron_int8.onnx` when present (the quantized
57+
/// default; `FLEISCHWOLF_FP32=1` opts out), else `models/layout_heron.onnx`.
5658
pub fn load() -> Result<Self, String> {
5759
Self::load_with(crate::intra_threads())
5860
}
@@ -61,8 +63,11 @@ impl LayoutModel {
6163
/// parallel page-worker pool loads its helper models on a single thread each
6264
/// and gets its speed-up from running pages concurrently instead.
6365
pub fn load_with(intra: usize) -> Result<Self, String> {
64-
let path = std::env::var("DOCLING_LAYOUT_ONNX")
65-
.unwrap_or_else(|_| "models/layout_heron.onnx".to_string());
66+
let path = crate::model_path(
67+
"DOCLING_LAYOUT_ONNX",
68+
"models/layout_heron.onnx",
69+
"models/layout_heron_int8.onnx",
70+
);
6671
let session = Session::builder()
6772
.map_err(|e| format!("layout: builder: {e}"))?
6873
// Let inference use the available cores (ort otherwise defaults low);

crates/fleischwolf-pdf/src/lib.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,29 @@ pub(crate) fn intra_threads() -> usize {
7474
.unwrap_or(1)
7575
}
7676

77+
/// True when `FLEISCHWOLF_FP32` (any value but `0`) forces the full-precision
78+
/// models even where an INT8 variant sits next to the fp32 default.
79+
pub(crate) fn fp32_forced() -> bool {
80+
std::env::var("FLEISCHWOLF_FP32")
81+
.map(|v| v != "0")
82+
.unwrap_or(false)
83+
}
84+
85+
/// Resolve a model path: an explicit env override always wins; otherwise the
86+
/// INT8 variant of the default path when it exists on disk (the quantized
87+
/// models are conformance-validated — see PDF_PERFORMANCE.md — and load/run
88+
/// markedly faster on CPU), unless `FLEISCHWOLF_FP32` opts back into full
89+
/// precision; else the fp32 default.
90+
pub(crate) fn model_path(env: &str, fp32_default: &str, int8_default: &str) -> String {
91+
if let Ok(p) = std::env::var(env) {
92+
return p;
93+
}
94+
if !fp32_forced() && std::path::Path::new(int8_default).exists() {
95+
return int8_default.to_string();
96+
}
97+
fp32_default.to_string()
98+
}
99+
77100
/// One page's assembled output: typed nodes plus the page's hyperlinks, kept
78101
/// separate so pages processed out of order can be stitched back in page order.
79102
type PageOut = (Vec<Node>, Vec<(String, String)>);

crates/fleischwolf-pdf/src/tableformer.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,13 @@ impl TableFormer {
8080
pub fn load_with(intra: usize) -> Option<Self> {
8181
let enc = std::env::var("DOCLING_TABLEFORMER_ENCODER")
8282
.unwrap_or_else(|_| "models/tableformer/encoder.onnx".to_string());
83-
let dec = std::env::var("DOCLING_TABLEFORMER_DECODER")
84-
.unwrap_or_else(|_| "models/tableformer/decoder.onnx".to_string());
83+
// Prefer the INT8 decoder when present (byte-identical output, faster
84+
// decode; FLEISCHWOLF_FP32=1 opts out) unless explicitly overridden.
85+
let dec = crate::model_path(
86+
"DOCLING_TABLEFORMER_DECODER",
87+
"models/tableformer/decoder.onnx",
88+
"models/tableformer/decoder_int8.onnx",
89+
);
8590
let bbx = std::env::var("DOCLING_TABLEFORMER_BBOX")
8691
.unwrap_or_else(|_| "models/tableformer/bbox.onnx".to_string());
8792
if [&enc, &dec, &bbx]

scripts/download_dependencies.sh

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@
2929
# models/asr/{encoder_model,decoder_model}.onnx + vocab.json (Whisper tiny,
3030
# from Hugging Face; skip with --no-asr)
3131
#
32-
# With --int8, additionally fetches the INT8-quantized CPU models (see
32+
# Also fetches the INT8-quantized CPU models when the release hosts them (see
3333
# PDF_PERFORMANCE.md — ~2.4x faster layout inference at unchanged conformance):
3434
# models/layout_heron_int8.onnx
3535
# models/tableformer/decoder_int8.onnx
36-
# and prints the DOCLING_* exports that opt the pipeline into them. If the
37-
# release doesn't host the int8 assets (older tag), it falls back to telling
38-
# you how to produce them locally with scripts/quantize_models.py.
36+
# The pipeline picks these up automatically when they sit next to the fp32
37+
# files (no env vars needed); set FLEISCHWOLF_FP32=1 at runtime to force full
38+
# precision, or skip fetching them entirely with --no-int8. If the release
39+
# doesn't host the int8 assets (older tag), a note explains how to produce
40+
# them locally with scripts/quantize_models.py.
3941
#
4042
# pdfium is Linux x64 only for now, matching what's hosted in the release; for
4143
# other platforms (or to build the models from source) see scripts/pdf_setup.sh.
@@ -52,14 +54,15 @@ ASR_BASE_URL="${FLEISCHWOLF_ASR_MODELS_URL:-https://huggingface.co/onnx-communit
5254

5355
FORCE=false
5456
WITH_ASR=true
55-
WITH_INT8=false
57+
WITH_INT8=true
5658
for arg in "$@"; do
5759
case "$arg" in
5860
--force) FORCE=true ;;
5961
--no-asr) WITH_ASR=false ;;
60-
--int8) WITH_INT8=true ;;
62+
--int8) WITH_INT8=true ;; # accepted for compatibility; int8 is the default
63+
--no-int8) WITH_INT8=false ;;
6164
*)
62-
echo "usage: download_dependencies.sh [--force] [--no-asr] [--int8]" >&2
65+
echo "usage: download_dependencies.sh [--force] [--no-asr] [--no-int8]" >&2
6366
exit 2
6467
;;
6568
esac
@@ -120,20 +123,18 @@ if [ "$WITH_ASR" = true ]; then
120123
fi
121124

122125
if [ "$WITH_INT8" = true ]; then
123-
# INT8-quantized CPU models (optional release assets). The pipeline stays on
124-
# fp32 unless the DOCLING_* env vars below point at these files.
126+
# INT8-quantized CPU models (optional release assets). The pipeline prefers
127+
# them automatically when they sit at the default paths; FLEISCHWOLF_FP32=1
128+
# forces the fp32 models at runtime.
125129
fetch_optional "$BASE_URL/layout_heron_int8.onnx" models/layout_heron_int8.onnx
126130
fetch_optional "$BASE_URL/decoder_int8.onnx" models/tableformer/decoder_int8.onnx
127131
if [ -f models/layout_heron_int8.onnx ]; then
128-
echo "int8 models fetched — opt in with:"
129-
echo " export DOCLING_LAYOUT_ONNX=$(pwd)/models/layout_heron_int8.onnx"
130-
if [ -f models/tableformer/decoder_int8.onnx ]; then
131-
echo " export DOCLING_TABLEFORMER_DECODER=$(pwd)/models/tableformer/decoder_int8.onnx"
132-
fi
132+
echo "int8 models present — used by default (FLEISCHWOLF_FP32=1 forces full precision)"
133133
else
134-
echo "int8 assets not hosted at $BASE_URL — build them locally instead:"
134+
echo "int8 assets not hosted at $BASE_URL — the fp32 models will be used."
135+
echo "To build the int8 models locally (see PDF_PERFORMANCE.md):"
135136
echo " pip install onnx onnxruntime sympy pypdfium2 pillow numpy"
136-
echo " python scripts/quantize_models.py # see PDF_PERFORMANCE.md"
137+
echo " python scripts/quantize_models.py"
137138
fi
138139
fi
139140

scripts/pdf_setup.sh

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,12 @@ if [ ! -f models/tableformer/decoder.onnx ]; then
5050
fi
5151
fi
5252

53-
# Optional: INT8-quantize the layout model + TableFormer decoder for faster CPU
54-
# inference (validated conformance-neutral — see PDF_PERFORMANCE.md). Opt in
55-
# with FLEISCHWOLF_INT8=1; needs onnx onnxruntime sympy pypdfium2 pillow numpy.
56-
if [ "${FLEISCHWOLF_INT8:-0}" = "1" ] && [ ! -f models/layout_heron_int8.onnx ]; then
53+
# INT8-quantize the layout model + TableFormer decoder for faster CPU
54+
# inference (validated conformance-neutral — see PDF_PERFORMANCE.md). The
55+
# pipeline prefers the int8 files automatically once they exist; skip building
56+
# them with FLEISCHWOLF_FP32=1. Needs onnx onnxruntime sympy pypdfium2 pillow
57+
# numpy — a missing-deps failure is non-fatal (fp32 keeps working).
58+
if [ "${FLEISCHWOLF_FP32:-0}" != "1" ] && [ ! -f models/layout_heron_int8.onnx ]; then
5759
echo "→ INT8-quantizing layout + TableFormer decoder"
5860
if ! "${PYTHON:-python3}" scripts/quantize_models.py; then
5961
echo " ! quantization failed (missing deps?). The fp32 models still work;"
@@ -64,7 +66,7 @@ fi
6466
echo "done. export these before running the pipeline:"
6567
echo " export PDFIUM_DYNAMIC_LIB_PATH=$(pwd)/.pdfium/lib"
6668
if [ -f models/layout_heron_int8.onnx ]; then
67-
echo " export DOCLING_LAYOUT_ONNX=$(pwd)/models/layout_heron_int8.onnx # int8 (fp32: layout_heron.onnx)"
69+
echo " export DOCLING_LAYOUT_ONNX=$(pwd)/models/layout_heron_int8.onnx # int8 default (fp32: layout_heron.onnx, or FLEISCHWOLF_FP32=1)"
6870
else
6971
echo " export DOCLING_LAYOUT_ONNX=$(pwd)/models/layout_heron.onnx"
7072
fi
@@ -77,7 +79,7 @@ if [ -f models/tableformer/decoder.onnx ]; then
7779
# no matter where the binary/binding is actually invoked from.
7880
echo " export DOCLING_TABLEFORMER_ENCODER=$(pwd)/models/tableformer/encoder.onnx"
7981
if [ -f models/tableformer/decoder_int8.onnx ]; then
80-
echo " export DOCLING_TABLEFORMER_DECODER=$(pwd)/models/tableformer/decoder_int8.onnx # int8 (fp32: decoder.onnx)"
82+
echo " export DOCLING_TABLEFORMER_DECODER=$(pwd)/models/tableformer/decoder_int8.onnx # int8 default (fp32: decoder.onnx, or FLEISCHWOLF_FP32=1)"
8183
else
8284
echo " export DOCLING_TABLEFORMER_DECODER=$(pwd)/models/tableformer/decoder.onnx"
8385
fi

scripts/performance.sh

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,11 @@ RUST_BIN="$(build_rust_release)"
4242
# Point the Rust PDF pipeline at the fetched libs/models (scripts/pdf_setup.sh)
4343
# using absolute paths, so it runs the full pipeline no matter the caller's CWD.
4444
# Harmless for non-PDF inputs (the binary only reads these for PDFs/images).
45-
# FLEISCHWOLF_INT8=1 benchmarks the INT8-quantized layout model + TableFormer
46-
# decoder instead (scripts/quantize_models.py; see PDF_PERFORMANCE.md).
45+
# Mirrors the pipeline's own default: the INT8 layout model + TableFormer
46+
# decoder when present (scripts/quantize_models.py; see PDF_PERFORMANCE.md),
47+
# fp32 with FLEISCHWOLF_FP32=1.
4748
[[ -e "$WORKSPACE_DIR/.pdfium/lib/libpdfium.so" ]] && export PDFIUM_DYNAMIC_LIB_PATH="${PDFIUM_DYNAMIC_LIB_PATH:-$WORKSPACE_DIR/.pdfium/lib}"
48-
if [[ "${FLEISCHWOLF_INT8:-0}" == "1" ]]; then
49-
[[ -e "$WORKSPACE_DIR/models/layout_heron_int8.onnx" ]] || {
50-
echo "error: FLEISCHWOLF_INT8=1 but models/layout_heron_int8.onnx is missing —" >&2
51-
echo "run scripts/quantize_models.py (or download_dependencies.sh --int8) first" >&2
52-
exit 1
53-
}
49+
if [[ "${FLEISCHWOLF_FP32:-0}" != "1" && -e "$WORKSPACE_DIR/models/layout_heron_int8.onnx" ]]; then
5450
export DOCLING_LAYOUT_ONNX="${DOCLING_LAYOUT_ONNX:-$WORKSPACE_DIR/models/layout_heron_int8.onnx}"
5551
[[ -e "$WORKSPACE_DIR/models/tableformer/decoder_int8.onnx" ]] && export DOCLING_TABLEFORMER_DECODER="${DOCLING_TABLEFORMER_DECODER:-$WORKSPACE_DIR/models/tableformer/decoder_int8.onnx}"
5652
fi

0 commit comments

Comments
 (0)