편찌

Qwen3.8-Flash-Next 로컬 구동 가이드 — 125B MoE를 RTX PRO 6000 한 장으로 (llama.cpp)

지난 글에서 RTX PRO 6000 Blackwell 96GB 사서 Qwen3.8-Flash-Next 125B를 초당 100토큰으로 돌린다고 자랑만 하고 끝냈는데, 댓글로든 어디서든 나올 질문이 뻔하다. “그래서 그거 어떻게 돌리는데?” — 이번 글이 그 상세편이다. llama.cpp 빌드부터 모델 다운로드, 벤치마크, 최종 실행 스크립트까지 전 과정을 정리했다. 셋업하면서 예상 못 한 반전이 세 번 있었는데, 그게 이 글의 재미 포인트니까 끝까지 읽어보자. 모든 수치는 내 머신(RTX PRO 6000 Blackwell + RTX 5090)에서 어제오늘 직접 실측한 값이다.

TL;DR — 알리바바가 새로 공개한 125B MoE 멀티모달 모델 Qwen3.8-Flash-Next를 llama.cpp로 로컬 구동했다. 원래 “두 GPU에 나눠 싣고 넘치는 건 RAM으로” 계획이었는데, 뚜껑을 열어보니 96GB 한 장이면 262K 풀 컨텍스트까지 들어가고, 그게 제일 빠르다. 비결은 이 모델의 26.8GiB짜리 n-gram 테이블이 llama.cpp에서 mmap lazy-read로 처리돼 VRAM에 아예 올라가지 않는다는 것. 단일 GPU에서 프롬프트 333 t/s, 생성 104 t/s.

Qwen3.8-Flash-Next가 RTX PRO 6000 Blackwell에서 토큰을 실시간 생성하는 GIF
결과물 미리보기. 이 속도가 로컬에서 나온다

1. Qwen3.8-Flash-Next가 뭐길래

Qwen3.8-Flash-Next는 “Qwen4 아키텍처 프리뷰”라는 부제가 붙은 실험적 모델이다. GGUF 메타데이터의 general.description에도 “A Preview of the Qwen4 Architecture”라고 박혀 있다. 스펙이 상당히 특이하다.

  • 125B 파라미터 MoE, 활성은 6B — 512개 전문가 중 토큰당 10개만 활성화
  • 여기에 51B짜리 n-gram 임베딩 테이블(PLE, per-layer embeddings)과 4B MTP(멀티토큰 예측) 헤드가 별도로 붙음 → BF16 원본이 330GiB
  • 하이브리드 어텐션: Gated DeltaNet(선형 어텐션) + QSA(Qwen Sparse Attention), 4레이어마다 풀 어텐션 1개
  • 네이티브 262,144 토큰 컨텍스트, YaRN으로 1M까지
  • 이미지+비디오 입력 멀티모달 (768px 패치16 비전 인코더, 27블록)

핵심 특이점은 n-gram 테이블이다. 16개 헤드 × 각 2천만 어휘 × 160차원짜리 거대한 룩업 테이블(약 3.2억 행)인데, 토큰당 몇 행만 sparse gather로 읽는다. “파라미터는 많지만 읽는 건 찔끔”이라는 이 성격이 뒤에서 중요한 반전을 만든다.

llama.cpp 웹UI에서 Qwen3.8-Flash-Next가 한국어 질문에 생성 102.07 t/s로 답변하는 화면
한국어로 물으면 한국어로 답한다. 생성 102.07 t/s — 클라우드 챗봇 안 부럽다

2. 하드웨어와 목표

항목사양
GPU0RTX PRO 6000 Blackwell Workstation Edition 96GB (sm_120)
GPU1ASUS ROG Astral RTX 5090 32GB (sm_120)
GPU 연결PCIe(PHB), NVLink 없음
CPU라이젠9 9950X3D (16C/32T)
RAM128GB (DDR5-6000 풀뱅)
OSUbuntu 24.04, 드라이버 610.43.02, CUDA 13.3
이 본체 맞춘 이야기는 지난 구매 후기 글 참고

목표: unsloth의 UD-Q4_K_XL 양자화(103.7GiB)를 llama.cpp로 서빙하기. 당초 계획은 “두 GPU 합산 128GB VRAM에 분산 + n-gram 테이블은 RAM 오프로드”였다. 이 계획이 어떻게 무너지는지(좋은 쪽으로) 보자.

3. 셋업

3.1 llama.cpp 지원 확인 — 아키텍처 이름 함정 (반전 1)

llama.cpp 소스를 뒤지면 qwen3next라는 아키텍처가 눈에 들어오는데, 이 모델은 그게 아니다. GGUF 헤더를 읽어보면:

general.architecture = qwen4exp
general.description  = A Preview of the Qwen4 Architecture
general.size_label   = 512x56B
qwen4exp.expert_count = 512
qwen4exp.expert_used_count = 10
qwen4exp.context_length = 262144

HF 원본 config의 Qwen4ExpForConditionalGeneration에 대응하는 별도 아키텍처 qwen4exp가 llama.cpp에 정식 등록돼 있다(src/models/qwen4exp.cpp). PLE 전용 텐서 타입까지 1급 지원. 마스터 브랜치가 이 모델 관련 커밋으로 활발히 움직이는 중이라 무조건 최신 master를 빌드하는 게 맞다. 릴리즈 바이너리 기다리지 말자.

3.2 CUDA 빌드 (Blackwell sm_120)

git clone --depth 1 https://github.com/ggml-org/llama.cpp.git
cd llama.cpp
cmake -B build -G Ninja \
  -DCMAKE_BUILD_TYPE=Release \
  -DGGML_CUDA=ON \
  -DCMAKE_CUDA_ARCHITECTURES=120 \
  -DCMAKE_CUDA_HOST_COMPILER=/usr/bin/g++-13 \
  -DCMAKE_C_COMPILER=/usr/bin/gcc-13 -DCMAKE_CXX_COMPILER=/usr/bin/g++-13
cmake --build build --config Release -j 32
  • CUDA 13.x 툴킷 필요 (sm_120은 CUDA 12.8부터). cmake가 알아서 120a로 치환한다
  • 컴파일러는 gcc-13 사용 (우분투 24.04 기본 gcc도 되긴 하는데 CUDA 13에는 13이 무난)
  • 9950X3D 32스레드 빌드로 약 2분 만에 끝났다
  • 듀얼 GPU면 NCCL 없다는 경고가 뜨는데 무시해도 된다 (layer split은 NCCL 안 씀)

3.3 모델 다운로드

unsloth 리포의 양자화 라인업(실측 크기): BF16 329.7 / Q8_0 175.3 / UD-Q6_K_XL 157.5 / UD-Q5_K_XL 147.4 / UD-Q4_K_XL 103.7 / UD-IQ4_XS 87.2 / UD-Q2_K_XL 73.5 / UD-IQ1_S 67.6 GiB. 나는 품질-용량 밸런스로 UD-Q4_K_XL을 골랐다.

pip install -U "huggingface_hub[cli]"
hf download unsloth/Qwen3.8-Flash-Next-GGUF \
  UD-Q4_K_XL/Qwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00004.gguf \
  UD-Q4_K_XL/Qwen3.8-Flash-Next-UD-Q4_K_XL-00002-of-00004.gguf \
  UD-Q4_K_XL/Qwen3.8-Flash-Next-UD-Q4_K_XL-00003-of-00004.gguf \
  UD-Q4_K_XL/Qwen3.8-Flash-Next-UD-Q4_K_XL-00004-of-00004.gguf \
  mmproj-F16.gguf \
  --local-dir ~/models/Qwen3.8-Flash-Next-GGUF

mmproj-F16.gguf(0.84GiB)는 비전 인코더다. 빼먹으면 텍스트 전용이 된다. 참고로 --include "UD-Q4_K_XL/*" 패턴이 hf CLI 버전에 따라 무시되는 경우가 있어서 파일명을 명시하는 쪽이 안전했다. 다운로드는 내 회선 기준 약 25분.

4. 반전 2 — n-gram 테이블은 VRAM에 올라가지 않는다

원래 계획대로 “n-gram 테이블을 RAM으로 오프로드”하려고 텐서 이름을 찾다가 두 가지 사실을 발견했다.

첫째, 이름 함정. blk.1.ple_key/ple_value 같은 텐서가 눈에 띄는데 이건 각각 {2560×320}, {2560×2560}짜리 소형 투영 행렬이다. 진짜 51B 테이블은 per_layer_token_embd.weight — {160 × 320,001,536} 형태의 단일 거대 텐서로, IQ4_NL 양자화 기준 26.8GiB다.

둘째, 진짜 반전. llama.cpp 소스에서 이 텐서는 TENSOR_READ_LAZY 플래그로 로드된다:

// src/llama-model-loader.h
static const int TENSOR_READ_LAZY = 1 << 5;
// read rows on demand instead of loading whole tensor; requires mmap for now

// src/models/qwen4exp.cpp
per_layer_tok_embd = create_tensor(tn(LLM_TENSOR_PER_LAYER_TOKEN_EMBD, "weight"),
                                   { hparams.ple_head_dim, ple_rows }, TENSOR_READ_LAZY);

26.8GiB짜리 테이블은 VRAM에도 RAM에도 통째로 올라가지 않고, mmap된 GGUF 파일에서 필요한 행만 그때그때 읽힌다. 토큰당 몇 행짜리 gather니까 이렇게 해도 성능 손해가 거의 없다. 103.7GiB 파일 중 GPU에 실제로 올라가는 건 약 77GiB뿐. 내가 하려던 “RAM 오프로드”를 llama.cpp가 이미 훨씬 똑똑한 방식으로 하고 있었던 거다.

여기서 실전 규칙 두 개가 나온다:

  1. --no-mmap 금지 — lazy 경로 자체가 mmap을 요구한다
  2. -ot "per_layer_token_embd=CPU" 금지 — 강제로 CPU 버퍼에 올리면 lazy read가 무력화되고, 실측으로 프롬프트 처리량이 40% 떨어졌다 (아래 벤치)

5. 벤치마크 — 한 장이 이긴다 (반전 3)

동일 조건(32K 컨텍스트, 160토큰 생성)으로 세 가지 배치를 비교했다.

구성프롬프트생성피크 VRAM
단일 GPU (PRO 6000만)333.1 t/s103.6 t/s79.0 GiB
듀얼 GPU (-sm layer -ts 77,23)324.9 t/s101.1 t/s81.7 GiB (합산)
듀얼 + n-gram 강제 CPU 오프로드202.6 t/s96.0 t/s
단일 GPU 승. 괜히 나누면 손해다
  • 두 장에 나누면 오히려 느리다. 내 보드에서 두 GPU는 PHB(PCIe 호스트 브리지) 연결이라 레이어 경계마다 액티베이션이 PCIe를 타기 때문. NVLink가 없으면 “나눌 수 있어도 나누지 마라”
  • n-gram 강제 오프로드는 최악 — lazy mmap이 이미 최적인 걸 손으로 망가뜨리는 셈
  • 262K 풀 컨텍스트도 단일 GPU에 들어간다: 피크 87.5/95.6 GiB, OOM 없음
Qwen3.8-Flash-Next 생성 요청 처리 중 nvidia-smi - RTX PRO 6000이 90% 사용률, 298W, 91GB VRAM
생성 요청 처리 중 nvidia-smi. GPU0(RTX PRO 6000) 사용률 90%, 298W, 91GB — 5090은 놀고 있다

125B 모델이 생성 100 t/s를 넘기는 건 활성 6B + Blackwell 대역폭 덕분이다. 비전도 확인했다. mmproj 붙이고 이미지를 던지면 정상 인식한다(OpenAI 호환 image_url 형식). 웹UI에서 한국어 질의 시에도 생성 102 t/s가 그대로 나온다.

표준 llama-bench 결과(단일 GPU, -fa 1, r=2):

| model                      |       size | test   |            t/s |
| -------------------------- | ---------: | -----: | -------------: |
| qwen4exp A3B Q4_K - Medium | 103.68 GiB | pp512  | 734.14 ± 100.58|
| qwen4exp A3B Q4_K - Medium | 103.68 GiB | pp4096 | 836.29 ±  14.27|
| qwen4exp A3B Q4_K - Medium | 103.68 GiB | tg128  | 100.73 ±   0.74|

배치가 충분히 크면 프롬프트 처리 800 t/s대까지 나온다(위 3-way 비교의 333 t/s는 짧은 프롬프트 1회짜리 실측이라 낮게 잡힌 것). 참고로 llama.cpp가 params를 176.94B로 표기하는 건 n-gram 테이블과 MTP까지 합산해 세기 때문이다.

6. 최종 실행 커맨드

CUDA_VISIBLE_DEVICES=0 llama-server \
  -m ~/models/Qwen3.8-Flash-Next-GGUF/UD-Q4_K_XL/Qwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00004.gguf \
  --mmproj ~/models/Qwen3.8-Flash-Next-GGUF/mmproj-F16.gguf \
  -ngl 99 -fa auto -c 262144 -t 16 -np 1 \
  --temp 1.0 --top-k 20 --top-p 0.95 \
  --jinja --host 0.0.0.0 --port 8080

각 플래그를 이렇게 잡은 이유:

  • CUDA_VISIBLE_DEVICES=0 — 96GB 한 장만. 나눠봤자 느려진다 (§5)
  • -c 262144 — 풀 컨텍스트. 87.5GiB로 여유 있음
  • --temp 1.0 --top-k 20 --top-p 0.95 — Qwen이 GGUF 메타데이터(general.sampling.*)에 박아둔 공식 권장값. 샘플링 잘못 잡으면 루프 돈다
  • --jinja — 이미지/비디오 블록이 든 정식 chat template 활성화
  • -np 1중요. §7 참고

서버 뜨면 브라우저로 http://서버IP:8080 열면 내장 웹UI가 바로 나온다. 별도 프론트엔드 설치가 필요 없어서 요즘 llama.cpp 웹UI만 쓰게 된다. 나는 우분투 본체에 서버 띄워두고 맥북에서 접속해서 쓴다.

llama-server 내장 웹UI 첫 화면 - Qwen3.8-Flash-Next 모델명 배지
llama-server 내장 웹UI. 하단에 로드된 모델명 배지가 뜬다

7. 주의사항 — 아직 이른 부분들

이 모델의 llama.cpp 지원은 병합된 지 얼마 안 됐다(2026년 8월 말). 발행 직전에 상태를 다시 확인했는데, 아래 이슈들이 여전히 열려 있다. 패턴이 뚜렷한데 전부 동시 배칭에서 터진다:

  • #27780 — 지속 부하에서 그래프 빌더 ggml_abort (단일/짧은 요청은 정상)
  • #27911-np 10 동시 배칭에서 CUDA “invalid configuration argument”
  • #28019 — 멀티 시퀀스 replay 시 recurrent 상태 손상

그래서 -np 1(단일 슬롯)이 현재로선 알려진 버그가 없는 유일한 경로다. 나처럼 개인용으로 쓰면 아무 문제 없고, 동시 사용자를 받아야 하는 상황이면 이슈 정리 전까지 보류를 권한다.

정확도 쪽 미병합 PR도 있다 (역시 발행 시점 기준 전부 미병합):

  • #27879 — QSA 블록 선택 등 correctness 수정 모음
  • #27941이미지 토큰이 블록 하나로 뭉개지는 버그 수정 포함
  • #28068 — GDN 정규화 maxrsqrt 수정

특히 #27941 때문에 비전 출력은 텍스트보다 신뢰도가 낮다고 봐야 한다. 간단한 이미지는 잘 읽지만 복잡한 이미지·장문맥·멀티이미지에서는 조용히 품질이 샐 수 있다. 몇 주 뒤 master 업데이트하고 다시 빌드하면 해결될 일이다.

8. 보너스 — 내가 쓰는 실행 스크립트

맨날 긴 커맨드 치기 귀찮아서 모드별 스크립트로 만들어뒀다. solo(기본, 제일 빠름) / dual(컨텍스트나 배치를 한 장 한계 너머로 올릴 때만) / cli(원샷 테스트용) 세 가지다.

#!/usr/bin/env bash
# ./run-qwen38.sh solo | dual | cli
set -euo pipefail
LCPP=~/llama.cpp/build/bin
DIR=~/models/Qwen3.8-Flash-Next-GGUF
MODEL=$DIR/UD-Q4_K_XL/Qwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00004.gguf
MMPROJ=$DIR/mmproj-F16.gguf
CTX=${CTX:-262144}   # CTX=65536 ./run-qwen38.sh 처럼 오버라이드 가능

COMMON=( -m "$MODEL" --mmproj "$MMPROJ" -ngl 99 -fa auto -t 16 -c "$CTX" -np 1
         --temp 1.0 --top-k 20 --top-p 0.95 )

case "${1:-solo}" in
  solo)  export CUDA_VISIBLE_DEVICES=0
         exec "$LCPP/llama-server" "${COMMON[@]}" --jinja --host 0.0.0.0 --port 8080 ;;
  dual)  exec "$LCPP/llama-server" "${COMMON[@]}" -sm layer -ts 77,23 --main-gpu 0 \
           --jinja --host 0.0.0.0 --port 8080 ;;
  cli)   export CUDA_VISIBLE_DEVICES=0
         exec "$LCPP/llama-cli" "${COMMON[@]}" -st "${@:2}" ;;
esac

9. 정리

질문
125B MoE를 로컬에서?된다. 96GB 한 장이면 충분
속도는?프롬프트 333 t/s(배치 크면 800대), 생성 104 t/s
262K 컨텍스트?단일 GPU에서 87.5GiB로 소화
듀얼 GPU 값어치?NVLink 없으면 오히려 마이너스
여러 명이 같이 쓰는 서빙?-np 1로만. 동시 배칭은 아직 지뢰밭

“355GB 모델”이라는 숫자에 겁먹을 필요가 없었다. MoE 활성 6B + lazy n-gram 테이블이라는 구조 덕에 실제 GPU 상주 워킹셋은 77GiB급이다. 이번에 다시 배운 교훈: 새 아키텍처가 나오면 스펙 숫자보다 런타임이 그걸 어떻게 적재하는지를 먼저 봐야 한다. 계획했던 “듀얼 GPU + RAM 오프로드”는 전부 필요 없었고, 심지어 했으면 더 느려졌을 거다.

다음 스텝은 이 머신으로 VLA 모델 파인튜닝 돌리는 거다. 그 삽질기도 정리되는 대로 올리겠다. 질문은 댓글로 — 특히 다른 양자화(IQ4_XS로 완전 온-GPU 같은 거) 벤치 궁금한 사람 있으면 돌려서 답글 달아줄게.

측정 환경: llama.cpp master(commit daef7b6, 2026-08-31), CUDA 13.3, Ubuntu 24.04. 이 글의 모든 수치는 직접 실측한 로그 기반이다.


📌 함께 보면 좋은 글

댓글 1개

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다