MacBook 의 GPU 위에서 어떻게 ML 코드를 짜고 디버깅할 것인가 — Nikita Shulga 가 PyTorch 의 MPS backend 를 만든 사람의 시점에서 깐 Metal compute pipeline, MPS · Metal Performance Shaders 의 이름 정리, unified memory 의 의미, Apple Silicon 의 GPU 모델의 학습 노트. CUDA 시점에서 매핑되는 자리를 명시적으로 짚으면서 — 그리고 디버깅 도구가 어디까지 있는지 정직하게 말하면서.
강의의 출발점은 — Apple Silicon 의 GPU 가 사실 매우 강력한데(M2 Max 의 GPU 는 이론적 13.6 TFLOPS FP32) 대부분의 ML tutorial 이 이 자리를 가르치지 않는다. Nikita 의 입장은 명시적으로 “agnostic 하게 GPU 를 쓰는 길의 일부”.
강의가 답하려는 두 줄 —
device="mps" 가 무엇이고 어떤 길로 도는가.강의의 frame 은 정직하다 — “나도 advanced engineer 가 아니다, 그저 코드를 쓰는 사람이다”(Nikita 의 자기소개). 그래서 강의의 톤이 입문자 시점의 정직한 재구성. CUDA 와 mapping 되는 자리는 명시적으로 짚되, “여기서는 cudaProfiler 같은 게 없습니다” 같이 갭도 가린 적 없다.
Apple Silicon 의 매력 — unified memory architecture. CPU 와 GPU 가 같은 RAM 을 공유한다. 64 GB / 128 GB 의 unified memory 가 — NVIDIA 의 80 GB H100 같은 자리에 — 훨씬 싼 가격에 같이 산다. “VRAM 80 GB 인 GPU 를 가질 수 없는 학생/연구자가 LLM inference 를 직접 돌리는 첫 번째 길” 이 Apple 위에 있다는 게 강의의 큰 메시지.
GPU core, thread group, SIMD groupM1 부터 M3 까지의 GPU 는 NVIDIA 와는 다른 단위 분해를 쓴다. 강의에서 Nikita 가 깐 매핑 — 이름은 다르지만 같은 자리에서 같은 일을 한다.
M2 / M3 chip 의 한 단계 더 —
M2 Ultra 의 메모리 대역폭 800 GB/s 는 — A100 의 1.5–2 TB/s 의 절반. 그런데 unified memory 라 CPU↔GPU 복사 비용이 0. memory-bound 워크로드(LLM inference 의 prefill, KV cache 처리)에서는 PCIe 왕복이 없어서 NVIDIA 와의 비교가 미묘해진다.
Metal 은 graphics + compute 를 한 API 로 묶은 Apple 의 native GPU 추상. 강의에서 Nikita 가 한 화면씩 보여준 객체들 — 각 자리에서 무엇이 일어나는지.
MTLCreateSystemDefaultDevice(). CUDA 의 device handle 의 자리. M-series 에서는 보통 1개 (integrated GPU).
≡ cudaDevice
device.makeCommandQueue(). 비동기 작업의 channel. 한 device 위에 여러 queue.
≡ cudaStream
device.makeBuffer(length:options:). storageModeShared 면 CPU↔GPU 가 같은 메모리 (unified). storageModePrivate 면 GPU 전용. storageModeManaged 는 macOS Intel 시절의 이중복사 (M-series 에서는 deprecated).
≡ cudaMalloc 변종
queue.makeCommandBuffer(). 한 dispatch 의 묶음. 여러 encoder 가 한 buffer 에 들어감.
≡ cudaGraph 의 single instance
cb.makeComputeCommandEncoder(). setComputePipelineState(컴파일된 셰이더), setBuffer(인자 binding), dispatchThreadgroups(launch).
≡ kernel launch
kernel<<<grid,block>>>(args))이 — Metal 에서는 (encoder 생성 → pipeline 설정 → buffer binding → dispatch → encoder.endEncoding → buffer.commit) 의 6 단계로 분해된다. boilerplate 가 더 많지만 explicit.// Metal compute — vector add 의 Objective-C++ (간략)
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
id<MTLCommandQueue> queue = [device newCommandQueue];
// 1. shader 컴파일 — 문자열에서 jit
NSString* src = @"kernel void add(device float* a, ...) { ... }";
id<MTLLibrary> lib = [device newLibraryWithSource:src options:nil error:&err];
id<MTLFunction> fn = [lib newFunctionWithName:@"add"];
id<MTLComputePipelineState> pipe = [device newComputePipelineStateWithFunction:fn error:&err];
// 2. buffer — unified memory 면 CPU 에서 직접 fill 가능
id<MTLBuffer> bufA = [device newBufferWithLength:N*sizeof(float) options:MTLResourceStorageModeShared];
// 3. encode + dispatch
id<MTLCommandBuffer> cb = [queue commandBuffer];
id<MTLComputeCommandEncoder> enc = [cb computeCommandEncoder];
[enc setComputePipelineState:pipe];
[enc setBuffer:bufA offset:0 atIndex:0];
[enc dispatchThreadgroups:MTLSizeMake(N/256,1,1)
threadsPerThreadgroup:MTLSizeMake(256,1,1)];
[enc endEncoding];
[cb commit];
[cb waitUntilCompleted]; // ≡ cudaDeviceSynchronize
Metal Shading Language 는 — C++14 의 부분집합. CUDA C++ 와 매우 비슷한 syntax. kernel, device, threadgroup, thread address space attribute. thread_position_in_grid, threadgroup_position_in_grid 같은 builtin. 학습 곡선이 매우 짧다는 게 강의의 작은 메시지.
PyTorch 는 1.12 부터 MPS backend 를 가진다. 이름의 미묘한 점 — MPS 는 사실 Metal Performance Shaders(Apple 의 vendor 라이브러리)이지만, PyTorch 의 device="mps" 안에는 Metal 직접 호출과 MPS 호출이 모두 섞여 있다.
한 줄 PyTorch 코드의 dispatch 분해 —
x = torch.randn(...).to("mps") — MTLBuffer 가 unified memory 위에 생긴다 (storageModeShared).y = x @ w — ATen 의 MPS dispatch key 가 호출됨. MPS 라이브러리의 MPSMatrixMultiplication 으로 mapping. 사용자 코드 그대로.y.softmax(dim=-1) — softmax 처럼 vendor 라이브러리에 없는 op 는 — PyTorch 가 직접 짠 Metal shader 가 호출됨.torch.mps.synchronize() — CUDA 의 sync 와 같은 자리.즉 MPS backend = Metal 직접 호출 + MPS 라이브러리 호출의 hybrid. 강의의 인상적 디테일 — “MPS 라이브러리만으로는 모든 PyTorch op 를 cover 못 하니, 부족한 자리는 직접 Metal shader 를 짜서 PyTorch 안에 넣는 작업이 계속 진행 중”.
# PyTorch MPS — 사용자 시점
import torch
assert torch.backends.mps.is_available()
device = "mps"
x = torch.randn(8192, 8192, device=device)
y = torch.randn(8192, 8192, device=device)
# MPS backend dispatch — MPSMatrixMultiplication
z = x @ y
torch.mps.synchronize()
# 직접 shader 끼워넣기 — torch.utils.cpp_extension 의 MPS 버전
from torch.utils.cpp_extension import load
ext = load(name="my_mps",
sources=["my_kernel.mm"],
extra_cflags=["-fobjc-arc"])
Apple 이 ML 워크로드에 맞춰 만든 MPSGraph — Metal 위 graph compiler. operator 단위 fusion, lazy execution. PyTorch 의 일부 op 가 MPSGraph 로 dispatch 된다 (MPSGraph.matrixMultiplication). MLX 도 이 layer 위에 산다는 점.
Apple Silicon 의 가장 큰 차이. NVIDIA GPU 는 별도 VRAM(HBM)이 있고 PCIe 를 통해 host RAM 과 통신. M-series 는 — CPU 와 GPU 가 같은 LPDDR5 메모리를 공유. 이게 디자인의 거의 모든 결정에 영향.
storageModeShared buffer 는 CPU 와 GPU 가 동일 주소로 접근.이 차이가 프로그래밍에 어떻게 흘러가는가 —
x.to("mps") 의 to 는 사실 — Buffer 의 metadata 만 바꾸는 일에 가깝다 (storageModeShared 라면).Q&A 에서 “CPU pinned memory 가 unified 에서도 의미가 있나?” 라는 질문. Nikita 의 답: “pinned 의 ‘이 페이지를 swap-out 하지 마라’ 의 의미가 unified 에서는 — GPU 가 이 페이지를 보고 있으니 CPU 가 캐시 invalidate 하지 말라 — 로 변환된다. 마음은 같다”.
실용적 충격 — “M2 Ultra MacBook Pro 192GB 가 70B BF16 LLM 의 inference 가 가능한 가장 작은 form factor”. ChatGPT-class 모델을 자기 노트북 위에서 부분 inference 가능.
강의에서 Nikita 가 가장 정직하게 깐 자리. CUDA 의 ncu, nsys, compute-sanitizer 의 자리에 — Metal 위에는 무엇이 있고 무엇이 없는가.
torch.profiler 가 MPS activity 를 잡는다. CPU + MPS 의 timeline. 가장 일반적 진입점.(1) logical bug — CPU 위에서 같은 코드를 돌려본다 (PyTorch 는 device='mps' 와 'cpu' 가 같은 ATen op). 결과가 다르면 MPS backend 의 버그 일 가능성. (2) numerical drift — bf16/fp32 의 자리를 명시적으로. (3) perf bug — torch.profiler + Metal Frame Capture. 더 깊이는 — os_signpost 로 직접 instrumentation.
kernel void kernel(...) — MSL 의 함수 attributedevice float helper(...) — address space attribute 가 동시에 storage 표시threadgroup float s[N] — block 단위 SRAMthreadgroup_barrier(mem_flags::mem_threadgroup)thread_position_in_threadgroup attributethreadgroup_position_in_gridthreads_per_threadgroupsimd_shuffle_xor — SIMD-group 내 shuffledevice.makeBuffer(length: options:)blitCommandEncoder.copyencoder.dispatchThreadgroups(grid, threadsPerThreadgroup: block)MTLCommandQueue + MTLCommandBufferMTLEvent · queue 간 동기화commandBuffer.waitUntilCompleted 또는 torch.mps.synchronizexcrun -sdk macosx metal 또는 jit 으로 device.makeLibrary(source:)MetalPerformanceShaders / MPSGraph강의의 큰 메시지 — “이 표만 옆에 두고 보면, 일주일 안에 CUDA 사용자가 Metal 커널을 읽을 수 있다”. 단어가 다를 뿐 — 모델은 거의 그대로.
강의 후반부의 본론. Apple Silicon 위 LLM inference 가 — 2024 시점에 — 어떻게 구체적으로 도는지. 두 큰 stack 이 있다 — llama.cpp(C++) 와 MLX(Apple 의 ML 프레임워크).
llama.cpp 의 Metal backend —
MLX — Apple 이 직접 만든 array 라이브러리. NumPy + PyTorch 의 mix. Metal native. lazy evaluation. Apple Silicon 전용이라 NVIDIA 와의 portability 는 의도적으로 포기.
Llama 70B BF16 ≈ 140 GB. M2 Ultra 192GB MacBook Pro 위에서 — 한 노트북에 통째로 적재 + inference. 같은 모델을 NVIDIA 로 돌리려면 H100 80GB × 2 (multi-GPU). Apple 의 unified memory 가 “집에서 70B 모델 돌리는” 가장 싼 길.
강의의 정직한 끝맺음. Apple GPU 가 빛나는 자리와 그렇지 않은 자리.
M3 부터 — SIMD-group matrix 명령. 8×8 fp16 matmul 을 한 SIMD group 단위로. NVIDIA mma 의 첫 등가물. 폭은 작지만(NVIDIA Ampere mma 가 16×8×16) 의미 있는 시작. “M4/M5 에서 더 큰 matrix unit 이 들어올 가능성” 이 강의의 추측.
kernel, device, threadgroup address space. thread_position_in_grid builtin.device="mps". ATen 의 MPS dispatch key. MPS 라이브러리 + 직접 Metal shader 의 hybrid.device="mps" 로 LLaMA-2 7B inference. 토큰/초 측정.kernel void add(...), jit 컴파일, dispatch.이 노트의 perf 수치(Llama 7B M2 Max 38 tok/s 등)는 강의 시점(2024 10월) 의 인터넷 데이터를 인용한 재구성. M3/M4 와 양자화 stack 의 변화가 빠르니 자기 환경에서 직접 측정해야 의미 있는 비교가 된다.