GPU VRAM이 부족할 때 해결 방법
AI와 그래픽 작업의 병목인 GPU VRAM. 통합 메모리, VRAM 오프로딩, NVMe 스와핑이 실제로 어떻게 동작하는지 살펴봅니다.

가장 흔한 머신러닝 에러 메시지는 Python 트레이스백이나 shape 불일치가 아닙니다. 바로 CUDA out of memory입니다. 모델이 너무 크거나, 배치 크기가 너무 크거나, 중간 activation이 GPU VRAM에 들어가지 않는 겁니다. RTX 4090의 VRAM은 24GB입니다. 반정밀도(half precision)의 70B 파라미터 모델은 약 140GB가 필요합니다. 계산이 안 맞아요. 더 큰 GPU에 돈을 쓰는 건 문제를 미루는 것일 뿐입니다. 80GB짜리 H100도 가장 큰 모델은 단일 카드에 올리지 못합니다.
그렇다면 시스템 RAM이나 심지어 NVMe 스토리지를 써서 GPU 메모리를 투명하게 늘릴 수 있다면 어떨까요? NVIDIA의 Greenboost 같은 도구와 비슷한 프로젝트들이 바로 이걸 하고 있습니다. 메모리 계층(VRAM → 시스템 RAM → SSD)을 활용해서 내 하드웨어에 안 들어가야 할 워크로드를 돌리는 거죠. 성능 트레이드오프는 실제로 존재하지만, 많은 사용 사례에서는 놀라울 정도로 감당할 만합니다. 이게 어떻게 동작하는지 이해하려면 GPU 메모리 계층 자체를 먼저 이해해야 합니다.
GPU 메모리가 CPU 메모리와 다른 이유
CPU와 GPU 메모리는 근본적으로 다른 접근 패턴을 위해 설계되었습니다. CPU 워크로드는 지연 시간(latency)에 민감합니다. 스레드 하나가 데이터 한 조각을 필요로 하면, 그게 도착할 때까지 멈춰 있습니다. CPU 캐시는 무작위 접근 패턴에서 지연 시간을 최소화하도록 설계되어 있죠.
GPU 워크로드는 처리량(throughput)에 민감합니다. 수천 개의 스레드가 각자 데이터를 필요로 하고, GPU는 스레드를 전환하면서 지연 시간을 숨깁니다. GPU 메모리(데이터센터 GPU는 HBM, 소비자용 카드는 GDDR)는 대역폭에 맞춰 설계되어 있습니다. 개별 접근 하나하나는 CPU 캐시 히트보다 오래 걸릴 수 있어도, 초당 엄청난 양의 데이터를 전달하는 게 목적입니다.
Memory bandwidth comparison (approximate):
RTX 4090 GDDR6X: 1,000 GB/s
H100 HBM3: 3,350 GB/s
DDR5 System RAM: 50 GB/s
PCIe 5.0 x16: 64 GB/s (theoretical max)
NVMe SSD: 7 GB/s
The bandwidth cliff between VRAM and system RAM is ~20x.
Between VRAM and NVMe it's ~140x.
This is why naive offloading to system RAM kills performance —
you're trying to feed a 1,000 GB/s appetite through a 50 GB/s straw.
이 대역폭 격차 때문에 GPU 메모리를 단순히 '가상화'해서, CPU가 디스크에 하듯 데이터를 시스템 RAM으로 페이징하는 방식은 그냥 적용하기 어렵습니다. CPU는 페이지 폴트를 대략 10배 정도의 속도 저하로 견뎌냅니다. 그런데 GPU가 VRAM 대신 시스템 RAM에 접근하면 대역폭이 20배 줄어들고, 대역폭에 묶인 워크로드(대부분의 ML 추론이 여기에 해당)는 20배 느려집니다.
VRAM 오프로딩은 실제로 어떻게 동작하나
핵심은 시스템 RAM을 느린 VRAM처럼 취급하지 않는 데 있습니다. GPU가 필요로 하기 전에 시스템 RAM의 데이터를 미리 VRAM으로 프리페치(prefetch)해서, 지연 시간을 연산 뒤에 숨기는 겁니다. 실제로 쓰이는 VRAM 확장 기법들의 핵심 아이디어가 바로 이것입니다.
신경망 추론은 레이어를 순서대로 거치면서 계산합니다. GPU가 레이어 5를 처리하는 동안, 다음이 레이어 6이라는 것을 알고 있죠. 똑똑한 오프로딩 시스템이라면 레이어 5가 계산되는 동안 레이어 6의 가중치를 시스템 RAM에서 VRAM으로 옮기기 시작할 수 있습니다. 계산 시간이 전송 시간보다 길면(큰 레이어에서는 흔한 경우) 전송은 완전히 가려지고, GPU는 멈추지 않습니다.
# Conceptual overlap of compute and transfer
# (simplified pseudocode)
def inference_with_offloading(model, input_data):
# Only 2 layers fit in VRAM at a time
# Rest are in system RAM
for i, layer in enumerate(model.layers):
# Start async transfer of NEXT layer while computing current
if i + 1 < len(model.layers):
async_transfer_to_gpu(model.layers[i + 1])
# Compute on current layer (GPU is busy, transfer happens in parallel)
output = layer.forward(input_data)
# Evict current layer from VRAM (it's done)
transfer_to_ram(layer)
# Wait for next layer transfer to complete (usually already done)
sync_transfer()
input_data = output
return output
이 파이프라인 방식은 추론에 잘 맞습니다. 계산 그래프가 예측 가능해서 다음에 어떤 가중치가 필요할지 정확히 알 수 있기 때문이죠. 학습은 더 어렵습니다. 역전파에는 순전파의 activation이 필요해서 데이터 이동 패턴이 훨씬 복잡해지거든요.
실제 적용 방식들
CUDA Unified Memory
NVIDIA의 CUDA Unified Memory는 GPU VRAM과 시스템 RAM을 아우르는 단일 주소 공간을 만듭니다. CUDA 런타임은 접근 패턴에 따라 두 메모리 사이에서 페이지를 자동으로 옮깁니다. GPU가 시스템 RAM의 페이지에 접근하면 페이지 폴트가 발생하고, 해당 페이지가 VRAM으로 마이그레이션됩니다.
장점은 애플리케이션 입장에서 투명하다는 점입니다. CUDA 코드가 데이터 배치를 직접 관리할 필요가 없어요. 단점은 페이지 폴트 비용이 크고, 런타임의 마이그레이션 휴리스틱이 애플리케이션의 접근 패턴과 항상 맞지는 않는다는 겁니다. 신경망 추론처럼 예측 가능한 워크로드에서는 명시적 관리가 자동 마이그레이션보다 낫습니다.
레이어 단위 오프로딩
Hugging Face Accelerate, DeepSpeed ZeRO-Inference, 그리고 llama.cpp의 --mmap 플래그 같은 도구들이 명시적인 레이어 오프로딩을 구현합니다. 활성 레이어만 VRAM에 두고, 나머지는 시스템 RAM이나 디스크에서 스트리밍합니다. 모델 전체가 VRAM에 들어갈 필요는 없고, 한두 개 레이어만 들어가면 됩니다.
일반 하드웨어에서 70B 모델을 돌리는 방식이 바로 이겁니다. 4비트 양자화된 70B 모델은 전체 약 40GB가 필요하지만, 레이어 하나는 약 1~2GB면 충분합니다. VRAM 24GB에 프리페칭을 더하면, 성능 영향을 감수할 만한 수준으로 모델을 돌릴 수 있습니다. VRAM에 완전히 올렸을 때보다 30~50% 정도 느린 수준이에요.
NVMe를 확장 메모리로 쓰기
가장 공격적인 접근은 NVMe SSD를 GPU 메모리의 세 번째 계층으로 쓰는 겁니다. 대역폭은 VRAM에 비하면 형편없습니다(약 7GB/s 대 약 1,000GB/s). 하지만 용량은 사실상 무제한이에요. 4TB NVMe 드라이브는 200달러면 살 수 있고, 대형 모델 수십 개를 동시에 저장할 수 있습니다.
NVIDIA의 Greenboost 같은 프로젝트는 이 방식을 투명하게 구현합니다. GPU 메모리 시스템이 NVMe까지 확장되고, 지능형 프리페칭으로 대역폭 한계의 영향을 최소화합니다. GPU가 데이터 이동이 아니라 연산에 상당한 시간을 쓰는 추론 워크로드라면, NVMe 지연 시간도 연산과 겹쳐서 완전히 숨길 수 있습니다.
성능은 워크로드에 크게 좌우됩니다. 연산 바운드 작업(큰 행렬 곱셈)은 전송 지연을 잘 숨깁니다. 메모리 바운드 작업(긴 컨텍스트의 어텐션 메커니즘)은 그렇지 못하죠. 실제로 NVMe 오프로딩은 작은 배치 크기로 대형 모델을 배치 추론하는 경우에 가장 잘 맞습니다. 로컬 LLM 추론이 바로 그 사용 사례예요.
Apple의 통합 메모리 장점
Apple Silicon의 통합 메모리 아키텍처는 완전히 다른 접근을 취합니다. VRAM과 RAM의 구분 자체를 없애는 거죠. CPU와 GPU가 같은 물리 메모리 풀을 공유합니다. 분리된 공간이 없으니 '오프로딩'이라는 개념도 없습니다. GPU가 CPU와 같은 메모리에 직접 접근하니까요.
이게 대역폭 문제를 없애는 건 아닙니다. M 시리즈의 메모리 대역폭(M4 Max 기준 약 400GB/s)은 전용 GPU보다 낮습니다. 하지만 오프로딩을 느리게 만드는 PCIe 병목은 제거합니다. 통합 메모리 128GB짜리 Mac은 70B 모델을 GPU가 접근 가능한 메모리에 통째로 올릴 수 있고, 오프로딩 오버헤드도 전혀 없습니다.
트레이드오프도 있습니다. 전용 GPU VRAM에 완전히 들어가는 워크로드에서는 최고 처리량이 낮습니다. 하지만 들어가지 않는 워크로드에서는 성능이 훨씬 좋아요. 모델이 전용 GPU VRAM을 넘어서는 대형 모델 추론에서는, Apple Silicon이 원시 연산 능력은 떨어져도 오프로딩을 쓰는 전용 GPU보다 빠른 경우가 많습니다.
개발자에게 의미하는 바
ML 추론, 그래픽, 과학 계산 등 GPU를 쓰는 애플리케이션을 만든다면, VRAM 제약은 아키텍처 결정에 구체적으로 영향을 줍니다.
- 작업 집합(working set) 크기를 파악하세요. GPU 메모리 사용량을 프로파일링하세요. 최대 할당량이 아니라, 어느 시점에 실제로 필요한 작업 집합 말입니다. 피크가 48GB여도 한 번의 연산에 활성 데이터가 8GB만 필요하다면 오프로딩이 잘 동작합니다. 단일 연산이 정말로 48GB를 동시에 요구한다면, 더 큰 GPU가 필요합니다.
- 접근 패턴에 따라 오프로딩 전략을 고르세요. 순차 접근(레이어 단위 추론)은 프리페칭과 잘 맞습니다. 무작위 접근(긴 컨텍스트에 대한 어텐션)은 그렇지 않아요. 내 워크로드가 어떤 패턴인지 알아야 합니다.
- 보통 양자화가 오프로딩보다 저렴합니다. FP16에서 INT4로 줄이면 품질 영향은 적은 채 메모리를 4배 줄일 수 있습니다. 시스템 RAM으로 오프로딩하면 품질 손실은 없지만 지연이 추가되고 메모리 절감폭도 제한적입니다. 양자화를 먼저 하고, 오프로딩은 그다음에 하세요.
- 배치 크기가 핵심 튜닝 노브입니다. 배치가 크면 메모리를 더 쓰지만 오버헤드를 더 잘 분산합니다. 배치가 작으면 메모리는 덜 쓰지만 초당 처리량이 줄어듭니다. VRAM 한계에 가까워졌다면, 배치 크기를 줄이는 게 가장 간단한 해결책입니다.
- 메모리 단편화를 모니터링하세요. CUDA 메모리 할당은 시간이 지나면서 VRAM을 단편화할 수 있습니다. 특히 가변 길이 입력을 쓸 때 그렇죠. 전체 여유가 8GB여도 연속된 2GB 블록은 없을 수 있습니다. PyTorch의
torch.cuda.memory_stats()로 단편화를 확인할 수 있습니다.torch.cuda.empty_cache()가 도움이 될 수 있지만, 만능 해결책은 아닙니다.
GPU 메모리는 대규모 연산 워크로드에서 언제나 병목일 겁니다. 모델은 VRAM보다 빠르게 커지니까요. 하지만 통합 메모리, 지능형 오프로딩, 다계층 캐싱 같은 병목 관리 도구들은 이제 'VRAM에 안 들어간다'는 것이 더 이상 넘을 수 없는 벽이 아닐 만큼 좋아지고 있습니다. 이제는 성능 트레이드오프이고, 점점 감당할 만한 수준이 되고 있습니다.


