13년차의 서버실

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

[태그:] ONNX Runtime

  • [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은 제값을 합니다.

  • [AI] LLM 추론 성능 벤치마크: TensorRT vs ONNX Runtime

    [AI] LLM 추론 성능 벤치마크: TensorRT vs ONNX Runtime

    [AI 인프라] LLM 추론 성능 벤치마크: NVIDIA TensorRT vs ONNX Runtime

    LLM 추론 성능 벤치마크를 해보면, 병목은 생각보다 GPU 스펙 한 줄로 설명되지 않더라고요. 같은 GPU, 같은 모델이라도 어떤 런타임이 어떤 shape를 얼마나 안정적으로 처리하느냐, 그리고 초기화 비용과 토큰 생성 구간을 어떻게 분리해 봤느냐에 따라 체감 성능이 꽤 달라집니다. 저도 초반에는 TensorRT 쪽 숫자가 더 높게 나오면 무조건 이긴 줄 알았는데, 실제 운영에선 첫 요청 지연, 엔진 캐시, 입력 길이 분산, Python 생성 루프 오버헤드가 더 크게 문제 되는 경우가 많았습니다.

    이번 글은 단순 비교가 아니라, 같은 조건에서 TensorRT와 ONNX Runtime를 어떻게 공정하게 재는지, 그리고 측정값이 실제 서비스 판단으로 어떻게 이어지는지에 집중했습니다. 홈랩, 사내 검증 서버, PoC 환경에서 바로 써먹을 수 있게 명령어와 체크 포인트를 실무 기준으로 묶었습니다.

    로컬 LLM 추론 성능 벤치마크를 위한 TensorRT와 ONNX Runtime 아키텍처 이미지

    로컬 GPU 환경에서 모델 로딩, 토크나이저, 런타임, 추론 결과까지 이어지는 전체 흐름을 한눈에 보여주는 아키텍처 이미지입니다.

    1. 왜 TensorRT와 ONNX Runtime를 같이 보게 되나

    실무에서 둘을 같이 보는 이유는 단순합니다. TensorRT는 NVIDIA GPU 고정 환경에서 최대 성능을 노릴 때 강력한 선택지고, ONNX Runtime은 모델 비교, 환경 이동, 운영 단순성을 챙기기 좋은 기준선이기 때문입니다. 둘은 경쟁 관계이면서도, 실제론 비교용 baseline과 최적화용 target이라는 식으로 같이 쓰는 경우가 많습니다.

    • TensorRT: 엔진 빌드 비용과 shape 관리 부담을 감수하는 대신, 고정 워크로드에서는 높은 처리량과 낮은 지연을 기대할 수 있습니다.
    • ONNX Runtime: CUDA Execution Provider만 붙여도 baseline이 빨리 잡히고, 디버깅 난도가 비교적 낮습니다.
    • ONNX Runtime + TensorRT Execution Provider: 코드 경로는 유지하면서 일부 최적화를 노릴 수 있지만, 문제 원인 분리가 오히려 더 어려워지는 경우도 있습니다.

    여기서 제가 중요하게 보는 건 하나입니다. 런타임 성능만 빠른지, 전체 생성 경로가 빠른지를 분리해야 한다는 점입니다. TensorRT 엔진 레벨 숫자가 좋아도 실제 서비스 응답이 별 차이 없으면, 대개 병목은 런타임 바깥에 있습니다. 반대로 ONNX Runtime가 조금 느려 보여도 재현성과 운영 안정성이 높으면, 초기 서비스에는 그쪽이 더 값어치 있는 선택일 수 있습니다.

    2. LLM 추론 성능 벤치마크에서 꼭 봐야 할 지표

    LLM 벤치마크는 이미지 분류처럼 한 번 넣고 한 번 끝나는 추론과 다릅니다. 프롬프트 처리(prefill)와 토큰 생성(decode)이 분리되고, KV cache가 붙고, 입력 길이 편차가 커서 평균값 하나로 끝내면 판단을 그르치기 쉽습니다. 그래서 LLM 추론 성능 벤치마크에선 평균 속도보다 구간별 특성을 같이 봐야 합니다.

    2-1. 꼭 맞춰야 하는 비교 조건

    1. 같은 모델 가중치, 같은 토크나이저, 같은 전처리 조건을 씁니다.
    2. 가능하면 같은 ONNX 그래프를 기준으로 비교하고, TensorRT 11.x처럼 강타입 빌드가 필요한 경우에는 사전 precision 변환 단계까지 함께 기록합니다.
    3. 정밀도는 FP16, BF16, FP32, INT8 중 하나로 고정합니다. 섞어 재면 비교가 무너집니다.
    4. 프롬프트 길이와 생성 토큰 수를 고정하고, 가능하면 짧은 입력/긴 입력 두 구간으로 나눠 봅니다.
    5. 워밍업 횟수와 측정 구간을 분리합니다. 첫 요청을 평균에 섞으면 해석이 흐려집니다.
    6. 배치 크기와 동시성은 별도 축입니다. 둘을 한 번에 바꾸면 원인을 찾기 어렵습니다.
    7. 전원 상태, 클럭 상태, 백그라운드 프로세스를 고정합니다. 특히 데스크톱 환경에서는 이 영향이 꽤 큽니다.

    2-2. 해석할 지표

    • TTFT(Time To First Token): 사용자 체감에 가장 직접적입니다. 첫 토큰이 느리면 답답하게 느껴지거든요.
    • Tokens/sec: 긴 출력에서 중요합니다. decode 구간 효율이 여기서 드러납니다.
    • p95 latency: 운영 안정성을 보려면 평균보다 이 수치가 더 중요합니다.
    • GPU memory usage: 속도가 비슷하면 메모리 여유가 있는 쪽이 운영성이 좋습니다.
    • Engine build / session init overhead: 재시작이 잦은 환경, 다중 모델 환경에서는 이 비용이 실제 총비용으로 이어집니다.
    • CPU time: 토크나이저, 후처리, Python 루프가 병목이면 GPU 최적화 이득이 상쇄됩니다.

    제가 실제로 더 신뢰하는 건 최고 기록이 아니라 세 번 돌렸을 때 비슷하게 나오는지입니다. 단일 최고치만 잘 나온 벤치마크는 운영 판단 기준으로는 거의 쓸모가 없습니다.

    3. TensorRT 최적화와 ONNX Runtime 비교를 위한 기준선 만들기

    로컬 LLM 추론 성능 벤치마크에서 제일 먼저 해야 할 일은, 화려한 최적화가 아니라 분리 측정입니다. 저는 보통 아래 네 구간으로 나눕니다.

    1. 모델 준비 구간: ONNX export, shape 정의, 동적 축 확인
    2. 초기화 구간: TensorRT 엔진 빌드 또는 ONNX Runtime 세션 생성
    3. prefill 구간: 긴 프롬프트를 한 번에 넣는 첫 추론
    4. decode 구간: 토큰을 한 개씩 이어서 생성하는 반복 추론

    이 네 구간을 섞어서 평균 latency 하나로 덮으면 실무에서는 해석이 거의 틀어집니다. TensorRT는 엔진 빌드가 무겁고, ONNX Runtime는 세션 초기화와 provider 구성 영향이 큽니다. LLM은 여기에 prefill과 decode의 성격까지 달라서, 어느 구간에서 이겼는지가 곧 선택 기준이 됩니다.

    항목 TensorRT ONNX Runtime 실무 판단 포인트
    최대 성능 잠재력 높음 중간~높음 GPU와 워크로드가 고정이면 TensorRT 투자 가치가 커집니다.
    초기 실험 속도 느린 편 빠른 편 모델 export 직후 baseline을 잡을 때는 ONNX Runtime가 훨씬 편합니다.
    입력 길이 변동 대응 shape 전략에 민감 상대적으로 유연 프롬프트 길이 편차가 크면 TensorRT 최적화 이점이 줄어들 수 있습니다.
    장애 분석 난이도 높음 낮음 팀에 추론 엔진 디버깅 경험이 없으면 운영 비용이 성능 이득을 먹어버릴 수 있습니다.
    재기동 비용 민감도 엔진 캐시 전략 필요 세션 재생성 중심 짧은 수명 컨테이너 환경에서는 캐시 설계가 성능만큼 중요합니다.
    권장 시작점 최적화 단계 기준선 단계 바로 TensorRT로 들어가기보다 ONNX Runtime로 병목 위치를 먼저 확인하는 편이 낫습니다.

    4. 실전 구현 1: TensorRT 엔진 생성과 기본 측정

    TensorRT는 <code>trtexec로 첫 감을 잡는 게 가장 빠릅니다. 다만 LLM에서는 단순히 FP16 옵션 하나만 넣고 끝내면 부족합니다. 입력 shape 범위를 명시하지 않으면 엔진이 실제 프롬프트 분포와 어긋날 수 있고, 그 결과 기대 이하의 최적화가 나올 수 있습니다.

    아래 예시는 TensorRT 10.x 계열에서 많이 쓰는 방식입니다. TensorRT 11.x부터는 일부 precision 관련 플래그가 바뀌었거나 제거됐기 때문에, 실제 사용 전에는 trtexec --help로 로컬 버전을 꼭 확인해 두는 게 좋습니다. 이거 안 보고 그대로 복붙했다가 한 번씩 막히더라고요.

    trtexec \
      --onnx=model.onnx \
      --saveEngine=model_fp16.plan \
      --fp16 \
      --minShapes=input_ids:1x16,attention_mask:1x16 \
      --optShapes=input_ids:1x512,attention_mask:1x512 \
      --maxShapes=input_ids:1x2048,attention_mask:1x2048 \
      --warmUp=200 \
      --duration=60 \
      --verbose

    이 명령에서 실무적으로 중요한 건 아래입니다.

    • --minShapes, --optShapes, --maxShapes: 동적 입력 모델이라면 사실상 핵심입니다. opt shape는 가장 자주 들어오는 프롬프트 길이대로 잡는 편이 낫습니다.
    • --warmUp: 보통 워밍업 시간 기준으로 해석합니다. 반복 횟수라고 착각하면 결과 비교가 꼬일 수 있습니다.
    • --verbose: 성능보다 먼저 봐야 할 건 변환 실패 레이어, precision 강등, 지원되지 않는 연산 흔적입니다.

    LLM에서 자주 놓치는 건 shape 전략이 사실상 성능 전략이라는 점입니다. 짧은 프롬프트 위주 서비스인데 max shape를 과하게 크게 잡으면 메모리와 최적화 효율이 함께 손해를 봅니다. 반대로 입력 길이 분산이 큰데 지나치게 좁게 잡으면 실제 요청에서 비효율이 생길 수 있습니다.

    엔진 생성과 추론을 분리해서 보고 싶다면 저장된 엔진으로 다시 측정하는 편이 낫습니다.

    trtexec \
      --loadEngine=model_fp16.plan \
      --shapes=input_ids:1x512,attention_mask:1x512 \
      --warmUp=200 \
      --duration=60

    이렇게 해야 엔진 빌드 시간이 빠진 순수 추론 구간을 보기 좋습니다. 엔진 빌드 시간을 평균 latency에 섞는 실수는 생각보다 자주 나오고, 그 순간부터 비교표는 의미가 많이 줄어듭니다.

    TensorRT 최적화와 GPU 가속 흐름을 설명하는 LLM 추론 성능 벤치마크 이미지

    ONNX 모델이 TensorRT 엔진으로 변환되고, 워밍업과 프로파일링을 거쳐 성능 지표가 수집되는 흐름을 설명하는 이미지입니다.

    5. 실전 구현 2: ONNX Runtime baseline 측정

    ONNX Runtime의 강점은 baseline을 빠르게 만들 수 있다는 점입니다. 저는 여기서 단순 평균 latency보다 provider가 제대로 붙었는지, 세션 옵션이 적절한지, 실제 생성 루프와 분리해 엔진 수준 시간을 볼 수 있는지를 먼저 확인합니다.

    import time
    import numpy as np
    import onnxruntime as ort
    
    session_options = ort.SessionOptions()
    session_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
    session_options.log_severity_level = 0
    session_options.log_verbosity_level = 1
    
    providers = [
        (
            "CUDAExecutionProvider",
            {
                "device_id": 0,
                "arena_extend_strategy": "kNextPowerOfTwo",
                "cudnn_conv_algo_search": "EXHAUSTIVE",
                "do_copy_in_default_stream": True,
            },
        ),
        "CPUExecutionProvider",
    ]
    
    session = ort.InferenceSession(
        "model.onnx",
        sess_options=session_options,
        providers=providers,
    )
    
    print("active providers:", session.get_providers())
    input_names = [x.name for x in session.get_inputs()]
    print("inputs:", input_names)
    
    feeds = {
        "input_ids": np.ones((1, 16), dtype=np.int64),
        "attention_mask": np.ones((1, 16), dtype=np.int64),
    }
    
    for _ in range(20):
        session.run(None, feeds)
    
    latencies_ms = []
    for _ in range(100):
        start = time.perf_counter()
        session.run(None, feeds)
        latencies_ms.append((time.perf_counter() - start) * 1000)
    
    latencies_ms.sort()
    p95 = latencies_ms[int(len(latencies_ms) * 0.95) - 1]
    print(f"avg latency: {sum(latencies_ms)/len(latencies_ms):.3f} ms")
    print(f"p95 latency: {p95:.3f} ms")

    이 코드는 baseline용입니다. 실제 LLM 생성 벤치마크로 가려면 past_key_values, position_ids, attention_mask shape, 그리고 prefill/decode 분리를 모델 구조에 맞게 넣어야 합니다. 중요한 건 엔진 호출 성능과 애플리케이션 생성 루프 성능을 따로 수집하는 습관입니다.

    ONNX Runtime 안에서 TensorRT Execution Provider를 시험해 보고 싶다면 아래처럼 provider를 분리해 보는 방법도 있습니다. 이 경우는 성능 수치보다 캐시 동작과 fallback 여부를 먼저 보는 편이 낫습니다.

    providers = [
        (
            "TensorrtExecutionProvider",
            {
                "trt_engine_cache_enable": True,
                "trt_engine_cache_path": "./trt_cache",
                "trt_fp16_enable": True,
            },
        ),
        "CUDAExecutionProvider",
        "CPUExecutionProvider",
    ]
    
    session = ort.InferenceSession("model.onnx", providers=providers)

    GPU 상태는 꼭 같이 봐야 합니다. 벤치마크 로그만 보면 빨라 보이는데 GPU util이 낮고 CPU 한 코어만 치솟는 경우가 꽤 많습니다.

    nvidia-smi dmon -s pucm -d 1

    여기서 저는 네 가지를 같이 봅니다. power는 클럭 유지 여부, util은 GPU 바쁨 정도, clock은 스로틀링 여부, memory는 컨텍스트 길이와 배치 변화의 영향을 읽는 데 도움이 됩니다.

    6. 주의사항과 트러블슈팅: 제가 자주 겪었던 문제들

    벤치마크는 숫자를 뽑는 작업이 아니라 왜 그 숫자가 나왔는지 설명하는 작업에 가깝습니다. 아래 문제들은 LLM 추론 비교에서 반복해서 나오는 실패 모드입니다. 실제로는 런타임보다 입력 경로에서 막히는 경우도 꽤 많더라고요.

    6-1. 같은 모델인데 결과가 안 맞는 경우

    • 토크나이저 옵션 차이로 입력 길이가 달라집니다. 성능 비교 전에 실제 token count부터 맞춰야 합니다.
    • padding 방향과 최대 길이 설정이 다르면 연산량이 달라집니다.
    • 배치 크기 자동 조정이나 동적 batching이 켜져 있으면 공정 비교가 깨집니다.
    • FP16과 FP32, 또는 BF16을 섞어 재면 런타임 차이보다 precision 차이가 더 크게 나옵니다.

    근본 원인은 대부분 같은 모델을 보고 있다고 착각하는 것입니다. 파일명이 같아도 export 옵션, 동적 축, 입력 schema가 달라지면 사실상 다른 워크로드입니다.

    6-2. TensorRT 엔진은 빠른데 실제 생성 속도는 애매한 경우

    이건 정말 자주 봅니다. 원인은 대체로 런타임 바깥에 있습니다.

    • 토크나이저가 CPU 병목입니다. 짧은 응답에서는 이 영향이 생각보다 큽니다.
    • Python 루프가 토큰마다 GPU 호출을 감싸면서 왕복 오버헤드를 만듭니다.
    • KV cache 레이아웃이나 메모리 복사가 비효율적입니다.
    • 입력 shape가 계속 바뀌어 TensorRT 최적화가 기대만큼 먹지 않습니다.
    • 엔진 캐시가 제대로 재사용되지 않아 재시작 후마다 준비 비용이 다시 듭니다.

    GPU util은 낮은데 TTFT와 p95가 큰 경우는 계산 문제가 아니라 상위 경로 병목일 가능성이 큽니다. 이때 GPU 커널을 더 최적화하는 건 우선순위가 아닙니다.

    6-3. ONNX Runtime가 예상보다 느린 경우

    • CUDAExecutionProvider가 실제로 활성화됐는지 먼저 확인합니다.
    • 지원되지 않는 연산이 CPU로 fallback 되는지 로그를 봅니다.
    • 세션 옵션의 그래프 최적화가 빠져 있지 않은지 확인합니다.
    • ONNX export에서 dynamic axis를 과하게 열어 둬 shape 추론과 최적화가 약해진 경우를 의심합니다.
    • 입력 텐서를 매 요청마다 새로 할당하면서 호스트 메모리 오버헤드를 키우는 경우도 꽤 있습니다.

    실무에서 제가 제일 경계하는 건 런타임이 느린 게 아니라 export 품질이 낮은 경우입니다. ONNX Runtime 성능 문제처럼 보여도, 실제로는 모델 export가 비효율적으로 된 사례가 적지 않습니다.

    ONNX Runtime 비교를 위한 CUDA Execution Provider 구성 이미지

    ONNX Runtime 세션 초기화, 그래프 최적화, CUDA Execution Provider 적용 구조를 이해하기 쉽게 풀어낸 이미지입니다.

    7. LLM 추론 성능 벤치마크 결과, 숫자보다 읽는 법이 먼저입니다

    측정이 끝났다면 이제 숫자를 읽어야 합니다. 저는 아래처럼 봅니다. 이 구간이 사실 제일 중요합니다. 숫자만 높다고 바로 정답은 아니거든요.

    1. TTFT가 길다: 엔진/세션 초기화, 긴 프롬프트 prefill, 토크나이저, 첫 decode 경로를 의심합니다.
    2. 평균 latency는 좋은데 p95가 나쁘다: 동시성 구간의 큐잉, 메모리 압박, shape 분산이 원인일 가능성이 큽니다.
    3. tokens/sec가 잘 안 오른다: decode 루프의 CPU 개입, KV cache 처리, 작은 배치가 문제일 수 있습니다.
    4. GPU util이 낮다: GPU가 한가한데 응답은 느리다면 애플리케이션 경로를 먼저 봐야 합니다.
    5. GPU memory가 높고 출렁인다: 최대 시퀀스 길이, 동시성, 정밀도 조합이 과한 신호일 수 있습니다.

    홈랩이나 PoC에서는 특히 재현성이 중요합니다. 저는 최소 3회 반복, 워밍업 제외, 첫 요청 별도 기록, prefill/decode 분리, GPU 모니터링 병행 이 다섯 가지를 기본으로 둡니다. 이렇게 해 두면 나중에 팀 내에서 숫자 해석으로 싸울 일이 확 줄어듭니다.

    관측 결과 자주 나오는 근본 원인 먼저 할 조치 TensorRT 쪽 힌트 ONNX Runtime 쪽 힌트
    첫 요청만 유독 느림 엔진 빌드, 세션 초기화, 캐시 미적용 초기화 구간을 분리 측정 엔진 캐시와 사전 빌드 여부 확인 세션 재사용과 provider 초기화 로그 확인
    긴 프롬프트에서 급격히 느려짐 prefill 비용 증가, shape 최적화 불일치 입력 길이 구간별로 분리 측정 opt shape를 실제 분포에 맞게 재설계 export dynamic axis와 입력 텐서 구성 점검
    GPU util은 낮은데 응답은 느림 토크나이저, Python 루프, CPU fallback CPU 사용률과 provider 로그 확인 엔진 바깥 호출 오버헤드 축소 fallback 연산과 메모리 할당 패턴 확인
    평균은 괜찮은데 p95가 나쁨 동시성, 큐잉, 메모리 압박 동시성 단계별로 별도 실행 shape 편차와 메모리 상한 재검토 세션 옵션, provider 설정, CPU 개입 확인

    결과 정리 시에는 숫자 순위표보다 어떤 조건에서 우세했는지를 남기는 편이 훨씬 유용합니다. TensorRT가 이긴 게 아니라, 고정 shape의 긴 decode 구간에서 TensorRT가 우세했다처럼 써야 다음 의사결정에 바로 써먹을 수 있습니다.

    LLM 추론 성능 벤치마크 결과와 GPU 사용률을 시각화한 이미지

    TTFT, tokens/sec, GPU utilization, memory usage를 한 화면에서 비교하는 대시보드 형태의 결과 이미지입니다.

    8. 실무 추천과 다음 단계

    제가 현업에서 내리는 기준은 꽤 단순합니다. 먼저 ONNX Runtime로 병목 위치를 밝히고, 그다음 TensorRT로 돈 되는 구간만 최적화하는 흐름이 가장 덜 아픕니다. 처음부터 TensorRT로 들어가면 성능은 빨리 보여도, 왜 빨라졌는지와 왜 안 빨라졌는지를 분리하기 어려운 경우가 많습니다.

    • 이럴 땐 ONNX Runtime: 모델을 자주 바꾼다, 비교 실험이 많다, 디버깅 시간을 아껴야 한다, NVIDIA 고정 배포가 아니다.
    • 이럴 땐 TensorRT: GPU가 고정돼 있다, 긴 기간 같은 워크로드를 운영한다, p95와 처리량을 끝까지 밀어야 한다, 엔진 관리까지 감당할 팀이 있다.
    • 이럴 땐 둘 다 본다: ONNX Runtime baseline은 안정적인데 비용이 아쉽고, 병목이 GPU 쪽이라는 근거가 이미 있다.

    조금 더 직설적으로 말씀드리면, PoC 단계에서 바로 TensorRT를 주력 경로로 잡는 건 대개 이릅니다. 반대로 이미 NVIDIA GPU 기반으로 워크로드가 굳어 있고, 하루 종일 같은 패턴의 요청을 받는다면 TensorRT 최적화는 충분히 투자할 만합니다. 이 경우에는 성능 향상 자체보다도 예측 가능한 지연과 재현성이 장점으로 돌아옵니다.

    1. 개발 속도, 실험 반복, 모델 교체 빈도가 중요하면 ONNX Runtime로 시작하는 편이 맞습니다.
    2. 배포 환경이 NVIDIA GPU로 고정이고, TTFT보다 지속 처리량과 p95가 더 중요하면 TensorRT 우선 검토가 맞습니다.
    3. 둘 중 하나로 바로 못 정하겠다면, 같은 ONNX 기준으로 prefill과 decode를 분리 측정한 뒤 TTFT, tokens/sec, p95, GPU memory 네 항목으로 결정하는 게 가장 안전합니다.

    다음 단계도 명확합니다. baseline이 흔들리면 런타임 최적화 전에 export와 입력 경로부터 정리하시고, baseline이 안정적인데 GPU 병목이 분명하면 그때 TensorRT로 들어가시면 됩니다. 이 순서를 지키면 삽질 시간이 확실히 줄어듭니다.

    관련해서 모델 경량화나 GPU 가속 운영 팁이 더 궁금하시면, 블로그의 다른 AI 인프라 글도 같이 읽어보시면 흐름이 훨씬 잘 잡힐 겁니다.

    TensorRT와 ONNX Runtime 선택 기준을 요약한 LLM 추론 성능 벤치마크 이미지

    어떤 상황에서 TensorRT를 고르고, 어떤 상황에서 ONNX Runtime를 고르면 되는지 한 장으로 정리한 요약 이미지입니다.

    FAQ

    Q. ONNX Runtime 안에서 TensorRT도 쓸 수 있나요?

    네, TensorRT Execution Provider로 구성할 수 있습니다. 다만 성능만 보지 말고, 엔진 캐시 재사용, fallback 경로, 디버깅 난이도를 같이 보셔야 합니다. 코드 경로는 단순해 보여도 장애 분석은 더 어려워질 수 있습니다.

    Q. LLM 추론 성능 벤치마크에서 숫자가 자꾸 달라집니다.

    워밍업, 프롬프트 길이, 출력 길이, 배치 크기, 전원/클럭 상태를 먼저 고정해 보세요. 거기에 백그라운드 프로세스와 첫 요청 분리 기록까지 넣으면 흔들림 원인을 상당히 빨리 좁힐 수 있습니다.

    Q. AI 모델 경량화가 꼭 필요할까요?

    메모리가 빠듯하거나 동시성을 올려야 한다면 우선순위가 높습니다. 다만 INT8이나 더 공격적인 최적화는 정확도 검증과 함께 가야 해서, baseline 없이 바로 들어가면 성능은 빨라져도 판단은 더 어려워질 수 있습니다.