디바이스 메모리
고친 사람 github-actions[bot]
디바이스 메모리는 그래픽 카드 같은 계산 장치가 다룰 데이터를 장치 바로 곁에 담아 둡니다. 장치는 이 메모리에 든 데이터를 가장 빨리 읽고 씁니다. 그래서 프로그램은 장치에 맡길 데이터를 먼저 이 메모리로 옮깁니다. 프로세서 설계 쪽에서는 같은 이름으로 다른 것을 부르기도 합니다.
쉽고 빠른 이해
디바이스 메모리는 그래픽 카드가 쓰는 작업용 메모리입니다. 게임 캐릭터의 모델과 그 겉에 입힐 그림은 화면에 그려지기 전에 이 메모리에 올라가 있습니다.
따로 두는 까닭은 길이 멀어서입니다. 그래픽 카드는 아주 많은 계산을 한꺼번에 돌리므로 데이터를 몹시 빨리 소비합니다. 본체 메모리에서 그때그때 끌어오면 카드와 본체를 잇는 통로가 막혀 계산이 멈춥니다.
어떻게 도나:
- 프로그램이 그래픽 카드 쪽 메모리를 빌립니다
- 본체 메모리의 데이터를 그 메모리로 복사합니다
- 그래픽 카드가 그 데이터로 계산합니다. 필요한 결과만 본체로 돌려보냅니다
대가가 있습니다. 복사가 한 번 더 듭니다. 그래서 데이터가 작고 계산이 가벼우면 그래픽 카드로 보내지 않고 본체에서 끝내는 편이 낫습니다.
같은 데이터가 두 군데에 있으니 어느 쪽이 최신인지 프로그램이 챙겨야 합니다. 그래픽 카드의 메모리는 대개 본체 메모리보다 작습니다. 넘치면 메모리를 빌리는 일 자체가 실패합니다.
상세
공장 하나를 떠올려 봅시다. 공장 바로 옆에는 작은 자재 창고가 있어서 일꾼들이 자재를 금방 꺼내 씁니다. 멀리 있는 본사 창고의 자재는 트럭에 실어 와야 합니다. 그래서 공장장은 그날 쓸 자재를 미리 옆 창고로 옮겨 둡니다.
이 절은 그 옆 창고가 컴퓨터에서 무엇인지 봅니다. 그래픽 카드 한 장을 꽂은 컴퓨터 한 대를 줄곧 예로 씁니다. 먼저 호스트와 디바이스를 가르고, 데이터가 오가는 차례를 본 뒤, 프로그램이 이 메모리를 고르고 관리하는 방법을 봅니다.
호스트와 디바이스
백엔드 서버에서 코드를 돌리는 칩은 CPU(Central Processing Unit, 중앙 처리 장치)입니다. CPU 가 읽고 쓰는 메모리는 서버에 꽂힌 RAM(Random Access Memory, 임의 접근 메모리)입니다. 그래픽 카드를 다루는 프로그래밍에서는 CPU 와 그 RAM 을 묶어 호스트라고 부릅니다. 그 RAM 이 호스트 메모리입니다.
GPU(Graphics Processing Unit, 그래픽 처리 장치)는 그래픽 카드에 올라간 계산 칩입니다. 같은 계산을 아주 많은 데이터에 한꺼번에 돌리도록 만들어졌습니다. 화면의 점 수백만 개에 색을 칠하거나 딥러닝 모델의 행렬을 곱하는 일이 그렇습니다. 호스트가 이런 일을 맡기는 장치를 디바이스라고 부릅니다.
디바이스가 계산하는 동안 곁에서 데이터를 대 주는 메모리가 디바이스 메모리입니다. 본체에 따로 꽂는 그래픽 카드라면 GPU 칩 둘레에 붙은 메모리 칩들이 디바이스 메모리입니다. 이 칩들을 흔히 VRAM(Video RAM, 비디오 메모리)이라고 부릅니다.
두 메모리를 잇는 버스
버스는 컴퓨터 부품끼리 데이터를 주고받는 공용 통로입니다. 호스트와 그래픽 카드는 메인보드의 PCIe(Peripheral Component Interconnect Express, 주변 장치 연결 버스)로 이어집니다. 호스트 메모리와 디바이스 메모리 사이를 오가는 데이터는 전부 이 버스를 지납니다.
아래 그림은 메모리가 두 벌이고 그 사이에 버스 하나만 있다는 것을 보입니다. CPU 는 호스트 메모리를, GPU 는 디바이스 메모리를 곁에 둡니다.
flowchart TD
subgraph H["호스트"]
C["CPU"] --- HM["호스트 메모리"]
end
subgraph D["그래픽 카드 · 디바이스"]
G["GPU"] --- DM["디바이스 메모리"]
end
H <-->|"PCIe 버스"| D
메모리를 장치 곁에 따로 두는 까닭
메모리 대역폭은 1초 동안 메모리와 칩 사이에서 옮길 수 있는 데이터의 양입니다. GPU 는 계산을 한꺼번에 많이 돌리므로 대역폭이 모자라면 계산 회로가 데이터를 기다리며 놉니다. 그래서 GPU 와 디바이스 메모리 사이의 길은 대역폭이 아주 크게 설계됩니다.
버스를 건너는 길은 카드 안의 길보다 대역폭이 훨씬 작습니다. GPU 가 계산할 때마다 호스트 메모리에서 데이터를 끌어오면 버스가 병목이 됩니다. 같은 서버의 메모리를 읽는 것과 네트워크 너머 데이터베이스를 부르는 것의 차이와 닮았습니다.
버스를 한 번 건널 때마다 시작 비용도 듭니다. 그래서 작은 복사를 여러 번 하는 것보다 큰 복사를 한 번 하는 편이 쌉니다. 백엔드에서 데이터베이스 왕복을 줄이려고 쿼리를 묶어 보내는 것과 같은 이유입니다.
데이터가 오가는 차례
호스트가 GPU 에 일을 맡기면 데이터는 아래 차례로 움직입니다. 아래에서 호스트 코드는 CPU 에서 도는 응용 프로그램 쪽 코드를 뜻합니다.
sequenceDiagram
participant 앱 as 호스트 코드
participant HM as 호스트 메모리
participant DM as 디바이스 메모리
participant G as GPU
앱->>DM: 메모리를 빌린다
HM->>DM: 데이터를 복사한다
앱->>G: 계산을 맡긴다
G->>DM: 읽고 결과를 쓴다
DM->>HM: 결과를 돌려보낸다
Note over HM,DM: 화면에 그릴 결과는 돌려보내지 않는다
먼저 호스트 코드가 디바이스 메모리를 빌립니다. 이 요청은 그래픽 드라이버가 받습니다. 그래픽 드라이버는 운영체제와 GPU 사이에서 명령을 옮겨 주는 소프트웨어입니다.
다음은 복사입니다. 이 복사는 대개 DMA(Direct Memory Access, 직접 메모리 접근)로 합니다. DMA 는 CPU 가 바이트를 하나씩 나르지 않고 장치가 메모리를 직접 읽어 가는 방식입니다. 그동안 CPU 는 다른 일을 할 수 있습니다.
그다음 GPU 가 셰이더를 돌립니다. 셰이더는 GPU 에서 도는 작은 프로그램입니다. 셰이더는 디바이스 메모리에서 데이터를 읽고 결과도 디바이스 메모리에 씁니다.
결과를 호스트가 써야 하면 다시 호스트 메모리로 복사해 옵니다. 화면에 그리는 일이라면 돌려보낼 필요가 없습니다. 그린 그림은 디바이스 메모리에서 바로 모니터로 나가기 때문입니다.
호스트가 디바이스 메모리를 직접 만질 때
이 소절은 호스트 코드가 디바이스 메모리를 포인터로 바로 읽고 쓸 수 있는지 봅니다. 그냥은 대개 안 됩니다.
디바이스 메모리를 빌리면 호스트는 핸들을 받습니다. 핸들은 빌린 메모리를 가리키는 번호표입니다. 호스트가 핸들로 메모리 안을 직접 읽을 수는 없습니다. 드라이버에 일을 시킬 때 어느 메모리인지 알려 주려고 이 핸들을 넘깁니다.
직접 만지려면 메모리 매핑을 거칩니다. 메모리 매핑은 디바이스가 보는 메모리 한 구간을 호스트 프로그램의 주소 공간에 이어 붙이는 일입니다. 이어 붙이고 나면 호스트가 평범한 포인터로 그 구간을 읽고 씁니다. 다만 모든 종류의 디바이스 메모리가 매핑을 허락하지는 않습니다.
매핑한 구간에 호스트가 쓴 값은 디바이스에 바로 안 보일 수 있습니다. CPU 는 쓴 값을 잠시 자기 CPU 캐시에 들고 있을 수 있기 때문입니다. 디바이스는 그동안 메모리에 남은 옛 값을 봅니다. 이렇게 두 쪽이 같은 메모리를 두고 서로 다른 값을 보지 않게 맞추는 문제를 캐시 일관성 문제라고 합니다.
그래서 캐시에 남은 값을 메모리로 밀어내는 단계가 따로 필요할 수 있습니다. 이 단계를 플러시라고 합니다. 플러시를 하고 나면 디바이스도 호스트가 쓴 새 값을 봅니다.
메모리 종류를 고르는 방법
프로그램은 디바이스 메모리를 빌릴 때 어떤 종류를 빌릴지도 고릅니다. 여기서는 그 방법을 Vulkan 을 예로 봅니다. Vulkan 은 GPU 를 다루는 저수준 API(Application Programming Interface, 응용 프로그래밍 인터페이스) 표준입니다.
OpenGL 같은 이전 세대의 그래픽 API 에서는 데이터를 어느 메모리에 둘지 드라이버가 알아서 정했습니다. Vulkan 은 그 결정을 응용 프로그램에 넘깁니다. 드라이버의 짐작이 빗나가 생기는 느려짐을 없애려는 것입니다. 대신 응용 프로그램이 메모리를 직접 고르고 관리해야 합니다.
Vulkan 은 디바이스 메모리라는 말을 이 편 첫머리보다 넓게 씁니다. 디바이스가 볼 수 있는 메모리라면 VRAM 이든 호스트 RAM 이든 전부 디바이스 메모리라고 부릅니다. 첫머리의 좁은 뜻, 곧 GPU 바로 곁의 메모리는 아래 표의 「디바이스 로컬」 쪽입니다.
디바이스는 쓸 수 있는 메모리를 메모리 힙 몇 개로 알립니다. 힙은 크기가 정해진 메모리 덩어리 하나입니다. 그래픽 카드 위의 VRAM 이 힙 하나가 됩니다. 호스트 RAM 가운데 디바이스가 볼 수 있는 구간이 또 하나의 힙이 됩니다.
힙마다 메모리 타입이 하나 이상 붙습니다. 메모리 타입은 그 힙을 어떤 성질로 쓰는지를 적은 꼬리표입니다. 자주 보는 성질은 넷입니다.
표에 나오는 프레임은 화면에 그려 내는 그림 한 장입니다. 게임은 1초에 수십 장을 그립니다. 그래서 「프레임마다」는 1초에 수십 번이라는 뜻입니다.
| 성질 | 뜻 | 어울리는 데이터 |
|---|---|---|
| 디바이스 로컬 | GPU 가 가장 빨리 읽고 쓴다 | 모델과 그 겉에 입히는 그림처럼 GPU 가 되풀이해 읽는 데이터 |
| 호스트에서 보임 | 호스트가 매핑해 직접 읽고 쓸 수 있다 | 프레임마다 바뀌는 작은 값 · 디바이스 로컬로 옮기기 전에 잠시 두는 데이터(아래 스테이징 버퍼) |
| 호스트 일관 | 호스트가 쓴 값이 플러시 없이 디바이스에 보인다 | 플러시를 빠뜨릴 걱정을 덜고 싶은 데이터 |
| 호스트 캐시 | 호스트가 읽을 때 CPU 캐시를 거친다. 없으면 읽을 때마다 메모리까지 다녀와야 한다 | GPU 가 낸 결과를 호스트가 읽어 오는 데이터 |
한 메모리 타입이 성질 여럿을 함께 가질 수 있습니다. 따로 꽂는 그래픽 카드에서는 디바이스 로컬인 타입이 대개 호스트에서 안 보입니다.
버퍼와 이미지
빌린 디바이스 메모리는 그 자체로는 이름 없는 바이트 덩어리입니다. 쓰려면 앞에서 받은 핸들을 버퍼나 이미지라는 객체에 붙입니다. 둘은 담는 데이터의 모양이 다릅니다.
버퍼는 데이터를 일렬로 담는 객체입니다. 정점은 3D 모델의 모양을 이루는 꼭짓점입니다. 모델은 정점 여럿을 이어 만든 삼각형들로 그립니다. 그 정점들의 좌표가 버퍼에 담기는 대표 데이터입니다.
이미지는 가로세로가 있는 픽셀 데이터를 담는 객체입니다. 모델 겉에 입히는 그림인 텍스처가 이미지에 담깁니다.
동시에 빌려 둘 수 있는 덩어리 수에는 한도가 있습니다. 그래서 큰 덩어리를 한 번 빌린 뒤 그 안을 잘게 나눠 여러 버퍼에 나눠 주는 방식을 흔히 씁니다. 이 일을 맡는 라이브러리가 메모리 할당자입니다. 백엔드의 메모리 풀도 객체를 매번 새로 만들지 않고 미리 잡아 둔 풀에서 꺼내 씁니다.
스테이징 버퍼로 올리기
호스트가 디바이스 로컬 메모리에 데이터를 넣을 때는 흔히 중간에 한 단계를 둡니다. 앞에서 본 것처럼 따로 꽂는 그래픽 카드의 디바이스 로컬 메모리는 대개 호스트에서 안 보이기 때문입니다.
스테이징 버퍼는 호스트에서 보이는 메모리에 잡은 임시 버퍼입니다. 호스트가 데이터를 먼저 여기에 씁니다. 그다음 GPU 에게 스테이징 버퍼에서 디바이스 로컬 메모리로 복사하라는 명령을 보냅니다.
sequenceDiagram
participant 앱 as 호스트 코드
participant S as 스테이징 버퍼
participant G as GPU
participant L as 디바이스 로컬 메모리
앱->>S: 매핑해서 데이터를 쓴다
앱->>G: 복사 명령을 보낸다
G->>S: 읽는다
G->>L: 옮겨 쓴다
Note over 앱,S: 복사가 끝나면 스테이징 버퍼를 다시 쓴다
복사는 GPU 가 하므로 호스트는 명령을 보낸 뒤 다른 일을 합니다. 복사가 끝나기 전에 스테이징 버퍼를 덮어쓰면 안 됩니다. 그래서 호스트는 끝났다는 신호를 받은 뒤에 그 버퍼를 다시 씁니다.
내장 GPU 와 통합 메모리 구조
내장 GPU는 CPU 와 같은 칩, 또는 같은 패키지 안에 들어간 GPU 입니다. 패키지는 칩을 감싸 메인보드에 붙이는 껍데기입니다. 노트북과 휴대폰이 대개 이 방식입니다. 내장 GPU 는 메모리 칩을 따로 두지 않고 호스트 RAM 을 함께 씁니다.
이런 구조를 통합 메모리 구조라고 부릅니다. 여기서는 호스트 메모리와 디바이스 메모리가 같은 메모리 칩입니다. 디바이스 로컬이면서 호스트에서도 보이는 메모리 타입이 흔합니다. 그래서 복사 없이 매핑만으로 데이터를 주고받을 수 있습니다.
대가는 대역폭을 나눠 쓴다는 것입니다. CPU 와 GPU 가 같은 메모리에서 동시에 많이 읽으면 서로의 몫을 깎아 먹습니다. 따로 꽂는 그래픽 카드처럼 GPU 전용의 넓은 길이 없습니다.
디바이스 메모리가 모자랄 때
디바이스 메모리는 대개 호스트 메모리보다 작습니다. 큰 텍스처를 잔뜩 올린 게임이나 큰 딥러닝 모델을 학습시킬 때 먼저 바닥나는 것이 이 메모리입니다.
모자라면 메모리를 빌리는 요청이 실패합니다. Vulkan 은 이때 VK_ERROR_OUT_OF_DEVICE_MEMORY 를 돌려줍니다. PyTorch 가 띄우는 CUDA out of memory 오류도 같은 상황입니다.
운영체제와 드라이버에 따라서는 덜 쓰는 데이터를 호스트 메모리로 밀어내 버티기도 합니다. 그러면 빌리는 일은 성공합니다. 대신 GPU 가 그 데이터를 버스 너머에서 읽어야 하므로 계산이 늦어집니다. 서버 메모리가 모자랄 때 디스크로 밀어내는 스왑도 같은 원리입니다.
디바이스 메모리에 둘 데이터
어떤 데이터를 디바이스 메모리에 둘지는 두 가지로 가릅니다. GPU 가 그 데이터를 몇 번 읽느냐와 복사 비용이 얼마냐입니다.
| 데이터 | 둘 곳 | 까닭 |
|---|---|---|
| 텍스처 · 모델의 정점 | 디바이스 로컬 메모리 | 한 번 올리고 프레임마다 여러 번 읽는다 |
| 프레임마다 바뀌는 작은 값 | 호스트에서 보이는 메모리 | 스테이징을 거치는 것보다 직접 쓰는 편이 싸다 |
| GPU 결과를 호스트가 읽을 것 | 호스트에서 보이고 캐시되는 메모리 | 호스트의 읽기가 캐시를 탄다 |
| 한두 번 쓰고 마는 작은 계산 | GPU 로 보내지 않는다 | 복사 비용이 계산 비용보다 크다 |
표의 마지막 줄이 놓치기 쉬운 경우입니다. GPU 가 계산은 훨씬 많이 해도, 데이터를 버스 너머로 보내고 받는 시간이 그 이득을 다 먹어 버릴 수 있습니다. 데이터가 작고 계산이 가벼우면 CPU 에서 끝내는 편이 이깁니다.
같은 이름의 다른 뜻
프로세서 설계 쪽에서는 디바이스 메모리가 다른 것을 가리키기도 합니다. 장치에는 장치 레지스터라는 작은 칸이 있습니다. 장치를 움직이는 스위치 같은 칸입니다. 값을 써 넣으면 장치가 동작합니다.
이 레지스터를 메모리 주소에 이어 붙이면 CPU 가 평범한 메모리처럼 읽고 씁니다. 이 방식이 메모리 맵 입출력입니다. 그렇게 이어 붙인 주소 구간을 디바이스 메모리라고 부릅니다.
CPU 는 평소 더 빨리 돌려고 읽기와 쓰기의 순서를 바꿔 실행하기도 합니다. 보통 메모리라면 결과가 같아서 문제가 없습니다. 장치 레지스터는 읽고 쓰는 일 하나하나가 장치를 움직입니다. 순서가 바뀌면 장치가 다르게 동작합니다.
그래서 이 구간은 CPU 캐시를 거치지 않게 다룹니다. 읽고 쓰는 순서도 바꾸지 않게 다룹니다. 이 편이 다룬 GPU 쪽 뜻과는 다른 개념입니다.
관련 항목
디바이스 메모리와 맞세워지는 메모리
호스트 메모리 · 주 메모리 · RAM · 가상 메모리 · 스왑
디바이스 메모리를 쓰는 장치와 소프트웨어
GPU · 내장 GPU · 외장 GPU · 호스트 · 그래픽 드라이버 · 가속기
디바이스 메모리를 이루는 하드웨어
VRAM · GDDR · HBM · 메모리 대역폭 · PCIe · 버스
디바이스 메모리를 나눠 고르는 단위
메모리 힙 · 메모리 타입 · 메모리 할당자 · 메모리 풀 · 서브할당
디바이스 메모리에 담기는 객체
버퍼 · 이미지 · 텍스처 · 정점 버퍼 · 유니폼 버퍼 · 프레임버퍼
호스트와 디바이스 사이에서 데이터를 옮기는 방법
DMA · 스테이징 버퍼 · 메모리 매핑 · 캐시 일관성 · CPU 캐시 · 통합 메모리 구조 · 핀 메모리
디바이스 메모리를 다루는 API
Vulkan · CUDA · OpenCL · Direct3D 12 · Metal · WebGPU · OpenGL · API
디바이스 메모리에서 나는 오류
메모리 부족 · VK_ERROR_OUT_OF_DEVICE_MEMORY · 메모리 누수 · 메모리 단편화
디바이스 메모리와 헷갈리는 이웃
메모리 맵 입출력 · 공유 메모리 · 글로벌 메모리 · 로컬 메모리 · 통합 메모리
디바이스 메모리가 속하는 상위 분류
다른 이름: device memory · 장치 메모리 · GPU 메모리 · 그래픽 메모리