본문으로 건너뛰기

[sglang] SGLang, 대규모 언어 모델 디버깅 속도 향상을 위한 스마트한 텐서 비교 최적화

PR 링크: sgl-project/sglang#30656 상태: Merged | 변경: +221 / -8

들어가며

대규모 언어 모델(LLM)을 개발하고 디버깅하는 과정에서 모델의 출력을 비교하고 검증하는 것은 필수적입니다. 특히, 두 개의 모델 출력(dump) 간의 차이를 분석할 때, 텐서(tensor) 단위의 비교는 핵심적인 역할을 합니다. 하지만 기존 SGLang의 디버깅 유틸리티에서는 이 텐서 비교 과정, 특히 백분위수(percentile) 계산에 상당한 시간이 소요되어 전체 디버깅 프로세스의 병목 현상을 야기했습니다.

이번 PR(#1234)은 이러한 문제를 해결하기 위해 FailureDisplayBudget라는 새로운 메커니즘을 도입하여, 불필요한 텐서 비교 연산을 줄이고 디버깅 속도를 크게 향상시키는 것을 목표로 합니다.

코드 분석

이번 변경의 핵심은 FailureDisplayBudget라는 새로운 클래스를 도입하고, 이를 텐서 비교 로직에 통합하여 불필요한 계산을 건너뛰도록 하는 것입니다. 변경 사항은 주로 comparator 모듈 내의 여러 파일에 걸쳐 이루어졌습니다.

1. sglang/srt/debug_utils/comparator/entrypoint.py

이 파일은 디버깅 도구의 진입점 역할을 합니다. run 함수에서 FailureDisplayBudget 객체가 생성되어 compare_bundle_pair 함수로 전달됩니다.

Before:

-            diff_threshold_rules=parse_diff_threshold_rules(
-                args.diff_threshold, default_predicate=DEFAULT_PREDICATE
-            ),
+            diff_threshold_rules=parse_diff_threshold_rules(
+                args.diff_threshold, default_predicate=DEFAULT_PREDICATE
+            ),

After:

+            failure_display_budget=FailureDisplayBudget(),
             thd_seq_lens_by_step_pair=ta_result.thd_seq_lens_by_step_pair,
             viz_output_dir=viz_output_dir,
             compute_per_token=visualize_per_token is not None,

_compare_bundle_pairs 함수 또한 failure_display_budget 인자를 받도록 수정되어, 하위 함수로 이를 전달합니다.

2. sglang/srt/debug_utils/comparator/bundle_comparator.py

이 파일은 번들(bundle) 단위의 비교를 담당합니다. compare_bundle_pair 및 내부 함수들이 failure_display_budget 인자를 받도록 수정되어, compare_tensor_pair 함수로 전달됩니다.

Before:

 def compare_bundle_pair(
     token_aligner_mode: Optional[str],
     token_aligner_plan: Optional[TokenAlignerPlan],
     diff_threshold_rules: Optional[list[DiffThresholdRule]] = None,
+    thd_seq_lens_by_step_pair: Pair[Optional[dict[int, list[int]]]] = Pair(
+        x=None, y=None
+    ),
     viz_output_dir: Optional[Path] = None,
     compute_per_token: bool = False,
 ) -> BundleComparisonInfo:

After:

 def compare_bundle_pair(
     token_aligner_mode: Optional[str],
     token_aligner_plan: Optional[TokenAlignerPlan],
     diff_threshold_rules: Optional[list[DiffThresholdRule]] = None,
+    failure_display_budget: Optional[FailureDisplayBudget] = None,
     thd_seq_lens_by_step_pair: Pair[Optional[dict[int, list[int]]]] = Pair(
         x=None, y=None
     ),
@@ -147,6 +149,7 @@ def compare_bundle_pair(
             token_aligner_mode=token_aligner_mode,
             token_aligner_plan=token_aligner_plan,
             diff_threshold_rules=diff_threshold_rules,
+            failure_display_budget=failure_display_budget,
             thd_seq_lens_by_step_pair=thd_seq_lens_by_step_pair,
             viz_output_dir=viz_output_dir,
             compute_per_token=compute_per_token,

3. sglang/srt/debug_utils/comparator/tensor_comparator/comparator.py

이 파일은 실제 텐서 비교 로직을 포함하며, 가장 중요한 변경이 이루어진 곳입니다.

  • FailureDisplayBudget 클래스 도입:
  • from dataclasses import dataclass
  • @dataclass
  • class FailureDisplayBudget:
  •    max_detail: int = 50
    
  •    num_emitted: int = 0
    
  •    def take(self) -> bool:
    
  •        if self.max_detail < 0:
    
  •            return True
    
  •        if self.num_emitted >= self.max_detail:
    
  •            return False
    
  •        self.num_emitted += 1
    
  •        return True
    
    이 클래스는 최대 몇 개의 실패한 텐서에 대해 상세 정보(백분위수 포함)를 계산할지 제한하는 역할을 합니다. `max_detail`이 음수이면 제한이 비활성화됩니다. `take()` 메소드는 예산이 남아있으면 `True`를 반환하고 예산에서 하나를 차감하며, 예산이 소진되면 `False`를 반환합니다.
    
    
  • compute_tensor_info 함수 수정: include_percentiles 인자가 추가되어, 이 값이 False일 경우 백분위수 계산을 건너뜁니다.
  • tensor: torch.Tensor, *, include_sample: bool = False
  • tensor: torch.Tensor,
  • *,
  • include_sample: bool = False,
  • include_percentiles: bool = True, ) -> TensorInfo: """Compute TensorInfo (shape, dtype, stats, optional sample) for a single tensor."""
  • stats: TensorStats = _compute_tensor_stats(tensor.float())
  • stats: TensorStats = _compute_tensor_stats(
  •    tensor.float(), include_percentiles=include_percentiles
    
  • ) sample: Optional[str] = ( str(get_truncated_value(tensor.float())) if include_sample else None ) ```
  • compare_tensor_pair 함수 로직 변경: 이 함수는 이제 두 단계로 나뉘어 실행됩니다. 먼저, 백분위수 계산 없이 기본적인 비교(shape mismatch, diff 값)를 수행하여 실패 여부를 결정합니다. 만약 실패하고 FailureDisplayBudget에서 예산을 할당받으면(failure_display_budget.take()True를 반환하면), 그때서야 백분위수 계산을 포함하여 상세 정보를 다시 계산합니다. 이를 통해 실패하지 않거나 예산을 초과한 실패의 경우 불필요한 백분위수 계산을 피할 수 있습니다.
  • baseline_info: TensorInfo = compute_tensor_info(x_baseline)
  • target_info: TensorInfo = compute_tensor_info(x_target)
  • x_baseline = try_unify_shape(x_baseline, target_shape=x_target.shape) unified_shape = list(x_baseline.shape)

@@ -81,8 +100,31 @@ def compare_tensor_pair( x_target=x_target_f, predicate=predicate, seq_dim=seq_dim,

  •        include_percentiles=False,
       )
    
  • is_failure = shape_mismatch or (diff is not None and not diff.passed)

  • needs_detail = is_failure and (

  •    failure_display_budget is None or failure_display_budget.take()
    
  • )

  • baseline_info: TensorInfo = compute_tensor_info(

  •    x_baseline_original, include_percentiles=needs_detail
    
  • )

  • target_info: TensorInfo = compute_tensor_info(

  •    x_target, include_percentiles=needs_detail
    
  • )

  • if not shape_mismatch and needs_detail:

  •    diff = compute_diff(
    
  •        x_baseline=x_baseline_f,
    
  •        x_target=x_target_f,
    
  •        predicate=predicate,
    
  •        seq_dim=seq_dim,
    
  •        include_percentiles=True,
    
  •    )
    
  • if diff is not None: needs_sample = diff.max_abs_diff > SAMPLE_DIFF_THRESHOLD if needs_sample: baseline_info.sample = str(get_truncated_value(x_baseline_f)) @@ -97,6 +139,7 @@ def compare_tensor_pair( x_baseline=x_baseline_f.to(downcast_dtype), x_target=x_target_f.to(downcast_dtype), predicate=predicate,

  •                include_percentiles=needs_detail,
               )
    

    return TensorComparisonInfo(

  • _compute_tensor_statscompute_diff 함수 수정: 이 함수들 역시 include_percentiles 인자를 받아 백분위수 계산 여부를 제어합니다. QUANTILE_NUMEL_THRESHOLD보다 큰 텐서의 경우, include_percentilesTrue일 때만 백분위수를 계산하도록 로직이 추가되었습니다.

4. test/registered/debug_utils/comparator/tensor_comparator/test_comparator.py

새로운 FailureDisplayBudget 클래스의 동작을 검증하는 테스트 케이스들이 추가되었습니다. 예산이 정확히 할당되는지, 음수 값으로 비활성화되는지, 통과하는 텐서가 예산을 소모하지 않는지 등을 확인합니다.

왜 이게 좋은가?

이 PR의 가장 큰 장점은 성능 향상입니다. 기존에는 모든 텐서 쌍에 대해, 특히 실패하는 경우 상세한 백분위수 통계를 계산했습니다. 하지만 PR 설명에 따르면 numpy.percentile 호출은 rel_diff 감소보다 10-40배 더 많은 비용이 들 수 있습니다. 대부분의 텐서가 실패하는 상황(예: 시스템적인 발산)에서는 이 비용이 누적되어 상당한 성능 저하를 초래했습니다.

FailureDisplayBudget 도입으로 다음과 같은 이점을 얻을 수 있습니다:

  1. 불필요한 계산 제거: 실패하지 않는 텐서나 예산을 초과하여 상세 정보가 필요 없는 실패 텐서의 경우, 비용이 많이 드는 백분위수 계산을 건너뜁니다.
  2. 효율적인 디버깅: 실제 디버깅 시 사용자가 관심을 가질 만한 소수의 실패 사례에만 상세 정보를 집중함으로써, 불필요한 정보 처리 시간을 줄이고 전반적인 디버깅 경험을 개선합니다.
  3. 유연성: max_detail 값을 조절하거나 음수로 설정하여 상세 정보 표시 방식을 유연하게 제어할 수 있습니다. 예를 들어, -1로 설정하면 이전과 같이 모든 실패에 대해 상세 정보를 표시합니다.

이 최적화는 LLM 디버깅과 같이 대규모 데이터셋과 복잡한 모델을 다룰 때 특히 중요합니다. 디버깅 시간을 단축함으로써 개발 생산성을 크게 향상시킬 수 있습니다.

일반적인 교훈:

  • 진단 정보와 결정 로직 분리: 성능에 민감한 결정 로직(pass/fail)과 사용자에게 보여주기 위한 진단 정보(percentiles, samples)를 분리하고, 진단 정보 생성 비용을 최적화하는 것이 중요합니다.
  • 점진적 상세 정보 로딩: 모든 상세 정보를 한 번에 계산하기보다, 필요에 따라 점진적으로 계산하거나 제한된 예산 내에서만 계산하는 방식을 고려할 수 있습니다.
  • 측정 및 프로파일링의 중요성: 어떤 부분이 병목인지 정확히 파악하고 최적화하는 것이 중요합니다. 이 PR은 백분위수 계산이 주요 병목임을 정확히 지적하고 해결했습니다.

References

참고 자료

⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.

댓글

관련 포스트

PR Analysis 의 다른글