Single RTX 3090로 Llama 70B 돌리기: CPU를 우회하는 NVMe Direct I/O의 미친 실험


엔지니어라면 누구나 한 번쯤 겪는 ‘현타’가 있다. 바로 VRAM 용량의 벽이다. RTX 3090의 24GB는 훌륭하지만, Llama 3 70B 같은 거대 모델을 돌리기엔 턱없이 부족하다. 보통 이 시점에서 우리는 양자화(Quantization)를 극한으로 깎거나, 비싼 Mac Studio를 알아보거나, 혹은 클라우드로 도망친다.

그런데 최근 Hacker News에서 ntransformer 라는 흥미로운 프로젝트가 화제가 되었다. 이 프로젝트의 핵심은 간단하면서도 도발적이다. “VRAM이 부족해? 그럼 시스템 메모리(RAM)도 거치지 말고, NVMe SSD에서 GPU로 데이터를 직접 꽂아버리자.”

오늘은 이 ‘변태적인’ 최적화 기법이 어떻게 가능한지, 그리고 이것이 실무적으로 어떤 의미가 있는지 깊게 파헤쳐 보겠다.

왜 NVMe Direct인가?

일반적으로 VRAM보다 큰 모델을 돌릴 때 우리는 mmap을 사용해 시스템 RAM을 오버플로우 공간으로 쓴다. 하지만 이 방식은 OS의 페이지 캐시 관리에 의존하며, 데이터가 Storage -> System RAM (CPU Copy) -> VRAM (PCIe Copy)의 경로를 거친다. 여기서 CPU가 병목이 되고, 불필요한 메모리 복사가 발생한다.

이 프로젝트는 GPU Direct Storage 와 유사한 개념을 커스텀하게 구현했다. 리눅스의 VFIO와 커스텀 유저스페이스 드라이버를 통해 NVMe 컨트롤러를 직접 제어한다. 즉, CPU를 건너뛰고 NVMe SSD의 데이터가 PCIe 버스를 타고 바로 GPU 메모리로 들어가는 구조다.

아키텍처의 핵심: 3-Tier Adaptive Caching

이 엔진은 메모리 계층을 세 단계로 나눈다:

  1. Tier A (VRAM Resident): 가장 빈번하게 접근하는 레이어 (약 20~30개)는 VRAM에 고정한다.
  2. Tier B (Pinned RAM): 그 다음 순위는 시스템 RAM(Pinned Memory)에 두고, 필요할 때 비동기 DMA로 GPU에 쏜다.
  3. Tier C (NVMe Direct): 나머지는 SSD에 두고, GPU가 연산하는 동안 백그라운드에서 다음 레이어를 미리 읽어온다 (Prefetching).

이 구조 덕분에 70B 모델을 돌릴 때 기본 mmap 방식 대비 83배의 속도 향상 을 달성했다고 주장한다.

성능: 현실은 0.5 tok/s

하지만 냉정해지자. “83배 빨라졌다”는 말은 비교 대상이 워낙 느렸기 때문이다. 실제 벤치마크 결과를 보면 Llama 3.1 70B (Q4_K_M) 기준으로 초당 0.3 ~ 0.5 토큰 수준이다.

| Model | Mode | Decode | VRAM | Notes |
|---|---|---|---|---|
| Llama 3.1 70B Q4_K_M | Tiered (auto) | 0.3 tok/s | 22.9 GB | 36 VRAM + 44 RAM |
| Llama 3.1 70B Q4_K_M | Tiered + layer skip | 0.5 tok/s | 22.9 GB | 20 layers skipped |

솔직히 말해서, 0.5 t/s는 대화형 챗봇으로 쓰기엔 인내심의 한계를 시험하는 속도다. 한 문장이 완성되는 데 커피 한 잔을 마실 수 있는 시간이다. 하지만 이 프로젝트의 가치는 ‘실사용’보다는 ‘한계 돌파’ 그 자체에 있다. 소비자용 하드웨어에서 PCIe 대역폭을 극한까지 쥐어짜는 소프트웨어 엔지니어링의 정수이기 때문이다.

위험한 장난: 시스템 레벨의 대공사

이 프로젝트를 당신의 메인 워크스테이션에서 돌리는 건 추천하지 않는다. 구현 방식이 상당히 로우레벨이고 위험하기 때문이다.

  • 커스텀 커널 모듈: NVIDIA 드라이버의 os-mlock.c를 패치해야 한다.
  • IOMMU 비활성화: AMD CPU의 경우 amd_iommu=off를 줘야 하는데, 이는 하드웨어 격리를 해제하는 것으로 보안상 위험할 수 있다.
  • Raw Block Access: 파일 시스템을 거치지 않고 dd 명령어로 SSD의 원시 블록(Raw Block)에 모델을 쓴다. /dev/nvme0n1 같은 경로를 실수로 잘못 지정하면? 부팅 드라이브가 날아간다.
# 경고: 절대 따라하지 마시오 (책임 못 짐)
sudo dd if=model.gguf of=/dev/nvme0n1 bs=1M oflag=direct status=progress

이건 마치 레이싱카의 무게를 줄이겠다고 브레이크와 에어백을 떼어내는 것과 비슷하다. 빠르고 효율적이지만, 사고 나면 끝장이다.

Hacker News의 반응과 나의 생각

HN 커뮤니티의 반응도 흥미롭다. 한 유저는 “0.2 tok/s는 실험용으로는 좋지만 상호작용은 불가능하다. 차라리 잘 깎은 8B 모델을 쓰는 게 낫다”고 지적했다. 전적으로 동의한다. 70B의 지능이 필요하다면 차라리 API를 쓰는 게 시간과 전기세를 아끼는 길이다.

또 다른 유저는 “DDR5 대역폭이 40GB/s가 넘는데, 굳이 NVMe(PCIe Gen4 x4는 약 7GB/s)까지 내려갈 필요가 있나? 그냥 RAM을 128GB로 늘리는 게 낫지 않나?”라는 의견을 냈다. 맞는 말이다. 하지만 이 프로젝트의 의의는 RAM조차 부족한 상황 에서의 최후의 보루를 기술적으로 구현했다는 데 있다.

개인적으로 가장 인상 깊었던 부분은 Layer SkippingSelf-Speculative Decoding 의 적용이다. 단순히 I/O만 최적화한 게 아니라, 모델의 추론 과정 자체를 건드려 속도를 높이려는 시도가 엿보인다. VRAM에 상주하는 레이어들을 ‘Draft Model’로 사용하여 추측 디코딩을 수행하는 아이디어는 꽤 영리하다.

결론: 기술적 낭만과 실용성의 괴리

이 프로젝트는 “할 수 있으니까 한다” 는 해커 정신의 결정체다. C++와 CUDA만으로 외부 의존성(PyTorch 등) 없이 바닥부터 짠 추론 엔진이라는 점은 시스템 엔지니어로서 경의를 표할 만하다.

하지만 CTO 관점에서 보자면, 이건 프로덕션에 절대 쓸 수 없는 기술이다. 유지보수 비용, 시스템 불안정성, 그리고 처참한 레이턴시 때문이다. 만약 당신이 로우레벨 시스템 프로그래밍을 공부하거나, PCIe 버스의 한계를 직접 체험해보고 싶은 엔지니어라면 일독을 권한다.

Verdict: 실용성은 제로에 가깝지만, 기술적 깊이는 만점이다. 남는 NVMe SSD와 3090이 있고, 주말에 리눅스 커널과 싸우고 싶은 변태적인 취미가 있다면 강력 추천한다.