13년차의 서버실

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

[태그:] 모델 변환

  • [AI] OpenVINO 도입 가이드: 기존 AI 모델 최적화 전환 기준

    [AI] OpenVINO 도입 가이드: 기존 AI 모델 최적화 전환 기준

    OpenVINO 도입 가이드: 기존 AI 모델 최적화 전환 기준

    OpenVINO 도입을 검토할 때 저는 먼저 병목부터 가릅니다. 엣지 장비나 사내 서버에서 추론만 오래 돌려보면 결국 질문이 하나로 모이거든요. 지금 느린 지점이 모델인지, 런타임인지, 하드웨어 배치인지를 먼저 나눠야 판단이 덜 흔들립니다. ONNX Runtime에서 이미 잘 도는데도 지연 시간이 출렁이거나 첫 요청만 유독 느릴 때가 있는데, 이런 경우 OpenVINO는 프레임워크를 갈아엎는 선택이라기보다 인텔 CPU 중심 추론 경로를 별도로 최적화하는 수단에 가깝습니다.

    이 글은 OpenVINO 입문서가 아니라, 기존 AI 모델을 언제 OpenVINO 경로로 분기할지 판단하는 실무 메모에 가깝습니다. 저는 보통 세 가지를 같이 봅니다. 첫째, 운영 하드웨어가 정말 인텔 CPU 중심인지. 둘째, ONNX 산출물이 이미 안정적으로 나오는지. 셋째, 병목의 본체가 모델 실행 경로인지 아니면 전처리·후처리·API 계층인지입니다. 사내 블로그에 ONNX export 체크리스트 글이 있다면 그 글과 함께 보시면 판단이 훨씬 빨라집니다.

    OpenVINO 도입 판단을 위한 전체 아키텍처 개요 이미지

    기존 ONNX 추론 경로와 OpenVINO 최적화 경로를 나란히 보여주는 개요 이미지입니다.

    OpenVINO 도입, 왜 여기서 판단이 갈리냐면요

    실무에서는 성능 숫자 하나보다 성능의 성격이 더 중요합니다. 같은 모델이라도 초당 많이 처리해야 하는 배치 작업과 한 요청 응답이 빨라야 하는 API는 최적점이 다르더라고요. OpenVINO는 특히 인텔 하드웨어에서 이 차이를 런타임 수준에서 조정하기 편합니다. 반대로 이미 NVIDIA GPU 최적화와 TensorRT 경로가 운영 표준이라면, OpenVINO를 주 경로로 들이는 순간 복잡도만 커질 수 있습니다.

    제가 보기에 OpenVINO는 아래 조건에서 특히 잘 맞았습니다.

    • 인텔 CPU가 메인 실행 장치라서 CPU 스레드 배치와 추론 요청 수 조정이 성능에 직접 반영되는 경우
    • 여러 엣지 노드에 같은 모델을 배포해야 해서 아티팩트를 단순하게 유지하고 싶은 경우
    • 학습 프레임워크는 그대로 두고, 배포 레이어만 별도로 최적화하고 싶은 경우
    • ONNX까지는 이미 정리됐지만 실제 서비스 지연 시간 편차가 커서 런타임 튜닝이 필요한 경우

    반대로 아래 상황이면 저는 먼저 보류합니다.

    • 커스텀 연산자가 많아 변환 호환성 자체가 리스크인 경우
    • GPU 혼합 환경에서 특정 벤더 최적화가 이미 운영 표준인 경우
    • 모델 추론보다 이미지 디코딩, 리사이즈, NMS, 직렬화가 더 무거운 경우
    • 서비스가 단건 응답 위주인데 비동기 다중 요청에서만 처리량이 좋아지는 경우

    OpenVINO 도입 기준을 AI 모델 최적화 관점에서 보면

    저는 “느리다”보다 “어디서 손해 보는가”를 먼저 적어 둡니다. 이 단계가 흐리면 OpenVINO를 붙인 뒤에도 왜 빨라졌는지, 왜 안 빨라졌는지 설명을 못 하게 되더라고요. 이 작업이 좀 귀찮아 보여도 나중엔 진짜 편합니다.

    상황 관찰 신호 OpenVINO 도입 적합도 제가 실제로 내리는 판단
    인텔 CPU 중심 API 서버 반복 추론 지연 편차가 크고 CPU 사용 패턴이 불안정함 높음 ONNX는 유지하고 OpenVINO IR을 추가 산출해 A/B 비교합니다.
    엣지 장비 다수 배포 GPU 없이 운영하고 패키징 단순성이 중요함 높음 IR 산출물 관리와 첫 로딩 시간, 캐시 전략까지 같이 검토합니다.
    NVIDIA GPU 표준 환경 TensorRT나 CUDA 경로가 이미 검증됨 낮음 주 경로는 유지하고 CPU fallback이 필요할 때만 제한적으로 씁니다.
    커스텀 연산자 다수 변환 경고가 반복되거나 지원 여부가 불명확함 주의 호환성 검증이 끝나기 전에는 마이그레이션 일정에 넣지 않습니다.
    서비스 응답보다 배치 처리량이 중요 동시 요청 수를 늘릴수록 전체 효율이 올라감 높음 <code>-hint throughput 기준으로 먼저 보고, API와 배치 경로를 분리합니다.
    첫 요청만 느림 웜업 이후엔 안정적이나 cold start가 큼 보통 이상 모델 캐시와 사전 컴파일 전략을 먼저 넣고 재측정합니다.

    OpenVINO 도입을 추천하는 가장 강한 신호는 이겁니다. 인텔 CPU 추론이 실제 핵심 경로이고, 모델은 이미 ONNX로 정리됐고, 남은 문제가 운영 레이어 성능과 일관성인 경우요. 이때는 투자 대비 회수 속도가 꽤 빠른 편입니다.

    반대로 학습 파이프라인까지 한 번에 옮기려는 접근은 저는 거의 항상 말립니다. 바꿔야 할 지점이 많아지는 순간 병목 분리가 안 되거든요. 가장 덜 아픈 경로는 여전히 학습은 기존 프레임워크 유지, 배포 추론만 OpenVINO로 분기하는 방식입니다.

    OpenVINO와 ONNX 관계, 실무에서는 이렇게 보시면 덜 헷갈립니다

    실무 관점에서 ONNX는 모델 교환용 공용 산출물이고, OpenVINO는 그 산출물을 인텔 실행 환경에 맞게 최적화해 배포하는 계층에 가깝습니다. 둘 중 하나만 고르는 관계가 아니라, ONNX를 중간 계약서처럼 두고 OpenVINO IR을 배포 전용 산출물로 추가하는 구조가 제일 안전했습니다.

    이 구조를 쓰면 좋은 점이 분명합니다.

    1. 학습 코드와 배포 코드를 억지로 묶지 않아도 됩니다.
    2. ONNX Runtime 경로를 롤백 스위치로 남길 수 있습니다.
    3. 변환 실패가 나도 원본 산출물 체계가 무너지지 않습니다.
    4. CPU 전용 최적화 실험을 서비스 전체 변경 없이 진행할 수 있습니다.

    실제로는 아래 흐름을 많이 씁니다. PyTorch에서 ONNX export를 만들고, OpenVINO IR로 한 번 더 변환한 뒤, benchmark_app으로 모델 단독 성능을 먼저 확인하고, 그다음 애플리케이션에 붙입니다. 이 순서를 지키면 앱 버그와 런타임 문제를 섞어 보지 않게 됩니다.

    실전 구현 1: ONNX 모델을 OpenVINO IR로 변환하기

    여기서는 이미 model.onnx가 있다고 가정하겠습니다. 현재 OpenVINO 배포판에서는 Python 패키지 openvino만으로 기본 런타임과 변환 도구를 시작할 수 있습니다. 예전 글처럼 openvino-dev를 기본 전제로 두는 문서는 아직 많지만, 최신 환경에서는 필수 전제처럼 쓰지 않는 편이 덜 헷갈립니다.

    python3 -m venv .venv
    source .venv/bin/activate
    python -m pip install --upgrade pip
    python -m pip install openvino onnx onnxruntime
    
    # 1) 가장 단순한 CLI 변환
    ovc model.onnx --output_model build/model.xml
    
    # 2) 입력 shape를 명시해야 하는 경우 예시
    ovc model.onnx --output_model build/model.xml --input_shape [1,3,224,224]

    여기서 --input_shape를 굳이 명시하는 이유는 변환 성공 여부보다 런타임 입력 계약을 명확히 하기 위해서입니다. 동적 차원을 넓게 둔 ONNX를 그대로 넘기면 앱 쪽 전처리 코드가 shape를 암묵적으로 가정하다가 운영 중에 터지는 일이 꽤 잦았습니다.

    import openvino as ov
    
    onnx_path = "model.onnx"
    ir_path = "build/model.xml"
    
    ov_model = ov.convert_model(onnx_path)
    ov.save_model(ov_model, ir_path)
    
    print("saved:", ir_path)

    변환 단계에서 제가 꼭 보는 건 세 가지입니다.

    • model.xml과 model.bin이 함께 생성되는지
    • 변환 로그에 unsupported operation, shape inference, precision 관련 경고가 남는지
    • 입력 이름과 입력 shape가 애플리케이션 코드가 기대하는 값과 맞는지

    중요한 건 변환 완료 자체보다 입력 계약이 명확해졌는가입니다. 현업에서 진짜 자주 나는 장애는 변환 실패보다 “변환은 됐는데 앱 입력이 미묘하게 안 맞는 상태”였어요.

    ONNX에서 OpenVINO IR로 변환되는 OpenVINO 도입 구성 이미지

    ONNX 파일이 OpenVINO IR로 바뀌고 CPU 플러그인으로 연결되는 과정을 보여주는 구성 이미지입니다.

    실전 구현 2: 인텔 CPU 추론과 기본 검증

    변환 직후에는 애플리케이션에 바로 붙이지 말고 모델 단독 성능부터 봅니다. 여기서 저는 목표를 둘로 나눕니다. API라면 지연 시간 중심, 배치 작업이라면 처리량 중심입니다. 이걸 안 나누고 평균 숫자 하나만 보면 해석이 자주 틀어집니다.

    # 지연 시간 우선: 단건 응답형 API 검증
    benchmark_app -m build/model.xml -d CPU -hint latency -api sync -t 15 \
      -report_type average_counters -report_folder reports/latency \
      -exec_graph_path reports/latency/exec_graph.xml
    
    # 처리량 우선: 다중 요청 또는 배치 작업 검증
    benchmark_app -m build/model.xml -d CPU -hint throughput -api async -t 15 \
      -report_type average_counters -report_folder reports/throughput \
      -pc

    제가 실제로 읽는 포인트는 아래입니다.

    • -hint latency에서 좋아지는 모델은 실시간 API 후보입니다. 반대로 -hint throughput에서만 성능이 오르면 단건 응답이 중요한 서비스에는 그대로 넣지 않는 편이 안전합니다.
    • -api async에서만 효율이 오르는 경우는 요청을 동시에 밀어 넣을 수 있을 때만 이득입니다. 호출 패턴이 순차형이면 체감 차이가 약할 수 있습니다.
    • -pc와 -report_type average_counters는 레이어별 힌트를 주지만, 서비스 전체 지연 시간을 그대로 대변하지는 않습니다.
    • -exec_graph_path를 남기면 실행 그래프를 따로 볼 수 있어서 레이어 융합 여부를 확인하기 좋습니다.

    CPU 쪽은 너무 빨리 저수준 옵션으로 내려가지 않는 게 중요합니다. 저는 보통 -hint latency나 -hint throughput로 시작하고, 그다음에만 -nthreads, -nireq, 필요 시 -pin 같은 옵션을 건드립니다. 시작부터 스레드 수를 직접 고정하면 휴리스틱보다 못한 조합으로 들어가는 일도 있더라고요.

    # CPU 스레드 경쟁이 의심될 때만 추가 비교
    benchmark_app -m build/model.xml -d CPU -hint latency -api async -t 15 \
      -nthreads 8
    
    # 동시 요청 수가 실제 서비스와 맞지 않는지 확인할 때
    benchmark_app -m build/model.xml -d CPU -hint throughput -api async -t 15 \
      -nireq 4

    NUMA나 멀티소켓 서버라면 스레드 고정 정책까지 볼 가치가 있습니다. 다만 이건 버전과 장치 구성에 따라 옵션 차이가 있어서, benchmark_app -h로 현재 설치 버전이 지원하는 플래그를 먼저 확인하는 게 안전합니다. 소형 단일 서버에서는 이런 저수준 옵션보다 입력 파이프라인 정리 쪽이 효과가 더 클 때가 많았습니다.

    파이썬 서비스에 붙일 때는 아래 정도로 시작하면 충분합니다.

    import openvino as ov
    import numpy as np
    
    core = ov.Core()
    core.set_property({"CACHE_DIR": "./ov_cache"})
    
    compiled_model = core.compile_model("build/model.xml", "CPU", {
        "PERFORMANCE_HINT": "LATENCY"
    })
    
    input_port = compiled_model.input(0)
    output_port = compiled_model.output(0)
    
    sample = np.random.rand(*input_port.shape).astype(np.float32)
    result = compiled_model({input_port.any_name: sample})
    output = result[output_port]
    
    print(output.shape)

    여기서 CACHE_DIR를 넣는 건 첫 요청 지연을 줄이기 위한 준비입니다. 다만 이건 무조건 빨라진다가 아니라 재시작이 잦고 컴파일 비용이 의미 있는 모델에서 특히 효과를 볼 수 있는 옵션으로 보는 편이 정확합니다. 장치와 모델에 따라 체감 차이가 달라서, 넣고 다시 재측정해야 합니다.

    주의사항과 트러블슈팅: 막히는 지점은 늘 비슷합니다

    제가 반복해서 본 실패 모드는 대체로 네 가지였습니다. 증상만 보는 것보다 근본 원인을 먼저 적어두면 대응이 훨씬 빨라집니다.

    1. 입력 shape 문제: 원인은 모델이 아니라 입력 계약 누락인 경우가 많습니다

    에러 메시지는 모델이 까다로운 것처럼 보이는데, 실제로는 전처리 코드가 NCHW, NHWC, dtype, 배치 차원을 제각각 가정하는 경우가 대부분이었습니다. 특히 ONNX 단계에서 동적 차원을 허용해 둔 모델은 “받아줄 줄 알았는데 실제 런타임 입력은 다르다”는 일이 자주 납니다. 해결은 단순합니다. 변환 시점에 입력 shape를 명시하고, 서비스 입력 스키마를 코드와 문서 양쪽에 고정하세요.

    2. 변환은 됐는데 추론 결과가 흔들리는 경우: 원인은 연산자 호환성보다 export 품질인 때도 많습니다

    이건 OpenVINO 쪽 문제로만 보면 안 됩니다. ONNX export 단계에서 이미 불안정한 그래프가 만들어졌거나, 커스텀 연산이 우회 변환되면서 의미가 달라지는 경우가 있습니다. 이런 때는 OpenVINO IR만 보지 말고 원본 프레임워크 출력, ONNX Runtime 출력, OpenVINO 출력을 같은 샘플로 나란히 비교해야 합니다. 분류 모델이면 top-k, 탐지 모델이면 박스 좌표와 score 분포를 같이 보는 편이 좋습니다.

    3. benchmark_app는 좋아 보이는데 API는 그대로인 경우: 원인은 모델 밖에 있습니다

    이건 정말 흔합니다. 예를 들어 FastAPI 뒤에 이미지 추론 API가 있고, 요청마다 파일 업로드를 받아 Pillow로 디코딩하고 NumPy 배열로 바꾸고, 후처리로 박스를 정리해 JSON으로 내보낸다고 해보겠습니다. 이 구조에서는 모델 추론이 빨라져도 디코딩, 리사이즈, 직렬화, 네트워크 대기가 더 크면 전체 응답 시간은 거의 안 줄 수 있습니다. OpenVINO 도입이 실패한 게 아니라 최적화 대상 선정이 틀린 것에 가깝습니다.

    4. 첫 요청만 유독 느린 경우: 원인은 cold start와 컴파일 비용입니다

    서비스 재배포 직후 첫 요청이 길게 나오는 건 흔히 모델 읽기, 컴파일, 캐시 미적용 때문입니다. 이때는 앱 시작 시 웜업 추론을 한 번 수행하고, CACHE_DIR를 켠 뒤 재시작 후 첫 요청을 다시 재보셔야 합니다. 첫 요청과 반복 요청을 같은 수치로 섞으면 운영 판단이 틀어집니다.

    제가 운영 검토 때 쓰는 간단한 구분법도 있습니다.

    증상 먼저 의심할 곳 근본 원인 후보 우선 조치
    모델은 로드되는데 입력 단계에서 바로 실패 전처리 shape, layout, dtype 불일치 입력 스키마를 고정하고 샘플 입력을 저장해 재현합니다.
    변환 경고 없이 결과만 이상함 export 품질 ONNX 그래프 의미 손실, 커스텀 연산 우회 프레임워크/ONNX/OpenVINO 3단 비교를 합니다.
    단독 벤치마크는 빠른데 API가 안 빨라짐 서비스 외곽 디코딩, 후처리, 직렬화, I/O 병목 모델과 서비스 벤치마크를 분리합니다.
    첫 요청만 느림 초기화 단계 컴파일, 캐시 미사용, 웜업 부재 캐시와 웜업을 넣고 cold start를 따로 측정합니다.
    인텔 CPU 추론 성능 검증과 OpenVINO 도입 병목 분석 이미지

    지연 시간과 처리량, CPU 사용 패턴을 함께 점검하는 검증 장면을 표현한 이미지입니다.

    OpenVINO 도입 검증에서 무엇을 보고 결정할지

    AI 모델 최적화에서 숫자 하나만 보면 거의 항상 놓치는 게 생깁니다. 저는 최소한 아래 네 축을 같이 봅니다.

    1. 기능 동등성: 같은 입력에서 결과 의미가 유지되는지 확인합니다. 분류면 top-k, 탐지면 박스 수와 score 경향, 세그멘테이션이면 마스크 경계를 봅니다.
    2. cold start와 steady state 분리: 첫 요청과 반복 요청을 같은 그래프에 섞지 않습니다.
    3. 단건 지연과 전체 처리량 분리: API냐 배치냐에 따라 좋은 설정이 다릅니다.
    4. 모델 안과 밖 분리: 모델 단독 벤치마크와 서비스 엔드투엔드 벤치마크를 따로 남깁니다.

    이 기준으로 보면 의사결정이 꽤 선명해집니다.

    • 정확도 변화가 없고 지연 시간 편차가 줄었다: 운영 전환 후보로 충분합니다.
    • 처리량은 좋아졌는데 단건 응답이 나빠졌다: 배치 경로엔 적합하지만 실시간 API엔 별도 설정이 필요합니다.
    • 변환 경고가 남고 일부 샘플 출력이 흔들린다: 런타임 튜닝 전에 export와 호환성부터 다시 잡아야 합니다.
    • 모델 숫자는 좋아졌는데 서비스 체감이 없다: 전처리와 후처리, I/O 비용이 본체일 가능성이 큽니다.

    실무에서는 여기서 결론을 분기하면 됩니다. 단건 응답형 서비스면 LATENCY 힌트부터, 다중 요청이나 배치형 작업이면 THROUGHPUT 힌트부터 시작하세요. 두 결과가 서로 다르게 나오면 “어느 쪽이 더 빠른가”보다 “현재 서비스 목표가 어느 쪽인가”로 결정하는 편이 맞습니다.

    운영 전환 체크리스트: 저는 이 항목이 안 채워지면 배포 안 합니다

    • 산출물 이원화: ONNX와 OpenVINO IR을 같이 보관하고, 어떤 버전에서 변환했는지 기록합니다.
    • 입력 계약 문서화: shape, dtype, layout, 채널 순서를 문서와 테스트 샘플로 고정합니다.
    • 검증 샘플 고정: 대표 입력 세트를 정해 변환 전후 비교를 자동화합니다.
    • 성능 측정 분리: 모델 단독 수치와 API 전체 수치를 별도로 저장합니다.
    • 롤백 스위치 유지: OpenVINO 경로에 장애가 나면 ONNX Runtime으로 즉시 되돌릴 수 있어야 합니다.
    • cold start 대비: 웜업 전략과 캐시 디렉터리 사용 여부를 배포 스크립트에 포함합니다.

    여기서 많이 놓치는 게 롤백입니다. 배포 표준을 바꾸는 게 아니라 실행 경로를 하나 더 두는 것이라고 생각하면 결정이 조금 쉬워집니다. 운영 안정성은 새로운 엔진을 넣는 것보다, 문제 났을 때 되돌아갈 길이 있는지가 더 크게 좌우합니다.

    OpenVINO 도입 전후 선택 기준을 정리한 요약 이미지

    어떤 환경에서 OpenVINO 도입이 유리한지 한눈에 정리한 요약 이미지입니다.

    자주 묻는 질문

    ONNX가 이미 있는데 굳이 OpenVINO까지 가야 하나요?

    인텔 CPU 추론이 핵심 경로라면 검토 가치는 충분합니다. 다만 기준은 단순하지 않습니다. ONNX Runtime이 이미 안정적이고 실제 병목이 전처리나 API 레이어라면 굳이 바꿀 이유는 약합니다. 반대로 CPU 지연 편차나 cold start, 요청 처리 패턴 최적화가 고민이라면 OpenVINO를 별도 경로로 두는 편이 낫습니다.

    엣지 AI 환경에서 특히 의미가 큰가요?

    네, 특히 GPU 없는 소형 노드에서 그렇습니다. 다만 “엣지라서 무조건”은 아닙니다. 장점은 하드웨어 특화 최적화와 배포 단순성이고, 단점은 변환 산출물 관리와 호환성 검증이 추가된다는 점입니다. 그래서 저는 엣지 환경일수록 산출물 버전 관리와 입력 스키마 고정을 더 엄격하게 잡습니다.

    도입 순서는 어떻게 잡는 게 안전한가요?

    가장 안전한 순서는 이렇습니다. 학습 코드는 그대로 유지하고, ONNX를 공통 산출물로 두고, OpenVINO IR을 배포 전용 산출물로 추가하세요. 그다음 모델 단독 벤치마크, 기능 비교, 서비스 A/B 테스트 순으로 가면 됩니다. 운영 경로를 한 번에 갈아엎는 방식은 추천하지 않습니다.

    여기서 이렇게 결정하시면 됩니다

    제 판단 기준은 꽤 단순합니다. 인텔 CPU 추론이 핵심이고, ONNX 산출물이 이미 안정적이며, 지금 문제의 본체가 모델 실행 경로라면 OpenVINO 도입 쪽으로 가는 편이 맞습니다. 이 경우에는 학습 스택을 건드리지 말고, 배포 레이어만 OpenVINO로 분기하세요. 특히 실시간 API라면 LATENCY 힌트 중심으로, 배치 작업이라면 THROUGHPUT 힌트 중심으로 검증하면 방향이 빨리 잡힙니다.

    반대로 GPU 최적화 체계가 이미 굳어 있거나, 모델보다 전처리·후처리·네트워크가 더 느리다면 지금 당장 OpenVINO로 옮길 이유는 약합니다. 이런 상황에서는 엔진을 바꾸기보다 병목 측정부터 다시 하는 게 맞습니다. 그리고 커스텀 연산자가 많은 모델이라면 성능 실험보다 먼저 호환성 검증을 별도 작업으로 떼어 두세요. 실무에서는 이 순서를 지키는 팀이 결국 덜 헤맵니다.