[sglang] [VLM 성능 최적화] Qwen-VL의 자잘한 H2D 오버헤드 줄이기: 단일 대형 전송으로의 전환
PR 링크: sgl-project/sglang#26167 상태: Merged | 변경: +48 / -44
들어가며
멀티모달 거대 언어 모델(VLM)은 텍스트뿐만 아니라 이미지, 비디오 등 대용량 데이터를 처리해야 합니다. 특히 Qwen-VL과 같은 모델은 입력된 시각적 요소를 여러 개의 패치(Patch)나 프레임으로 나누어 처리하는데, 이 과정에서 수많은 작은 텐서들이 생성됩니다.
기존 SGLang의 구현에서는 이러한 멀티모달 아이템들을 개별적으로 GPU로 복사(Host-to-Device, H2D)한 뒤 나중에 병합하는 방식을 사용했습니다. 하지만 CUDA 환경에서 작은 크기의 데이터를 여러 번 전송하는 것은 오버헤드가 매우 큽니다. 각 전송마다 커널 런치(Kernel Launch) 비용과 PCIe 대역폭 활용 저하가 발생하기 때문입니다.
이번 PR은 Qwen-VL 모델군에 대해 이러한 자잘한 H2D 호출을 하나로 통합하고, 불필요한 CPU-GPU 동기화 지점을 제거하여 전체적인 추론 지연 시간(Latency)을 단축하는 최적화를 담고 있습니다.
코드 분석: 핵심 변경 사항
1. mm_utils.py: 불필요한 조기 H2D 전송 방지
가장 중요한 변경점은 특정 모델(Qwen3VL 등)에 대해 미리 데이터를 GPU로 옮기지 않도록 제어하는 로직이 추가된 것입니다.
Before:
모든 멀티모달 아이템에 대해 임베딩 함수를 호출하기 전 무조건 _move_items_to_device를 수행했습니다.
# mm_utils.py (Legacy)
def get_chunked_embedding_legacy(...):
# ...
if embedding_per_req is None:
_move_items_to_device(embedding_items_per_req, device)
embedding = data_embedding_func(embedding_items_per_req)
After:
_can_skip_pre_embed_feature_move 함수를 통해 Qwen-VL 계열 모델인지 확인하고, 해당될 경우 개별 전송을 건너뜁니다. 대신 나중에 모델의 forward 단계에서 한꺼번에 전송합니다.
# mm_utils.py (Updated)
def _can_skip_pre_embed_feature_move(data_embedding_func: DataEmbeddingFunc) -> bool:
# Qwen3VL 계열 모델인지 확인
owner = getattr(data_embedding_func, "__self__", None)
# ... (생략) ...
return owner.__class__.__name__ in {
"Qwen3VLForConditionalGeneration",
"Qwen3_5ForConditionalGeneration",
# ...
}
def get_chunked_embedding_legacy(...):
if embedding_per_req is None:
if not _can_skip_pre_embed_feature_move(data_embedding_func):
_move_items_to_device(embedding_items_per_req, device)
embedding = data_embedding_func(embedding_items_per_req)
2. mm_utils.py: Python 루프와 CPU 동기화 제거
기존에는 이미지나 비디오의 그리드(Grid) 정보를 처리할 때 텐서를 CPU 리스트로 변환(tolist())하여 처리하는 비효율적인 로직이 있었습니다. 이는 암시적인 GPU-CPU 동기화를 유발합니다.
Before:
def _grid_rows_to_cpu_list(value):
if isinstance(value, torch.Tensor):
value = value.detach()
if value.device.type != "cpu":
value = value.cpu() # GPU 데이터를 CPU로 가져옴 (동기화 발생)
return value.tolist()
# ... 내부 루프에서 사용 ...
image_grid_rows = _grid_rows_to_cpu_list(image_grid_thw)
for grid in image_grid_rows:
patches_per_item.append(_prod_grid_values(grid))
After: PyTorch의 벡터화된 연산을 사용하여 CPU로의 데이터 이동 없이 GPU 상에서 직접 계산하거나, 필요한 경우에만 최소한으로 처리합니다.
if isinstance(image_grid_thw, torch.Tensor):
# torch.prod를 사용하여 한 번에 계산
patches_per_item = (
torch.prod(image_grid_thw, dim=-1).long().tolist()
)
else:
patches_per_item = [int(np.prod(grid)) for grid in image_grid_thw]
이 변경을 통해 _grid_rows_to_cpu_list와 _prod_grid_values라는 헬퍼 함수 자체를 삭제할 수 있었습니다.
3. qwen3_vl.py: Non-blocking 전송 활용
모델의 forward 함수 내에서 데이터를 GPU로 옮길 때 non_blocking=True 옵션을 추가했습니다.
Before/After:
# qwen3_vl.py
- x = x.to(device=self.device, dtype=self.dtype)
+ x = x.to(device=self.device, dtype=self.dtype, non_blocking=True)
non_blocking=True를 설정하면, CPU는 데이터 전송이 완료될 때까지 기다리지 않고 다음 명령(예: patch_embed 연산 준비)을 즉시 수행할 수 있습니다. 이는 CPU와 GPU 간의 파이프라이닝을 극대화합니다.
왜 이게 좋은 최적화인가?
1. PCIe 대역폭 효율성 향상
PCIe 버스는 작은 데이터를 여러 번 보낼 때보다 큰 덩어리의 데이터를 한 번에 보낼 때 훨씬 높은 처리량(Throughput)을 보여줍니다. 이번 PR은 수십 개의 작은 이미지 패치 특징들을 각각 H2D 하는 대신, 모델 입력 단계에서 하나로 묶인 텐서를 전송함으로써 이 이점을 취했습니다.
2. CPU-GPU 동기화(Sync) 최소화
tensor.cpu()나 tensor.tolist()는 GPU에서 계산 중인 작업이 끝날 때까지 CPU를 멈추게 만듭니다(Blocking). 최적화된 코드에서는 torch.prod와 같은 벡터화 연산을 사용하여 GPU 내부에서 처리를 완결하거나, 전송 시 non_blocking을 사용하여 CPU가 쉬지 않고 일하게 만들었습니다.
3. 코드 클린업 및 유지보수성
불필요한 유틸리티 함수(_grid_rows_to_cpu_list)를 제거하고 PyTorch 표준 API를 활용함으로써 코드가 더 간결해졌습니다. 또한, Qwen-VL 전용 로직을 _can_skip_pre_embed_feature_move로 추상화하여 다른 모델에 영향을 주지 않으면서도 특정 모델의 성능을 타겟팅할 수 있게 되었습니다.
결론
고성능 추론 엔진을 개발할 때 가장 경계해야 할 적 중 하나는 "자잘한 오버헤드의 누적"입니다. 이번 PR은 VLM의 특성상 발생하기 쉬운 파편화된 데이터 전송을 통합함으로써, 특히 배치 사이즈가 커지거나 고해상도 이미지를 처리할 때 유의미한 성능 향상을 이끌어낼 수 있는 정석적인 최적화 사례를 보여줍니다.
시니어 엔지니어로서 배울 점은, 단순히 기능을 구현하는 것에 그치지 않고 데이터의 흐름(Data Flow)을 추적하여 어디에서 병목(H2D, Sync)이 발생하는지 정확히 짚어내고 이를 제거하는 통찰력입니다.
참고 자료
- https://pytorch.org/docs/stable/generated/torch.Tensor.to.html
- https://pytorch.org/docs/stable/generated/torch.prod.html
- https://pytorch.org/docs/stable/generated/torch.cumsum.html
⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.
관련 포스트
- [sglang] SGLang VLM 최적화: CUDA IPC Staging 오버헤드 제거를 통한 성능 향상
- [flashinfer] FlashInfer: Wide Vector 최적화와 1900줄의 코드 삭제로 달성한 성능 개선
- [sglang] SGLang 성능 최적화: torch.cuda.empty_cache() 호출 제어를 통한 가중치 업데이트 병목 해결
- [vllm] vLLM Gemma4 모델의 GPU/CPU 동기화 병목 현상 해결하기: non_blocking 전송의 중요성
- [sglang] SGLang의 KV-Canary JIT 커널 도입: 효율적인 KV 캐시 검증 최적화
PR Analysis 의 다른글
- 이전글 [sglang] SGLang VLM 최적화: CUDA IPC Staging 오버헤드 제거를 통한 성능 향상
- 현재글 : [sglang] [VLM 성능 최적화] Qwen-VL의 자잘한 H2D 오버헤드 줄이기: 단일 대형 전송으로의 전환
- 다음글 [sglang] SGLang의 MoE 성능 최적화: 512 전문가 모델을 위한 커널 최적화
댓글