docs/cookbook/autoregressive/Baidu/PaddleOCR-VL.mdx
For all methods and hardware platforms, see the official SGLang installation guide. The two paths below match the Python / Docker toggle in the command panel.
<Tabs> <Tab title="Python (pip / uv)">pip install --upgrade pip
pip install uv
uv pip install --prerelease=allow sglang
Then run the Python output of the command panel below in that environment.
</Tab> <Tab title="Docker">docker pull lmsysorg/sglang:dev
For how to launch the image, see Install → Method 3: Using Docker. Substitute the inner sglang serve ... with what the command generator below produces.
Pick a release and your hardware to generate the launch command. The model is 0.9B and single-GPU, so there is one serving recipe per platform; the axis that actually moves cost is Page Resolution, which caps how many image tokens one page is worth.
import { Deployment } from "/src/snippets/_deployment.jsx"; import { config } from "/src/snippets/configs/PaddlePaddle/paddleocr-vl.jsx";
<Deployment config={config} />Use the Playground to layer tensor parallelism on top of the selected deployment cell. At this size TP is a latency knob, not a capacity one — the weights fit on one GPU.
import { Playground } from "/src/snippets/_playground.jsx";
<Playground config={config} />PaddleOCR-VL is Baidu's compact document-parsing vision-language model: a NaViT-style dynamic-resolution SigLIP vision encoder feeding an ERNIE-4.5-0.3B language backbone, 0.9B parameters in total, released under Apache 2.0. It recognizes text, tables, formulas, charts and seals across 109 languages, and is small enough that a single GPU serves it comfortably.
SGLang serves this model, which is the recognition stage of PaddleOCR's document-parsing pipeline — not the pipeline itself. Layout and region detection, cropping, reading order and the assembly of markdown or JSON stay in PaddleOCR, which calls a model endpoint once per region. That split is why the model is prompt-conditioned rather than instruction-following: the caller decides whether a crop is a table, a formula or a chart (§3.1), and SGLang is the backend that answers.
All three releases share an identical config.json (same PaddleOCRVLForConditionalGeneration architecture, same tower and backbone dimensions), so one SGLang recipe serves every variant and only the model path changes.
Recommended generation: greedy decoding (temperature=0) with a per-page max_tokens budget — the model card uses 512 for a single region and the reference server allows more for a full page. These are informational; do not hardcode them in library code.
Resources: Hugging Face · PaddleOCR on GitHub
max_pixels is expressed in 28x28 units (patch size 14 with a 2x2 merge), so max_pixels / 784 is the image-token budget per page. The checkpoint's own default is 1280 tokens; the Page Resolution selector in the Deploy panel emits the corresponding --mm-process-config value. Lower it for clean born-digital PDFs, raise it for dense scans and small print.--trust-remote-code off. The checkpoints ship their own configuration_paddleocr_vl.py / processing_paddleocr_vl.py, but transformers 5.12 supports paddleocr_vl natively — and the bundled remote image processor is the slower of the two implementations (measured 87.4 ms vs 39.1 ms per 1080p page). Passing the flag pins SGLang to the remote copy. Serving without it produced byte-identical OCR output on every page we checked and about 5% more requests per second at 32-way concurrency.--mm-processor-worker-num overrides the count; raising it past the default did not help in our measurements.--disable-radix-cache only if every request carries a different page.--enable-mixed-chunk, --num-continuous-decode-steps 2) measured +11% requests per second at 32-way concurrency and cut queued TTFT by 23%, with single-stream latency unchanged. Measured on an H200; on a smaller card lower --chunked-prefill-size until it fits.--context-length 16384 so the KV pool stays small and concurrency stays high; raise it only if you batch many pages into one request.One 1080p page (~2700 image tokens) in, 128 tokens out, prefix cache disabled, median TTFT:
<table style={{width: "100%", borderCollapse: "collapse", tableLayout: "fixed"}}> <thead> <tr style={{borderBottom: "2px solid #d55816"}}> <th style={{textAlign: "left", padding: "10px 12px", fontWeight: 700}}>Configuration</th> <th style={{textAlign: "right", padding: "10px 12px", fontWeight: 700}}>TTFT, 1 stream</th> <th style={{textAlign: "right", padding: "10px 12px", fontWeight: 700}}>req/s at 32 concurrent</th> </tr> </thead> <tbody> <tr> <td style={{padding: "9px 12px"}}>With <code>--trust-remote-code</code> (remote image processor)</td> <td style={{padding: "9px 12px", textAlign: "right"}}>219 ms</td> <td style={{padding: "9px 12px", textAlign: "right"}}>10.9</td> </tr> <tr style={{backgroundColor: "rgba(255,255,255,0.02)"}}> <td style={{padding: "9px 12px"}}>Recipe above (native image processor)</td> <td style={{padding: "9px 12px", textAlign: "right"}}><strong>114 ms</strong></td> <td style={{padding: "9px 12px", textAlign: "right"}}><strong>11.3</strong></td> </tr> </tbody> </table>Throughput at saturation is bound by the vision tower, which runs full attention over every patch of the page — so the Page Resolution selector is the lever that moves it, not tensor parallelism.
PaddleOCR-VL exposes its capabilities through a small set of fixed prompts. Send the prompt as the text part and the page as the image part of the same user turn.
<table style={{width: "100%", borderCollapse: "collapse", tableLayout: "fixed"}}> <thead> <tr style={{borderBottom: "2px solid #d55816"}}> <th style={{textAlign: "left", padding: "10px 12px", fontWeight: 700}}>Prompt</th> <th style={{textAlign: "left", padding: "10px 12px", fontWeight: 700}}>Task</th> </tr> </thead> <tbody> <tr> <td style={{padding: "9px 12px"}}><code>OCR:</code></td> <td style={{padding: "9px 12px"}}>Plain text recognition.</td> </tr> <tr style={{backgroundColor: "rgba(255,255,255,0.02)"}}> <td style={{padding: "9px 12px"}}><code>Table Recognition:</code></td> <td style={{padding: "9px 12px"}}>Table structure and cell contents.</td> </tr> <tr> <td style={{padding: "9px 12px"}}><code>Formula Recognition:</code></td> <td style={{padding: "9px 12px"}}>Mathematical expressions.</td> </tr> <tr style={{backgroundColor: "rgba(255,255,255,0.02)"}}> <td style={{padding: "9px 12px"}}><code>Chart Recognition:</code></td> <td style={{padding: "9px 12px"}}>Chart contents.</td> </tr> <tr> <td style={{padding: "9px 12px"}}><code>Spotting:</code></td> <td style={{padding: "9px 12px"}}>Text with locations. Benefits from the high-detail resolution setting.</td> </tr> <tr style={{backgroundColor: "rgba(255,255,255,0.02)"}}> <td style={{padding: "9px 12px"}}><code>Seal Recognition:</code></td> <td style={{padding: "9px 12px"}}>Seals and stamps (1.6).</td> </tr> </tbody> </table>Structured tasks answer in the model's own markup, not HTML: Table Recognition: returns OTSL-style cell tokens (<fcel> per cell, <nl> per row), so a caller that wants HTML or markdown converts them itself.
<fcel>Methods<fcel>R<fcel>P<fcel>F<fcel>FPS<nl><fcel>SegLink [26]<fcel>70.0<fcel>86.0<fcel>77.0<fcel>8.9<nl><fcel>PixelLink [4]<fcel>73.2<fcel>83.0<fcel>77.8<fcel>-<nl><fcel>TextSnake [18]<fcel>73.9<fcel>83.2<fcel>78.3<fcel>1.1<nl>
... (one <fcel> per cell, one <nl> per row, to the end of the table)
from openai import OpenAI
client = OpenAI(base_url="http://localhost:30000/v1", api_key="EMPTY")
response = client.chat.completions.create(
model="PaddlePaddle/PaddleOCR-VL-1.6",
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": "OCR:"},
{
"type": "image_url",
"image_url": {
"url": "https://raw.githubusercontent.com/PaddlePaddle/PaddleOCR/release/2.9/doc/imgs_en/img_12.jpg"
},
},
],
}
],
max_tokens=2048,
)
print(response.choices[0].message.content)
ACKNOWLEDGEMENTS
We would like to thank all the designers and contributors who have been involved in the
production of this book; their contributions have been indispensable to its creation. We would
also like to express our gratitude to all the producers for their invaluable opinions and
assistance throughout this project. And to the many others whose names are not credited but
have made specific input in this book, we thank you for your continuous support.
The model parses one page per request. Render each page to an image, then fan the pages out concurrently — SGLang batches the vision encoders of in-flight requests into a single forward, so concurrency is what keeps the GPU busy on a model this small.
<Accordion title="Concurrent Page Parsing (Python)">import base64
from concurrent.futures import ThreadPoolExecutor
import pymupdf
from openai import OpenAI
client = OpenAI(base_url="http://localhost:30000/v1", api_key="EMPTY")
def render(page, dpi=200):
pixmap = page.get_pixmap(dpi=dpi)
return base64.b64encode(pixmap.tobytes("png")).decode("ascii")
def parse(page_png_b64):
response = client.chat.completions.create(
model="PaddlePaddle/PaddleOCR-VL-1.6",
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": "OCR:"},
{
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{page_png_b64}"
},
},
],
}
],
max_tokens=2048,
)
return response.choices[0].message.content
document = pymupdf.open("your_document.pdf")
pages = [render(page) for page in document]
with ThreadPoolExecutor(max_workers=16) as pool:
for index, text in enumerate(pool.map(parse, pages)):
print(f"--- page {index + 1} ---")
print(text)
--- page 1 ---
(a) Total-Text
(b) Total-Text
(c) CTW1500
(d) CTW1500
Figure 8. Visual experimental results. The blue contours are boundary proposals, and the
green contours are final detection boundaries.
Table 6. Experimental results on CTW-1500.
Methods
Ext
R
P
F
FPS
TextSnake [18]
Syn
85.3
67.9
75.6
-
... (page continues)
--- page 2 ---
... (one block per page, in page order)