코드는 다 있는데 아무것도 안 돌아간다
추론 서비스를 새 서버에 체크아웃했다. 커밋은 전부 있다. 그런데 안 뜬다. 없는 것은 이런 것들이다.
| 자산 | 위치 | 원본 | git 추적 |
|---|---|---|---|
| 검출 엔진 | srv/models/{MODEL}/1/model.plan | ONNX에서 굽는다 | ✗ (*.plan ignore) |
| 임베딩 모델 저장소 | srv/vpe-models/ | 별도 경로 | ✗ |
| 보조 엔진 | srv/models/*_fp16.engine | 별도 경로 | ✗ |
| 학습 산출물 | — | 학습 서버 경로 | ✗ |
전부 git 밖이다. 그리고 이건 잘못된 설계가 아니다. TensorRT 엔진은 GPU 아키텍처와 드라이버 버전에 묶여 있어서 다른 서버에서 그대로 쓸 수 없고, 크기도 수백 MB다. 커밋할 이유가 없다.
문제는 git 밖에 있는 것들이 관리 대상에서 같이 빠진다는 점이다.
git 명령은 git 밖의 것을 지켜주지 않는다
워크트리를 정리하다가 이것들을 통째로 잃은 적이 있다. 과정은 이랬다.
git worktree remove가 서브모듈 때문에 거부됨- 커밋이 전부 master에 반영됐는지 확인함
rm -rf로 지움
2번에서 확인한 것은 git이 추적하는 것뿐이었다. 그런데 srv/vpe-models가 그 워크트리를 가리키는 심링크였고, 검출 엔진 실체도 거기 있었다.
전부 되살렸지만(원본이 온전했다), 절차에 한 줄이 추가됐다.
디렉터리를 지우기 전에 심링크와 git 밖 자산을 훑는다.
1
2
3
# 지우려는 경로가 어디를 가리키는지, 누가 그것을 가리키는지
find . -maxdepth 3 -type l -exec ls -l {} \;
git status --ignored --short | head -50
--ignored가 핵심이다. 평소 git status는 이 파일들을 안 보여주는데, 그게 정확히 이 사고가 생기는 이유다.
경로 규약을 하나로 고정한다
1
{MODEL_REPO_HOST}/{MODEL_NAME}/1/model.plan
1
2
3
MODEL_REPO_HOST=/data/ai-project/service/srv/models # 호스트에서 본 경로
MODEL_REPO=/opt/models # 컨테이너 안에서 본 경로
MODEL_NAME=detection-v11
컴포즈가 바인드 마운트한다.
1
2
volumes:
- ${MODEL_REPO_HOST}:/opt/models:ro
바인드 마운트라는 것은 컨테이너가 자기 사본을 갖지 않는다는 뜻이다. 호스트에서 엔진을 지우면 컨테이너 안에서도 사라진다. “컨테이너는 살아 있으니 거기서 꺼내면 된다”가 성립하지 않는다. 엔진을 잃었을 때 이걸 몰라서 한참 헤맸다.
이름에 속지 않기
변수 이름이 TRITON_MODEL인데 Triton이 이 저장소를 서빙하지 않는 경우가 있었다. 워커가 자체 TensorRT 런타임으로 .plan을 직접 연다. 그래서 config.pbtxt가 필요 없는데, 옛 구조의 잔재로 파일이 남아 있었다.
이런 잔재가 위험한 이유는, 다음 사람이 그걸 계약으로 믿는다는 것이다. 새 모델을 추가하면서 config.pbtxt를 정성껏 작성하고, 그게 반영되지 않는 이유를 찾느라 시간을 쓴다. 아무도 안 읽는 설정 파일은 없는 것보다 나쁘다.
별개의 실제 Triton 저장소가 있다면 디렉터리를 분리해야 한다. --model-control-mode=none은 저장소의 모든 모델을 로드하므로 섞어두면 검출 엔진까지 올리려 든다.
엔진 굽기를 재현 가능하게
1
2
3
4
5
cp /path/to/training/output/best.onnx converter/models/model-v1.1.onnx
CONVERTER_GPUS=1,2,3 DYNAMIC=1 MIN_BATCH=1 OPT_BATCH=8 MAX_BATCH=16 \
./converter/build_engine.sh converter/models/model-v1.1.onnx fp16 \
srv/models/detection-v11/1/model.plan
몇 가지가 의도적이다.
저장소 밖 경로를 스크립트가 거부한다. ONNX를 저장소 안으로 복사하게 강제하면 “어떤 ONNX로 구웠나”가 파일시스템에 남는다. *.onnx는 커밋되지 않지만 적어도 그 서버에서는 추적된다.
CONVERTER_GPUS를 반드시 준다. 기본값 all은 GPU 0을 잡을 수 있는데, 공용 서버의 0번은 대개 다른 사람이 쓰고 있다(실측 11.3GB 점유). 엔진 빌드는 GPU를 통째로 쓰면서 7~8분 돈다. 남의 학습을 OOM으로 죽이는 방법치고는 너무 쉽다.
동적 배치 범위를 명시한다. MIN/OPT/MAX_BATCH를 안 주면 빌드된 엔진이 배치 1에서만 동작하는데, 이건 런타임에서 “배치 8을 넣었더니 모양이 안 맞는다”로 나타난다.
산출물 소유자 문제
빌드 컨테이너가 root로 돌면 출력 디렉터리가 root 소유로 생긴다. 그 안의 파일을 지우거나 바꾸려면 컨테이너를 거쳐야 한다.
1
2
docker run --rm -v "$PWD":/w -w /w --entrypoint rm \
<builder-image> -rf srv/models/detection-v11
이걸 몰라서 sudo rm을 쓰다가 다른 것까지 지우는 사고가 나기 쉽다. 빌드 스크립트에서 --user $(id -u):$(id -g)를 넘기는 것이 근본 해결이지만, 컨테이너 안에서 쓰기 권한이 필요한 임시 경로가 있으면 그것도 간단하지 않다.
자산을 되채우는 스크립트를 코드와 함께 둔다
핵심은 이거다. git 밖 자산은 관리하지 않는 게 아니라, git 안에 “되채우는 방법”을 두는 것으로 관리한다.
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env bash
# tools/fetch_assets.sh — 원본 경로가 살아 있으면 인자 없이 동작
set -euo pipefail
SRC_VPE="${1:-/data/yoloe-srv}"
DST_VPE="srv/vpe-models"
[ -d "$SRC_VPE" ] || { echo "원본 없음: $SRC_VPE" >&2; exit 1; }
mkdir -p "$DST_VPE"
rsync -a --info=progress2 "$SRC_VPE/" "$DST_VPE/"
echo "복사 완료. 검출 엔진은 converter/build_engine.sh 로 직접 굽는다."
이 스크립트가 없으면 자산 위치가 사람 머릿속에만 있다. 그리고 그 사람이 휴가 중일 때 서버가 죽는다.
마지막 줄처럼 스크립트가 담당하지 않는 부분을 명시하는 것도 중요하다. “이 스크립트를 돌렸는데 왜 안 되지”를 막는다.
체크리스트로 정리
새 서버에 배포할 때 확인하는 것들이다.
git status --ignored로 무엇이 추적 밖인지 확인- 심링크가 가리키는 대상이 존재하는지 확인 (
find . -type l -xtype l로 끊긴 링크 탐지) - 엔진 파일이 이 GPU 아키텍처에서 구워진 것인지 확인
- 바인드 마운트 경로가 호스트에 실제로 있는지 확인 (없으면 도커가 빈 디렉터리를 만든다)
- 자산 되채우기 스크립트를 실행하고, 그것이 커버하지 않는 항목을 수동 처리
네 번째가 특히 조용하다. 도커는 바인드 마운트 소스가 없으면 오류를 내는 대신 빈 디렉터리를 만들어준다. 그러면 컨테이너는 정상 기동하고, 모델 로드 시점에 “파일 없음”으로 죽는다. 원인이 두 단계 떨어져 있어서 찾기 어렵다.