Skip to content

Commit b831318

Browse files
committed
Clean up entrypoints and flags; budget batch VLM concurrency
Batch parallelization: - The parent now reads the inference server's capacity after spawn and budgets aggregate client concurrency across workers (~1.5x capacity) instead of every worker independently fanning out to full capacity - --disable_ocr batch runs skip the server entirely and size the pool by CPU cores - maxtasksperchild 50 -> 200 (workers only hold small CPU models now) - --num_chunks/--chunk_idx re-documented as multi-node file-list sharding (one marker invocation per node, each with its own server) Cruft removal: - Delete marker_chunk_convert + chunk_convert.{py,sh}: the per-GPU wrapper was broken by design (all chunks attach to the first chunk's server) and is superseded by VLLM_GPUS + multi-node sharding - Delete dead config fields: LineMergeProcessor.min_merge_ydist, LLMTableProcessor.table_image_expansion_ratio, LLMTableMergeProcessor.table_start_threshold, and the unreachable large-page reading-order branch (subsumed by use_pdftext_reading_order defaulting on) Fixes: - ConfigParser no longer drops explicit falsy CLI values (False/0); only None (unset) is skipped - marker_extract: add missing streamlit-ace dependency - Server API: add mode param (fast/balanced) to /marker endpoints Claude-Session: https://claude.ai/code/session_01SxGJdyccsAdKbjNAth35MA
1 parent a86bee6 commit b831318

12 files changed

Lines changed: 125 additions & 182 deletions

File tree

README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ First, some configuration:
105105
- Marker runs layout, OCR, and table recognition through a single surya VLM, served by a local inference server (used for OCR in both modes, and for layout in balanced mode). The server is spawned automatically on first use - vLLM (docker) on NVIDIA GPUs, llama.cpp elsewhere. You can also point marker at an already-running server with `SURYA_INFERENCE_URL=http://host:port/v1`.
106106
- Useful server settings (all surya env vars): `SURYA_INFERENCE_BACKEND` (`vllm` or `llamacpp`), `SURYA_INFERENCE_PARALLEL` (concurrent requests — by default this auto-scales to the server's capacity: the GPU's `max_num_seqs` under vllm, a conservative slot count under llama.cpp; set an int only to override), `SURYA_INFERENCE_KEEP_ALIVE` (keep the server running between invocations), `VLLM_GPUS` (GPU indices for the server).
107107
- Some PDFs, even digital ones, have bad text in them. Set `--force_ocr` to force OCR on all pages, or the `strip_existing_ocr` to keep all digital text, and strip out any existing OCR text.
108-
- If you care about inline math, set `force_ocr` to convert inline math to LaTeX.
108+
- Inline math is converted to LaTeX automatically in balanced mode (`ocr_inline_math`); in fast mode, set `--force_ocr` or `--ocr_inline_math` to get the same.
109109

110110
## Interactive App
111111

@@ -130,7 +130,7 @@ Options:
130130
- `--output_dir PATH`: Directory where output files will be saved. Defaults to the value specified in settings.OUTPUT_DIR.
131131
- `--paginate_output`: Paginates the output, using `\n\n{PAGE_NUMBER}` followed by `-` * 48, then `\n\n`
132132
- `--use_llm`: Uses an LLM to improve accuracy. You will need to configure the LLM backend - see [below](#llm-services).
133-
- `--force_ocr`: Force OCR processing on the entire document, even for pages that might contain extractable text. This will also format inline math properly.
133+
- `--force_ocr`: Force OCR processing on the entire document, even for pages that might contain extractable text.
134134
- `--block_correction_prompt`: if LLM mode is active, an optional prompt that will be used to correct the output of marker. This is useful for custom formatting or logic that you want to apply to the output.
135135
- `--strip_existing_ocr`: Remove all existing OCR text in the document and re-OCR with surya.
136136
- `--redo_inline_math`: If you want the absolute highest quality inline math conversion, use this along with `--use_llm`.
@@ -152,11 +152,16 @@ marker /path/to/input/folder
152152
```
153153

154154
- `marker` supports all the same options from `marker_single` above.
155-
- `--workers` is the number of conversion workers to run simultaneously. This is automatically set by default, but you can increase it to increase throughput, at the cost of more CPU usage. All workers share a single inference server, which the parent process spawns - GPU parallelism is handled server-side (`SURYA_INFERENCE_PARALLEL`).
155+
- `--workers` is the number of conversion workers to run simultaneously. This is automatically set by default, but you can increase it to increase throughput, at the cost of more CPU usage. All workers share a single inference server, which the parent process spawns.
156+
- The parent budgets total VLM concurrency automatically: it reads the server's capacity and splits it across workers (aggregate in-flight ≈ 1.5× capacity), so adding workers never over-queues the server. Set `SURYA_INFERENCE_PARALLEL` yourself only to override.
157+
- With `--disable_ocr` no inference server is started at all, and the pool is sized purely by CPU cores.
156158

157-
## Convert multiple files on multiple GPUs
159+
### Batch sizing cheat sheet (e.g. 1000 docs)
158160

159-
For multi-GPU machines, run a single `marker` command and let the inference server span GPUs by setting `VLLM_GPUS` (e.g. `VLLM_GPUS=0,1,2,3`). The old `marker_chunk_convert` script assumed one model instance per GPU - with the shared inference server, chunks on the same machine will attach to the first chunk's server. To run truly separate servers, start one per GPU manually and pass a distinct `SURYA_INFERENCE_URL` per chunk.
161+
- **One GPU machine**: `marker /folder --output_dir out` — defaults handle it: one vllm server, a CPU-sized worker pool, concurrency budgeted to the GPU. Add `--mode fast` if you want cheaper/faster conversion for mostly-digital corpora.
162+
- **Multi-GPU machine**: same single command, with the server spanning GPUs: `VLLM_GPUS=0,1,2,3 marker /folder ...`
163+
- **Multiple machines**: shard the file list — run one `marker` per node with `--num_chunks <nodes> --chunk_idx <this node>`. Each node spawns its own server.
164+
- **CPU-only / no VLM**: `marker /folder --disable_ocr` (pure text-layer extraction; equations and scanned pages are skipped).
160165

161166
## Use from python
162167

marker/builders/line.py

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -76,24 +76,6 @@ class LineBuilder(BaseBuilder):
7676
"multi-column pages (olmocr-bench order tests: 75% vs 56%). The learned",
7777
"head is still used on OCR pages, which have no pdftext positions.",
7878
] = True
79-
large_page_pdftext_order: Annotated[
80-
bool,
81-
"On very large clean-digital (pdftext) pages - newspapers, broadsheets -",
82-
"use the PDF's character reading order instead of the learned order head.",
83-
"The order model is trained at a fixed small resolution with a box-count",
84-
"cap, so it degrades on big multi-column pages where pdftext's column-aware",
85-
"char stream stays reliable.",
86-
] = True
87-
large_page_min_long_side: Annotated[
88-
int,
89-
"Long side (px at lowres dpi) above which a pdftext page counts as large",
90-
"for large_page_pdftext_order.",
91-
] = 2000
92-
large_page_min_short_side: Annotated[
93-
int,
94-
"Short side (px at lowres dpi) above which a pdftext page counts as large",
95-
"for large_page_pdftext_order.",
96-
] = 1000
9779
mode: Annotated[
9880
str,
9981
"Conversion mode. 'balanced' promotes any page with flagged blocks to",
@@ -155,13 +137,6 @@ def __call__(self, document: Document, provider: PdfProvider):
155137
if not self.disable_ocr:
156138
self.flag_bad_blocks(document)
157139

158-
def _is_large_page(self, page: PageGroup) -> bool:
159-
w, h = page.get_image(highres=False).size
160-
return (
161-
max(w, h) >= self.large_page_min_long_side
162-
and min(w, h) >= self.large_page_min_short_side
163-
)
164-
165140
def order_blocks_by_reading_order(self, document: Document):
166141
"""Order layout blocks on pdftext pages by the PDF's character reading
167142
order, instead of the layout model's position.
@@ -175,20 +150,15 @@ def order_blocks_by_reading_order(self, document: Document):
175150
blocks by ``span.minimum_position`` (pdftext char start index =
176151
column-aware reading order) instead.
177152
178-
Applies to a pdftext page when ``use_pdftext_reading_order`` forces it
179-
for all pages, or ``large_page_pdftext_order`` and the page is large.
180153
Text-less blocks (figures, empty boxes) keep their placement relative to
181154
the text block they followed. OCR'd ("surya") pages are left untouched
182155
(no pdftext positions - they rely on the learned order head).
183156
"""
157+
if not self.use_pdftext_reading_order:
158+
return
184159
for page in document.pages:
185160
if page.text_extraction_method != "pdftext" or not page.structure:
186161
continue
187-
apply = self.use_pdftext_reading_order or (
188-
self.large_page_pdftext_order and self._is_large_page(page)
189-
)
190-
if not apply:
191-
continue
192162

193163
order = []
194164
last_pos = -1 # text-less blocks before any text sort to the top

marker/config/parser.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,24 +95,32 @@ def generate_config_dict(self) -> Dict[str, any]:
9595
config = {}
9696
output_dir = self.cli_options.get("output_dir", settings.OUTPUT_DIR)
9797
for k, v in self.cli_options.items():
98-
if not v:
98+
# None means "not provided". Explicit falsy values (False, 0) are
99+
# real settings and must flow through - dropping them made it
100+
# impossible to turn a default-True option off from the CLI.
101+
if v is None:
99102
continue
100103

101104
match k:
102105
case "debug":
103-
config["debug_pdf_images"] = True
104-
config["debug_layout_images"] = True
105-
config["debug_json"] = True
106-
config["debug_data_folder"] = output_dir
106+
if v:
107+
config["debug_pdf_images"] = True
108+
config["debug_layout_images"] = True
109+
config["debug_json"] = True
110+
config["debug_data_folder"] = output_dir
107111
case "page_range":
108-
config["page_range"] = parse_range_str(v)
112+
if v:
113+
config["page_range"] = parse_range_str(v)
109114
case "config_json":
110-
with open(v, "r", encoding="utf-8") as f:
111-
config.update(json.load(f))
115+
if v:
116+
with open(v, "r", encoding="utf-8") as f:
117+
config.update(json.load(f))
112118
case "disable_multiprocessing":
113-
config["pdftext_workers"] = 1
119+
if v:
120+
config["pdftext_workers"] = 1
114121
case "disable_image_extraction":
115-
config["extract_images"] = False
122+
if v:
123+
config["extract_images"] = False
116124
case _:
117125
config[k] = v
118126

marker/processors/line_merge.py

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,38 +12,39 @@ class LineMergeProcessor(BaseProcessor):
1212
"""
1313
A processor for merging inline math lines.
1414
"""
15-
block_types = (BlockTypes.Text, BlockTypes.TextInlineMath, BlockTypes.Caption, BlockTypes.Footnote, BlockTypes.SectionHeader)
15+
16+
block_types = (
17+
BlockTypes.Text,
18+
BlockTypes.TextInlineMath,
19+
BlockTypes.Caption,
20+
BlockTypes.Footnote,
21+
BlockTypes.SectionHeader,
22+
)
1623
min_merge_pct: Annotated[
17-
float,
18-
"The minimum percentage of intersection area to consider merging."
19-
] = .015
24+
float, "The minimum percentage of intersection area to consider merging."
25+
] = 0.015
2026
block_expand_threshold: Annotated[
21-
float,
22-
"The percentage of the block width to expand the bounding box."
23-
] = .05
24-
min_merge_ydist: Annotated[
25-
float,
26-
"The minimum y distance between lines to consider merging."
27-
] = 5
27+
float, "The percentage of the block width to expand the bounding box."
28+
] = 0.05
2829
intersection_pct_threshold: Annotated[
2930
float,
30-
"The total amount of intersection area concentrated in the max intersection block."
31-
] = .5
31+
"The total amount of intersection area concentrated in the max intersection block.",
32+
] = 0.5
3233
vertical_overlap_pct_threshold: Annotated[
33-
float,
34-
"The minimum percentage of vertical overlap to consider merging."
35-
] = .8
36-
use_llm: Annotated[
37-
bool,
38-
"Whether to use LLMs to improve accuracy."
39-
] = False
34+
float, "The minimum percentage of vertical overlap to consider merging."
35+
] = 0.8
36+
use_llm: Annotated[bool, "Whether to use LLMs to improve accuracy."] = False
4037

4138
def __init__(self, config):
4239
super().__init__(config)
4340

4441
def merge_lines(self, lines: List[Line], block: Block):
45-
lines = [l for l in lines if l.polygon.width * 5 > l.polygon.height] # Skip vertical lines
46-
line_bboxes = [l.polygon.expand(self.block_expand_threshold, 0).bbox for l in lines] # Expand horizontally
42+
lines = [
43+
ln for ln in lines if ln.polygon.width * 5 > ln.polygon.height
44+
] # Skip vertical lines
45+
line_bboxes = [
46+
ln.polygon.expand(self.block_expand_threshold, 0).bbox for ln in lines
47+
] # Expand horizontally
4748
intersections = matrix_intersection_area(line_bboxes, line_bboxes)
4849

4950
merges = []
@@ -53,7 +54,9 @@ def merge_lines(self, lines: List[Line], block: Block):
5354
intersection_row[i] = 0 # Zero out the current idx
5455

5556
if i < len(line_bboxes) - 1:
56-
intersection_row[i+1] = 0 # Zero out the next idx, so we only evaluate merge from the left
57+
intersection_row[i + 1] = (
58+
0 # Zero out the next idx, so we only evaluate merge from the left
59+
)
5760

5861
if len(merge) == 0:
5962
merge.append(i)
@@ -74,14 +77,17 @@ def merge_lines(self, lines: List[Line], block: Block):
7477
vertical_overlap = max(0, vertical_overlap_end - vertical_overlap_start)
7578
vertical_overlap_pct = vertical_overlap / max(1, lines[i].polygon.height)
7679

77-
if all([
78-
# Overlaps enough
79-
intersection_pct >= self.min_merge_pct,
80-
# Within same line
81-
vertical_overlap_pct > self.vertical_overlap_pct_threshold,
82-
# doesn't overlap with anything else
83-
merge_intersection / total_intersection > self.intersection_pct_threshold
84-
]):
80+
if all(
81+
[
82+
# Overlaps enough
83+
intersection_pct >= self.min_merge_pct,
84+
# Within same line
85+
vertical_overlap_pct > self.vertical_overlap_pct_threshold,
86+
# doesn't overlap with anything else
87+
merge_intersection / total_intersection
88+
> self.intersection_pct_threshold,
89+
]
90+
):
8591
merge.append(i)
8692
else:
8793
merges.append(merge)
@@ -112,7 +118,6 @@ def merge_lines(self, lines: List[Line], block: Block):
112118
elif "math" not in line.formats:
113119
line.formats.append("math")
114120

115-
116121
def __call__(self, document: Document):
117122
# Merging lines only needed for inline math
118123
if not self.use_llm:

marker/processors/llm/llm_table.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,6 @@ class LLMTableProcessor(BaseLLMComplexBlockProcessor):
2828
int,
2929
"The maximum number of rows in a table to process with the LLM processor. Beyond this will be skipped.",
3030
] = 175
31-
table_image_expansion_ratio: Annotated[
32-
float,
33-
"The ratio to expand the image by when cropping.",
34-
] = 0
3531
rotation_max_wh_ratio: Annotated[
3632
float,
3733
"The maximum width/height ratio for table cells for a table to be considered rotated.",

marker/processors/llm/llm_table_merge.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ class LLMTableMergeProcessor(BaseLLMComplexBlockProcessor):
2525
float,
2626
"The minimum height ratio relative to the page for the first table in a pair to be considered for merging.",
2727
] = 0.6
28-
table_start_threshold: Annotated[
29-
float,
30-
"The maximum percentage down the page the second table can start to be considered for merging.",
31-
] = 0.2
3228
vertical_table_height_threshold: Annotated[
3329
float, "The height tolerance for 2 adjacent tables to be merged into one."
3430
] = 0.25

marker/scripts/chunk_convert.py

Lines changed: 0 additions & 21 deletions
This file was deleted.

marker/scripts/chunk_convert.sh

Lines changed: 0 additions & 54 deletions
This file was deleted.

0 commit comments

Comments
 (0)