[sglang] [SGLang] LingBot 실시간 서빙 최적화: 카메라 컨디셔닝 캐싱과 전송 프로토콜 개선
PR 링크: sgl-project/sglang#27297 상태: Merged | 변경: +462 / -119
들어가며
실시간 비디오 생성 및 제어 모델인 LingBot과 같은 시스템에서 '지연시간(Latency)'은 사용자 경험을 결정짓는 가장 중요한 요소입니다. 특히 Diffusion 모델 기반의 실시간 서빙에서는 매 디노이징 스텝(Denoising Step)마다 발생하는 반복적인 연산과 생성된 고해상도 프레임을 클라이언트에 전송하는 과정이 주요 병목 지점이 됩니다.
최근 SGLang 프로젝트에 반영된 이 PR은 LingBot의 실시간 서빙 성능을 개선하기 위해 두 가지 핵심 영역을 최적화했습니다. 첫째는 모델 내부의 Camera Conditioning 연산 캐싱이고, 둘째는 데이터 전송 프로토콜의 효율화입니다. 이 글에서는 실제 코드 변경 사항을 통해 시니어 엔지니어의 관점에서 어떤 고민이 담겼는지 분석해 보겠습니다.
코드 분석: 핵심 변경 사항
1. Camera Conditioner Scale/Shift 캐싱 (lingbot_world.py)
LingBot은 카메라의 위치 정보(c2ws)를 입력받아 모델의 특징 맵(Feature Map)에 주입합니다. 기존에는 매 디노이징 스텝마다 동일한 카메라 정보에 대해 Linear Layer 연산을 반복했습니다.
Before
def forward(
self, hidden_states: torch.Tensor, c2ws_plucker_emb: torch.Tensor | None
) -> torch.Tensor:
# ... 생략 ...
c2ws_hidden_states = self.cam_injector(c2ws_plucker_emb)
c2ws_hidden_states = c2ws_hidden_states + c2ws_plucker_emb
cam_scale = self.cam_scale_layer(c2ws_hidden_states)
cam_shift = self.cam_shift_layer(c2ws_hidden_states)
return (1.0 + cam_scale) * hidden_states + cam_shift
After
def _cam_conditioner_scale_shift(
self,
c2ws_plucker_emb: torch.Tensor | None,
) -> tuple[torch.Tensor, torch.Tensor] | None:
# ... 캐시 확인 로직 ...
source_key = (
c2ws_plucker_emb.data_ptr(),
tuple(c2ws_plucker_emb.shape),
tuple(c2ws_plucker_emb.stride()),
c2ws_plucker_emb.dtype,
c2ws_plucker_emb.device.type,
c2ws_plucker_emb.device.index,
c2ws_plucker_emb._version,
)
if cache.get("source_key") != source_key:
cache.clear()
cache["source_key"] = source_key
cache["scale_shift"] = self.cam_conditioner.compute_scale_shift(c2ws_plucker_emb)
return cache["scale_shift"]
왜 좋은 최적화인가?
실시간 제어 시나리오에서 카메라 파라미터는 한 프레임을 생성하는 여러 디노이징 스텝 동안 변하지 않습니다. 엔지니어는 data_ptr()와 _version을 조합한 source_key를 사용하여 텐서의 내용이 변경되지 않았음을 보장하면서, 무거운 Linear 연산 결과를 재사용하도록 설계했습니다. 이는 불필요한 GPU 연산을 줄여 scheduler forward 시간을 단축시킵니다.
2. 전송 레이어: Delta-Gzip에서 Lossless Raw 전송으로의 전환 (realtime_output_adapter.py)
기존에는 대역폭을 아끼기 위해 이전 프레임과의 차이점만 추출하여 Gzip으로 압축하는 delta-gzip 방식을 사용했습니다. 하지만 이는 CPU 부하를 가중시키는 원인이 되었습니다.
Before
elif content_type == RAW_RGB_CONTENT_TYPE and transport_frames:
raw_payload = build_delta_gzip_raw_rgb_payload(
transport_frames,
reference_frame=reference_frame,
)
payload_content_type = RAW_RGB_DELTA_GZIP_CONTENT_TYPE
# ...
After
elif content_type == RAW_RGB_CONTENT_TYPE and transport_frames:
raw_payload = b"".join(transport_frames)
payload_metadata = {
"raw_size": len(raw_payload),
"encoding": RAW_LOSSLESS_OUTPUT_FORMAT,
}
왜 좋은 최적화인가?
벤치마크 결과에 따르면 raw payload build 시간이 기존 20~22ms에서 1ms로 급감했습니다. 실시간 시스템에서는 네트워크 대역폭보다 CPU 연산에 의한 지연이 더 큰 병목이 될 수 있음을 보여주는 사례입니다. 특히 H200과 같은 고성능 인프라에서는 네트워크 속도가 충분히 빠르기 때문에, 복잡한 압축 알고리즘을 제거하는 것이 전체 지연시간 단축에 훨씬 유리합니다.
3. 대용량 페이로드의 효율적 전송 (Header/Payload 분리)
대용량 바이너리 데이터를 msgpack으로 인코딩할 때 발생하는 메모리 복사 오버헤드를 줄이기 위해, 헤더와 데이터를 분리하여 전송하는 방식을 도입했습니다.
After
if len(transport_payload.payload) >= FRAME_BATCH_PACK_OFFLOAD_BYTES:
header_payload = _pack_frame_batch_header(header)
await ws.send_bytes(header_payload) # 헤더 먼저 전송
await ws.send_bytes(transport_payload.payload) # 실제 데이터 전송
else:
message_payload = _pack_frame_batch_message(header, transport_payload.payload)
await ws.send_bytes(message_payload)
이 방식은 msgpack 라이브러리가 거대한 바이너리 데이터를 처리하며 발생하는 직렬화 오버헤드를 피하고, WebSocket 수준에서 효율적으로 데이터를 스트리밍할 수 있게 합니다.
왜 이게 좋은가: 성능 수치와 교훈
벤치마크 결과 (4xH200 환경)
- Steady chunk total: 2360 ms → 2114 ms (10.3% 개선)
- Raw payload build: ~21 ms → ~1 ms
- Scheduler forward: 2252 ms → 2092 ms (7.1% 개선)
일반적인 교훈
- 변하지 않는 컨디셔닝은 캐싱하라: Diffusion 모델에서 텍스트 임베딩이나 카메라 파라미터처럼 스텝 간 고정된 값은 반드시 캐싱하여 중복 연산을 막아야 합니다.
- 압축의 트레이드오프를 고려하라: 대역폭 절약보다 CPU 연산 시간이 더 비싼 환경(예: 고속 로컬 네트워크 또는 데이터센터 내부)에서는 단순한 Raw 데이터 전송이 더 빠를 수 있습니다.
- Zero-copy 지향: 대용량 데이터를 다룰 때는 전체를 하나의 객체로 묶어 직렬화하기보다, 메타데이터(Header)와 본문(Body)을 분리하여 전송함으로써 메모리 복사와 인코딩 비용을 최소화해야 합니다.
이 PR은 단순히 코드를 깔끔하게 만드는 것을 넘어, 실제 서빙 환경에서의 병목을 정확히 짚어내고 데이터 기반으로 최적화를 수행한 훌륭한 사례입니다.
참고 자료
- https://pytorch.org/docs/stable/generated/torch.nn.Linear.html
- https://msgspec.readthedocs.io/en/latest/msgpack.html
- https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread
⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.
관련 포스트
- [sglang] SGLang의 Ideogram4 추론 성능 최적화: Denoising 루프 내 오버헤드 제거
- [sglang] SGLang Diffusion 최적화: CFG Gating을 통한 추론 속도 20% 향상
- [sglang] SGLang VLM 최적화: CUDA IPC Staging 오버헤드 제거를 통한 성능 향상
- [vllm] vLLM Qwen3.5 GDN 최적화: `einops.rearrange`를 `torch.flatten`으로 교체하여 20배 성능 향상!
- [sglang] [AMD/ROCm] Temporal Unfolding을 통한 VAE Conv3D 성능 최적화 분석
PR Analysis 의 다른글
- 이전글 [uv] uv, 대규모 워크스페이스 탐색 속도 1.8배 향상: 중복 파일 읽기 제거
- 현재글 : [sglang] [SGLang] LingBot 실시간 서빙 최적화: 카메라 컨디셔닝 캐싱과 전송 프로토콜 개선
- 다음글 [hermes-agent] CLI 사용자 경험 개선: 백그라운드 캐시 워밍을 통한 모델 선택기 응답 속도 최적화
댓글