Skip to content

🧑‍🔬 연구원 리서치 노트 — 작성자: singon (김신곤) (4×4 & 4096×4096 완전 해설)

💡 핵심 기여 요약: 작은 4×4 행렬곱 타일링의 손가락 추적부터 4096×4096 대규모 행렬곱의 온칩 버퍼 용량 계산까지 전 과정을 상세히 풀어냈습니다.


행렬곱으로 이해하는 Roofline과 Tiling — 산술 강도, 데이터 재사용, 타일 크기 ​

핵심 질문
같은 계산을 하면서 데이터를 덜 읽어오면 커널은 얼마나 빨라질 수 있을까? 더 큰 타일로 재사용을 늘리는 데에는 어떤 비용이 따를까?


목차 ​

  1. 이번 주에 알아야 할 큰 그림
  2. 공통 예제의 수학과 텐서
  3. Arithmetic Intensity — 데이터 1 Byte로 얼마나 계산할까
  4. 두 시간에서 Roofline 성능 상한으로
  5. Roofline 그래프 — 축과 기울기와 분기점 읽기
  6. Tiling — 작은 조각으로 입력을 다시 쓰기
  7. 계산 예제 1 — 입력 재사용이 없는 행렬곱
  8. 계산 예제 2 — 32·64·128 타일 비교
  9. 타일 크기와 온칩 공간 — 크게 만들수록 좋을까
  10. 코드에서 읽기와 재사용과 쓰기 찾기
  11. 자주 생기는 오해와 문제 찾기
  12. 연습문제 3개
  13. 해답과 해설
  14. 스터디 발표와 함께 이야기할 질문
  15. 용어 정리와 마지막 확인
  16. 공식 자료와 추가 읽기

1. 이번 주에 알아야 할 큰 그림 ​

1.1 한 문장으로 정리 ​

같은 행렬곱도 입력을 다시 읽는 횟수가 줄면 더 빨라질 수 있다. 다만 재사용할 데이터를 보관할 온칩 공간이 필요하다.

이번 주에는 다음 한 줄을 계산량, 데이터 이동량, 저장 공간의 관점에서 살펴본다.

python
C = A @ B

수학적으로는 같은 행렬곱이다. 하지만 출력 하나씩 계산할 때마다 입력을 다시 읽는 방법과, 출력 묶음이 입력 조각을 함께 사용하는 방법은 HBM 이동량이 다르다.

관점먼저 물어볼 질문이번 주에 확인할 것
Arithmatic Intensity읽고 쓴 데이터 1 Byte당 얼마나 계산하는가?FLOPs / HBM Byte
Roofline어떤 자원이 처리율의 상한을 정하는가?연산 상한과 대역폭 상한
Tiling입력 하나를 어떤 출력들이 함께 쓰는가?타일 안의 입력 재사용
온칩 공간재사용할 입력과 중간 합을 어디에 둘까?Shared Memory·레지스터 사용량
실행 가능성타일을 키워도 충분한 작업이 함께 실행될까?자원 한도와 상주 작업 수

1.2 지난주와 이번 주의 연결 ​

지난주에는 연산 시간과 메모리 시간을 각각 구했다. 이번에는 같은 계산을 처리율 그래프로 표현하고, 데이터를 덜 옮기는 구현을 비교한다.

지난주에 배운 것이번 주에 이어갈 질문
연산 시간 = FLOPs / 연산 처리율이 시간이 그래프의 수평 상한과 어떻게 연결될까?
메모리 시간 = Byte / 대역폭산술 강도가 높아지면 왜 대역폭 측 상한도 높아질까?
두 시간 중 큰 값으로 병목 예상두 상한 중 작은 값으로 성능을 표현할 수 있을까?
중간값을 덜 옮기면 이동량 감소같은 입력을 여러 출력에 재사용하면 얼마나 줄어들까?

1.3 이번 주의 범위 ​

먼저 4×4 행렬곱을 2×2 출력 타일로 나누어 손으로 읽기 횟수를 센다. 이후 4096×4096 FP32 행렬곱으로 확장해 재사용 없음과 32·64·128 타일을 비교한다.

이번 주에 설명할 것후속 실습에서 확인할 것
산술 강도와 Roofline의 축·단위·분기점실제 커널의 처리율과 HBM 트래픽
타일링 전후의 FLOPs와 HBM 이동량Triton·CUDA의 타일 구현
입력 버퍼와 중간 합의 논리적 크기컴파일된 커널의 실제 자원 사용량
큰 타일의 이점과 비용장치별 최적 타일과 실행 설정

입력은 이미 장치에 있다고 가정한다. CPU↔GPU 전송, 다중 장치 통신, 전체 GPU 커널 구현은 이번 범위에서 제외한다.

1.4 이번 주의 성공 기준 ​

  1. HBM 읽기와 쓰기를 포함해 산술 강도를 계산할 수 있는가?
  2. Roofline의 기울어진 선·수평선·분기점을 단위와 함께 설명할 수 있는가?
  3. A와 B의 원소 하나가 어떤 출력 계산에 쓰이는지 가리킬 수 있는가?
  4. 타일링 전후에 같은 연산량과 달라진 이동량을 셀 수 있는가?
  5. 큰 타일이 요구하는 Shared Memory·레지스터 공간을 계산할 수 있는가?
  6. 출력 타일의 크기와 스레드 수가 다른 개념임을 설명할 수 있는가?

2. 공통 예제의 수학과 텐서 ​

2.1 기호와 shape ​

공통 예제는 두 정사각 행렬의 곱이다.

Cij=∑k=0N−1AikBkj
기호shapedtype역할
A[N, N]FP32왼쪽 입력
B[N, N]FP32오른쪽 입력
C[N, N]FP32별도 공간에 저장하는 출력
acc[T, T]FP32선택한 출력 타일의 중간 합
N스칼라정수행렬 한 변의 길이
T스칼라정수출력 타일과 K 조각의 한 변 길이

예를 들어 C[0, 1]은 A의 0번째 행과 B의 1번째 열을 곱해 더한 값이다. C의 서로 다른 위치는 독립적으로 계산할 수 있지만, 한 출력의 중간 합은 K 방향 계산이 끝날 때까지 유지해야 한다.

2.2 계산에 사용할 기호와 단위 ​

기호·용어의미단위
F총 부동소수점 연산 횟수FLOPs
QHBM에서 읽고 쓴 총 데이터 이동량Byte
I=F/QHBM 기준 산술 강도, Arithmetic IntensityFLOPs/Byte
P(이론상) 해당 자료형·연산에 적용 가능한 최대 연산 처리량FLOPs/s
BWHBM 대역폭. 행렬 B와 구분한다Byte/s
R(실제) 초당 연산 처리율FLOPs/s
N정사각 행렬의 한 변 길이원소 수
T정사각 출력 타일 및 K 방향 입력 조각의 한 변 길이원소 수
FMA곱셈과 누산을 함께 수행하는 연산 (Fused Multiply-Add)2 FLOPs로 계산
OccupancySM당 상주 활성 워프 수 / SM당 최대 상주 워프 수비율

FLOPs는 작업량, FLOPs/s는 처리 속도다. 이 정리본은 1 GB=109 Byte, 1 TB=1012 Byte, 1 TFLOPs/s=1012 FLOPs/s를 사용한다. 저장 공간 표에서는 1 KiB=1024 Byte, 1 MiB=10242 Byte를 사용한다.

2.3 공통 가상 하드웨어와 재사용 조건 ​

C=A@B,A,B,C∈R4096×4096
  • A·B·C 모두 FP32이며 원소 하나는 4 Byte다. 중간 합도 FP32로 가정한다.
  • P=20×1012 FLOPs/s, BW=1×1012 Byte/s다. 다른 자료형이나 Tensor Core의 별도 처리량을 가져오지 않는다.
  • 입력은 이미 장치에 있다. CPU↔GPU 전송과 메모리 할당 비용은 제외한다.
  • 기존 C는 읽지 않는다. 중간 합은 온칩에 유지하고 최종 C만 한 번 쓴다.
  • 재사용 없음에서는 매 곱셈마다 A·B 원소를 HBM에서 다시 읽는다.
  • 타일링에서는 출력 타일 안에서만 A·B를 재사용한다. 서로 다른 출력 타일 사이의 캐시 재사용은 없다.
  • T=32,64,128이고 K 방향 조각의 폭도 T다. 모두 4096의 약수이므로 경계 패딩은 없다.
  • 이상적 시간은 연산과 HBM 이동이 충분히 겹친다고 가정한 하한이다. 동기화·주소 계산·명령 발행·온칩 메모리 병목 등은 시간 모델에 넣지 않는다.

HBM 이동량과 텐서 용량은 다르다. 행렬 하나의 크기는 40962×4=67,108,864 Byte = 64 MiB다. A·B·C의 총 용량은 192 MiB지만, 같은 입력을 여러 번 읽으면 누적 이동량은 이보다 훨씬 커진다.


3. Arithmetic Intensity — 데이터 1 Byte로 얼마나 계산할까 ​

3.1 정의와 메모리 구간의 선택 ​

산술 강도는 I=F/Q다. 여기서는 Q에 HBM 읽기와 쓰기를 모두 포함한다. FLOPs가 같아도 HBM 재읽기를 줄이면 Q가 작아져 I가 커진다. 산술 강도를 논할 때는 어느 메모리 경계를 세었는지 먼저 정해야 한다. 정의와 시간 모델은 Scaling Book — Where Does the Time Go?를 참고했다.

예를 들어 HBM에서 가져온 값을 shared memory에 보관한 뒤 32번 사용하면, 이 모델에서는 HBM 읽기는 한 번이다. 온칩에서 발생하는 읽기까지 같은 Q에 다시 더하지 않는다. 또한 소스 코드에 전역 메모리 load가 여러 번 있다고 해서 실제 HBM 접근도 반드시 같은 횟수는 아니다. 캐시가 개입할 수 있기 때문이다. 과제의 ‘재사용 없음’은 캐시 효과까지 없다고 정한 비교 기준이다.

3.2 Vector Add와 행렬곱의 차이 ​

FP32 Vector Add는 C[i]=A[i]+B[i]다. 길이가 L이면 다음과 같다.

Fvec=L,Qvec=4L+4L+4L=12LIvec=112≈0.08333FLOPs/Byte

각 입력 원소는 자기 위치의 출력 하나에만 사용된다. 이 연산 자체를 작은 구간으로 나눈다고 입력 재사용이 새로 생기지는 않는다.

반면 행렬곱은 다음과 같다.

Cij=∑k=0N−1AikBkj

A의 원소 Aik는 j를 바꾸며 C의 i번째 행에 있는 N개 출력에 사용된다. B의 원소 Bkj는 i를 바꾸며 C의 j번째 열에 있는 N개 출력에 사용된다. 즉, 행렬곱은 입력을 여러 출력 계산에 재사용할 기회가 있다. 실제 HBM 이동량은 이 기회를 구현에서 얼마나 활용하는지에 달려 있다.

비교 항목FP32 Vector AddFP32 행렬곱
입력 원소가 기여하는 출력원소당 출력 하나A는 출력 한 행, B는 출력 한 열
같은 연산 안의 입력 재사용기본적으로 없음가능함
연산량L2N3−N2
타일링의 효과단순 분할만으로 Q가 줄지 않음여러 출력이 입력 조각을 공유해 Q 감소

여기서 ‘행렬곱의 산술 강도’는 행렬 크기만으로 정해지는 하나의 숫자가 아니다. 같은 행렬곱도 재사용 범위와 캐시 가정에 따라 I가 달라진다.


4. 두 시간에서 Roofline 성능 상한으로 ​

4.1 성능 상한의 유도 ​

연산에 필요한 시간과 데이터 이동에 필요한 시간은 각각 다음과 같다.

tcompute=FP,tmemory=QBW

이상적으로 두 작업이 겹치면 필요한 시간의 하한은 더 큰 시간이다. 이를 처리율로 바꾸면 다음을 얻는다.

tideal=max(FP,QBW)Rroof=Ftideal=min(P,FQBW)=min(P,BW×I)

따라서 I가 작은 구간에서는 데이터 공급량이 처리율을 제한하고, I가 충분히 큰 구간에서는 P가 상한이 된다. 두 식이 같아지는 분기점은 다음과 같다.

I∗=PBW=20×10121×1012=20FLOPs/Byte

이 해석은 Scaling Book — Visualizing rooflines의 그래프와 연결된다. 위의 20이라는 수치는 해당 문서의 제품 수치가 아니라 과제의 가상 조건을 대입한 결과다.


5. Roofline 그래프 — 축과 기울기와 분기점 읽기 ​

5.1 기울어진 선과 수평선의 의미 ​

메모리는 초당 BW Byte를 제공한다. 데이터 1 Byte당 I FLOPs를 수행한다면 이 공급으로 가능한 연산은 초당 BW×I FLOPs다. I가 두 배가 되면 메모리 측 성능 상한도 두 배가 된다. 반면 P는 같은 하드웨어·연산 조건에서 일정하므로 수평선이다.

일반 선형 좌표에서 R=BW×I의 기울기는 BW다. 아래와 같은 로그–로그 그래프에서는 이 선의 기울기가 1이고 BW는 선의 높이를 결정한다. 로그를 취하면 log⁡R=log⁡BW+log⁡I이기 때문이다.

image

*그림 1. 직접 계산한 Roofline. 가로축은 FLOPs/Byte, 세로축은 TFLOPs/s이며 두 축은 로그 축이다. 주황색 점은 각 구현 가정의 이론적 상한이고 측정값이 아니다. 32·64 타일은 경사 구간, 128 타일은 수평 구간에 놓인다.

Nsight Compute — Roofline Charts / Overview는 축, 메모리 경계, 연산 경계, ridge point, 실제 달성 성능을 구분한다. 실제 측정점을 그린다면 세로 좌표는 F/tmeasured로 얻는다. 이 정리본은 측정 시간이 없으므로 실제 성능점을 표시하지 않았다.

5.2 그래프를 읽을 때 주의할 점 ​

  • 세로축은 시간이 아니다. 처리율 상한이 높을수록 같은 F를 더 짧은 시간에 처리할 가능성이 있다.
  • 선은 도달을 보장하지 않는다. 온칩 자원 부족이나 동기화 비용 때문에 실제 점은 선보다 낮을 수 있다.
  • 오른쪽 이동이 언제나 속도 향상은 아니다. 이미 P에 도달한 모델에서는 Q를 더 줄여도 같은 연산량의 시간 하한 F/P는 변하지 않는다.
  • 모델상의 병목과 실제 병목을 구분한다. 아래 표의 compute-bound 판정은 HBM 대역폭과 연산 상한만 비교한 결과다. 실제 GPU에서는 다른 제약이 앞설 수 있다.

6. Tiling — 작은 조각으로 입력을 다시 쓰기 ​

6.1 Local Blocking과 Shared Memory Blocking ​

MLC의 Local Blocking은 스레드가 작은 출력 묶음을 계산하며 입력을 재사용하는 모습을 설명한다. Shared Memory Blocking은 같은 스레드 블록의 이웃 스레드들이 필요한 입력을 공유 메모리에 함께 가져와 쓰는 방법으로 이를 확장한다.

방식재사용 범위역할
Local Blocking한 스레드의 작은 출력 묶음피연산자와 중간 합을 가까이 유지
Shared Memory Blocking같은 블록의 여러 스레드공통 입력 조각을 함께 적재하고 공유

둘은 함께 적용할 수 있다. 이 정리본의 자원 계산은 설명을 위해 입력 A·B 타일을 shared memory에, 출력 중간 합을 스레드들에 분산한 레지스터에 둔다고 가정한다. 이는 논리적 저장 배치이며 모든 컴파일러·GPU 구현이 반드시 이대로 배치한다는 뜻은 아니다.

전체적인 흐름은 다음과 같다.

HBM / Global Memory
        ↓
큰 A, B 전체
        ↓
Shared-memory tiling
        ↓
Block이 필요한 A_tile, B_tile
        ↓
Register tiling / Local blocking
        ↓
Thread 또는 warp가 계산할 더 작은 fragment
        ↓
Tensor Core / CUDA Core 연산

Shared-memory blocking은 한 GPU block이 계산할 C tile을 위해 필요한 A/B의 일부를 어떤 크기의 tile로 잘라 shared memory에 올릴지 정하는 단계이다.

Local blocking은 shared memory에 올라온 A/B tile 중에서 각 thread 또는 warp가 어떤 작은 부분을 register로 가져가고, C의 어떤 작은 output tile을 계산할지 정하는 단계이다.

local blocking은 단순히 “thread를 어떻게 나눌까”만 의미하지는 않는다.

각 thread가 register에 몇 개의 accumulator를 들고, 몇 개의 C 원소를 계산할지 정하는 것

예를 들어 block이 C의 64×64를 계산한다고 해보겠습니다.

Block output tile:
C_block = 64 × 64

이걸 계산하려고 k 방향으로 8개씩 처리하면 shared memory에는:

A_shared = 64 × 8
B_shared = 8 × 64

를 가져옵니다. 이게 shared-memory blocking입니다. 그 다음 64개의 thread가 있다고 하면 각 thread가 예를 들어 C의 8×8씩 계산할 수 있습니다.

Thread 0 → C_local 8×8
Thread 1 → C_local 8×8
...

각 thread의 C_local 8×8은 보통 register accumulator가 됩니다.

register:
c00 c01 ... c07
c10 c11 ... c17
...
c70 ...     c77

이게 local blocking입니다. 그래서 전체 관계를 아주 짧게 정리하면:

Global blocking
→ 어떤 Block이 C의 어느 큰 영역을 계산할까?

Shared-memory blocking
→ 그 C 영역을 계산하기 위해 A/B의 어떤 tile을 shared memory에 올릴까?

Local blocking
→ 그 shared tile을 이용해서 각 thread/warp가 C의 어느 작은 tile을 register에서 계산할까?

6.2 4×4 행렬곱을 2×2 출력 타일로 보기 ​

이 예제의 핵심은 입력을 계산에 사용하는 횟수와 HBM에서 가져오는 횟수가 다르다는 점이다. HBM에서 한 번 가져온 값을 온칩에 보관하고 두 번 사용하면, 계산에는 두 번 참여하지만 HBM 읽기는 한 번으로 센다. 온칩에서 다시 읽는 비용은 이 HBM 이동량 표에 포함하지 않는다.

조건을 먼저 구분하자.

항목이 예제의 조건
전체 행렬A·B·C 모두 4×4
출력 타일한 번에 C의 2×2, 즉 출력 4개를 계산
전체 K 길이출력 하나마다 k=0,1,2,3의 항을 모두 누산
K 조각 폭한 단계에서 k 두 개씩 처리하므로 총 2단계
데이터 크기FP32 원소 하나당 4 Byte
재사용 범위같은 출력 타일 안에서만 재사용하며, 다른 출력 타일에서는 입력을 다시 읽음
출력 저장중간 합은 온칩에 유지하고, 두 K 단계가 끝난 뒤 최종 C만 한 번 씀

0부터 시작하는 인덱스를 사용하며, 슬라이스 0:2는 인덱스 0과 1을 뜻한다. 먼저 왼쪽 위 출력 타일 C[0:2,0:2]을 계산한다. 중간 합 4개를 0으로 초기화하고 다음 두 K 단계를 수행한다.

C[0:2,0:2]=A[0:2,0:2]B[0:2,0:2]+A[0:2,2:4]B[2:4,0:2]image

*그림 2. 위 행은 k=0,1, 아래 행은 k=2,3에 해당한다. 파란 A 조각과 초록 B 조각은 단계마다 바뀌고 주황 C 중간 합 4개는 유지된다. 마지막 단계가 끝난 뒤에만 C를 쓴다.

6.2.1 첫 K 단계 — 입력 8개를 읽어 중간 합 4개 계산 ​

첫 단계에서는 k=0,1에 해당하는 다음 조각을 HBM에서 가져온다.

Atile=[A00A01A10A11],Btile=[B00B01B10B11]

A 4개와 B 4개이므로 HBM 입력 읽기는 8개다. 이 값들을 온칩에 보관하고 다음 네 중간 합을 계산한다. 아직 k=2,3의 항을 더하지 않았으므로 완성된 C가 아니라 acc로 표시한다.

acc00=A00B00+A01B10acc01=A00B01+A01B11acc10=A10B00+A11B10acc11=A10B01+A11B11

첫째·둘째 식에 A00이 등장한다. 첫째·셋째 식에는 B00이 등장한다. 따라서 A00은 C00과 C01에, B00은 C00과 C10에 기여한다. 각 입력 원소를 한 번 가져와 두 번 사용하는 것이다. 다만 A00 하나가 출력 4개 모두에 쓰이는 것은 아니다. A 원소는 출력 타일의 한 행, B 원소는 한 열에 기여한다.

이 단계의 비용은 다음과 같다.

  • FLOPs: 출력 4개 × 출력당 FMA 2회 × FMA당 2 FLOPs = 16 FLOPs.
  • HBM 읽기: 입력 8개 × 4 Byte = 32 Byte.
  • HBM 쓰기: 아직 출력이 완성되지 않았으므로 0 Byte. 중간 합은 온칩에 유지한다.

FMA는 acc += a * b인 곱셈·누산 연산이며, 곱셈과 덧셈을 합쳐 2 FLOPs로 센다. 여기서는 0으로 초기화한 중간 합에 누산하는 기준을 사용한다. 위 수식에서 첫 곱셈 결과에 0을 더하는 과정을 생략해 썼더라도, FLOPs는 출력당 FMA 2회로 일관되게 계산한다.

6.2.2 두 K 단계를 끝내면 출력 타일 하나 완성 ​

두 번째 단계에서는 k=2,3에 해당하는 A[0:2,2:4]와 B[2:4,0:2]를 가져온다. 이번에도 A 4개와 B 4개를 읽고, 기존 중간 합에 16 FLOPs만큼의 계산 결과를 더한다. 예를 들어 왼쪽 위 출력은 다음처럼 완성된다.

A00B00+A01B10⏟첫 단계의 중간 합+A02B20+A03B30⏟둘째 단계에서 더하는 값=C00

다른 출력 세 개도 같은 방식으로 완성한다. 두 K 단계는 서로 다른 출력 타일을 만드는 것이 아니라, 같은 출력 4개를 끝까지 누산하는 과정이다. 두 번째 단계가 끝난 뒤에만 최종 출력 4개를 HBM에 쓴다.

출력 타일 하나의 전체 비용계산결과
입력 읽기단계당 8개 × 2단계16개 = 64 Byte
연산량단계당 16 FLOPs × 2단계32 FLOPs
최종 출력 쓰기출력 4개 × 4 Byte16 Byte
총 HBM 이동량입력 읽기 64 Byte + 출력 쓰기 16 Byte80 Byte

6.2.3 전체 출력에는 2×2 타일이 4개 ​

전체 C는 4×4이므로, 행 방향으로 2개, 열 방향으로 2개의 출력 타일이 있다.

C의 범위열 0,1열 2,3
행 0,1왼쪽 위 2×2 타일오른쪽 위 2×2 타일
행 2,3왼쪽 아래 2×2 타일오른쪽 아래 2×2 타일

각 출력 타일이 위의 두 K 단계를 수행하므로, 출력 타일 하나의 전체 비용에 4를 곱한다.

  • 입력 읽기: 16개 × 4타일 = 64개, 즉 256 Byte.
  • 최종 출력 쓰기: 4개 × 4타일 = 16개, 즉 64 Byte.
  • 연산량: 32 FLOPs × 4타일 = 128 FLOPs.
  • 총 HBM 이동량: 80 Byte × 4타일 = 320 Byte.

여기서 입력 64개는 서로 다른 원소 64개가 아니라, 중복을 포함한 원소 읽기 횟수다. 실제 입력 원소는 A 16개와 B 16개, 총 32개다. 예를 들어 A00은 왼쪽 위 출력 타일을 위해 한 번 읽고, 오른쪽 위 출력 타일을 위해 다시 읽는다. 각 타일 안에서는 두 번씩 재사용하지만, 타일 사이에서는 캐시 재사용을 하지 않는다는 가정이다.

따라서 A00은 전체 계산에서 출력 4개에 사용되지만 HBM에서는 2번 읽힌다. 재사용이 없는 모델이라면 같은 원소를 4번 읽는다. 다른 입력 원소도 같으므로 입력 읽기 횟수가 절반이 된다.

6.2.4 재사용이 없을 때는 왜 입력을 128개 읽을까? ​

재사용이 없으면 매 곱셈마다 A 원소 하나와 B 원소 하나를 HBM에서 다시 가져온다. 출력 하나에는 곱셈이 4회 필요하므로 입력을 8개 읽고, 이런 출력이 16개 있다.

입력 읽기 횟수=16⏟출력 개수×4⏟출력당 곱셈 수×2⏟곱셈당 A*B 읽기=128

입력 읽기는 128×4=512 Byte다. 재사용이 없어도 중간 합은 온칩에 유지하고 최종 출력만 한 번 쓰므로, 출력 쓰기는 16×4=64 Byte로 같다. 따라서 총 이동량은 512 + 64 = 576 Byte다. 연산량 역시 16×4×2=128 FLOPs로 타일링과 같다.

6.2.5 범위별 비용과 산술 강도 비교 ​

아래 표의 입력 읽기·출력 쓰기는 원소 단위의 접근 횟수다. 각 행이 한 단계인지, 출력 타일 하나의 전체 계산인지, 전체 행렬 계산인지 구분해서 읽어야 한다.

범위A·B 입력 읽기최종 C 쓰기FLOPs총 HBM Byte
출력 타일 하나의 K 단계 하나8개아직 쓰지 않음2×2×2×2=1632
출력 타일 하나의 전체 K 단계16개4개3280
전체 행렬, 2×2 타일 4개64개16개128320
전체 행렬, 재사용 없음128개16개128576

전체 행렬끼리 비교하면 다음과 같다.

방식입력 읽기최종 C 쓰기총 HBM 이동량 Q연산량 F
재사용 없음512 Byte64 Byte576 Byte128 FLOPs
2×2 타일링256 Byte64 Byte320 Byte128 FLOPs

타일링은 같은 계산을 하면서 입력을 덜 가져오는 방법이다. 입력 읽기는 정확히 절반이 되지만, 출력 쓰기 64 Byte는 줄지 않는다. 따라서 총 이동량은 576→320 Byte로 줄며, 기존의 320/576≈55.6%가 된다. 총 이동량은 약 44.4% 감소한 것이고, 두 이동량의 비는 576/320=1.8이다.

산술 강도는 연산량을 총 HBM 이동량으로 나눈다.

Inaive=128576≈0.2222FLOPs/Byte,Itiled=128320=0.4FLOPs/Byte

연산량은 그대로이고 이동량이 줄었으므로, HBM에서 옮기는 1 Byte당 수행하는 연산량이 1.8배로 증가한다. 이것이 입력 재사용으로 산술 강도가 높아진다는 뜻이다. 이 비율이 곧 실제 실행시간의 개선 비율을 보장하는 것은 아니며, 실행시간은 연산 처리율과 다른 구현 비용에도 영향을 받는다.

6.3 GPU에서의 논리적 수행 순서 ​

text
출력 타일마다:
    중간 합 acc[T, T] = 0                 # K 루프 전체에 걸쳐 유지
    K 방향으로 T개씩 이동:
        A 조각[T, T]을 온칩 입력 버퍼에 적재
        B 조각[T, T]을 온칩 입력 버퍼에 적재
        [공유 메모리 적재 완료 및 가시성 동기화]
        acc += A 조각 @ B 조각             # 입력을 타일 내부에서 재사용
        [모든 소비 완료 후 입력 버퍼 재사용]
    최종 acc를 C에 한 번 저장

대괄호 부분은 협력적으로 입력을 적재하는 GPU 구현에서 고려할 동기화다. 단순히 파이썬 슬라이싱을 썼다고 shared memory 적재가 구현되는 것은 아니다. Triton의 Matrix Multiplication — Motivations에서도 출력 블록별 중간 합을 유지하고 K 방향 조각을 누적한 뒤 저장하는 알고리즘을 볼 수 있다. 원문 실습은 FP16 행렬곱이지만, 여기서는 블록 알고리즘의 구조만 참고하고 FP32 조건으로 별도 계산한다.


7. 계산 예제 1 — 입력 재사용이 없는 행렬곱 ​

7.1 연산량은 모든 경우에 같다 ​

출력은 N2개이고 각 출력에 대해 N번 곱셈·누산한다. FMA 하나를 2 FLOPs로 세는 관례를 사용한다.

F=2N3=2×40963=137,438,953,472FLOPs

수학적으로 내적의 덧셈을 N−1번만 세면 N2(2N−1)이다. 이 정리본은 0에서 시작해 N번 누산하는 코드 및 일반적인 행렬곱 처리량 표기와 맞추어 2N3을 모든 경우에 일관되게 사용한다. 초기화·인덱스 연산은 행렬곱 FLOPs에 포함하지 않는다.

tcompute=137,438,953,47220×1012=6.8719476736ms

7.2 재사용 없음 ​

N³번의 곱셈마다 A·B 원소를 읽고, 최종 C를 한 번 쓴다.

Qno reuse=8N3⏟A*B 읽기+4N2⏟C 쓰기Qno reuse=549,755,813,888+67,108,864=549,822,922,752ByteIno reuse=2N38N3+4N2=14+2/N≈0.249969FLOPs/Byte

입력 텐서 자체가 약 550 GB라는 뜻이 아니다. 같은 원소를 반복해서 읽은 누적량이다. 또한 재사용이 없다는 조건은 입력에 대한 것이다. 과제 조건에 따라 중간 합은 여전히 온칩에 유지한다.

7.3 두 시간과 병목을 구해보자 ​

재사용이 없을 때 HBM 시간은 총 이동량을 HBM 대역폭으로 나누어 구한다.

tmemory=549,822,922,7521012=549.822922752ms
항목결과
연산 시간6.871948 ms
HBM 시간549.822923 ms
이상적 실행시간549.822923 ms
Roofline 성능 상한0.249969 TFLOPs/s
모델상의 병목HBM 대역폭

7.4 이 결과를 어떻게 설명하면 좋을까? ​

4096×4096 FP32 행렬곱은 약 137.439 GFLOPs를 수행한다. 입력 재사용이 없다면 약 549.823 GB를 읽고 쓰므로, 주어진 하드웨어에서는 연산보다 데이터 이동에 훨씬 오래 걸린다. 같은 입력을 다시 읽는 횟수를 줄이는 것이 다음 개선 방향이다.


8. 계산 예제 2 — 32·64·128 타일 비교 ​

8.1 출력 타일 안에서 재사용 ​

출력 타일 수는 (N/T)2, 각 출력 타일의 K 단계 수는 N/T다. K 단계마다 A·B 타일 원소를 2T2개 읽으므로 8T² Byte가 이동한다.

Qread,tiled=(NT)2NT(8T2)=8N3TQT=8N3T+4N2,IT=2N38N3/T+4N2=NT4N+2T

N이 T보다 충분히 크면 IT≈T/4다. 타일 크기를 두 배로 하면 입력 이동량은 절반이 되고 I는 거의 두 배가 된다. 단, 출력 쓰기는 줄어들지 않으므로 정확히 두 배는 아니다.

다른 방식으로도 확인할 수 있다. A의 원소는 서로 다른 출력 열 타일 N/T개에서 필요하고, B의 원소는 서로 다른 출력 행 타일 N/T개에서 필요하다. 타일 사이 캐시 재사용이 없으므로 A와 B 각각을 전체적으로 N/T회 읽는 셈이다.

T출력 타일 수 (N/T)2타일당 K 단계 N/T적재한 입력 원소의 타일 내부 사용 횟수
3216,38412832회
644,0966464회
1281,02432128회

8.2 연산량·이동량·산술 강도 비교표 ​

각 행의 F는 동일하게 137,438,953,472 FLOPs다. GB는 십진 단위이며 Byte 원값도 함께 표시한다.

방식A·B 읽기(Byte)C 쓰기(Byte)총 Q(Byte)총 Q(GB)I(FLOPs/Byte)
재사용 없음549,755,813,88867,108,864549,822,922,752549.8229230.249969
T=3217,179,869,18467,108,86417,246,978,04817.2469787.968872
T=648,589,934,59267,108,8648,657,043,4568.65704315.875969
T=1284,294,967,29667,108,8644,362,076,1604.36207631.507692

8.3 Roofline 상한과 이상적 실행시간 비교표 ​

Rroof=min(P,BW×I),tideal=max(tcompute,tmemory)
방식F(GFLOPs)연산 시간(ms)HBM 시간(ms)성능 상한(TFLOPs/s)이상적 시간(ms)모델상의 제한기준 대비 이상적 속도 향상
재사용 없음137.4389536.871948549.8229230.249969549.822923HBM 대역폭1.000배
T=32137.4389536.87194817.2469787.96887217.246978HBM 대역폭31.879배
T=64137.4389536.8719488.65704315.8759698.657043HBM 대역폭63.512배
T=128137.4389536.8719484.36207620.0000006.871948연산 처리량80.010배

예를 들어 T=64일 때 BW×I=15.875969 TFLOPs/s이므로 P보다 작다. T=128에서는 메모리 측 상한이 31.507692 TFLOPs/s로 높아져도 연산 상한 20 TFLOPs/s에 막힌다.

따라서 T=64→128은 입력 읽기를 절반으로 줄이지만 이상적 시간은 8.657043→6.871948 ms, 약 1.260배만 개선된다. ‘128번 재사용하니 128배 빨라진다’는 결론은 출력 쓰기와 연산 시간 하한을 모두 놓친 것이다.

8.4 연산 제한 영역에 들어가는 타일 크기 ​

정확한 I 식에 IT≥20을 대입한다.

NT4N+2T≥20⇒T(N−40)≥80NT≥80NN−40=327,6804,056≈80.789

이것은 연속값으로 본 모델상의 경계다. 출력 쓰기를 무시한 I≈T/4에서는 T≈80이지만, 정확한 계산은 조금 더 크다. 실제 후보는 행렬 크기에 대한 분할 조건과 자원 제약도 만족해야 한다. 과제의 32·64·128 중 경계를 넘는 후보는 128이다.

8.5 ‘입력을 딱 한 번만 읽는다’는 별도 가정과의 구분 ​

참고로 A·B를 각각 전체 연산에서 한 번만 읽고 C를 한 번 쓸 수 있다면 다음과 같다.

Qone pass=12N2=201,326,592Byte,Ione pass=N6≈682.667

이는 이상적인 일회 읽기의 비교값이다. 이번 타일링 조건에서는 다른 출력 타일이 같은 입력을 다시 가져오므로 이 값을 쓸 수 없다. 유한한 온칩 공간으로 이 일회 읽기를 달성할 수 있는지도 별도의 문제다. 이 비교는 ‘행렬 크기 합계’와 ‘커널 실행 중의 누적 HBM 이동량’을 구분하기 위한 것이다.


9. 타일 크기와 온칩 공간 — 크게 만들수록 좋을까 ​

9.1 입력 버퍼와 중간 합의 크기 ​

정사각 타일 하나에서 동시에 필요한 논리적 저장 공간은 다음과 같다.

SA=4T2,SB=4T2,Sacc=4T2

입력을 shared memory에 놓는 단일 버퍼 구성은 Sinput=8T2 Byte다. 중간 합은 4T2 Byte이며 일반적인 설명에서는 레지스터들에 분산해 둔다.

방식A 입력 조각B 입력 조각입력 버퍼 합계중간 합논리적 합계
재사용 없음: 출력 하나 기준 최소 상태4 B4 B8 B4 B12 B
T=324 KiB4 KiB8 KiB4 KiB12 KiB
T=6416 KiB16 KiB32 KiB16 KiB48 KiB
T=12864 KiB64 KiB128 KiB64 KiB192 KiB

재사용 없음 행은 전체 GPU 사용량이나 블록 사용량이 아니라, 출력 하나를 순차 누산하는 최소 논리 상태다. 실제로 여러 출력이 병렬 실행되면 그만큼 상태가 늘어난다. 또한 마지막 열은 이해를 위한 합계다. shared memory와 레지스터는 별도 자원이므로 합계 하나만으로 실행 가능성을 판정할 수 없다.

다음 입력을 미리 가져오는 이중 버퍼링에서는 입력 공간이 16T2 Byte가 된다. T=32·64·128의 입력 버퍼는 각각 16·64·256 KiB이며, 중간 합은 여전히 4·16·64 KiB다. 이는 추가 배치 가정에 대한 공간 계산이다. 단일 버퍼만으로 완전한 연산·이동 중첩이 자동으로 확보되는 것은 아니다.

9.2 Occupancy와 동시에 실행할 수 있는 작업 ​

9.2.1 Occupancy는 무엇의 비율일까? ​

GPU에는 여러 SM(Streaming Multiprocessor)이 있다. 각 SM은 블록을 배정받고, 그 블록에 속한 스레드를 워프 단위로 실행한다. NVIDIA CUDA에서 워프 하나는 32개 스레드로 구성된다. 예를 들어 256개 스레드를 가진 블록에는 8개 워프가 있다.

상주(resident)한다는 것은 SM에 작업의 실행 상태와 필요한 자원이 배정되어 있다는 뜻이다. 상주 워프 모두가 매 순간 명령을 실행하는 것은 아니다. 어떤 워프는 데이터를 기다리고, 어떤 워프는 앞선 연산 결과나 동기화를 기다린다. 스케줄러는 이들 중 다음 명령을 실행할 준비가 된 워프를 선택한다. CUDA Programming Guide — Advanced Kernel Programming

Occupancy는 이 상주 워프 수가 하드웨어 한도의 몇 퍼센트인지를 나타낸다.

Occupancy=SM에 상주하는 활성 워프 수SM이 지원하는 최대 상주 워프 수

여기서 ‘활성’은 아직 실행이 끝나지 않고 SM에 상주하는 상태를 뜻하며, 지금 당장 연산 중이라는 뜻은 아니다. 예를 들어 최대 64개 워프를 수용하는 SM에 32개 워프가 상주하면 occupancy는 32/64=50%다. 이 값이 연산 성능 P의 50%를 낸다거나, GPU가 시간의 절반만 일한다는 뜻은 아니다.

자원과 실행 설정으로 계산한 이론적 occupancy는 가능한 상주량의 상한이다. 실제 실행 중에는 배정할 블록이 부족하거나 일부 블록이 먼저 끝나는 등의 이유로 평균 occupancy가 더 낮을 수 있다. CUDA Best Practices — Occupancy

9.2.2 상주 블록 수의 식은 왜 나올까? ​

한 출력 타일을 한 블록이 담당한다고 하자. 블록이 SM에 올라가려면 입력 버퍼를 둘 shared memory, 중간 합 등을 둘 레지스터, 스레드·워프 수용 여유가 모두 필요하다. 이 자원은 상주하는 블록들이 나눠 쓴다.

먼저 shared memory만 생각해 보자. SM 전체에 SSM Byte가 있고 블록 하나에 Sblock Byte가 필요하면, b개 블록이 들어가기 위한 조건은 다음과 같다.

bSblock≤SSM⇒b≤⌊SSMSblock⌋

⌊x⌋는 소수점 아래를 버리는 내림이다. 예를 들어 100 KiB 공간에 블록당 32 KiB가 필요하면 100/32=3.125이지만, 블록의 일부만 올릴 수 없으므로 최대 3개다.

레지스터와 스레드 수도 같은 방식으로 센다. 아래에서 레지스터 개수는 G로 표기한다. 앞 절에서 사용한 처리율 R과 혼동하지 않기 위해 기호를 구분했다.

기호의미단위
SSM,SblockSM의 shared memory 용량, 블록 하나의 사용량Byte
GSM,GblockSM의 레지스터 용량, 블록 하나의 할당량32비트 레지스터 개수
HSM,HblockSM의 최대 상주 스레드 수, 블록의 스레드 수스레드 개수
WSM,WblockSM의 최대 상주 워프 수, 블록의 워프 수워프 개수
BmaxSM이 지원하는 최대 상주 블록 수블록 개수

각 자원으로 계산한 한도를 전부 만족해야 하므로 가장 작은 값을 선택한다. 이 때문에 식에 min이 들어간다.

상주 블록 수≤min(⌊SSMSblock⌋,⌊GSMGblock⌋,⌊HSMHblock⌋,⌊WSMWblock⌋,Bmax)

예를 들어 shared memory로는 3개, 레지스터로는 4개, 스레드·워프 수로는 8개, 블록 수 한도로는 16개가 가능하면, 상주 블록은 최대 3개다. 레지스터에 여유가 있어도 shared memory가 부족하면 네 번째 블록을 올릴 수 없다.

이 식은 자원 관계를 이해하기 위한 상한이다. 실제로는 레지스터·shared memory 할당 단위, SM 내부 자원 배치, 블록당 자원 한도 등을 추가로 고려한다. shared memory를 사용하지 않는 블록이라면 해당 항은 제한에서 제외한다. CUDA Best Practices — Calculating Occupancy

9.2.3 숫자로 계산하기 — 블록 수에서 occupancy까지 ​

다음은 식 설명용으로 별도로 만든 가상 조건이며, 앞의 P=20 TFLOPs/s·BW=1 TB/s에서 유도한 하드웨어 사양이 아니다. 블록당 자원 한도는 만족하고 세부 할당 제약은 없다고 가정한다.

자원SM 하나의 한도블록 하나의 사용량해당 자원으로 가능한 블록 수
Shared memory96 KiB32 KiB3
32비트 레지스터65,536개16,384개4
스레드2,048개256개8
워프64개8개8
상주 블록16개1개16

이 단순 모델에서는 다음처럼 계산된다.

b=min(3,4,8,8,16)=3 블록상주 워프 수=3×8=24,Occupancy=2464=37.5%

블록 수를 그대로 occupancy라고 부르면 안 된다. 블록마다 포함하는 워프 수가 다를 수 있으므로, 먼저 상주 블록 수에 블록당 워프 수를 곱해야 한다.

이제 블록당 스레드 수와 레지스터 사용량은 그대로 두고, shared memory만 64 KiB로 늘린다고 하자. shared memory 기준으로 ⌊96/64⌋=1개 블록만 상주할 수 있다. 따라서 상주 워프는 8개, occupancy는 8/64=12.5%가 된다. 남은 32 KiB에는 두 번째 블록이 들어가지 못한다. 이처럼 자원 사용량 증가에 따라 상주 블록 수는 정수 단위로 떨어진다.

9.2.4 Occupancy가 높으면 어떤 점이 유리할까? ​

워프 A가 메모리에 데이터를 요청한 뒤 기다리는 동안, 준비된 워프 B의 명령을 실행할 수 있다. B도 기다리면 준비된 C를 실행할 수 있다. 이처럼 다른 워프의 일을 진행해 대기 시간 동안 연산 장치가 쉬는 시간을 줄이는 것을 **지연 숨기기(latency hiding)**라고 한다.

상주 워프가 많으면 그중 실행 준비가 된 워프를 찾을 기회가 늘어난다. 따라서 대기 때문에 연산 장치가 자주 쉬는 커널에서는 occupancy 증가가 성능에 도움이 될 수 있다. 다만 메모리 요청 하나의 응답 시간이 짧아지는 것은 아니다. 그 응답을 기다리는 동안 다른 일을 하는 것이다.

9.2.5 왜 높은 occupancy가 항상 더 빠르지는 않을까? ​

Occupancy는 상주한 작업의 양을 세고, 성능은 단위 시간에 완료한 유효 연산량을 센다. 따라서 occupancy만으로 실행시간을 결정할 수 없다. 다음 상황을 구분해야 한다.

상황Occupancy를 높여도 빨라지지 않거나 느려질 수 있는 이유
이미 대기를 충분히 숨기고 있음현재 워프만으로 연산 장치에 일을 계속 공급한다면, 상주 워프를 더 늘려도 연산 처리량 한도 P는 커지지 않는다.
HBM 대역폭을 이미 충분히 사용 중워프가 늘어도 BW 자체는 증가하지 않는다. 같은 이동량을 처리하는 시간에는 대역폭 한계가 남는다.
많은 워프가 다음 명령을 기다림상주 워프에는 데이터·연산 결과·동기화를 기다리는 워프도 포함된다. 높은 occupancy가 많은 실행 준비 워프를 보장하지 않는다.
작은 타일로 바꿔 상주 블록을 늘림입력 재사용이 줄어 Q가 커질 수 있다. 더 많은 워프가 상주해도 처리해야 할 메모리 이동량 자체가 늘어난다.
스레드당 레지스터를 과도하게 줄임필요한 값을 레지스터에 담지 못하면 spill이 발생해 메모리 접근이 추가될 수 있다.

반대로 한 워프 안에 서로 독립적인 연산이 충분히 있으면, 여러 중간 합의 계산을 겹쳐 진행하면서 대기를 줄일 수 있다. 이를 명령어 수준 병렬성(ILP)이라고 한다. 이런 커널은 상대적으로 적은 상주 워프로도 높은 처리율을 낼 수 있다. 높은 occupancy의 효과가 포화될 수 있고, 레지스터 여유와 ILP 때문에 낮은 occupancy가 유리할 수도 있다는 점은 CUDA Best Practices — Occupancy에서도 설명한다.

따라서 다음 식처럼 occupancy를 단순한 성능 배율로 사용하면 안 된다.

Rmeasured≠P×Occupancy(일반적으로 성립하지 않음)

Occupancy 50%인 커널의 성능이 반드시 P의 50%인 것은 아니며, occupancy 100%라고 P에 도달하는 것도 아니다.

9.2.6 이번 타일링 예제와 연결하기 ​

T를 키우면 서로 반대 방향의 효과가 생긴다.

변화기대 효과 또는 비용
입력 원소의 타일 내부 사용 횟수가 T에 비례해 증가HBM 입력 읽기량 8N3/T가 줄어든다.
블록의 입력 버퍼가 8T2 Byte로 증가shared memory가 상주 블록 수를 제한할 수 있다.
출력 중간 합이 4T2 Byte로 증가레지스터 요구량이 커져 상주 블록 수가 줄거나 spill이 발생할 수 있다.

예를 들어 T=32에서 64로 바꾸면 입력 읽기는 절반으로 줄지만, 입력 버퍼와 중간 합은 네 배가 된다. 이때 occupancy가 낮아져도 남은 워프만으로 대기를 충분히 숨긴다면 이동량 감소 덕분에 더 빨라질 수 있다. 반대로 준비된 워프가 부족해 연산 장치가 자주 쉬게 되면 이득이 줄거나 더 느려질 수 있다. 어느 효과가 큰지는 실제 자원 사용량과 실행시간으로 확인해야 한다.

이번 가상 하드웨어에는 SM별 자원량이 주어지지 않았다. 따라서 occupancy의 숫자를 확정할 수 없다. 다만 T를 두 배로 하면 입력 버퍼와 중간 합은 네 배가 되므로, 상주 블록 수가 줄거나 해당 구성이 실행 불가능해질 수 있다는 방향은 알 수 있다.

레지스터가 부족해 중간 합이 메모리로 밀려나면 spill 접근이 추가된다. 이 경우에는 ‘중간 합이 온칩에 유지된다’는 과제 가정부터 다시 검토해야 한다. 따라서 128 타일의 6.871948 ms는 자원 사용량이 실현 가능한지와 별개로 계산한 하한이다.

9.3 출력 타일 크기와 스레드 수는 같지 않다 ​

64×64 출력 타일은 출력 4,096개를 의미한다. 이것이 반드시 스레드 4,096개라는 뜻은 아니다. 개념적으로 256개 스레드가 균등하게 나누면 스레드당 출력 16개의 중간 합을 맡을 수 있다.

동일하게 256개 스레드를 가정하면 다음과 같다.

출력 타일출력 원소 수스레드당 평균 중간 합 개수
32×321,0244
64×644,09616
128×12816,38464

이는 분배를 설명하는 산술 예시다. 실제 레지스터 개수에는 입력값·주소·루프 상태가 추가되며, 컴파일러가 배치하는 방식도 영향을 준다. Triton에서도 출력 블록의 M·N 크기와 실행 워프 설정은 서로 다른 선택 항목이다. 공식 Matrix Multiplication 튜토리얼

9.4 K 방향 조각만 키우면 재사용도 늘어날까? ​

일반적인 M×K와 K×N 행렬곱에서 출력 타일을 BM×BN, K 조각 폭을 BK로 두자. 한 K 단계의 연산량과 입력 이동량은 다음과 같다.

Fstep=2BMBNBK,Qread,step=4BK(BM+BN)

출력 쓰기를 제외한 단계별 입력 산술 강도는 다음과 같다.

Iinput=BMBN2(BM+BN)

BK는 약분된다. 즉, 출력 타일은 고정하고 K 조각만 넓히면 입력 버퍼와 단계당 계산량은 함께 늘지만, 이 모델의 전체 입력 읽기량은 줄지 않는다. 대신 단계 수와 동기화 빈도, 파이프라이닝 조건이 달라질 수 있다. 과제에서는 세 방향 크기를 함께 바꾸지만, 구현에서는 각각의 역할을 구분해야 한다.


10. 코드에서 읽기와 재사용과 쓰기 찾기 ​

10.1 계산식을 재현하는 핵심 코드 ​

아래 코드는 큰 행렬을 실제로 만들지 않고 손계산을 재현한다. tile=1은 이동량 식에서 재사용 없음과 같다.

python
n = 4096
peak = 20 * 10**12       # FLOPs/s: 과제의 FP32 연산 처리량
bw = 10**12              # Byte/s: HBM 대역폭
flops = 2 * n**3

for tile in (1, 32, 64, 128):
    read_bytes = 8 * n**3 // tile
    write_bytes = 4 * n**2
    total_bytes = read_bytes + write_bytes
    intensity = flops / total_bytes
    ceiling = min(peak, bw * intensity)
    compute_s = flops / peak
    memory_s = total_bytes / bw
    ideal_s = max(compute_s, memory_s)
    print(tile, total_bytes, intensity, ceiling / 1e12, ideal_s * 1e3)

10.2 입력을 한 번 가져와 여러 번 쓰는 코드 ​

전체 스크립트의 naive_counted는 매 곱셈에서 원본 A·B를 읽고 접근 횟수를 센다. tiled_counted는 원본에서 조각을 복사할 때만 HBM 읽기로 세고, 조각 내부의 반복 접근은 온칩 재사용으로 취급한다.

python
# 개념 발췌: i0, j0는 선택한 출력 타일의 시작 위치다.
acc = [[0 for _ in range(tile)] for _ in range(tile)]
for k0 in range(0, n, tile):
    at = [[a[i0+i][k0+k] for k in range(tile)] for i in range(tile)]
    bt = [[b[k0+k][j0+j] for j in range(tile)] for k in range(tile)]
    hbm_read_elements += 2 * tile**2

    for i in range(tile):
        for j in range(tile):
            for k in range(tile):
                acc[i][j] += at[i][k] * bt[k][j]
                # at, bt의 반복 읽기는 이 모델에서 HBM 읽기가 아니다.

# 전체 K 루프가 끝난 후에만 C에 저장한다.

Python 리스트가 실제 GPU shared memory인 것은 아니다. 이 코드는 데이터 수명과 접근 횟수를 드러내는 시뮬레이션이다. Python 실행시간으로 GPU 성능을 추정하지 않는다. 검증에는 작은 정수를 사용하므로 결과를 정확히 비교할 수 있으며, FP32 반올림 오차나 FMA의 수치적 차이를 검증하는 코드는 아니다. 이동량만 원소당 4 Byte로 계산한다.

10.3 검증 내용 ​

스크립트는 N=2·4·8에서 기본 행렬곱과 가능한 타일 크기의 행렬곱 결과가 같은지 확인한다. 동시에 루프가 실제로 센 읽기·쓰기·FLOPs가 7·8절의 수식과 일치하는지 검사한다. 4×4의 재사용 없음 576 Byte, 2×2 타일 320 Byte도 확인한다. 큰 행렬의 네 조건은 반복문으로 행렬곱을 실행하지 않고 식으로 계산한다.

이렇게 계산식 검증과 작은 실행 예제를 연결하면, ‘타일링이 FLOPs를 줄이는가?’와 ‘중간 합을 매 단계 HBM에 쓰고 있지는 않은가?’를 코드 수준에서 확인할 수 있다.


11. 자주 생기는 오해와 문제 찾기 ​

11.1 자주 생기는 오해 ​

오해더 정확한 설명
산술 강도는 FLOPs만 알면 구할 수 있다어느 메모리 구간에서 몇 Byte를 읽고 썼는지도 필요하다.
행렬곱의 I는 행렬 크기만으로 정해진다재사용 범위와 캐시 가정에 따라서도 달라진다.
Roofline의 세로축은 시간이다초당 연산 처리율이며 여기서는 TFLOPs/s다.
로그–로그 그래프의 경사선 기울기는 BW다로그 좌표에서 기울기는 1이고 BW는 선의 높이를 정한다.
그래프에 찍힌 상한이 실제 커널 성능이다이 문서의 점은 가정으로 계산한 상한이다.
Tiling은 곱셈 횟수를 줄인다이번 비교에서는 FLOPs는 같고 입력 재읽기가 줄어든다.
A 원소 하나는 출력 타일 전체에 쓰인다A는 타일의 한 행, B는 타일의 한 열에 기여한다.
64번 재사용하면 전체 시간이 정확히 64배 줄어든다출력 쓰기와 연산 시간 하한도 고려해야 한다.
128×128 타일에는 스레드 16,384개가 필요하다스레드 하나가 여러 출력의 중간 합을 맡을 수 있다.
중간 합까지 합친 공간은 모두 Shared Memory다입력 버퍼와 레지스터 중간 합은 별도 자원으로 계산한다.
K 조각만 키우면 전체 HBM 읽기가 줄어든다출력 타일이 고정이면 이 모델의 총 입력 읽기는 같다.
가장 큰 타일이 항상 가장 빠르다실행 가능성, 레지스터 사용량, 상주 작업 수를 함께 봐야 한다.

11.2 계산이 이상하면 어디부터 볼까? ​

확인 순서확인할 질문잘못되었을 때 나타나는 문제
1. 단위Byte와 bit, GB와 GiB, s와 ms를 구분했는가?크기와 시간이 배수 단위로 어긋남
2. 연산량FMA를 일관되게 2 FLOPs로 셌는가?처리율과 분기점 해석이 달라짐
3. 이동 범위HBM 읽기·쓰기만 세었는가?온칩 읽기를 중복 집계함
4. 출력기존 C는 읽지 않고 마지막에만 썼는가?과제와 다른 Q를 계산함
5. 타일 수출력 타일 수와 K 단계 수를 모두 곱했는가?출력 타일 하나의 비용을 전체 비용으로 착각함
6. 재사용서로 다른 출력 타일 사이 캐시를 가정했는가?타일 내부 재사용만 허용한 비교와 달라짐
7. 실현 가능성버퍼와 중간 합을 온칩에 유지할 수 있는가?이론상 유리한 구성을 실제로 실행하지 못함

11.3 손계산과 실측을 비교하게 된다면 ​

실제 시간이 예상보다 짧다면 먼저 가정한 HBM 이동이 모두 발생했는지 확인한다. 캐시가 입력을 공급했다면 계산에 쓴 Q부터 바뀐다. 반대로 더 길다면 부족한 병렬성, 동기화, 추가 데이터 이동, 온칩 자원 제약처럼 단순 모델에서 제외한 비용을 살펴본다.

계산식이 맞는 것과 실제 커널이 그 상한에 도달하는 것은 서로 다른 확인 항목이다. 이번 코드는 전자를 검증한다.


12. 연습문제 3개 ​

아래 문제는 모두 이번 정리본의 조건을 사용한다. 원소는 FP32 4 Byte, FMA는 2 FLOPs이며 중간 합은 온칩에 유지한다. 해답을 보기 전에 식과 단위를 함께 적어보자.

문제 1. 2×2 타일에서 입력을 몇 번 사용할까? ​

4×4 행렬곱을 2×2 출력 타일로 계산한다. K 조각 폭도 2다. 서로 다른 출력 타일 사이의 캐시 재사용은 없다.

  1. 출력 타일 하나의 한 K 단계에서 입력 원소 몇 개를 읽는가?
  2. 읽어온 A 원소 하나와 B 원소 하나는 각각 출력 몇 개에 기여하는가?
  3. 전체 행렬곱의 FLOPs, HBM 입력 읽기, 출력 쓰기를 구하자.
  4. 재사용 없음과 비교해 총 이동량과 산술 강도가 어떻게 달라지는가?

문제 2. 64 타일과 128 타일 중 무엇이 달라질까? ​

4096×4096 행렬곱에서 타일 한 변을 64에서 128로 바꾼다. P=20 TFLOPs/s, BW=1 TB/s다.

  1. 두 경우의 FLOPs, Q, I를 구하자.
  2. 두 시간과 Roofline 성능 상한을 구하자.
  3. 입력 읽기가 절반이 되어도 실행시간 하한이 절반이 되지 않는 이유를 설명하자.
  4. 단일 입력 버퍼와 중간 합의 크기는 각각 몇 배가 되는가?

문제 3. 출력 타일은 그대로 두고 K 조각만 키운다면? ​

출력 타일을 64×64로 고정하고 K 조각 폭을 32에서 64로 바꾼다. 전체 K는 4096이다. 입력 A·B 조각은 단일 버퍼에 두며 타일 사이 캐시 재사용은 없다.

  1. 한 단계의 입력 버퍼와 FLOPs는 어떻게 변하는가?
  2. 단계 수와 출력 타일 하나의 전체 입력 읽기량을 구하자.
  3. 중간 합의 크기는 변하는가?
  4. 이 변경만으로 HBM 기준 산술 강도가 증가한다고 말할 수 있는가?

공통 풀이 틀 ​

text
행렬 shape와 dtype:
출력 타일 크기와 K 조각 폭:
HBM 읽기·쓰기 및 캐시 가정:
총 FLOPs F:
총 HBM 이동량 Q:
산술 강도 I = F / Q:
분기점 I* = P / BW:
연산 시간과 HBM 시간:
성능 상한과 이상적 실행시간:
입력 버퍼와 중간 합의 크기:
실제 실행을 위해 추가 확인할 것:

13. 해답과 해설 ​

13.1 문제 1 해답 — 작은 타일의 읽기와 재사용 ​

한 K 단계에서 A 4개와 B 4개, 총 8개 원소를 읽는다. 각 입력은 선택한 출력 타일 안에서 두 번 사용된다. 단계 수는 2, 출력 타일 수는 4다.

F=2×43=128FLOPsQread=4×2×8×4=256Byte,Qwrite=42×4=64Byte
방식FLOPs총 QI
재사용 없음128576 Byte0.222222 FLOPs/Byte
2×2 타일128320 Byte0.4 FLOPs/Byte

입력 읽기는 절반이지만 출력 쓰기는 그대로다. 따라서 총 이동량의 감소 비율은 576/320=1.8배다.

13.2 문제 2 해답 — 대역폭 상한에서 연산 상한으로 ​

두 경우 모두 F=137,438,953,472 FLOPs이며 연산 시간은 6.871948 ms다.

항목T=64T=128
총 Q8,657,043,456 Byte4,362,076,160 Byte
I15.875969 FLOPs/Byte31.507692 FLOPs/Byte
HBM 시간8.657043 ms4.362076 ms
성능 상한15.875969 TFLOPs/s20 TFLOPs/s
이상적 시간8.657043 ms6.871948 ms
단일 입력 버퍼32 KiB128 KiB
중간 합16 KiB64 KiB

T=128에서는 I가 분기점 20을 넘는다. 메모리 시간이 더 짧아져도 연산 시간 아래로 실행시간 하한을 낮출 수 없으므로 이상적 속도 향상은 약 1.260배다. 입력 버퍼와 중간 합은 각각 네 배가 된다.

13.3 문제 3 해답 — K 조각 폭과 출력 재사용의 구분 ​

출력 타일 한 변은 64로 고정되어 있다. K 조각 폭을 BK라고 하면 입력 버퍼는 4(64BK+BK64)=512BK Byte다.

항목K 조각 폭 32K 조각 폭 64
단일 입력 버퍼16 KiB32 KiB
단계당 FLOPs262,144524,288
K 단계 수12864
출력 타일 하나의 전체 입력 읽기2,097,152 Byte2,097,152 Byte
출력 타일 하나의 최종 쓰기16,384 Byte16,384 Byte
중간 합16 KiB16 KiB

단계당 입력과 연산이 두 배가 되지만 단계 수는 절반이다. 전체 F와 Q는 같으므로 HBM 기준 I도 같다. 단계 수가 줄어 동기화 등의 비용이 달라질 가능성은 있지만, 그것은 이 이동량 계산과 구분해야 한다.

13.4 풀이에서 공통으로 확인할 점 ​

  • 출력 쓰기를 마지막에 한 번 포함했는가?
  • 타일 내부 재사용과 타일 사이 재사용을 구분했는가?
  • 입력 버퍼와 중간 합을 별도 자원으로 계산했는가?
  • 처리율의 상한을 실제 측정 성능처럼 설명하지 않았는가?

14. 스터디 발표와 함께 이야기할 질문 ​

14.1 같은 행렬곱을 네 관점에서 설명하기 ​

관점설명할 핵심 질문핵심 용어 3개함께 보여줄 자료
산술 강도같은 FLOPs인데 왜 I가 달라질까?FLOPs, HBM traffic, Arithmetic IntensityVector Add·행렬곱 비교표
Roofline두 상한은 무엇이며 어디서 만날까?BW, Peak performance, Ridge point축·단위·분기점이 있는 그래프
Tiling입력 하나가 어느 출력에 재사용될까?Output tile, K step, Accumulator4×4 행렬의 2×2 타일 그림
타일 크기큰 타일의 비용은 무엇일까?Shared Memory, Register, Occupancy버퍼·중간 합의 공간 표

각 발표는 같은 C = A @ B에 연결한다. 식을 설명한 뒤 그림에서 재사용을 짚고, 계산표에서 실제로 달라지는 열을 가리키면 네 관점이 하나로 이어진다.

14.2 함께 이야기할 질문 ​

  1. Vector Add도 타일을 크게 하면 산술 강도가 높아질까? 입력을 다른 출력에 재사용하지 않으므로 단순 타일링으로는 I=1/12가 바뀌지 않는다. 연산 결합으로 중간 출력 이동을 제거하는 경우는 별도 문제다.
  2. 64 타일에서 128 타일로 바꾸면 왜 정확히 두 배 빨라지지 않을까? HBM 시간보다 연산 시간이 커지는 구간에 진입하기 때문이다. 실제로는 온칩 자원 비용도 더 커진다.
  3. 다른 출력 타일이 L2 캐시에 있는 입력을 재사용하면 어떤 가정을 바꿔야 할까? 타일 사이 재사용이 없다는 조건을 수정해야 한다. 실제 HBM Byte가 줄면 HBM 기준 I는 증가한다.
  4. 128 타일의 이론적 상한이 가장 높으니 반드시 선택해야 할까? 입력 버퍼·레지스터·상주 블록 수를 확인해야 한다. Roofline 표만으로 실제 최적 타일을 정할 수 없다.
  5. 같은 출력 타일에서 K 조각만 두 배로 하면 Q가 절반이 될까? 이 모델에서는 아니다. 단계당 입력이 두 배, 단계 수가 절반이어서 전체 읽기량은 같다.
  6. 실측점이 Roofline보다 크게 낮으면 무엇을 볼까? 먼저 자료형·FLOPs 정의·HBM 이동량·측정 범위를 맞췄는지 확인하고, 자원 사용량과 동기화·접근 패턴 등 모델에 없는 비용을 조사한다.

15. 용어 정리와 마지막 확인 ​

15.1 용어 사전 ​

용어이번 주에 필요한 뜻
Arithmetic Intensity정한 메모리 경계의 이동 Byte당 연산 횟수
Roofline연산 처리량과 메모리 대역폭에 따른 처리율 상한
Ridge point두 상한이 만나는 산술 강도 P/BW
Memory-bandwidth-bound이 모델에서 HBM 이동 시간이 더 긴 상태
Compute-bound이 모델에서 연산 시간이 더 긴 상태
Tiling계산을 작은 출력·입력 조각으로 나누는 방법
Local Blocking스레드의 작은 출력 묶음 안에서 입력을 재사용하는 방식
Shared Memory Blocking같은 블록의 스레드들이 공통 입력 조각을 공유하는 방식
AccumulatorK 방향 계산이 끝날 때까지 유지하는 중간 합
K step축약 차원 K의 입력 조각 하나를 처리하는 단계
Double buffering현재 입력을 계산하면서 다음 입력을 준비하기 위한 두 입력 버퍼 구성
Register spill레지스터에 두지 못한 값이 메모리로 밀려나는 현상
OccupancySM당 활성 상주 워프 수를 최대 상주 워프 수로 나눈 비율

15.2 계산 결과를 다시 한 장에 놓기 ​

Roofline은 연산 시간 F/P와 메모리 시간 Q/BW를 min(P,BW×I)라는 성능 상한으로 연결한다. 따라서 커널을 이해하려면 연산량과 함께 어느 메모리에서 몇 Byte를 옮겼는지 계산해야 한다.

이번 행렬곱은 모든 경우에 약 137.439 GFLOPs를 수행한다. 타일링은 이 연산량을 줄이지 않고 입력 재사용으로 HBM 이동량을 줄인다. 재사용 없음의 약 549.823 GB는 32·64·128 타일에서 각각 약 17.247·8.657·4.362 GB가 된다. 이상적 시간은 약 549.823 ms에서 17.247·8.657·6.872 ms로 낮아진다.

반면 타일 한 변을 두 배로 하면 입력 버퍼와 중간 합의 공간은 네 배가 된다. 따라서 좋은 타일은 재사용 이득뿐 아니라 shared memory·레지스터 사용량과 동시에 상주할 작업량을 함께 고려해 선택해야 한다. 이 정리본의 128 타일은 가상 Roofline에서 가장 높은 상한을 갖지만, 실제 최적 구현인지는 후속 GPU 커널 실습과 측정으로 확인할 문제다.

15.3 마지막으로 기억할 여섯 문장 ​

  1. 산술 강도는 FLOPs를 정한 메모리 구간의 총 이동 Byte로 나눈 값이다.
  2. Roofline의 세로축은 시간 대신 처리율이며 상한은 min(P, BW × I)다.
  3. 이번 타일링은 FLOPs를 유지하고 HBM 입력 재읽기를 줄인다.
  4. 중간 합은 K 단계 사이에 유지하고 최종 출력만 한 번 쓴다.
  5. 타일 한 변을 두 배로 하면 입력 버퍼와 중간 합은 네 배가 된다.
  6. 이론적 상한, 자원상 실행 가능성, 실제 측정 성능은 각각 확인해야 한다.

후속 Triton·Tiled MatMul 실습에서는 이 읽기·재사용·누산 구조를 실제 커널로 옮기고, 계산한 이동량과 자원 사용량이 구현에서 어떻게 나타나는지 확인한다.


16. 공식 자료와 추가 읽기 ​

아래 자료는 2026-10-04에 확인했다. 본문 그림은 원문을 복사하지 않고 과제 조건으로 직접 생성했다.

16.1 공통으로 읽을 자료 ​

자료읽을 범위·참고 그림읽으면서 확인할 질문
Scaling Book — All About RooflinesWhere Does the Time Go?의 산술 강도 정의, Visualizing rooflines두 시간식을 어떻게 처리율 그래프로 바꿀까?
MLC — Local Blocking첫 설명과 GPU Local Blocking 그림하나의 입력이 여러 출력에 어떻게 쓰일까?
MLC — Shared Memory Blocking첫 설명과 GPU Shared Memory Blocking 그림이웃 스레드의 공통 입력을 어떻게 공유할까?

공통 읽기의 목적은 수식과 그림에서 같은 데이터 재사용을 찾아보는 것이다. TVM 스케줄 전체를 실행하거나 실제 제품의 처리율을 외울 필요는 없다.

16.2 주제별 보충 자료 ​

자료읽을 범위·참고 그림읽으면서 확인할 질문
Nsight Compute — Roofline ChartsOverview 그림과 축·경계·ridge point 설명이론 상한과 실제 측정점을 어떻게 구분할까?
CUDA Best Practices — Calculating OccupancyOccupancy 및 Calculating Occupancy자원 사용량이 상주 작업 수를 어떻게 제한할까?
Triton — Matrix MultiplicationMotivations의 첫 블록 알고리즘, 설정의 블록 크기·num_warps출력 타일, K 조각, 실행 스레드 구성은 어떻게 다를까?

메인 교안 대표 집필 & 커리큘럼 설계: 이영빈 (Youngbin Lee, @yblee110) · Supported by MSIT & IITP (RS-2026-25521385)