본문 바로가기
C.W.K.
Stream
Lesson 02 of 05 · published

Apple Silicon Unified Memory가 왜 게임 체인저인가

~16 min · metal, unified-memory, apple-silicon, architecture

Level 0Beginner
0 XP0/38 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

RAM은 하나, 프로세서는 둘

NVIDIA discrete GPU와 Apple Silicon을 가르는 가장 큰 구조 차이는 최고 FLOPs도 아니고 어느 회사 편이냐도 아니야. 데이터가 어디 사느냐야.

시스템CPU 메모리GPU 메모리GPU 메모리 bandwidthCPU ↔ GPU 연결
RTX 4090 데스크톱64 GB DDR524 GB GDDR6X1,008 GB/sPCIe 4.0 ×16, ~32 GB/s
Mac Studio M3 Ultra (512 GB)512 GB LPDDR5 (공유)819 GB/s (CPU와 나눠 씀)없음 — 같은 RAM

숫자 출처부터 밝혀둘게. NVIDIA는 4090의 메모리 버스가 384-bit라고만 적고 bandwidth는 안 적어. 그래서 1,008 GB/s는 계산한 값이야 (21 Gbps GDDR6X × 384 bit ÷ 8). 애플은 819 GB/s를 직접 공개하지만 메모리 규격 이름은 안 밝혀. 게다가 819라는 숫자 하나로는 규격을 못 가려. LPDDR5-6400 16채널도, LPDDR5X-8533 12채널도 똑같이 819.2 GB/s가 나오거든. 판가름은 나머지 M3 칩들이 내줘. 같은 세대 칩들이 메모리 속도를 하나로 맞췄다고 보면 이래. 6400 MT/s에선 64-bit 채널 하나가 51.2 GB/s야. 애플이 M3(100), M3 Pro(150), M3 Max 두 구성(300, 400)에 붙인 수치는 각각 2, 3, 6, 8채널(102.4, 153.6, 307.2, 409.6 GB/s)을 내려 적은 값이야. 네 개를 모두 채널 정수로 맞추는 LPDDR5X 속도는 없어. 그래서 표에 LPDDR5라고 썼어.

데스크톱에선 cudaMalloc으로 GPU에 메모리를 잡고 PCIe로 데이터를 복사해. CPU와 GPU가 각자 DRAM을 갖고 있고, 둘을 잇는 건 ~32 GB/s짜리 연결 하나야. GPU가 읽는 GDDR6X보다 30배쯤 좁지. discrete GPU를 다루는 요령도 여기서 나와. 데이터는 한 번만 넘기고, 넘긴 뒤엔 카드에 계속 둬.

Apple Silicon은 SoC 하나에 CPU 코어, GPU 코어, DRAM 컨트롤러를 넣고 메모리 체계도 하나로 묶었어. device.makeBuffer(length:options:)를 storageModeShared로 부르면 buffer가 하나 나와. CPU는 그걸 바로 읽고 쓰고, GPU도 그 buffer를 같은 물리 주소로 봐. 복사도 없고 PCIe도 없어.

당장 눈에 띄는 변화는 cudaMemcpy가 사라진다는 거야. Track 5에서 볼 9단계 GPU 워크플로우가 Apple Silicon에선 7단계로 줄어. 4단계와 7단계가 할 일이 없어지거든.

unified memory가 이기는 곳, 지는 곳

PCIe가 없다니까 "그럼 애플이 더 빠르겠네" 하기 쉬워. 근데 사람들이 제일 많이 돌리는 경우엔 정반대야. 모델이 VRAM에 다 들어가면 가중치는 로드할 때 딱 한 번 PCIe를 건너가고, 그다음부턴 카드에 머물러. 토큰을 하나 만들 때마다 GDDR6X에서 그 가중치를 읽어 올 뿐이고, PCIe는 이 반복에 끼지도 않아. decode 속도는 메모리 bandwidth가 정하는데, 4090 쪽이 23%쯤 더 넓어.

  • 둘 다에 들어가는 모델이면 4090이 더 빨라. 8B 모델을 FP16으로 올리면 ~16 GB야. bandwidth를 토큰 하나 만들 때 읽는 바이트 수로 나누면, 상한이 4090은 초당 63토큰 근처고 M3 Ultra는 51토큰쯤이야. 실제로는 연산과 KV 캐시가 제 몫을 가져가서 둘 다 상한보다 낮게 나오지만, 순서가 뒤집히진 않아.
  • 24 GB에 안 들어가는 모델이면 unified memory가 이겨. 그것도 크게. Llama 70B를 FP16으로 올리면 ~141 GB야. 표의 데스크톱은 전부 합쳐 88 GB (VRAM 24 GB + DDR5 64 GB)라서, 디스크로 페이징하지 않으면 가중치를 담지도 못해. 192 GB Mac Studio는 GPU에 161 GiB쯤까지 쓰라고 권해 (M2 Ultra, macOS 26.6.2에서 측정). 그러니 ~131 GiB짜리 가중치가 전부 GPU가 바로 읽는 메모리에 올라가. 다만 이건 용량에서 이긴 거지 속도에서 이긴 게 아냐. M2 Ultra의 bandwidth가 800 GB/s니까 상한은 800 ÷ 141 ≈ 초당 5–6토큰이야.
  • PCIe가 진짜 발목을 잡는 건 offload야. 데스크톱에 RAM을 넉넉히 꽂아도, VRAM에 못 들어간 레이어는 어딘가에서 돌아야 해. CPU가 듀얼 채널 DDR5 (DDR5-5600이면 ~90 GB/s)를 읽으면서 그 레이어를 돌리든가, 아니면 ~32 GB/s 연결로 GPU에 계속 실어 날라야 해. 어느 쪽이든 넘친 부분을 읽는 속도는 GPU 메모리의 10분의 1도 안 돼. unified memory는 이렇게 우회할 일이 없고, 로드할 때 따로 떨어진 VRAM으로 한 번 더 복사할 일도 없어.
  • 연결이 드러나는 곳은 더 있어. 모델을 카드 두 장에 나눌 때가 그래. 4090엔 NVLink가 없어서 두 카드가 PCIe로 주고받아. 경계마다 활성값 하나만 넘기는 파이프라인 분할이면 부담이 적은데, 레이어 안에서 매번 동기화하는 텐서 병렬이면 비싸. 매 단계 텐서를 CPU로 끌어오는 호스트 코드도 마찬가지야.
  • 애플 쪽에도 약점이 있어. 819 GB/s는 CPU와 GPU가 나눠 써. 둘이 동시에 메모리를 세게 두드리면 각자 받는 몫이 줄어. discrete GPU라면 CPU가 메모리를 오가는 길은 아예 다른 버스야.

MLX (애플이 만든 머신러닝용 배열 프레임워크)가 배열이 어느 장치에 있는지 묻지 않는 것도 같은 이유야. 배열은 전부 이미 한 풀에 있으니까, 배열을 옮기는 대신 연산마다 돌릴 장치만 고르면 돼. 그렇다고 "unified니까 복사는 절대 안 일어난다"까지 넘겨짚으면 안 돼. mx.array(np_array)는 NumPy 배열의 데이터를 MLX가 따로 잡은 메모리로 복사해. MLX 문서에도 그렇게 적혀 있고, 만든 뒤에 NumPy 배열을 바꿔도 MLX 배열은 그대로야. 복사 없는 길은 반대 방향이야. np.array(mx_array, copy=False)를 쓰면 NumPy가 MLX 메모리를 view로 받아. unified memory가 없애는 건 프로세서 사이를 건너는 복사지, 라이브러리가 알아서 하는 복사까지는 아니야.

Code

CPU + GPU 둘 다 보는 buffer 할당·swift
import Metal

let device = MTLCreateSystemDefaultDevice()!
let n = 1024
let bytes = n * MemoryLayout<Float>.stride

// storageModeShared: 같은 물리 RAM, CPU + GPU 둘 다 주소 매김.
let buf = device.makeBuffer(length: bytes, options: .storageModeShared)!

// CPU가 바로 write — staging 없음, copy 없음.
let ptr = buf.contents().bindMemory(to: Float.self, capacity: n)
for i in 0..<n { ptr[i] = Float(i) }

// GPU가 같은 buffer에 커널 launch.
// (encoder.setBuffer(buf, offset: 0, index: 0) ... )
// 4090에선 같은 게 malloc + cudaMalloc + cudaMemcpy.
Storage mode — Metal의 옵션 셋·swift
// shared    — CPU + GPU 같은 RAM, 복사 없음, compute 기본.
.storageModeShared

// private   — GPU-only 메모리 (여전히 unified RAM 안인데 CPU
//             안 보임). 일부 모양에선 GPU access 더 빠름.
.storageModePrivate

// managed   — discrete GPU 박힌 Intel Mac 전용. Apple Silicon은
//             무시 — shared처럼 동작.
.storageModeManaged

External links

Exercise

Apple Silicon Mac을 아무거나 골라서 system_profiler SPHardwareDataType | grep Memory로 unified RAM 크기를 확인해. 그다음 최신 MLX가 깔린 환경에서 python3 -c "import mlx.core as mx; d = mx.device_info(); print(d['memory_size'] / 2**30, d['max_recommended_working_set_size'] / 2**30)"를 돌려. 앞 숫자는 전체 GiB, 뒤 숫자는 느려지기 전까지 GPU에 쓰라고 Metal이 권하는 양이야. 비교 삼아 system_profiler SPDisplaysDataType도 봐. GPU가 Bus: Built-In으로만 나오고 VRAM 줄은 아예 없어. 따로 떼어 보고할 메모리 풀이 없으니까. NVIDIA 머신이 있으면 거기서 nvidia-smi --query-gpu=memory.total --format=csv로 VRAM을 확인해. 이제 돌려보고 싶은 모델을 하나 골라서 가중치 크기(파라미터 수 × 파라미터당 바이트)를 계산하고, 머신마다 두 가지를 답해 봐. 모델이 들어가? decode 상한(메모리 bandwidth ÷ 가중치 크기)은 얼마야? 칩별 bandwidth는 애플 기술 사양 페이지에 나와.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.