상태: 실측 기록 (2026-10-28 부터 10-30). 증상과 명령은 측정 로그와 실행 스크립트에서 가져왔습니다. 로그에 남지 않아 정확한 오류 문구를 옮길 수 없는 것은 그렇다고 적었습니다. PR 셋은 2026-10-30 기준 모두 열려 있고(병합 전), 결과는 단정하지 않습니다.
목차
- 1. 한눈에
- 2. 모델 받기가 2.88GB 에서 멈췄다
- 3. 한 이미지에 두 프레임워크를 넣으면
- 4. OpenCV, libGL, 그리고 깨진 cv2 폴더
- 5. 긴 측정이 두 번 끊겼다
- 6. 채점 함정 하나
- 7. 보안 메모: 모델 출력을 eval() 하던 코드
- 8. 다시 한다면 처음부터 할 일
- 참고
1편은 결과만 보였습니다. 그 뒤에는 설치하며 막힌 일이 꽤 있었습니다. 같은 길을 가는 분이 덜 헤매도록 증상과 원인, 고친 방법을 적습니다. 환경은 24GB GPU 한 장이 달린 리눅스 서버이고, 측정은 GPU 를 쓰는 일회용 컨테이너 안에서 했습니다.
1. 한눈에
| 막힌 곳 | 증상 | 고친 방법 |
|---|---|---|
| 모델 받기 | Hugging Face 다운로드가 2.88GB 에서 멈춤 | HF_HUB_DISABLE_XET=1 로 Xet 을 끄고 받기 |
| 두 프레임워크 | PyTorch 이미지에 Paddle 을 넣자 torch 가 깨짐(NCCL 겹침) | Paddle 은 별도 가상 환경에, Unlimited-OCR 의존성은 별도 폴더에 |
| OpenCV 와 libGL | ImportError: libGL.so.1 |
시스템에 libgl1·libglib2.0-0 설치 |
| headless OpenCV | PaddleX 의존성 검사에 걸림, 바꿔 끼우다 cv2 가 깨짐 |
네 판을 모두 지우고 한 판만 --force-reinstall --no-deps |
| 긴 측정 | 원격 연결이 끊기며 측정이 컨테이너째 사라짐 | 묶음을 나눠 돌리고, 쪽마다 결과를 로그에 한 줄씩 |
| 채점 | 좌표 표식 때문에 CER 이 16%로 부풀려짐 | 좌표 정규식 추가(1편 3절) |
| 보안 | 원격 코드가 모델 출력을 eval() |
save_results=False 로 측정, 업스트림에 ast.literal_eval PR |
2. 모델 받기가 2.88GB 에서 멈췄다
Unlimited-OCR-2 의 가중치를 처음 받을 때 다운로드가 2.88GB 에서 더 나아가지 않았습니다. 오류도 없이 진행률만 멈춘 상태였습니다.
Hugging Face 허브는 큰 파일을 Xet 이라는 새 저장 방식(파일을 조각으로 나눠 중복 없이 주고받는다)으로 내려주고, huggingface_hub 는
hf_xet 이 깔려 있으면 그 길을 씁니다.
환경 변수 HF_HUB_DISABLE_XET=1 을 주면 예전 방식의 HTTP 다운로드로 돌아갑니다. 그렇게 한 뒤에는 가중치 두 파일이 1분 50초에 끝났고,
모델 적재까지 114.7초가 걸렸습니다. 왜 멈췄는지(네트워크 경로, hf_xet 판, 서버 쪽 사정)는 따로 재현하지 않았습니다(미확인). 우리는
측정용 컨테이너를 띄우는 스크립트에 이 변수를 기본으로 넣었습니다.
한 가지 덧붙이면, PaddleOCR 의 모델들도 Hugging Face 에서 받았습니다. PaddleX 의 PADDLE_PDX_MODEL_SOURCE=huggingface 로 출처를 정하고,
캐시 폴더(PADDLE_PDX_CACHE_HOME)를 지워지지 않는 볼륨에 두면 컨테이너를 새로 띄울 때마다 다시 받지 않습니다.
3. 한 이미지에 두 프레임워크를 넣으면
처음에는 PyTorch 가 든 컨테이너 이미지 하나에 PaddlePaddle 까지 넣어 세 엔진을 한곳에서 돌리려 했습니다. 두 가지가 걸렸습니다.
Paddle 바퀴(wheel)가 요구하는 NCCL 판이 PyPI 에 없었습니다. paddlepaddle-gpu==3.4.0 은 NVIDIA 의 집합 통신 라이브러리 NCCL 을
nvidia-nccl-cu12==2.28.3 으로 요구하는데, 기본 색인에는 2.27.3 까지만 있어 설치가 실패했습니다. Paddle 이 운영하는 CUDA 12.9 용 색인을
먼저 보고 PyPI 를 덧붙이는 식으로 받으면 됩니다.
pip install paddlepaddle-gpu==3.4.0 \
-i https://www.paddlepaddle.org.cn/packages/stable/cu129/ \
--extra-index-url https://pypi.org/simple
같은 환경에 넣으면 torch 가 깨졌습니다. PyTorch 와 Paddle 은 각자 맞춰 빌드된 nvidia-* CUDA 라이브러리 묶음을 끌고 옵니다. Paddle 을
같은 site-packages 에 넣자 두 쪽의 NCCL 이 겹쳤고, torch 가 불러오기 단계에서 깨졌습니다(어느 판이 이겼는지와 정확한 오류 문구는
로그에 남지 않았습니다). 한 프로세스에서 두 프레임워크를 함께 쓸 일이 없으니 환경을 나누는 것이 답이었습니다.
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 로 빈 가상 환경을 만들고 바깥 pip 의 --python 으로 그 안에 설치하면, 가상 환경마다 pip 를 새로 까는 시간을 아낍니다.
Unlimited-OCR-2 쪽도 이미지의 패키지를 건드리지 않았습니다. 모델 카드가 시험한 Transformers 4.52.1 과 그 판에 맞는 tokenizers·huggingface-hub
등을 pip install --target 으로 별도 폴더에 넣고 PYTHONPATH 로만 불렀습니다. 이미지가 가진 torch 는 그대로 두고, 모델 코드가 요구하는 판만
얹는 방식입니다.
4. OpenCV, libGL, 그리고 깨진 cv2 폴더
Paddle 환경을 만들고 import cv2 를 하자 이렇게 멈췄습니다.
ImportError: libGL.so.1: cannot open shared object file: No such file or directory
PaddleOCR 이 끌고 오는 OpenCV(opencv-contrib-python)는 화면 출력용 기능까지 든 판이라 시스템의 libGL 을 찾습니다. 서버용 컨테이너
이미지에는 그 라이브러리가 없습니다.
흔한 처방은 화면 기능을 뺀 headless 판(opencv-contrib-python-headless)으로 바꾸는 것입니다. 여기서 두 번 더 막혔습니다.
- PaddleX 의 의존성 검사가 배포판 이름을 봅니다. PaddleX 는 필요한 패키지가 깔렸는지를
import가 되는지가 아니라 설치된 배포판의 이름으로 확인했습니다. headless 판은 같은cv2를 주지만 이름이 다르므로 「필요한 패키지가 없다」고 판단해 멈췄습니다. - 두 판을 바꿔 끼우다
cv2폴더가 깨졌습니다. OpenCV 의 네 판(opencv-python,-headless,-contrib,-contrib-headless)은 모두 같은site-packages/cv2/폴더에 파일을 씁니다. 하나를 지우면 다른 판이 쓴 파일까지 함께 지워지고, 그 위에 새 판을 깔면 반쯤 남은 폴더가 됩니다. pip 는 이것을 막지 않습니다.
마지막에 남긴 순서는 이렇습니다. 원래 판을 그대로 두고, 빠진 시스템 라이브러리를 채웁니다.
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__)"
네 판을 모두 지운 뒤 한 판만 --force-reinstall --no-deps 로 다시 깔아야 폴더가 깨끗해집니다. 판 번호는 PaddleOCR 이 처음 고른 값을
그대로 씁니다. 컨테이너를 새로 띄울 때마다 apt-get 이 도는 것이 아깝다면 libgl1 이 든 이미지를 따로 만들어 두는 편이 낫습니다.
5. 긴 측정이 두 번 끊겼다
PaddleOCR 두 엔진을 한 번에 다 돌리는 측정은 원격 실행 연결이 도중에 끊기면서 측정용 컨테이너째 지워졌습니다. 로그 끝에는 TLS 오류와 「조건을 기다리다 시간 초과」만 남았습니다. Unlimited-OCR-2 의 40쪽 묶음도 세션이 다시 시작되며 출력 도중에 끊겼습니다(그래서 1편에는 10쪽과 20쪽만 있습니다).
두 번 다 살릴 수 있었던 것은 측정 스크립트가 쪽 하나를 끝낼 때마다 결과를 남겼기 때문입니다.
- 출력 원문은 쪽마다 파일로 바로 씁니다.
- 시간·메모리·오류는 쪽마다 JSON 한 줄로 표준 출력에 찍습니다. 요약 파일은 맨 끝에 한 번만 쓰므로 중간에 끊기면 생기지 않지만, 로그의 JSON 줄을 모으면 요약을 그대로 되살릴 수 있습니다. 1편의 표는 이렇게 로그에서 복원한 값입니다.
그 뒤로는 묶음(일반 본문, 흐린 사본, 표, 긴 문서)을 인자로 받아 나눠 돌리고, 묶음마다 로그를 따로 남겼습니다. 한 번에 몇 시간짜리 실행을 걸지 않는 것이 연결이 끊기는 환경에서는 가장 싼 보험입니다. 덧붙여 매 실행의 첫 쪽은 예열로 버렸습니다. Unlimited-OCR-2 의 첫 쪽은 43.9초로, 이어진 쪽들(30초 안팎)보다 길었습니다.
6. 채점 함정 하나
1편 3절에 적은 것입니다. Unlimited-OCR-2 출력의 블록 좌표 text[71, 96, 900, 160] 를 지우지 않으면 일반 본문 CER 이 16.3%로 나오고, 지우면
0.80%입니다. 엔진 출력의 꼴이 바뀌면 채점기가 조용히 틀린 숫자를 냅니다. 새 엔진을 붙일 때는 출력 원문 몇 쪽을 먼저 눈으로 읽고, 정규화한
글자를 정답과 나란히 출력해 본 뒤에 점수를 믿습니다.
7. 보안 메모: 모델 출력을 eval() 하던 코드
Unlimited-OCR-2 는 모델 코드를 저장소에서 받아 실행하는 trust_remote_code=True 로 불러옵니다. 그 코드를 읽다가 결과 저장 경로에서 이런
줄을 찾았습니다(modeling_unlimitedocr.py, revision 5632329a).
cor_list = eval(ref_text[2])
infer(..., save_results=True) 로 부르면 원격 코드는 모델 출력에서 <|det|>이름[…]<|/det|> 꼴의 표식을 정규식으로 찾고, 대괄호 안의
글자를 좌표로 읽으려고 eval() 에 넘깁니다. 정규식은 대괄호 안에 ] 만 없으면 무엇이든 통과시킵니다. 그리고 모델 출력은 입력 문서에 따라
달라지는 글자입니다. 문서는 누구나 만들 수 있으니, 원리상 문서 안의 글자가 모델 출력을 거쳐 파이썬 식으로 실행될 수 있는 길입니다.
어디까지가 확인한 사실인지 나눠 둡니다.
- 확인함: 위
eval()줄과,save_results=True일 때만 그 함수가 불린다는 것(코드에서 읽음). - 확인함: 이번 측정의 기본 프롬프트에서 Unlimited-OCR-2 는
<BLOCK>text[…]<FORK>꼴로 출력했고, 이 꼴은 위 정규식에 걸리지 않습니다. - 미실측: 문서 내용으로 모델이
<|det|>표식과 임의의 식을 내게 만들 수 있는지는 시험하지 않았습니다. 공격 가능성을 증명한 것이 아니라, 신뢰할 수 없는 입력이eval()까지 가는 길이 코드에 있다는 것입니다.
우리는 측정에서 save_results=False 로 불러 출력 글자만 받았습니다. 그림을 그리거나 상자를 잘라 저장하는 기능이 필요하면, 좌표는
ast.literal_eval 로 읽으면 됩니다. 숫자·목록·문자열 같은 리터럴만 받고 식은 ValueError 로 거절하므로, 정상 좌표에서는 결과가 같습니다.
이 함수는 DeepSeek-OCR 계열 예제 코드에서 이어져 온 것이라 같은 줄이 여러 저장소에 있습니다. 같은 수정을 세 곳에 PR 로 냈습니다. 각 PR 은 원래 함수와 고친 함수를 같은 입력에 돌려 정상 좌표의 결과가 같고, 식이 든 입력은 실행되지 않고 기존 오류 경로로 빠지는 것을 확인한 내용을 본문에 적었습니다. AI 도움을 받아 준비했다는 고지도 넣었습니다.
| 저장소 | PR | 상태(2026-10-30) |
|---|---|---|
| deepseek-ai/DeepSeek-OCR-2 (vLLM 예제 스크립트) | #61 | 열림 |
| openvinotoolkit/openvino_notebooks (deepseek-ocr, deepseek-vl2 노트북) | #3820 | 열림 |
| basetenlabs/truss-examples (deepseek-ocr 예제) | #594 | 열림 |
trust_remote_code=True 로 모델을 쓸 때 우리가 지키는 것도 함께 적습니다. revision(커밋 해시)을 고정해 받은 코드가 바뀌지 않게 하고, 고정한
그 판의 코드를 한 번은 읽고, 쓰지 않는 저장·시각화 경로는 끕니다. GPU 를 쓰는 측정 컨테이너는 클러스터 API 토큰 없이(서비스 계정 토큰 자동
마운트 끄기) 띄웠습니다.
8. 다시 한다면 처음부터 할 일
- 측정용 컨테이너를 띄우는 스크립트에
HF_HUB_DISABLE_XET=1과 모델 캐시 볼륨을 기본으로 넣는다. - 프레임워크마다 환경을 나눈다. PyTorch 이미지 위에 Paddle 은 별도 가상 환경, 원격 코드 모델의 의존성은
--target폴더. libgl1·libglib2.0-0이 든 이미지를 쓴다. OpenCV 판은 PaddleOCR 이 고른 것 하나만, 바꿔야 하면 네 판을 다 지우고 다시.- 측정 스크립트는 쪽마다 출력 파일과 JSON 한 줄을 남기고, 묶음을 나눠 돌리고, 첫 쪽은 예열로 버린다.
- 채점 전에 출력 원문을 읽고 정규화한 글자를 정답과 나란히 본다.
trust_remote_code모델은 revision 을 고정하고, 그 판의 코드에서eval·exec·pickle·subprocess를 먼저 찾아본다.
티티 한마디. OCR 모델이 읽은 글자는 문서가 시킨 대로 나와요. 그러니까 모델 출력도 손님이 건넨 쪽지처럼 다뤄야 해요. 쪽지에 적힌 걸 그대로 실행하는 사람은 없잖아요?
참고
- Hugging Face Hub 환경 변수(
HF_HUB_DISABLE_XET): https://huggingface.co/docs/huggingface_hub/package_reference/environment_variables - Unlimited-OCR-2 모델 카드(Transformers 시험 판, SGLang 권장): https://huggingface.co/baidu/Unlimited-OCR-2
- PaddleOCR 설치 안내: https://www.paddleocr.ai/
- 파이썬
ast.literal_eval: https://docs.python.org/3/library/ast.html#ast.literal_eval - 업스트림 PR: DeepSeek-OCR-2#61, openvino_notebooks#3820, truss-examples#594