MoE의 VRAM 디스크 캐시를 통해 단일 DGX Spark에서 Kimi 2.7 모델로 340 pp/s, 9.6 tg/s 달성
요약
llama.cpp를 활용하여 MoE 모델의 전문가(experts)를 디스크 대신 VRAM 캐시로 관리하는 최적화 전략을 소개합니다. 이를 통해 DGX Spark 환경에서 Kimi 2.7 모델로 높은 추론 성능을 달성하는 벤치마크 결과를 제시합니다.
핵심 포인트
- VRAM을 캐시로 활용하여 MoE 전문가의 CUDA 연산 경로 유지
- Unified Memory와 mmap을 활용한 전문가 페이지 관리 최적화
- Kimi 2.7 모델 기준 340 pp/s 및 9.6 tg/s 성능 달성
- 레이어 핀닝(pinned) 여부에 따른 성능 차이 분석
이 전략은 llama.cpp에서 MoE (Mixture of Experts) 전문가들을 CUDA 연산 경로에 유지하기 위해 디스크 대신 VRAM을 캐시로 효과적으로 사용합니다. 수치부터 말씀드리겠습니다. 상세한 설명은 아래에 있습니다.
DGX Spark 상의 Kimi-K2.7-Code.i1-IQ_S.gguf 204GB 1T.A32B (https://huggingface.co/mradermacher/Kimi-K2.7-Code-i1-GGUF) 수치
| 실행 방법 | pp512 | tg128 | 비고 |
|---|---|---|---|
| A: cuda, -ot regex A (아래 참조), GGML_CUDA_ENABLE_UNIFIED_MEMORY, GGML_OP_OFFLOAD_MIN_BATCH | 340.02 ± 37.75 | 9.58 ± 0.07 | 모든 텐서를 Unified RAM에 배치 + mmap을 통해 전문가를 pageable host memory에 유지 + 마지막 레이어들을 RAM에 pinned 처리 |
| B: cuda, -ot regex B (아래 참조), GGML_CUDA_ENABLE_UNIFIED_MEMORY, GGML_OP_OFFLOAD_MIN_BATCH | 154.31 ± 30.62 | 8.75 ± 0.22 | A와 동일하지만, 마지막 레이어들이 pinned 처리되지 않음 |
| C: cuda, -ot regex C (아래 참조), GGML_CUDA_ENABLE_UNIFIED_MEMORY | 222.20 ± 66.04 | 3.26 ± 0.01 | B와 동일하지만, min batch 플래그를 제거함 |
| D: cuda, GGML_CUDA_ENABLE_UNIFIED_MEMORY | crash | crash | C와 동일하지만, 전문가 오프로딩(experts offloading)을 제거함 |
| E: cpu mmap | 4.23 ± 0.22 | 1.63 ± 0.66 | CUDA를 사용하지 않음 |
-ot regex A: '^(?!blk.(5[5-9]|60).ffn_(down|gate|up)exps.weight$).*.ffn(down|gate|up)exps.weight=CPU'
-ot regex B: '.*.ffn(down|gate|up)exps.weight=CPU'
-ot regex C: '.*.ffn(down|gate|up)_exps.weight=CPU'
3090s PCIe 4.0 + 128GB DDR4 상의 Minimax-M2.7-K_G_3.00.gguf 80GB 230B.A10B (https://huggingface.co/Goldkoron/MiniMax-M2.7) 수치
| 실행 방법 | pp512 | tg128 | 비고 |
|---|---|---|---|
| A: cuda, -ot regex A (아래 참조), GGML_CUDA_ENABLE_UNIFIED_MEMORY, GGML_OP_OFFLOAD_MIN_BATCH | 54.03 ± 3.85 | 2.90 ± 0.09 | DGX에서의 B와 동일한 전략 |
| B: cuda -ngl 17 | 73.80 ± 2.10 | 1.66 ± 0.02 | 일반적인 레이어 분할 |
| C: 3090x2, cuda, -ot regex C (아래 참조), GGML_CUDA_ENABLE_UNIFIED_MEMORY, GGML_OP_OFFLOAD_MIN_BATCH | 48.86 ± 0.53 | 2.40 ± 0.01 | A와 동일하지만, 두 개의 3090 사용 |
-ot regex A: '^(?!blk.(5[5-9]|60).ffn_(down|gate|up)exps.weight$).*.ffn(down|gate|up)exps.weight=CPU'
-ot regex C: '^(?!blk.(5[5-9]|60).ffn(down|gate|up)exps.weight$).*.ffn(down|gate|up)_exps.weight=CPU'
배경: 저는 단일 DGX Spark에서 128GB보다 큰 모델을 실행하고 싶습니다.
MoE 모델은 훌륭한 기회를 제공합니다. 활성 파라미터 (active params)를 완전히 CUDA로 계산할 수 있다면, 런타임 시 모든 모델 가중치 (model weights)를 GPU RAM 내에 저장할 필요가 없기 때문입니다. 실험 끝에 저는 llama.cpp를 사용하여 Kimi 2.7을 빠르게 실행할 수 있는 설정 조합을 찾아냈습니다. llama-bench를 위한 일반 설정은 다음과 같습니다: -r 50, -fa on, -mmp 1, 시스템 스왑 (system swap) 비활성화. Kleidai 지원이 포함된 llama.cpp b10075를 사용하여, 승리 전략은 다음과 같습니다: GGML_CUDA_ENABLE_UNIFIED_MEMORY=1 GGML_OP_OFFLOAD_MIN_BATCH=1 ./llama-bench -m ~/Kimi-K2.7-Code.i1-IQ_S.gguf -fa on -mmp 1 -ot '^(?!blk.(5[5-9]|60).ffn_(down|gate|up)exps.weight$).*.ffn(down|gate|up)_exps.weight=CPU' -r 50. 저는 b10075를 cmake -B build -DGGMLCUDA=ON -DGGML_CPU_KLEIDIAI=ON 으로 구성하고 cmake --build build --config Release 로 컴파일했습니다.
작동 원리는 다음과 같습니다. 첫째, GGML_CUDA_ENABLE_UNIFIED_MEMORY가 1이면, CUDA 디바이스 버퍼 (device buffers)는 cudaMallocManaged를 사용하며, 모든 텐서 (tensor)는 GPU와 CPU 모두에서 유효한 하나의 가상 주소 (virtual address)를 가집니다. CUDA 커널이 VRAM에 존재하는 페이지 (page)에 접근하면 정상적으로 실행됩니다. 만약 VRAM에 없다면, CPU RAM에 있는 페이지가 해당 페이지를 VRAM으로 마이그레이션 (migrate)합니다. GPU RAM이나 CPU RAM에도 없다면, mmap을 통해 디스크에서 CPU RAM으로 페이지를 가져온 다음 VRAM으로 전송합니다. DGX Spark에서는 GPU RAM과 CPU RAM이 통합되어 있기 때문에 cudaMallocManaged의 비용이 훨씬 낮습니다. 둘째, GGML_OP_OFFLOAD_MIN_BATCH (기본값은 32)의 경우, 배치 크기 (batch size)가 임계값 (threshold)보다 작고 가중치가 CPU RAM에 있으면 CPU에서 실행되고, 그렇지 않으면 GPU에서 실행됩니다. 이를 1로 설정하면 모든 계산을 GPU에서 강제합니다. 이는 토큰 생성 (token generation) 시 임계값에 도달하지 않으므로 토큰 생성 속도 (tg)에 도움이 됩니다. 셋째, -ot '.*.ffn_(down|gate|up)_exps.weight=CPU'를 지정하면 전문가 가중치 (expert weights)를 CPU RAM에 배치하며, mmap으로 인해 축출 (evict)될 수 있습니다. 이는 콜드 전문가 가중치 (cold expert weights)가 디스크에 머물 가능성을 더 높여줍니다.
그리고 마지막으로, '^(?!blk\.(5[5-9]|60)\.ffn_(down|gate|up)_exps\.weight$).*\.ffn_(down|gate|up)_exps\.weight=CPU'는 마지막 몇 개의 블록을 GPU RAM에 유지하기 위해 사용됩니다. 이 블록들은 더 역동적인 라우팅 (dynamical routing)을 수행하기 때문에, RAM 공간이 충분히 클 경우 오버헤드를 피하기 위해 GPU RAM에 유지하는 것이 더 빠릅니다. DGX Spark의 통합 RAM (Unified RAM) 시나리오에서는 메모리 풀이 크기 때문에 일부 전문가 블록을 VRAM에 고정 (pin)할 수 있습니다 (Run A). 만약 이들을 고정하지 않으면 더 느려집니다 (Run B). GGML_OP_OFFLOAD_MIN_BATCH 플래그를 제거하면 토큰 생성 (token generation) 속도가 더 느려지는데 (Run C), 이는 전문가 순전파 (expert forward pass)가 CPU에서 수행되기 때문입니다. 여기서 GGML_CUDA_ENABLE_UNIFIED_MEMORY 플래그까지 추가로 제거하면 (Run D), GPU RAM이 mmap 메커니즘을 통해 축출 (evict)될 수 없기 때문에 OOM (Out of Memory)이 발생합니다. 마지막으로, 모든 연산이 CPU에서 수행되면 가장 느립니다 (Run E). Run B와 Run C 사이에서 흥미로운 트레이드오프 (tradeoff)가 관찰되었는데, GGML_OP_OFFLOAD_MIN_BATCH가 PP (prompt processing)를 약간 저하시키는 대신 TG (token generation)를 극적으로 향상시킨다는 점입니다. 이 트레이드오프를 완화하기 위해 잠재적인 코드 수정이 이루어질 수 있습니다. 3090 + DDR4 조합의 비통합 RAM (Non-unified RAM) 하드웨어 시나리오의 경우, 심층적으로 테스트하지는 않았지만 이 전략이 어느 정도 적용될 수 있습니다. 24GB는 너무 작기 때문에 마지막 몇 개의 블록을 VRAM에 두지는 않았습니다. Run A의 TG가 일반적인 NG Split (Run B)보다 높은 것을 볼 수 있는데, 이는 전문가들이 GPU에서 계산되기 때문입니다. 하지만 프롬프트 처리 (prompt processing)는 PCIe 오버헤드 때문인지 성능 저하를 겪습니다. 3090 2개 + DDR4 조합도 테스트해 보았으나, 일반적으로 더 느립니다. 아마도 PCIe 전송 비용이 너무 높기 때문일 것입니다. 2개의 GPU 시나리오에서는 -ot offload를 적절히 튜닝하면 개선될 가능성이 있다고 생각합니다. 참고로 Mac Apple Silicon은 빠른 통합 RAM 덕분에 매력적이지만, 스왑 (swap)을 비활성화하는 쉬운 방법을 찾지 못했습니다. 제안된 접근 방식은 RAM 스와핑으로 인해 심각한 SSD 쓰기를 유발할 수 있습니다. SSD 마모를 초래할 수 있으므로 실행하는 것이 좋은 아이디어는 아닐 것입니다. 새로운 모델에서 이를 수행하는 권장 방식은 전문가를 먼저 오프로드 (offload)하고 (DGX Spark의 Run B 전략), 그 다음 점점 더 많은 전문가를 VRAM에 고정 (pin)하는 것입니다.
다가오는 Kimi K3 양자화 (quants) 모델에도 이 전략이 적용될 수 있을지 기대됩니다. 통합 메모리 (unified memory) 및 비통합 메모리 (non-unified memory) 아키텍처 전반에 걸쳐 동일한 전략을 이전할 수 있을 것입니다. 3090 + DDR4 설정에서의 토큰 생성 속도 (tg) 또한 개선되었습니다. 수정: /u/ylchao가 제출한 서식 수정 [link] [comments]
AI 자동 생성 콘텐츠
본 콘텐츠는 r/LocalLLaMA의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기