Repository navigation
[Model] Serve DeepSeek-OCR-2 with its official 768px local-crop geometry - #38996
Merged
Merged
Conversation
chaijiacheng888
requested review from
HaiShaw,
JustinTong0323,
mickqian,
yctseng0211,
yhyang201 and
yuan-luo
as code owners
September 11, 2026 03:44
cx22757
reviewed
Sep 11, 2026
chaijiacheng888
force-pushed
the
ds-ocr2-768
branch
from
September 11, 2026 07:37
d7962e6 to
9535bb4
Compare
DeepSeek-OCR-2 was served with the 640px local-crop size inherited from DeepSeek-OCR. At 640px each local crop yields (640/16/4)^2 = 100 visual tokens; the OCR-2 vision decoder only ships tuned query tables for 144 (query_768) and 256 (query_1024) tokens, so 100-token crops fall back to a linearly interpolated query embedding. The official OCR-2 geometry is 768px (144 tokens/crop, matching query_768). Select the local crop size from the OCR-2/OCR-1 vision encoder instead of hardcoding 640, and make the crop pixel size follow the processor image_size so crop geometry and the visual-token budget stay coupled. OCR-1 is unchanged (640px). Add a CPU unit test pinning the crop-size selection and the 144/256/100 per-crop token mapping, plus a runnable example under examples/runtime/multimodal.
Allow driving a whole page-image directory through /generate into per-page markdown (official prompt / greedy / ngram no-repeat / official post-process), so the OmniDocBench accuracy reproduction in the PR description needs no private scripts.
Removed outdated comments regarding OCR2_IMAGE_SIZE and its relation to token budgets.
chaijiacheng888
force-pushed
the
ds-ocr2-768
branch
from
September 15, 2026 03:27
67a0083 to
8a9633d
Compare
Collaborator
|
/tag-and-rerun-ci |
sglang-npu-bot
approved these changes
Sep 17, 2026
Contributor
|
/rerun-failed-ci |
Contributor
Author
|
/rerun-failed-ci |
2 similar comments
Contributor
Author
|
/rerun-failed-ci |
Contributor
Author
|
/rerun-failed-ci |
Contributor
Author
|
/rerun-failed-ci |
1 similar comment
Contributor
Author
|
/rerun-failed-ci |
Collaborator
|
only failed test/registered/utils/test_numa_utils.py, and it repaired by 40055 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
[Model] Serve DeepSeek-OCR-2 with its official 768px local-crop geometry
Motivation
DeepSeek-OCR-2 is served with the 640px local-crop geometry inherited from the DeepSeek-OCR (v1)
processor. Its own geometry is 768px: a 768px crop yields
(768 // 16 // 4) ** 2 == 144visualtokens, the length of the tuned
query_768table, while 640px yields 100, which matches no tunedtable and falls back to
F.interpolatequery embeddings. The model's own paper states the pagebudget this produces —
V-token1120 for OCR-2 against 1156 for OCR-1, i.e.6 x 144 + 256and9 x 100 + 256.#17897ported the DeepEncoder V2 classes and query tables, but the shared processor kept OCR-1'scrop size and two thresholds compared against OCR-1's
640as a literal, so OCR-2 ran off-design inthree places, all inside
tokenize_with_images:IMAGE_SIZEconstant, so tiles stayed 640px no matter whatimage_sizesaid — this is what actually keeps the 768px crop from happening;if img_w <= 640 and img_h <= 640:— the small-image early-out, so 641–768px pages were tiledat all;
if self.image_size <= 640 and not cropping:— the global-view guard, so >2-image requestsskipped the resize the official processor still performs.
All three are silent. Restoring the official geometry halves
text Edit: 0.0985 → 0.0533 on Ascendand 0.0892 → 0.0519 on RTX 4090, with every other metric improving too (tables below).
Modifications
Geometry has a single source of truth, selected per checkpoint from the model config. Neither
checkpoint carries the crop size — both ship identical processor configs whose
candidate_resolutions=[[1024, 1024]]is the global base.python/sglang/srt/configs/deepseek_ocr.py: addOCR2_IMAGE_SIZE = 768andis_ocr2_config()/local_crop_size(); pass the selected size into the crop step and thesmall-image early-out; reduce the global-view guard to
if not cropping:. OCR-1 is byte-identical.python/sglang/srt/multimodal/processors/deepseek_ocr.py: addapply_ocr_geometry(), whichpatches
ocr2_modeandimage_sizeonto the HF processor — Transformers builds it before SGLangsees it, so this cannot be a constructor argument. The
_is_ocr2predicate moves down toconfigs/and is shared by both layers.python/sglang/srt/models/deepseek_ocr.py: import that predicate instead of defining it.test/registered/unit/multimodal/test_deepseek_ocr_geometry.py(new, CPU): pins the 768/640selection, the patching, and that
dynamic_preprocessfollows theimage_sizeit is given.Accuracy Tests
OmniDocBench, page level,
quick_match. The blocks are scored version-matched: the v1.5 block onthe official v1.5 dataset with the v1.5 scorer, the v1.6 block on the v1.6 dataset with the v1.6
scorer. The datasets are not interchangeable, and v1.6 replaced the CDM implementation, so a CDM
number only means something within one block.
v1.5, 1355 pages, Ascend 910B (1x,
bfloat16):v1.6, 1651 pages:
Both platforms agree: the two 768 rows are 0.20 apart and the two 640 rows 0.66.
The ground truth is the released v1.6 dataset with 37 dangling
relation_type="truncated"reading-order edges dropped; the official scorer raises on the first one. Only the reading-order
metric on those 12 pages is affected.
Speed Tests and Profiling
Preprocessing-only change. A 768px crop carries 144 visual tokens instead of 100, while 641–768px
pages drop from 657 tokens to 257 (no local crops), so the cost depends on the page mix.
End-to-end, all 1651 pages through the RTX 4090 server at 12 concurrent requests: ~30 minutes
either way, 0.9 pages/s, indistinguishable.
Checklist
Notes
--context-length 8192+--allow-auto-truncate: a 768px page expandsto ~1100 image tokens and long pages can emit up to 8192, so the output budget is clamped to the
remaining context rather than the request being rejected.
CI States
Latest PR Test (Base): ❌ Run #35303604206
Latest PR Test (Extra): ❌ Run #35303604121
Latest PR Test (AMD ROCm 10): ❌ Run #35303604233