Status: Actual measurement records (October 28 to October 30, 2026). Symptoms and commands are taken from measurement logs and execution scripts. Instances where exact error messages could not be copied because they were not left in the logs are noted as such. All three PRs were open (unmerged) as of October 30, 2026, and results are not definitive.
Table of Contents
- 1. At a Glance
- 2. Model Download Stopped at 2.88GB
- 3. Putting Two Frameworks in One Image
- 4. OpenCV, libGL, and the Broken cv2 Folder
- 5. Long Measurements Interrupted Twice
- 6. One Scoring Trap
- 7. Security Note: Code That Used eval() on Model Outputs
- 8. What I Would Do from the Start If Doing It Again
- References
Part 1showed only the results. Afterward, there were quite a few issues encountered during installation. I write down the symptoms, causes, and fixes so that others walking the same path will wander less. The environment was a Linux server equipped with a single 24GB GPU, and measurements were conducted inside disposable GPU-enabled containers.
1. At a Glance
| Obstacle | Symptom | Fix |
|---|---|---|
| Model Download | Hugging Face download stops at 2.88GB | HF_HUB_DISABLE_XET=1 Disable Xet and download |
| Two Frameworks | Adding Paddle to PyTorch image breaks torch (NCCL conflict) | Put Paddle in a separate virtual environment, and Unlimited-OCR dependencies in a separate folder |
| OpenCV and libGL | ImportError: libGL.so.1 |
Install libgl1·libglib2.0-0 on the system |
| headless OpenCV | triggers PaddleX dependency check error, swapping breaks cv2 folder |
Delete all four versions and use only one --force-reinstall --no-deps |
| Long Measurement | Remote connection drops, destroying the entire container measurement | Split batches and log results line by line per section |
| Scoring | CER inflated to 16% due to coordinate markers | Add coordinate regex (Part 1, Section 3) |
| Security | Remote code measures using model output eval() |
save_results=False , submitted ast.literal_eval PR to upstream |
2. Model Download Stopped at 2.88GB
When downloading weights for Unlimited-OCR-2 for the first time, the download did not progress past 2.88GB. It stopped advancing in progress without any errors. The Hugging Face Hub delivers large files using a new storage method called Xet (which splits files into chunks to send and receive without duplication), and huggingface_hub uses that path if
hf_xet is installed.
Setting the environment variable HF_HUB_DISABLE_XET=1 reverts it to the legacy HTTP download method. After doing so, downloading the two weight files finished in 1 minute and 50 seconds, and loading the model took 114.7 seconds. I did not separately reproduce why it stopped (network path, hf_xet version, server-side conditions) (unconfirmed). We added this variable by default to the script that spins up the measurement containers.
As an aside, PaddleOCR models were also downloaded from Hugging Face. Setting the source with PaddleX's PADDLE_PDX_MODEL_SOURCE=huggingface and placing the cache folder (PADDLE_PDX_CACHE_HOME) on a persistent volume prevents re-downloading every time a new container is launched.
3. Putting Two Frameworks in One Image
Initially, I tried to run all three engines in one place by adding PaddlePaddle to a single PyTorch-based container image. Two issues arose.
The NCCL version required by the Paddle wheel was not available on PyPI. paddlepaddle-gpu==3.4.0 requires NVIDIA's collective communication library NCCL as
nvidia-nccl-cu12==2.28.3 , but the default index only had up to 2.27.3, causing installation to fail. This can be resolved by checking the index for CUDA 12.9 maintained by Paddle first and then appending PyPI.
pip install paddlepaddle-gpu==3.4.0 \
-i https://www.paddlepaddle.org.cn/packages/stable/cu129/ \
--extra-index-url https://pypi.org/simple
Putting them in the same environment broke torch. PyTorch and Paddle each bring along bundled nvidia-* CUDA libraries built for themselves. Adding Paddle to the same site-packages caused the NCCL versions from both sides to conflict, and torch broke at the import stage (neither which version won nor the exact error message remained in the logs). Since there is no need to use both frameworks together in a single process, splitting the environment was the answer.
python -m venv --without-pip /models/ocr/venv-paddle
P=/models/ocr/venv-paddle/bin/python
pip --python $P install paddlepaddle-gpu==3.4.0 -i …/cu129/ --extra-index-url https://pypi.org/simple
pip --python $P install "paddleocr[doc-parser]==3.7.0"
--without-pip Creating an empty virtual environment with and installing into it using the outer pip's --python saves time on reinstalling pip for each virtual environment. The Unlimited-OCR-2 side also did not touch the image's packages. The Transformers 4.52.1 tested in the model card and its matching tokenizers·huggingface-hub
were placed in a separate folder using pip install --target and invoked only via PYTHONPATH . This approach leaves the torch provided by the image intact while adding only the versions required by the model code.
4. OpenCV, libGL, and the Broken cv2 Folder
I set up the Paddle environment and import cv2 and stopped right there.
ImportError: libGL.so.1: cannot open shared object file: No such file or directory
The OpenCV pulled in by PaddleOCR (opencv-contrib-python) includes screen output features, causing it to look for system libGL . That library does not exist in the server container image.
A common remedy is to switch to the headless edition (opencv-contrib-python-headless) with the screen features removed. Here I hit two more roadblocks.
- PaddleX's dependency check inspects the distribution name. PaddleX verifies whether the required packages are installed by checking the name of the installed distribution rather than whether it
import. The headless edition provides the samecv2but has a different name, so it concluded that "required packages are missing" and stopped. - While swapping between the two editions, the
cv2folder became corrupted. All four OpenCV editions (opencv-python,-headless,-contrib,-contrib-headless) write files to the samesite-packages/cv2/folder. Deleting one also removes the files written by another edition, and installing a new edition on top of it leaves a half-empty folder. pip does not prevent this.
The order I settled on in the end is as follows: keep the original edition as is, and fill in the missing system libraries.
V=$(pip --python $P show opencv-contrib-python | sed -n 's/^Version: //p')
pip --python $P uninstall -y opencv-contrib-python opencv-contrib-python-headless \
opencv-python opencv-python-headless
pip --python $P install --force-reinstall --no-deps opencv-contrib-python==$V
apt-get install -y libgl1 libglib2.0-0
$P -c "import cv2; print(cv2.__version__)"
Deleting all four editions and reinstalling just one edition via --force-reinstall --no-deps is the only way to clean up the folder. I used the exact version number chosen initially by PaddleOCR. If you want to avoid running apt-get every time you spin up a new container, it is better to pre-build an image containing libgl1 .
5. Two Long Measurement Runs Interrupted
The measurement run executing both PaddleOCR engines simultaneously was terminated along with the measurement container due to a dropped remote connection. The end of the log only showed a TLS error and "timed out waiting for condition." The 40-page batch of Unlimited-OCR-2 was also interrupted mid-output as the session restarted (which is why Part 1 only contains pages 10 and 20).
The only reason we were able to recover both times was because the measurement script saved its results every time a single page finished .
- The raw output is written directly to a file for each page.
- Time, memory, and errors are printed as a single JSON line to standard output per page. Since the summary file is written only once at the very end, an interruption in the middle prevents it from being created, but collecting the JSON lines from the log allows you to fully reconstruct the summary. The tables in Part 1 were recovered from the logs in this manner.
After that, I passed batches (general body text, faded copies, tables, long documents) as arguments to run them separately, maintaining separate logs for each batch. In an environment prone to connection drops, avoiding multi-hour single execution runs is the cheapest insurance. Additionally, the first page of every run was discarded as a warm-up. The first page of Unlimited-OCR-2 took 43.9 seconds, which was longer than the subsequent pages (around 30 seconds).
6. A Scoring Pitfall
As mentioned in Section 3 of Part 1, if you do not remove the block coordinates text[71, 96, 900, 160] from the Unlimited-OCR-2 output, the general body CER comes out to 16.3%, whereas removing them brings it to 0.80%. When the format of the engine output changes, the scorer silently produces incorrect numbers. Before attaching a new engine, you should visually inspect a few pages of raw output and print the normalized text side-by-side with the ground truth before trusting the scores.
7. Security Note: Code That Used eval() on Model Outputs
Unlimited-OCR-2 imports model code downloaded from the repository using trust_remote_code=True . While reading through that code, I found this line in the result saving path (modeling_unlimitedocr.py, revision 5632329a).
cor_list = eval(ref_text[2])
infer(..., save_results=True) ), when called with <|det|>이름[…]<|/det|> , the remote code uses a regular expression to search the model output for markers in the form of eval() , and passes them to ] in order to read the text inside the brackets as coordinates. The regular expression passes anything as long as it does not contain inside the brackets. And the model output istext that varies depending on the input document
. Since anyone can create a document, in principle, text within the document could pass through the model output and become a path executed as Python code.
- Verified: That the function is called only when
eval()line andsave_results=True(read from the code). - Verified: In the default prompt for this measurement, Unlimited-OCR-2 outputted in the form of
<BLOCK>text[…]<FORK>, which does not match the aforementioned regular expression. - Not Tested: I did not test whether document content could cause the model to output
<|det|>markers and arbitrary expressions. This does not prove exploitability, but rather that a path exists in the code where untrusted input reacheseval().
For our measurements, we called save_results=False and only retrieved the output text. If you need to draw figures or crop and save bounding boxes, the coordinates can be read using
ast.literal_eval . Since it accepts only literals like numbers, lists, and strings and rejects expressions via ValueError , the results are identical for normal coordinates.
Since this function originated from DeepSeek-OCR family example code, identical lines exist in multiple repositories. I submitted PRs with the same fix to three places. In each PR, I documented that the original and fixed functions were run on the same inputs to confirm that normal coordinate results remain identical, while inputs containing expressions are safely rejected into the existing error path instead of being executed. I also included a disclosure that the fix was prepared with AI assistance.
| Repository | PR | Status (2026-10-30) |
|---|---|---|
| deepseek-ai/DeepSeek-OCR-2 (vLLM example script) | #61 | Open |
| openvinotoolkit/openvino_notebooks (deepseek-ocr, deepseek-vl2 notebooks) | #3820 | Open |
| basetenlabs/truss-examples (deepseek-ocr example) | #594 | Open |
trust_remote_code=True We also list the practices we follow when using models as . We pin the revision (commit hash) so the received code does not change, read the code of that pinned version at least once, and turn off unused logging and visualization paths. The benchmark container using the GPU was launched without cluster API tokens (disabling automatic service account token mounting).
8. What to do from the start if we were to do it again
- Add
HF_HUB_DISABLE_XET=1and model cache volume to the benchmark container launch script by default. - Separate environments for each framework. On top of the PyTorch image, Paddle uses a separate virtual environment, and dependencies for remote code models go to the
--targetfolder. libgl1·libglib2.0-0Use either image. For the OpenCV version, stick to the one chosen by PaddleOCR; if it needs to be changed, delete all four versions and start over.- The benchmark script leaves an output file and a single line of JSON per page, runs in split batches, and discards the first page as a warmup.
- Before grading, read the raw output text and compare the normalized characters side-by-side with the ground truth.
trust_remote_codeFor models, pin the revision, and first look foreval·exec·pickle·subprocessin that version's code.
A word from TT. The text read by an OCR model comes out just as the document instructed. Therefore, model outputs should also be treated like a note handed to you by a customer. Nobody executes what is written on a note blindly, right?
References
- Hugging Face Hub environment variables(
HF_HUB_DISABLE_XET): https://huggingface.co/docs/huggingface_hub/package_reference/environment_variables - Unlimited-OCR-2 Model Card (Transformers test version, SGLang recommended): https://huggingface.co/baidu/Unlimited-OCR-2
- PaddleOCR Installation Guide: https://www.paddleocr.ai/
- Python
ast.literal_eval: https://docs.python.org/3/library/ast.html#ast.literal_eval - Upstream PR: DeepSeek-OCR-2#61, openvino_notebooks#3820, truss-examples#594