
cpu-moe에 만족하지 마세요: Qwen 35B를 더 빠르게 만드는 3가지 플래그 실측
요약
llama.cpp를 사용하여 RTX 4070 환경에서 Qwen 3.5 35B 모델의 추론 속도를 최적화하는 방법을 다룹니다. cpu-moe 설정 외에 추가적인 3가지 플래그를 적용하여 성능을 약 1.2배 향상시킨 실측 결과를 공유합니다.
핵심 포인트
- RTX 4070 환경에서 Qwen 3.5 35B 모델의 추론 속도를 34.6에서 41.2 tok/s로 향상
- llama.cpp의 추가 플래그 활용을 통해 기존 cpu-moe 구성보다 높은 성능 달성 가능
- Flash Attention 적용 시 KV 캐시 메모리 액세스 효율 개선 및 대역폭 제한 완화
- 정확한 성능 측정을 위해 베이스라인 재현 및 반복 측정의 중요성 강조
먼저 결론부터 쓰겠습니다. RTX 4070 (12GB)에서 Qwen3.5-35B-A3B를 llama.cpp를 통해 구동할 때, -ngl 99 --cpu-moe 이 두 가지 플래그만 사용하고 멈추는 것은 아깝습니다. 제가 동일한 머신에서 추가로 3가지 플래그를 적용해 본 결과, 34.6 tok/s에서 41.2 tok/s까지 향상되었습니다. 약 1.2배 정도의 차이지만, 체감되는 응답 시간은 명확하게 짧아집니다.
"전문가(Expert)를 전부 CPU로 넘긴다"는 승리 구성에 대한 이야기는 이전 기사에서 다루었습니다. 그것만으로도 확실히 Ollama 대비 2.8배의 성능을 낼 수 있습니다. 하지만 그 너머에는 아직 적용할 수 있는 플래그가 남아 있습니다. 이 기사는 그 3가지 플래그를 실측을 통해 추적한 기록입니다.
솔직히 말하면, 저는 처음에 cpu-moe를 넣었을 때 이미 상당히 만족하고 있었습니다. "이 이상은 양자화(Quantization) 수준을 낮추거나 모델을 바꾸는 수밖에 없을 것이다"라고 단정 지어 생각했고, 남은 플래그들은 절반 정도 읽지 않고 넘겼던 것이 몇 주 전의 일입니다. 그러던 어느 날 llama.cpp의 릴리스 노트를 살펴보던 중, --flash-attn의 4070 지원이 명시되어 있는 것을 발견했고, 재미 삼아 적용해 보았더니 숫자가 실제로 올라가는 것을 확인한 것이 이번 기사의 출발점입니다. 선입견 때문에 멈춰 서 있던 사람의 기록으로서 읽어주시면 적절할 것 같습니다.
구분해 두겠습니다. 과거에 작성한 "cpu-moe로 2.8배" 기사는 -ngl 99 --cpu-moe -c 4096이라는 승리 구성의 입구에 대한 이야기였습니다. 그것을 바닥으로 삼아, 그 너머를 채워나가는 것이 본 기사의 범위입니다.
- 대상: cpu-moe를 이미 사용 중인 사람
- 검증: llama.cpp
llama-bench를-r 3이상으로 반복, 표준 편차 포함 - 초점: cpu-moe에 추가하는 3가지 플래그의 효과
- 제외: 양자화 레벨(Q4/Q8/FP16)이나 다른 모델과의 비교는 하지 않음
"더 큰 모델로 교체한다"거나 "vLLM으로 바꾼다"와 같은 런타임 교체에 관한 이야기도 이번에는 다루지 않습니다. 동일한 Qwen3.5-35B-A3B, 동일한 Q4_K_M, 동일한 RTX 4070을 고정하고 플래그만 변경합니다.
비교를 왜곡하지 않기 위해 플래그 이외의 모든 조건은 고정했습니다.
- 머신: RTX 4070 (VRAM 12GB) / DDR5 64GB / WSL2 Ubuntu 24.04
- CUDA: 12.9
- 모델: Qwen3.5-35B-A3B Q4_K_M (20.49 GiB, 34.66B params)
- llama.cpp: 2026-07 시점의 최신 빌드 (CUDA 활성화)
- 측정:
llama-bench -n 128 -r 3(tg128, 3회 반복, 표준 편차 표기) - 동행 프로세스: 측정 전 VRAM을 해제한 후 시작
- 베이스라인 병행: 매번 순수한
-ngl 99 --cpu-moe로 34.6 tok/s ± 0.4가 나오는 것을 확인한 후 플래그를 추가
측정 방법론에 대해서는 로컬 LLM 서적 (local-llm-qwen-4070)의 제4장에 정리해 두었습니다. 여기서도 그 방법론을 그대로 따릅니다.
단 한 번만 측정하고 "빨라졌다"라고 말하는 것은 금물입니다. 로컬 LLM의 측정은 VRAM의 여유 상태와 사고 모드(Thinking mode)의 취급 방식에 따라 쉽게 흔들립니다. "기존의 베이스라인 값이 동일한 머신에서 재현되는가"를 매번 체크하면, 오염된 환경을 가장 빠르게 찾아낼 수 있습니다.
먼저 첫 번째. --flash-attn은 어텐션(Attention) 계산을 블록 단위로 GPU에 올리는 구현으로 전환합니다. KV 캐시(KV Cache)의 메모리 액세스 패턴이 바뀌며, 대역폭 제한(Bandwidth-bound)이 발생하는 구간이 빨라집니다.
제 환경에서의 실측 결과는 다음과 같습니다.
| 설정 | 생성 tok/s (tg128, r=3) |
|---|---|
-ngl 99 --cpu-moe (base) | 34.6 ± 0.4 |
base + --flash-attn | 38.9 ± 0.5 |
+4.3 tok/s. 향상 폭은 약 12%입니다. 향상되는 이유는 단순합니다. 35B 클래스의 희소 MoE(Sparse MoE) 모델이라 하더라도 어텐션 자체는 밀집 연산(Dense operation)이기 때문에, 구현 차이가 그대로 스루풋(Throughput) 차이로 나타납니다.
주의할 점은 "반드시 빨라진다는 보장은 없다"는 것입니다. llama.cpp의 --flash-attn
은 CUDA 버전과 GPU 세대(SM)에 따라 활성화 여부가 결정됩니다. RTX 4070 (Ada, SM89)에서는 효과가 있었지만, 오래된 GPU에서는 회피 로직이 작동하여 속도 차이가 거의 zero가 될 수 있습니다. 자신의 환경에서 llama-bench를 한 번 실행하여 확인하는 것이 빠릅니다.
두 번째는 KV 캐시 (KV cache) 양자화입니다. 기본 KV 캐시는 fp16으로 유지하지만, 생성 중에 읽고 쓰기만 한다면 q8_0로도 충분히 정밀도를 유지할 수 있습니다. VRAM 여유가 생기며, 그만큼 어텐션 (Attention) 대역폭이 효과를 발휘합니다.
| 설정 | 생성 tok/s (tg128, r=3) |
|---|---|
| base | 34.6 ± 0.4 |
base + --cache-type-k q8_0 --cache-type-v q8_0 | 37.2 ± 0.3 |
+2.6 tok/s (약 7.5%). 단독으로는 플래시 어텐션 (Flash Attention)만큼 크게 늘어나지는 않지만, 다음 배치 사이즈 (Batch size)와 조합하면 효과가 나타납니다.
품질 측면의 영향은 local-llm-qwen-4070 [IMG:1] 제5장의 표준 7문항으로 비교한 결과, q8_0에서도 전 문항 정답률이 떨어지지 않았습니다. q4_0까지 낮추면 명확하게 저하되므로, 경계선은 q8_0입니다. VRAM이 아슬아슬한 모델에서 캐시 (cache)를 가볍게 하고 싶을 때, 저는 이 설정을 가장 먼저 적용합니다.
KV 캐시의 크기는 컨텍스트 길이 (Context length)에 대해 선형적으로 작용합니다. -c 4096을 -c 8192로 늘리고 싶을 때는 KV 양자화와 세트로 검토하면 VRAM 오버플로우가 일어나기 어려워집니다.
세 번째는 입력 측의 이야기입니다. 생성 (tg128)의 tok/s만 보면 이해하기 어렵지만, 프롬프트 (Prompt)가 길어질수록 효과가 큽니다. --batch-size와 --ubatch-size를 크게 잡으면 프롬프트 평가 (Prompt eval)의 처리량 (Throughput)이 올라가며, 실제 채팅이나 에이전트 (Agent) 용도에서 "답변이 시작되기까지의 체감 시간"이 단축됩니다.
llama-bench에서는 pp512 (Prompt eval 512 토큰)와 tg128을 모두 출력할 수 있습니다.
| 설정 | pp512 tok/s | tg128 tok/s |
|---|---|---|
| base | 780 ± 20 | 34.6 ± 0.4 |
base + --batch-size 512 --ubatch-size 512 | 1,140 ± 30 | 35.1 ± 0.5 |
pp512는 +46%. 생성은 오차 범위 내이지만, 이것은 프롬프트 평가 (Prompt eval)에 관한 이야기입니다. 600 토큰 정도의 입력을 던지는 것만으로도 첫 번째 토큰 (First-token)까지의 시간이 체감될 정도로 짧아집니다.
--ubatch-size를 높이면 VRAM 사용량도 증가합니다. RTX 4070에서는 512까지는 넘치지 않고 버티지만, 1024로 설정하면 --cpu-moe와 조합해도 한계를 초과합니다. 머신 구성마다 상한선을 직접 측정하며 찾아야 합니다.
여기까지는 단독 효과를 살펴보았습니다. 모든 플래그를 다 적용했을 때의 수치는 다음과 같습니다.
| 설정 | pp512 tok/s | tg128 tok/s | 베이스라인 대비 |
|---|---|---|---|
| Ollama 기본값 | — | 12.2 ± 0.8 | 0.35배 |
-ngl 99 --cpu-moe (cpu-moe 단독) | 780 ± 20 | 34.6 ± 0.4 | 1.00배 |
+ --flash-attn | 820 ± 15 | 38.9 ± 0.5 | 1.12배 |
+ --flash-attn + KV q8_0 | 840 ± 20 | 40.4 ± 0.4 | 1.17배 |
| + 3개 플래그 모두 적용 | 1,190 ± 25 | 41.2 ± 0.4 | 1.19배 |
생성은 34.6 → 41.2 tok/s로, +6.6 tok/s (약 1.2배)입니다. 프롬프트 평가는 780 → 1,190 tok/s로, 이쪽은 1.5배입니다. 실제 운용에서의 사용자 체감은 이 pp512의 상승폭이 더 크게 나타납니다. 3초 만에 채팅이 시작되던 것이 2초 만에 돌아오는 듯한 느낌에 가깝습니다.
동일한 명령어를 복사해서 붙여넣어도 이 41.2 tok/s가 나오지 않는 경우가 있습니다. 원인은 크게 세 가지입니다.
- VRAM이 부족함: 측정 전에 Windows 측의 VRAM을 해제하지 않으면,
--flash-attn
--flash-attn을 활성화하더라도 어텐션 블록이 다 올라가지 못해 오히려 성능이 떨어집니다. 저는 브라우저를 닫은 상태에서 측정하고 있습니다. 한 번은 Chrome을 켜둔 채로 측정했다가, "--flash-attn이 작동하지 않는다, 환경 의존성 때문에 invisible fallback(보이지 않는 폴백)이 발생한다"라고 반나절 동안 의심했던 적이 있습니다. 원인은 200MB 단위로 VRAM을 점유하는 확장 프로그램이었습니다 - **CUDA와 llama.cpp 버전**: CUDA 12.9와 2026-07 시점의 llama.cpp 조합으로 동작을 확인했습니다. 오래된 CUDA + 오래된 llama.cpp 조합에서는 --flash-attn` 분기 방식이 다릅니다 -
모델과 양자화 (Quantization): Q4_K_M과 Q4_0에서도 플래시 어텐션 (Flash Attention)의 효과는 미묘하게 다릅니다. 동일한 Q4_K_M으로 측정하고 있습니다.
재현할 수 있을 때는 명령어를 첨부하고, 재현할 수 없을 때는 그렇게 적는다. 이것은 제가 측정의 예법으로서 지키고 있는 선입니다. 또한, "빠를 것이다"라고 미리 단정 짓지 않는 것. Chrome 건으로 제가 뼈저리게 느낀 점은 바로 그것이었습니다.
lama.cpp 외로 눈을 돌리면 선택지는 두 가지입니다.
- vLLM: GGUF 로드를 지원합니다. 동시 요청 수가 많은 서버 용도라면, 연속 배칭 (Continuous Batching)을 통해 처리량 (Throughput)을 몇 배 더 높일 수 있습니다. 다만 단발성 채팅 용도로는 실행과 VRAM 예약 과정이 무겁고, RTX 4070의 12GB 환경에서는 35B 모델을 올린 시점에서 여유가 거의 없기 때문에 저는 구분해서 사용하고 있습니다 -
- mistral.rs: Rust로 제작된 추론 런타임 (Inference Runtime)입니다. 플래시 어텐션 (Flash Attention)과 ISQ (In-Situ Quantization)를 조합하기 쉽습니다. 다만 2026-07 시점에서는 cpu-moe 만큼 유연한 오프로딩 (Offloading)은 llama.cpp가 더 다루기 쉽습니다.
"모두가 llama.cpp를 써라"라고 말하지는 않겠습니다. 단발성 개인 용도로 35B를 돌린다면, 현재로서는 llama.cpp + 위의 플래그들을 사용하는 것이 가장 손이 덜 가고 빠르다는 것이 저의 결론입니다.
솔직하게 말씀드립니다. 플래그를 추가할수록 확실히 부작용도 늘어납니다.
- KV 양자화 (KV Quantization)는 정밀도 (Precision)에 영향을 줄 수 있습니다 (q8_0는 7개 문항에서 확인되었으나, 실무 코드에서는 더 어려운 태스크에서 재확인하는 것이 좋습니다)
- flash-attn은 환경 의존성에 따라 invisible하게 fallback 합니다
- batch-size를 높이면 VRAM 부족 현상이 가까워져, cpu-moe의 의미가 퇴색됩니다
단순히 "빨라졌다"는 것만 보고 채택하지 않는 것이 좋습니다. 동일한 환경에서 한 번, llama-bench를 돌려 수치를 내보세요. 표준 편차를 확인하여 결과가 일관되게 재현되는지 확인하세요. 이 두 가지만으로도 플래그 선택의 정확도는 비약적으로 상승합니다.
저 자신도 승리 구성의 입구인 cpu-moe에 도달했을 때, 한동안 측정을 멈췄었습니다. "한계겠지"라며 만족했던 지점에서, 이렇게 플래그 3개 분량의 성능 향상을 놓치고 있었던 것입니다. 로컬 LLM은 플래그 하나만으로 국소 최적화 (Local Optimization)가 움직입니다. 정기적으로 llama-bench를 돌리며 승리 구성의 바깥쪽을 의심하는 습관을 들여두는 것이 이득입니다.
-ngl 99 --cpu-moe는 최고 속도가 아니라 승리 구성의 입구입니다 ---flash-attn으로 +4.3 tok/s (RTX 4070에서 확인) ---cache-type-k q8_0 --cache-type-v q8_0로 +2.6 tok/s, 컨텍스트 길이 (Context Length)를 늘릴 여지도 증가 ---batch-size 512 --ubatch-size 512는 생성에는 효과가 없으나, prompt eval이 1.5배 -- 3개 플래그를 모두 적용했을 때 생성 41.2 tok/s, pp512는 1,190 tok/s -
llama-bench -r 3으로 표준 편차를 확인하지 않으면 이 차이는 노이즈에 묻힙니다.
저는 "속도는 3개 플래그의 축적으로 만들어진다"는 것을 이번 측정을 통해 상당히 익숙하게 만들었습니다. VRAM 12GB인 RTX 4070에서도 35B 희소 모델 (Sparse Model)은 아직 성장 가능성이 있습니다. 직접 llama-bench를 한 번 실행해 보세요. 수치가 나오면 다음에 어떤 플래그를 건드려야 할지가 명확히 보일 것입니다. 즐겁게 해봅시다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기