Skip to content

🧑‍🔬 연구원 리서치 노트 — 작성자: mbsong (송무복) (Roofline & 타일 크기별 분석)

💡 핵심 기여 요약: 타일 크기(t=32, 64, 128)에 따른 HBM 이동량 감소와 산술 강도 변화를 로그 스케일 Roofline 그래프와 함께 완벽히 유도했습니다.


4주차 통합 문서: Roofline과 Tiling ​

주차 안내와 ①~⑤ 학습 문서를 읽는 순서대로 모았다. 산술 강도에서 시작해 Roofline, 타일 재사용, 저장 공간과 Occupancy를 살펴보고 4096×4096 FP32 행렬곱을 손계산한다.

목차 ​


4주차: Roofline과 Tiling ​

한 줄 답. 같은 계산을 하더라도 HBM에서 데이터를 덜 가져오면 산술 강도가 높아져 성능 상한이 올라간다. 다만 연산 성능의 한계와 온칩 저장 공간의 제약이 있다.

지난주에는 연산 시간과 메모리 시간을 따로 계산했다. 이번에는 두 시간을 Roofline 그래프로 연결하고, Tiling으로 입력을 재사용하는 방법을 살펴본다. 중심 질문은 “같은 계산을 하면서 데이터를 덜 읽어오면, 커널은 얼마나 빨라질 수 있을까?”다.

그림과 손계산으로 원리를 익히는 주차다. 전체 GPU 커널 구현은 뒤의 Triton·Tiled MatMul 주차에서 이어간다. 입력은 이미 장치에 있고, 하나의 GPU 안에서 HBM을 읽고 쓰는 이동만 센다. 다중 장치 통신은 이번 범위에서 제외한다.

1. 학습 목표와 읽는 순서 ​

순서문서중심 질문
①Arithmetic Intensity읽고 쓴 1 Byte로 얼마나 계산하는가?
②Roofline기울어진 선과 수평선은 각각 무엇을 제한하는가?
③Tiling과 4×4 그림입력 한 번을 읽어 여러 출력을 어떻게 계산하는가?
④타일 크기와 저장 공간·Occupancy더 큰 타일의 이점과 비용은 무엇인가?
⑤4096×4096 FP32 MatMul 손계산재사용 없음과 32·64·128 타일은 얼마나 다른가?

이전 주차 → 3주차 안내 · 3주차 MatMul 풀이

2. 공통으로 읽어올 자료 ​

자료읽을 절과 범위확인할 내용
Scaling Book 1장: All About Rooflines**Where Does the Time Go?**의 두 시간 공식 복습, Arithmetic Intensity 정의부터 Visualizing rooflines까지가로축·세로축, 메모리 상한·연산 상한, 두 선의 교점
MLC: GPU Acceleration — Local BlockingLocal Blocking의 그림과 첫 설명출력 일부를 계산할 때 입력을 여러 번 사용하는 방법
MLC: Shared Memory Blocking해당 절의 그림과 첫 설명같은 블록의 스레드들이 입력 조각을 공유하는 방법

TVM 스케줄 코드 전체를 이해하거나 실행할 필요는 없다. MLC는 본문을 확인한 book.mlc.ai 주소를 사용한다.

3. 주제별 보조 자료 ​

자료읽을 범위연결할 질문
NVIDIA Nsight Compute: Roofline ChartsOverview 그림, 축과 Ridge Point 설명상한선과 실제 측정점은 어떻게 다른가?
CUDA Best Practices: Calculating Occupancy레지스터·Shared Memory가 상주 블록 수를 제한하는 설명데이터를 더 보관하면 동시에 실행할 작업은 어떻게 되는가?
Triton: Matrix MultiplicationCompute Kernel의 첫 블록 알고리즘출력 타일을 고정하고 K 방향으로 입력 조각을 바꾸는 과정

Triton 자료의 구현 예제는 FP16 행렬곱이다. 이번 주는 블록 알고리즘을 읽고, 계산표에는 아래 FP32 조건을 사용한다. 뒤의 L2 캐시 최적화·autotuning은 필수 범위가 아니다.

4. 함께 사용할 기호와 모델 ​

기호뜻단위
F총 부동소수점 연산량, FMA 한 번을 2 FLOPs로 셈FLOPs
QHBM에서 읽고 쓴 총 이동량Byte
I = F/QHBM 기준 산술 강도FLOPs/Byte
P해당 자료형·연산에 적용 가능한 최대 연산 성능FLOPs/s
BWHBM 대역폭, 행렬 B와 구분Byte/s
T정사각 출력 타일의 한 변, 이번 문제는 K 조각 폭도 T원소 개수
Rroof=min(P,BW×I),I∗=PBWtideal=FRroof=max(FP,QBW)

그래프의 세로축은 초당 처리율이다. 선은 성능 상한이며, 이상적 시간은 주어진 모델의 실행시간 하한이다. 실제 커널이 그 성능을 달성한다는 뜻은 아니다.

5. 함께 계산할 문제 ​

먼저 4×4 행렬곱을 2×2 출력 타일로 나누는 그림을 그린다. 이어서 A·B·C가 모두 4096×4096 FP32인 C = A @ B를 계산한다.

  • 원소와 중간 합은 FP32, 원소당 4 Byte다.
  • 가상 하드웨어는 이 FP32 연산에 적용 가능한 P = 20 × 10¹² FLOPs/s, BW = 1 × 10¹² Byte/s다.
  • 재사용 없음: 곱셈할 때마다 A·B 원소를 HBM에서 다시 읽는다.
  • 타일 안에서 재사용: 출력 타일과 K 방향 입력 조각의 폭을 모두 T = 32, 64, 128로 바꾼다.
  • 입력은 이미 장치에 있고 중간 합은 온칩에 유지한다. 기존 C는 읽지 않고, 최종 C만 한 번 쓴다.
  • 서로 다른 출력 타일 사이의 캐시 재사용은 없다. 추가 이동·레지스터 spill은 없다고 가정한다.
  • 버퍼는 A·B 입력 조각을 한 벌만 보관하는 단일 버퍼 기준이다.

각 경우의 FLOPs, HBM 이동량, 산술 강도, Roofline 상한, 두 시간과 이상적 실행시간, 입력 버퍼와 중간 합 크기를 구한다. 풀이와 비교표에서 답을 확인한다. 실제 제품의 측정값을 예측하기 위한 조건이 아니라 재사용 효과를 구분하기 위한 단순화다.

HBM 산술 강도에 따른 성능 상한: 교점은 20 FLOPs/Byte이고 128 타일만 연산 상한에 도달한다. 점은 측정값이 아닌 모델 상한이다.

6. 각자 정리해 올 내용 ​

  1. 맡은 질문의 설명과 핵심 용어를 C = A @ B 예제로 연결한다.
  2. 축·단위·분기점이 표시된 Roofline 그림 또는 입력 재사용이 보이는 타일 그림을 그린다.
  3. 연산량·이동량·산술 강도·예상 시간을 계산표로 만든다.
  4. 자료형, 세어 본 메모리 구간, 캐시·재사용·중간 합 보관에 관한 가정을 적는다.
  5. 참고 링크와 읽은 절, 함께 이야기하고 싶은 질문을 남긴다.

7. 모임에서 서로 확인할 질문 ​

  • Roofline의 두 상한과 교점의 단위를 설명할 수 있는가?
  • Tiling 전후에 같은 FLOPs와 달라진 HBM Byte를 각각 셀 수 있는가?
  • Vector Add를 조각내기만 해도 MatMul처럼 산술 강도가 높아질까?
  • 64에서 128 타일로 키울 때 이동량이 거의 절반이 되어도 시간이 절반이 되지 않는 이유는 무엇인가?
  • 128×128 출력 타일이 16,384개 스레드를 뜻하지 않는 이유는 무엇인가?
  • 큰 타일의 재사용 이점과 Shared Memory·레지스터·동시 실행량의 비용을 함께 설명할 수 있는가?

그림은 이 문제의 수치로 직접 작성했다. 그림 생성 스크립트는 학습 자료 재생성용이며 GPU 커널이나 벤치마크가 아니다.


① Arithmetic Intensity: 읽고 쓴 데이터로 얼마나 계산할까? ​

한 줄 답. 산술 강도는 이동한 1 Byte당 수행한 FLOPs다. 같은 계산량이라도 HBM에서 덜 읽고 쓰면 값이 커진다.

4주차 목차 · 다음 → ② Roofline

1. 정의와 세는 경계 ​

I=FQ=총 FLOPsHBM에서 읽고 쓴 총 Byte[FLOPs/Byte]

산술 강도는 저장된 파일 크기나 할당한 메모리 용량으로 나누는 값이 아니다. 같은 원소를 HBM에서 10번 읽었다면 10번의 이동량을 센다. 입력 읽기뿐 아니라 출력 쓰기도 포함한다. 정의와 두 시간의 관계는 Scaling Book 1장의 Arithmetic Intensity 설명을 참고한다.

이번 문서에서는 HBM ↔ 칩 내부 경계를 센다. Shared Memory에서 레지스터로 같은 값을 다시 읽는 것은 HBM 이동량 Q에 더하지 않는다. 그 온칩 이동도 실제로는 시간과 자원을 쓰지만 이번 Roofline에는 별도 상한으로 넣지 않는다. 따라서 I를 말할 때는 항상 어느 메모리 구간을 기준으로 삼았는지 밝혀야 한다.

2. Vector Add: 입력을 다시 쓸 곳이 없다 ​

FP32 C[i] = A[i] + B[i]는 출력 하나당 덧셈 1회, 입력 2개 읽기와 출력 1개 쓰기다. 길이가 L이면 다음과 같다.

F=L,Q=4L+4L+4L=12L,I=112≈0.08333

각 A·B 원소는 하나의 출력에만 쓰인다. 입력을 한 번씩 읽는 조건에서 벡터를 작은 구간으로 나누기만 해서는 즉, tiling만 해서는 Q가 줄지 않는다.

3. MatMul: 같은 입력으로 여러 출력을 계산한다 ​

C[i,j]=∑k=0K−1A[i,k]B[k,j]

A[i,k]는 같은 행의 C[i,0], …, C[i,N−1]에, B[k,j]는 같은 열의 C[0,j], …, C[M−1,j]에 쓰인다. 수학적으로는 재사용 기회가 있지만, 값을 온칩에 유지하는 실행 방식이 있어야 HBM 읽기도 줄어든다.

정사각 행렬 한 변을 N, 출력 타일 한 변을 T라고 하자. 중간 합은 온칩에 두고 최종 C만 한 번 쓰는 조건이다.

계산 방식F (FLOPs)Q (Byte)I (FLOPs/Byte)
Vector Add, 길이 LL12L1/12
MatMul, 입력 재사용 없음2N³8N³ + 4N²N/(4N+2), 약 1/4
MatMul, T×T 출력 타일 안에서 재사용2N³8N³/T + 4N²NT/(4N+2T), N≫T이면 약 T/4
MatMul, 전체 입력을 각각 한 번만 읽는 이상적 기준2N³12N²N/6

마지막 행은 3주차 MatMul에서 사용한 낙관적인 기준이다. 이번 주의 타일 사이 캐시 재사용 없음 조건에서는 입력이 출력 타일마다 다시 필요하므로, 32·64·128 타일에 12N² Byte를 적용하면 안 된다. 타일별 공식은 ③ Tiling에서 직접 센다.

4. FLOPs가 같고 Byte만 줄면 무엇이 바뀔까? ​

F를 고정하고 Q를 절반으로 줄이면 I는 두 배가 되고, Q/BW는 절반으로 줄어든다. 하지만 F/P는 그대로다. 메모리 시간이 더 클 때는 시간 단축을 기대할 수 있고, 연산 시간이 더 커진 뒤에는 HBM 이동량 감소만으로 이상적 시간이 더 줄어들지 않는다.

함께 이야기할 질문: “행렬곱의 산술 강도”를 하나의 숫자로만 말해도 될까? 입력 자료형·타일·캐시 가정이 바뀌면 어느 항이 달라지는지 설명해 보자.

답: 행렬곱의 크기뿐 아니라 자료형과 입력 재사용 조건을 함께 밝혀야 한다. 같은 N×N 행렬곱이라도 연산량 F는 같고 HBM 이동량 Q는 달라질 수 있으므로, 산술 강도 I도 달라진다.

입력 A·B의 원소당 크기를 s_in Byte, 출력 C의 원소당 크기를 s_out Byte라고 하자. 타일 사이 캐시 재사용 없이 T×T 출력 타일 안에서 입력을 재사용하고, 중간 합은 온칩에 유지하며 최종 C만 한 번 쓰면 다음과 같다.

F=2N3,Q=2sinN3T+soutN2,I=2N32sinN3/T+soutN2
바뀌는 조건달라지는 항산술 강도에 미치는 영향
입력 자료형입력 읽기 항의 s_inFP32의 4 Byte에서 FP16의 2 Byte로 바꾸면 입력 읽기량이 절반이 된다. 출력 자료형이 같다면 출력 쓰기량은 그대로이므로 I가 정확히 두 배가 되는 것은 아니다.
출력 타일 크기 T입력 읽기 항의 1/T더 큰 타일에서 같은 입력을 더 많은 출력에 쓰면 입력 읽기량이 줄어 I가 커진다. 최종 C 쓰기량은 그대로다.
타일 사이 캐시 재사용HBM에서 실제로 다시 읽는 입력 횟수다른 출력 타일이 필요한 입력을 캐시에서 얻으면 HBM 읽기량이 위 식보다 줄어 I가 커진다.

예를 들어 입력과 출력이 모두 FP32이면 Q = 8N³/T + 4N²다. 입력만 FP16으로 바꾸면 Q = 4N³/T + 4N²다. 입력과 출력을 모두 FP16으로 바꾸면 Q 전체가 절반이 되어 I가 두 배가 된다. 이 비교에서 곱셈과 누산 횟수 F는 같지만, 자료형에 따라 하드웨어의 연산 처리 성능 P는 달라질 수 있다.

캐시 재사용이 충분해 A·B 전체를 각각 HBM에서 한 번만 읽는 이상적 조건이라면 Q = 2s_in N² + s_out N²까지 줄어든다. 모두 FP32일 때 I = N/6은 이 조건의 값이다. 따라서 “이 행렬곱의 산술 강도는 N/6이다”라고 말할 때도 자료형·메모리 경계·입력 재사용 가정을 함께 설명해야 한다.


② Roofline: 어느 자원이 성능을 제한할까? ​

한 줄 답. 데이터 공급으로 가능한 처리율 BW×I와 연산 장치의 최대 처리율 P 중 작은 값이 성능 상한이다.

이전 ← ① Arithmetic Intensity · 4주차 목차 · 다음 → ③ Tiling

1. 두 시간을 하나의 성능 그래프로 연결하기 ​

지난주의 두 시간은 다음과 같다.

tcompute=FP,tmemory=QBW

최대 성능을 쓰고 연산·이동이 충분히 겹친다고 두면 실행시간의 이상적 하한은 두 값 중 큰 값이다. 처리율은 같은 연산량 F를 시간으로 나눈 값이므로 다음처럼 바뀐다.

Rroof=Fmax(F/P,Q/BW)=min(P,FQ/BW)=min(P,BW×I)

시간에서는 max, 처리율에서는 min이다. 같은 모델을 다른 방향에서 읽는 것이다. 두 시간과 산술 강도의 연결은 Scaling Book: Where Does the Time Go? · Visualizing rooflines을 참고한다.

2. 축과 두 선 읽기 ​

가상 FP32 하드웨어 Roofline. 가로축은 HBM 산술 강도, 세로축은 TFLOPs/s이며 양쪽 모두 로그 축이다.

그래프 요소뜻단위·읽는 법
가로축 IHBM 이동 1 Byte당 계산량FLOPs/Byte
세로축 R초당 부동소수점 연산 처리율그림은 TFLOPs/s, 1 TFLOPs/s = 10¹² FLOPs/s
기울어진 선 BW×IHBM이 공급할 수 있는 데이터로 가능한 처리율Byte/s × FLOPs/Byte = FLOPs/s
수평선 P연산 장치가 낼 수 있는 최대 처리율I가 커져도 주어진 P는 일정
꺾이는 점 I*두 상한이 같아지는 산술 강도P/BW, FLOPs/Byte

HBM이 초당 BW Byte를 옮기고 Byte당 I FLOPs를 수행한다면 초당 최대 BW×I FLOPs를 공급할 수 있다. I가 두 배가 되면 이 상한도 두 배가 되어 기울어진 선이 된다. 반면 P는 연산 장치가 처리할 수 있는 고정된 한계이므로 수평선이다.

그림은 로그–로그 축이다. 이때 log R = log BW + log I이므로 메모리 선의 기울기는 1이고, BW가 커지면 선이 위로 이동한다. 선형 축에서 R = BW×I의 기울기는 BW다. 로그 축의 기울기 숫자를 대역폭 자체로 읽지 않는다.

Nsight Compute: Roofline Charts — Overview의 그림에서도 축·메모리 경계·연산 경계·Ridge Point를 찾아보자. 실제 프로파일러의 측정점과 상한선을 구분하는 것이 중요하다.

3. 가상 하드웨어의 분기점 ​

I∗=20×1012 FLOPs/s1×1012 Byte/s=20 FLOPs/Byte
  • I < 20: 이 모델에서는 HBM 대역폭 상한이 더 낮다.
  • I = 20: 두 자원의 이상적 시간이 같다.
  • I > 20: 이 모델에서는 연산 성능 상한이 더 낮다.

그래프의 네 점은 4096×4096 문제의 계산된 상한이다. 재사용 없음·32 타일·64 타일은 각각 약 0.250·7.969·15.876 TFLOPs/s다. 128 타일의 I는 약 31.508이지만 처리율 상한은 20 TFLOPs/s에서 멈춘다. 점을 그래프 위에 놓은 것은 실제 측정에서 상한을 달성했다는 뜻이 아니다.

4. 그래프에서 시간을 구하기 ​

같은 행렬곱의 F = 137,438,953,472 FLOPs를 사용하면 다음과 같다.

경우성능 상한 (TFLOPs/s)Roofline 예상 시간 (ms)
재사용 없음0.249969549.822923
T = 327.96887217.246978
T = 6415.8759698.657043
T = 12820.0000006.871948

세로축 값이 높을수록 같은 F를 처리하는 시간은 짧다. 서로 다른 연산량의 커널은 처리율만으로 실행시간을 비교할 수 없다.

5. 상한과 실제 성능은 다르다 ​

이 모델은 메모리 지연, 동기화, 명령어 부가 비용, 온칩 대역폭, 자원 사용에 따른 동시 실행량 등을 별도 계산하지 않는다. 실제 커널은 상한 아래에 놓일 수 있으며, 상한과의 차이를 모두 HBM 병목 때문이라고 단정할 수 없다. 최대 P와 BW의 단위뿐 아니라 해당 자료형·연산 경로와 메모리 구간도 맞춰야 한다.

함께 이야기할 질문: I가 10인 커널에서 P만 두 배로 높이면 Roofline 상한이 변할까? 같은 커널의 BW를 두 배로 높이면 어떻게 될까? (현재 조건에서 각각 10 TFLOPs/s 유지, 20 TFLOPs/s로 증가.)


③ Tiling: 작은 조각으로 나누면 왜 다시 쓸 수 있을까? ​

한 줄 답. 출력 타일을 계산하는 동안 필요한 A·B 조각을 온칩에 유지하면 같은 입력으로 여러 출력의 중간 합을 갱신할 수 있다.

이전 ← ② Roofline · 4주차 목차 · 다음 → ④ 타일 크기

1. 원소 하나가 쓰이는 위치 ​

C[i,j]=∑kA[i,k]B[k,j]

A[i,k]는 C의 i행 여러 열에, B[k,j]는 C의 j열 여러 행에 기여한다. N×N 행렬 전체를 보면 각각 N개의 출력에 쓰인다. 이 값을 매번 HBM에서 읽는 대신 가까이 보관하고 다시 쓰는 것이 Tiling의 목적이다.

가까이 보관하는 메모리는 빠르지만 용량이 작다. 따라서 Tiling으로 쪼개서 작은 메모리 용량을 가지고 계산할 수 있게 한다.

2. 4×4 행렬곱을 2×2 출력 타일로 나누기 ​

text
각 칸은 2×2 조각이다.

         A                   B                   C
     ┌─────┬─────┐       ┌─────┬─────┐       ┌─────┬─────┐
     │ A00 │ A01 │       │ B00 │ B01 │       │ C00 │ C01 │
     ├─────┼─────┤   @   ├─────┼─────┤   =   ├─────┼─────┤
     │ A10 │ A11 │       │ B10 │ B11 │       │ C10 │ C11 │
     └─────┴─────┘       └─────┴─────┘       └─────┴─────┘

목표: C00 = A00 @ B00 + A01 @ B10

                   HBM                  온칩
K 단계 1 (k=0,1):  A00, B00 ──읽기──▶ acc = 0 + A00 @ B00
K 단계 2 (k=2,3):  A01, B10 ──읽기──▶ acc += A01 @ B10
                                         │
                  C00 ◀──한 번 쓰기───────┘

acc는 중간 2×2 = 출력값 4개다. 첫 단계가 끝나도 HBM에 쓰지 않고 유지한다. 입력 버퍼는 두 번째 A·B 조각으로 교체한다. 나머지 C01·C10·C11도 같은 방식으로 계산한다.

재사용은 행렬곱에서 A의 한 행과 B의 한 열이 여러 출력 계산에 공통으로 쓰이는 특성에서 나온다. 첫 단계의 A00 @ B00을 행과 열의 내적으로 보면 다음과 같다. 여기서 a₀·a₁은 A00의 두 행, b₀·b₁은 B00의 두 열이며, 각각 원소가 2개인 벡터다.

text
                         B00의 열
                      b₀          b₁
                  ┌───────────┬───────────┐
  A00의 행   a₀   │ a₀ · b₀   │ a₀ · b₁   │ → 같은 a₀을 두 번 사용
             a₁   │ a₁ · b₀   │ a₁ · b₁   │ → 같은 a₁을 두 번 사용
                  └───────────┴───────────┘
                        ↓           ↓
                    같은 b₀을   같은 b₁을
                    두 번 사용  두 번 사용

각 칸의 내적을 acc의 같은 위치에 더한다.

A00의 첫 행은 C[0,0]과 C[0,1]을 계산할 때 공통으로 쓰이고, B00의 첫 열은 C[0,0]과 C[1,0]을 계산할 때 공통으로 쓰인다. 나머지 행과 열도 마찬가지다. 따라서 A00·B00을 한 번 온칩에 가져오면 각 입력 원소를 두 출력 계산에 재사용할 수 있다. 두 번째 단계의 A01 @ B10도 같은 구조다. Tiling은 이 공통 입력을 가까이 보관해, 여러 내적에서 쓸 때마다 HBM에서 다시 읽는 일을 줄인다.

3. 작은 예에서 연산량과 이동량 세기 ​

곱셈 1회와 누산 덧셈 1회를 합쳐 2 FLOPs로 센다. 0으로 초기화한 acc에 누산하는 관례이므로 F = 2N³이다. 수학적 내적의 덧셈만 N−1회로 세는 N²(2N−1)과 구분한다.

세는 대상출력 타일 하나전체 4×4 C
출력 개수416
K 단계 수2타일마다 2
연산량(2×2³)×2단계 = 32 FLOPs32×4타일 = 128 FLOPs
재사용 없음: A·B 읽기4출력×4곱×2입력×4 B = 128 B512 B
2×2 타일 재사용: A·B 읽기2단계×(4+4)원소×4 B = 64 B256 B
최종 C 쓰기4×4 B = 16 B64 B

K 단계 수: K는 A의 열 수이자 B의 행 수. 이 예는 타일은 2 x 2로 K를 2개씩 처리하기 때문에 출력 타일 하나는 2단계로 합산하게 된다. (C00의 경우 A00 @ B00 와 A01 @ B10)

방식F (FLOPs)Q (Byte)I (FLOPs/Byte)
재사용 없음128512 + 64 = 5762/9 ≈ 0.2222
2×2 타일 재사용128256 + 64 = 3200.4

입력 읽기는 절반이지만 최종 C 쓰기는 같다. 따라서 총 이동량이 정확히 절반이 되는 것은 아니다. 전체 행렬의 A·B 원소는 서로 다른 출력 타일에서도 필요하다. 타일 사이 캐시 재사용을 제외했으므로 이 예에서는 각 입력 원소를 HBM에서 두 번씩 읽는다.

4. N×N 행렬과 T×T 타일로 확장하기 ​

N×N 행렬곱의 출력을 T×T 타일로 나누고, 각 출력 타일의 내적 계산도 K 방향으로 T개 원소씩 처리한다고 하자. N은 T의 배수이므로 모든 출력 타일은 T×T 크기이고, 각 타일은 K 방향으로 N/T단계를 거쳐 계산된다. 이 조건에서 연산량과 HBM 이동량은 다음과 같다.

대상개수·크기
출력 타일 수(N/T)²
출력 타일당 K 단계 수N/T
한 단계의 A·B 읽기2T² 원소 = 8T² Byte
한 단계의 연산량2T³ FLOPs
출력 타일당 최종 쓰기4T² Byte
F=(NT)2NT×2T3=2N3Qread=(NT)2NT×8T2=8N3T,Qwrite=4N2

타일 안에서 각 입력 원소를 T개의 출력 계산에 쓰기 때문에 입력 읽기가 재사용 없는 경우의 1/T로 줄어든다. 연산량과 출력 쓰기는 그대로다.

5. Local Blocking과 Shared Memory Blocking ​

방식재사용 범위읽으며 볼 점
Local Blocking한 스레드가 담당하는 작은 출력 묶음입력 값을 여러 출력의 계산에 쓰고 중간 합을 가까이 유지
Shared Memory Blocking같은 스레드 블록의 여러 스레드공통으로 필요한 입력 조각을 함께 가져와 공유

두 방식은 함께 사용할 수 있다. MLC Local Blocking과 Shared Memory Blocking의 그림에서 각각 어느 범위의 출력이 입력을 공유하는지 따라가자. 이번 계산은 출력 타일 전체에서 입력을 재사용한다고 가정한 것이다.

Shared Memory를 사용하는 구현에서는 입력을 함께 가져온 뒤 사용 준비를 맞추고, 기존 입력을 다 쓰기 전에 버퍼를 덮어쓰지 않도록 동기화해야 한다. 여기서는 그 비용을 별도로 계산하지 않는다.

함께 이야기할 질문: 작은 예에서 A[0,0]은 C00과 C01에 모두 필요하다. 두 출력 타일 사이의 캐시 재사용을 허용하면 지금의 읽기 횟수는 어떻게 달라질까?

답: C00을 계산할 때 가져온 A[0,0]이 캐시에 남아 있고, C01을 계산할 때 그 캐시를 사용할 수 있다면 HBM에서는 두 번 대신 한 번만 읽는다. C01도 이 값을 가져와 사용하지만, 두 번째 접근은 캐시에서 처리하므로 HBM 이동량 Q에 다시 더하지 않는다.

같은 방식으로 A00·A01은 C00과 C01 사이에서, A10·A11은 C10과 C11 사이에서 재사용할 수 있다. B00·B10은 C00과 C10 사이에서, B01·B11은 C01과 C11 사이에서 재사용할 수 있다.

모든 A·B 조각이 필요한 시점까지 캐시에 유지되어 각 입력 원소를 HBM에서 한 번씩만 읽는 이상적 경우, 전체 4×4 행렬곱의 이동량은 다음과 같이 달라진다.

항목타일 사이 캐시 재사용 없음모든 입력을 타일 사이에서 재사용
입력 원소당 HBM 읽기 횟수2회1회
A·B 입력 읽기량256 Byte(16 + 16)×4 = 128 Byte
최종 C 쓰기량64 Byte64 Byte
총 이동량 Q320 Byte192 Byte
연산량 F128 FLOPs128 FLOPs
산술 강도 I128/320 = 0.4 FLOPs/Byte128/192 = 2/3 ≈ 0.6667 FLOPs/Byte

캐시 재사용을 허용한다고 모든 입력의 읽기 횟수가 자동으로 한 번이 되는 것은 아니다. 타일 실행 순서와 캐시 용량에 따라 일부 입력만 재사용될 수도 있고, 재사용하기 전에 캐시에서 밀려나면 다시 HBM에서 읽어야 한다. 위의 192 Byte는 모든 입력의 타일 사이 재사용이 성공한 경우의 값이다.


④ 타일 크기: 크게 만들수록 좋을까? ​

한 줄 답. 큰 출력 타일은 입력 재사용을 늘리지만, 입력 조각과 중간 합을 보관할 공간이 커져 동시에 상주할 작업 수를 줄일 수 있다.

이전 ← ③ Tiling · 4주차 목차 · 다음 → ⑤ MatMul 풀이

1. 출력 타일과 K 조각은 서로 다른 크기다 ​

일반적인 타일은 출력 크기 T_M×T_N, K 조각 폭 T_K로 표현한다.

보관 대상모양FP32 공간
A 입력 조각T_M×T_K4T_M T_K Byte
B 입력 조각T_K×T_N4T_K T_N Byte
출력의 중간 합 accT_M×T_N4T_M T_N Byte

출력 타일을 고정(출력 타일 공간 재사용)하고 K 방향으로 A·B 조각을 차례로 바꿔 누산한다. Triton Matrix Multiplication의 Compute Kernel 첫 블록 알고리즘에서 이 관계를 확인할 수 있다. 이번 연습만 T_M = T_N = T_K = T로 둔다.

입력 한 벌의 공간은 8T² Byte, 중간 합은 4T² Byte다. 한 변이 두 배면 각각 네 배의 저장 공간이 필요하다.

2. 단일 버퍼 기준의 공간 계산 ​

아래는 출력 타일 하나를 계산하는 작업 단위당 필요한 논리적 데이터 크기다. 1 KiB = 1024 Byte다.

TA 입력 버퍼B 입력 버퍼입력 합계중간 합논리적 총합
324,096 B (4 KiB)4,096 B (4 KiB)8 KiB4 KiB12 KiB
6416,384 B (16 KiB)16,384 B (16 KiB)32 KiB16 KiB48 KiB
12865,536 B (64 KiB)65,536 B (64 KiB)128 KiB64 KiB192 KiB

전형적인 Shared Memory 타일링 설명에서는 A·B 입력 버퍼를 Shared Memory에 두고, acc를 스레드들에 분산된 레지스터에 둔다. 총합 192 KiB를 하나의 Shared Memory 요구량으로 해석하면 안 된다. 두 자원은 각각 제한을 갖는다. 실제 배치는 구현·컴파일러·연산 경로에 따라 달라진다.

이 표에는 주소·루프 변수용 레지스터, 연산에 쓸 입력의 레지스터 복사본, 정렬·padding, 할당 단위에 따른 추가 공간이 없다. A·B를 두 벌 준비하는 이중 버퍼라면 입력 공간은 16T² Byte로 두 배가 된다. 중간 합 크기는 그대로다.

재사용 없는 방식은 출력 하나를 순차 누산하는 최소 논리 단위에서 A·B 스칼라 2개(8 B), acc 1개(4 B)가 필요하다. 전체 커널에서는 동시에 처리하는 출력 수만큼 이런 상태가 늘어난다. 스레드 배치를 지정하지 않았으므로 전체 레지스터 수로 단정할 수 없다.

3. 저장 공간이 동시 실행량을 제한하는 과정 ​

한 SM에는 Shared Memory와 레지스터가 유한하다. 한 블록이 자원을 더 많이 차지하면 같은 SM에 함께 상주할 수 있는 블록 수가 줄 수 있다. Occupancy는 상주하는 활성 warp 수 ÷ 하드웨어 최대 warp 수다. 전체 커널의 블록 수나 연산 장치 사용률과 같은 뜻은 아니다. CUDA Best Practices: Calculating Occupancy

text
출력 타일 확대
  ├─ 입력 재사용 증가 → HBM 이동량 감소 → 메모리 시간 감소
  └─ 입력 버퍼·중간 합 증가 → 블록당 자원 사용 증가
                              → 상주 블록·warp 수가 줄 수 있음
                              → 다른 작업으로 지연을 숨길 기회 감소

자원 제한을 생각하는 간단한 식은 다음과 같다. 실제 계산에는 할당 단위와 스레드·warp·블록 수 제한도 적용된다.

상주 블록 수≤min(⌊SM당 Shared Memory블록당 Shared Memory⌋,⌊SM당 레지스터 수블록당 레지스터 수⌋,기타 하드웨어 제한)

이번 가상 하드웨어는 P와 BW만 주어졌다. SM별 자원 용량과 스레드 배치가 없으므로 정확한 Occupancy나 128 타일의 실행 가능 여부는 계산할 수 없다. 레지스터가 부족해 중간값이 메모리로 밀려나는 spill이 생기면 추가 이동이 발생하여 이번 계산 가정도 깨진다. 다만 Occupancy가 높다고 항상 더 빠른 것은 아니다. 재사용과 레지스터 보관으로 얻는 이점도 함께 평가해야 한다.

4. 출력 타일 크기 ≠ 스레드 수 ​

출력 타일 크기는 계산할 데이터의 개수, 스레드 수는 그 작업에 참여하는 실행 주체의 개수다. 한 스레드가 여러 출력의 중간 합을 담당할 수 있다.

예를 들어 32×32 타일의 출력은 1,024개다. 이를 256개 스레드가 균등하게 나누면 스레드당 4개의 출력을 맡는 개념적 배치를 생각할 수 있다. 1,024개 출력이 곧 1,024개 스레드를 요구하는 것은 아니다. 같은 이유로 128×128 타일도 16,384개 스레드를 뜻하지 않는다.

Triton에서도 출력의 BLOCK_SIZE_M, BLOCK_SIZE_N과 실행 설정 num_warps는 별도 항목이다. 이번 주에는 특정 배치를 구현하지 않고 크기를 구분하는 데 집중한다.

5. K 폭만 늘려도 HBM 재사용이 늘까? ​

출력 타일을 고정하고 전체 K가 T_K로 나누어떨어지면, 한 출력 타일의 입력 이동량은 다음과 같다.

KTK×4(TMTK+TKTN)=4K(TM+TN)

이 단순 모델에서는 T_K가 소거된다. K 조각을 두 배로 하면 한 번에 읽는 양은 두 배지만 단계 수는 절반이 된다. 따라서 K 폭 확대만으로 총 HBM Byte가 줄지는 않는다. 버퍼 공간·동기화 횟수·파이프라이닝에는 영향을 줄 수 있다.

이번 정사각 타일 비교에서는 T_M = T_N = T_K = T로 두므로, 타일의 한 변 T를 키우면 K 조각 폭뿐 아니라 출력 타일의 행·열 수도 함께 커진다. 이에 따라 한 번 읽은 A 값은 타일의 T_N개 열 출력에, B 값은 T_M개 행 출력에 재사용되어 HBM 읽기가 줄고 산술 강도 I가 높아진다. 다만 이 가상 하드웨어의 Ridge Point는 P/BW = 20 FLOPs/Byte이며, T=128에서 I ≈ 31.508 FLOPs/Byte로 이미 연산 제한 구간에 들어선다. 이때 계산에는 최소 F/P ≈ 6.872 ms가 걸리지만, 메모리 이동 시간은 Q/BW ≈ 4.362 ms다. Roofline의 이상적 시간은 두 값 중 큰 값이므로, 타일을 더 키워 메모리 시간을 더 줄여도 연산 시간의 하한 아래로 내려갈 수는 없다. 이는 다른 하드웨어 자원 제약이나 실제 커널 실행 비용을 반영하지 않은 Roofline 모델의 결론이다.

함께 이야기할 질문: 출력 크기를 유지하며 T_K를 줄이면 입력 버퍼와 중간 합은 각각 어떻게 변할까? (입력 버퍼는 작아지고, 중간 합은 그대로다.)


⑤ MatMul 손계산: 재사용 없음과 32·64·128 타일 ​

한 줄 답. 같은 약 137.439 GFLOPs를 수행해도 HBM 이동량은 약 549.823 GB에서 4.362 GB까지 줄어든다. 이상적 시간은 549.823 ms에서 6.872 ms로 줄지만, 128 타일에서는 연산 성능이 한계다.

이전 ← ④ 타일 크기 · 4주차 목차

1. 문제와 가정 ​

A,B,C∈R4096×4096,C=A@B
항목조건
크기M = N = K = 4096, 이하 정사각 행렬 한 변을 N으로 표기
자료형입력·중간 합·출력 모두 FP32, 원소당 4 Byte
연산량 관례곱셈 + 누산 덧셈 = 2 FLOPs, FMA도 2 FLOPs
적용 가능한 최대 연산 성능 P20 × 10¹² FLOPs/s
HBM 대역폭 BW1 × 10¹² Byte/s
입력 위치이미 장치 HBM에 있음, CPU 전송 시간 제외
중간 합온칩에 계속 유지, HBM에 쓰거나 읽지 않음
출력기존 C를 읽지 않음, 최종 C만 한 번 씀
재사용 범위출력 타일 안에서만 재사용, 서로 다른 출력 타일 사이 캐시 재사용 없음
입력 버퍼A·B 조각 한 벌, K 조각 폭도 T
기타 가정추가 이동·spill·padding 없음, 연산과 이동의 충분한 중첩

모든 수치는 가상 조건이다. 1 GFLOPs = 10⁹ FLOPs, 1 TFLOPs/s = 10¹² FLOPs/s, 1 GB = 10⁹ Byte, 1 KiB = 1024 Byte, 1 ms = 10⁻³ s로 구분한다.

2. 공통 연산량과 출력 쓰기 ​

F=2N3=2×40963=137,438,953,472 FLOPsQC=4N2=67,108,864 Bytetcompute=137,438,953,47220×1012=0.0068719476736 s=6.8719476736 ms

F, 최종 C 쓰기, 연산 시간은 네 경우 모두 같다. FMA의 개수는 N³이지만 FLOPs는 그 두 배다.

3. 재사용 없음 ​

곱셈은 N³번이고, 매번 A·B를 하나씩 읽는다. 읽은 스칼라는 해당 곱셈 후 재사용하지 않는다. 중간 합만 온칩에 남긴다.

Q0=N3×2×4⏟A와 B 읽기+4N2⏟C 쓰기=549,822,922,752 ByteI0=2N38N3+4N2=N4N+2≈0.249969486 FLOPs/ByteR0=min(20×1012,1×1012×I0)≈0.249969486×1012 FLOPs/s

메모리 시간은 549.822922752 ms이며 연산 시간보다 크다. 따라서 이 모델의 이상적 시간도 그 값이다.

4. 타일 안에서 재사용 ​

출력 타일 수 (N/T)², 타일당 K 단계 수 N/T, 단계당 입력 이동량 8T² Byte를 곱한다. 작은 타일 그림과 유도를 먼저 보면 이 계산을 따라가기 쉽다.

QT=(NT)2NT×8T2+4N2=8N3T+4N2IT=2N38N3/T+4N2=NT4N+2T

T=64를 예로 들면 출력 타일은 64²=4,096개, 타일당 K 단계는 64번, 단계당 읽기는 32,768 Byte다.

Q64=4096×64×32768+67,108,864=8,657,043,456 Byte

전체 A를 한 번 읽고 끝나는 것은 아니다. 각 A 원소는 출력의 열 타일 수 N/T만큼, 각 B 원소는 행 타일 수 N/T만큼 HBM에서 읽는다. T=64에서는 각각 64회다.

5. FLOPs·HBM 이동량·산술 강도 비교 ​

방식F (FLOPs)A·B 읽기 (Byte)C 쓰기 (Byte)총 Q (Byte)총 Q (GB)I (FLOPs/Byte)
재사용 없음137,438,953,472549,755,813,88867,108,864549,822,922,752549.8229230.249969
T = 32137,438,953,47217,179,869,18467,108,86417,246,978,04817.2469787.968872
T = 64137,438,953,4728,589,934,59267,108,8648,657,043,4568.65704315.875969
T = 128137,438,953,4724,294,967,29667,108,8644,362,076,1604.36207631.507692

입력 읽기는 정확히 T배 줄지만, 총 Q에는 변하지 않는 C 쓰기가 포함된다. 따라서 I를 정확히 T/4로 두지 않는다. T/4는 출력 쓰기를 무시할 수 있을 때의 근사다.

6. Roofline 상한과 이상적 시간 비교 ​

Rroof=min(P,BW×I),tideal=max(F/P,Q/BW),I∗=20
방식BW×I (TFLOPs/s)Roofline 상한 (TFLOPs/s)연산 시간 (ms)메모리 시간 (ms)이상적 시간 (ms)모델상 제한 자원재사용 없음 대비 시간 비율 t₀/t
재사용 없음0.2499690.2499696.871948549.822923549.822923HBM 대역폭1.00배
T = 327.9688727.9688726.87194817.24697817.246978HBM 대역폭31.88배
T = 6415.87596915.8759696.8719488.6570438.657043HBM 대역폭63.51배
T = 12831.50769220.0000006.8719484.3620766.871948연산 성능80.01배

표시는 소수점 아래 6자리로 반올림했다. 비율은 반올림 전 값으로 구했다. 이 비율은 두 이상적 시간의 비교이며 실측 speedup이 아니다.

T=128에서 메모리는 약 4.362 ms 안에 데이터를 공급할 수 있어도, 계산에는 최소 약 6.872 ms가 필요하다. 따라서 메모리 시간을 그대로 실행시간으로 삼으면 안 된다. T=64에서 128로 키우면 이상적 시간은 8.657 ms에서 6.872 ms로 줄어, 약 1.785 ms(20.6%) 감소한다.

네 경우의 Roofline 성능 상한과 20 FLOPs/Byte 분기점

7. 입력 버퍼와 중간 합 비교 ​

단일 입력 버퍼와 FP32 누산 기준이다. 타일 행은 출력 타일 하나당, 재사용 없음 행은 출력 하나를 계산하는 최소 논리 단위당 크기다.

방식보관하는 A 입력보관하는 B 입력입력 합계 (Byte)중간 합 (Byte)논리적 총합 (Byte)
재사용 없음1원소 = 4 B1원소 = 4 B8412
T = 3232²원소 = 4,096 B32²원소 = 4,096 B8,192 (8 KiB)4,096 (4 KiB)12,288 (12 KiB)
T = 6464²원소 = 16,384 B64²원소 = 16,384 B32,768 (32 KiB)16,384 (16 KiB)49,152 (48 KiB)
T = 128128²원소 = 65,536 B128²원소 = 65,536 B131,072 (128 KiB)65,536 (64 KiB)196,608 (192 KiB)

입력 버퍼와 중간 합은 다른 자원에 놓일 수 있고, 실제 레지스터·Shared Memory 할당량에는 추가 비용이 있다. 정확한 Occupancy는 주어진 P·BW만으로 구할 수 없다. 자원 부족으로 spill이 발생하거나 타일 자체가 실행 불가능할 수도 있다. ④ 타일 크기에서 이 제약을 설명한다.

8. 검산과 토론 ​

  • 모든 경우의 F가 같고, C 쓰기가 67,108,864 Byte로 같은가?
  • 입력 읽기만 1/T로 줄였는가? 중간 합을 매 K 단계마다 HBM에 쓰는 항을 넣지는 않았는가?
  • 시간 계산에서 GB와 GiB를 섞지 않았는가? 이 문제의 1 TB/s에서는 Q의 GB 숫자와 메모리 시간의 ms 숫자가 같다.
  • 성능 상한에서 min을, 이상적 시간에서 max를 적용했는가?
  • 한 변을 두 배로 할 때 입력 버퍼와 중간 합이 네 배가 되는가?

전체 A·B·C의 저장 용량은 12N² = 201,326,592 Byte(192 MiB)다. 이 용량과 표의 반복 HBM 이동량은 다른 값이다. 3주차의 “A·B를 전체에서 한 번만 읽기”를 적용하면 I=N/6≈682.667이지만, 이번 문제에서는 타일 사이 재사용을 제외했으므로 그 값을 사용할 수 없다.

추가 손계산: 출력 쓰기를 포함하여 I_T≥20을 풀면 T≥80N/(N−40)이다. N=4096에서는 약 80.789다. 32·64·128 중에서는 128만 이를 넘는다. 이 임계값은 Roofline의 경계이며 최적 타일 크기나 실행 가능성을 보장하지 않는다.

공식의 출발점은 Scaling Book 1장의 시간·산술 강도 모델이다. 타일 안에서만 재사용한다는 사용자 지정 조건에 맞춰 이동량과 버퍼 크기를 직접 계산했다.


정리 ​

한 줄 답. Tiling은 온칩 용량에 맞게 데이터를 쪼개 재사용률을 높여 연산을 메모리 병목에서 컴퓨트 병목 쪽으로 옮기고, Roofline은 그 이동과 남은 병목의 상한을 정량적으로 보여준다.

Tiling. HBM에서 읽어온 데이터를 온칩 메모리에 두고 여러 번 재사용해 HBM 접근을 줄이는 기법이다. 온칩 메모리는 빠르지만 용량이 작아 전체 행렬을 올릴 수 없으므로, 온칩 용량에 맞는 작은 조각(tile)으로 나누어 계산한다.

  • FLOPs는 그대로이고 HBM 이동량만 줄어든다. 그 결과 산술 강도(FLOPs/Byte)가 올라간다.
  • T×T 출력 타일에서는 입력 읽기가 재사용 없는 경우의 1/T로 줄고, 최종 C 쓰기는 그대로다.
  • 타일 크기의 상한은 온칩 용량이다. 타일이 커질수록 입력 버퍼와 중간 합이 T²으로 늘어, 동시 실행량(Occupancy)이 줄 수 있다.

Roofline. 메모리 병목과 컴퓨트 병목의 성능 상한을 하나의 그래프로 나타낸 모델이다.

  • 성능 상한은 min(P, BW×I)이고, 시간 하한은 max(F/P, Q/BW)다.
  • 기울어진 선은 HBM 대역폭이 상한인 구간이고, 수평선은 연산 성능이 상한인 구간이다. 두 선이 만나는 지점이 분기점 I* = P/BW다.

둘의 연결. 타일을 키우면 산술 강도가 올라 Roofline 위의 점이 오른쪽으로 이동하고, 병목이 메모리에서 연산으로 옮겨 간다. 4096×4096 FP32 MatMul에서 확인한 결과는 다음과 같다.

방식산술 강도 I (FLOPs/Byte)이상적 시간 (ms)모델상 제한 자원
재사용 없음0.250549.823HBM 대역폭
T = 327.96917.247HBM 대역폭
T = 6415.8768.657HBM 대역폭
T = 12831.5086.872연산 성능 (I* = 20 초과)
  • 병목이 연산으로 넘어간 뒤에는 이동량을 더 줄여도 이상적 시간이 F/P 아래로 내려가지 않는다. 이후 과제는 데이터 이동 감소가 아니라 연산 유닛 활용률, 저정밀도, 연산량 감소다. 자세한 방향은 참고: 컴퓨트 바운드일 때의 해결 방향에 있다.
  • 이 값들은 모델 상한이며 실측 성능이 아니다.

참고: 컴퓨트 바운드일 때의 해결 방향 ​

한 줄 답. 병목이 연산으로 넘어가면 Tiling을 더 키워도 이상적 시간이 줄지 않는다. 연산 성능 P를 올리거나, 연산량 F를 줄이거나, 달성 성능을 P에 가깝게 끌어올려야 한다.

이번 주 범위 밖의 참고 사항이다. 먼저 달성 FLOP/s가 peak에 얼마나 가까운지 확인한다. 한참 낮다면 겉보기에만 컴퓨트 바운드이고 실제로는 아래 3번의 문제다. 프로파일러(Nsight Compute 등)로 연산 파이프 사용률과 stall 원인을 본다.

방향방법비고
1. 더 빠른 연산 유닛저정밀도(FP32 → TF32/BF16/FP16 → FP8 → FP4), Tensor Core·MXU 같은 전용 유닛 사용, 2:4 같은 structured sparsity, 칩 수 늘리기(병렬화)정밀도를 한 단계 낮추면 처리량이 대략 2배가 되고 메모리 트래픽도 준다. 병렬화는 통신이 새 병목이 된다.
2. 연산량(F) 줄이기모델 구조 변경(MoE, GQA/MQA, sliding window, low-rank, pruning), 불필요한 계산 제거(causal mask, padding 제거, KV cache, speculative decoding), Winograd·Strassen 같은 알고리즘알고리즘 변경은 수치 안정성과 하드웨어 적합성 때문에 제한적으로 쓴다. KV cache는 과거 토큰의 K·V 재계산을 없애 연산량을 줄이지만, 매 step 캐시 전체를 HBM에서 읽어야 해서 산술 강도가 낮아지고 decode가 메모리 바운드로 이동한다. 컴퓨트 바운드인 prefill에는 해당하지 않는다.
3. 활용률(MFU) 높이기타일 크기를 연산 유닛 단위의 배수로 맞춰 padding 낭비 줄이기, load·compute 파이프라이닝(double buffering), softmax·layernorm 같은 non-matmul 연산 fusion, autotuning달성 성능이 peak보다 많이 낮을 때 해당한다.
  • Roofline 위에서 보면 1번은 수평선(P)을 올리고, 2번은 같은 문제의 F를 줄이며, 3번은 점을 상한선에 가깝게 끌어올린다.
  • LLM 추론에서는 decode가 메모리 바운드이고 prefill이 컴퓨트 바운드이므로, 단계마다 처방이 다르다.
  • 연산량을 줄이는 기법이 병목을 반대 방향으로 옮길 수도 있다. KV cache가 그 예이며, 이후에는 GQA/MQA, KV cache 양자화처럼 캐시 크기와 읽기량을 줄이는 메모리 쪽 최적화가 필요해진다.

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