배치 처리 도입 테스트 - 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로 이동했지만 그만큼의 이득은 크지 않았다 ]

  • 일반적으로 GPU 전처리는 단건 처리보다 배치가 커질수록 효율적인 방식

  • 그러나 배치 크기에 따른 스케쥴링을 분석해볼 필요가 있음

    • 배치 16은 임베딩과 전처리가 동시 실행(Overlap) 가능성이 높음 → GPU 스케줄링이 자연스럽게 interleave
    • 배치 32는 두 작업 모두 자원 소모가 커짐 → 동시에 스케줄 불가, context switch or 순차화 발생
  • 예시로 설명

    배치 크기 전처리 연산량 임베딩 연산량 병렬 실행 가능성 결과
    16 작음 (40%) 작음 (40%) 높음 (동시 가능) 빠름
    32 중간 (60%) 중간 (60%) 낮음 (동시 어려움) 충돌 → 느려짐
    64 많음 (80%) 많음 (80%) 낮음 (동시 어려움) 그래도 효율적

나. 배치 크기 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이 발생
    • 약 0.16%의 확률
  • 생성되는 스레드 수가 너무 많지 않을까?