Part 3.14 - 비합리적인 끝없는 시리즈
우리는 문제에 대해 이야기했고, 그다음 무엇(what)에 대해 이야기했습니다. 다음으로는 왜(why)에 깊이 파고들 것입니다.
다음 트릭을 위해, 저는 50달러를 사라지게 만들 것입니다! (아니요, 정말입니다. 실험을 돌리기 위해 H200과 B200 임대 서비스를 구매했을 뿐입니다 lol)
이번 후속편에서는 여러분의 고려를 위한 몇 가지 더 흥미로운 결과를 제시할 예정입니다. 서로 다른 클라우드 제공업체의 H200 카드, B200 카드, 그리고 SM120 rtx 6k에서 완전히 재현 가능한 테스트를 실행하면 어떤 일이 발생할까요?
모두가 틀릴 때 아무도 옳지 않다
문제는 객관적인 '정답'이 없다는 것입니다. 모델을 위한 모든 하드웨어/소프트웨어 설정은 아무리 정확하고 정교하게 다듬어졌더라도 서로 다릅니다.
Alibaba는 우리가 소비할 수 있도록 “이 프롬프트를 기반으로, 여러분의 설정이 생성해야 하는 정확한 로짓(logits)은 다음과 같습니다”와 같은 데이터셋을 공개하지 않습니다.
그렇다면 끝없이 이어지는 다양한 답변 속에서 '정답'을 어떻게 정의할까요? 부디 이 영상이 도움이 되기를 바랍니다.
이 사진에 단위가 없는 것을 주목하세요. 이것은 사물들이 얼마나 다른지에 대한 절대적인 시각이 아닙니다. 모든 것은 상대적입니다. SM120과 B200과 H200에서 flash attention 2는 얼마나 다를까요? 삼각형을 그리세요. 단일 H200 카드에서 flash inference가 triton attention으로부터 flash attention 2와 얼마나 다른지? 또 다른 삼각형을 그리세요. 이 삼각형들을 연결함으로써 우리는 3차원 영역을 얻게 됩니다. 그리고 이제 모두가 그 영역의 중심으로부터 상대적인 차이를 플로팅할 수 있습니다. 그렇다면 누가 가장 가까운가요?
이 결과는 제가 설명할 몇 가지 이유 때문에 상당히 흥미롭습니다. 당연히 계산은 프롬프트에 따라 달라지므로, 저는 발산을 계산하기 위해 많은 컨텍스트 깊이에 걸쳐 가장 넓은 범위의 데이터를 사용했습니다.
| Message |
Depth |
Tokens |
Content |
| SP01-M001 |
6,335 |
108 |
text+tool_calls |
| SP01-M002 |
13,319 |
118 |
text+tool_calls |
| SP01-M003 |
13,765 |
185 |
text+tool_calls |
| SP03-M010 |
19,873 |
331 |
text |
| SP03-M027 |
29,634 |
389 |
텍스트와 네 개의 병렬 CLI(Command Line Interface) 도구 호출 |
| SP03-M046 |
67,144 |
505 |
텍스트와 다섯 개의 병렬 CLI 도구 호출 |
| SP03-M054 |
79,488 |
627 |
뮤테이션 재시도(mutation retry)를 포함한 텍스트와 네 개의 병렬 도구 호출 |
| SP03-M073 |
94,146 |
225 |
텍스트와 하나의 정확한 원격 CLI 명령어 |
| SP03-M097 |
122,863 |
698 |
텍스트와 하나가 확정된 switch-configuration apply 호출 |
| SP04-M018 |
19,548 |
145 |
text+tool_calls |
| SP04-M026 |
31,058 |
376 |
text+tool_calls |
| SP04-M031 |
39,292 |
202 |
tool_calls |
| SP04-M046 |
52,085 |
130 |
tool_calls |
| SP04-M081 |
76,659 |
342 |
text+tool_calls |
| SP04-M106 |
94,962 |
340 |
text+tool_calls |
| SP06-M008 |
18,829 |
389 |
진단 설명(diagnostic prose)과 세 개의 병렬 호스트/컨테이너 도구 호출 |
| SP06-M010 |
27,525 |
936 |
진단 설명과 세 개의 병렬 셸/Python/SQLite 도구 호출 |
| SP06-M018 |
42,192 |
347 |
마이그레이션 계획(migration plan)과 하나의 뮤테이팅 Podman 도구 호출 |
| SP06-M019 |
42,769 |
270 |
간략한 복구 설명(brief recovery prose)과 하나의 수정된 Podman 도구 호출 |
| SP06-M023 |
48,626 |
1,570 |
원격 쓰기 호출(remote-write call) 내에서 완전한 Python SQLite-to-PostgreSQL 마이그레이션 스크립트가 포함된 설명 |
| SP06-M042 |
73,149 |
330 |
설명과 두 개의 병렬 코드 검사(code-inspection) 도구 호출 |
| SP06-M050 |
83,233 |
497 |
Mermaid와 정확한 구성 이름이 포함된 설명 형태의 아키텍처 권장 사항 |
총계 |
|
9,060 |
22 ranges |
다양한 길이의 프롬프트가 사용되어 9000개 이상의 위치 샘플링을 초래했습니다.
"필요한 것은 어텐션(attention)뿐이다" 하지만... 어떤 종류일까?
저희의 경우, FA2, FI, TRT라는 세 가지 다른 플러그 가능한 어텐션 백엔드를 테스트하고 있습니다.
모델의 입력값(inputs), 가중치(weights), key/value 캐시, 그리고 출력 로짓(output logits)에는 bfloat16이 사용되었습니다. 이는 32비트 부동소수점보다 정밀도는 현저히 낮지만 넓은 수치 범위를 유지하는 16비트 부동소수점 형식입니다.
각 하드웨어/소프트웨어 좌표는 매우 반복 가능했습니다. H200 결과는 두 공급자 간에 바이트 단위로 동일했으며, B200과 로컬 SM120의 반복 결과 역시 바이트 단위로 동일했습니다. 따라서 이러한 차이점들은 무작위적인 실행별 노이즈라기보다는 서로 다른 산술 프로그램의 안정적인 결과처럼 보입니다.
일반적으로 지나친 단순화에 대한 면책 조항을 알려드립니다.
그림 속 주황색 상자는 32비트 숫자가 16비트 bfloat16으로 반올림되는 위치이며, 파란색 상자는 32비트 상태를 유지합니다. 보라색 상자는 입력 토큰이 병렬 작업 그룹(parallel work groups)으로 분할되는 방식을 보여줍니다.
!!! 경고 !!!
a5d06761d4d3fed9158d034359c934b4
길고 지루한 수학이 다가옵니다! 지금 눈을 피하세요!!!
이 섹션은 전적으로 @splifingate의 잘못입니다. 그가 저에게 수학하는 방법을 보여주었고, 이제 여러분 모두 고통받게 될 것입니다. 게다가 제 수학 실력은 여러분 GPU만큼 정확하지 않습니다. :stuck_out_tongue: :stuck_out_tongue:
어텐션(attention)이 계산하려는 것
새로운 토큰 하나에 대해, 모델은 **쿼리 벡터(query vector)**를 구성합니다. 이는 현재 찾고 있는 정보입니다. 이전의 모든 토큰들은 해당 토큰의 관련성을 측정하는 데 사용되는 **키 벡터(key vector)**와, 해당 토큰이 가중치를 받을 경우 검색되는 정보를 담는 **값 벡터(value vector)**를 가지고 있습니다.
이전 토큰 번호 token에 대해
이상적인 실수 계산은 다음과 같습니다:
ext{attention score}_{ ext{token}}
= ext{scale} imes ext{dot}( ext{query}, ext{key}_{ ext{token}}),
ext{unnormalized weight}_{ ext{token}}
= ext{exp}( ext{attention score}_{ ext{token}}),
ext{normalized weight}_{ ext{token}}
= rac{ ext{unnormalized weight}_{ ext{token}}}{ ext{earlier tokens의 unnormalized weight 합}},
= ext{earlier tokens에 대한} ext{normalized weight}{ ext{token}} imes ext{value}{ ext{token}}.
Qwen은 어텐션 헤드당 256개의 숫자를 사용하므로, 그 점수 스케일(score scale)은
scale}=\frac{1}{\sqrt{256}}=\frac{1}{16}=2^{-4}$.
이 스케일은 이진수로 정확하게 표현됩니다. 차이는 점 곱(dot products), 지수 함수(exponentials), 합계(sums), 중간 반올림(intermediate rounding), 병렬 조합(parallel combination)이 수행되는 방식에서 발생합니다.
계산이 그룹으로 분할되는 이유
27,525 토큰의 이력은 하나의 GPU 작업 블록으로는 너무 큽니다. 각 어텐션 구현은 이력을 그룹으로 나누고, 모든 그룹에 대해 부분적인 답을 계산한 다음, 나중에 부분적인 답들을 조합합니다.
하나의 그룹에 대해 커널은 먼저 **국소 최대 점수(local maximum score)**를 찾는데, 이는 해당 그룹에서 가장 큰 어텐션 점수를 의미합니다. 이를 빼주는 것은 지수 함수가 오버플로우하는 것을 방지합니다:
\text{local exponential weight}{\text{token}}\n=\exp(\n\text{attention score}{\text{token}}\n-\n\text{local maximum score}\n).
정확한 산술(exact arithmetic)에서는, 다른 국소 최대 값을 빼더라도 최종 결과가 변하지 않습니다. 왜냐하면 스케일 팩터가 최종 조합 과정에서 상쇄되기 때문입니다. 실제 bfloat16 경로에서 FlashAttention 2는 다음과 같은 구조를 사용합니다:
\text{partial weighted value sum}\n=\operatorname{float32}_sum\n(\n\operatorname{round to bfloat16}\n(\text{local exponential weight})\n\times\n\text{value}\n)
반면, 정규화 합계(normalization sum)는 반올림되지 않은 32비트 지수 가중치(unrounded 32-bit exponential weights)를 사용합니다:
\text{partial normalization sum}\n=\operatorname{float32}_sum\n(\n\text{local exponential weight}\n).
반올림은 스케일 재조정(rescaling)과 교환 법칙이 성립하지 않습니다. 일반적으로,
\operatorname{round to bfloat16}(e^{x-c})\n\ne^{-c}\n\operatorname{round to bfloat16}(e^x).\n
따라서 그룹 경계를 변경하는 것은 국소 최대값을 바꿀 수 있으며, 이로 인해 모든 가중치가 bfloat16 숫자 격자에 놓이고, 가중치-값 합계(weighted-value sum)와 최종 조합 트리의 모양이 달라질 수 있습니다.
FlashAttention 2가 54, 62 또는 87 그룹을 선택하는 이유
FlashAttention 2는 이 Qwen 디코드에서 64 토큰의 키/값 블록(key/value blocks)으로 작동합니다. 27,525 토큰일 때 블록 수는 다음과 같습니다:
\text{key value block count}\n=\left[\frac{27{,}525}{64}\right]\n=431.
스트리밍 멀티프로세서(streaming multiprocessor)는 GPU의 병렬 처리 장치 중 하나입니다. H200은 132개, B200은 148개, 테스트된 SM120 카드는 188개를 가지고 있습니다. 고정된 FlashAttention 2 휴리스틱은 디바이스 용량을 다음과 같이 추정합니다:
\text{device capacity} = 2 \times \text{streaming multiprocessor count}.
Qwen은 그룹화된 쿼리 리쉐이프(grouped-query reshape) 후 네 개의 키/값 헤드(key/value heads)를 가지므로, 후보 분할 개수(candidate split count)는 다음을 사용하여 점수가 매겨집니다:
\text{work ratio} = \frac{4 \times \text{candidate split count}}{\text{device capacity}},
\text{estimated efficiency} = \frac{\text{work ratio}}{\lceil\text{work ratio}\rceil}.
출처는 추정 효율성이 최고 후보의 최소 85% 이상인 가장 작은 적격 분할 개수를 선택합니다. 이를 통해 다음 표가 나옵니다:
| Platform | GPU processing units | Chosen groups | 64-token blocks per group | Maximum normal group span |
|---|
| H200 | 132 | 54 | 8 | 512 tokens |
| B200 | 148 | 62 | 7 | 448 tokens |
| SM120 | 188 | 87 | 5 | 320 tokens |
H200 경계는 토큰 0, 512, 1,024 등에서 시작합니다. B200 경계는 0, 448, 896에서 시작합니다. SM120 경계는 0, 320, 640에서 시작합니다. 결과적으로, 동일한 토큰이 각 GPU에서 서로 다른 이웃 토큰과 함께 정규화됩니다.
실제 숫자 대입하기
첨부된 Python 프로그램은 결정론적인 27,525-토큰의 학습 입력(teaching input)을 생성합니다. 이것들은 합성 점수 및 값이며, 포착된 Qwen 활성화가 아니지만, 이 프로그램은 실제 64-토큰 블록과 실제 54/62/87 분할 개수를 사용합니다.
해당 입력에 대한 이상적인 실수 출력(exact real number output)은 다음과 같습니다:
\text{exact real number output} = 1.325056137248.
스칼라 FlashAttention-2 유사 계산 결과는 다음과 같습니다:
| Platform | Split count | First group size | First local maximum | 32-bit output before final rounding | Final bfloat16 output |
|---|
| H200 | 54 | 512 tokens | 1.875 | 1.3241630793 | 1.3203125 |
| B200 | 62 | 448 tokens | 1.750 | 1.3242336512 | 1.3281250 |
| SM120 | 87 | 320 tokens | 1.500 | 1.3242592812 | 1.3281250 |
이 FlashAttention 2 예제에서는 플랫폼 선택 그룹핑만 변경되었습니다. H200 결과는 하나의 bfloat16 값에 떨어지는 반면, B200 및 SM120 결과는 다음 값으로 반올림됩니다. 이 예제는 경계를 노출하기 위해 의도적으로 선택된 것이며, 특정 분할 횟수가 일반적으로 더 정확하다는 것을 의미하지 않습니다.
AI 슬롭(slop)을 즐겨보세요:
#!/usr/bin/env python3
"""순수 언어적이고 의존성 없는 어텐션 산술 워크스루입니다.
이것은 비트 단위의 CUDA 에뮬레이터가 아니라 교육용 스칼라 시뮬레이터입니다.
...
"""
위 슬롭 생성기는 각 구현에 대해 local_maximum_score, normalization_sum, weighted_value_sum, 및 normalized_partial_output와 같은 설명적인 중간 이름을 출력합니다.
백엔드 이름은 네 가지 디코드 알고리즘으로 확장됩니다
점수화된 SP06 워크로드는 99.84% 토큰별 한 번에 하나씩 디코딩이므로, 디코드 알고리즘이 비교를 지배합니다.
Triton attention은 항상 16개의 연속적인 토큰 그룹을 사용합니다. 이는 가중치-값 합계(weighted-value sum), 지역 최대값(local maximum), 및 정규화 합계(normalization sum)를 32비트 부동 소수점으로 저장한 다음, 32비트 축소 트리에서 최대 16개 그룹 레코드를 결합합니다.
FlashAttention 2는 연속적인 64토큰 블록을 사용하지만, 그 54/62/87 그룹 수는 플랫폼의 처리 장치(processing-unit) 수에 따라 달라집니다. 이는 각 정규화된 부분 출력과 그 로그 정규화 통계량을 32비트 부동 소수점으로 저장합니다.
**FlashInfer의 디코더인 “XQA”**는 H200 및 SM120에서 실행됩니다. XQA는 해당 출처의 커널 패밀리 이름입니다. 이는 H200에서는 33개의 라운드 로빈 그룹을, SM120에서는 47개의 그룹을 사용하여 256토큰 GPU 작업 타일(work tiles)을 이들 그룹에 순환적으로 할당합니다. 또한 정규화 합계와 가중치-값 계산 모두에 bfloat16 지수 가중치를 사용하며, 최종 결합 전에 모든 정규화된 그룹 출력을 bfloat16으로 반올림합니다.
FlashInfer가 B200에서 대신 TensorRT-LLM 생성-어텐션 커널을 선택합니다. 이의 가시적인 리듀서(reducer)는 각 128 토큰 작업 타일(work tile)에 대해 하나의 bfloat16 가중치 값 부분합과 32비트 최대값 및 정규화 통계를 소비한 다음, 순서가 지정된 누적 재귀(ordered running recurrence)를 사용하여 해당 부분합들을 결합합니다. 주 어텐션 본체는 패키징된 기계어 코드이므로, 커널 실행 추적(kernel launch trace) 없이는 내부 확률 반올림을 명시할 수 없습니다.
Triton에서의 GPU별 로어링(lowering)
Triton 소스에는 "내적(dot product)" 및 "합계(sum)"와 같은 추상적인 연산이 포함되어 있습니다. 타겟별 GPU 로어링은 컴파일러 단계 중 하나로, 이러한 추상적인 연산을 특정 NVIDIA 아키텍처가 지원하는 레인 레이아웃(lane layout), 텐서 코어 명령어(tensor-core instructions), 샤플 연산(shuffle operations), 및 리덕션 트리(reduction tree)에 매핑합니다:
Triton program:
-> common compiler representations
-> SM90, SM100, or SM120-specific layout
...
고정된(pinned) Triton 어텐션 소스는 세 플랫폼 모두에서 공통의 LLVM 컴파일러 표현을 통해 동일하지만, 타겟 레이아웃, 가상 명령어, 그리고 기계어 바이너리는 다릅니다.
부동 소수점 괄호 트리(floating-point parenthesis tree)만 변경해도 결과가 달라질 수 있습니다. 1.0에서 $2^{-24}$는 다음 float32 숫자까지의 거리의 절반입니다: