DEV Community

EME GUG
EME GUG

Posted on

Running MoE LLMs Larger Than Your RAM: A Practical Guide to mmap, Expert Offloading and SSD Streaming with llama.cpp

Tuần này trên Hacker News có một project khá thú vị tên Janas: chạy LLM lớn hơn RAM bằng cách stream các expert từ SSD. Đọc xong mình nhớ lại hồi đầu năm, khi cố nhét một model MoE hơn 100B params vào con laptop 32GB RAM. Lần đầu thì máy treo cứng, swap kín ổ. Lần sau thì chạy được khoảng 8 tokens/s, vì lúc đó mình đã hiểu model MoE thực sự dùng bộ nhớ thế nào. Ở Việt Nam, card 24GB VRAM vẫn còn đắt, thuê GPU cloud tính theo USD cũng không rẻ, nên biết cách vắt kiệt phần cứng đang có là một kỹ năng đáng học. Bài này mình chia sẻ cách làm thực tế với llama.cpp: cái gì nên nằm trên GPU, cái gì để ở RAM, cái gì có thể để trên SSD.

Vì sao MoE chạy được khi lớn hơn RAM, còn dense model thì không

Với một dense model như Llama 3.1 70B, token nào cũng phải đi qua toàn bộ weights. Nếu weights không nằm hết trong RAM, mỗi token sẽ phải đọc lại hàng chục GB từ ổ đĩa. Kể cả NVMe Gen4 đọc được khoảng 7 GB/s thì tốc độ vẫn chỉ quanh mức 1 token mỗi vài giây, gần như không dùng được.

MoE (Mixture of Experts) thì khác. Lấy ví dụ gpt-oss-120b: tổng cộng khoảng 117B params, nhưng mỗi token chỉ kích hoạt khoảng 5.1B. Qwen3-30B-A3B là 30B tổng và khoảng 3B active. Router ở mỗi layer chọn vài expert (thường top-4 hoặc top-8 trong vài chục đến 128 expert), phần còn lại không bị đụng tới.

Từ đó có hai hệ quả:

  1. Attention, embedding và shared layers được dùng ở mọi token. Phần này nhỏ, nên đặt ở chỗ nhanh nhất là VRAM.
  2. Expert FFN weights chiếm phần lớn dung lượng nhưng mỗi lần chỉ một phần nhỏ được đọc. Phần này đặt ở RAM, và nếu RAM không đủ thì để OS page từ SSD vào.
flowchart LR
    T[Token input] --> A[Attention + Norm<br/>VRAM - luôn dùng]
    A --> R{Router}
    R -->|top-k| E1[Expert 3<br/>RAM / page cache]
    R -->|top-k| E2[Expert 17<br/>RAM / page cache]
    R -.không chọn.-> E3[Expert khác<br/>nằm trên SSD]
    E1 --> O[Output layer]
    E2 --> O

Điều mấu chốt là các expert không được chọn đều như nhau. Thực tế, với cùng một loại prompt (ví dụ toàn là code Python), router có xu hướng chọn đi chọn lại một nhóm expert. Linux page cache sẽ tự giữ nhóm “hot” này trong RAM, còn các expert “cold” nằm trên SSD và chỉ được đọc khi cần. Janas và các project tương tự tối ưu thêm ở tầng này: prefetch, cache LRU theo expert. Nhưng chỉ với mmap mặc định của llama.cpp, bạn đã có được phần lớn lợi ích.

Tính toán trước khi tải model 60GB về

Đừng tải model rồi mới biết là không chạy nổi. Mình dùng script nhỏ dưới đây để ước lượng trước. Nó đọc metadata GGUF bằng package gguf (pip install gguf, mình dùng bản 0.10+) và cộng dung lượng tensor theo nhóm:

# estimate_moe.py - ước lượng bộ nhớ cho model GGUF
import sys, re
from gguf import GGUFReader

path = sys.argv[1]
reader = GGUFReader(path)

expert_bytes, other_bytes = 0, 0
for t in reader.tensors:
    size = int(t.n_bytes)
    # tensor expert trong llama.cpp có tên dạng blk.N.ffn_up_exps.weight
    if re.search(r"ffn_(up|down|gate)_exps", t.name):
        expert_bytes += size
    else:
        other_bytes += size

gb = lambda b: b / 1024**3
print(f"Non-expert (nên ở VRAM): {gb(other_bytes):6.1f} GB")
print(f"Expert weights (RAM/SSD): {gb(expert_bytes):6.1f} GB")
print(f"Tổng:                     {gb(expert_bytes + other_bytes):6.1f} GB")
Enter fullscreen mode Exit fullscreen mode

Chạy thử với bản Q4 của một model khoảng 120B, mình thấy phần non-expert chỉ vài GB, vừa với card 8GB. Expert thì khoảng 55-60GB. Nếu máy có 64GB RAM thì gần như nằm gọn trong page cache. Máy 32GB thì một nửa sẽ phải đọc từ SSD, và đến lúc này tốc độ SSD bắt đầu ảnh hưởng thật.

Kinh nghiệm của mình:

  • RAM khoảng ≥ 70% dung lượng expert: tốc độ gần như không khác chạy full RAM.
  • RAM khoảng 40-70%: chạy được, tốc độ dao động theo prompt. Đổi chủ đề sẽ chậm vài giây đầu.
  • RAM < 40%: thường không đáng, nên chọn quant nhỏ hơn hoặc model nhỏ hơn.

Cấu hình llama.cpp: đẩy expert ra khỏi GPU

Từ khoảng giữa 2025, llama.cpp đã có flag --n-cpu-moe và --override-tensor (-ot) để điều khiển chính xác tensor nào nằm ở đâu. Đây là cấu hình mình đang dùng với card 12GB:

# Build bản mới nhất có CUDA
git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp
cmake -B build -DGGML_CUDA=ON && cmake --build build -j --config Release

# Cách 1: offload toàn bộ layer lên GPU, nhưng ép expert về CPU
./build/bin/llama-server \
  -m ~/models/gpt-oss-120b-Q4_K_M.gguf \
  -ngl 99 \
  -ot ".ffn_.*_exps.=CPU" \
  -c 16384 -t 8 --port 8080

# Cách 2: cú pháp gọn hơn, giữ expert của N layer đầu trên CPU
# Tăng/giảm N cho tới khi VRAM vừa đủ
./build/bin/llama-server -m ~/models/gpt-oss-120b-Q4_K_M.gguf \
  -ngl 99 --n-cpu-moe 30 -c 16384
Enter fullscreen mode Exit fullscreen mode

Một vài lưu ý mình học được sau khi thử sai:

  • Đừng thêm --no-mmap khi model lớn hơn RAM. Flag đó bắt llama.cpp load hết vào RAM, và process sẽ bị OOM killer giết. mmap mặc định để kernel tự quyết page nào giữ, page nào bỏ.
  • Đừng dùng --mlock trong trường hợp này. Nó khóa toàn bộ pages vào RAM và sẽ fail ngay.
  • -t (threads) nên đặt bằng số physical core, không phải số logic thread. Expert matmul trên CPU bị giới hạn bởi memory bandwidth, thêm thread hyperthreading thường còn chậm hơn.
  • Nếu dư VRAM, giảm --n-cpu-moe để đưa vài layer expert lên GPU. Mỗi layer chuyển lên thường tăng thêm vài phần trăm tốc độ.
sequenceDiagram
    participant G as GPU (VRAM)
    participant C as CPU
    participant P as Page cache (RAM)
    participant S as NVMe SSD
    G->>G: Attention layer N
    G->>C: hidden state + expert ids
    C->>P: đọc weights expert
    P-->>S: page fault (expert cold)
    S-->>P: load pages
    P-->>C: weights
    C->>G: kết quả FFN

Theo dõi xem SSD có đang là bottleneck không

Khi tokens/s thấp hơn dự kiến, việc đầu tiên mình làm là xem ổ đĩa có đang bị đọc liên tục không. Trên Linux:

# Terminal 1: theo dõi read throughput của NVMe mỗi giây
iostat -xm 1 nvme0n1

# Terminal 2: đếm major page fault của llama-server
PID=$(pgrep -f llama-server)
watch -n1 "ps -o maj_flt,rss -p $PID"

# Xem bao nhiêu phần của file model đang nằm trong page cache
# (cần cài vmtouch: apt install vmtouch)
vmtouch -v ~/models/gpt-oss-120b-Q4_K_M.gguf | tail -3
Enter fullscreen mode Exit fullscreen mode

Cách đọc kết quả:

  • rMB/s trong iostat cao liên tục (vài GB/s) trong lúc generate nghĩa là đang thrash. Working set lớn hơn RAM, cần giảm context, dùng quant nhỏ hơn, hoặc đóng bớt Chrome.
  • maj_flt tăng mạnh ở vài chục token đầu rồi đứng yên là bình thường, đó là giai đoạn warm-up.
  • vmtouch cho biết khoảng bao nhiêu % file đã resident. Nếu muốn warm cache trước khi demo, chạy vmtouch -t lên file model.

Trên macOS với Apple Silicon thì hơi khác, vì unified memory làm ranh giới giữa VRAM và RAM bị xóa. Lúc này bạn chỉ cần quan tâm tổng RAM và dùng sudo fs_usage -f diskio | grep llama để xem ổ đĩa có bị đọc liên tục không.

Kết luận

Chạy model lớn hơn RAM không phải là phép màu. Nó chỉ khả thi với MoE và chỉ hiệu quả khi working set của expert vừa với page cache. Những gì nên làm ngay:

  1. Chọn MoE thay vì dense nếu phần cứng hạn chế. Một model 30B-A3B trên laptop 16GB thường cho trải nghiệm tốt hơn một dense 14B bị quant quá tay.
  2. Chạy script ước lượng trước khi tải về để biết tỷ lệ RAM so với dung lượng expert.
  3. Dùng -ngl 99 cùng --n-cpu-moe (hoặc -ot) để attention ở GPU, expert ở CPU. Giữ mmap mặc định, bỏ --no-mmap và --mlock.
  4. Đầu tư SSD NVMe Gen4 trước khi nâng GPU, nếu bạn hay chạy model lớn. Đọc ngẫu nhiên nhanh ảnh hưởng trực tiếp đến tốc độ khi cache miss.
  5. Đo bằng iostat và vmtouch thay vì đoán. Đa số lần chậm mình gặp đều do thrash chứ không phải do CPU.

Các project như Janas đang đẩy hướng này đi xa hơn với prefetch thông minh theo router. Nhưng nếu nắm chắc nền tảng mmap và tensor placement, bạn đã có thể chạy những model mà một năm trước phải cần server mới chạy nổi, ngay trên máy cá nhân.

Top comments (0)