[onnxruntime] ONNX Runtime 스레드 풀의 지능형 대기: Exponential Backoff 도입으로 성능 및 전력 효율성 향상
PR 링크: microsoft/onnxruntime#28096 상태: Merged | 변경: +None / -None
들어가며
ONNX Runtime은 딥러닝 모델 추론을 위한 고성능 라이브러리로, 다양한 하드웨어 가속기를 지원하며 최적의 성능을 제공하기 위해 끊임없이 발전하고 있습니다. 특히, 멀티스레딩 환경에서의 효율성은 추론 속도와 시스템 자원 활용에 직접적인 영향을 미칩니다. 이번 PR(#28788)은 ONNX Runtime의 스레드 풀 내부에서 작업자 스레드가 유휴 상태일 때 발생하는 '스핀 루프(spin loop)' 동작을 개선하여, 성능 향상과 전력 소비 감소라는 두 마리 토끼를 잡고자 합니다.
기존 ONNX Runtime은 스레드가 작업을 기다릴 때 SpinPause() 명령어를 반복적으로 호출하며 CPU를 소모하는 스핀 루프를 사용했습니다. 이는 짧은 대기 시간에는 효율적일 수 있지만, 불필요하게 높은 CPU 사용률과 전력 소모를 유발할 수 있습니다. 특히 하이브리드 아키텍처(P-core와 E-core)나 모바일 환경에서는 이러한 비효율성이 더욱 두드러질 수 있습니다.
이 PR은 spin_backoff_max라는 새로운 옵션을 도입하여 'Exponential Backoff' 전략을 스핀 루프에 적용합니다. 이 전략은 스핀 루프의 각 반복마다 SpinPause() 호출 횟수를 기하급수적으로 늘려(1, 2, 4, ...), CPU 사용 밀도를 낮추고 전력 효율성을 높이는 것을 목표로 합니다. 동시에, 이전 PR에서 도입된 spin_duration_us 옵션과 결합하여 스핀 루프의 총 대기 시간을 일정하게 유지하도록 설계되었습니다. 즉, '얼마나 오래' 대기할지와 '얼마나 집중적으로' 대기할지를 독립적으로 제어할 수 있게 된 것입니다.
이번 글에서는 이 PR의 코드 변경 사항을 상세히 분석하고, Exponential Backoff 전략이 ONNX Runtime의 성능과 효율성에 어떤 긍정적인 영향을 미치는지, 그리고 실제 벤치마크 결과를 통해 그 효과를 검증해 보겠습니다.
코드 분석
이번 PR은 주로 core/platform/EigenNonBlockingThreadPool.h 파일의 스레드 풀 구현과 관련 설정, 그리고 테스트 도구를 중심으로 변경되었습니다.
1. core/platform/EigenNonBlockingThreadPool.h: 핵심 스레드 풀 로직 개선
가장 핵심적인 변경은 스레드 풀의 유휴 대기 로직을 담당하는 ThreadPoolTempl 클래스 내부에 있습니다. 새로운 ThreadPoolWaiter 클래스가 추가되었고, 기존 WorkerLoop 메서드의 스핀 루프 로직이 수정되었습니다.
ThreadPoolWaiter 클래스 추가:
이 클래스는 Exponential Backoff 로직을 캡슐화합니다. wait() 메서드는 max_backoff_ 값에 따라 SpinPause() 호출 횟수를 결정합니다. 초기에는 1번의 SpinPause()를 호출하고, 이후 호출마다 pause_time_을 두 배로 늘리며 최대 max_backoff_까지 증가시킵니다. 이는 스핀 루프의 각 반복마다 SpinPause() 호출 횟수를 점진적으로 늘려, CPU 사용 밀도를 낮추는 효과를 가져옵니다.
// Before (기존 로직의 일부, SpinPause() 직접 호출)
// ...
for (int i = 0; i < spin_count_ && !done_; i++) {
// ...
onnxruntime::concurrency::SpinPause();
}
// ...
// After (새로운 ThreadPoolWaiter 사용)
ThreadPoolWaiter waiter{spin_backoff_max_};
int steal_countdown = steal_interval_;
for (int i = 0; i < spin_count_ && !done_; i++) {
// ...
if (waiter.wait([&]() {
// ... interrupt check ...
})) {
break;
}
}
ScaleSpinCountForBackoff 함수 추가:
Exponential Backoff를 적용할 때, spin_duration_us로 설정된 총 스핀 대기 시간(wall-clock budget)을 유지하기 위해 스핀 루프의 총 반복 횟수(spin_count_)를 조정합니다. spin_backoff_max 값으로 spin_count_를 나누어, 각 반복에 더 많은 SpinPause()가 포함되더라도 전체 스핀 시간은 비슷하게 유지되도록 합니다.
// Before (기존 spin_count_ 계산)
spin_count_(ComputeSpinCount(spin_duration_us)),
// After (새로운 spin_count_ 계산)
spin_backoff_max_(NormalizeBackoff(spin_backoff_max)),
spin_count_(ScaleSpinCountForBackoff(ComputeSpinCount(spin_duration_us), spin_backoff_max_)),
NormalizeBackoff 함수 추가:
사용자가 입력한 spin_backoff_max 값을 유효한 범위(최소 1, 최대 kSpinBackoffMaxLimit)로 조정합니다. 1은 백오프가 없음을 의미하며, 이는 기존 동작과 동일합니다.
WorkerLoop 내 waiter.wait() 호출:
기존의 직접적인 SpinPause() 호출 대신, 새로 추가된 ThreadPoolWaiter의 wait() 메서드를 호출하도록 변경되었습니다. 이 wait() 메서드는 내부적으로 SpinPause()를 여러 번 호출할 수 있으며, 동시에 작업이 들어왔는지 또는 종료 신호가 있는지(should_interrupt() 콜백)를 주기적으로 확인합니다.
2. 설정 및 구성 파이프라인 변경
새로운 spin_backoff_max 옵션을 사용자가 설정하고 ONNX Runtime 내부로 전달하기 위한 여러 파일에서 관련 변경이 이루어졌습니다.
onnxruntime_session_options_config_keys.h:session.intra_op.spin_backoff_max및session.inter_op.spin_backoff_max라는 새로운 설정 키가 추가되었습니다. 이를 통해 사용자는 세션 옵션에서 직접 백오프 최대값을 지정할 수 있습니다.threadpool.h:ThreadPool생성자에spin_backoff_max파라미터가 추가되었습니다. 기본값은1로 설정되어 기존 동작과의 호환성을 유지합니다.threadpool.cc:ThreadPoolTempl생성자로spin_backoff_max값이 전달됩니다.ort_session_options_config_keys.h:OrtThreadPoolParams구조체에spin_backoff_max필드가 추가되었습니다.inference_session.cc: 새로운ParseSpinBackoffMax()헬퍼 함수가 추가되어 설정 키로부터 값을 파싱하고, 이를OrtThreadPoolParams에 적용합니다. 또한,allow_intra_op_spinning및allow_inter_op_spinning설정에 따라 백오프 설정이 적용되도록 로직이 개선되었습니다 (리뷰어 피드백 반영).
3. 성능 테스트 도구 업데이트
command_args_parser.cc:onnxruntime_perf_testCLI 도구에--spin_backoff_max플래그가 추가되었습니다.ort_test_session.cc:RunConfig구조체에spin_backoff_max필드가 추가되고, CLI 플래그가 세션 옵션에 적용되도록 수정되었습니다.benchmark_spin_settings.py: 새로운 파이썬 스크립트가 추가되었습니다. 이 스크립트는 다양한spin_duration_us와spin_backoff_max조합으로onnxruntime_perf_test를 실행하고, 그 결과를 종합하여 보고하는 역할을 합니다. 이를 통해 최적의 설정을 찾는 데 도움을 줍니다.
왜 이게 좋은가?
성능 및 전력 효율성 향상
이 PR의 핵심 목표는 스핀 루프의 CPU 사용 밀도를 낮추어 성능과 전력 효율성을 개선하는 것입니다. 특히 다음과 같은 시나리오에서 효과가 두드러집니다:
- 하이브리드 아키텍처 (P-core/E-core): E-core는 성능이 낮지만 전력 효율성이 높습니다. 스핀 루프에서
SpinPause()호출 횟수를 늘리면 E-core가 불필요하게 높은 부하를 유지하는 것을 방지하여 전력 소비를 줄일 수 있습니다. - 모바일 및 저전력 환경: 배터리 수명이 중요한 모바일 기기에서는 CPU 사용률을 최소화하는 것이 필수적입니다. Exponential Backoff는 유휴 상태에서의 불필요한 전력 낭비를 줄여줍니다.
- 높은 스레드 동시성: SqueezeNet 모델에 대한 벤치마크 결과에서 볼 수 있듯이, 스레드 수가 많을수록(예: 16개 스레드) 스핀 루프 경합이 심화됩니다. 이때 Exponential Backoff는 지연 시간을 최대 43%까지 줄이고 처리량을 76%까지 향상시키면서 CPU 사용량은 오히려 2% 감소시키는 놀라운 결과를 보여주었습니다.
유연한 제어 및 호환성
- 기본값 호환성:
spin_backoff_max의 기본값은1입니다. 이는 기존의 동작과 동일하므로, 별도의 설정을 하지 않은 사용자는 성능 변화를 느끼지 못합니다. 즉, 기존 사용자에게는 아무런 영향 없이 새로운 기능을 옵트인(opt-in)할 수 있습니다. spin_duration_us와의 조합: Exponential Backoff는spin_duration_us옵션과 독립적으로 작동하며 함께 사용할 수 있습니다. 사용자는 스핀 루프의 총 대기 시간(spin_duration_us)과 스핀 루프 내의SpinPause()호출 밀도(spin_backoff_max)를 별도로 제어하여 특정 워크로드에 최적화된 설정을 찾을 수 있습니다.- 정확한 제어:
spin_duration_us를0으로 설정하면 스핀 루프가 비활성화되고 즉시 블로킹(blocking) 대기로 전환됩니다.spin_duration_us에 양수 값을 주면 해당 시간 동안 스핀 루프가 실행되며,spin_backoff_max는 이 시간 내에서SpinPause()호출 밀도를 조절합니다.
일반적인 교훈
- 유휴 상태 최적화의 중요성: CPU 집약적인 작업뿐만 아니라, 스레드가 유휴 상태일 때 발생하는 비용(CPU 사용, 전력 소모) 또한 전체 시스템 성능에 큰 영향을 미칠 수 있습니다. 특히 동시성이 높은 환경에서는 이러한 유휴 비용 최적화가 더욱 중요합니다.
- 점진적 개선과 옵트인: 새로운 최적화 기법을 도입할 때는 기존 시스템과의 호환성을 유지하고, 사용자가 명시적으로 활성화할 수 있도록 하는 것이 중요합니다. 이를 통해 안정성을 확보하면서 점진적으로 성능을 개선해 나갈 수 있습니다.
- 벤치마킹의 중요성: 다양한 설정 조합에 대한 성능을 측정하고 분석하는 것은 최적의 설정을 찾는 데 필수적입니다. 이번 PR에서 제공된
benchmark_spin_settings.py스크립트는 이러한 과정을 자동화하고 가속화하는 좋은 예시입니다.
리뷰 피드백 반영
리뷰 과정에서 몇 가지 중요한 피드백이 있었고, PR은 이를 반영하여 개선되었습니다:
spin_backoff_max값 검증 및 클램핑: Copilot은ParseSpinBackoffMax함수에서 매우 큰spin_backoff_max값이 들어올 경우pause_time_ * 2U연산에서 오버플로우가 발생하거나, 의도한 것보다 훨씬 긴 스핀 시간을 유발할 수 있음을 지적했습니다. 이에 따라onnxruntime/core/session/inference_session.cc에서 입력 값을kSpinBackoffMaxLimit(64)으로 클램핑하도록 수정되었습니다. 또한,perftest도구에서도 이 제한을 적용하여 CLI 출력과 실제 동작 간의 불일치를 줄였습니다.allow_spinning조건부 로직: Copilot은allow_intra_op_spinning또는allow_inter_op_spinning이false일 때도spin_duration_us나spin_backoff_max를 파싱하고 경고를 출력하는 것이 오해의 소지가 있음을 지적했습니다. 이에 대한 수정 제안(if (allow_intra_op_spinning) { ... } else { ... })이 있었고, 이는 코드에 반영되어 스핀이 비활성화된 경우 관련 설정을 파싱하거나 경고하지 않도록 개선되었습니다.SpinPause()호출 로직:ThreadPoolWaiter::wait()메서드 내에서SpinPause()호출 루프가 끝난 후에도should_interrupt()를 한 번 더 호출하는 것에 대한 논의가 있었습니다. 이는 각 스핀 반복마다SpinPause()호출 횟수 + 1회의 인터럽트 체크가 발생하는 오버헤드를 의미합니다. 리뷰어는 이 오버헤드가 성능에 미치는 영향을 인지하고 문서화하는 것이 좋다고 제안했으며, PR에서는 이 동작을 유지하되 해당 오버헤드를 인지하고 있음을 명시했습니다.ScaleSpinCountForBackoff구현: PR 설명과ScaleSpinCountForBackoff함수의 실제 구현 간의 약간의 불일치(설명에서는spin_backoff_max - 1로 나누는 듯한 뉘앙스, 실제로는spin_backoff_max로 나눔)가 지적되었습니다. 최종적으로 구현은spin_backoff_max로 나누는 것으로 확정되었고, 문서화(f0b6bf723c커밋)를 통해 이를 명확히 했습니다.- 테스트 스크립트 개선:
benchmark_spin_settings.py스크립트의 재시도 로직과-r플래그 처리 방식에 대한 지적이 있었습니다. 특히,-r플래그를 제거하는 로직이 잘못된 인자를 제거하거나IndexError를 발생시킬 수 있다는 점이 지적되었고, 이에 대한 개선 제안(_remove_retry_flag함수 추가)이 있었습니다.
이러한 리뷰 피드백 반영을 통해 코드의 견고성, 정확성, 그리고 문서화가 크게 향상되었습니다.
결론
이번 PR은 ONNX Runtime의 스레드 풀 스핀 루프에 Exponential Backoff 전략을 도입함으로써, 특히 높은 스레드 동시성 환경에서 성능과 전력 효율성을 동시에 개선하는 중요한 발걸음을 내디뎠습니다. spin_backoff_max 옵션을 통해 사용자는 기존 동작을 유지하면서도 필요에 따라 더 지능적인 스핀 루프 제어를 활용할 수 있게 되었습니다. 벤치마크 결과는 이 최적화가 실제 워크로드에서 상당한 성능 향상(최대 43% 지연 시간 감소, 76% 처리량 증가)을 가져올 수 있음을 명확히 보여줍니다.
이러한 개선은 ONNX Runtime이 더욱 다양한 하드웨어 환경에서 효율적으로 동작하고, 특히 전력 소비에 민감한 환경에서의 경쟁력을 높이는 데 기여할 것입니다. 앞으로도 ONNX Runtime의 지속적인 성능 최적화 노력을 기대해 봅니다.
References
- ONNX Runtime Session Options Documentation
- SpinPause intrinsic (Conceptual explanation of
SpinPauselike instructions) - Intel® Architecture Manuals - PAUSE instruction (For x86 architectures)
- ARM Architecture Reference Manual - Yield instruction
- ONNX Runtime PR #27916 - Configurable spin duration
- ONNX Runtime PR #23278 - Related power/latency improvements
참고 자료
- https://onnxruntime.ai/docs/api_reference/c_api.html#c.OrtSessionOptions
- https://learn.microsoft.com/en-us/windows-hardware/drivers/safari/spin-instructions
- https://www.intel.com/content/www/us/en/docs/processor/64-ia-architectures-manual/index.html
- https://developer.arm.com/documentation/ddi0602/2023-03/base-instructions/yield--yield-a-arch64
- https://github.com/microsoft/onnxruntime/pull/27916
- https://github.com/microsoft/onnxruntime/pull/23278
⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.
관련 포스트
PR Analysis 의 다른글
- 이전글 [flashinfer] FlashInfer의 고성능 분산 연산: All-Gather Matmul 최적화 분석
- 현재글 : [onnxruntime] ONNX Runtime 스레드 풀의 지능형 대기: Exponential Backoff 도입으로 성능 및 전력 효율성 향상
- 다음글 [vllm] vLLM에 고성능 JIT 양자화 커널 'Humming' 도입하기
댓글