DGX Spark 실전기: Qwen3.6을 llama.cpp에서 vLLM + DFlash로 갈아타기
발단: 숫자가 너무 좋아서 의심스러웠다
llama.cpp로 Qwen3.6-35B-A3B을 쓰는 건 편하다. GGUF 파일 하나 받아서 llama-server 띄우면 끝이니까요. Q4_K_M 양자화 기준으로 싱글 스트림 40~50 tok/s 정도가 나오는데, 실사용에서 충분히 쾌적한 속도이다.
그런데 허깅페이스를 돌아다니다가 AEON-7이라는 사람이 올려놓은 벤치마크를 봤다. 같은 Qwen3.6-35B-A3B을 NVFP4 양자화하고 DFlash라는 스펙큘러티브 디코딩을 붙여서 싱글 스트림 83.9 tok/s, 정상 상태 115~118 tok/s를 찍었다고 한다. llama.cpp 대비 2~3배라는 건데, 직접 확인하지 않으면 믿기 어려운 수치였다.
그래서 직접 해봤다.
현재 환경: llama.cpp 기준선
비교를 위해 먼저 llama.cpp 환경을 정리한다. DGX Spark에서 Qwen3.6-35B-A3B GGUF를 돌릴 때의 일반적인 성능이다.
llama-server \
-m Qwen3.6-35B-A3B-UD-Q4_K_M.gguf \
-ngl 999 -c 32768 -fa \
--host 0.0.0.0 --port 8080
NVIDIA 개발자 포럼에 보고된 DGX Spark 실측 데이터를 종합하면, Q4_K_M에서 대략 45~50 tok/s, Q6_K_XL에서 38~45 tok/s, MXFP4_MOE에서 45~55 tok/s 정도가 나온다. 모델 크기는 12~20GB 범위로, 128GB 통합 메모리에서 아주 여유롭게 돌아간다.
llama.cpp의 강점은 단순함이다. GGUF 파일 하나, 바이너리 하나면 돌아간다. Docker도 필요 없고, 특별한 설정도 없다. 5분이면 모델을 돌릴 수 있다.
vLLM + NVFP4 + DFlash 설치: 실전 과정
사전 확인
# GPU 확인 — "NVIDIA GB10"이 보여야 함
nvidia-smi
# Docker + NVIDIA Container Toolkit
docker run --rm --gpus all nvidia/cuda:13.0-base-ubuntu24.04 nvidia-smi
# 디스크 여유 — 최소 50GB 필요
df -h /opt
모델 다운로드
두 개를 받아야 한다. NVFP4 양자화된 타겟 모델(~22GB)과 DFlash 드래프터(~905MB)이다.
huggingface-cli가 "command not found"로 안 되는 경우가 많은데, 몇 가지 대안이 있다.
sudo mkdir -p /opt/qwen36 && sudo chown $USER:$USER /opt/qwen36
cd /opt/qwen36
방법 1: Python 스크립트 (가장 확실) — CLI 도구의 PATH 문제나 xet 백엔드 충돌을 완전히 우회한다.
pip install -U huggingface_hub hf_transfer
HF_HUB_ENABLE_HF_TRANSFER=1 python3 << 'EOF'
from huggingface_hub import snapshot_download
snapshot_download(
repo_id="AEON-7/Qwen3.6-35B-A3B-heretic-NVFP4",
local_dir="/opt/qwen36/qwen36-nvfp4",
local_dir_use_symlinks=False
)
snapshot_download(
repo_id="z-lab/Qwen3.6-35B-A3B-DFlash",
local_dir="/opt/qwen36/qwen36-dflash",
local_dir_use_symlinks=False
)
print("다운로드 완료!")
EOF
방법 2: hf 명령어 — 작동한다면 더 간단하다.
pip install -U huggingface_hub[hf_xet]
export HF_HUB_ENABLE_HF_TRANSFER=1
hf download AEON-7/Qwen3.6-35B-A3B-heretic-NVFP4 --local-dir ./qwen36-nvfp4
hf download z-lab/Qwen3.6-35B-A3B-DFlash --local-dir ./qwen36-dflash
방법 3: Git LFS
sudo apt install git-lfs && git lfs install
git clone https://huggingface.co/AEON-7/Qwen3.6-35B-A3B-heretic-NVFP4 ./qwen36-nvfp4
git clone https://huggingface.co/z-lab/Qwen3.6-35B-A3B-DFlash ./qwen36-dflash
rm -rf ./qwen36-nvfp4/.git ./qwen36-dflash/.git # 디스크 절약
Docker 이미지
docker pull ghcr.io/aeon-7/vllm-spark-omni-q36:v1.2
약 9GB 압축 이미지이다. vLLM HEAD 소스 빌드(cu130/sm_120), FlashInfer 0.6.8, SM121 대응 8개 패치가 모두 포함되어 있다.
첫 번째 삽질: 오토튜닝으로 시스템 마비
처음에는 AEON-7의 공식 docker-compose.yml을 그대로 사용했다. max-model-len 262144, max-num-seqs 128, gpu-memory-utilization 0.85. 서버가 올라오는 과정에서 torch.compile과 autotuning이 돌아가기 시작했는데, 이 과정에서 시스템이 완전히 마비됐다. SSH도 안 먹히고, 전원 버튼으로 강제 재부팅하는 수밖에 없었다.
DGX Spark의 128GB 통합 메모리는 GPU와 CPU가 공유하기 때문에, vLLM이 메모리를 과도하게 잡아먹으면 OS 자체가 움직일 수 없게 된다. 공식 벤치마크 설정은 "이 머신을 이것만을 위해 쓴다"는 전제 하의 설정이었던 거다.
해결책은 두 가지였다. 오토튜닝을 끄고, 메모리를 보수적으로 잡는 것이다.
안정적인 설정: 실사용을 위한 docker-compose.yml
services:
vllm:
image: ghcr.io/aeon-7/vllm-spark-omni-q36:v1.2
container_name: vllm-qwen36-heretic
restart: unless-stopped
network_mode: host
environment:
- VLLM_ALLOW_LONG_MAX_MODEL_LEN=1
- TORCH_MATMUL_PRECISION=high
- PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
- NVIDIA_FORWARD_COMPAT=1
- VLLM_TEST_FORCE_FP8_MARLIN=1
- TORCHINDUCTOR_MAX_AUTOTUNE=0
- TRITON_MAX_AUTOTUNE=0
- VLLM_USE_V1=1
volumes:
- /opt/qwen36/qwen36-nvfp4:/models/qwen36
- /opt/qwen36/qwen36-dflash:/models/qwen36-dflash
command:
- bash
- -c
- |
exec vllm serve /models/qwen36 \
--served-model-name qwen36-think qwen36-fast \
--host 0.0.0.0 --port 8080 \
--tensor-parallel-size 1 \
--dtype auto \
--quantization compressed-tensors \
--max-model-len 131072 \
--max-num-seqs 2 \
--max-num-batched-tokens 4096 \
--gpu-memory-utilization 0.50 \
--enable-chunked-prefill \
--enable-prefix-caching \
--trust-remote-code \
--enable-auto-tool-choice \
--tool-call-parser qwen3_coder \
--reasoning-parser qwen3 \
--speculative-config '{"method":"dflash","model":"/models/qwen36-dflash","num_speculative_tokens":15}' \
--attention-backend flash_attn
ipc: host
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
공식 설정과 무엇이 달라졌나
TORCHINDUCTOR_MAX_AUTOTUNE=0과 TRITON_MAX_AUTOTUNE=0은 시스템 마비를 일으킨 오토튜닝을 끈다. gpu-memory-utilization을 0.85에서 0.50으로 낮췄다. 124GB의 50%인 약 62GB만 vLLM에 할당하고, 나머지 62GB는 다른 작업을 위해 남겨둔다. max-model-len은 262144에서 131072(128K)로, max-num-seqs는 128에서 2로, max-num-batched-tokens는 65536에서 4096으로 줄였다. 혼자 에이전트용으로 쓸 거라면 이 정도면 충분하다.
VLLM_TEST_FORCE_FP8_MARLIN=1은 필수이다. GB10의 SM12.1에서 NVFP4 MoE를 돌릴 때 유일하게 작동하는 백엔드가 Marlin이기 때문이다. 이 환경변수가 없으면 다른 백엔드가 시도되다가 실패한다.
핵심 트릭: 모델 이름으로 Thinking On/Off 분기
이 설정에서 가장 마음에 드는 부분이다. vLLM의 --served-model-name에 이름을 두 개 등록했다.
--served-model-name qwen36-think qwen36-fast
물리적으로는 같은 모델이지만, 클라이언트에서 어떤 이름으로 호출하느냐에 따라 다르게 쓸 수 있다.
qwen36-think으로 호출하면 Thinking 모드가 켜져서, 모델이 <think> 태그 안에 추론 과정을 먼저 생성한 뒤 최종 답변을 내놓는다. 수학, 코딩, 복잡한 추론에 유리하다.
qwen36-fast로 호출하면 Thinking 없이 바로 답변한다. 일반 대화나 빠른 응답이 필요할 때 사용한다.
서버에서 기본 Thinking을 끄고 싶다면 다음 플래그를 추가한다.
--default-chat-template-kwargs '{"enable_thinking": false}'
이렇게 하면 qwen36-fast는 기본적으로 Thinking OFF, qwen36-think으로 호출할 때 클라이언트에서 "chat_template_kwargs": {"enable_thinking": true}를 보내면 된다.
Hermes.ai 같은 에이전트 프레임워크에서는 모델을 두 개 등록해놓고 작업 성격에 따라 골라 쓸 수 있다. 코딩 에이전트는 qwen36-think, 단순 응답은 qwen36-fast. 메모리 추가 사용 없이 하나의 서버에서 두 가지 성격의 모델을 제공하는 셈이다.
실측 결과
모든 측정은 실제 DGX Spark(ASUS GX10, 128GB, GB10)에서 수행했다.
vLLM + NVFP4 + DFlash 속도
| 테스트 | 모드 | 생성 토큰 | 소요 시간 | 속도 |
|---|---|---|---|---|
| quicksort (영어) | think ON | 1,015 | 11.47초 | 88.4 tok/s |
| quicksort (영어) | think OFF | 837 | 8.66초 | 96.6 tok/s |
| 소수 찾기 (한국어) | think ON | 2,047 | 19.64초 | 104.3 tok/s |
| quicksort (영어, 이전 설정) | think ON | 2,048 | 21.52초 | 95.1 tok/s |
Thinking ON과 OFF에서 tok/s 자체는 비슷하지만(88~96 tok/s), Thinking OFF일 때 생성하는 총 토큰 수가 적기 때문에 체감 응답 시간은 훨씬 빠르다. quicksort 예제에서 11.47초 vs 8.66초, 약 25% 빠른 체감이다.
llama.cpp vs vLLM 직접 비교
| 환경 | 양자화 | 모델 크기 | 속도 | llama.cpp 대비 |
|---|---|---|---|---|
| llama.cpp Q4_K_M | INT4 GGUF | ~12GB | 45~50 tok/s | 1× |
| llama.cpp Q6_K_XL | INT6 GGUF | ~16GB | 38~45 tok/s | 0.9× |
| llama.cpp MXFP4_MOE | MXFP4 GGUF | ~12GB | 45~55 tok/s | 1.1× |
| vLLM NVFP4 + DFlash | NVFP4 W4A4 | ~22GB | 88~104 tok/s | ~2× |
메모리를 절반(0.50)으로 줄이고 오토튜닝을 꺼도 속도가 유지된다. 공식 벤치마크의 83.9 tok/s(중앙값)을 오히려 넘는 수치가 나왔는데, 동시 요청 수를 2로 줄여서 단일 요청에 리소스가 집중된 덕분으로 보인다.
메모리 사용량 비교
| 환경 | 메모리 사용 | 남는 메모리 |
|---|---|---|
| llama.cpp Q4_K_M | ~12GB | ~112GB |
| vLLM NVFP4+DFlash (0.85) | ~108GB | ~16GB |
| vLLM NVFP4+DFlash (0.50) | ~52GB | ~72GB |
gpu-memory-utilization 0.50 설정이 핵심이다. 52GB만 사용하면서 88~104 tok/s를 유지하고, 72GB를 다른 용도로 남겨둔다. llama.cpp만큼 여유롭진 않지만, 다른 모델이나 작업을 동시에 돌리기에 충분한 여유이다.
속도 차이가 나는 이유
같은 4비트 양자화인데 왜 2배 차이가 나는지 궁금할 수 있다. 세 가지 요인이 결합된 결과이다.
1. 하드웨어 FP4 텐서코어 활용
llama.cpp의 GGUF INT4는 소프트웨어로 디퀀타이징한 뒤 연산하지만, NVFP4는 GB10의 Blackwell FP4 텐서코어가 직접 연산한다. 같은 4비트여도 연산 경로가 근본적으로 다르다.
2. DFlash 스펙큘러티브 디코딩
905MB 크기의 경량 블록 디퓨전 모델이 한 번에 15개 토큰을 병렬 예측하고, 타겟 모델이 이를 한 번에 검증한다. 수학이나 코딩처럼 예측 가능한 패턴에서는 수용률이 62~78%에 달해서, 한 스텝에 평균 2.7~4.4개 토큰이 확정된다.
3. vLLM의 서빙 최적화
연속 배칭, KV 캐시 관리, CUDA 그래프 캡처 등 서버급 추론 최적화가 자동 적용된다.
트러블슈팅 모음
시스템 마비 / 리부트
원인은 거의 항상 메모리이다. gpu-memory-utilization을 0.50 이하로 낮추고, TORCHINDUCTOR_MAX_AUTOTUNE=0과 TRITON_MAX_AUTOTUNE=0을 반드시 설정하세요. 그래도 마비되면 --enforce-eager를 추가해서 CUDA graph 캡처를 건너뛰세요. 속도가 약간 떨어지지만(~42 tok/s) 안정성이 확보된다.
huggingface-cli가 안 됨
PATH 문제이거나 xet 백엔드 충돌이다. Python 스크립트로 snapshot_download()를 직접 호출하는 게 가장 확실하다. pip uninstall xet-core 후 재시도하는 것도 도움이 된다.
"Your GPU does not have native support for FP4"
로그에 이 경고가 나와도 정상 작동한다. Marlin 커널이 weight-only FP4를 처리하고 있다는 의미이다. 실측 속도가 88~104 tok/s 나오고 있으니 걱정할 필요 없다.
content: null, finish_reason: "length"
Thinking 모드에서 max_tokens가 너무 낮은 경우이다. Qwen3.6은 <think> 태그 안에 추론 과정을 먼저 생성하는데, 이 과정에서 토큰 예산을 소진해버린다. max_tokens를 2048 이상으로 올리거나, Thinking을 꺼서 해결한다.
visual.blocks... not found WARNING
비전 인코더 가중치 관련 경고이다. 텍스트 전용으로 사용할 경우 무시해도 된다.
결론: 언제 무엇을 써야 하나
| 시나리오 | 추천 | 이유 |
|---|---|---|
| 빠르게 테스트하고 싶다 | llama.cpp + GGUF | 5분이면 돌아감, Docker 불필요 |
| 최대 싱글 스트림 속도 | vLLM + NVFP4 + DFlash | 88~104 tok/s |
| 에이전트 / tool calling | vLLM + NVFP4 + DFlash | think/fast 모드 분기, 병렬 호출 지원 |
| 메모리 아껴서 다른 것도 돌려야 | llama.cpp Q4_K_M | 12GB면 충분 |
| 간편함/안정성 최우선 | llama.cpp + GGUF | 단일 바이너리, 설정 거의 없음 |
| 128K 장문 맥락 + 속도 | vLLM + NVFP4 + DFlash | 128K에서도 속도 유지 |
llama.cpp와 vLLM은 경쟁 관계가 아니라 용도가 다르다. llama.cpp는 "파일 하나로 바로 돌리는" 도구이고, vLLM은 "서비스를 구축하는" 엔진이다. 현재 제 DGX Spark에서는 vLLM + DFlash를 에이전트 서빙용으로 상시 운영하면서(gpu-memory-utilization 0.50), 남는 72GB로 다른 실험을 돌리고 있다.
다만 이 세팅에는 대가가 있다. Docker 의존성, SM12.1 전용 커스텀 이미지, 8개 패치가 적용된 비표준 vLLM 빌드, DFlash 드래프터 버전 관리, 그리고 첫 세팅에서의 시스템 마비 경험까지. llama.cpp의 단순함과는 거리가 먼 세계이다. 이 복잡성을 감수할 가치가 있는지는 각자의 상황에 달려 있지만, 속도 차이 2배는 한번 경험하면 돌아가기 어렵다.
다음 글 예고
DGX Spark에 172B 파라미터 모델을 우겨넣는 실험을 해볼 예정이다. MiniMax-M2.7-REAP-172B를 NVFP4로 양자화한 체크포인트가 허깅페이스에 올라와 있는데, 92GB짜리 모델을 128GB 메모리에 올리면 과연 쓸 만한 수준의 속도가 나올지 확인해보겠다.
2026년 4월 27일 작성. ASUS GX10 (DGX Spark, GB10, 128GB) 실측 기반.
참고: AEON-7/Qwen3.6-35B-A3B-heretic-NVFP4 · z-lab/Qwen3.6-35B-A3B-DFlash · GitHub 배포 가이드