AI 모델을 개발하다 보면 흥미로운 현상을 마주하게 됩니다. 정확도를 높이기 위해 앙상블(Ensemble) 기법을 적용하여 모델 개수를 1개에서 5개로 늘렸는데, 추론 시간은 5배로 늘어나지 않고 거의 비슷하거나 아주 조금만 증가하는 현상입니다.
이는 단순히 "초기화가 잘 되어서"가 아닙니다. 시스템 내부에서 '고정 오버헤드'와 '하드웨어 병렬성'이 복합적으로 작용한 결과입니다. 오늘은 이 미스터리한 현상을 기술적으로 분석하고, 더 나아가 TorchScript의 for 문이 실제로 어떻게 동작하는지 파헤쳐 보겠습니다.
추론 시간이 배로 늘어나지 않는 것은 시스템이 효율적으로 동작하고 있다는 긍정적인 신호입니다. 그 원인은 크게 네 가지로 분석할 수 있습니다.
추론 프로세스에는 모델의 개수와 상관없이 무조건 소요되는 '기본 요금' 같은 시간이 존재합니다.
가 실제 연산 시간()보다 상대적으로 크다면, 모델()을 추가해도 전체 시간의 증가율은 둔해 보입니다. 이를 암달의 법칙(Amdahl's Law) 관점에서도 해석할 수 있습니다.
가장 유력한 원인입니다. GPU는 수천 개의 코어를 가진 대규모 병렬 연산 장치입니다. 단일 모델이 GPU의 연산 능력(Compute)이나 메모리 대역폭을 100% 사용하지 못하는 유휴 상태(Underutilization)였을 가능성이 큽니다.
PyTorch(CUDA) 환경에서 시간 측정 시 흔히 발생하는 실수입니다. model(input)을 호출하면 CPU는 GPU에게 "일하라"는 명령(Kernel Launch)만 내리고 즉시 다음 코드로 넘어갑니다.
만약 torch.cuda.synchronize()로 GPU 작업이 끝날 때까지 기다리지 않았다면, 우리가 측정한 시간은 실제 연산 시간이 아니라 CPU가 명령을 내리는 시간일 뿐입니다. 모델이 늘어나도 명령 내리는 시간은 거의 차이가 없으므로 속도 변화가 없는 것처럼 보일 수 있습니다.
앙상블 모델들이 같은 아키텍처(Backbone)를 공유한다면, CPU/GPU의 명령어 캐시(Instruction Cache) 적중률이 높아져 실행 효율이 증대됩니다.
for 문의 진실"그럼 TorchScript에서 for 문을 돌리면 알아서 병렬처리가 되는 건가요?"
결론부터 말씀드리면, 아닙니다.
# TorchScript 내부
for model in models:
output = model(input) # 이것은 병렬처리가 아닙니다.
CUDA에는 스트림(Stream)이라는 작업 대기열이 있습니다. 별도 설정 없이 for 문을 돌리면 모든 연산은 Default Stream 하나에 순서대로 쌓입니다. 즉, 직렬(Sequential)로 처리됩니다.
그럼에도 불구하고 사용자가 "빠르다"고 느끼는 이유는 비동기(Asynchronous) 특성 때문입니다.
CPU는 아주 빠르게 명령만 던지고(Kernel Launch) 지나갑니다. GPU는 이 명령을 받아 쉴 새 없이 처리(Pipelining)합니다. 이 간격이 매우 짧기 때문에 마치 동시에 처리되는 것처럼 느껴지는 것입니다.
GPU 메모리가 넉넉하여 물리적으로 동시에 여러 모델을 돌리고 싶다면, fork와 wait를 사용해야 합니다.
# TorchScript 병렬 처리 예시
futures = []
for model, input in zip(models, inputs):
# 모델 실행을 별도 task로 분기 (Non-blocking)
futures.append(torch.jit.fork(model, input))
results = []
for future in futures:
# 결과가 나올 때까지 대기 및 수집
results.append(torch.jit.wait(future))
이 방식을 사용하면 논리적인 병렬 실행이 가능해지며, GPU 자원을 극한으로 활용할 수 있게 됩니다.
사용자님의 관찰대로 추론 시간이 선형적으로 늘어나지 않는 것은 GPU 자원을 알뜰하게 잘 쓰고 있다는 증거입니다. 현재 속도에 만족하신다면 그대로 사용하셔도 무방합니다.
하지만 더 극한의 성능을 원하신다면 다음 두 가지를 확인해 보세요.
torch.cuda.synchronize()가 있는지 확인하여 정확한 시간을 측정해 보세요.nvidia-smi를 통해 GPU 사용률(Utilization)이 100%에 근접하는지 확인해 보세요. 만약 사용률이 낮다면 torch.jit.fork를 도입하여 '진짜 병렬처리'를 구현해 보시길 권장합니다.