Skip to content
Published on

NPU 완전 해부: 트랜스포머 아키텍처가 실리콘 위에서 어떻게 달리는가

Authors

들어가며: AI가 우리 주머니 속으로

ChatGPT가 처음 나왔을 때, 우리는 클라우드 서버에 있는 수십 개의 GPU에 요청을 보내야 했습니다. 하지만 2025년 현재, 여러분의 스마트폰에서 Llama 3.2 3B 모델이 실시간으로 돌아갑니다. iPhone 16의 Neural Engine이 초당 30개 이상의 토큰을 생성합니다.

이걸 가능하게 한 것이 NPU (Neural Processing Unit) 입니다.

NPU는 단순히 작은 GPU가 아닙니다. 근본적으로 다른 설계 철학을 가진 특화 가속기입니다. 이 글에서는 NPU가 무엇인지, 트랜스포머의 각 연산이 실리콘 위에서 어떻게 돌아가는지, 그리고 왜 "메모리 대역폭이 TFLOPS보다 중요한가"를 완전히 이해하게 됩니다.


1. CPU vs GPU vs NPU: 설계 철학의 삼각형

CPU: 복잡한 일을 빠르게 처리하는 제너럴리스트
┌─────────────────────────────────────────────────────┐
│ 코어 수:    8-128 (빅 코어)│ 클럭:      3-5 GHz (높은 단일 스레드 성능)│ 특기:      복잡한 제어 흐름, 분기 예측              │
│            운영체제, 데이터베이스, 웹 서버           │
│ 캐시:      L1/L2/L3 대형 캐시 계층 (MB 단위)│ 약점:      병렬 연산 시 에너지 비효율               │
└─────────────────────────────────────────────────────┘

GPU: 단순한 일을 엄청나게 많이 동시에
┌─────────────────────────────────────────────────────┐
│ 코어 수:    수천~ (작은 CUDA 코어)│ 클럭:      1-3 GHz (낮지만 대량 병렬)│ 특기:      어떤 병렬 연산이든 (렌더링, AI, 물리)│ 메모리:    GDDR6/HBM, 높은 대역폭                   │
│ 약점:      전력 소비 (300-700W), 범용성의 비용       │
└─────────────────────────────────────────────────────┘

NPU/Neural Engine: AI 연산만, 그것도 극한 효율로
┌─────────────────────────────────────────────────────┐
│ 코어 수:    소수의 특화된 MAC 배열                   │
│ 특기:      정수 행렬 곱셈 (INT8/INT4), 양자화 추론  │
│ 에너지 효율: GPU10-100배                         │
│ 전력:      1-10W (스마트폰/노트북)│ 약점:      범용 연산 불가, FP32 학습 지원 안 함      │
└─────────────────────────────────────────────────────┘

NPU가 필요한가?
- 스마트폰 AI: 배터리 5000mAh → GPULLM 돌리면 30분도 못 버팀
- NPU: 같은 연산, 1/50의 전력
- "항상 켜져 있는 AI": 사진 분류, 음성 감지 등 상시 실행

실제 에너지 효율 비교:

# 추론 에너지 효율 계산 (가상 7B 모델, INT8, 배치=1)
perf_data = {
    'NVIDIA H100 SXM':     {'tops': 3958, 'tdp_w': 700},
    'NVIDIA A100 40GB':    {'tops': 1248, 'tdp_w': 400},
    'Apple M4 Neural Eng': {'tops': 38,   'tdp_w': 4},    # Neural Engine만
    'Qualcomm Hexagon NPU':{'tops': 45,   'tdp_w': 5},
    'Intel Meteor Lake NPU':{'tops': 10,  'tdp_w': 8},
}

for name, data in perf_data.items():
    efficiency = data['tops'] / data['tdp_w']
    print(f"{name:<28} {efficiency:>8.1f} TOPS/W")

# NVIDIA H100 SXM:           5.7 TOPS/W
# Apple M4 Neural Engine:    9.5 TOPS/W (NPU만, 배터리 미포함 TDP 기준)
# Qualcomm Hexagon:          9.0 TOPS/W
# Intel Meteor Lake NPU:     1.25 TOPS/W
# (모바일 NPU는 절대 성능은 낮지만 와트당 효율 경쟁력 있음)

2. Apple Neural Engine (ANE) 완전 해부

Apple의 Neural Engine은 가장 잘 알려진 NPU 중 하나입니다. 2017년 A11 Bionic에 처음 도입되어 꾸준히 발전했습니다.

Apple Neural Engine 세대별 발전:

A11 Bionic (2017): Neural Engine 1세대
- 코어: 2 (추론 전용)
- 성능: 0.6 TOPS
- 용도: Face ID, Animoji

A12 Bionic (2018): 8코어로 확장
- 성능: 5 TOPS
- Siri 음성 인식 온디바이스 처리

A15 Bionic (2021): 16코어
- 성능: 15.8 TOPS
- 온디바이스 번역, 사진 분류 실시간

A17 Pro (2023): 16코어, 3nm
- 성능: 35 TOPS (INT8 기준)
- Llama 3.2 3B 온디바이스 실행 가능

M4 (2024): 16코어 Neural Engine
- 성능: 38 TOPS
- Context length 8K까지 로컬 처리

ANE 하드웨어 구조

Apple Neural Engine 내부 구조 (추정, 공개 특허 기반):

┌─────────────────────────────────────────────────────────────┐
Neural Engine Die Area├─────────────────────────────────────────────────────────────┤
Command Queue│  ┌──────────────────────────────────────────────────────┐  │
│  │         16Execution Units (코어)                  │  │
│  │  [MAC Array][MAC Array] ... [MAC Array] × 16         │  │
│  │  각 EU: 행렬 곱셈 + 활성화 함수 + Layer Norm 지원    │  │
│  └──────────────────────────────────────────────────────┘  │
├─────────────────────────────────────────────────────────────┤
Dedicated SRAM (L1 캐시): ~30 MB  (GPU와 공유 안 함 → 캐시 오염 없음)├─────────────────────────────────────────────────────────────┤
DMA Engine (메모리 이동 전용 하드웨어)Memory: Unified Memory (CPU/GPU/ANE 공유 물리 메모리)└─────────────────────────────────────────────────────────────┘

중요한 제약:
- CoreML 통해서만 프로그래밍 가능 (직접 접근 API 없음)
- INT8, INT16지원 (FP32 학습 불가)
- 배치 크기 제한 있음 (대형 배치 비효율적)
- 특정 연산자 지원 안 할 경우 CPU로 폴백

CoreML로 ANE 활용하기

# PyTorch 모델을 CoreML로 변환 → Apple Neural Engine에서 실행
import torch
import coremltools as ct

# 예시: 작은 트랜스포머 모델
model = MyTransformerModel()
model.eval()

# 더미 입력으로 tracing
example_input = torch.randint(0, 32000, (1, 512))
traced_model = torch.jit.trace(model, example_input)

# CoreML 변환 (INT8 양자화 포함)
mlmodel = ct.convert(
    traced_model,
    inputs=[ct.TensorType(name='input_ids', shape=(1, 512))],
    compute_precision=ct.precision.FLOAT16,  # FP16으로 변환
    compute_units=ct.ComputeUnit.ALL  # ANE + GPU + CPU 자동 선택
)

# 모델 저장
mlmodel.save('my_transformer.mlpackage')

# 실행 시: CoreML이 자동으로 ANE/GPU/CPU 최적 배분
# ANE에서 실행되면: 초저전력 + 빠른 추론
# ANE 지원 안 되면: GPU 자동 폴백

3. 트랜스포머의 모든 연산을 하드웨어로 매핑하기

트랜스포머 forward pass의 각 단계가 어떤 하드웨어에서 어떻게 실행되는지 완전히 추적해봅시다.

Transformer Forward Pass 하드웨어 매핑:
┌──────────────────────────────────────────────────────────────┐
Input Token IDs [batch, seq_len]│  ↓ 임베딩 룩업 (Gather 연산)│  하드웨어: NPU SRAM에 임베딩 테이블 캐시 → 거의 무료         │
├──────────────────────────────────────────────────────────────┤
Layer Norm [batch, seq, d_model][batch, seq, d_model]│  ↓ MeanVarianceNormalizeScaleBias│  하드웨어: NPU 벡터 연산 유닛 (XLA가 단일 커널로 융합)│  연산 비중: ~1-2% (매우 작음)├──────────────────────────────────────────────────────────────┤
Q, K, V 투영 (Linear Layer × 3)[batch, seq, d_model] × [d_model, d_head] = GEMM│  하드웨어: Systolic Array / MAC 배열 (전력의 40%+)│  연산 비중: ~38% (지배적!)├──────────────────────────────────────────────────────────────┤
Attention Score: Q × K^T / sqrt(d)[batch, heads, seq, d_h] × [batch, heads, d_h, seq]│  하드웨어: GPU Tensor Core / NPU MAC (O(n² × d) 복잡도)│  연산 비중: ~12% (시퀀스 길이에 제곱 비례!)├──────────────────────────────────────────────────────────────┤
Softmax: exp → sum → divide                                  │
│  하드웨어: NPU 벡터 연산 (exp은 특수 함수 하드웨어)│  연산 비중: ~1% (하지만 메모리 집약적)├──────────────────────────────────────────────────────────────┤
Attention × V (GEMM)[batch, heads, seq, seq] × [batch, heads, seq, d_h]│  하드웨어: MAC 배열                                          │
│  연산 비중: ~12%├──────────────────────────────────────────────────────────────┤
Output Projection + FFN Layer 1 + FFN Layer 2 (GEMM × 3)│  하드웨어: MAC 배열 (가장 큰 행렬: d_model × 4×d_model)│  연산 비중: ~37%└──────────────────────────────────────────────────────────────┘

결론:87%GEMMNPU/Systolic Array에서 처리
      나머지 13%는 벡터 연산 (LayerNorm, Softmax, GELU)

Flash Attention: Attention의 메모리 문제 해결

# 표준 Attention vs Flash Attention 메모리 비교
def analyze_attention_memory(seq_len, d_model, n_heads, batch_size=1):
    d_head = d_model // n_heads
    dtype_bytes = 2  # FP16

    # 표준 Attention: O(n²) 메모리
    # Attention Score 행렬: [batch, heads, seq, seq]
    attn_score_gb = (batch_size * n_heads * seq_len * seq_len *
                     dtype_bytes) / (1024**3)

    # Flash Attention: O(n) 메모리
    # 타일별 처리, 전체 score 행렬 저장 안 함
    flash_extra_gb = (batch_size * n_heads * seq_len *
                      d_head * dtype_bytes) / (1024**3)

    print(f"시퀀스 길이: {seq_len}")
    print(f"Standard Attention score 행렬: {attn_score_gb:.2f} GB")
    print(f"Flash Attention 추가 메모리: {flash_extra_gb:.4f} GB")
    print(f"메모리 절약: {attn_score_gb/flash_extra_gb:.0f}배")

# GPT-4 규모 (추정): seq=8192, d=12288, heads=96
analyze_attention_memory(8192, 12288, 96)
# Standard: ~49.2 GB (단일 레이어!)
# Flash Attention: ~0.75 GB
# 65배 메모리 절약!

# Flash Attention이 NPU에서 특히 중요한 이유:
# NPU는 SRAM이 작음 (보통 30-100 MB)
# 표준 Attention은 수십 GB SRAM 필요 → 불가능
# Flash Attention의 타일 연산 = 작은 SRAM으로도 긴 시퀀스 처리 가능

4. 왜 LLM 추론은 메모리 바운드인가: 루프라인 모델

이것이 LLM 추론을 이해하는 가장 중요한 개념입니다. 많은 엔지니어들이 "더 많은 TFLOPS = 더 빠른 LLM"이라고 생각하지만, 이건 틀렸습니다.

루프라인 모델로 분석하기

# LLM 추론 병목 분석 (루프라인 모델)

# === 모델 설정 ===
model_name = "Llama 2 7B"
num_params = 7_000_000_000    # 70억 파라미터
bytes_per_param = 2           # FP16 = 2 bytes
model_size_bytes = num_params * bytes_per_param  # 14 GB

# === 하드웨어 설정 ===
hardware_configs = {
    'H100 SXM': {
        'memory_bw_tbs': 3.35,     # TB/s (3,350 GB/s)
        'compute_tflops': 1979,     # TFLOPS FP16
    },
    'A100 80GB': {
        'memory_bw_tbs': 2.0,
        'compute_tflops': 312,
    },
    'Apple M3 Max (GPU)': {
        'memory_bw_tbs': 0.3,      # 300 GB/s 통합 메모리
        'compute_tflops': 14.2,
    }
}

# === 분석 ===
for hw_name, hw in hardware_configs.items():
    # 토큰당 필요 연산
    # (각 토큰 생성 시 모든 가중치를 메모리에서 읽어야 함)
    bytes_per_token = model_size_bytes  # 14 GB/token

    # 메모리 대역폭이 지배하는 시간
    bw_gb_per_s = hw['memory_bw_tbs'] * 1000  # TB -> GB
    mem_time_sec = bytes_per_token / (bw_gb_per_s * 1e9)

    # 실제 연산 시간 (FLOPS/token ÷ 가용 FLOPS)
    flops_per_token = 2 * num_params  # rough: 2 × params
    compute_time_sec = flops_per_token / (hw['compute_tflops'] * 1e12)

    # 실제 병목은 두 값 중 큰 쪽
    bottleneck_time = max(mem_time_sec, compute_time_sec)
    tokens_per_sec = 1.0 / bottleneck_time

    # Arithmetic Intensity (연산 집약도)
    ai = flops_per_token / bytes_per_token  # FLOP/byte
    hw_ridge_point = hw['compute_tflops'] * 1e12 / (bw_gb_per_s * 1e9)

    bottleneck = "메모리 바운드" if ai < hw_ridge_point else "연산 바운드"

    print(f"\n=== {hw_name} ===")
    print(f"  메모리 시간: {mem_time_sec*1000:.1f} ms/token")
    print(f"  연산 시간:   {compute_time_sec*1000:.4f} ms/token")
    print(f"  병목:        {bottleneck}")
    print(f"  예상 처리량: ~{tokens_per_sec:.0f} tokens/sec")

# 출력 (배치 크기=1):
# H100: 메모리 4.2ms vs 연산 0.007ms → 메모리 바운드 → ~240 tok/s
# A100: 메모리 7.0ms vs 연산 0.045ms → 메모리 바운드 → ~143 tok/s
# M3 Max: 메모리 46.7ms vs 연산 0.98ms → 메모리 바운드 → ~21 tok/s
# 결론: 배치 크기 1에서 모든 하드웨어가 메모리 바운드!

메모리 바운드가 의미하는 것

메모리 바운드의 의미:

1. TFLOPS2배 높여도 속도는 안 변함
   (메모리 대역폭이 병목이므로)

2. 메모리 대역폭을 2배 높이면 정확히 2배 빨라짐

3. 모델 크기를 2줄이면 (양자화) 정확히 2배 빨라짐
   INT8INT4: 모델 크기 절반 → 처리량 2
4. 배치 크기를 키우면 연산 바운드로 전환 가능
   (같은 가중치로 여러 입력 처리 = 가중치 재사용 증가)
   배치=64이면: 대부분의 GPU에서 연산 바운드로 전환

5. Apple Silicon의 강점:
   - 546 GB/s 통합 메모리 (M3 Ultra)
   - GPU, CPU, Neural Engine이 같은 물리 메모리 공유
   - 32GB/64GB/128GB 용량: 큰 모델도 로컬 실행 가능

5. KV Cache: Transformer의 핵심 메모리 최적화

KV Cache 없이는 긴 시퀀스 생성이 실용적으로 불가능합니다.

# KV Cache의 원리와 메모리 계산

def analyze_kv_cache(model_name, context_len, n_layers, n_heads,
                     head_dim, batch_size=1, dtype_bytes=2):
    """KV Cache 메모리 사용량 계산"""

    # KV Cache 크기: [seq, layers, 2(K+V), heads, head_dim]
    kv_cache_bytes = (context_len * n_layers * 2 *
                      n_heads * head_dim * batch_size * dtype_bytes)
    kv_cache_gb = kv_cache_bytes / (1024**3)

    # 토큰당 KV 읽기량 (메모리 대역폭 관점)
    bytes_per_token_kv = (n_layers * 2 * n_heads * head_dim * dtype_bytes)
    bytes_per_token_kv_gb = bytes_per_token_kv / (1024**3)

    print(f"\n=== {model_name} ===")
    print(f"  컨텍스트 길이: {context_len:,} 토큰")
    print(f"  KV Cache 크기: {kv_cache_gb:.2f} GB")
    print(f"  토큰당 KV 읽기: {bytes_per_token_kv/1024:.1f} KB")
    print(f"  현재 문맥에 추가: 현재 Key/Value 벡터만 계산 (이전 캐시 재사용)")

# Llama 2 7B 설정
analyze_kv_cache("Llama 2 7B",
                 context_len=4096, n_layers=32,
                 n_heads=32, head_dim=128)
# KV Cache: 4K 컨텍스트 = ~2.0 GB

analyze_kv_cache("Llama 3.1 70B",
                 context_len=128_000, n_layers=80,
                 n_heads=64, head_dim=128)
# KV Cache: 128K 컨텍스트 = ~160 GB (!!)

# 실제 KV Cache vs 모델 가중치 메모리 분포
model_weights_gb = 14  # 7B FP16
kv_4k_gb = 2.0         # 4K context
kv_128k_gb = 64.0      # 128K context

print(f"\n메모리 분포 (Llama 2 7B, batch=1):")
print(f"  모델 가중치:    {model_weights_gb} GB (고정)")
print(f"  KV Cache 4K:   {kv_4k_gb} GB")
print(f"  KV Cache 128K: {kv_128k_gb} GB (모델의 {kv_128k_gb/model_weights_gb:.1f}배!)")

NPU에서의 KV Cache 최적화

# NPU 친화적 KV Cache 구현 전략

# 전략 1: Paged Attention (vLLM)
# KV Cache를 페이지 단위로 관리 → 동적 할당 가능
# GPU VRAM처럼 NPU SRAM을 관리

class PagedKVCache:
    def __init__(self, block_size=16, n_blocks=256):
        self.block_size = block_size
        self.n_blocks = n_blocks
        # 각 블록: [block_size, n_heads, head_dim] × 2 (K, V)
        self.blocks = {}

    def allocate_block(self, seq_id):
        """시퀀스에 새 블록 할당"""
        block_id = len(self.blocks)
        self.blocks[block_id] = {
            'seq_id': seq_id,
            'tokens': 0,
            'data': None  # 실제로는 텐서
        }
        return block_id

# 전략 2: GQA (Grouped Query Attention)
# Llama 3, Mistral 등에서 사용
# K, V 헤드 수 줄임 → KV Cache 크기 대폭 감소
def compute_gqa_savings(n_kv_heads_mha, n_kv_heads_gqa, n_layers,
                        context_len, head_dim, dtype_bytes=2):
    mha_size = n_kv_heads_mha * 2 * n_layers * context_len * head_dim * dtype_bytes
    gqa_size = n_kv_heads_gqa * 2 * n_layers * context_len * head_dim * dtype_bytes
    reduction = (1 - gqa_size/mha_size) * 100
    print(f"GQA KV Cache 절감: {reduction:.0f}%")

# Llama 3.1 8B: MHA 32헤드 → GQA 8헤드
compute_gqa_savings(32, 8, n_layers=32,
                    context_len=4096, head_dim=128)
# GQA KV Cache 절감: 75%!

6. 양자화가 NPU를 어떻게 도와주는가

양자화는 NPU에서 LLM을 실행 가능하게 만드는 핵심 기술입니다.

수 형식과 하드웨어 지원:

FP32: [1 부호][8 지수][23 가수] = 32비트
      모든 CPU/GPU 지원, 학습의 표준
      메모리: 7B 모델 = 28 GB

FP16: [1 부호][5 지수][10 가수] = 16비트
      GPU/NPU 지원, 추론 표준
      메모리: 7B 모델 = 14 GB

INT8: [1 부호][7 크기값]         = 8비트  ← NPU의 기본
      모든 NPU에서 지원, 4배 빠른 SIMD
      메모리: 7B 모델 = 7 GB
      정확도 손실: 보통 < 0.5%

INT4: 4비트                       ← 최신 NPU (A17 Pro, Hexagon)
      8비트의 2배 처리량
      메모리: 7B 모델 = 3.5 GB
      정확도 손실: 보통 1-3% (GPTQ 사용 시)

INT2/Binary: 실험적, 일부 특화 NPU
      메모리 절약 극대화, 성능 손실 큼

NPU에서 INT8 SIMD의 이점:
- 32비트 레지스터에 INT84개 저장 → 4배 처리량
- 실제: INT8 행렬 곱셈이 FP32보다 4-8배 빠름

실제 양자화 구현

# LLM.int8()을 사용한 포스트 트레이닝 양자화
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
import torch

# INT8 양자화 (LLM.int8() 알고리즘)
quantization_config_int8 = BitsAndBytesConfig(
    load_in_8bit=True,
    llm_int8_threshold=6.0,       # 이상치 처리 임계값
    llm_int8_has_fp16_weight=False
)

model_int8 = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.1-8B",
    quantization_config=quantization_config_int8,
    device_map='auto'
)
# FP16: 16 GB → INT8: 8 GB, 정확도 손실 ~0.3%

# INT4 양자화 (GPTQ 알고리즘)
quantization_config_int4 = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type='nf4',         # NF4: 정규분포에 최적화된 4비트
    bnb_4bit_use_double_quant=True,    # 양자화 상수 자체도 양자화
    bnb_4bit_compute_dtype=torch.bfloat16
)

model_int4 = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.1-8B",
    quantization_config=quantization_config_int4,
    device_map='auto'
)
# FP16: 16 GB → INT4 NF4: 4 GB, 정확도 손실 ~1.2%

# Apple CoreML용 INT8 양자화
import coremltools as ct
from coremltools.optimize.coreml import (
    PostTrainingQuantizer, OptimizationConfig, OpLinearQuantizerConfig
)

config = OptimizationConfig(
    global_config=OpLinearQuantizerConfig(
        mode='linear_symmetric',  # 대칭 양자화
        dtype='int8',
        granularity='per_channel'  # 채널별 스케일 (정확도 향상)
    )
)

# 이미 변환된 CoreML 모델에 적용
quantizer = PostTrainingQuantizer(mlmodel, config)
quantized_model = quantizer.compress()
# 모델 크기: 절반, ANE 처리량: 2배

양자화 캘리브레이션의 중요성

# 양자화 품질을 결정하는 캘리브레이션
from datasets import load_dataset
import torch

def calibrate_quantization(model, tokenizer, n_samples=128):
    """
    캘리브레이션: 실제 데이터로 양자화 스케일 결정
    이 과정이 정확도 유지의 핵심!
    """
    dataset = load_dataset("wikitext", "wikitext-2-raw-v1", split="train")
    calibration_texts = dataset['text'][:n_samples]

    model.eval()
    activation_ranges = {}

    # 각 레이어의 활성화 범위 수집
    def hook_fn(name):
        def hook(module, input, output):
            if name not in activation_ranges:
                activation_ranges[name] = {'min': float('inf'), 'max': float('-inf')}
            activation_ranges[name]['min'] = min(
                activation_ranges[name]['min'], output.min().item()
            )
            activation_ranges[name]['max'] = max(
                activation_ranges[name]['max'], output.max().item()
            )
        return hook

    # 훅 등록
    hooks = []
    for name, module in model.named_modules():
        if isinstance(module, torch.nn.Linear):
            hooks.append(module.register_forward_hook(hook_fn(name)))

    # 캘리브레이션 실행
    with torch.no_grad():
        for text in calibration_texts:
            inputs = tokenizer(text, return_tensors='pt', max_length=512,
                              truncation=True)
            model(**inputs)

    # 훅 제거
    for hook in hooks:
        hook.remove()

    # 수집된 범위로 양자화 스케일 계산
    quantization_scales = {}
    for name, ranges in activation_ranges.items():
        abs_max = max(abs(ranges['min']), abs(ranges['max']))
        quantization_scales[name] = abs_max / 127.0  # INT8 최대값

    return quantization_scales

7. Qualcomm Hexagon NPU와 Intel NPU

Qualcomm Snapdragon X Elite: Hexagon NPU

Qualcomm Hexagon NPU (Snapdragon X Elite, 2024):

성능: 45 TOPS (INT8)
아키텍처:
- HTA (Hexagon Tensor Accelerator)
- HMNN (Hexagon Multi-Network Node): 여러 AI 워크로드 동시 실행
- 전용 Vector DSP + Scalar DSP

지원 데이터 형식: INT4, INT8, FP16
온칩 SRAM: ~3 MB (빠른 캐시)

지원하는 LLM 실행:
- Llama 3.2 3B: 30+ tok/s 로컬 실행
- Phi-3.5 mini 3.8B: 25+ tok/s
- Gemma 2 2B: 35+ tok/s

프로그래밍:
- Qualcomm AI Engine Direct SDK
- ONNX Runtime + QNN 백엔드
- llama.cpp의 Hexagon 백엔드

Windows Copilot Plus PC에서의 활용:
- 실시간 자동 캡션 (Live Captions)
- Cocreator (AI 이미지 생성)
- 스마트 스냅숏
모두 NPU에서 실행 → 배터리 소모 최소화

Intel Meteor Lake Neural Processing Unit

Intel NPU (Meteor Lake / Core Ultra, 2023):

성능: 10 TOPS (INT8)
아키텍처:
- NN 연산자 가속기: MAC 배열
- 슬라이스 아키텍처: 독립적인 처리 타일
- 전용 메모리 컨트롤러

특징:
- 항상 켜져 있는 AI 처리
- 백그라운드 AI 작업에 특화
- 실시간 노이즈 캔슬링, 눈 감지 등

OpenVINO로 활용:
from openvino.runtime import Core
ie = Core()
compiled_model = ie.compile_model(onnx_model, device_name="NPU")
output = compiled_model({"input": input_data})

주요 사용 사례:
- Windows Studio Effects (배경 흐리기, 눈 맞춤)
- 음성 인식 전처리
- 실시간 번역 (소형 모델)
- 이미지 향상 (사진 자동 보정)

8. 기기별 LLM 실행 가능 모델 크기

2025년 기준, 실용적인 온디바이스 LLM 실행:

iPhone 16 (8GB RAM, A18 Pro, 35 TOPS ANE):
├── 가능: Llama 3.2 3B INT4 (~2GB, ~25 tok/s)
├── 가능: Phi-3 mini 3.8B INT4 (~2.3GB, ~20 tok/s)
├── 불가능: Llama 3.1 8B FP16 (16GB 필요)
└── 가능: Llama 3.1 8B INT4 (~5GB, ~12 tok/s)

MacBook Air M3 (16GB RAM, 38 TOPS ANE):
├── 가능: Llama 3.1 8B Q4 (~5GB, ~40 tok/s)
├── 가능: Mistral 7B Q4 (~4.5GB, ~45 tok/s)
├── 가능: Llama 3.1 70B Q4 (~40GB - M3 Max 64GB 필요)
└── 불가능: 70B FP16 (140GB 필요)

M3 Ultra (192GB RAM, 800 GB/s):
├── 가능: Llama 3.1 70B FP16 (~140GB, ~35 tok/s)
├── 가능: Llama 3.1 405B Q4 (~230GB, ~8 tok/s)
└── 가능: Claude Sonnet급 모델 로컬 실행

Snapdragon X Elite PC (32GB RAM):
├── 가능: Llama 3.2 3B (~30 tok/s on NPU)
├── 가능: Phi-3.5 mini (~25 tok/s on NPU)
├── 가능: Llama 3.1 8B Q4 (~15 tok/s)
└── 불가능: 70B 이상 (메모리 부족)

9. 미래: LLM 칩 전쟁

범용 GPU만이 아니라, 전용 추론 칩들이 맹렬히 추격하고 있습니다.

전용 LLM 추론 칩의 현재:

1. Groq LPU (Language Processing Unit)
   아키텍처: 결정론적 데이터플로우 (컴파일 타임에 모든 실행 계획)
   강점: 매우 낮은 지연시간, 예측 가능한 성능
   실측: Llama 2 70B에서 500 tok/s (H1004!)
   이유: 메모리 접근 패턴이 완전히 정적 → 파이프라인 완벽 활용
   약점: 특정 모델 아키텍처만 지원, 유연성 낮음

2. Cerebras WSE-3 (Wafer Scale Engine)
   칩 크기: 웨이퍼 전체 (46,225 mm²)
   AI 코어: 900,000   온칩 SRAM: 900 MB (집적 밀도 극대화)
   강점: 모델 전체를 칩 위에 올림 → HBM 접근 없음!
   약점: 1대 가격 수백만 달러

3. SambaNova Reconfigurable Dataflow Architecture (RDA)
   아키텍처: FPGA처럼 재구성 가능한 데이터플로우
   강점: 다양한 모델에 최적화 가능
   고객: 정부 기관, 대형 연구소

4. Etched Sohu
   아키텍처: 트랜스포머만을 위한 하드와이어드 칩
   특징: 트랜스포머 외 연산 불가 (그래서 매우 효율적)
   예상 성능: H100 대비 20배 효율

왜 전용 칩이 범용 GPU를 이길 수 있는가:
- GPU: 범용성의 오버헤드 (스케줄러, 레지스터 파일, 복잡한 캐시)
- 전용 칩: 알려진 워크로드에 맞게 하드웨어 자체가 최적화
- 트랜스포머 추론은 결정론적 → 컴파일 타임 최적화 극대화

미래 NPU의 방향

# 2026-2030 NPU 발전 예측

future_npu_trends = {
    '2026': [
        '모든 스마트폰에 30+ TOPS NPU 기본 탑재',
        '온디바이스 10B 모델 실시간 추론',
        'Apple ANE: 100+ TOPS 목표',
        'FP8 지원으로 정밀도/속도 균형',
    ],
    '2027-2028': [
        '3D 패키징: NPU + HBM 칩 적층',
        'Analog In-Memory Computing 실험적 도입',
        '30B 모델 스마트폰에서 실용적 추론',
        'PIM (Processing In Memory): 메모리 칩 내부에서 연산',
    ],
    '2029-2030': [
        '광 연산 (Photonic Computing) 실험적 NPU',
        '100B 모델 온디바이스 가능성',
        '에너지 효율: 현재 대비 10-50배 개선',
        'Neuromorphic 요소 도입 (스파이킹 뉴런)',
    ]
}

10. 실전: llama.cpp로 온디바이스 LLM 실행

# llama.cpp 설치 및 빌드 (Apple Silicon NPU 지원)
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp

# Metal (Apple GPU/ANE) 지원으로 빌드
cmake -B build -DLLAMA_METAL=ON
cmake --build build -j $(nproc)

# 모델 다운로드 (GGUF 형식, 이미 양자화됨)
# Llama 3.1 8B Q4_K_M: ~5.0 GB
huggingface-cli download \
  bartowski/Meta-Llama-3.1-8B-Instruct-GGUF \
  Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf \
  --local-dir ./models

# 실행 (Metal GPU 사용)
./build/bin/llama-cli \
  -m ./models/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf \
  -n 512 \
  --n-gpu-layers 35 \  # GPU/ANE로 오프로드할 레이어 수
  -p "트랜스포머 아키텍처의 핵심 혁신을 설명해줘"

# Apple M3 Max 16코어 예상 결과:
# - 로드 시간: ~3초
# - 처리량: ~45 tok/s (GPU 오프로드)
# - 메모리: ~5.5 GB
# Python에서 llama.cpp 사용 (llama-cpp-python 패키지)
from llama_cpp import Llama

# 모델 로드 (GPU 오프로드 포함)
llm = Llama(
    model_path="./models/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf",
    n_gpu_layers=35,    # GPU로 오프로드할 레이어 수
    n_ctx=4096,         # 컨텍스트 길이
    n_threads=8,        # CPU 스레드 수
    verbose=False
)

# 추론 실행
response = llm.create_chat_completion(
    messages=[
        {"role": "system", "content": "당신은 AI 하드웨어 전문가입니다."},
        {"role": "user", "content": "NPU와 GPU의 차이를 설명해주세요."}
    ],
    max_tokens=512,
    temperature=0.7
)

print(response['choices'][0]['message']['content'])
print(f"\n처리량: {response['usage']['completion_tokens'] / response['usage']['total_time']:.1f} tok/s")

# 성능 측정
import time

def benchmark_inference(llm, prompt, n_tokens=100):
    start = time.time()
    output = llm(prompt, max_tokens=n_tokens, echo=False)
    elapsed = time.time() - start
    tokens = output['usage']['completion_tokens']
    return tokens / elapsed

tok_per_sec = benchmark_inference(llm, "AI 칩 역사를 설명해줘", n_tokens=200)
print(f"벤치마크: {tok_per_sec:.1f} tokens/second")

마치며

NPU는 "AI를 모두의 손에"라는 비전을 실현하는 하드웨어 혁명입니다.

핵심 교훈들:

  1. 전력이 제약이다: 스마트폰에서 AI를 실행하려면 GPU의 1/50 전력이 필요 → NPU가 답
  2. LLM 추론은 메모리 바운드: TFLOPS보다 메모리 대역폭이 중요 → 양자화와 메모리 최적화가 핵심
  3. 양자화는 마법이 아니다: 철저한 캘리브레이션이 정확도 유지의 관건
  4. KV Cache가 긴 문맥의 열쇠: 효율적인 KV Cache 없이는 1000 토큰 이상 생성 불가
  5. 전용 칩의 시대가 온다: Groq, Cerebras, Etched 등 순수 LLM 추론 칩들이 GPU를 위협

하드웨어와 소프트웨어가 공진화하는 AI 시대에, TPU와 NPU를 이해하는 것은 단순한 호기심을 넘어 경쟁력 있는 AI 엔지니어가 되기 위한 필수 지식입니다.


참고 자료

  • Apple Neural Engine 특허 문서 (US Patent Office)
  • Qualcomm AI Engine Direct SDK 문서
  • "FlashAttention: Fast and Memory-Efficient Exact Attention" (Dao et al., 2022)
  • "GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers" (2023)
  • "Roofline: An Insightful Visual Performance Model" (Williams et al., 2009)
  • llama.cpp GitHub: github.com/ggerganov/llama.cpp
  • Groq LPU 기술 백서 (groq.com)