[sglang] sglang, MoE 모델 로딩 속도 5.6배 향상: mmap 뷰 최적화 분석
PR 링크: sgl-project/sglang#32315 상태: Merged | 변경: +91 / -0
들어가며
최근 대규모 언어 모델(LLM) 분야에서는 Mixture-of-Experts (MoE) 아키텍처가 큰 주목을 받고 있습니다. MoE는 모델의 특정 부분만 활성화하여 연산 효율성을 높이는 방식이지만, 모델 로딩 과정에서 복잡성과 성능 병목을 야기할 수 있습니다. 특히, sglang 프로젝트에서 DeepSeek-V4-Pro와 같은 MoE 모델을 AMD GPU 환경에서 로딩할 때 상당한 시간이 소요되는 문제가 발견되었습니다. 본 글에서는 이 문제를 해결하고 모델 로딩 속도를 획기적으로 개선한 GitHub Pull Request(PR) #32315를 심층 분석하고, 그 원리와 개선 효과를 자세히 살펴보겠습니다.
이 PR은 DeepSeek-V4-Pro TP8 모델 로딩 시 발생하는 병목 현상을 해결하는 데 중점을 둡니다. 기존에는 각 GPU 랭크(rank)마다 수많은 작은 MoE 가중치(weight) 조각들을 Host-to-Device (H2D)로 복사하는 과정에서 비효율이 발생했습니다. 이는 특히 엣지 랭크(TP0/TP7)에서 H2D 복사에 27~32분까지 소요되는 심각한 성능 저하로 이어졌습니다. 본 PR은 이러한 문제를 해결하기 위해, 특정 조건 하에서만 CPU 메모리에 있는 MoE 가중치 뷰(view)를 H2D 복사 직전에 독립적이고 연속적인(contiguous) 저장 공간으로 복사하는 최적화 기법을 도입했습니다.
코드 변경사항 분석
이번 PR의 핵심은 SGLANG_MOE_COPY_WEIGHT_VIEWS_BEFORE_H2D라는 새로운 환경 변수를 통해 MoE 가중치 로딩 방식을 최적화하는 것입니다. 이 기능은 기본적으로 비활성화되어 있으며, 특정 AMD DSV4, Qwen, GPT-OSS 검증 작업에서 옵트인(opt-in) 방식으로 활성화됩니다.
1. 환경 변수 및 설정 (.github/workflows/ 및 python/sglang/srt/environ.py)
가장 먼저 눈에 띄는 변경사항은 CI/CD 워크플로우 파일(.github/workflows/)에서 새로운 환경 변수 SGLANG_MOE_COPY_WEIGHT_VIEWS_BEFORE_H2D=1을 설정하는 부분입니다. 이는 특정 AMD GPU 환경에서의 야간(nightly) 테스트 및 PR 테스트에서 이 최적화 기능을 활성화하여 검증하겠다는 의도를 보여줍니다.
--- a/.github/workflows/nightly-test-amd-rocm720.yml
+++ b/.github/workflows/nightly-test-amd-rocm720.yml
@@ -493,6 +493,7 @@ jobs:
timeout-minutes: 180
run: |
bash scripts/ci/amd/amd_ci_exec.sh -w /sglang-checkout/test \
+ -e SGLANG_MOE_COPY_WEIGHT_VIEWS_BEFORE_H2D=1 \
-e GITHUB_STEP_SUMMARY="/sglang-checkout/github_summary.md" \
python3 run_suite.py --hw amd --suite nightly-amd-accuracy-8-gpu-gpt-oss --nightly --timeout-per-file 7200 ${{ (github.event_name == 'schedule' || inputs.continue_on_error) && '--continue-on-error' || '' }} || TEST_EXIT_CODE=$?
echo "$(<github_summary.md )" >> $GITHUB_STEP_SUMMARY || true
또한, python/sglang/srt/environ.py 파일에서는 이 새로운 환경 변수를 정의하고 관리합니다.
--- a/python/sglang/srt/environ.py
+++ b/python/sglang/srt/environ.py
@@ -1081,6 +1081,9 @@ class Envs:
# Set False when using FP4-to-FP8 converted DeepSeek V4 checkpoint.
SGLANG_DSV4_FP4_EXPERIMENTS = EnvBool(True)
SGLANG_DSV4_FP4_DEQUANT = EnvBool(False)
+ # Copy rank-local MoE slices into independent CPU storage before H2D when
+ # they reference a larger mmap-backed checkpoint storage.
+ SGLANG_MOE_COPY_WEIGHT_VIEWS_BEFORE_H2D = EnvBool(False)
# Default reasoning_effort for dsv4 chat encoder when request doesn't set it.
# Accepts "", "max", "high" (empty string means unset); other values filtered to None.
SGLANG_DSV4_REASONING_EFFORT = EnvStr("")
SGLANG_MOE_COPY_WEIGHT_VIEWS_BEFORE_H2D는 기본적으로 False로 설정되어 있어, 기존 동작에 영향을 주지 않으면서 필요할 때만 활성화할 수 있도록 유연성을 제공합니다.
2. 핵심 로직 구현 (python/sglang/srt/layers/moe/fused_moe_triton/layer.py)
실제 최적화 로직은 fused_moe_triton/layer.py 파일에 구현되었습니다. 두 개의 새로운 헬퍼 함수 _copy_weight_view_before_h2d와 _maybe_copy_weight_view_before_h2d가 추가되었습니다.
_copy_weight_view_before_h2d 함수는 입력된 loaded_weight 텐서가 CPU에 있고, 연속적이지 않거나(not contiguous), 저장 오프셋이 0이 아니거나, 전체 저장 공간보다 작은 경우에만 clone(memory_format=torch.contiguous_format)을 사용하여 새로운 연속적인 메모리 공간에 복사본을 생성합니다. 이는 mmap(memory-mapping)된 파일에서 생성된 텐서 뷰(view)가 원본 파일의 특정 부분만 참조하고 있을 때, H2D 복사 전에 이 뷰를 독립적인 연속 메모리로 만들어 복사 효율을 높이려는 목적입니다.
def _copy_weight_view_before_h2d(loaded_weight: torch.Tensor) -> torch.Tensor:
"""Copy a CPU tensor view into independent contiguous storage."""
if loaded_weight.device.type != "cpu":
return loaded_weight
tensor_bytes = loaded_weight.numel() * loaded_weight.element_size()
needs_copy = not (
loaded_weight.is_contiguous()
and loaded_weight.storage_offset() == 0
and loaded_weight.untyped_storage().nbytes() == tensor_bytes
)
if not needs_copy:
return loaded_weight
return loaded_weight.clone(memory_format=torch.contiguous_format)
_maybe_copy_weight_view_before_h2d 함수는 SGLANG_MOE_COPY_WEIGHT_VIEWS_BEFORE_H2D 환경 변수 값을 확인하여, 활성화된 경우에만 _copy_weight_view_before_h2d를 호출합니다.
def _maybe_copy_weight_view_before_h2d(
loaded_weight: torch.Tensor,
) -> torch.Tensor:
if not envs.SGLANG_MOE_COPY_WEIGHT_VIEWS_BEFORE_H2D.get():
return loaded_weight
return _copy_weight_view_before_h2d(loaded_weight)
이 두 함수는 기존의 가중치 로딩 로직(_load_per_channel_weight_scale, _load_w13, _load_w2) 내에서 H2D 복사 직전에 호출됩니다. 예를 들어, _load_w2 함수는 다음과 같이 수정되었습니다.
--- a/python/sglang/srt/layers/moe/fused_moe_triton/layer.py
+++ b/python/sglang/srt/layers/moe/fused_moe_triton/layer.py
@@ -700,6 +700,7 @@ def _load_w2(
)
# w2, down_proj: Load into only logical weight of w2.
+ loaded_weight = _maybe_copy_weight_view_before_h2d(loaded_weight)
expert_data.copy_(loaded_weight)
def _maybe_load_fp8_shared_expert_as_fp4(
이 변경을 통해, 환경 변수가 활성화되면 expert_data.copy_(loaded_weight) 연산 이전에 loaded_weight가 필요한 경우 연속적인 CPU 메모리로 복사됩니다. 이는 H2D 전송 시 발생하는 오버헤드를 줄여 성능을 향상시킵니다.
3. 단위 테스트 (test/registered/unit/layers/moe/test_copy_weight_views_before_h2d.py)
새로운 기능의 정확성을 검증하기 위해 test_copy_weight_views_before_h2d.py 파일에 단위 테스트가 추가되었습니다. 이 테스트는 다양한 시나리오(연속적, 비연속적, 오프셋 존재 여부 등)에서 _copy_weight_view_before_h2d 함수가 올바르게 동작하는지 확인합니다.
--- /dev/null
+++ b/test/registered/unit/layers/moe/test_copy_weight_views_before_h2d.py
@@ -0,0 +1,50 @@
+import torch
+
+from sglang.srt.layers.moe.fused_moe_triton.layer import (
+ _copy_weight_view_before_h2d,
+) from sglang.test.ci.ci_register import register_cpu_ci
+
+register_cpu_ci(est_time=2, suite="base-a-test-cpu")
+
+
def _assert_independent_storage(tensor):
+ """Assert that the tensor uses independent and contiguous storage."""
+ assert tensor.is_contiguous(), "Tensor is not contiguous"
+ assert tensor.storage_offset() == 0, "Tensor storage offset is not 0"
+ # Check if the storage is independent (not a view of a larger tensor)
+ # This is a bit tricky to assert directly, but we can infer it if the
+ # tensor's size matches the storage size.
+ assert tensor.untyped_storage().nbytes() == tensor.numel() * tensor.element_size(), \
+ "Tensor storage is not independent or has unexpected size"
+
+def test_non_contiguous_view():
+ # Create a large tensor
+ base_tensor = torch.randn(1024, 1024)
+ # Create a non-contiguous view
+ view_tensor = base_tensor[:, ::2]
+ assert not view_tensor.is_contiguous()
+ assert view_tensor.storage_offset() != 0
+
+ # Apply the optimization function
+ copied_tensor = _copy_weight_view_before_h2d(view_tensor)
+
+ # Assert that the copied tensor is independent and contiguous
+ _assert_independent_storage(copied_tensor)
+ # Ensure the data is the same
+ assert torch.allclose(copied_tensor, view_tensor)
+
+def test_zero_offset_non_contiguous_view():
+ # Create a tensor with zero offset but non-contiguous (e.g., transposed)
+ base_tensor = torch.randn(1024, 1024)
+ view_tensor = base_tensor.T # Transposed tensor is often non-contiguous
+ # Note: Depending on PyTorch version and optimizations, .T might be contiguous or not.
+ # We ensure a non-contiguous case for testing.
+ if view_tensor.is_contiguous():
+ view_tensor = view_tensor.contiguous()
+ # Manually create a non-contiguous view if needed
+ view_tensor = view_tensor[:, ::2]
+
+ assert not view_tensor.is_contiguous()
+ # Storage offset might be 0 for transposed tensors, but the storage size check is key
+
+ copied_tensor = _copy_weight_view_before_h2d(view_tensor)
+ _assert_independent_storage(copied_tensor)
+ assert torch.allclose(copied_tensor, view_tensor)
+
+def test_already_contiguous_and_independent():
+ # Create a tensor that is already contiguous and independent
+ original_tensor = torch.randn(1024, 1024)
+ assert original_tensor.is_contiguous()
+ assert original_tensor.storage_offset() == 0
+ assert original_tensor.untyped_storage().nbytes() == original_tensor.numel() * original_tensor.element_size()
+
+ # Apply the optimization function
+ copied_tensor = _copy_weight_view_before_h2d(original_tensor)
+
+ # The function should return the original tensor without copying
+ assert copied_tensor is original_tensor
+
+def test_cpu_tensor_with_offset():
+ # Create a tensor with an offset in its storage
+ base_storage = torch.randn(2048)
+ offset_tensor = base_storage[512:1536]
+ assert offset_tensor.storage_offset() == 512
+ assert not offset_tensor.is_contiguous()
+
+ copied_tensor = _copy_weight_view_before_h2d(offset_tensor)
+ _assert_independent_storage(copied_tensor)
+ assert torch.allclose(copied_tensor, offset_tensor)
+
+def test_gpu_tensor():
+ if torch.cuda.is_available():
+ gpu_tensor = torch.randn(1024, 1024, device='cuda')
+ # The function should return the original tensor without modification
+ returned_tensor = _copy_weight_view_before_h2d(gpu_tensor)
+ assert returned_tensor is gpu_tensor
+ else:
+ print("CUDA not available, skipping GPU tensor test.")
이 테스트들은 다양한 엣지 케이스를 커버하며, 최적화 로직이 의도한 대로 동작함을 보장합니다.
왜 이게 좋은가?
1. 획기적인 성능 향상
이 PR의 가장 큰 장점은 모델 로딩 시간을 극적으로 단축시킨다는 것입니다. PR 설명에 따르면, DeepSeek-V4-Pro TP8 모델 로딩 시간이 약 35분에서 6분 20초로 5.6배 빨라졌습니다. 이는 엣지 랭크(edge-rank)의 H2D 복사 시간(p50)이 41-45ms에서 약 5.4ms로 크게 감소한 결과입니다. CPU 복사 오버헤드는 약 10초 정도로, 전체 로딩 시간 단축 효과에 비하면 미미한 수준입니다.
추가적인 MI300 나이틀리 검증 결과에서도 Qwen 및 GPT-OSS 모델들에 대해 1.79배에서 최대 7.8배까지 로딩 속도 향상이 관찰되었습니다. 이는 MoE 모델, 특히 가중치 파티셔닝이 복잡한 모델에서 이 최적화 기법이 매우 효과적임을 입증합니다.
2. 일반적인 교훈: mmap 뷰의 비효율성 이해 및 해결
이 PR은 LLM 로딩 과정에서 흔히 발생하는 mmap 뷰(view)의 비효율성을 잘 보여줍니다. safetensors와 같은 라이브러리는 mmap을 사용하여 모델 가중치를 효율적으로 로드하지만, 이 과정에서 생성되는 텐서 뷰는 원본 파일의 일부만을 참조하며 연속적이지 않거나(non-contiguous), 특정 오프셋에서 시작하는 경우가 많습니다. 이러한 뷰를 GPU로 직접 전송(H2D)하려고 할 때, 하드웨어는 이를 효율적으로 처리하지 못하고 추가적인 복사 또는 비효율적인 전송을 수행하게 됩니다. 이는 특히 수많은 작은 MoE 가중치 조각들을 다룰 때 병목 현상을 심화시킵니다.
이 PR에서 제시한 해결책은 이러한 비효율성을 명확히 인지하고, H2D 전송 직전에만 필요한 경우에 한해 CPU 메모리 상에서 해당 뷰를 독립적이고 연속적인 텐서로 복사하는 것입니다. 이
참고 자료
- https://pytorch.org/docs/stable/generated/torch.Tensor.html#torch.Tensor.clone
- https://pytorch.org/docs/stable/tensor_attributes.html#torch.Tensor.is_contiguous
- https://pytorch.org/docs/stable/generated/torch.Tensor.html#torch.Tensor.storage_offset
- https://pytorch.org/docs/stable/generated/torch.Tensor.html#torch.Tensor.untyped_storage
⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.
관련 포스트
- [sglang] SM120 Blackwell에서 DeepSeek-V4 모델 서빙 최적화: FlashInfer MXFP4 MoE 도입 및 메모리 절감
- [sglang] 실시간 RGB 전송 속도 향상을 위한 최적화 분석
- [axolotl] Axolotl MoE 모델 최적화: Tiled-MLP 도입 및 FSDP2 통합으로 성능 극대화
- [sglang] SGLang MoE 라우팅 최적화: AMD GPU에서 aiter.biased_grouped_topk 활용
- [sglang] sglang, GLM-5.1-FP8 모델 성능 및 정확도 벤치마크 추가: AMD GPU 환경에서의 최적화 분석
PR Analysis 의 다른글
- 이전글 [sglang] SGLang, KV VMM 할당자 스텁 최적화를 통한 시작 시간 및 라이브러리 크기 대폭 개선
- 현재글 : [sglang] sglang, MoE 모델 로딩 속도 5.6배 향상: mmap 뷰 최적화 분석
- 다음글 [sglang] SGLang의 Session-Aware Unified Radix Cache를 통한 추론 성능 최적화
댓글