- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며
- 왜 행렬곱이 AI의 핵심 연산인가
- Systolic Array란 무엇인가
- 데이터가 흐르며 곱해진다 — 동작 추적
- TPU의 Systolic 설계
- Weight-Stationary vs Output-Stationary
- 데이터 재사용과 에너지
- Dataflow 아키텍처 일반론
- GPU의 텐서코어와 비교
- 활용률과 타일링
- 컴파일러 매핑
- 장단점
- 개발자 시사점
- 마치며
- 참고 자료
들어가며
오늘날 거대한 언어 모델을 학습시키고 추론하는 일은 결국 엄청난 양의 행렬곱(matrix multiplication)으로 환원됩니다. Transformer의 어텐션도, MLP 층도, 합성곱(convolution)도 내부적으로는 곱셈-누산(multiply-accumulate, MAC) 연산의 거대한 더미입니다.
그런데 이 MAC 연산 자체는 사실 그렇게 비싸지 않습니다. 정말 비싼 것은 데이터를 메모리에서 연산기로 옮기는 일입니다. 곱셈 한 번에 드는 에너지보다, 그 곱셈에 필요한 숫자를 DRAM에서 읽어 오는 에너지가 수백 배 더 큰 경우가 흔합니다. 이것이 이른바 메모리 월(memory wall) 문제입니다.
systolic array는 바로 이 문제를 정면으로 공략하기 위해 고안된 구조입니다. 데이터를 한 번 읽어 들이면, 그 데이터가 칩 내부의 연산기 격자(grid)를 따라 흐르면서 여러 번 재사용되도록 만듭니다. Google의 TPU(Tensor Processing Unit)가 바로 이 원리의 가장 유명한 구현체이고, NVIDIA GPU의 텐서코어 역시 비슷한 발상을 공유합니다.
이 글에서는 systolic array가 무엇이고 어떻게 동작하는지, weight-stationary와 output-stationary 같은 dataflow 전략이 무엇을 결정하는지, 그리고 컴파일러가 큰 행렬을 어떻게 작은 격자에 매핑하는지를 단계적으로 살펴보겠습니다.
왜 행렬곱이 AI의 핵심 연산인가
먼저 기준점을 잡아 봅시다. 크기가 M×K인 행렬 A와 K×N인 행렬 B를 곱해 M×N인 행렬 C를 얻는 연산을 생각해 보겠습니다.
C[m][n] = sum over k of A[m][k] * B[k][n]
이 한 줄을 위해 필요한 곱셈-누산 횟수는 M×N×K 입니다. 예를 들어 1024×1024 행렬 두 개를 곱하면 약 10억(10^9)에 가까운 MAC 연산이 일어납니다. 거대 모델에서는 이런 행렬곱이 초당 수천 번 반복됩니다.
핵심 통찰은 다음과 같습니다. 같은 데이터가 여러 번 쓰인다는 점입니다.
- A의 한 행
A[m][:]은 C의 한 행 전체(N개 원소)를 계산하는 데 반복 사용됩니다. - B의 한 열
B[:][n]은 C의 한 열 전체(M개 원소)를 계산하는 데 반복 사용됩니다.
순진하게 매번 메모리에서 다시 읽으면 데이터 이동량이 폭발합니다. 그래서 한 번 읽은 데이터를 칩 안에 붙잡아 두고 최대한 재사용해야 합니다. systolic array는 이 재사용을 하드웨어 구조 자체에 박아 넣은 설계입니다.
Systolic Array란 무엇인가
systolic array라는 이름은 1978년 H. T. Kung과 Charles Leiserson이 제안한 개념에서 나왔습니다. systolic은 심장이 규칙적으로 박동하며 혈액을 밀어내는(systole) 모습에서 따온 단어입니다. 데이터가 클럭마다 한 칸씩, 마치 심장 박동처럼 규칙적으로 격자를 따라 흘러가기 때문입니다.
구조는 단순합니다. 작은 연산기(processing element, PE)를 2차원 격자로 빽빽하게 배치합니다. 각 PE는 다음 일만 합니다.
- 위(또는 왼쪽)에서 들어온 값을 받는다.
- 자신이 가진 값과 곱한다.
- 누산기에 더한다.
- 받은 값을 아래(또는 오른쪽) 이웃 PE로 그대로 넘긴다.
PE는 외부 메모리에 직접 접근하지 않습니다. 오직 이웃 PE하고만 대화합니다. 데이터는 격자의 가장자리로 들어와서 한 칸씩 전진하며 내부의 모든 PE를 거칩니다. 한 번 들어온 데이터가 격자 전체를 가로지르는 동안 수많은 MAC에 재사용되는 것이죠.
B 가중치가 위에서 아래로 흐름
b00 b01 b02
| | |
a0 → [PE]→[PE]→[PE]→ (A 입력이 왼쪽에서 오른쪽으로 흐름)
| | |
a1 → [PE]→[PE]→[PE]→
| | |
a2 → [PE]→[PE]→[PE]→
| | |
부분합이 아래로 누적되어 빠져나감
각 PE 내부를 의사코드로 표현하면 다음과 같습니다.
매 클럭 사이클마다 PE가 하는 일:
in_left = 왼쪽 이웃에서 받은 활성값
in_top = 위쪽 이웃에서 받은 부분합
product = in_left * weight_held_here
out_bottom = in_top + product
out_right = in_left (활성값을 오른쪽으로 그대로 전달)
여기서 결정적인 점은 weight_held_here가 PE 안에 머물러 있다는 것입니다. 가중치를 한 번 적재해 두면, 흘러 들어오는 모든 활성값에 대해 같은 가중치가 재사용됩니다. 이것이 다음 절에서 다룰 weight-stationary 전략입니다.
데이터가 흐르며 곱해진다 — 동작 추적
개념을 손으로 따라가 봅시다. 2×2 행렬 두 개를 곱해 보겠습니다.
A = | a00 a01 | B = | b00 b01 |
| a10 a11 | | b10 b11 |
원하는 결과:
C[0][0] = a00*b00 + a01*b10
C[0][1] = a00*b01 + a01*b11
C[1][0] = a10*b00 + a11*b10
C[1][1] = a10*b01 + a11*b11
2×2 PE 격자를 사용하고, B의 가중치를 각 PE에 고정 적재합니다.
PE 위치별로 적재된 가중치:
[PE00=b00] [PE01=b01]
[PE10=b10] [PE11=b11]
이제 A의 행들을 왼쪽 가장자리에서 흘려 넣습니다. 중요한 점은 데이터를 비스듬히(skewed) 넣는다는 것입니다. 행마다 한 클럭씩 지연시켜야 누산 타이밍이 맞습니다.
시간 →
입력 스트림 (왼쪽 가장자리):
행 0 (PE00, PE01로): a00, a01
행 1 (PE10, PE11로): a10, a11 (한 사이클 지연되어 진입)
클럭별로 PE00을 추적하면 다음과 같습니다.
사이클 1: a00 도착 → 누산 = a00*b00
사이클 2: a01 도착 → 누산 = a00*b00 + a01*b10 ← C[0][0] 완성
PE00 위에서 흐르던 부분합이 아래 방향으로 합쳐지면서 한쪽 가장자리로 빠져나올 때, 우리가 원한 C의 각 원소가 완성됩니다. 핵심은 a00, a01 같은 값을 메모리에서 단 한 번만 읽었고, b00, b10 같은 가중치는 아예 읽지 않고 PE 안에 상주시켰다는 점입니다.
이 작은 예시를 256×256 격자로 확장하면, 한 번에 256×256 = 65,536개의 MAC을 매 클럭마다 동시에 수행하게 됩니다. TPU가 행렬곱에서 압도적 처리량을 내는 이유가 바로 이것입니다.
TPU의 Systolic 설계
Google이 2016년 논문에서 공개한 1세대 TPU의 심장은 256×256 크기의 systolic 행렬 곱셈 유닛(Matrix Multiply Unit, MXU)이었습니다. 65,536개의 8비트 정수 MAC 유닛이 격자를 이루어, 단일 명령으로 거대한 행렬 타일을 처리했습니다.
TPU 설계의 핵심 특징을 정리하면 다음과 같습니다.
- 거대한 단일 MXU: 수많은 작은 코어 대신, 하나의 큰 격자에 연산을 집중시킵니다. 제어 회로 오버헤드가 작아 칩 면적의 대부분을 실제 연산기에 쓸 수 있습니다.
- 온칩 데이터 재사용: 가중치와 활성값이 격자 안에서 흐르며 재사용되므로 DRAM 접근 횟수가 극적으로 줄어듭니다.
- 결정론적 실행: 캐시 미스나 분기 예측 같은 비결정적 요소가 거의 없어 성능을 정확히 예측하고 컴파일 단계에서 스케줄링할 수 있습니다.
세대를 거치며 TPU는 진화했습니다. 2026년 현재 Google은 6세대 Trillium(TPU v6)을 전세대 대비 peak 성능 약 4.7배 수준으로 끌어올렸고, 추론에 특화된 7세대 Ironwood를 함께 운용하고 있습니다. 하지만 격자 형태의 systolic 연산 유닛에 데이터를 흘려 재사용한다는 근본 원리는 1세대부터 변하지 않았습니다.
Weight-Stationary vs Output-Stationary
systolic array를 설계할 때 가장 중요한 결정은 무엇을 PE 안에 고정(stationary)시킬 것인가입니다. 세 가지 데이터(가중치, 활성값, 부분합) 중 무엇을 붙잡아 두느냐에 따라 dataflow 전략의 이름이 갈립니다.
Weight-Stationary (가중치 고정)
가중치를 PE에 적재해 두고, 활성값을 흘려 넣으며, 부분합을 격자 밖으로 빼냅니다.
PE 안에 머무름: 가중치 (W)
흘러 들어옴: 활성값 (A)
흘러 나감: 부분합 (psum)
- 장점: 같은 가중치를 여러 입력 배치에 재사용할 수 있어, 배치 크기가 클 때 가중치 재사용 효율이 최고입니다.
- 적합한 경우: 추론처럼 가중치는 고정이고 입력만 계속 바뀌는 워크로드.
Output-Stationary (출력 고정)
각 PE가 출력 원소 하나의 누산기를 끝까지 들고 있습니다. 가중치와 활성값이 둘 다 흘러 들어옵니다.
PE 안에 머무름: 부분합 (psum) — 출력 원소
흘러 들어옴: 가중치 (W)와 활성값 (A)
흘러 나감: 완성된 출력 (마지막에 한 번)
- 장점: 부분합을 이리저리 옮기지 않으므로 누산 정밀도 손실이 없고, psum 이동 에너지가 0에 수렴합니다.
- 적합한 경우: 누산 깊이(K)가 매우 길어 부분합 이동 비용이 부담스러운 경우.
Input-Stationary (입력 고정)
활성값을 PE에 고정하고 가중치를 흘립니다. 같은 입력이 여러 출력 채널에 재사용되는 합성곱 패턴 등에서 유용합니다.
세 전략을 한눈에 비교하면 다음과 같습니다.
| 전략 | 고정 데이터 | 흐르는 데이터 | 강점 | 약한 상황 |
|---|---|---|---|---|
| Weight-stationary | 가중치 | 활성값, 부분합 | 가중치 재사용 극대화, 추론 친화 | 부분합 이동 에너지 발생 |
| Output-stationary | 부분합 | 가중치, 활성값 | 누산 정밀도, psum 이동 최소 | 가중치를 매번 다시 흘려야 함 |
| Input-stationary | 활성값 | 가중치, 부분합 | 입력 재사용 큰 합성곱에 유리 | 출력 채널 많으면 부담 |
실제 가속기는 워크로드에 따라 이 전략들을 선택적으로 또는 혼합해 사용합니다. 어떤 전략도 모든 경우에 최선은 아니며, 행렬의 모양(M, N, K의 비율)과 배치 크기가 최적 선택을 좌우합니다.
데이터 재사용과 에너지
왜 dataflow 전략이 이렇게 중요할까요? 답은 에너지에 있습니다. 다양한 측정에서 공통적으로 나타나는 대략적인 경향은, 연산 한 번보다 데이터 이동 한 번이 훨씬 비싸다는 것입니다.
대략적인 에너지 비용 비교 (상대값, 경향성):
레지스터/PE 내부 접근 : 1배
온칩 SRAM 접근 : 수~수십 배
칩 밖 DRAM 접근 : 수백~수천 배
정확한 수치는 공정과 설계에 따라 다르지만, 방향은 일관됩니다. DRAM까지 다녀오는 비용이 압도적으로 큽니다. 따라서 가속기 설계의 목표는 단순히 곱셈기를 많이 박는 게 아니라, 한 번 읽은 데이터로 최대한 많은 MAC을 수행하는 것입니다.
이 효율을 정량화하는 개념이 산술 강도(arithmetic intensity)입니다.
산술 강도 = 수행한 연산 수 / 이동한 바이트 수 (단위: FLOP/byte)
산술 강도가 높을수록 메모리 대역폭에 덜 묶이고 연산기 활용률이 높아집니다. systolic array가 노리는 것이 바로 이 지표의 극대화입니다. 가중치 하나를 읽어 격자 안에 적재한 뒤 수백 개의 활성값과 곱하면, 그 가중치의 산술 강도는 수백 FLOP/byte까지 치솟습니다.
Dataflow 아키텍처 일반론
systolic array는 더 큰 개념인 dataflow 아키텍처의 한 사례입니다. 전통적인 폰 노이만(von Neumann) 구조는 명령어가 데이터를 끌어오는 명령 주도(control-driven) 방식입니다. 반면 dataflow는 데이터가 준비되면 연산이 발화(fire)하는 데이터 주도 방식입니다.
폰 노이만: PC가 명령을 가리킴 → 데이터를 가져옴 → 실행 → 결과 저장
Dataflow : 피연산자가 도착함 → 즉시 발화 → 결과를 다음 노드로 전달
dataflow의 매력은 다음과 같습니다.
- 명시적인 프로그램 카운터나 복잡한 제어 흐름이 줄어듭니다.
- 데이터 의존성이 자연스럽게 병렬성을 드러냅니다.
- 데이터가 연산기 사이를 직접 흐르므로 중간 결과를 메모리에 저장했다 다시 읽는 낭비가 줄어듭니다.
현대 AI 가속기들은 정도의 차이는 있어도 이 dataflow 철학을 공유합니다. 연산 그래프(computation graph)를 칩 위에 공간적으로 펼쳐 놓고, 텐서를 그 위로 흘려보내는 것이 핵심 발상입니다.
GPU의 텐서코어와 비교
NVIDIA GPU는 전통적으로 수천 개의 작은 코어가 SIMT 방식으로 동작하는 구조였습니다. 그런데 2017년 Volta 세대부터 텐서코어(Tensor Core)라는 전용 행렬곱 유닛이 추가되었습니다. 텐서코어는 작은 행렬 타일(예: 4×4 또는 16×16)을 한 명령으로 곱-누산하는 하드웨어입니다.
systolic array와 텐서코어를 비교하면 다음과 같습니다.
| 측면 | TPU systolic array | GPU 텐서코어 |
|---|---|---|
| 격자 규모 | 하나의 거대한 격자(예: 256×256) | 작은 타일 유닛 다수를 코어에 분산 |
| 제어 방식 | 컴파일 타임에 결정론적 스케줄 | 런타임 스레드/워프 스케줄링 |
| 유연성 | 행렬곱에 고도로 특화 | 범용 GPU 연산과 공존, 유연 |
| 데이터 재사용 | 격자 흐름으로 하드웨어가 강제 | 레지스터/공유 메모리 활용에 의존 |
거칠게 요약하면, TPU는 행렬곱이라는 한 가지 일을 위해 칩 전체를 한 덩어리로 설계한 반면, GPU는 범용성을 유지하면서 행렬곱 가속 유닛을 끼워 넣은 형태입니다. 2026년 현재 NVIDIA Blackwell 세대와 차세대 Vera Rubin은 2세대 Transformer Engine과 더 큰 텐서코어를 탑재하며, 저정밀 연산과 메모리 대역폭에서 큰 진전을 보이고 있습니다.
어느 쪽이 우월하다기보다, 워크로드와 운영 환경에 따라 트레이드오프가 다릅니다. 대규모 학습/추론을 위한 데이터센터 환경에서는 두 접근이 모두 살아남아 경쟁하고 있습니다.
활용률과 타일링
systolic array가 이론적으로 65,536개의 MAC을 매 클럭 수행할 수 있다고 해서, 실제로 항상 그만큼 일하는 것은 아닙니다. 실제 활용률(utilization)은 종종 그보다 낮습니다. 이유는 행렬의 모양이 격자 크기와 딱 떨어지지 않기 때문입니다.
예를 들어 256×256 격자에 9×9 행렬을 흘려 넣으면, 격자의 극히 일부만 일하고 나머지는 놀게 됩니다. 또한 격자를 가득 채우려면 데이터가 흘러 들어오고 빠져나가는 파이프라인 채우기/비우기(fill/drain) 구간이 있는데, 이 구간 동안 일부 PE는 유휴 상태가 됩니다.
이 문제를 다루는 기법이 타일링(tiling)입니다. 큰 행렬을 격자 크기에 맞는 작은 타일로 쪼개어 차례로 흘려 넣습니다.
큰 행렬 C (1024 x 1024)를
격자 크기 256 x 256 타일로 분할:
+------+------+------+------+
| T00 | T01 | T02 | T03 |
+------+------+------+------+
| T10 | T11 | T12 | T13 |
+------+------+------+------+
| ... 4 x 4 = 16개 타일 ... |
+------+------+------+------+
각 타일은 격자를 가득 채우므로 활용률이 높음.
타일 크기를 격자에 맞게 정렬하고, 파이프라인이 충분히 깊게 채워지도록 충분히 많은 타일을 연속으로 흘려 넣는 것이 활용률을 끌어올리는 핵심입니다. 행렬 차원이 격자 크기의 배수일 때 활용률이 가장 좋아집니다.
컴파일러 매핑
개발자가 직접 PE 하나하나에 데이터를 흘려 넣지는 않습니다. 그 일은 컴파일러가 합니다. TPU의 경우 XLA, GPU의 경우 cuBLAS/cuDNN이나 Triton 같은 도구가 고수준 텐서 연산을 하드웨어 격자에 매핑합니다.
컴파일러가 하는 핵심 작업은 다음과 같습니다.
1. 연산 그래프에서 행렬곱 노드를 찾는다.
2. 행렬 차원(M, N, K)을 격자 크기에 맞게 타일로 분할한다.
3. dataflow 전략(weight-stationary 등)을 선택한다.
4. 타일들을 흘려 넣는 순서를 스케줄링한다.
5. 데이터 적재(load)와 연산이 겹치도록 파이프라인을 구성한다.
이 과정의 품질이 최종 성능을 좌우합니다. 같은 하드웨어라도 컴파일러가 타일 크기와 흐름을 얼마나 잘 잡느냐에 따라 활용률이 크게 달라집니다. 그래서 가속기 회사들은 하드웨어만큼이나 컴파일러 스택에 막대한 투자를 합니다.
개발자 입장에서 실용적인 시사점은, 행렬 차원을 가급적 하드웨어 친화적인 값(예: 8의 배수, 128의 배수)으로 맞추면 컴파일러가 더 좋은 매핑을 찾기 쉽다는 것입니다.
장단점
systolic array 접근의 장단점을 정리해 보겠습니다.
장점:
- 행렬곱에 대해 매우 높은 처리량과 에너지 효율을 냅니다.
- 데이터 이동을 하드웨어 구조가 강제로 줄여 주어 메모리 월 문제를 완화합니다.
- 제어가 단순해 칩 면적의 대부분을 실제 연산기에 할당할 수 있습니다.
- 결정론적이라 성능 예측과 스케줄링이 쉽습니다.
단점:
- 행렬곱이 아닌 불규칙한 연산에는 부적합합니다.
- 행렬 모양이 격자와 맞지 않으면 활용률이 떨어집니다.
- 파이프라인 채우기/비우기 오버헤드가 작은 행렬에서 두드러집니다.
- 격자 크기가 고정이라 유연성이 GPU보다 낮습니다.
이 트레이드오프 때문에, systolic array는 만능 해법이 아니라 행렬곱이 압도적으로 지배적인 딥러닝 워크로드에 특화된 도구로 이해하는 것이 정확합니다.
개발자 시사점
하드웨어를 직접 설계하지 않는 개발자에게도 이 원리는 실무적으로 유용합니다.
- 배치 크기를 키우면 weight-stationary 가속기에서 가중치 재사용이 늘어 효율이 좋아집니다. 추론 서빙에서 배칭이 중요한 이유 중 하나입니다.
- 행렬 차원을 정렬하면 (예: 128의 배수로 패딩) 컴파일러가 격자를 가득 채우는 타일을 만들기 쉬워 활용률이 올라갑니다.
- 저정밀 연산(FP8, FP4 등)을 활용하면 같은 격자로 더 많은 MAC을 처리할 수 있고 데이터 이동 바이트 수가 줄어 산술 강도가 올라갑니다.
- 메모리 접근 패턴을 의식하면 텐서를 연속적이고 정렬된 레이아웃으로 두는 것이 데이터 흐름에 유리합니다.
요컨대 "곱셈은 싸고 데이터 이동은 비싸다"는 원칙을 머릿속에 두면, 모델 구조와 서빙 설정을 하드웨어 친화적으로 다듬는 직관이 생깁니다.
마치며
systolic array는 단순한 발상을 끝까지 밀어붙인 결과물입니다. 데이터를 한 번 읽었으면 최대한 재사용하라는 원칙을, 연산기를 격자로 배치하고 데이터를 그 위로 흘려보내는 구조에 그대로 새겨 넣었습니다. 심장이 박동하듯 클럭마다 데이터가 한 칸씩 전진하며 곱해지고 누적되는 이 우아한 흐름이, TPU의 압도적 행렬곱 성능을 떠받치는 심장 원리입니다.
dataflow 전략의 선택, 타일링과 활용률, 컴파일러 매핑이라는 세 축을 이해하면, 왜 특정 가속기가 특정 워크로드에서 빛나는지, 그리고 내 모델을 어떻게 하드웨어 친화적으로 다듬을지에 대한 단단한 직관을 갖게 됩니다. AI 하드웨어의 발전은 결국 메모리 월과의 끝없는 싸움이며, systolic array는 그 싸움에서 가장 오래 검증된 무기 중 하나입니다.
참고 자료
- Google Cloud TPU 공식 문서
- In-Datacenter Performance Analysis of a Tensor Processing Unit (arXiv:1704.04760)
- NVIDIA Blackwell Architecture
- NVIDIA Tensor Cores 소개
- H. T. Kung, Why Systolic Architectures? (IEEE Computer, 1982)
- Eyeriss: Energy-Efficient Dataflow for CNNs (arXiv:1606.04887)
- XLA: Optimizing Compiler for Machine Learning
- arXiv 검색: systolic array accelerators