13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[태그:] MLOps

  • [AI] AI 모델 클라우드 마이그레이션 체크리스트: 온프레미스 AI 전환 기준

    [AI] AI 모델 클라우드 마이그레이션 체크리스트: 온프레미스 AI 전환 기준

    AI 모델 클라우드 마이그레이션 체크리스트: 온프레미스 AI 전환 기준

    AI 모델 클라우드 마이그레이션을 검토할 때 많은 팀이 처음엔 GPU 인스턴스만 확보하면 된다고 생각합니다. 저도 예전엔 그렇게 봤는데, 실제 전환 프로젝트에 들어가면 GPU보다 먼저 터지는 문제는 데이터 경로, 모델 아티팩트 배포, 보안 경계, 준비 상태 검증이더라고요. 결국 이 작업은 서버 위치만 바꾸는 일이 아니라, 추론 요청이 들어와서 모델이 로드되고 결과가 저장되기까지의 흐름을 다시 설계하는 일에 가깝습니다.

    특히 AI 워크로드는 일반 웹 서비스보다 실패 양상이 더 복잡합니다. 애플리케이션 프로세스는 살아 있는데 모델 다운로드가 끝나지 않아 readiness가 실패할 수 있고, GPU는 한가한데 전처리 CPU가 막혀 지연이 튀는 경우도 흔합니다. 클라우드로 옮긴 뒤에도 입력 데이터가 온프레미스에 남아 있으면 왕복 지연과 전송 비용이 계속 쌓이죠. 그래서 이 글은 원론보다, 전환 회의와 전환 당일에 바로 써먹을 수 있는 체크 기준으로 정리해보겠습니다.

    AI 모델 클라우드 마이그레이션 전체 아키텍처 개요 이미지

    온프레미스 AI, 스토리지, 네트워크, 클라우드 추론 계층이 어떻게 연결되는지 한눈에 보여주는 개요 이미지입니다.

    1. 왜 AI 모델 클라우드 마이그레이션이 까다로운가

    일반 애플리케이션 이전은 보통 애플리케이션 바이너리와 데이터베이스를 중심으로 보면 됩니다. 그런데 AI 추론 서비스는 여기에 모델 파일, 토크나이저와 전처리 리소스, GPU 드라이버 호환성, 컨테이너 이미지 크기, 아티팩트 캐시, 배치와 실시간 트래픽의 공존이 더해집니다. 이 중 하나만 설계가 어긋나도 서비스는 떠 있지만 실제 요청은 느리거나 불안정해집니다.

    현장에서 자주 보는 실패는 크게 세 가지입니다. 첫째, 서비스는 클라우드에 올렸는데 입력 데이터와 피처 저장소는 사내망에 그대로 둬서 지연 시간 대부분을 네트워크 왕복에 써버리는 경우입니다. 둘째, 컨테이너 이미지는 잘 만들었는데 모델 아티팩트를 언제 어디서 받아오는지 기준이 없어 배포마다 시작 시간이 들쑥날쑥해지는 경우입니다. 셋째, 개발팀은 Kubernetes를 원하고 운영팀은 VM을 선호하는데 책임 경계를 정하지 않은 채 진행해서 장애가 나면 누구도 원인 구간을 단정하지 못하는 경우입니다.

    핵심은 기술 스택 이름이 아니라 실패 지점을 어디서 끊어 볼 수 있게 만들었느냐입니다. 모델 로딩 실패를 애플리케이션 로그에서만 보게 만들면 대응이 늦어집니다. 배포 시스템, 스토리지 접근, 네트워크 정책, 컨테이너 이벤트, readiness probe까지 각 단계에서 실패가 드러나야 운영이 됩니다. 제 경험상 마이그레이션이 잘된 팀은 최신 GPU를 쓰는 팀이 아니라, 실패를 빨리 국소화하는 팀이었습니다.

    2. AI 모델 클라우드 마이그레이션 전, 무엇을 옮기고 무엇을 남길까

    클라우드 AI 전환은 전부 이전하거나 전부 유지하는 식으로 가면 대개 비효율적입니다. 워크로드를 성격별로 나눠야 하거든요. 같은 모델이라도 실시간 API와 야간 배치의 최적 해법이 다를 수 있습니다.

    항목 온프레미스 유지가 유리한 경우 클라우드 이전이 유리한 경우 판단 포인트
    실시간 추론 API 내부 시스템 전용이고 입력 데이터가 사내망에서만 생성될 때 트래픽 변동이 크고 외부 서비스, CDN, API Gateway 연동이 많을 때 왕복 지연, 오토스케일링, 외부 노출 경로
    배치 추론 야간 고정 작업이고 유휴 GPU를 계획적으로 재활용할 수 있을 때 특정 기간에만 대량 실행이 몰리고 큐 길이가 자주 출렁일 때 실행 창 유연성, 큐 적체, 자원 점유 시간
    모델 학습 데이터 반출 제한이 강하고 장기 점유형 학습이 많을 때 짧은 기간 대규모 자원을 집중 투입해야 할 때 데이터 거버넌스, 체크포인트 저장 경로, 대역폭
    모델 저장소 폐쇄망 정책이 우선이고 외부 반출 심사가 까다로울 때 멀티 리전 배포, 버전 배포 자동화, 캐시 전략이 중요할 때 버전 전파 속도, 접근 통제, 캐시 일관성
    전처리/후처리 사내 DB, 파일서버, 내부 인증 체계와 강하게 결합돼 있을 때 API 기반으로 분리 가능하고 수평 확장이 자주 필요할 때 CPU 병목, 내부 연동 의존도, 서비스 분리 비용

    이 단계에서 꼭 문서화해두면 좋은 질문은 아래 다섯 가지입니다.

    • 모델 아티팩트는 이미지에 포함할지, 시작 시 다운로드할지, 볼륨으로 마운트할지
    • 입력 데이터는 클라우드에 복제할지, 전용 회선이나 프록시로 접근할지
    • GPU가 꼭 필요한 경로와 CPU로 충분한 경로를 분리했는지
    • 장애 시 온프레미스로 되돌리는 경로가 DNS 전환인지, 로드밸런서 가중치 조정인지, 배포 태그 롤백인지
    • 모델 버전 롤아웃과 인프라 롤아웃을 같은 절차로 묶을지, 분리할지

    실무적으로 정리하면 이렇습니다. 데이터가 사내망을 벗어나기 어렵고 지연 시간 요구가 빡빡하면 온프레미스나 하이브리드가 맞고, 트래픽 변동과 배포 빈도가 더 큰 문제라면 클라우드가 더 잘 맞습니다. 둘 다 애매하면 전체 이전보다 외부 노출 추론 API나 배치 작업부터 부분 이전이 안전합니다.

    3. AI 모델 클라우드 마이그레이션 사전 진단 체크리스트

    이 섹션은 실제 킥오프 회의에서 그대로 읽어도 됩니다. 저는 마이그레이션 전에 아래 항목이 하나라도 비어 있으면 일정을 바로 잡지 않습니다. 나중에 메꾸면 되겠지 싶어도, 보통은 막판에 가장 비싼 문제로 돌아오더라고요.

    1. 모델 목록 정리: 모델 이름, 버전, 프레임워크, 파일 크기, 로딩 경로, 토크나이저/전처리 리소스 위치, 의존 라이브러리 버전을 적습니다.
    2. 트래픽 패턴 확인: 실시간 API, 비동기 큐, 배치 실행을 분리해서 보고, 요청 급증 시점과 재시도 패턴이 있는지 확인합니다.
    3. 데이터 경로 파악: 요청 원천, 전처리 위치, 모델 호출 위치, 결과 저장소, 로그 전송 경로를 한 장의 흐름도로 그립니다.
    4. 보안 요구사항 정리: 네트워크 분리, TLS 종료 지점, Secret 주입 방식, 감사 로그 보존 위치를 정합니다.
    5. 운영 표준 확정: VM인지, 컨테이너인지, Kubernetes인지 결정하고, 배포 책임 팀을 명시합니다.
    6. 관측 지표 선정: p95 지연 시간, 오류율, 모델 로딩 시간, GPU 메모리 사용, Pod 재시작, 큐 적체, 스토리지 대기를 최소 기준으로 둡니다.
    7. 롤백 계획 수립: 어떤 이상 징후가 몇 분간 지속되면 되돌릴지, 누가 승인할지, 어떤 명령으로 되돌릴지 적습니다.
    8. 의존성 분리 검증: 코드 안에 절대 경로, 사내 DNS 이름, 로컬 마운트 경로, 특정 NIC 이름 같은 환경 종속값이 박혀 있지 않은지 확인합니다.

    제가 한 번 크게 헤맸던 사례도 딱 이 단계에서 걸렸어야 했습니다. 모델 파일은 컨테이너 시작 시 객체 저장소에서 내려받도록 바꿨는데, 토크나이저 사전 파일 경로는 예전 온프레미스 NFS 마운트 경로를 그대로 바라보고 있었거든요. 애플리케이션 로그에는 단순히 No such file or directory만 찍혀서 처음엔 이미지 문제로 봤고, 실제 원인은 설정 누락이었습니다. 모델 추론 실패는 모델 자체보다 주변 리소스 경로 누락에서 나는 경우가 생각보다 많습니다.

    사전 진단 단계에서 아래처럼 의존 파일을 한 번 훑어보면 이런 실수를 꽤 줄일 수 있습니다.

    find /opt/app -maxdepth 3 -type f \( -name "*.py" -o -name "*.yaml" -o -name "*.yml" -o -name "*.json" \) \
      -print0 | xargs -0 rg -n "/mnt/|/nfs/|/data/|\.internal|localhost|127\.0\.0\.1"

    이 명령은 코드와 설정 파일 안에 박혀 있는 환경 종속 경로, 내부 도메인, 로컬 참조를 빠르게 찾는 데 유용합니다. “이 정도는 나중에 고치면 되지” 하고 넘기면, 마이그레이션 막판에 가장 까다로운 형태로 돌아옵니다.

    4. 실전 구현 1: 현재 환경 계측과 병목 확인

    AI 모델 클라우드 마이그레이션 전에 먼저 해야 할 일은 현재 온프레미스 환경이 어디서 느린지 평균값이 아니라 병목 패턴으로 읽는 겁니다. 평균 CPU 사용률이 낮다고 안심하면 안 됩니다. 피크 시간에 CPU 전처리가 치솟는지, 디스크 대기가 늘어나는지, GPU 메모리 부족으로 컨테이너가 재시작하는지, 네트워크 인터페이스 하나에 트래픽이 몰리는지까지 봐야 합니다.

    초기에 많이 수집하는 명령은 아래 정도입니다.

    sar -u 1 5
    sar -n DEV 1 5
    iostat -x 1 5
    vmstat 1 5
    ss -ltnp
    df -h
    mount | rg -n "nfs|ceph|fuse|cifs"

    읽는 기준은 이렇게 가져가면 됩니다.

    • iostat -x에서 특정 디바이스의 await가 길고 대기열이 함께 늘면, 모델 파일 로딩이나 캐시 저장 과정에서 스토리지 병목이 있을 가능성이 큽니다. 다만 최신 SSD나 병렬 스토리지에선 %util만으로 포화 여부를 단정하긴 어렵습니다.
    • sar -n DEV에서 NIC 하나만 유난히 바쁘면 데이터 경로가 비대칭일 수 있습니다. 특히 모델 다운로드와 요청 처리 트래픽이 같은 인터페이스를 타면 전환 후 더 티가 납니다.
    • vmstat에서 swap 징후가 보이면, 모델 로딩 시 메모리 압박으로 초기화가 길어지거나 OOM 직전까지 가는 구조일 수 있습니다.
    • ss -ltnp로 실제 어떤 포트에 무엇이 바인딩되어 있는지 확인해두면, 클라우드 전환 후 readiness probe 경로와 포트 매핑 실수를 줄일 수 있습니다.
    • mount 결과에서 NFS나 원격 파일시스템 의존이 있으면, 그 경로를 클라우드에서 어떻게 대체할지 미리 정해야 합니다.

    GPU 워크로드라면 운영체제 지표만 보고 끝내면 아쉽습니다.

    nvidia-smi
    nvidia-smi dmon -s pucmem
    nvidia-smi --query-gpu=name,driver_version,memory.total,memory.used,utilization.gpu,utilization.memory --format=csv

    여기서 특히 봐야 할 건 GPU 사용률 하나가 아니라 GPU 사용률과 지연 시간의 관계입니다. GPU 사용률이 낮은데 응답이 느리면 대개 GPU가 핵심 원인은 아닙니다. 전처리 CPU, 네트워크 왕복, 스토리지 다운로드, 직렬화 구간을 먼저 의심하는 편이 맞습니다. 반대로 GPU 사용률과 메모리 사용률이 함께 치솟고 배포 직후 실패가 늘면, 컨테이너 메모리 제한이나 모델 샤딩 전략을 다시 봐야 합니다.

    현업에서 자주 놓치는 또 하나는 모델 시작 시간입니다. 서비스 지연만 재고 배포 시간을 안 재면, 오토스케일링이 필요한 순간에 새 Pod가 제때 준비되지 않습니다. 저는 아래처럼 컨테이너 이벤트와 readiness를 같이 봅니다.

    kubectl get pods -n ml -w
    kubectl describe pod -n ml <pod-name>
    kubectl logs -n ml <pod-name> --since=10m

    만약 이벤트에 이미지 풀, 볼륨 마운트, readiness probe 실패가 순서대로 찍히면 모델 자체보다 시작 절차가 병목인 경우가 많습니다. 초기화가 긴 서비스라면 readiness와 liveness만 둘 게 아니라 startupProbe까지 함께 검토하는 편이 더 안전합니다. 이거 하나로 불필요한 재시작 루프를 꽤 줄일 수 있거든요.

    AI 모델 클라우드 마이그레이션 전 온프레미스 AI 병목 점검 이미지

    마이그레이션 전 병목을 확인하는 장면을 시각화한 이미지입니다. GPU와 디스크, 네트워크 지표를 함께 보는 느낌이면 좋습니다.

    5. 실전 구현 2: 컨테이너와 설정 분리부터 정리하기

    온프레미스 AI 환경에서 흔히 보이는 상태가 “서버 한 대에 파이썬 가상환경, 모델 파일, 인증서, 임시 스크립트가 다 엉켜 있는 구조”입니다. 이 상태로 클라우드에 가면 재현성이 떨어지고, 장애가 나도 어느 레이어 문제인지 분리하기 어려워집니다. 그래서 저는 이럴 때 가장 먼저 이미지, 설정, 시크릿, 모델 아티팩트를 분리합니다.

    원칙은 단순합니다. 이미지에는 코드와 런타임만 넣고, 환경별 차이는 환경 변수와 Secret으로 분리하고, 모델 아티팩트는 별도 경로에서 가져오게 만듭니다. 모델을 이미지에 굽는 방식은 작은 모델이나 배포 빈도가 낮을 때는 편하지만, 이미지가 비대해지고 버전 교체가 잦아지면 배포 속도를 늦춥니다. 반대로 매번 시작 시 다운로드하게 하면 시작 시간이 길어질 수 있으니, 배포 빈도와 모델 크기를 같이 봐야 합니다.

    배포 방식 언제 유리한가 피해야 할 상황 주요 리스크
    모델을 이미지에 포함 모델 크기가 작고 배포 빈도가 낮을 때 버전 교체가 잦고 이미지 전파 시간이 길 때 이미지 비대화, 롤백 지연
    시작 시 객체 저장소에서 다운로드 버전 교체가 잦고 중앙 관리가 중요할 때 시작 시간이 엄격하거나 네트워크 변동이 큰 환경 Cold start 증가, 다운로드 실패
    공유 볼륨/PVC 마운트 같은 노드나 클러스터에서 여러 Pod가 재사용할 때 스토리지 성능이 불안정하거나 락 경합이 있을 때 I/O 병목, 마운트 실패

    Kubernetes 배포를 예로 들면, 최소한 아래 정도까지는 분리해두는 편이 좋습니다.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: inference-api
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: inference-api
      template:
        metadata:
          labels:
            app: inference-api
        spec:
          containers:
            - name: api
              image: registry.example.com/inference-api:1.0.0
              imagePullPolicy: IfNotPresent
              ports:
                - containerPort: 8080
              env:
                - name: MODEL_PATH
                  value: /models/current
                - name: LOG_LEVEL
                  value: info
              envFrom:
                - secretRef:
                    name: inference-api-secret
              readinessProbe:
                httpGet:
                  path: /ready
                  port: 8080
                initialDelaySeconds: 10
                periodSeconds: 5
                failureThreshold: 6
              livenessProbe:
                httpGet:
                  path: /healthz
                  port: 8080
                initialDelaySeconds: 30
                periodSeconds: 10
              startupProbe:
                httpGet:
                  path: /healthz
                  port: 8080
                failureThreshold: 30
                periodSeconds: 10
              volumeMounts:
                - name: model-volume
                  mountPath: /models
          volumes:
            - name: model-volume
              persistentVolumeClaim:
                claimName: model-pvc

    이 설정에서 중요한 건 화려한 기능이 아니라 네 가지입니다. 모델 경로를 코드에 하드코딩하지 않는 것, readiness와 liveness를 분리하는 것, 초기화가 길면 startupProbe를 두는 것, Secret을 이미지 밖으로 빼는 것입니다. readiness는 “트래픽을 받을 준비가 됐는지”, liveness는 “프로세스가 비정상 상태인지”를 보는 기준입니다. 초기화가 오래 걸리는 서비스에서 startupProbe 없이 liveness를 너무 일찍 때리면 재시작 루프로 빠질 수 있습니다.

    배포 태그도 latest보다는 고정 버전을 쓰는 편이 낫습니다. 이건 취향 문제가 아니라 롤백 속도 문제에 가깝습니다.

    kubectl apply -f deployment.yaml
    kubectl rollout status deployment/inference-api
    kubectl get pods -l app=inference-api
    kubectl logs deploy/inference-api --tail=100
    kubectl rollout history deployment/inference-api

    검증할 때는 Pod가 뜨는지만 보면 부족합니다. 로그에서 모델 로딩 완료 메시지, 외부 저장소 접근 성공, 포트 바인딩, readiness 통과를 함께 확인해야 합니다. 특히 애플리케이션이 첫 요청 직전에 모델을 lazy load하도록 짜여 있으면, rollout status가 끝나도 실제 서비스 시점에만 장애가 날 수 있습니다. 이 경우엔 사전 검증 요청을 명시적으로 날려 워밍업까지 확인하는 편이 안전합니다.

    컨테이너 이미지, 환경 변수, 스토리지 볼륨, 서비스 경로가 어떻게 분리되는지 보여주는 구성 다이어그램입니다.

    6. 네트워크와 보안: 늦게 보면 일정이 무너집니다

    클라우드 AI 전환에서 일정이 가장 자주 밀리는 구간은 성능 튜닝보다 보안 승인입니다. 온프레미스에서는 그냥 되던 내부 호출이, 클라우드로 오면 Ingress, Egress, DNS, 인증서, 프록시, NAT, 감사 로그 요건을 통과해야 합니다. 이걸 배포 직전에 맞추려 하면 예외 규칙만 늘고, 운영 설명 가능성은 오히려 떨어집니다.

    • Ingress와 내부 서비스 경로를 분리합니다. 외부 공개 API와 내부 관리 API를 같은 진입점에 두지 않는 편이 낫습니다.
    • Secret은 이미지에 넣지 말고 Secret 관리 기능이나 외부 저장소에서 주입합니다.
    • Egress 허용 대상을 도메인, 포트, 목적별로 문서화합니다. 모델 다운로드, 인증, 로그 전송, 메트릭 전송을 분리해서 적어야 합니다.
    • 감사 로그는 누가 모델 버전을 배포했고, 누가 Secret을 변경했고, 누가 네트워크 정책을 열었는지 추적 가능해야 합니다.

    제가 실제로 겪었던 전형적인 사고는 이렇습니다. 개발 환경에서는 잘 되던 모델 API가 운영 클러스터에서만 외부 인증 토큰 발급에 실패했습니다. 앱 로그에는 단순한 timeout처럼 보여서 처음엔 코드 문제로 봤죠. 원인은 운영 네트워크 정책에서 인증 엔드포인트로 나가는 Egress가 막혀 있던 것이었습니다. 이런 문제는 “애플리케이션이 떠 있느냐”보다 의존 대상까지 실제로 연결되느냐를 따로 검증해야 잡힙니다.

    배포 전 연결성 테스트는 아래처럼 나눠서 보는 편이 좋습니다.

    curl -vk https://example.internal.health/ready
    curl -vk https://auth.example.com/
    nslookup auth.example.com
    dig auth.example.com +short
    openssl s_client -connect auth.example.com:443 -servername auth.example.com </dev/null

    이 조합이 좋은 이유는 실패 지점을 분리해주기 때문입니다. nslookup이나 dig에서 이름 해석이 안 되면 DNS 문제고, curl -vk에서 연결 자체가 안 되면 라우팅이나 방화벽 문제일 가능성이 큽니다. openssl s_client 단계에서 깨지면 인증서 체인이나 SNI 관련 문제를 의심할 수 있습니다. 보안팀이나 네트워크팀과 이야기할 때도 “안 돼요”보다 “DNS는 되고 TLS handshake에서 끊깁니다”가 훨씬 빠릅니다.

    추가로, AI 추론 서비스는 외부 모델 저장소나 내부 객체 저장소에서 큰 파일을 가져오는 경우가 많습니다. 그래서 Egress를 한 번 열어놓고 끝낼 게 아니라, 어떤 Pod가 어떤 목적지로 나가야 하는지를 좁혀 적는 편이 좋습니다. 운영팀 입장에서는 이게 정책 관리의 시작점입니다.

    7. AI 모델 클라우드 마이그레이션 전환 당일 체크리스트와 롤백 기준

    마이그레이션 전략은 기술보다 절차가 더 중요할 때가 많습니다. 전환 당일에는 “한 번에 바꾸고 보자”보다 단계적으로 바꾸는 편이 낫습니다. 제가 자주 쓰는 순서는 대체로 아래와 같습니다.

    1. 기존 온프레미스 서비스의 버전, 헬스 상태, 주요 로그 패턴을 기록합니다.
    2. 클라우드 환경에서 동일 입력으로 응답 형식과 로딩 시간을 사전 검증합니다.
    3. 읽기 전용 트래픽이나 일부 가중치만 새 환경으로 보냅니다.
    4. 오류 로그, 지연 시간, 모델 로딩 상태, 외부 연동 성공 여부를 집중 관찰합니다.
    5. 이상 징후가 없으면 트래픽 비율을 점진적으로 올립니다.
    6. 문제가 생기면 DNS, 로드밸런서, 서비스 라우팅 기준으로 즉시 롤백합니다.

    여기서 핵심은 “언제 되돌릴지”를 미리 적어두는 겁니다. 현장에서는 기술적 판단보다 심리적 지연이 더 무섭거든요. 그래서 애매한 표현보다 징후 중심 기준을 쓰는 편이 낫습니다.

    • readiness probe 실패가 연속으로 발생한다
    • 모델 로딩 실패 로그가 반복된다
    • 인증, 저장소 접근, 외부 API 연동 timeout이 지속된다
    • 온프레미스 대비 명확한 성능 저하가 보이는데 원인을 즉시 분리하지 못한다
    • Pod 재시작이나 노드 재스케줄링이 예상보다 잦다

    실무에서는 숫자 하나로 모든 걸 결정하기보다, 정상 로그와 비정상 로그의 차이를 미리 캡처해두는 편이 훨씬 도움이 됩니다. 예를 들어 정상일 때는 모델 로딩 완료, tokenizer 초기화 완료, readiness 성공이 순서대로 나오고, 비정상일 때는 다운로드 재시도나 mount 실패가 먼저 보인다면 전환 당일에 대시보드보다 로그 한 줄이 더 빨리 방향을 줍니다.

    가중치 기반 전환을 쓰는 환경이라면 전체 컷오버보다 부분 전환이 낫습니다. 이유는 단순합니다. AI 추론 서비스는 첫 요청 시 캐시가 비어 있는 경우가 많아서, 일부 트래픽으로 시작해야 cold path를 실제로 밟아볼 수 있기 때문입니다. 저는 이런 상황이면 전환 초반엔 새 환경을 정상 동작 확인용으로 보고, 안정화가 끝난 뒤에야 확장 수단으로 취급합니다.

    AI 모델 클라우드 마이그레이션 후 검증 대시보드 이미지

    전환 직후 검증 단계에서 운영자가 어떤 화면을 중점적으로 보는지 보여주는 대시보드형 이미지입니다.

    8. 검증과 결과 해석: 성공 여부는 이렇게 봅니다

    AI 모델 클라우드 마이그레이션이 끝났다고 판단하려면 서버가 켜져 있다는 사실만으로는 부족합니다. 적어도 아래 네 축이 같이 맞아야 합니다.

    • 기능 검증: 같은 입력에 대해 기대한 형식의 응답이 나오는가
    • 운영 검증: 로그 수집, 알림, 접근 제어, 배포 이력이 정상 동작하는가
    • 성능 검증: 병목이 GPU인지, CPU 전처리인지, 네트워크인지, 스토리지인지 구분 가능한가
    • 복구 검증: 재배포와 롤백이 문서대로 실제 수행되는가

    판단 기준도 조금 더 실무적으로 가져가야 합니다.

    • 응답은 정상이지만 시작 시간이 과하게 길다면 모델 아티팩트 다운로드 경로, 이미지 크기, PVC 마운트 지연을 먼저 봅니다.
    • GPU 사용률이 낮은데 지연이 높다면 CPU 전처리, 직렬화, 네트워크 왕복을 먼저 의심하는 편이 맞습니다.
    • Pod 재시작이 잦다면 메모리 제한, readiness 경로, 파일 마운트 실패, liveness 기준 과민 설정을 차례로 봅니다.
    • 특정 시간대에만 문제가 생기면 배치와 실시간 추론이 같은 노드 풀이나 같은 스토리지를 공유하는지 확인합니다.
    • 첫 요청만 느리고 이후는 괜찮다면 lazy load, 캐시 미스, DNS 캐시, 원격 다운로드를 먼저 의심합니다.

    여기서 중요한 건 수치 그 자체보다 설명 가능성입니다. 운영자가 “왜 느린지”를 두세 단계 안에 좁힐 수 있으면 성공에 가깝고, 장애가 나도 어느 레이어를 먼저 봐야 할지 모르면 아직 미완성입니다. 화려한 대시보드보다 원인 추적 경로가 짧은 구조가 더 값집니다.

    실제로 재현 가능한 시나리오를 하나 들면 이렇습니다. 평소엔 응답이 괜찮다가 배포 직후 몇 분 동안만 지연이 튀는 서비스가 있었습니다. GPU는 여유가 있었고 애플리케이션도 살아 있었습니다. 원인은 새 Pod가 올라올 때마다 모델 파일을 원격 저장소에서 다시 받아오고, 동시에 전처리 사전 파일도 초기화하느라 readiness 통과 직전까지 시간이 밀리던 구조였습니다. 이 경우 GPU 교체는 답이 아니고, 모델 캐시 전략과 readiness 기준을 손보는 게 답입니다.

    9. 자주 묻는 질문과 현업 권고

    온프레미스 AI를 전부 클라우드로 옮겨야 할까요?

    그럴 필요는 없습니다. 데이터 반출 제한이 강하거나 내부 시스템 전용이라면 일부는 남기는 게 맞습니다. 특히 학습 데이터, 민감 로그, 내부 전처리 파이프라인은 남기고, 외부 공개형 추론 API나 탄력성이 필요한 배치만 클라우드로 분리하는 구성이 현실적입니다.

    Kubernetes가 꼭 필요할까요?

    작은 팀이고 모델 수가 적고 변경 빈도가 낮다면 처음부터 복잡도를 올릴 필요는 없습니다. 다만 모델 버전이 자주 바뀌고, 환경이 늘고, 롤백 속도가 중요해지면 컨테이너 오케스트레이션이 운영 피로도를 줄여줍니다. 제 기준은 단순합니다. 서비스가 한두 개이고 담당자가 고정돼 있으면 VM도 가능하지만, 버전 수와 팀 수가 늘기 시작하면 Kubernetes 쪽이 결국 덜 아픕니다.

    비용보다 먼저 볼 것은 뭔가요?

    데이터 경로와 운영 절차입니다. 비용은 나중에 줄일 수 있어도, 데이터 왕복 구조와 롤백 체계가 꼬이면 되돌리기가 어렵습니다. 특히 모델은 클라우드에 있고 데이터는 온프레미스에 남아 있는 상태를 아무 생각 없이 만들면, 지연과 운영 복잡도가 같이 올라갑니다.

    이럴 땐 어떤 선택이 맞을까요?

    명확하게 정리하면 이렇습니다. 내부 데이터 의존이 강하고 지연에 민감하면 하이브리드가 맞고, 배포 속도와 탄력성이 더 중요하면 클라우드 이전이 더 잘 맞습니다. 작은 팀이 첫 전환을 한다면 전부 옮기기보다 배치 또는 외부 노출 API부터 시작하는 편이 안전합니다. 반대로 이미 모델 버전이 자주 바뀌고 롤백이 잦다면, 미루지 말고 컨테이너화와 설정 분리부터 정리한 뒤 클라우드로 가는 게 낫습니다.

    AI 모델 클라우드 마이그레이션 선택 기준 요약 이미지

    상황별 권장 전략을 빠르게 비교할 수 있는 요약 인포그래픽입니다.

    마무리: 이런 경우엔 이렇게 가시면 됩니다

    온프레미스 AI 환경이 이미 안정적이고 데이터가 사내망에 깊게 묶여 있다면, 무리해서 전부 옮기지 않는 편이 좋습니다. 이 경우엔 배치 추론이나 외부 공개 API부터 부분 이전이 잘 맞습니다. 반대로 트래픽 변동이 크고 모델 배포 주기가 빠르며, 운영팀이 버전 롤백과 오토스케일링에 자주 시달린다면 클라우드 쪽이 더 유리합니다.

    한 줄로 줄이면 이렇습니다. AI 모델 클라우드 마이그레이션은 GPU 이전 프로젝트가 아니라 운영 모델 재설계 프로젝트입니다. 데이터 경로가 복잡하면 하이브리드로 시작하고, 출시 속도가 우선이면 컨테이너와 설정 분리부터 끝내고 가는 편이 안전합니다. 무엇부터 할지 애매하다면 제일 먼저 계측과 의존성 탐색부터 해보세요. 이 순서가 생각보다 많이 살려줍니다.

    실무적으로는 이렇게 판단하면 됩니다. 이럴 땐 A: 사내 데이터 의존이 강하고 보안 경계가 복잡하면 하이브리드. 저럴 땐 B: 모델 버전 변경이 잦고 외부 트래픽 변동이 크면 클라우드 이전. 둘 다 애매하면 C: 전환보다 먼저 현재 병목과 숨은 의존성을 계측. 관련 글로 AI 인프라 구축 체크리스트나 모델 배포 전략 가이드를 함께 묶어두면 내부 링크 구조와 SEO 흐름도 더 좋아집니다.

  • [AI] MLflow MLOps 실험 추적 체크리스트와 운영 베스트 프랙티스

    [AI] MLflow MLOps 실험 추적 체크리스트와 운영 베스트 프랙티스

    MLflow MLOps 실험 추적 체크리스트와 운영 베스트 프랙티스

    MLflow MLOps를 붙일 때 진짜 어려운 건 설치 자체보다 실험 기록을 나중에도 믿을 수 있게 만드는 일이더라고요. 실험은 돌아갔는데 어떤 데이터 버전으로, 어떤 코드 커밋에서, 누가, 왜 돌렸는지 안 남으면 그 run은 숫자만 남은 흔적에 가깝습니다. 저도 초반엔 UI만 뜨면 된다고 생각했는데, 팀 협업이 시작되자 바로 문제가 터졌습니다. run은 많은데 배포 후보는 안 보이고, 모델은 등록돼 있는데 검증 사유가 없고, artifact는 있는데 다시 못 읽는 식이었죠.

    그래서 이 글은 MLflow 사용법을 나열하는 글이 아니라 실험 추적 체계를 망치지 않기 위한 운영 체크리스트에 집중합니다. 특히 아래 세 가지를 분리해서 보셔야 합니다. 이 구분이 안 되면 실험 관리가 금방 꼬이거든요.

    • 메타데이터: param, metric, tag, run 상태가 어디에 저장되는가
    • 대용량 산출물: 모델 파일, plot, 리포트, 샘플 입력이 어디에 저장되는가
    • 승격 판단: 어떤 모델이 단순히 기록된 모델이고, 어떤 모델이 배포 가능한 모델인가

    이 셋을 한 덩어리로 보면 운영이 꼬입니다. 반대로 분리해서 설계하면, MLflow MLOps 워크플로우가 꽤 단단해집니다. 실험 관리 기준만 잘 잡아도 나중에 UI에서 헤매는 시간이 확 줄어들더라고요.

    MLflow MLOps 실험 추적 아키텍처 개요 이미지

    MLflow Tracking Server, artifact storage, model registry, training job 흐름을 한눈에 보여주는 개요 이미지입니다.

    MLflow MLOps 시작 전 먼저 볼 체크리스트

    제가 실무에서 먼저 확인하는 건 UI가 아니라 규칙입니다. 아래 항목이 빠져 있으면, 서버를 잘 띄워도 몇 주 뒤엔 실험 관리가 아니라 실험 발굴 작업을 하게 됩니다.

    • Tracking URI를 모든 학습 작업이 같은 값으로 사용하도록 강제했는가
    • Experiment 이름에 프로젝트, 작업 종류, 환경이 들어가는가
    • Artifact 저장소가 서버 로컬 디스크인지, S3/GCS/Azure Blob 같은 공용 스토리지인지 합의됐는가
    • Run tag에 최소한 git_sha, data_version, owner, purpose가 남는가
    • 등록 기준이 "최고 점수"인지, "검증 통과 + 설명 작성"인지 명확한가
    • 모델 승격 방식을 stages보다 alias/tag 중심으로 설계했는가
    • 접근 제어를 네트워크 레벨, 리버스 프록시, 또는 MLflow 인증 기능 중 무엇으로 처리할지 정했는가
    • 실패 run 보존 정책이 있는가, 아니면 좋은 결과만 남기고 나쁜 기록을 지우는가

    여기서 특히 중요한 판단이 하나 있습니다. 개인 실험 환경과 팀 공용 환경을 섞지 마세요. 혼자 노트북에서 돌리는 추적과 팀 공용 추적을 같은 URI 체계 없이 섞어버리면, 나중에 "왜 내 run은 UI에 없지?" 같은 문제가 너무 쉽게 생깁니다. 그때는 도구 문제가 아니라 운영 모델이 애매했던 겁니다.

    MLflow MLOps 핵심 개념, 이 4개만 구분하시면 됩니다

    초반에 많이 헷갈리는 부분이 Tracking, Artifact, Registry, Deployment를 한 기능처럼 생각하는 겁니다. 실제로는 성격이 다릅니다. 저는 아래처럼 분리해서 설명하는 편입니다.

    구성 요소 무엇을 저장하나 언제 중요해지나 자주 터지는 실패 모드 권장 판단 기준
    Tracking run, param, metric, tag, 상태 여러 실험을 비교할 때 같은 코드인데 run 이름과 tag 규칙이 제각각임 검색 가능한 메타데이터가 30초 안에 보여야 합니다
    Artifact Store 모델 파일, 이미지, 리포트, input example 재현과 검증이 필요할 때 run은 보이는데 artifact 다운로드가 실패함 학습 노드가 아니라 서버 또는 공용 스토리지가 파일의 기준점이어야 합니다
    Model Registry 모델 버전, 설명, alias, tag 배포 후보가 2개 이상 생길 때 버전은 늘어나는데 어떤 버전이 운영용인지 모름 버전 번호보다 alias와 승인 상태 태그를 믿는 편이 낫습니다
    Serving/Deployment 실제 추론 엔드포인트와 연결된 버전 정보 운영 반영과 롤백 시 서빙 중인 모델과 Registry가 따로 놈 배포 코드가 models:/name@alias를 보게 해야 합니다

    여기서 하나 짚고 갈 점이 있습니다. Model Stages는 MLflow 2.9.0부터 deprecated입니다. 예전 글처럼 Production, Staging만 믿고 설계하면 금방 한계가 보입니다. 요즘은 model version alias와 tag를 중심으로 보는 쪽이 더 낫습니다. 저는 운영 코드에는 @champion, 검증 중인 후보에는 validation_status=pending 같은 태그를 붙이는 방식을 권합니다.

    혹시 이런 경험 있으신가요? 성능이 제일 좋던 run은 찾았는데, 그 모델이 실제 배포 가능한 버전인지 아닌지 판단이 안 되는 경우요. 그건 Tracking은 했는데 Registry 정책이 비어 있었던 겁니다. 숫자를 남긴 것과, 의사결정을 남긴 것은 다릅니다.

    실험 관리 기준부터 정해야 MLflow MLOps가 굴러갑니다

    실험 추적이 망가지는 패턴은 거의 비슷합니다. 사람마다 run 이름이 다르고, 어떤 사람은 tag를 남기고 어떤 사람은 안 남기고, 어떤 사람은 Registry에 바로 등록하고 어떤 사람은 artifact만 남깁니다. 이걸 막으려면 최소 운영 규칙을 코드 수준에서 강제해야 합니다.

    1. Experiment 이름: 프로젝트-태스크-환경 형식으로 고정합니다. 예: fraud-detection-train-dev
    2. Run name: 모델 종류와 핵심 파라미터가 드러나게 둡니다. 예: xgb-md6-lr0.05-seed42
    3. 필수 tag: git_sha, data_version, owner, purpose, pipeline는 무조건 남깁니다
    4. Artifact 구조: model/, plots/, reports/, samples/처럼 목적별로 분리합니다
    5. 등록 기준: metric 1개로 자동 등록하지 말고, 검증 통과 여부와 설명 입력까지 포함합니다
    6. 배포 참조 방식: 버전 번호가 아니라 alias 기반으로 읽게 합니다. 그래야 운영 코드 수정 없이 교체할 수 있습니다

    저는 여기서 등록 조건과 승격 조건을 꼭 나눕니다. 등록은 "비교 가능한 후보를 남긴다"에 가깝고, 승격은 "이 버전에 트래픽을 태워도 된다"에 가깝습니다. 둘을 합치면 점수 하나 높은 실험이 그대로 운영 모델이 되는 사고가 납니다.

    그리고 metric 자동 등록은 생각보다 위험합니다. 예를 들어 검증셋 누수가 있거나, 데이터 전처리가 우연히 섞여 숫자만 좋아졌을 수 있거든요. 그래서 저는 최소한 아래 중 둘 이상이 충족돼야 등록 후보로 봅니다.

    • 핵심 metric이 베이스라인보다 명확히 개선됨
    • 데이터 버전과 코드 커밋이 명시돼 있음
    • input signature와 input example이 함께 저장돼 있음
    • 검증 리포트 artifact가 같이 올라감
    • 설명(description) 또는 모델 버전 태그에 승인 맥락이 남아 있음

    실전 구현 1: MLflow Tracking Server를 제대로 띄우기

    처음 시작은 mlflow ui로 많이 합니다. 개인 노트북에서 보는 용도로는 괜찮습니다. 다만 팀이 붙는 순간에는 서버 프로세스, backend store, artifact 경로를 명시하는 편이 훨씬 안전합니다. 애매하게 두면 나중에 어디에 저장됐는지부터 찾게 되더라고요.

    빠르게 시작하는 최소 구성은 아래처럼 갈 수 있습니다. 여기서 포인트는 기본값에 기대지 말고 의도를 코드와 명령어에 남기는 겁니다. 특히 MLflow 공식 문서 기준으로 MLflow 3.7.0부터 기본 tracking backend가 file store 중심에서 SQLite 중심으로 바뀌었기 때문에, 더더욱 명시적으로 적는 편이 덜 헷갈립니다.

    mkdir -p /srv/mlflow/artifacts
    cd /srv/mlflow
    
    python -m venv .venv
    source .venv/bin/activate
    pip install mlflow
    
    mlflow server \
      --backend-store-uri sqlite:////srv/mlflow/mlflow.db \
      --default-artifact-root file:///srv/mlflow/artifacts \
      --host 0.0.0.0 \
      --port 5000

    이 구성은 개인 실험이나 1~2명 PoC까지는 충분합니다. 다만 운영을 조금이라도 염두에 둔다면 아래 옵션 의미는 꼭 이해하고 넘어가세요.

    • --backend-store-uri: run 메타데이터 저장소입니다. 검색, 비교, tag 조회 속도와 안정성에 직접 영향 줍니다
    • --default-artifact-root: 새 experiment의 artifact 기본 위치입니다. 이미 만들어진 experiment에는 소급 적용되지 않습니다
    • --serve-artifacts 또는 --artifacts-destination: artifact를 서버가 프록시할지, 원격 저장소와 어떻게 연결할지 결정합니다
    • --registry-store-uri: Registry 저장소를 backend store와 분리할 때 명시합니다. 작게 시작할 땐 같은 DB도 괜찮습니다

    조금 더 운영에 가깝게 가려면 PostgreSQL과 S3 계열 스토리지를 붙이는 구성이 낫습니다. 제 경험상 이 시점의 분기점은 동시 사용자 수보다 artifact를 누가 어떤 네트워크에서 읽을 건가예요. 브라우저와 작업 노드가 스토리지에 직접 접근 가능한 환경이면 직접 다운로드도 괜찮고, 그렇지 않으면 서버 프록시가 더 단순합니다.

    export AWS_ACCESS_KEY_ID="YOUR_ACCESS_KEY"
    export AWS_SECRET_ACCESS_KEY="YOUR_SECRET_KEY"
    export AWS_DEFAULT_REGION="ap-northeast-2"
    
    mlflow server \
      --backend-store-uri postgresql://mlflow:[email protected]:5432/mlflow \
      --artifacts-destination s3://company-mlflow-artifacts \
      --serve-artifacts \
      --host 0.0.0.0 \
      --port 5000 \
      --allowed-hosts "mlflow.example.com" \
      --cors-allowed-origins "https://mlops.example.com"

    이 설정에서 중요한 트레이드오프는 이렇습니다.

    선택지 언제 권하나 장점 주의할 점
    로컬 파일 + SQLite 개인 실험, 짧은 PoC 설치가 빠르고 디버깅이 단순함 동시성, 백업, 공유 접근에서 금방 한계가 옵니다
    PostgreSQL + 서버 프록시 artifact 팀 공용, 내부망 위주 클라이언트 설정이 단순하고 권한 통제가 쉬움 대용량 artifact 트래픽이 서버를 통과해 병목이 될 수 있습니다
    PostgreSQL + 원격 object storage 직접 접근 대용량 모델, 다수 사용자 서버 부하를 줄이기 좋음 브라우저와 작업 노드가 스토리지에 직접 접근 가능해야 합니다

    서비스로 굴릴 거면 프로세스 생명주기도 정리하셔야 합니다. systemd로 묶어두면 재시작과 장애 복구가 한결 낫습니다.

    [Unit]
    Description=MLflow Tracking Server
    After=network.target
    
    [Service]
    User=mlflow
    WorkingDirectory=/srv/mlflow
    Environment=MLFLOW_FLASK_SERVER_SECRET_KEY=replace-with-random-secret
    ExecStart=/srv/mlflow/.venv/bin/mlflow server \
      --backend-store-uri postgresql://mlflow:[email protected]:5432/mlflow \
      --artifacts-destination s3://company-mlflow-artifacts \
      --serve-artifacts \
      --host 0.0.0.0 \
      --port 5000
    Restart=on-failure
    RestartSec=5
    
    [Install]
    WantedBy=multi-user.target

    보안 쪽도 한 줄로 넘기면 안 됩니다. 예전엔 다들 NGINX 뒤에 두는 정도로 끝냈는데, 지금은 MLflow가 기본 인증 기능도 제공해서 선택지가 늘었습니다. 내부 테스트라면 프록시 뒤에서 Basic Auth만 걸어도 충분할 때가 있고, 여러 팀이 같이 쓰는 환경이면 내장 auth나 별도 SSO 연동을 같이 검토하는 편이 낫습니다.

    pip install 'mlflow[auth]'
    export MLFLOW_FLASK_SERVER_SECRET_KEY='replace-with-random-secret'
    mlflow server --app-name basic-auth --host 0.0.0.0 --port 5000

    제 판단은 이렇습니다. 사내 단일 팀이면 리버스 프록시 + TLS + 네트워크 제한으로 시작해도 됩니다. 여러 조직이 한 서버를 공유하면 접근 권한을 MLflow 자원 단위로 관리할 수 있는 쪽이 운영이 덜 아픕니다.

    Tracking Server, backend store, artifact root가 각각 어떤 역할인지 구분해서 보여주는 구성 이미지입니다.

    실전 구현 2: 코드에서 MLflow 사용법을 일관되게 강제하기

    서버보다 더 중요하다고 느끼는 건 학습 코드 템플릿입니다. 팀원마다 로그 방식이 다르면 UI는 몇 주 안에 금방 더러워집니다. 저는 아예 "기록 안 하면 실험으로 인정하지 않는다"는 쪽으로 갑니다. 이거 진짜 편하더라고요. 기준이 한 번 잡히면 리뷰도 빨라집니다.

    아래 예시는 최소 템플릿입니다. 포인트는 tracking URI 지정, 실험 이름 고정, 필수 tag 기록, signature와 input example 저장, 등록 이름 부여입니다.

    import os
    import mlflow
    from mlflow.models import infer_signature
    from sklearn.datasets import load_iris
    from sklearn.ensemble import RandomForestClassifier
    from sklearn.metrics import accuracy_score
    from sklearn.model_selection import train_test_split
    
    TRACKING_URI = os.getenv("MLFLOW_TRACKING_URI", "http://127.0.0.1:5000")
    EXPERIMENT_NAME = os.getenv("MLFLOW_EXPERIMENT_NAME", "iris-train-dev")
    REGISTERED_MODEL_NAME = os.getenv("MLFLOW_REGISTERED_MODEL", "dev.ml_team.iris-rf")
    
    mlflow.set_tracking_uri(TRACKING_URI)
    mlflow.set_experiment(EXPERIMENT_NAME)
    
    X, y = load_iris(return_X_y=True)
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42
    )
    
    params = {
        "n_estimators": 100,
        "max_depth": 4,
        "random_state": 42,
    }
    
    with mlflow.start_run(run_name="rf-md4-seed42"):
        mlflow.log_params(params)
        mlflow.set_tags({
            "owner": os.getenv("USER", "unknown"),
            "purpose": "baseline",
            "data_version": os.getenv("DATA_VERSION", "iris_builtin_v1"),
            "git_sha": os.getenv("GIT_COMMIT", "unknown"),
            "pipeline": os.getenv("PIPELINE_NAME", "manual-train"),
        })
    
        model = RandomForestClassifier(**params)
        model.fit(X_train, y_train)
        preds = model.predict(X_test)
        acc = accuracy_score(y_test, preds)
    
        mlflow.log_metric("accuracy", acc)
    
        signature = infer_signature(X_train, model.predict(X_train))
        mlflow.sklearn.log_model(
            sk_model=model,
            artifact_path="model",
            signature=signature,
            input_example=X_train[:2],
            registered_model_name=REGISTERED_MODEL_NAME,
            await_registration_for=0,
        )

    여기서 특히 중요한 건 세 가지입니다. git_sha가 없으면 코드 재현성이 거의 사라지고, data_version이 없으면 metric 비교가 절반만 유효해집니다. 그리고 signature와 input_example이 없으면 서빙 시 입력 스키마 문제를 늦게 발견하는 경우가 많습니다.

    다만 모든 실험에서 바로 등록까지 가는 구조는 저는 권하지 않습니다. 탐색 단계에선 Registry가 후보 저장소가 아니라 쓰레기장처럼 되기 쉽거든요. 아래처럼 성능 기준을 만족한 경우에만 등록하게 분기하는 편이 운영상 훨씬 낫습니다.

    import mlflow
    from mlflow import MlflowClient
    
    client = MlflowClient()
    threshold = 0.90
    
    with mlflow.start_run(run_name="rf-candidate") as run:
        # ... train and evaluate
        score = 0.95
        mlflow.log_metric("accuracy", score)
    
        model_info = mlflow.sklearn.log_model(
            sk_model=model,
            artifact_path="model",
            signature=signature,
            input_example=X_train[:2],
        )
    
        if score >= threshold:
            mv = mlflow.register_model(model_uri=model_info.model_uri, name="dev.ml_team.iris-rf")
            client.set_model_version_tag("dev.ml_team.iris-rf", mv.version, "validation_status", "pending")
            client.set_model_version_tag("dev.ml_team.iris-rf", mv.version, "source_run_id", run.info.run_id)

    실무에선 이 분기가 꽤 중요합니다. 모든 run을 다 등록하면 Registry는 비교용 데이터베이스가 아니라 잡동사니 서랍이 됩니다. Registry는 후보군의 저장소여야지, raw experiment dump가 되면 안 됩니다.

    CLI에서 실행한다면 환경 변수도 템플릿에 포함시키세요. 사람 손으로 입력하면 늘 하나씩 빠집니다.

    export MLFLOW_TRACKING_URI=http://127.0.0.1:5000
    export MLFLOW_EXPERIMENT_NAME=fraud-detection-train-dev
    export MLFLOW_REGISTERED_MODEL=dev.ml_team.fraud_xgb
    export DATA_VERSION=2026-08-raw-v3
    export GIT_COMMIT=$(git rev-parse --short HEAD)
    python train.py

    이 설정 없이 실행했는데 현재 디렉터리에 mlruns/ 폴더가 생겼다면 거의 확실하게 Tracking URI 적용이 안 된 겁니다. 이건 제가 제일 먼저 보는 신호입니다. UI에 안 보이는 run을 한참 찾기 전에, 로컬에 mlruns/가 생겼는지부터 확인하세요.

    MLflow 모델 레지스트리와 승격 기준, 여기서 운영 냄새가 납니다

    Registry는 단순히 모델 파일 버전을 쌓는 곳이 아닙니다. 누가 어떤 근거로 이 버전을 다음 단계로 넘겼는지를 남기는 곳에 가깝습니다. 그래서 저는 예전의 stage 중심 설계보다 alias/tag 중심 설계를 더 권합니다.

    • 등록 전 확인: 학습 코드 커밋, 데이터 버전, 핵심 metric, artifact 저장, signature 존재 여부
    • 승인 보류 상태: validation_status=pending 같은 모델 버전 태그 부여
    • 검증 통과 상태: validation_status=approved, 검증 리포트 링크 또는 설명 기록
    • 배포 참조: 운영 코드는 models:/prod.ml_team.fraud_xgb@champion 같은 alias를 읽도록 구성
    • 롤백 준비: 직전 배포 버전에 previous_champion alias를 유지하거나 버전 태그를 남김

    이걸 stages로 해도 되지 않느냐고 많이 물으시는데, 새 글에서는 stages를 중심축으로 잡지 않는 편이 맞습니다. 이유는 간단합니다. 단계 이름이 고정되면 실제 운영 흐름을 충분히 표현하기 어렵기 때문이죠. A/B 테스트 후보, 지역별 배포 후보, 오프라인 승인 완료 상태 같은 걸 표현하려면 alias/tag가 훨씬 유연합니다.

    예를 들어 저는 이런 식으로 씁니다.

    from mlflow import MlflowClient
    
    client = MlflowClient()
    model_name = "prod.ml_team.fraud_xgb"
    version = 12
    
    client.set_model_version_tag(model_name, version, "validation_status", "approved")
    client.set_model_version_tag(model_name, version, "approved_by", "ml-reviewer")
    client.set_registered_model_alias(model_name, "champion", version)
    client.set_registered_model_alias(model_name, "shadow", 13)

    이렇게 해두면 서빙 시스템은 @champion만 보면 되고, 실험 시스템은 shadow나 validation_status를 보면서 다음 후보를 준비할 수 있습니다. 운영 코드와 실험 코드가 느슨하게 분리되는 거죠. 이 설계는 생각보다 큽니다. 배포 스크립트를 매번 수정하지 않아도 되니까요.

    ⚠️ 트러블슈팅: 실험은 보이는데 모델 파일이 안 보이는 상황

    이 문제는 꽤 흔하고, 원인도 생각보다 선명합니다. backend store와 artifact store의 기준점이 다를 때 생깁니다. 메타데이터는 DB에 잘 남는데 파일 경로가 서버 기준으로 일관되지 않으면, UI에서는 run이 보이는데 artifact 탭만 깨집니다.

    재현 시나리오를 하나 들어보겠습니다.

    1. 개발자 A가 자신의 서버에서 --default-artifact-root file:///home/a/mlartifacts로 Tracking Server를 띄웁니다.
    2. 개발자 B가 같은 Tracking URI로 실험을 기록합니다.
    3. run 메타데이터는 정상 저장되지만, artifact 저장 위치와 접근 주체가 서버 기준으로 설계되지 않아 다운로드가 실패합니다.

    이 상황에서 핵심은 누가 파일을 쓰고 누가 파일을 읽는가를 분리해서 보는 겁니다. 초보 단계에선 학습 노드가 파일을 쓴다고 생각하기 쉽지만, 실제 운영에서는 서버가 관리 가능한 위치 또는 공용 object storage가 기준이어야 합니다.

    제가 점검할 때는 아래 순서로 갑니다.

    • 1단계: run 상세에서 param, metric, tag가 보이는지 확인합니다. 보이면 backend store는 대체로 살아 있습니다
    • 2단계: artifact 탭만 비어 있거나 다운로드가 실패하면 artifact root 설계를 의심합니다
    • 3단계: 새 experiment 생성 시점에 어떤 artifact_location이 박혔는지 확인합니다. 기존 experiment는 나중에 --default-artifact-root를 바꿔도 자동 수정되지 않습니다
    • 4단계: 서버 프로세스 사용자와 스토리지 권한을 확인합니다. 로컬 디스크면 쓰기 권한, S3/GCS면 자격 증명과 네트워크 경로를 봅니다
    • 5단계: 원격 저장소 직접 다운로드를 쓴다면 브라우저가 해당 스토리지 엔드포인트에 접근 가능한지도 봅니다

    판단 기준도 같이 가져가시면 좋습니다.

    증상 가장 의심할 원인 우선 확인할 것
    run 목록은 보이는데 artifact만 안 열림 artifact store 경로/권한 불일치 --default-artifact-root, experiment의 artifact_location, 스토리지 권한
    로컬엔 파일이 있는데 UI엔 run이 없음 Tracking URI 미적용 현재 디렉터리의 mlruns/ 생성 여부, 환경 변수 설정
    run은 생기는데 등록이 안 됨 Registry 접근/권한 또는 등록 로직 누락 registered_model_name, 등록 API 호출, 인증 설정
    운영 코드가 옛 버전을 계속 읽음 버전 번호 고정 참조 models:/name/version 대신 alias 참조 여부

    또 하나 자주 터지는 게 experiment 이름 난립입니다. 누군가는 fraud-dev, 누군가는 fraud_detection, 누군가는 tmp-test로 남기면 실험 관리가 아니라 run 수색이 됩니다. 이건 교육으로 해결이 잘 안 됩니다. 환경 변수나 설정 파일로 강제하는 쪽이 훨씬 빠릅니다.

    MLflow MLOps 아티팩트 경로 오류 비교 이미지

    아티팩트 경로가 잘못되었을 때와 올바르게 구성되었을 때의 차이를 비교하는 이미지입니다.

    검증과 결과 확인은 숫자보다 연결 상태를 먼저 보세요

    MLflow가 제대로 붙었는지 확인할 때 숫자부터 보는 분이 많습니다. 그런데 운영 준비도는 metric보다 연결 상태가 먼저입니다. 저는 아래 순서대로 확인합니다.

    1. Tracking 검증: 예상한 experiment 아래에 run이 쌓이는가
    2. Metadata 검증: param, metric, tag가 최소 기준을 만족하는가
    3. Artifact 검증: 모델 파일, 리포트, input example을 실제로 열 수 있는가
    4. Registry 검증: 등록된 버전이 원본 run과 연결되어 보이는가
    5. 배포 검증: 서빙 코드가 버전 번호가 아니라 alias를 읽는가
    6. 재현성 검증: 같은 코드와 데이터 버전으로 재실행했을 때 비교가 성립하는가

    결과를 읽는 기준도 숫자 중심으로만 보면 안 됩니다. 예를 들어 run은 많은데 tag가 비어 있다면 실험 정책이 없는 겁니다. 모델 버전은 쌓이는데 설명이 없다면 Registry가 보관함 역할만 하는 겁니다. champion alias는 있는데 승인 태그가 없다면 배포 경로는 있으나 책임 추적은 약한 상태라고 봐야 합니다.

    제가 실제로 자주 쓰는 확인 시나리오를 하나 말씀드리면, 새 팀원이 첫 실험을 올린 날엔 점수보다 먼저 세 가지를 봅니다. git_sha가 남았는지, artifact가 다운로드되는지, 등록된 모델이 있으면 왜 등록했는지 설명이 있는지요. 여기서 하나라도 비면 그 실험은 재현성과 운영 연결성이 약하다고 판단합니다.

    MLflow MLOps 실험 결과와 모델 레지스트리 검증 이미지

    실험 결과, 태그, 모델 버전 연결 관계를 확인하는 대시보드 예시 이미지입니다.

    현업에서 바로 쓰는 MLflow MLOps 체크리스트

    여기서는 제가 실제 배포 전 점검 때 보는 항목을 좀 더 엄격하게 적어보겠습니다.

    • ✅ 모든 학습 잡이 같은 MLFLOW_TRACKING_URI를 사용한다
    • ✅ experiment 이름 규칙이 코드 또는 설정으로 강제된다
    • ✅ run마다 owner, git_sha, data_version, purpose 태그가 있다
    • ✅ 현재 디렉터리에 우발적으로 mlruns/가 생기지 않는다
    • ✅ artifact root가 서버 로컬 고정 경로 또는 공용 스토리지다
    • ✅ signature와 input example이 저장된다
    • ✅ Registry 등록은 기준을 통과한 run에만 허용된다
    • ✅ 모델 버전에 승인 상태 태그와 설명이 남는다
    • ✅ 운영 배포 코드는 alias 기반으로 모델을 참조한다
    • ✅ 직전 안정 버전으로 롤백 가능한 경로가 있다
    • ✅ 실패 run도 삭제하지 않고 비교 자료로 남긴다

    실패 run을 남기는 건 의외로 중요합니다. 예전에 저도 지저분해 보여서 정리하고 싶었던 적이 많았는데, 시간이 지나면 실패 기록이 오히려 팀의 판단 근거가 됩니다. 어떤 파라미터가 왜 버려졌는지, 어떤 전처리가 왜 제외됐는지, 문서보다 run 기록이 더 정직하게 남는 경우가 많거든요.

    자주 묻는 질문과 추천 시나리오

    Q1. 작은 팀도 모델 레지스트리가 꼭 필요할까요?

    혼자만 실험하는 단계라면 당장은 Tracking 중심으로 시작해도 됩니다. 다만 배포 후보가 둘 이상 생기기 시작하면 Registry를 미루지 마세요. 그 시점부터는 "좋은 숫자"와 "배포 가능한 버전"이 갈라집니다. 제 추천은 이렇습니다. 개인 프로젝트 초반이면 Tracking + artifact 정리까지만, 팀 협업이나 재배포가 시작되면 Registry + alias까지 바로 붙이세요.

    Q2. 로컬 파일 기반으로 시작해도 괜찮을까요?

    네, PoC나 홈랩 단계라면 충분히 괜찮습니다. 대신 두 가지는 지키세요. 첫째, artifact 경로를 임시 디렉터리나 사용자 홈의 애매한 위치로 두지 마세요. 둘째, 나중에 공용 스토리지로 옮길 걸 감안해 experiment와 artifact 구조를 단순하게 유지하세요. 시작은 가볍게 해도 되지만, 이사하기 어려운 경로 설계는 초반부터 피하는 게 좋습니다.

    Q3. 자동 등록이 좋을까요, 수동 승인 방식이 좋을까요?

    탐색 단계라면 성능 기준 충족 시 자동 등록도 괜찮습니다. 하지만 실제 운영 직전이라면 자동 등록 + 수동 승인 조합이 가장 안전합니다. 제가 권하는 방식은 이렇습니다. 성능 문턱을 넘으면 자동으로 Registry 후보로 등록하되, validation_status=pending으로 두고, 검증 리포트 확인 후에만 @champion alias를 이동하세요. 이러면 속도와 통제를 둘 다 가져갈 수 있습니다.

    개인 실험, 팀 협업, 배포 직전 단계별로 무엇을 우선 적용할지 요약한 이미지입니다.

    마지막으로, 이런 경우엔 이렇게 가시면 됩니다

    상황별로 딱 잘라 추천드리면 이렇습니다.

    • 개인 실험 단계: SQLite + 로컬 artifact + 엄격한 tag 규칙으로 충분합니다. 다만 experiment 이름과 필수 tag는 초반부터 습관을 들이세요
    • 팀 공용 추적 단계: PostgreSQL + 공용 object storage + 고정 Tracking URI로 가세요. 이 시점부터는 서버 설치보다 경로 일관성이 더 중요합니다
    • 배포 후보 운영 단계: Registry + alias + 승인 태그 + 롤백 경로까지 묶으세요. 버전 번호 직접 참조는 여기서 끊는 게 좋습니다
    • 여러 팀 공유 단계: 네트워크 제한만으로 끝내지 말고 인증과 권한 모델까지 포함해서 설계하세요

    제가 실제로 굴려보면서 내린 결론은 단순합니다. MLflow MLOps를 잘 쓰는 팀은 기능을 많이 쓰는 팀이 아니라, 남길 정보를 명확히 정한 팀입니다. 실험 추적의 품질은 UI보다도 이름 규칙, tag, artifact 기준점, Registry 승인 정책에서 갈립니다. 혼자라면 가볍게 시작하셔도 됩니다. 다만 둘 이상이 같은 모델을 만지기 시작했다면, 그때부터는 "툴 사용법"보다 "운영 기준"이 먼저입니다.

    제 추천을 한 줄로 줄이면 이렇습니다. 개인 단계에선 Tracking을 단단하게, 팀 단계에선 artifact를 공용화하고, 배포 단계에선 alias와 승인 흐름을 분리하세요. 이 순서만 지켜도 나중에 "이 모델 누가 왜 올렸죠?"라는 질문 앞에서 멈출 일은 크게 줄어듭니다. MLflow 사용법을 더 넓게 정리한 글이나 모델 배포 글과 내부 링크로 이어두면 검색 유입과 체류 시간 측면에서도 도움이 됩니다.

  • [AI] Triton Inference Server 프로덕션 배포 가이드와 고려사항

    [AI] Triton Inference Server 프로덕션 배포 가이드와 고려사항

    목차

    Triton Inference Server 프로덕션 배포: 도입 사례와 고려사항

    Triton Inference Server로 AI 모델 배포를 붙일 때, 문제는 대개 모델 정확도보다 운영 계약에서 먼저 터지더라고요. 개발 노트북에서는 잘 돌던 모델이 서비스에 올라가면 지연 시간(latency, 응답 지연), 동시성(concurrency, 동시 처리), 버전 교체, 헬스체크, 타임아웃 정책이 한꺼번에 얽힙니다. 저도 초반엔 “모델만 띄우면 끝”이라고 봤는데, 실제 운영에선 어떤 문제를 Triton으로 해결할지를 먼저 정하는 쪽이 훨씬 중요했습니다. 이번 글은 홈랩과 실무 검증에서 반복해서 확인한 패턴을 바탕으로, Triton을 단순한 AI 모델 서빙 엔진이 아니라 운영 경계(operational boundary)로 보는 관점에서 정리해보겠습니다.

    Triton Inference Server 기반 AI 모델 배포 아키텍처 개요 이미지

    클라이언트, 로드밸런서, Triton 서버, 모델 저장소, 모니터링까지 연결된 전체 구조를 보여주는 개요 이미지입니다.

    Triton Inference Server를 왜 보게 되나

    실무에서 Triton Inference Server가 자꾸 거론되는 이유는 단순합니다. 모델을 한 번 띄우는 건 어렵지 않은데, 같은 방식으로 오래 운영하는 일이 생각보다 어렵거든요. 프레임워크가 제각각이어도 API는 통일하고 싶고, 모델 버전은 분리하고 싶고, GPU는 놀지 않게 쓰고 싶고, 장애가 났을 때는 “서버가 죽었는지”, “모델만 실패했는지”, “큐가 막혔는지”를 빨리 구분하고 싶어집니다. Triton은 이 지점을 꽤 정직하게 해결해줍니다.

    제가 현업에서 Triton을 검토하는 기준은 기능 목록보다 아래 네 가지에 가깝습니다.

    • 모델 프레임워크가 섞여 있는데 운영 인터페이스는 하나로 가져가야 하는가
    • 실시간 추론 API와 소규모 배치 처리 요구가 한 시스템에 같이 들어오는가
    • 버전 롤백, 점진 배포, 모델 상태 점검을 서버 표준으로 묶고 싶은가
    • GPU를 여러 모델이나 여러 테넌트가 나눠 쓰는데, 감이 아니라 지표로 튜닝해야 하는가

    반대로 아래 조건이면 Triton이 과할 수 있습니다.

    • 단일 모델 하나만 낮은 트래픽으로 서비스하고 있고, 모델 교체 주기도 길다
    • 버전 병행 운영보다 애플리케이션 재배포가 더 단순하다
    • 메트릭과 큐 제어보다 구현 속도가 더 중요하다
    • CPU 기반 간단 추론만으로 충분하고, GPU 운영 복잡성을 들일 이유가 없다

    이럴 땐 FastAPI나 Flask 뒤에 모델 하나 붙이고, 앞단 리버스 프록시와 애플리케이션 메트릭만 잘 관리하는 편이 더 낫습니다. Triton은 성능 마법사가 아니라 운영 표준화 도구라서, 표준화가 아직 필요 없다면 이점도 그만큼 줄어듭니다.

    Triton Inference Server 도입 전에 먼저 정해야 하는 질문

    제가 팀에 Triton을 권할지 말지 결정할 때는 제품 요구보다 운영 질문을 먼저 던집니다.

    1. 지연 시간 상한이 빡빡한가: p95, p99 기준으로 큐잉을 얼마나 허용할지 먼저 정해야 dynamic batching을 안전하게 만질 수 있습니다.
    2. 모델 버전이 자주 바뀌는가: 교체 빈도가 높으면 모델 저장소 구조와 로드 정책부터 표준화해야 합니다.
    3. 한 GPU에 몇 개의 모델을 얹을 건가: instance_group의 count를 늘리는 순간 메모리 압박과 컨텍스트 전환 비용이 같이 따라옵니다.
    4. 장애 원인을 5분 안에 좁혀야 하는가: 이 요구가 있다면 health, stats, Prometheus metrics를 갖춘 서빙 계층이 훨씬 유리합니다.

    여기서 자주 놓치는 게 하나 있습니다. Triton은 모델 실행 속도를 빠르게 만드는 도구이기도 하지만, 실제 운영에서는 문제 위치를 빨리 찾게 해주는 도구로 더 큰 가치를 냅니다. 성능이 안 나오는 원인이 모델인지, 큐인지, 게이트웨이 타임아웃인지, 잘못된 입력 계약인지가 빨리 분리되거든요.

    핵심 개념: 모델은 올리는 게 아니라 운영하는 겁니다

    모델 저장소(Model Repository, 모델 저장소) 구조

    Triton은 모델 파일을 임의 경로에서 읽어주는 런타임이 아닙니다. 모델 저장소를 운영 단위로 본다는 점이 핵심입니다. 즉, 파일 배치 규칙이 곧 배포 규칙이 됩니다. 저도 초반에는 구조를 대충 맞췄다가 “컨테이너는 healthy인데 모델만 안 올라오는” 상태를 몇 번 겪었습니다. 그 뒤로는 모델 저장소를 코드 리뷰 대상에 포함시켰습니다.

    models/
    └── resnet50/
        ├── config.pbtxt
        ├── 1/
        │   └── model.onnx
        └── 2/
            └── model.onnx

    1, 2 같은 숫자 디렉터리가 단순 규칙처럼 보여도 운영에선 중요합니다. 버전 롤백, 병행 검증, 정책 기반 선택이 모두 이 구조에서 시작되기 때문입니다. 현업에서는 “모델 파일 교체”보다 “새 버전 디렉터리 추가 후 정책 전환”이 훨씬 덜 위험합니다.

    config.pbtxt: 성능보다 먼저 계약을 명시하는 파일

    많은 팀이 이 파일을 성능 튜닝용으로만 보는데, 저는 먼저 입력·출력 계약 명세서로 봅니다. 입력 이름, shape, 데이터 타입, 배치 허용 여부가 애플리케이션과 달라지는 순간, 서버는 살아 있어도 서비스는 실패합니다. 특히 이미지 추론에선 NHWC와 NCHW 혼동이 정말 자주 나옵니다. 클라이언트 전처리 코드와 config.pbtxt가 같은 문서를 보고 있지 않으면, 장애는 거의 반드시 다시 생기더라고요.

    dynamic batching: 처리량을 사는 대신 큐 대기를 허용하는 선택

    dynamic batching은 단순한 성능 옵션이 아닙니다. 더 정확히 말하면, GPU 효율을 높이기 위해 짧은 대기 시간을 의도적으로 허용하는 정책에 가깝습니다. 요청이 조금 모일 때까지 기다렸다가 한 번에 처리하니 처리량은 좋아질 수 있습니다. 대신 큐에서 기다리는 시간이 생기죠. 그래서 이 기능은 켜느냐 마느냐보다, 어느 정도 지연 증가를 서비스가 받아들일 수 있느냐가 먼저입니다.

    실시간 API라면 보통 max_queue_delay_microseconds를 길게 잡지 않습니다. 반대로 내부 배치성 서비스라면 queue delay를 조금 더 허용해 GPU 효율을 챙길 수 있습니다. 같은 모델이라도 서비스 성격이 다르면 정답이 달라집니다.

    instance_group: 인스턴스를 늘린다고 항상 빨라지진 않습니다

    instance_group은 같은 모델 실행 인스턴스를 CPU나 GPU에 몇 개 둘지 정하는 설정입니다. 여기서 자주 나오는 오해가 “count를 늘리면 병렬성이 늘어 성능이 무조건 좋아진다”는 생각입니다. 실제로는 모델 가중치가 인스턴스 수만큼 메모리를 더 먹고, GPU 메모리 압박이나 컨텍스트 전환 비용 때문에 응답 시간이 오히려 흔들릴 수 있습니다. 저는 이 설정을 만질 때 항상 GPU 메모리 여유, 큐 길이, compute duration 세 가지를 같이 봅니다.

    model control mode: 자동 재로딩의 편의와 운영 사고 가능성은 같이 갑니다

    이 부분은 의외로 글에서 자주 빠지는데, 운영에선 꽤 중요합니다. Triton은 모델 제어 방식을 바꿀 수 있습니다. 파일 변경을 주기적으로 감시하는 방식이 편할 때도 있지만, 프로덕션에서는 의도치 않은 재로딩이나 스토리지 지연이 문제를 만들기도 합니다. 저는 개발·실험 단계에서는 poll 기반 방식이 편하다고 보고, 운영에선 명시적 로드/언로드를 더 선호합니다. 이유는 단순합니다. 모델 교체 타이밍을 배포 파이프라인이 통제해야 장애 원인 추적이 쉬워지기 때문입니다.

    실전 구현: Triton Inference Server 최소 구성부터 올려보기

    처음 검증할 때 쿠버네티스로 바로 가는 접근은 보통 손해가 큽니다. 플랫폼 이슈와 모델 이슈가 섞여버리거든요. 저는 아래 순서로 가져가는 편입니다.

    1. 단일 Docker에서 모델 로딩 성공 여부 확인
    2. 헬스체크, 모델 상태, 메트릭 노출 확인
    3. 입력 계약과 클라이언트 payload 검증
    4. 그다음에야 Ingress, 오토스케일링, 롤링 배포 규칙 추가

    이 순서를 지키면 장애가 났을 때 “플랫폼 문제인지 모델 문제인지”를 훨씬 빨리 가를 수 있습니다.

    1. 모델 저장소와 설정 파일 만들기

    mkdir -p ./models/resnet50/1
    cp ./model.onnx ./models/resnet50/1/model.onnx
    
    cat > ./models/resnet50/config.pbtxt <<'EOF'
    name: "resnet50"
    platform: "onnxruntime_onnx"
    max_batch_size: 8
    
    input [
      {
        name: "input"
        data_type: TYPE_FP32
        dims: [ 3, 224, 224 ]
      }
    ]
    
    output [
      {
        name: "output"
        data_type: TYPE_FP32
        dims: [ 1000 ]
      }
    ]
    
    instance_group [
      {
        kind: KIND_GPU
        count: 1
      }
    ]
    
    dynamic_batching {
      preferred_batch_size: [ 4, 8 ]
      max_queue_delay_microseconds: 1000
    }
    
    version_policy: {
      latest {
        num_versions: 2
      }
    }
    EOF

    이 설정에서 운영 영향이 큰 항목은 네 가지입니다.

    • max_batch_size: 서버가 배치를 어떻게 받아들일지의 상한입니다. 값을 키우기 전에 클라이언트 요청 형식과 모델 shape가 배치를 실제로 감당하는지부터 확인해야 합니다.
    • dynamic_batching: 처리량을 끌어올리는 대신 큐 지연을 허용합니다. 실시간 추론이면 queue delay를 보수적으로 시작하는 편이 안전합니다.
    • instance_group: 병렬성뿐 아니라 GPU 메모리 사용량을 같이 바꿉니다. 메모리 부족이 나면 count 증가가 바로 비용 증가로 이어질 수 있습니다.
    • version_policy: 운영 중 남겨둘 버전 수를 제한합니다. 이걸 명시하지 않으면 저장소에 쌓인 버전이 예기치 않게 운영 면적을 넓힐 수 있습니다.

    실무에서는 config.pbtxt를 모델 산출물과 따로 보지 않고, 애플리케이션 계약 파일처럼 같이 리뷰하는 편이 훨씬 낫습니다. 모델만 바뀌었다고 생각했는데 실제로는 입력 dtype이나 shape가 바뀌는 경우가 적지 않거든요.

    Triton Inference Server 모델 저장소와 config.pbtxt 구성 설명 이미지

    모델 버전 디렉터리, config.pbtxt, 백엔드 선택, 배치 설정의 관계를 이해하기 쉽게 보여주는 구성 이미지입니다.

    2. 컨테이너 실행과 서버 상태 확인

    docker run --gpus=all --rm --name triton \
      -p 8000:8000 \
      -p 8001:8001 \
      -p 8002:8002 \
      -v $(pwd)/models:/models \
      nvcr.io/nvidia/tritonserver:24.01-py3 \
      tritonserver \
        --model-repository=/models \
        --model-control-mode=explicit \
        --load-model=* \
        --log-verbose=1

    포트는 보통 아래 역할로 씁니다.

    • 8000: HTTP endpoint
    • 8001: gRPC endpoint
    • 8002: Prometheus metrics endpoint

    여기서 한 가지 짚고 갈 점이 있습니다. 최근 운영 예시에선 config.pbtxt를 명시적으로 관리하는 쪽이 더 일반적이고, 예전 글에서 자주 보이던 --strict-model-config=true 같은 옵션은 최신 가이드 기준으로 굳이 앞세우지 않는 편이 낫습니다. 또 --model-control-mode=explicit를 쓰면 서버 시작 시 모델을 자동으로 다 올리지 않기 때문에, 위처럼 --load-model=*를 주거나 이후 API로 명시적으로 load해야 합니다. 이 차이를 놓치면 “서버는 떴는데 모델이 없다”는 상황을 바로 만나게 됩니다.

    3. 헬스체크, 개별 모델 상태, 메트릭을 분리해서 봅니다

    curl -s http://localhost:8000/v2/health/live
    curl -s http://localhost:8000/v2/health/ready
    
    curl -s http://localhost:8000/v2/models/resnet50/ready
    curl -s http://localhost:8000/v2/models/resnet50/stats
    
    curl -s http://localhost:8002/metrics | egrep "nv_inference_(request|queue|compute)"

    명시적 제어 모드에서 특정 모델만 따로 올리고 싶다면 아래처럼 repository API를 호출하면 됩니다.

    curl -s -X POST http://localhost:8000/v2/repository/models/resnet50/load

    여기서 중요한 건 ready 하나로 끝내지 않는 것입니다. 컨테이너가 ready여도 개별 모델은 로딩 실패 상태일 수 있습니다. 운영 헬스체크를 설계할 때는 컨테이너 준비 상태와 모델 가용 상태를 분리해야 합니다. 이걸 안 하면 배포는 열렸는데 특정 모델만 실패한 반쪽짜리 정상 상태가 됩니다.

    4. 초기에 꼭 보는 로그 패턴

    제가 검증 초반에 로그에서 먼저 찾는 건 세 종류입니다.

    • backend 관련 메시지: ONNX 모델인데 백엔드 선택이 어긋났거나, 모델 파일 위치가 기대 구조와 다를 때 여기서 드러납니다.
    • shape/dtype 관련 오류: 서버 설정과 클라이언트 payload 계약이 안 맞는 경우입니다.
    • model load/unload 이벤트: 의도한 시점에만 모델이 교체되는지 확인해야 합니다.

    실무에서는 서버가 떠 있는지만 보는 팀이 많습니다. 그런데 실제 장애는 “프로세스 다운”보다 계약 불일치와 큐 적체에서 더 자주 납니다.

    어떤 설정을 먼저 만질지, 비교 표로 보겠습니다

    항목 먼저 보는 상황 얻는 것 대가와 주의점
    dynamic_batching 동시 요청이 몰릴 때 GPU 활용률이 낮고 처리량이 안 나올 때 처리량 증가, GPU 활용 개선 큐 대기 시간이 늘 수 있어 실시간 API의 p95/p99를 해칠 수 있음
    max_queue_delay_microseconds 응답은 느린데 compute 시간보다 queue 시간이 먼저 커질 때 배치 효율과 지연 시간 사이 균형 조절 길게 잡으면 사용자는 “서버가 느리다”고 느끼지만 GPU는 바쁘게 보일 수 있음
    instance_group count 큐 적체가 있고 모델 병렬 실행이 실제로 필요할 때 병렬 처리 증가 가능 모델 메모리 복제, GPU 메모리 압박, 컨텍스트 전환 비용 증가 가능
    version_policy 롤백 요구가 있고 저장소에 여러 버전이 누적될 때 운영 버전 수 통제, 롤백 경로 명확화 버전 보존 전략을 배포 파이프라인과 같이 설계해야 함
    model-control-mode 모델 교체 타이밍을 엄격히 통제해야 할 때 의도한 시점에만 load/unload 수행 배포 자동화가 미흡하면 오히려 운영 절차가 번거로워질 수 있음
    HTTP vs gRPC 게이트웨이 호환성과 SDK 통일성이 중요할 때 팀 표준화 또는 고성능 연동 선택 가능 디버깅 편의, 프록시 호환성, 조직의 운영 경험을 같이 봐야 함

    제 경험상 첫 튜닝 포인트는 대개 배치와 큐입니다. 모델 최적화 전에 큐 정책을 잘못 잡아서 체감 성능을 망치는 경우를 훨씬 많이 봤습니다.

    트러블슈팅: 실제로 많이 걸리는 지점과 근본 원인

    1. 모델이 안 뜨는 경우: 파일이 아니라 운영 단위가 잘못 배치된 상태

    모델 파일을 models/resnet50/model.onnx에 바로 두는 실수는 흔합니다. 겉으로 보면 파일은 있는데, Triton 입장에선 버전이 없는 모델입니다. 운영 관점에서 보면 단순 경로 실수가 아니라 “배포 단위가 정의되지 않은 상태”에 가깝습니다. 로그에서 model version 관련 메시지가 보이면 권한보다 구조를 먼저 의심하는 편이 빠릅니다.

    2. 입력 shape 불일치: 서버 문제처럼 보이지만 계약 문제입니다

    NHWC와 NCHW 혼동, batch 차원 포함 여부, FP32와 UINT8 차이, 입력 이름 오타. 이 네 가지가 특히 잦습니다. 이때 중요한 건 서버를 재시작하는 게 아니라 클라이언트 payload와 config.pbtxt를 한 줄씩 대조하는 겁니다. readiness는 정상인데 요청만 실패하는 패턴이면, 거의 늘 계약 문제입니다.

    제가 자주 보는 실제 시나리오는 이렇습니다. 이미지 업로드 API가 애플리케이션 서버에서 전처리를 한 뒤 Triton으로 넘기는데, 전처리 코드는 NHWC 텐서를 만들고 모델은 NCHW를 기대합니다. 애플리케이션 로그에는 단순 500만 남고, 운영자는 Triton 장애로 오해합니다. 이런 건 모델 서버보다 전처리 코드와 서빙 계약을 같은 저장소에서 관리하지 않은 구조가 근본 원인입니다.

    3. 지연 시간 급증: 실행이 느린 게 아니라 큐가 길어진 경우

    실시간 추론에서 가장 헷갈리는 장애입니다. GPU 사용률은 낮거나 애매한데 응답은 느립니다. 이때는 모델 자체보다 먼저 nv_inference_queue_duration_us를 봐야 합니다. queue duration이 compute duration보다 눈에 띄게 커지면, 문제는 대개 dynamic batching 정책이나 인스턴스 병렬성 부족입니다. 반대로 compute 쪽이 크면 모델 최적화, 백엔드 선택, 하드웨어 자원 부족 쪽이 더 유력합니다.

    여기서 많이 하는 실수가 하나 있습니다. queue가 길다고 바로 instance count를 늘리는 겁니다. 모델이 무거우면 메모리가 더 들어가고, 결국 더 큰 GPU가 필요해져 비용이 올라갑니다. 먼저 queue delay, 요청 패턴, 앞단 연결 풀 크기, 게이트웨이 타임아웃을 같이 보셔야 합니다.

    4. readiness는 OK인데 서비스는 실패하는 경우

    컨테이너 readiness만 보고 배포를 열어버리면 생기는 전형적인 문제입니다. 프로세스는 살아 있고 일부 모델도 로딩됐지만, 실제로 서비스해야 하는 모델은 실패했을 수 있습니다. 운영에서는 단순 포트 체크보다 /v2/models/<name>/ready나 모델 상태 확인을 포함한 상위 헬스체크가 필요합니다. 저는 라우터가 대상 모델의 ready 여부를 확인하지 못하는 구조면, 그 구조부터 위험 신호로 봅니다.

    5. 모델 교체 때만 간헐적으로 장애가 나는 경우

    이건 성능 튜닝보다 배포 설계 문제인 경우가 많습니다. 파일 감시 기반 재로딩, 네트워크 스토리지 지연, 새 버전 업로드 중간 상태 노출이 겹치면 교체 시점에만 이상 현상이 납니다. 그래서 운영에선 새 버전 디렉터리를 완성한 뒤 명시적으로 load하는 절차가 안전합니다. 모델 파일을 덮어쓰는 방식은 복구도 추적도 둘 다 불리합니다.

    Triton Inference Server 메트릭과 실시간 추론 지연 분석 대시보드 이미지

    queue duration, compute duration, request success 지표를 함께 보고 병목을 판단하는 모니터링 예시 이미지입니다.

    검증은 이렇게 봤습니다: 응답이 왔다가 아니라, 병목 위치를 찾는 방식으로

    검증 단계에서 제가 먼저 보는 건 세 가지입니다.

    1. 헬스 상태: live, ready, 개별 모델 ready가 모두 정상인지
    2. 메트릭 이동 방향: 요청이 늘 때 queue duration과 compute duration 중 어디가 먼저 커지는지
    3. 로그 패턴: 로딩 실패, 텐서 계약 오류, 백엔드 초기화 오류가 반복되는지

    여기서 중요한 건 단순 성공률보다 병목 위치의 일관성입니다. 같은 부하 패턴에서 늘 queue가 먼저 커지면 배치 정책 문제일 가능성이 높고, 늘 compute가 먼저 치솟으면 모델 최적화나 하드웨어 부족을 의심하면 됩니다. 증상이 매번 다르면 앞단 게이트웨이, 네트워크, 클라이언트 요청 형식이 흔들리는 경우도 많습니다.

    • nv_inference_request_success가 늘어도 사용자 응답이 느리면 애플리케이션 레벨 타임아웃과 큐 대기를 같이 보셔야 합니다.
    • nv_inference_queue_duration_us가 커지면 dynamic batching의 queue delay, instance_group, 요청 분산을 우선 검토합니다.
    • nv_inference_compute_input_duration_us나 nv_inference_compute_output_duration_us가 크면 전처리·후처리 경계에서 병목이 생길 수 있습니다.
    • GPU 사용률이 낮은데 응답이 느리면 모델 엔진보다 클라이언트 연결 풀, 게이트웨이 timeout, 작은 요청 단위 반복을 먼저 의심하는 편이 맞습니다.

    저는 검증 완료 기준도 조금 보수적으로 잡습니다. 200 OK가 나오는 순간이 아니라, 요청 패턴이 바뀌어도 어느 구간이 먼저 무너지는지 설명 가능해지는 순간을 통과 기준으로 봅니다. 그때부터 운영이 덜 불안해집니다.

    언제 Triton Inference Server가 맞고, 언제 단순 구성이 더 낫나

    상황 추천 이유
    단일 모델, 낮은 트래픽, 빠른 실험 우선 앱 서버 직접 서빙 구조가 단순하고 디버깅 경로가 짧습니다. Triton의 운영 이점이 아직 크지 않습니다.
    여러 모델 병행 운영, 버전 롤백 필요 Triton 도입 모델 저장소, 버전 정책, 상태 점검, 공통 API를 한 계층으로 묶을 수 있습니다.
    GPU 자원을 여러 워크로드가 공유 Triton 우선 검토 배치, 인스턴스, 메트릭 기반 튜닝이 가능해 운영 판단이 빨라집니다.
    팀이 아직 MLOps 표준보다 기능 검증 속도를 더 중시 단일 Docker로 먼저 검증 문제 분리가 쉽고, 불필요한 플랫폼 복잡성을 늦출 수 있습니다.

    핵심은 이겁니다. 운영 표준화가 이미 필요한 팀이면 Triton이 맞고, 아직 모델 가치 검증이 먼저인 단계면 단순 구성이 더 낫습니다. 둘 다 맞는 도구인데, 맞는 시점이 다릅니다.

    실무에서 붙여둘 운영 체크리스트

    • 모델 저장소 구조를 CI 검증 대상에 넣으세요. 버전 디렉터리 규칙이 무너지면 배포 자동화가 바로 흔들립니다.
    • config.pbtxt를 모델 아티팩트와 함께 리뷰하세요. 입력 이름, dtype, shape 계약이 가장 자주 깨집니다.
    • Prometheus metrics를 기본값처럼 붙이세요. 감으로 튜닝하면 queue와 compute를 자꾸 혼동하게 됩니다.
    • 헬스체크는 컨테이너 ready만 보지 말고 개별 모델 ready를 포함하세요.
    • 모델 교체 절차는 파일 덮어쓰기보다 새 버전 디렉터리 추가 후 명시적 load/unload 쪽이 안전합니다.
    • Ingress나 API Gateway 앞단 타임아웃을 같이 보세요. 서버는 정상인데 앞단에서 먼저 끊기는 사례가 생각보다 많습니다.
    • instance_group 증가는 성능 개선안이 아니라 비용 증가안일 수도 있다는 전제로 접근하세요.

    이 체크리스트는 단순해 보여도 실제 장애 예방 효과가 큽니다. 저는 특히 모델 저장소 구조, 계약 파일, 상위 헬스체크 세 개를 먼저 잡는 편입니다. 이 셋이 정리되면 나머지 성능 튜닝은 훨씬 다루기 쉬워집니다.

    관련해서 MLOps 관측성과 모델 성능 최적화 글도 함께 보시면 흐름이 더 잘 잡힙니다. 내부 문서나 사내 기술 블로그가 있다면 이 체크리스트를 기준으로 연결해두는 것도 꽤 편합니다.

    Triton Inference Server 도입 판단 기준과 운영 체크리스트 인포그래픽

    단일 앱 서버 추론과 Triton 기반 운영을 어떤 기준으로 나눌지 한눈에 보는 요약 이미지입니다.

    자주 묻는 질문

    꼭 GPU가 있어야 하나요?

    반드시 그렇진 않습니다. 다만 Triton을 도입하는 실익은 대개 GPU 활용 최적화, 다중 모델 운영, 메트릭 기반 튜닝에서 커집니다. CPU만으로 단일 모델을 단순 서빙하는 상황이면 구조가 과할 가능성이 높습니다.

    처음부터 쿠버네티스로 가야 하나요?

    저는 권하지 않습니다. 단일 컨테이너로 모델 로딩, 상태 확인, 메트릭 노출, 입력 계약 검증까지 끝낸 뒤에 오케스트레이션으로 넘어가는 편이 낫습니다. 플랫폼 문제가 끼어들면 장애 원인 분리가 어려워집니다.

    실시간 추론에 dynamic batching을 켜도 되나요?

    됩니다. 다만 처리량만 보고 켜면 안 되고, queue delay를 짧게 시작한 뒤 p95, p99 지연과 queue duration 변화를 함께 보면서 조정하셔야 합니다. 사용자가 느끼는 느림은 compute보다 queue에서 더 자주 생깁니다.

    모델 버전은 많이 남겨둘수록 안전한가요?

    항상 그렇진 않습니다. 롤백 여지는 늘어나지만, 저장소 관리 범위와 운영 복잡성도 같이 커집니다. 운영에 필요한 수만 남기고, 버전 정책을 배포 파이프라인과 함께 관리하는 편이 더 안정적입니다.

    마무리: 이런 경우엔 Triton이 맞고, 이런 경우엔 단순 구성이 낫습니다

    여러 모델을 함께 운영해야 하고, 버전 교체와 롤백이 잦고, 실시간 추론과 처리량 사이에서 정책 결정을 계속해야 한다면 Triton Inference Server는 꽤 강한 선택지입니다. 특히 운영 중 문제를 성능, 계약, 배포, 관측성 관점으로 분리해 다뤄야 하는 팀이라면 더 그렇습니다.

    반대로 단일 모델 하나를 낮은 트래픽으로 빠르게 붙이는 단계라면, Triton이 기술적으로 가능하더라도 조직적으로는 과할 수 있습니다. 이럴 땐 앱 서버 직접 서빙이 더 낫습니다. 제 권장은 분명합니다. MLOps 요구가 이미 생겼다면 Triton부터 검토하시고, 아직 검증 단계라면 단일 Docker에서 성공 기준을 먼저 만든 뒤 확장하세요. 결국 중요한 건 화려한 기능보다 운영 계약을 어디까지 표준화해야 하느냐입니다. 이 질문이 생기는 순간, Triton은 제값을 합니다.