
HuggingFace Space 데모를 로컬로 이전해 보았다. 코드 수정은 단 한 줄이었다
요약
HuggingFace Space의 데모를 로컬 환경으로 이전하는 방법과 주의사항을 다룹니다. Gradio 버전 명시와 Python 버전 일치의 중요성을 강조하며, uv를 활용한 효율적인 환경 구축 과정을 설명합니다.
핵심 포인트
- HuggingFace Space는 git clone을 통해 로컬로 가져올 수 있음
- README.md의 frontmatter를 통해 SDK 및 Python 버전을 확인해야 함
- Gradio가 requirements.txt에 없는 경우 직접 명시 필요
- uv를 사용하면 특정 Python 버전을 간편하게 설치하고 관리 가능
최근 HuggingFace Space에서 공개되고 있는 데모를 접할 기회가 상당히 많아졌습니다. 브라우저에서 테스트할 수 있는 것은 편리하지만, ZeroGPU 할당 대기나 GPU 시간 제한(수십 초~수 분)이 있어 본격적으로 검증하려고 하면 솔직히 힘듭니다.
그래서 이번에는 NVIDIA가 공개하고 있는 객체 탐지 데모 LocateAnything(자연어로 이미지·영상 내의 오브젝트를 탐지하는 Vision-Language 모델)를 제 손안의 GPU 머신으로 이전해 보았습니다.
결론부터 말하자면, 코드 수정은 requirements.txt에 한 줄 추가하는 것만으로 작동했습니다. 다만, 그 단계에 도달하기까지 'Space 고유의 메커니즘'을 몇 가지 이해할 필요가 있었기에, 실제 절차와 주의할 점을 정리하여 공유합니다.
마찬가지로 'Space의 데모를 로컬에서 실행하고 싶다'는 분들에게 참고가 된다면 좋겠습니다.
| 항목 | 내용 |
|---|---|
| GPU | NVIDIA GeForce RTX 5080 Laptop (16GB VRAM, Blackwell 세대) |
| ... | |
| 포인트는 RTX 5080이 Blackwell (sm_120) 세대라는 점입니다. 후술하겠지만, 이 부분이 PyTorch의 휠 (wheel) 선택에 영향을 미칩니다. |
HuggingFace Space는 실체가 단순한 git 리포지토리이므로, 그대로 clone 할 수 있습니다.
git clone https://huggingface.co/spaces/nvidia/LocateAnything
cd LocateAnything
내용물은 이것뿐입니다.
app.py # 메인 애플리케이션 (Gradio Server)
index.html # 커스텀 프론트엔드
requirements.txt # 의존성
...
로컬 이전 시 가장 먼저 읽어야 할 것은 코드가 아니라 README.md의 frontmatter입니다. Space의 실행 환경은 여기서 정의됩니다.
---
title: LocateAnything
sdk: gradio
...
여기서 읽어낼 수 있는 중요한 정보가 두 가지 있습니다.
— Space 측에서는 Gradio가 플랫폼으로부터 공급되기 때문에, sdk_version: 6.14.0
requirements.txt에 gradio가 적혀 있지 않습니다. 로컬에서는 직접 설치해야 합니다. -
— Python 버전 지정입니다. "3.13에서도 돌아가겠지"라며 무시하면, 나중에 의존성 해결에 실패합니다 (후술). python_version: "3.10.13"
실제로 이번 유일한 코드 수정은 이것이었습니다.
huggingface_hub
spaces
+gradio==6.14.0
버전은 README의 sdk_version에 맞춥니다. 이 프로젝트는 from gradio import Server나 @app.api와 같은 Gradio 6 계열의 API를 사용하고 있으므로, 적당히 최신 버전을 넣으면 작동하지 않을 가능성이 있습니다.
제 손안의 Python은 3.13이었지만, requirements.txt를 보니 numpy==1.25.0이 고정되어 있습니다. numpy 1.25는 Python 3.9~3.11까지만 대응하기 때문에, 3.13에서는 애초에 설치할 수 없습니다.
이럴 때 uv를 사용하면 Python 본체 자체의 버전을 지정하여 가상 환경을 만들 수 있어 편리합니다.
# README의 python_version에 맞춰 3.10을 지정
# (미설치 상태라면 uv가 자동으로 다운로드해 줍니다)
uv venv --python 3.10
...
pyenv로 빌드하거나 deadsnakes PPA를 찾을 필요가 없습니다. 개인적으로는 이 "Python 본체 조달까지 포함하여 1개의 커맨드"로 해결되는 점이 uv로 이전하고 가장 고마웠던 부분입니다.
RTX 5080은 Compute Capability **sm_120 (12.0)**입니다. 오래된 CUDA 빌드의 PyTorch라면 "sm_120 is not compatible"이라는 에러와 함께 작동하지 않습니다.
requirements.txt에서는 torch==2.8.0
고정되어 있었습니다. torch 2.8.0의 PyPI 표준 휠 (wheel)은 cu128 (CUDA 12.8) 빌드이므로, 사실 추가 작업 없이도 Blackwell에 대응합니다. 하지만 짐작만으로 판단하는 것은 금물이기에, 반드시 실제로 연산을 실행하여 확인합니다.
.venv/bin/python -c "
import torch
print(torch.__version__, 'cuda:', torch.version.cuda)
...
실행 결과입니다.
2.8.0+cu128 cuda: 12.8
NVIDIA GeForce RTX 5080 Laptop GPU
capability: (12, 0)
...
torch.cuda.is_available()만으로 판단하지 않고, 실제로 randn → 연산까지 실행해 보는 것이 포인트입니다. 커널이 대응하지 않는 경우, 연산을 실행했을 때 비로소 에러가 발생하는 케이스가 있기 때문입니다.
만약 사용 중인 환경에 오래된 CUDA 빌드가 설치되었다면, 명시적으로 cu128 인덱스 (index)를 지정하여 다시 설치하면 됩니다.
uv pip install torch==2.8.0 torchvision==0.23.0 --index-url https://download.pytorch.org/whl/cu128
참고로, 드라이버 측의 CUDA 버전 (이번의 경우 13.2)과 휠 (wheel)의 CUDA 버전 (12.8)은 일치하지 않더라도, 드라이버가 더 최신 버전이라면 하위 호환성(backward compatibility) 덕분에 작동합니다.
app.py를 열면, 영락없는 "Space 전용" 코드가 눈에 들어옵니다.
import spaces # MUST BE THE ABSOLUTE FIRST IMPORT FOR ZEROGPU EMULATION
# ...(중략)...
@spaces.GPU(duration=120, size="xlarge")
...
처음에는 "이걸 전부 삭제하지 않으면 작동하지 않는 것 아닌가"라고 생각했지만, 삭제할 필요가 없었습니다. spaces 라이브러리는 Space 외부(SPACE_ID 환경 변수가 없는 환경)에서 실행되고 있음을 감지하면, @spaces.GPU를 단순한 패스스루 (no-op)로 취급하도록 설계되어 있습니다.
즉, 로컬에서는 데코레이터 (decorator)가 "없었던 것"처럼 처리될 뿐, 부작용은 없습니다. 이 점을 이해해 두면 diff를 0으로 유지하면서 Space 버전과의 호환성을 유지할 수 있습니다. upstream의 업데이트를 git pull로 계속 가져오고 싶을 때, 이 "수정하지 않는다"는 판단은 매우 효과적입니다.
마찬가지로 Space 고유의 로깅 메커니즘 (HuggingFace Dataset으로 이용 로그를 전송하는 기능)도 확인해 본 결과, 토큰 (token)이 없으면 자동으로 비활성화되는 구조였습니다.
if LOG_DATASET_REPO and LOG_HF_TOKEN:
_log_scheduler = CommitScheduler(...)
else:
...
로컬에서는 LOG_HF_TOKEN을 설정하지 않았으므로, 아무것도 하지 않고 로그 전송이 중단됩니다. "Space 전용처럼 보이는 코드"는 삭제하기 전에 "로컬에서 실행되었을 때 어떤 일이 일어나는가"를 추적해 보는 것을 추천합니다. 잘 만들어진 데모 앱은 대부분 로컬 실행까지 고려하여 작성되어 있습니다.
가장 미묘하게 빠졌던 함정은 바로 이곳입니다. 실행 후 샘플 이미지로 추론을 진행했더니, 다음과 같은 에러가 발생했습니다.
PIL.UnidentifiedImageError: cannot identify image file
'/u01/workspace/LocateAnything/assets/book.jpg'
file 명령어로 정체를 확인해 보니, JPEG여야 할 파일이 ASCII 텍스트였습니다.
$ file assets/book.jpg
assets/book.jpg: ASCII text
$ head -c 130 assets/book.jpg
...
HuggingFace 리포지토리 (repository)는 이미지, 폰트 등의 바이너리 (binary)를 git-lfs로 관리합니다. clone한 머신에 git-lfs가 설치되어 있지 않으면, 실제 데이터가 아닌 **포인터 파일 (pointer file)**이 내려받아집니다. 게다가 에러는 "그 파일을 여는 순간" 발생하기 때문에, 실행 시점에는 알아차릴 수 없습니다.
대처 방법은 두 가지가 있습니다.
# 정공법: git-lfs를 설치하여 실체를 가져옴
apt install git-lfs
git lfs pull
...
https://huggingface.co/<repo>/resolve/main/<path>
라는 URL 형식을 기억해 두면, LFS 파일을 개별적으로 실체 파일로 가져올 수 있어 편리합니다.
준비가 되었으므로 실행합니다. 모델(nvidia/LocateAnything-3B, 약 7.2GB)은 첫 추론 시에 지연 로딩 (Lazy Loading) 되도록 설계되어 있어, 실행 자체는 10초 정도면 완료됩니다.
.venv/bin/python app.py
# → http://localhost:7860
브라우저로 확인해도 좋지만, 기왕 하는 김에 API를 통해서도 검증해 보겠습니다. Gradio 6 버전은 POST /gradio_api/call/<api_name> → GET 방식의 이벤트 스트림 (Event Stream)이라는 2단계 구조의 REST API를 가지고 있습니다.
# 추론 요청을 보내 event_id를 획득
EVENT_ID=$(curl -s -X POST http://localhost:7860/gradio_api/call/run_inference \
-H "Content-Type: application/json" \
...
결과는 "success": true였으며, 샘플 이미지에서 book이 22건 바운딩 박스(Bounding Box)와 함께 검출되었습니다. OCR 태스크 (slow 모드 = 표준 자기회귀 디코딩 (Autoregressive Decoding))도 마찬가지로 성공했습니다. ZeroGPU의 시간 제한을 신경 쓰지 않고 몇 번이고 돌릴 수 있습니다.
3B 파라미터, bfloat16 모델이므로 가중치만 약 6~7GB 정도 예상했습니다. 실제 측정 결과는 다음과 같습니다.
$ nvidia-smi --query-gpu=memory.used,memory.total --format=csv
memory.used [MiB], memory.total [MiB]
10094 MiB, 16303 MiB
이 머신은 다른 프로세스 (llama-server)가 함께 실행 중인 상태에서의 수치이므로, 16GB 클래스의 GPU라면 3B VLM을 여유롭게 함께 구동할 수 있다는 느낌을 받았습니다. KV 캐시 (KV Cache)는 max_new_tokens=4096 설정에서도 이 범위 안에 들어왔습니다.
이번 경험을 다른 Space에도 적용할 수 있도록 정리해 둡니다.
| 확인 항목 | 확인 위치 | 이번 케이스 |
|---|---|---|
| SDK 및 버전 | README.md의 sdk / sdk_version | gradio 6.14.0을 requirements에 추가 (유일한 수정 사항) |
| Python 버전 | README.md의 python_version | 3.10 지정. numpy 1.25가 3.13을 지원하지 않으므로 필수 |
@spaces.GPU 처리 | app.py | 로컬에서는 no-op. 삭제 불필요 |
| Space 고유 로깅 등 | app.py | 토큰 미설정 시 자동 비활성화. 삭제 불필요 |
| 바이너리 파일 | file assets/* | git-lfs 포인터였음 → 실체 파일 획득 |
| GPU 세대 및 휠 (Wheel) | torch.cuda.get_device_capability() | sm_120 → torch 2.8.0 표준 cu128로 대응 완료 |
| 모델 획득 가능 여부 | HF 모델 페이지 | public. gated 모델이라면 HF_TOKEN이 필요 |
| VRAM | 모델 크기 × dtype + α | 3B bf16 → 실측 10GB 미만 (동시 실행 프로세스 포함) |
"Space 데모를 로컬로 이전한다"라고 하면 대규모 수정이 필요할 것이라 상상하기 쉽지만, 실제로 해보니,
- 코드 수정은 requirements.txt에
gradio==6.14.0을 추가한 단 한 줄뿐입니다. @spaces.GPU와 같은 Space 고유 코드는 건드리지 않는 것이 정답입니다 (no-op이 되도록 설계되어 있음).- 진짜 고생스러운 부분은 코드가 아니라, README frontmatter, Python 버전, git-lfs와 같은 "리포지토리 주변 환경"입니다.
였습니다. 개인적으로는 "수정을 최소한으로 유지하는 것 = upstream(업스트림)에 대한 추종성을 유지하는 것"이라는 관점에서, 삭제하고 싶은 코드를 굳이 삭제하지 않기로 한 판단이 가장 큰 배움이었습니다.
수중에 GPU가 있는 분들은, 마음에 드는 Space에서 꼭 시도해 보세요!
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기