
TPU는 왜 유행하지 않는가 — 토큰 단가가 저렴함에도 아무도 사용하지 않는 이유
요약
TPU와 Trainium 같은 시스톨릭 어레이 기반 가속기가 NVIDIA GPU에 비해 낮은 비용과 높은 효율에도 불구하고 널리 쓰이지 못하는 기술적 이유를 분석합니다. 핵심 원인은 정적 셰이프(Static Shape) 제약으로 인한 컴파일 비용과 동적 데이터 처리의 어려움에 있습니다.
핵심 포인트
- TPU는 시스톨릭 어레이 구조로 인해 데이터 형태가 고정되어야 최적의 성능을 냄
- 입력 데이터의 셰이프가 변할 때마다 새로운 컴파일이 필요한 정적 구조의 한계
- NVIDIA GPU는 동적 스케줄링을 통해 다양한 입력 크기에 유연하게 대응
- 하드웨어 선택의 핵심 기준은 벤더 로고가 아닌 정적/동적 처리 방식의 차이
스펙 시트만 보면 TPU 이야기는 압도적이다. 토큰 단가가 저렴하고, 와트당 처리량이 차원이 다르게 좋으며, 레이턴시(Latency)가 결정론적(Deterministic)이다. Trainium도 같은 이야기를 한다. 그럼에도 불구하고 업계의 대부분은——ChatGPT나 Claude의 Web UI 뒷단에서 발생하는 추론 트래픽을 포함하여——여전히 NVIDIA로 구동되고 있다.
"종이 위에서는 저렴하다"와 "실제로 배포되어 있는 것" 사이의 이 괴리는 마케팅의 실패가 아니다. 시스톨릭 어레이(Systolic Array)형 실리콘이 코드, 파이프라인, 조직 구조라는 3가지 요소에 부과하는 "세금" 그 자체다. 이 글에서는 그 세금이 어디에서 오는지, 그리고 왜 극소수의 회사만이 그것을 지불할 수 있는지 깊이 파헤쳐 본다.
NVIDIA GPU는 SIMT(Single Instruction, Multiple Threads) 프로세서다. 실행 시 스레드를 동적으로 스케줄링하고, 메모리를 온디맨드(On-demand)로 페이징(Paging)한다. 반면, TPU나 AWS Trainium은 GPU가 아니다. 시스톨릭 어레이(Systolic Array)——인접한 연산기(MAC)끼리 물리적인 구리선으로 직접 연결된 격자 구조로, 사전 컴파일러(TPU라면 XLA, Trainium이라면 Neuron 컴파일러)가 데이터를 흘려보낸다.
시스톨릭 어레이가 피크 성능을 내는 것은 흘러가는 데이터의 형태(Shape)가 컴파일 시점에 고정되어 있을 때뿐이다. 가중치(Weight)는 처음에 한 번 로드되면 연산기 내에 정지(Stationary)해 있고, 입력(Activation)만이 양동이 릴레이처럼 옆으로 슬라이드된다. 시퀀스 길이(Sequence length)나 배치 사이즈(Batch size)가 단 1토큰이라도 바뀌면, 데이터의 경로와 메모리 주소를 전부 재계산해야 한다——즉, 컴파일러가 **새로운 바이너리(Binary)**를 다시 생성해야 한다는 뜻이다.
이 단 하나의 제약이 하류(Downstream)의 모든 고통의 근원이다. 추론 시에 일어나는 일은 다음과 같다:
| 실행 시 입력 | NVIDIA (동적) | TPU / Trainium (정적) |
|---|---|---|
| 컴파일된 버킷보다 큰 경우 | 동적 할당으로 처리 | Shape Mismatch로 크래시 |
| ... |
토큰이 칩에 도달하기 전에, "이것은 어떤 형태이며, 어떤 컴파일된 바이너리로 라우팅할 것인가"에 답해야 한다. NVIDIA에서는 이 질문을 던질 필요조차 없다.
가장 깔끔한 멘탈 모델(Mental model)은 이것이다. NVIDIA는 Python, TPU/Trainium은 Java다.
NVIDIA = Python. 동적 타이핑(Dynamic typing) $\approx$ 동적 셰이프(Dynamic shape). 런타임(Runtime)이 혼돈을 흡수한다. 100토큰의 프롬프트든 50,000토큰이든, 동일한 forward에 집어넣기만 하면 "적당히 빠르게" 동작한다. 컴파일 공정이 눈앞에 나타나지 않는다.
TPU/Trainium = Java. 정적 타이핑(Static typing) $\approx$ 정적 셰이프(Static shape). 고정된 바이너리(Neuron의 NEFF, TPU의 경우 XLA 실행 파일)로 컴파일될 때까지 단 한 줄도 움직이지 않는다. 보일러플레이트(Boilerplate)와 엄격한 규율을 대가로, 모든 것이 "계약" 안에 들어왔을 때의 극한의 실행 효율을 얻는다.
참고로 AMD의 Instinct 계열(CDNA / ROCm)은 완전히 NVIDIA(Python) 측이다. SIMT, 동적 셰이프, PagedAttention 대응, 그리고 기존 CUDA 코드를 그대로 실행하기 위한 HIPIFY 툴체인을 갖추고 있다. 진정한 단층은 벤더의 로고가 아니라, 정적인가 동적인가에 있다.
세 명의 사용자가 동시에 왔다고 가정하자. 3,000 / 4,000 / 1,000 토큰. NVIDIA라면 패딩(Padding)도 마스크(Mask) 생성도 하지 않는다. 평탄한(Flat) 8,000토큰 버퍼에 연결하고, 경계를 나타내는 cu_seqlens 인덱스를 FlashAttention에 전달할 뿐이다.
# NVIDIA: 가변 길이 어텐션. 패딩 없음, 마스크 행렬 없음.
# 평탄한 데이터 + 누적 시퀀스 길이 [0, 3000, 7000, 8000]를 전달할 뿐.
outputs = flash_attn_varlen_func(
...
커널이 경계 인덱스를 보고 각 사용자의 문맥을 하드웨어 레벨에서 격리한다. 사용자 간 어텐션의 불필요한 FLOPs는 제로다. 코드는 "단순한 모델 로직"이다.
TPU에서는 시스톨릭 어레이의 형태를 바꿀 수 없으므로 반대로 한다. 모든 것을 고정된 [batch, STATIC_SEQ_LEN] 사각형에 밀어 넣고, 계산하고 싶지 않은 부분을 수식으로 지워버린다.
import torch
import torch.nn as nn
import torch.nn.functional as F
...
이 코드의 두 가지 지점은 정적 실리콘 (Static Silicon)의 순수한 결과물이다.
CUDA의 eager mode와 달리, XLA에서는 xm.mark_step()이야말로 실제 실행 트리거이다. model(x)를 호출해도 그래프가 축적될 뿐이다. mark_step()이 쌓인 그래프를 하나의 고정된 바이너리로 컴파일하여 전송하기 전까지는 칩 위에서 아무것도 실행되지 않는다. 새로운 형상 (Shape)이 들어오면 새로운 컴파일이 발생한다.
NVIDIA의 masked_fill(..., -1e9)는 최적화가 아니라 해킹(Hack)이다. varlen은 사용자 간의 곱셈을 애초에 건너뛴다. 시스톨릭 어레이 (Systolic Array)는 건너뛸 수 없다. 사각형의 모든 셀(0을 포함하여)을 성실하게 곱한 다음, softmax로 수학적으로 뭉개버린다. 전력을 소비한 뒤에 결과를 버리는 것이다.
오버플로우 (Overflow, 범위를 벗어나 발생하는 충돌) 측면은 직관적이다. 1,024용으로 컴파일된 바이너리에 1,025개의 토큰을 흘려보내면 Shape Mismatch로 인해 충돌한다. 까다로운 것은 *언더플로우 (Underflow)*이다. 즉, 1,024 규모의 시스템에 100개의 토큰 요청이 들어오는 경우다.
그대로 흘려보내기: XLA가 새로운 형상으로 인식하여 JIT 재컴파일을 시작한다. 운영 환경(Production)에서는 수 분간의 프리징(Freeze)이 발생한다. 스톨 (Stall).
1,024로 패딩 (Padding): 어레이는 연산기의 약 90%에서 0 × 0 + 0을 성실하게 실행하며, 아무것도 하지 않기 위해 모든 전력을 소비한다. 효율이 폭락한다.
탈출구는 **패킹 (Packing)**이다. 1개의 버킷에 1명의 사용자를 할당하는 대신, 여러 사용자의 요청을 테트리스처럼 고정된 사각형에 채워 넣고, 사용자 간에 어텐션 (Attention)이 새나가지 않도록 세그먼트 ID 마스크를 생성한다.
고정 버킷 [ 8192 토큰 ]
├─ 사용자 A 쿼리 (3000)
├─ 사용자 B 쿼리 (4000)
...
"이 사각형"이 물리적으로 무엇인지 구체화하면 이해하기 쉽다. BATCH_SIZE = 4, STATIC_SEQ_LEN = 8192로 컴파일하면, XLA는 TPU의 HBM 위에 하나의 연속된 [4, 8192] 정적 영역을 예약한다. "8192 크기의 방 4개가 독립적으로 나열되는 것"이 아니라, 컴파일러가 연산 회로의 경로를 고정하는 **하나의 거대한 시트 (Sheet)**다. 단일 사용자가 8,192개의 레인을 하나 다 쓰는 경우조차 드물기 때문에, 서빙 (Serving) 계층은 이 4개의 레인에 여러 사용자를 동시에 채워 넣는다.
【1개의 TPU 프로세서 = 1장의 정적인 [4 x 8192] 시트】
레인[0] (8192): [ A(2000) + B(5000) + C(1000) + 보간(192) ]
레인[1] (8192): [ D(8000) + 보간(192) ]
...
물리적으로는 4개의 레인(총 32K 공간)이지만, 논리적으로는 프록시(Proxy)가 **9명의 불규칙한(Ragged) 사용자(A~I)**를 그곳에 압축해 넣었을 뿐이다. 애플리케이션 측면에서는 "1개의 TPU가 다수의 작은 요청을 병렬로 동시에 처리하고 있는 것"처럼 보이지만, 실체는 1장의 경직된 시트에 세그먼트 마스크를 덧씌운 것에 불과하다. 하드웨어가 "처음부터 작은 방을 나누는 것"이 아니라 "하나의 두꺼운 시트"를 원하는 이유는 시스톨릭 어레이의 물리적 특성 때문이다. 행렬이 클수록 어레이의 충전율 (Utilization)이 올라가고, 데이터 공급 사이의 공회전이 줄어든다.
잘 처리한다면 MFU (Model FLOPs Utilization)는 100% 가까이 올라갈 수 있다. 하지만 여기서 우리가 무엇을 만들었는지에 주목해 달라. 클러스터 앞단에, 불규칙한 입력을 받아 실시간으로 사각형에 채워 넣기만을 위한 고속 Go/C++ 제작 프록시를 만든 것이다. NVIDIA에서는 이 레이어가 애초에 존재하지 않는다.
xm.xla_device()가 공유된 OpenXLA/PJRT 런타임(TPU라면 libtpu.so, Neuron이라면 libneuronpjrt.so) 덕분에 TPU와 Trainium 모두에 투명하게 대응하므로, "torch_xla가 하드웨어를 추상화해 준다"고 생각하기 쉽다. model.to(device)나 기본 연산에 대해서는 맞는 말이다. 하지만 핵심적인 부분에 대해서는 완전히 거짓이 된다.
forward의 시그니처(Signature)부터가 갈라진다.
# NVIDIA forward: 거친(Ragged) 데이터 + 경계 인덱스. 길이는 매번 임의.
def forward(self, input_ids, cu_seqlens, max_seqlen):
return self.flash_attn_func(input_ids, cu_seqlens, max_seqlen)
...
그리고 이것은 최하층까지 폭포수(Cascade)처럼 이어진다.
| 컴포넌트 | NVIDIA 파이프라인 | Trainium 파이프라인 |
|---|---|---|
| 추론 엔진 | vLLM (CUDA), TensorRT-LLM | NxD / vllm-neuron |
| 커스텀 커널 | Triton, CUDA C++ (FlashAttention) | NKI (Neuron Kernel Interface), 처음부터 다시 작성 |
| 베이스 이미지 | nvcr.io/nvidia/pytorch | AWS Neuron DLC |
| CI 빌드 결과물 | weight + CUDA/Triton 바이너리 | weight + 버킷별 NEFF 정적 바이너리 |
| 배포 대상 | g5 / p5 인스턴스 | trn1 / inf2 인스턴스 |
| 모니터링 | nvidia-smi, DCGM exporter | neuron-top, Neuron exporter |
완전히 병행하는 두 세계다. CUDA 컨테이너도, eval 스크립트도, 오토스케일링(Auto-scaling) 트리거도, 단 하나도 유용할 수 없다. vLLM의 하드웨어 플러그인 메커니즘이 비즈니스 로직 계층에 '얇은 한 겹' 정도의 공통화를 제공하지만, 그 아래의 엔진은 100% 별개의 코드이며 별개의 버그를 가지고 있다.
데이터 타입(Data type)에 관한 이야기도 대칭적이지 않다. BF16(Google 초기 TPU가 제안한 방식)은 양쪽 모두에서 안정적이다. FP32와 동일한 지수 범위를 가지므로, -1e9와 같은 마스크(Mask) 값을 받아도 NaN(Not a Number)이 되지 않는다. 하지만 FP8—현재 처리량(Throughput)의 주력—은 NVIDIA가 유리하다. FP8의 어텐션 스코어(Attention score)는 격렬하게 변동하기 때문에, 클리핑(Clipping)을 방지하기 위해 실행 시점의 **동적 스케일링 (Dynamic Scaling)**이 필요하다. 정적 컴파일러(Static compiler)는 컴파일 시점에 고정된 스케일 계수를 구워 넣을 수밖에 없으므로, TPU/Trainium에서 공격적인 FP8 어텐션을 구동하면 클리핑으로 인해 모델의 지능(Perplexity)이 저하될 리스크가 높아진다. "FP8로 전환하자"는 NVIDIA에서는 한 줄이면 되지만, 정적 실리콘(Static silicon)에서는 연구 프로젝트가 된다.
이 부분이 도입을 가로막는 지점이며, 아무도 슬라이드에 적지 않는다. NVIDIA에는 깔끔한 추상화 경계가 있다.
[ AI 엔지니어 / 데이터 사이언티스트 ]
아키텍처, 하이퍼파라미터, Eval
│
...
데이터 사이언티스트는 메모리 배치를 고민할 필요가 없다. MLOps 엔지니어는 어텐션 수식을 읽을 필요가 없다. 깔끔한 인터페이스를 통해 결과물을 주고받는다.
TPU로 넘어가는 순간, 이 벽은 사라진다. 모델 구조가 물리적 제약과 직결되기 때문이다.
- 패킹(Packing) 방식(MLOps 측)과
forward내의 세그먼트 마스크(Segment mask) 로직(AI 엔지니어 측)은 하나의 설계의 양면이다. 배치를 구성하는 방식을 바꾸면 수식도 동시에 바꿔야 한다. 사양서(Specification)를 기반으로 서로 다른 사람에게 업무를 분담시키는 것은 불가능하다. - AI 엔지니어가 가볍게
if분기를 추가하거나 레이어 수를 변경하면, 컴파일 후의 그래프 토폴로지(Graph topology)가 바뀌어 운영 환경에서 JIT 스톨(JIT stall)이나 OOM(Out of Memory)을 유발한다. 디버깅을 위해서는 XLA HLO 그래프 덤프 분석이 필요하며, 이는 AI 엔지니어를 "인프라" 장애에 휘말리게 만든다. - "BF16 → FP8로 전환하여 처리량 2배 확보" (MLOps)와 "FP8의 정적 스케일링은 특정 태스크에서 할루시네이션(Hallucination)을 일으킨다" (데이터 사이언티스트)가 정면으로 충돌한다. NVIDIA라면 런타임(Runtime)이 이를 조율해 준다. TPU에서는 인간 두 명이 머리를 맞대고 협상할 수밖에 없다.
TPU로 성공하고 있는 조직들—Google의 Gemini 팀, Anthropic의 Claude 팀, Meta의 Llama-on-TPU 부대—은 가로로 나누어진 '데이터 사이언스 부서 / 인프라 부서'라는 분업 체계를 완전히 버렸다. 대신, 어텐션 (Attention) 수식과 컴파일러 (Compiler) 내부 둘 다에 정통한 수직 통합형 원팀 (One-team)을 배치했다. 대부분의 회사는 그런 팀을 편성할 수 없다. 오래된 분업 방식을 유지하려 했던 프로젝트부터 차례대로 컴파일 에러와 OOM (Out of Memory)의 산더미에 파묻혀 죽어간다.
입력 채널을 스스로 지배하여 형상 (Shape)을 예측할 수 있게 되는 순간, 모든 계산이 뒤집힌다. 여기 두 가지 깔끔한 사례가 있다.
Google / YouTube 요약. Google은 영상을 다시 재생하지 않는다. 업로드 시점에 (여유 TPU 리소스로) 비동기 배치 (Asynchronous batch)가 ASR (Automatic Speech Recognition)을 실행하고, 타임스탬프가 포함된 텍스트를 Bigtable 등의 스토리지에 저장한다. 요약을 요청받았을 때, 정확한 텍스트 길이는 이미 토큰 단위로 판명되어 있다—따라서 라우터는 딱 맞는 크기의 버킷 (Bucket)을 선택할 수 있고, 패킹 (Packing)의 낭비는 거의 제로에 가깝다. Gemini Flash와 같은 경량 모델이 사전 생성된 텍스트를 스캔한다. "2시간짜리 영상을 순식간에 요약"하는 마법의 정체는, "몇 달 전에 거의 무료로 만들어 두었던 작은 텍스트 인덱스를 스캔했을 뿐"이다. -
Anthropic / Claude Code. CLI 코딩 에이전트는 입력이 거의 확정되어 있다. 리포지토리 구조, 도구 정의, Git 차이점(diff), 시스템 프롬프트. 컨텍스트의 처음 약 90%가 불변이다—이는 바로 정적 컴파일 (Static compilation)과 프롬프트 캐싱 (Prompt Caching)이 가장 선호하는 형태다. 실제로 Anthropic은 프로덕션 서빙을 Trainium으로 이전 완료했으며 (neuronx-distributed, 실시간 패킹을 수행하는 Go/C++ 프록시 포함), Claude Code는—역설적으로 읽자면—Java형 실리콘을 고생시킨 보람이 있게 만드는 완벽한 "입력 잠금 채널 (Input lock channel)"이다. 긴 문장의 워크로드도 유리하게 작용한다. 200K 토큰의 prefill은 32K 버킷을 거의 제로 패딩 (Zero padding) 없이 채우기 때문에, 정적 어레이 (Static array)의 약점이 Claude의 가장 강력한 영역에서 정확히 사라진다.
역도 마찬가지로 논리적이며, 채팅 UI가 NVIDIA에 머무는 이유를 설명한다. ChatGPT나 Claude.ai의 웹 프런트엔드는 임의의 텍스트를 받아들이며, 갑작스러운 이미지 업로드나 대화 도중의 화제 전환이 발생한다. 전송 버튼이 눌리기 전까지 시스템은 형상을 예측할 수 없다. 그 혼돈이야말로 동적 SIMT + PagedAttention이 만들어진 이유 그 자체다.
TPU가 유행하지 않는 것은 느려서도 비싸서도 아니다—토큰 단가는 오히려 더 저렴하다. 유행하지 않는 이유는, 그 저렴함이 "모든 텐서 형상을 컴파일 시점에 고정한다"라는, 대부분의 팀이 지킬 수 없는 규율을 조건으로 하기 때문이다. -
비용은 사라진 것이 아니라 이동했다. 정적 실리콘은 불확실성을 모두 하드웨어에서 몰아내어 소프트웨어 (패킹, 마스킹, 버킷 라우팅)와 인간 (붕괴된 개발/운영의 경계)에게 떠넘긴다. CapEx (실리콘, 전력)를 OpEx (해킹 레이어를 유지보수하는 탑 엔지니어)로 트레이드(Trade)하고 있는 것이다. -
의사 결정의 규칙은 칩이 아니라 채널에 있다. 입력을 지배하고 있다면—CLI, 고정된 업무 워크플로우, 자사 스토리지 파이프라인—TPU/Trainium은 무기다. 입력이 자유 형식의 채팅창이나 제3자 API 연동이라면, NVIDIA (또는 AMD)가 유일하게 제대로 된 선택지이며, EC2의 카탈로그 가격이 싸다는 이유만으로 TPU에 뛰어드는 것은 MFU (Model Flops Utilization)가 조용히 한 자릿수까지 폭락하는 길이다.
스펙 시트는 토큰 단가에 대해 거짓말을 하지 않았다. 다만, 그전에 고용해야 하는 엔지니어, 분기된 파이프라인, 조직 재설계의 비용이 기재되어 있지 않았을 뿐이다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기