[triton] Blackwell Two-CTA MMA에 맞춘 CGA 레이아웃: M-first 분할로 멀티 CTA matmul 최대 9% 향상
PR 링크: triton-lang/triton#10817 상태: Merged | 변경: +146 / -16
들어가며
NVIDIA Blackwell(sm_100 계열)의 5세대 Tensor Core는 two-CTA MMA를 지원합니다. 같은 cluster 안의 인접한 두 CTA가 하나의 큰 MMA 타일을 함께 계산하는 방식입니다. 보통 두 CTA가 M 방향으로 나란히 서고, 각 CTA가 M의 절반씩(예: 128행)을 맡습니다. 이 구조의 장점을 살리려면 컴파일러가 CTA 간 텐서 분할(CGA layout)을 정할 때 다음 두 조건을 지켜야 합니다.
- M 축을 CTA 사이에 나눠야 합니다.
- 인접한 CTA ID(0과 1, 2와 3 …)가 M 축에서 짝이 되어야 합니다.
그런데 기존 AssignCGALayouts pass의 getDotCGASplit은 아키텍처와 무관한 휴리스틱을 썼습니다. 그 결과 2-CTA 테스트에서는 N 축만 분할했고(CGALayout = [[0, 1]]), 4-CTA에서는 첫 번째 CTA 비트가 N을 가리켰습니다([[0, 1], [1, 0]]). 하드웨어가 기대하는 "M 페어"와 컴파일러가 만든 레이아웃이 어긋나 있었던 것입니다. 이 PR은 Blackwell에서 M-first CTA 레이아웃을 우선 선택하도록 바꿉니다.
코드 분석
1. preferTwoCTALayout: 언제 적용할지 판단
bool preferTwoCTALayout(triton::DotOp dot) {
if (ttg::lookupNumCTAs(dot) < 2)
return false;
auto module = dot->getParentOfType<ModuleOp>();
int cc = getNVIDIAComputeCapability(module);
return cc >= 100 && cc < 120;
}
CTA가 2개 이상이고 compute capability가 100~119일 때만 새 정책을 켭니다. 이는 two-CTA MMA를 지원하는 datacenter Blackwell 계열입니다. sm_120(consumer Blackwell)은 범위에서 빠지므로, 기존 동작이 유지되어 회귀 위험이 그 범위 안에 갇힙니다.
리뷰에서 lezcano가 "이 체크가 tritonnvidiagpu dialect에 꼭 필요한가"라고 물었습니다. 이에 Jokeren은 처음에 있던 CUDA target 확인 코드를 제거했습니다(7c7a349e1e). 근거는 두 가지입니다. 이 pass는 이미 NVIDIA 전용이라 CUDA target을 전제하고, getNVIDIAComputeCapability가 target을 자체 검증합니다. 같은 검사를 중복하지 않고 기존 유틸리티의 보장에 맡긴 것입니다.
2. getDotCGASplit: M 페어 + N 분할
Before:
SmallVector<unsigned> getDotCGASplit(ArrayRef<int64_t> shape,
unsigned numCTAs) {
...
unsigned splitM = 1;
unsigned splitN = numCTAs;
// Prefer a larger M chunk, up to 128 elements, by assigning splitM first.
After:
SmallVector<unsigned> getDotCGASplit(ArrayRef<int64_t> shape, unsigned numCTAs,
bool preferTwoCTA) {
...
if (preferTwoCTA && numCTAs >= 2) {
unsigned splitM = std::max(2u, numCTAs / 2);
unsigned splitN = numCTAs / splitM;
if (m / splitM >= kMinChunkSize && m / splitM <= kPreferredChunkSize &&
n / splitN >= kMinChunkSize && n / splitN <= 256) {
ctaSplit[rank - 2] = splitM;
ctaSplit[rank - 1] = splitN;
return ctaSplit;
}
}
CTA 수별 분할은 다음과 같습니다.
- 2 CTAs:
splitM=2, splitN=1→ 한 쌍이 M을 나눕니다. - 4 CTAs:
splitM=2, splitN=2→ M 페어 두 개가 N을 반씩 맡습니다. - 8 CTAs:
splitM=4, splitN=2
이 경로는 조건을 만족할 때만 탑니다. CTA당 M 청크가 kMinChunkSize(64) 이상 kPreferredChunkSize 이하이고, N 청크가 64~256 범위여야 합니다. 이는 MMAv5 타일로서 유효하고 유용한 크기입니다. 조건을 벗어나면 기존 휴리스틱으로 fallback하므로 작은 shape나 극단적인 shape에서 안전합니다.
3. ctaOrder swap: 인접 CTA가 M에서 짝이 되도록
SmallVector<unsigned> ctaOrder;
for (unsigned dim = dTy.getRank(); dim > 0; --dim)
ctaOrder.push_back(dim - 1);
// The first two order entries are the last tensor dimensions: N, then M.
if (preferTwoCTA)
std::swap(ctaOrder[0], ctaOrder[1]);
분할 개수만 바꿔서는 부족합니다. CTA ID의 가장 빠르게 변하는 축이 N이면, CTA 0과 1은 N 방향으로 이웃하게 됩니다. order를 swap해 M을 fastest 축으로 만들어야 CTA 0/1, 2/3이 M 방향 페어가 되고, two-CTA MMA의 하드웨어 페어링과 정확히 맞습니다.
4. 테스트 변화로 보는 레이아웃
4-CTA dot_split_mn 테스트입니다.
- CGALayout = [[0, 1], [1, 0]]
+ CGALayout = [[1, 0], [0, 1]]
첫 번째 basis(CTA ID bit 0)가 이제 M([1, 0])을 가리킵니다. 2-CTA 테스트에서는 dot 레이아웃이 [[0, 1]]에서 [[1, 0]](M 분할)로 바뀌었습니다. B operand load 레이아웃은 [[0, 0]]이 되었는데, 이는 B가 두 CTA에 broadcast된다는 의미입니다. M 페어는 같은 N 범위를 공유하므로 자연스러운 결과입니다. threadsPerWarp도 [2, 16]에서 [1, 32]로 바뀌었습니다. CTA 내부 타일이 N 방향으로 넓어진 만큼 warp 내 스레드 배치도 그에 맞게 재계산된 것입니다.
왜 이게 좋은가
GB200, FP16, M=N=8192, SM clock 1.2 GHz 고정, WS(warp specialization) 비활성 조건의 측정 결과(TFLOPS)입니다.
| K | 2 CTAs | 4 CTAs | 8 CTAs |
|---|---|---|---|
| 512 | 810.8 → 807.8 | 468.0 → 493.5 | 529.5 → 542.3 |
| 2048 | 1189.5 → 1189.6 | 525.8 → 562.7 | 716.1 → 774.4 |
| 8192 | 1354.3 → 1335.9 | 531.4 → 579.3 | 743.9 → 805.6 |
4-CTA는 K=8192에서 약 +9%, 8-CTA는 약 +8% 향상됐습니다. 2-CTA는 사실상 변화가 없거나 노이즈 범위입니다. 이 표의 절대값을 보면 4/8-CTA 구성은 여전히 2-CTA보다 느리므로, 멀티 CTA 경로는 앞으로도 최적화 여지가 큽니다.
일반적 교훈
- 레이아웃은 하드웨어 협업 단위를 반영해야 합니다. "몇 개로 나누느냐"만큼 "어떤 순서로 이웃하느냐"(
ctaOrder)가 중요합니다. 하드웨어가 인접 ID끼리 협업한다면 그 축을 fastest로 두어야 합니다. - 아키텍처 특화 휴리스틱은 좁게 gating하고 fallback을 남겨야 합니다. compute capability 범위와 청크 크기 조건을 둘 다 만족할 때만 새 경로를 타게 해서 기존 동작을 보존했습니다.
- 검증은 한 곳에서 합니다. 리뷰 피드백에 따라 중복 target 체크를 제거하고
getNVIDIAComputeCapability의 검증에 맡긴 것은 작지만 좋은 정리입니다.
참고 자료
- CUDA C++ Programming Guide — Thread Block Cluster 개념(멀티 CTA 협업의 기반) 공식 문서
- PTX ISA — Blackwell tcgen05(two-CTA MMA) 명령어가 정의된 PTX 공식 문서
- std::swap — ctaOrder 교환에 사용된 표준 함수
⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.
관련 포스트
PR Analysis 의 다른글
- 이전글 [vllm] Blackwell에서 fp8 KV 캐시 기반 QSA sparse attention을 2.6배 빠르게: split-K를 대체하는 single-pass 커널
- 현재글 : [triton] Blackwell Two-CTA MMA에 맞춘 CGA 레이아웃: M-first 분할로 멀티 CTA matmul 최대 9% 향상
- 다음글 [GPT-SoVITS] GPT-SoVITS DiT 샘플러에 cache-dit(DBCache) 블록 캐싱 도입하기: forward를 세 조각으로 나눈 이유
댓글