본문으로 건너뛰기

[flashinfer] FlashInfer, Cake DSA 커널 최적화: Sparse Attention 성능 2배 향상

PR 링크: flashinfer-ai/flashinfer#7098 상태: Merged | 변경: +7600 / -6347

들어가며

최근 대규모 언어 모델(LLM)의 발전은 Attention 메커니즘의 효율성을 극대화하는 데 달려있습니다. 특히 Sparse Attention은 연산량을 줄여 성능을 향상시키는 핵심 기법 중 하나입니다. 이번 PR (Pull Request)은 FlashInfer 라이브러리의 cake_dsa_sparse_attention 및 dsa_sparse_attention_varlen 커널에서 DeepSeek Sparse Attention (DSA) 훈련 커널의 성능을 획기적으로 개선하는 데 초점을 맞추고 있습니다. 이 PR은 기존의 Cake DSA 구현을 기반으로, 특히 인덱스 준비(index preparation) 단계와 backward 연산에서 발생하는 비효율성을 제거함으로써, 최대 2배에 달하는 놀라운 성능 향상을 이루어냈습니다.

본 글에서는 이 PR이 어떤 문제들을 해결했고, 구체적으로 어떤 코드 변경을 통해 이러한 성능 향상을 달성했는지, 그리고 그 의미는 무엇인지 심층적으로 분석해보겠습니다.

코드 분석

이번 PR의 핵심 변경사항은 benchmarks/bench_cake_dsa_train.py 파일에 집중되어 있습니다. 주요 개선점은 다음과 같이 파일별로 나누어 살펴볼 수 있습니다.

1. 인덱스 준비 단계 최적화: Hole-Free Index Rows

기존 DSA 커널은 인덱스 행(index rows)에 "구멍(holes)"이 존재할 경우, 해당 구멍이 연산 비용을 발생시켰습니다. 이 PR은 이러한 구멍을 GPU 상에서 효율적으로 압축(compact)하여, 실제 유효한 인덱스 데이터만 처리하도록 개선했습니다. 이는 특히 희소(sparse)한 Attention 패턴에서 불필요한 연산을 제거하여 성능을 크게 향상시킵니다.

Before:

기존 코드에서는 인덱스 준비 과정에서 -1과 같은 값으로 표시된 구멍들이 그대로 유지되었습니다. 이는 GPU 커널이 해당 위치를 처리할 때 추가적인 분기(branching)나 스킵(skipping) 로직을 필요로 하여 성능 저하의 원인이 되었습니다.

After:

_globalize 함수와 _glue_compacts 함수가 추가되어, 인덱스 준비 단계에서 -1과 같은 구멍들을 제거하고 유효한 인덱스들만 압축하여 처리합니다. offset_gather_kv_indices 함수에 compact=True 옵션을 사용하여 이 기능을 활성화합니다. 이는 다음과 같은 코드 변경으로 나타납니다.

def _glue_compacts():
    """Whether this package's torch glue can compact (``offset_gather_kv_indices(..., compact=True)``, issue #7041); an older package keeps the position-preserving rows (its own behaviour, what the baseline arm measures)."""
    return (
        "compact" in inspect.signature(cake_backend.offset_gather_kv_indices).parameters
    )

def _globalize(local, cu_q, cu_k):
    """Global rows and lengths of per-document rows through the torch glue: the stable compaction with the valid counts where the package compacts (what its entries do on device), else the position-preserving rows."""
    kw = dict(compact=True) if _glue_compacts() else {}
    return cake_backend.offset_gather_kv_indices(
        local, cu_q, cu_k, causal=True, return_topk_length=True, **kw
    )

이 변경은 _nope_row 함수에 twin 인자가 추가되고, nope_repro_16k_gap, nope_docs264x992_rank0_fixedtail 등의 새로운 index 타입(repro_gap, pooled_fixed_tail)이 도입되면서 더욱 명확해집니다. 특히 twin='stable' 옵션은 기존의 구멍이 있는 행과 비교하기 위한 안정적인 압축 행을 생성합니다.

2. In-place Strided dout 처리

Backward 연산, 특히 RoPE (Rotary Positional Embedding) backward 단계에서 dout (output gradient) 텐서를 처리하는 방식이 개선되었습니다. 기존에는 dout을 복사하는 과정에서 추가적인 메모리 및 시간 비용이 발생했지만, 이제는 in-place 연산을 통해 이를 방지합니다.

Before:

이전 구현에서는 dout 텐서의 strided view를 처리하기 위해 복사본을 생성했을 가능성이 높습니다. 이는 특히 head-major 레이아웃에서 더 두드러질 수 있습니다.

After:

dsa_sparse_attention의 backward 함수에서 dout을 직접 in-place로 수정하여 사용합니다. 이는 _kv_leaf 함수와 issue_step 함수 등에서 dout을 requires_grad_()로 설정하고 직접 사용하는 방식으로 구현되었습니다. doc_16231_hm과 packed_glm_a_hm과 같은 새로운 ROWS 항목은 head-major dout 레이아웃을 명시적으로 지원하며, 이는 in-place 처리가 가능함을 시사합니다.

def _kv_leaf(self):
    leaf = getattr(self, "_kv_leaf_cache", None)
    if leaf is None:
        leaf = self._kv_leaf_cache = self.inp.kv.detach().requires_grad_()
    return leaf

def issue_step(self):
    """The issue's step (#7041): forward + ``autograd.grad`` to ``q`` and ``kv`` with a fresh ``dkv``."""
    kv = self._kv_leaf()
    out, _ = self._call(self.q, kv, None)
    return torch.autograd.grad(out, (self.q, kv), self.inp.dout)

이 최적화는 dout 텐서의 복사를 피함으로써 메모리 사용량을 줄이고, 복사 오버헤드를 제거하여 backward 연산 속도를 향상시킵니다.

3. Varlen 인덱스 준비 최적화: Programmatic Dependent Launch

dsa_sparse_attention_varlen 커널에서 각 문서별 인덱스를 전역 행(global rows)으로 변환하는 과정이 개선되었습니다. 이전에는 여러 개의 torch.launch를 순차적으로 호출해야 했지만, 이제는 단일 GPU launch로 이를 처리합니다.

Before:

Per-document 인덱스를 global rows로 변환하기 위해 여러 단계의 torch.launch가 필요했습니다. 이는 launch 오버헤드를 발생시키고 전체 처리 시간을 증가시켰습니다.

After:

_globalize 함수는 offset_gather_kv_indices를 호출하여 per-document 인덱스를 global rows로 변환하는 과정을 단일 launch로 통합합니다. 이 함수는 compact=True 옵션을 사용하여 인덱스를 압축하며, 이는 dsa_sparse_attention_varlen의 인덱스 준비 단계를 크게 가속화합니다.

# ... (이전 코드에서 _globalize 함수 참조)

def backward_chunks(self, st):
    # ...
    if self.eager is None:
        idx, length = _globalize(inp.idx_local, inp.cu_seqlens_q, inp.cu_seqlens_k)
        out, lse, o_lo = fwd(
            inp.q, inp.kv, idx, topk_length=length, softmax_scale=DEFAULT_NOPE_SCALE
        )
        chunks = []
        for q0, q1, cu_q, cu_k, segments in _chunk_bounds(inp, self.chunk):
            idx_c, len_c = _globalize(inp.idx_local[q0:q1], cu_q, cu_k)
            chunks.append((q0, q1, idx_c, len_c, segments))
        self.eager = dict(out=out, lse=lse, o_lo=o_lo, chunks=chunks)
    # ...

이 변경은 launch 오버헤드를 줄이고 GPU 활용률을 높여 dsa_sparse_attention_varlen의 효율성을 증대시킵니다.

4. Programmatic Dependent Launch

인덱스 준비 단계와 forward 커널이 이제 programmatic dependent launch 하에서 실행됩니다. 이는 커널 간의 실행 순서를 보다 유연하고 효율적으로 제어할 수 있게 합니다.

Before:

기존에는 커널 실행 순서가 고정되어 있거나, 명시적인 동기화 메커니즘에 의존했을 수 있습니다.

After:

Programmatic dependent launch를 통해 커널 실행이 동적으로 제어됩니다. 이는 _globalize 함수와 같이 여러 단계를 포함하는 연산에서 특히 유용하며, 불필요한 대기 시간을 줄이고 GPU 파이프라인을 최적화합니다.

왜 이게 좋은가?

이 PR은 다음과 같은 이유로 매우 훌륭한 최적화 및 개선이라고 할 수 있습니다.

  1. 상당한 성능 향상:

    • NoPE (cake_dsa_h64_nope_train): 특히 nope_repro_16k_gap (100% hole) 및 nope_docs264x992_rank0_fixedtail (75.8% hole)과 같은 희소한 데이터셋에서 Forward Pass에서 최대 2.084배, Backward Pass에서 최대 2.220배, Training Step에서 최대 2.202배의 속도 향상을 보여줍니다 (GB200 기준). 이는 Hole-Free Index Rows 최적화의 효과를 명확히 보여줍니다.
    • RoPE (cake_dsa_h64_train): RoPE 모델에서도 packed_glm_a_hm (head-major dout)에서 Forward Pass 1.130배, Backward Pass 1.061배의 속도 향상을 기록했습니다. 이는 In-place Strided dout 처리의 효과를 나타냅니다.
    • GB300에서도 유사한 수준의 성능 향상이 관찰되어, 다양한 최신 GPU 아키텍처에서 이점은 유효함을 보여줍니다.
  2. 메모리 효율성 증대:

    • Backward 연산에서 dout 텐서를 in-place로 처리함으로써 불필요한 메모리 복사를 제거했습니다. 이는 특히 큰 배치 사이즈나 긴 시퀀스 길이를 다룰 때 메모리 사용량을 줄여줍니다.
  3. GPU 활용률 극대화:

    • Varlen 인덱스 준비 단계에서 여러 launch를 단일 launch로 통합하고, Programmatic Dependent Launch를 도입함으로써 GPU launch 오버헤드를 줄이고 커널 실행의 효율성을 높였습니다.
  4. 일반적인 교훈:

    • 희소성(Sparsity) 활용: Attention 메커니즘에서 발생하는 희소성을 적극적으로 활용하여 불필요한 연산을 제거하는 것이 성능 향상의 핵심입니다. Hole-Free Index Rows는 이러한 희소성을 효과적으로 처리하는 좋은 예시입니다.
    • In-place 연산의 중요성: 가능한 경우 데이터를 복사하지 않고 기존 메모리 공간을 재사용하는 in-place 연산은 메모리 대역폭과 레이턴시를 절약하는 데 매우 효과적입니다.
    • Launch 오버헤드 최소화: GPU 커널 launch는 그 자체로 오버헤드를 가집니다. 여러 작은 launch를 하나의 큰 launch로 통합하거나, launch 간의 의존성을 효율적으로 관리하는 것이 중요합니다.
    • 정교한 벤치마킹: 다양한 시나리오 (e.g., hole이 많은 경우, head-major dout)에 대한 상세한 성능 측정은 최적화의 효과를 명확히 보여주고, 잠재적인 회귀(regression)를 방지하는 데 필수적입니다.

결론

이번 PR은 FlashInfer의 Cake DSA Sparse Attention 커널에 대한 매우 인상적인 최적화입니다. Hole-Free Index Rows, In-place Strided dout 처리, 그리고 Varlen 인덱스 준비 단계의 최적화를 통해 다양한 시나리오에서 최대 2배 이상의 성능 향상을 달성했습니다. 이는 LLM의 효율성을 높이는 데 크게 기여할 것이며, Sparse Attention 구현에 있어 중요한 기술적 진보를 보여줍니다. 이러한 최적화 기법들은 다른 딥러닝 프레임워크나 커널 개발에도 적용될 수 있는 귀중한 교훈을 제공합니다.

참고 자료

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

댓글

관련 포스트

PR Analysis 의 다른글