배치 처리 도입 테스트 - 100-hours-a-week/5-yeosa-wiki GitHub Wiki
1. 개요
a. 목적
- 이미지 임베딩 파이프라인에서 **배치 크기(16, 32, 64)**와 **전처리 위치(CPU vs GPU)**에 따른 성능 차이를 분석
- 가장 효율적인 전처리 위치 및 배치 단위를 도출하고, GPU 자원 활용률 최적화 및 임베딩 처리 시간 단축을 목표
b. 배경
가. 기존 테스트에서 확인된 주요 이슈:
- GPU 사용률이 60% 수준에 도달했음에도 임베딩 처리 속도 저하 현상 발생
- 작은 배치 단위의 반복 전송 및 스케줄링 오버헤드로 추정
- 각 배치마다
.to("cuda")
및 커널 launch가 반복되며, 메모리 I/O와 컨텍스트 스위칭 비용 증가
나. 환경
구분 |
값 |
배치 크기 |
16, 32, 64 |
전처리 위치 |
CPU, GPU |
모델 |
CLIP ViT-B/32 |
테스트 환경 |
T4 GPU 서버, 논리 코어 8개(물리 4), RAM/GPU 메모리 여유 확보 상태 |
테스트 방식 |
요청당 이미지 50장 * 30개의 요청 = 총 1500장 |
다. 테스트 방식 설명
- CPU 배치 전처리
- 이미지 resize 및 normalization까지 CPU에서 처리
- 후처리된 텐서를 한 번에 GPU로 전송 (
.to('cuda')
)
- GPU 배치 전처리
resize
만 CPU, 나머지를 GPU 상에서 수행
- 텐서 생성 직후
.to('cuda')
하여 GPU에서 모든 연산 처리
c. 테스트 배치 크기
배치 크기 |
목적 |
16 |
기준선 비교 (현재 사용 중) |
32 |
I/O 횟수 절반 감소, 중간 규모 배치 성능 확인 |
64 |
최대 효율 예상 구간, GPU 메모리 활용 극대화 |
d. 측정 항목
항목 |
설명 |
이미지 로딩 시간 |
장당 이미지 다운로드 시간 |
디코딩 시간 |
장당 cv2 기반 디코딩 시간 |
디코딩 대기 시간 |
디코딩이 실행되기까지 대기 시간 |
전처리 시간 |
전처리 및 .to("cuda") 포함 시간 |
임베딩 시간 |
CLIP 모델 인퍼런스 시간 |
총 처리 시간 |
전체 파이프라인 기준 소요 시간 |
GPU 사용률 |
gpustat으로 매 초단위로 기록 |
CPU 사용륭 |
top으로 매 초단위로 기록 |
2. 시간 측정 결과
a. 결과
전처리 |
배치 |
로딩 (min / max / avg) |
디코딩 완료 (min / max / avg) |
디코딩 대기 (min / max / avg) |
로딩+디코딩 (min / max / avg) |
전처리 (min / max / avg) |
임베딩 (min / max / avg) |
총시간 (min / max / avg) |
CPU |
16 |
11.62 / 1050.00 / 119.20 |
7.77 / 48.14 / 16.39 |
0.49 / 315.62 / 70.92 |
149.81 / 1070.00 / 290.97 |
120.27 / 376.09 / 178.47 |
209.16 / 474.08 / 363.31 |
541.07 / 1670.00 / 882.24 |
CPU |
32 |
12.29 / 785.97 / 113.19 |
8.53 / 57.80 / 16.82 |
1.33 / 243.91 / 80.25 |
138.21 / 833.20 / 291.60 |
90.93 / 382.72 / 166.94 |
132.77 / 482.31 / 249.71 |
462.84 / 1590.00 / 775.07 |
CPU |
64 |
11.70 / 750.74 / 104.70 |
8.13 / 249.76 / 18.49 |
0.62 / 318.74 / 70.01 |
153.24 / 1040.00 / 286.76 |
118.76 / 459.86 / 177.16 |
112.51 / 270.22 / 163.20 |
467.10 / 1530.00 / 703.85 |
GPU |
16 |
11.59 / 936.47 / 133.62 |
7.99 / 202.10 / 17.45 |
2.31 / 343.29 / 79.77 |
140.60 / 979.72 / 346.41 |
62.54 / 188.99 / 100.72 |
239.89 / 614.65 / 450.69 |
545.63 / 1570.00 / 962.27 |
GPU |
32 |
10.43 / 991.20 / 123.86 |
8.30 / 44.66 / 17.08 |
0.90 / 487.44 / 70.35 |
144.35 / 1060.00 / 304.05 |
102.03 / 357.31 / 138.13 |
186.22 / 338.26 / 272.70 |
524.89 / 1500.00 / 757.59 |
GPU |
64 |
12.85 / 932.99 / 113.44 |
8.26 / 55.81 / 17.09 |
1.60 / 308.07 / 75.52 |
137.12 / 989.91 / 293.52 |
84.63 / 216.84 / 133.98 |
114.42 / 371.29 / 202.94 |
383.90 / 1430.00 / 688.26 |
b. 개선 결과 요약
가. CPU 전처리 - 배치 크기 16 대비 변화율
항목 |
배치 크기 32 |
배치 크기 64 |
로딩 |
-5.06% |
-12.13% |
디코딩 완료 |
+2.37% |
-12.63% |
디코딩 대기 |
+13.17% |
-1.28% |
로딩+디코딩 |
-0.90% |
-1.51% |
전처리 |
-6.52% |
-0.73% |
임베딩 |
-31.26% |
-55.07% |
총시간 |
-12.13% |
-20.20% |
나. GPU 전처리 - 배치 크기 16 대비 변화율
항목 |
배치 크기 32 |
배치 크기 64 |
로딩 |
-7.29% |
-15.13% |
디코딩 완료 |
-2.12% |
-2.06% |
디코딩 대기 |
-11.83% |
-5.32% |
로딩+디코딩 |
-12.24% |
-15.26% |
전처리 |
+37.25% |
+24.36% |
임베딩 |
-39.51% |
-54.97% |
총시간 |
-21.29% |
-28.49% |
다. CPU 전처리 vs GPU 전처리
[ 배치 크기 32 ]
항목 |
CPU 전처리 |
GPU 전처리 |
차이 (CPU → GPU) |
로딩 |
12.29ms |
10.43ms |
-1.86ms |
디코딩 완료 |
8.53ms |
8.30ms |
-0.23ms |
디코딩 대기 |
1.33ms |
0.90ms |
-0.43ms |
로딩+디코딩 |
138.21ms |
144.35ms |
+6.14ms |
전처리 |
90.93ms |
102.03ms |
+11.10ms |
임베딩 |
132.77ms |
186.22ms |
+53.45ms |
총시간 |
462.84ms |
524.89ms |
+62.05ms |
- GPU 전처리는 로딩/디코딩은 빠르지만, 전처리와 임베딩에서 오히려 더 느림.
- 특히 임베딩 시간이 GPU 쪽에서 많이 증가한 것이 총시간 차이의 주 원인.
[ 배치 크기 64 ]
항목 |
CPU 전처리 |
GPU 전처리 |
차이 (CPU → GPU) |
로딩 |
11.70ms |
12.85ms |
+1.15ms |
디코딩 완료 |
8.13ms |
8.26ms |
+0.13ms |
디코딩 대기 |
0.62ms |
1.60ms |
+0.98ms |
로딩+디코딩 |
153.24ms |
137.12ms |
-16.12ms |
전처리 |
118.76ms |
84.63ms |
-34.13ms |
임베딩 |
112.51ms |
114.42ms |
+1.91ms |
총시간 |
467.10ms |
383.90ms |
-83.20ms |
- 배치 크기 32와 반대의 결과
- 특히 전처리 + 디코딩 + 임베딩이 전체적으로 CPU보다 안정적으로 개선됨.
3. 분석
a. CPU 전처리 성능 분석
가. 배치 크기 32에서의 성능 변화
- 임베딩 시간 평균 31.26% 감소 → GPU에 넘기는 횟수가 줄면서 효율 상승
- 전처리 시간은 오히려 6.5% 감소 → 작업 묶음 최적화 효과가 있음
- 총 소요 시간 약 12.1% 감소 → 병목 구간이었던 임베딩이 완화되며 전체 시간 단축
나. 배치 크기 64에서의 성능 변화
- 임베딩 시간 평균 55.1% 감소 (가장 큰 개선) → GPU로의 이동 및 스케줄링 비용 절감 극대화
- 전처리 시간은 되려 0.7% 감소 → 병렬 처리 효율 유지
- 총 소요 시간 20.2% 감소 → 병목이 확실히 완화됨
b. GPU 전처리 성능 분석
가. 배치 크기 32에서의 성능 변화
- 임베딩 시간 평균 39.5% 감소 → 배치 단위로 한 번에 임베딩하므로 GPU 연산 효율 증가
- 전처리 시간은 오히려 37.2% 증가 → GPU에서 병렬로 처리하나, 데이터 이동/연산 순서 문제 가능성
- 총 소요 시간 21.3% 감소 → 전처리 느려졌지만 임베딩 개선 폭이 커서 전체 성능 개선
나. 배치 크기 64에서의 성능 변화
- 임베딩 시간 평균 55.0% 감소 → GPU 스케줄링 효율이 극대화됨
- 전처리 시간 증가폭은 줄어듦 (+24.4%) → 메모리 전송 비용이 더 이상 커지지 않음
- 총 소요 시간 28.5% 감소 → GPU 사용률 최적화로 전체 시간 최소화
c. CPU vs GPU
가. 배치 크기 32에서는 CPU 전처리가 유리하다
[ 분석 1. CPU 전처리 방식은 GPU 스케줄링 빈도가 낮아 임베딩이 빠르다 ]
- CPU 전처리 방식에서는 이미지 전처리를 CPU에서 모두 수행한 후, 임베딩 시에만 GPU를 사용
- 이 경우 GPU는 임베딩 커널만 실행되기 때문에, GPU 스케줄링 오버헤드가 적고 커널 실행이 집중
- 따라서 임베딩 속도는 GPU 전처리 방식보다 더 빠름
[ 분석 2. GPU 전처리 방식은 CUDA 스케줄링이 잦아 임베딩 효율이 낮다 ]
- GPU 전처리 방식에서는 전처리(정규화 포함)도 GPU에서 수행하므로, GPU는 전처리와 임베딩 모두 순차적으로 처리
- 이 과정에서 GPU는 각 커널을 짧은 간격으로 반복 호출해야 하고, 스트림 간 컨텍스트 전환과 메모리 이동도 많아짐
- 결과적으로 GPU 스케줄러의 오버헤드가 발생하여, 임베딩 처리 시간이 느려지는 원인이 됨
[ 분석 3. 전처리 단계를 GPU로 이동했지만 그만큼의 이득은 크지 않았다 ]
나. 배치 크기 64에서는 GPU 전처리가 유리하다
[ 분석 1. CPU 전처리에서 CPU 자원 포화로 인한 병목 발생 ]
- 배치 크기 64는 CPU 입장에서는 상당한 연산 부담
- 이미지 64장을 디코딩하고 전처리하는 작업은
ThreadPoolExecutor
로 병렬화되더라도, 결국 물리 코어 수에 한계가 있어 전체 작업이 지연
- 특히
np.stack
, torch.from_numpy
, 정규화 연산 등은 모두 CPU에서 수행되어 CPU 캐시 / 메모리 대역폭 부담 증가.
[ 분석 2. 배치 크기가 커짐으로써 작업당 처리 효율 극대화 ]
- 전처리와 임베딩이 병렬 실행되지 못하는 것은 여전하지만, 각 작업당 처리 효율이 커짐
- GPU는 대량 병렬 연산에 최적화되어 있어, 작은 커널을 많이 실행하는 것보다 큰 연산을 fewer launch로 처리하는 것이 효율적
- 배치 64는 단일 커널이 커지므로, context switch 없이 전처리 → 임베딩 순차 실행만으로도 오버헤드가 줄어듦
- 결과적으로:
- 전처리는 커졌지만 더 빠르게 처리
- 임베딩 시간도 안정적으로 유지
- 총 처리 시간 단축으로 이어짐
4. 추가 테스트 - 요청당 100장
a. 테스트 개요
항목 |
테스트 조건 |
요청당 이미지 수 |
100장(우리 서비스에서의 최대 장 수) |
동시 요청 수 |
30명 |
전처리 위치 |
CPU vs GPU |
배치 크기 |
32, 64 |
측정 항목 |
로딩, 디코딩, 전처리, 임베딩, 총시간 (min / max / avg) |
b. 테스트 결과
c. 배치 크기에 따른 변화율
가. CPU 전처리: 배치 32 → 64
항목 |
변화율 (%) |
로딩 |
+12.8% |
디코딩 완료 |
-1.6% |
디코딩 대기 |
-3.1% |
로딩+디코딩 |
+2.1% |
전처리 |
+4.2% |
임베딩 |
-11.7% |
총시간 |
-1.9% |
- CPU에서는 배치 64가 약간 더 나은 성능이지만, 차이는 크지 않음
- CPU 태스크에서 병목이 발생했을 가능성
나. GPU 전처리: 배치 32 → 64
항목 |
변화율 (%) |
로딩 |
-1.2% |
디코딩 완료 |
-5.6% |
디코딩 대기 |
-5.2% |
로딩+디코딩 |
+1.9% |
전처리 |
+7.8% |
임베딩 |
-34.4% |
총시간 |
-11.5% |
- GPU에서는 배치 64에서 확실한 성능 개선
- 특히 임베딩 시간 대폭 감소
다. CPU vs GPU 전처리 비교 (배치 64 기준)
항목 |
CPU 전처리 |
GPU 전처리 |
차이 (CPU → GPU) |
로딩 |
200.21 |
198.78 |
-1.43ms |
디코딩 완료 |
17.27 |
15.96 |
-1.31ms |
디코딩 대기 |
119.54 |
115.97 |
-3.57ms |
로딩+디코딩 |
538.66 |
548.02 |
+9.36ms |
전처리 |
347.43 |
320.39 |
-27.04ms |
임베딩 |
472.96 |
379.72 |
-93.24ms |
총시간 |
1454.33 |
1321.02 |
-133.31ms |
- GPU 전처리가 전처리 + 임베딩 모두에서 더 빠름
- 총 처리 시간도 약 9% 이상 개선
5. 결론 : GPU 전처리 + 배치 64
a. 이유
가. GPU 자원 효율이 가장 높게 발휘됨
- 배치가 커질수록 GPU는 fewer kernel launch로 대량 연산을 처리 → launch overhead 최소화
- 전처리와 임베딩 모두 고효율 커널 실행 가능
나. CPU 병목을 피함
- CPU 전처리는
torch.from_numpy
, np.stack
, normalization 등이 배치가 커질수록 스레드 경합 유발
- GPU 전처리는 해당 연산을 전부 병렬 커널로 수행
b. 개선 필요 사항
- 배치 사이즈가 64일 때, Request Timeout이 발생
- 생성되는 스레드 수가 너무 많지 않을까?