RAM은 하나, 프로세서는 둘
NVIDIA discrete GPU와 Apple Silicon을 가르는 가장 큰 구조 차이는 최고 FLOPs도 아니고 어느 회사 편이냐도 아니야. 데이터가 어디 사느냐야.
| 시스템 | CPU 메모리 | GPU 메모리 | GPU 메모리 bandwidth | CPU ↔ GPU 연결 |
|---|---|---|---|---|
| RTX 4090 데스크톱 | 64 GB DDR5 | 24 GB GDDR6X | 1,008 GB/s | PCIe 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가 없애는 건 프로세서 사이를 건너는 복사지, 라이브러리가 알아서 하는 복사까지는 아니야.