콘텐츠로 이동

ONNX Runtime으로 CPU 배포

TL;DR

이 저장소의 경량 인코더를 CPU에서 실행했을 때 ONNX Runtime의 p50 latency는 PyTorch보다 31배 낮았고, 테스트 입력에서는 출력 차이가 없었습니다. GPU보다 느리지만 더 저렴한 인스턴스를 사용할 수 있습니다.


ONNX Runtime을 선택한 이유

같은 인스턴스(ml.c6i.2xlarge)와 모델(mDeBERTa 278M)을 사용해 SageMaker endpoint에서 측정한 결과입니다.

런타임 b=1 p50 정확도 PyTorch 대비 오차
PyTorch 1810 ms 4/4 기준
ONNX Runtime fp32 58 ms 4/4 0.000000
GPU PyTorch (참고) 28 ms 4/4 -

ONNX Runtime fp32의 p50 latency는 PyTorch보다 31배 낮았고, 테스트한 출력은 소수점 6자리까지 일치했습니다(maxdiff=0.000000).

GPU PyTorch보다 약 두 배 느리지만 CPU 인스턴스 비용은 더 낮습니다. 수십 ms 수준의 latency를 허용할 수 있다면 CPU와 ONNX Runtime 조합을 검토할 수 있습니다.

정밀도별 결과

다음 결과는 ONNX Runtime의 기본 CPU Execution Provider와 이 저장소의 mDeBERTa 모델을 사용한 실측입니다.

정밀도 결과
fp32 ✅ 정확도와 로딩 검증 완료
INT8 예측이 모두 neutral로 붕괴 (정확도 1/4)
fp16 변환 후 로드 실패 (Cast(to=FLOAT) 노드 128개와 타입 충돌)
bf16 ORT CPU EP가 네이티브 실행하지 않음

GPU 경로는 결론이 다릅니다

위 표는 ONNX Runtime CPU에서 확인한 것입니다. 같은 모델을 GPU TensorRT FP16으로 실행한 시나리오 07에서는 정확도 4/4를 유지하면서 p50 latency가 27.3ms에서 15.4ms로 감소했습니다.

따라서 이 결과는 DeBERTa 전체가 아니라 ONNX Runtime CPU 동적 양자화 경로에 한정해 해석해야 합니다.

TensorRT 경로에는 엔진 컴파일 시간이 추가됩니다. 첫 단건 호출은 47.7초였고, /tmp에 저장한 캐시는 컨테이너 재시작 시 사라졌습니다. trtexec로 미리 컴파일한 .plan을 배포할 수 있지만, TensorRT 엔진은 대상 GPU 아키텍처와 버전에 종속됩니다. 자세한 내용은 TensorRT engine compatibility를 참고하세요.

INT8 동적 양자화 결과

optimum의 ONNX Runtime 양자화avx512_vnni, per_channel 설정으로 적용하면 모든 테스트 입력이 neutral로 분류되었습니다. Latency는 451ms로 감소했지만 정확도는 1/4이므로 이 구성에는 사용할 수 없습니다.

DeBERTa의 disentangled attention(p2c, c2p)이 영향을 받았을 가능성이 있지만, 연산별 오차 분석으로 확인하지는 않았습니다. ONNX Runtime의 양자화 방식과 제약은 공식 quantization 문서에 정리되어 있습니다.

양자화 모델 파일을 명시해야 합니다

ORTModelForSequenceClassification.from_pretrained(quantized_dir)만 호출하면 model_quantized.onnx가 아니라 기본 fp32 모델을 선택할 수 있습니다. 양자화 파일을 로드하려면 file_name="model_quantized.onnx"를 명시해야 합니다.

fp16 변환 결과

ONNX Runtime의 float16 변환 도구로 변환했을 때, DeBERTa ONNX 그래프의 Cast(to=FLOAT) 노드와 fp16 출력 타입이 충돌했습니다.

Type Error: Type (tensor(float16)) of output arg (/deberta/embeddings/Cast_output_0)
does not match expected type (tensor(float)).

keep_io_typesTrueFalse로 각각 설정해도 같은 오류가 발생했습니다. 또한 ONNX Runtime CPU 경로에서는 fp16 연산이 제한되므로, 이 구성에서는 fp32를 사용했습니다.

transformers 5.x와 변환 도구의 의존성 분리

이 저장소에서 사용한 optimum-onnx 버전은 transformers>=4.36,<4.58을 요구하므로 프로젝트의 transformers 5.14.1과 함께 설치할 수 없습니다. 모델 변환 환경과 서빙 환경을 분리해 이 문제를 해결했습니다.

단계 필요한 패키지 transformers 5.x
변환 (1회성) optimum[onnxruntime] ❌ 별도 venv
서빙 onnxruntime ✅ 제약 없음

서빙 컨테이너에는 optimum이 필요하지 않습니다. onnxruntime만 설치한 프로젝트 환경 (transformers 5.14.1, optimum 미설치)에서 정상 동작을 확인했습니다.

scripts/export_onnx.py는 별도 가상 환경을 만들고 Optimum ONNX Runtime 변환 경로를 실행한 뒤, PyTorch 출력과 비교해 결과를 검증합니다.

uv run python 02_cpu_cohost_multicontainer/scripts/export_onnx.py --models mdeberta minilm

torch.onnx.export와의 비교

torch.onnx.export는 프로젝트 환경에서 바로 사용할 수 있지만, 이 경로로 변환한 모델의 latency는 optimum-onnx 경로보다 약 세 배 높았습니다.

변환 경로 latency PyTorch 대비 오차
optimum-onnx (별도 venv) 35~52 ms 0.000000
torch.onnx.export (본체) 160 ms 0.005859

변환 로그에는 Could not find a CPU kernel ... MatMulAddFusion 경고가 기록되어 일부 graph fusion이 적용되지 않은 것으로 보입니다. 변환은 일회성 작업이므로 이 저장소에서는 별도 가상 환경에서 optimum-onnx를 사용합니다.

서빙 구성 시 주의 사항

onnxruntime 패키지 추가

이 저장소에서 사용한 HF DLC에는 onnxruntime이 포함되어 있지 않습니다. code/requirements.txt에 추가하지 않으면 endpoint 생성과 상태 확인은 통과하지만 첫 추론 요청에서 실패합니다.

ModelError: "No module named 'onnxruntime'"
# 02_cpu_cohost_multicontainer/code/requirements.txt
onnxruntime>=1.28.0

optimum은 서빙에 필요하지 않으며 컨테이너의 transformers 5.5.3과 의존성 충돌을 일으킬 수 있으므로 포함하지 않습니다.

워커 수와 메모리 사용량

SAGEMAKER_MODEL_SERVER_WORKERS를 지정하지 않으면 여러 워커가 생성되고, 각 워커의 ONNX 세션이 모델을 별도로 로드합니다. mDeBERTa ONNX 모델은 1137MB이므로 ml.c6i.2xlarge(16GB)에서 컨테이너 두 개가 각각 워커 여덟 개를 생성하면 메모리가 부족해집니다.

이 경우 CloudWatch에 명시적인 OOM 메시지가 남지 않았습니다. 큰 모델의 워커는 model_fn 실행 직후 종료되고 작은 모델의 워커만 유지되었습니다.

"SAGEMAKER_MODEL_SERVER_WORKERS": "1",
"ORT_INTRA_OP_THREADS": "0",   # 0 = os.cpu_count()

이 구성에서는 워커 하나와 ONNX Runtime 내부 스레드를 함께 사용하는 방식이 메모리 대비 throughput이 가장 높았습니다. ORT_INTRA_OP_THREADS=0의 동작은 ONNX Runtime threading 문서를 참고하세요.

tokenizer 출력 필터링

tokenizer는 token_type_ids를 만들지만 이 모델의 ONNX 그래프는 input_idsattention_mask만 입력으로 받습니다.

input_names = {i.name for i in session.get_inputs()}
feed = {k: v for k, v in enc.items() if k in input_names}

Microsoft Olive 실험 결과

Microsoft Olive로 ONNX 최적화 탐색을 추가로 시도했습니다. OrtTransformersOptimizationmodel_type: "bert", opt_level: 1로 실행한 결과는 다음과 같습니다.

  • 19분 동안 산출물과 진행 로그가 생성되지 않아 실행을 중단했습니다(캐시 36KB).
  • CPU 사용률은 99.8%를 유지했고 메모리는 최대 27GB까지 증가했습니다.
  • BERT용 fusion 규칙이 DeBERTa의 disentangled attention과 일치하지 않아 1.1GB 그래프를 반복해서 처리했을 가능성이 있습니다. 프로파일링으로 확인한 원인은 아닙니다.

optimum 변환은 23초에 완료되었고, 변환된 모델은 PyTorch보다 6.6배 낮은 latency를 기록했습니다. 이 결과만으로 Olive가 다른 인코더 모델에도 적합하지 않다고 일반화할 수는 없습니다.

관련 문서