Dual 3090 설정: Qwen 3.6 27B에서 400 pp t/s를 1600 pp t/s로 향상... 약간 낮은 tps와 함께
요약
Dual RTX 3090 환경에서 llama.cpp의 SPLIT_MODE_TENSOR 설정 시 발생하는 프롬프트 처리(PP) 병목 현상을 분석합니다. CPU 샘플러 사용으로 인해 GPU 성능을 제대로 활용하지 못하던 문제를 해결하여 PP t/s를 400에서 1600으로 대폭 향상시킨 사례를 다룹니다.
핵심 포인트
- SPLIT_MODE_TENSOR 사용 시 프롬프트 처리가 CPU에서 수행되는 문제 발견
- CPU 샘플러 사용으로 인해 batch 및 ubatch 설정이 무력화됨
- 설정 최적화를 통해 프롬프트 처리 속도를 약 4배 향상
- Dual GPU 환경에서 llama.cpp 성능 극대화를 위한 로그 확인의 중요성
먼저, 제 설정은 다음과 같습니다:
Ryzen 9 5950x DDR4 3200Mhz 64gb (2x32) Dual 3090s, NVLINK 없음
Runtime: llama.cpp
Nvidia Drivers 610
Windows 11 25H2
Qwen 3.6 27B Q8
저는 지난 몇 달 동안 --split-mode tensor를 사용하여 llama-server를 사용해 왔습니다. 이는 전반적인 tps(tokens per second)를 꽤 괜찮게 10%-20% 정도 향상시켜 주었기 때문이며, 특히 MTP(Multi-Token Prediction)의 경우 더욱 그렇습니다 (기본적으로 일관되게 34-35tps를 얻는 반면, MTP는 40에서 최대 70tps까지 높일 수 있습니다). 하지만, llama.cpp의 터미널에서 항상 나타나지만 충분히 높은 tps를 얻고 있었기에 별로 신경 쓰지 않았던 중요한 로그가 있었습니다:
failed to fit params to free device memory: llama_params_fit is not implemented for SPLIT_MODE_TENSOR
sampling not supported with SPLIT_MODE_TENSOR, using CPU sampler
이는 모든 프롬프트 처리(prompt processing)가 CPU에서 일어나고 있었음을 의미했으며, 이 특정 설정에서는 batch 및 ubatch가 전혀 아무런 역할도 하지 못했습니다. 저의 평균 pp t/s는 약 400에서 430 t/s였습니다.
print_timing: id 2 | task 38441 | prompt processing, n_tokens = 30782, progress = 0.33, t = 71.69 s / 429.39 tokens per second
print_timing: id 2 | task 38441 | prompt processing, n_tokens = 32830, progress = 0.36, t = 76.67 s / 428.18 tokens per second
print_timing: id 2 | task 38441 | prompt processing, n_tokens = 34878, progress = 0.38, t = 81.69 s / 426.96 tokens per second
print_timing: id 2 | task 38441 | prompt processing, n_tokens = 36926, progress = 0.40, t = 86.74 s / 425.73 tokens per second
print_timing: id 2 | task 38441 | prompt processing, n_tokens = 38974, progress = 0.42, t = 91.81 s / 424.50 tokens per second
print_timing: id 2 | task 38441 | prompt processing, n_tokens = 41022, progress = 0.44, t = 96.92 s / 423.27 tokens per second
print_timing: id 2 | task 38441 | prompt processing, n_tokens = 43070, progress = 0.47, t = 102.05 s / 422.03 tokens per second
print_timing: id 2 | task 38441 | prompt processing, n_tokens = 45118, progress = 0.49, t = 107.22 s / 420.81 tokens per second
이것은 모든 실행에서 일관되게 나타났습니다.
나의 t/s (tokens per second)를 높이기 위해 batch 및 ubatch를 조절해 보았지만, 아무런 성과도 얻지 못했습니다. t/s는 항상 정확히 동일한 범위 내에 있었고, 오히려 약간 더 나빠지기도 했습니다. llama-bench를 조금 사용해 본 후, 듀얼 GPU 환경에서 보고된 t/s가 1600을 넘고 어떤 경우에는 1900까지 올라가는 것을 발견했는데, 이는 전혀 말이 되지 않는 수치였습니다. (단일 GPU에서도 그런 수치를 얻지 못했습니다). (이 포스트를 더 보기 좋고 분석하기 쉽게 만들기 위해 일부 행을 생략했습니다): | qwen35 27B Q8_0 | 27.04 GiB | 512 | 128 | q8_0 | q8_0 | 1 | pp512 | 1423.83 ± 6.61 | | qwen35 27B Q8_0 | 27.04 GiB | 512 | 128 | q8_0 | q8_0 | 1 | pp4096 | 1461.57 ± 2.65 | qwen35 27B Q8_0 | 27.04 GiB | 512 | 128 | q8_0 | q8_0 | 1 | tg128 | 26.73 ± 0.02 | | qwen35 27B Q8_0 | 27.04 GiB | 512 | 256 | q8_0 | q8_0 | 1 | pp512 | 1484.13 ± 5.55 | | qwen35 27B Q8_0 | 27.04 GiB | 512 | 256 | q8_0 | q8_0 | 1 | pp4096 | 1771.88 ± 13.97 | qwen35 27B Q8_0 | 27.04 GiB | 512 | 256 | q8_0 | q8_0 | 1 | tg128 | 26.63 ± 0.01 | | qwen35 27B Q8_0 | 27.04 GiB | 512 | 512 | q8_0 | q8_0 | 1 | pp512 | 1310.25 ± 7.83 | | qwen35 27B Q8_0 | 27.04 GiB | 512 | 512 | q8_0 | q8_0 | 1 | pp4096 | 1935.29 ± 11.51 | qwen35 27B Q8_0 | 27.04 GiB | 512 | 512 | q8_0 | q8_0 | 1 | tg128 | 26.54 ± 0.02 | | qwen35 27B Q8_0 | 27.04 GiB | 512 | 1024 | q8_0 | q8_0 | 1 | pp512 | 1289.56 ± 8.83 이는 동일한 llama-cpp, 동일한 OS, 동일한 모든 환경에서 듀얼 3090 설정이 4배 이상 빠른 t/s에 도달할 능력이 충분히 있음을 의미했습니다. 많은 테스트 끝에, 범인은 처음부터 --split-mode tensor였다는 것이 밝혀졌습니다. --split-mode layer로 전환한 후 나의 tps (tokens per second)는 타격을 입었으며, 측정 가능한 수준으로 주로 6070 tps에서 4055 tps 사이로 떨어졌고, 이제 70 tps에 도달하는 경우는 드뭅니다.
tensor: print_timing: id 1 | task 39896 n_decoded = 184, tg = 60.86 t/s, tg_3s = 60.85 t/s print_timing: id 1 | task 39896 n_decoded = 378, tg = 62.33 t/s, tg_3s = 63.80 t/s print_timing: id 1 | task 39896 n_decoded = 565, tg = 62.16 t/s, tg_3s = 61.82 t/s print_timing: id 1 | task 39896 n_decoded = 755, tg = 62.27 t/s, tg_3s = 62.59 t/s print_timing: id 1 | task 39896 n_decoded = 950, tg = 62.64 t/s, tg_3s = 64.12 t/s print_timing: id 1 | task 39896 n_decoded = 1153, tg = 63.38 t/s, tg_3s = 67.09 t/s layer: print_timing: id 0 | task 0 | n_decoded = 2730, tg = 53.10 t/s, tg_3s = 53.37 t/s print_timing: id 0 | task 0 | n_decoded = 2862, tg = 52.59 t/s, tg_3s = 43.88 t/s print_timing: id 0 | task 0 | n_decoded = 3014, tg = 52.45 t/s, tg_3s = 49.88 t/s print_timing: id 0 | task 0 | n_decoded = 3138, tg = 51.89 t/s, tg_3s = 41.28 t/s print_timing: id 0 | task 0 | n_decoded = 3282, tg = 51.70 t/s, tg_3s = 47.90 t/s print_timing: id 0 | task 0 | n_decoded = 3426, tg = 51.52 t/s, tg_3s = 47.62 t/s print_timing: id 0 | task 0 | n_decoded = 3570, tg = 51.33 t/s, tg_3s = 47.31 t/s print_timing: id 0 | task 0 | n_decoded = 3725, tg = 51.35 t/s, tg_3s = 51.65 t/s (최대 70에 도달할 수 있지만 빈도가 낮으며, tensor의 경우 최대치가 80 tps일 수 있음.) 그러나 pp t/s는 다음과 같습니다: print_timing: id 3 | task 0 | prompt processing, n_tokens = 6144, progress = 0.57, t = 3.70 s / 1659.69 tokens per second print_timing: id 3 | task 0 | prompt processing, n_tokens = 8192, progress = 0.77, t = 4.91 s / 1670.10 tokens per second print_timing: id 3 | task 0 | prompt processing, n_tokens = 10186, progress = 0.95, t = 6.14 s / 1659.67 tokens per second print_timing: id 3 | task 0 | prompt processing, n_tokens = 10648, progress = 0.99, t = 6.66 s / 1599.44 tokens per second print_timing: id 3 | task 0 | prompt processing, n_tokens = 10661, progress = 1.00, t = 6.83 s / 1561.48 tokens per second 이는 pp 처리량에서 거의 4배 증가한 수치입니다.
또한 새로운 현상이 나타났습니다. 이전에는 처리 레이어 (processing layer)가 CPU에 할당되었기 때문에, 전체 컨텍스트 (context) 동안 t/s가 거의 일정하게 유지되었으며, 200k 컨텍스트에서도 약 370 t/s 정도로 아주 조금만 떨어졌습니다. 하지만 이번에는 약 200k 토큰 지점에서 720 t/s로 떨어졌습니다: prompt processing, n_tokens = 194118, progress = 0.96, t = 259.17 s / 749.01 tokens per second prompt processing, n_tokens = 196166, progress = 0.97, t = 263.50 s / 744.46 tokens per second prompt processing, n_tokens = 198214, progress = 0.98, t = 267.85 s / 740.03 tokens per second prompt processing, n_tokens = 200262, progress = 0.99, t = 272.23 s / 735.62 tokens per second prompt processing, n_tokens = 201925, progress = 1.00, t = 275.89 s / 731.91 tokens per second prompt processing, n_tokens = 202342, progress = 1.00, t = 277.53 s / 729.08 tokens per second prompt processing, n_tokens = 202400, progress = 1.00, t = 278.07 s / 727.86 tokens per second prompt processing, n_tokens = 202437, progress = 1.00, t = 278.56 s / 726.73 tokens per second 이는 이 시점에서도 기존 CPU t/s의 거의 두 배에 달하는 수치입니다. 따라서 약 10-20%의 tps 손실이 발생하더라도 거의 2배에서 4배에 달하는 pp t/s 향상은 확실히 가치 있는 거래입니다. 참고로, 이는 NVLink가 없는 설정이며, 이론적으로는 이와 같이 매우 긴 컨텍스트 윈도우 (context windows)에서 차이가 발생해야 합니다. 이제, 설정을 주의 깊게 다루지 않으면 CPU 처리로 넘어가기 매우 쉽다는 점을 명심하십시오. llama.cpp의 상세 출력 (verbosity)은 무엇이 그 원인인지 명확하게 알려주지 않습니다. 예를 들어, ubatch를 너무 많이 늘리면 메모리 사용량이 급증하여 단일 레이어가 CPU에 할당될 수 있고, 이로 인해 모든 이득이 상쇄될 수 있습니다: layer 0은 device CPU에 할당되었으나, fused Gated Delta Net (chunked)은 device CUDA0에 할당됨 (보통 지원 부족으로 인해 발생). 컨텍스트 윈도우를 262k에서 240k로 낮추니 이 문제가 해결되었습니다... 두 GPU 전체에 걸쳐 여전히 2GB 이상의 여유 VRAM이 남아 있었음에도 불구하고 말입니다.
저는 몇 달 동안 --split-mode tensor를 사용하고 있었지만, 제 환경에서는 프롬프트 처리가 사실상 CPU로 폴백(fallback)되고 있다는 것을 깨닫지 못했습니다. batch와 ubatch는 PP 처리량에 어떤 개선도 가져오지 않았습니다 (CPU에는 영향을 미치지 않는 것 같습니다). 제가 --split-mode layer로 전환하고 모든 레이어가 GPU에 남아 있도록 확인하자, 프롬프트 처리가 즉시 1.5~1.7k 토큰/초 범위로 확장되었습니다. 사실 split tensor만 사용하라는 권장 사항이 너무 흔해서 많은 사람들이 무슨 일이 일어나고 있는지 모른 채 이 문제에 직면할 수 있습니다. MoE(Mixture-of-Experts)를 다루는 사람들은 이미 알고 있습니다. 왜냐하면 llama는 어떤 레이어가 CPU로 처리되고 어떤 레이어가 GPU로 처리될지 실시간으로 선택할 수 있기 때문입니다. 하지만 이는 밀집 모델(dense models)에서는 옵션이 아닙니다. 즉, CPU에 폴백하거나 아니면 아예 안 되거나 둘 중 하나입니다. 그리고 tensor는 아직 백엔드 처리가 되어 있지 않습니다. split tensor가 비교적 새로운 기술이기 때문에 시간이 지나면서 바뀔 수도 있습니다. 제가 많은 사람들이 이미 알고 있는 이야기를 할지도 모르지만, 이 서브레딧에서도 답을 찾으려 할 때 항상 (그리고 주변에서 일관되게 들었던) 것은 '그냥 ubatch를 늘리세요'였습니다. 하지만 제가 여기서 언급하고 싶은, 그다지 공개적으로 이야기되지 않는 제한 사항들이 있습니다. /u/DjCanalex 제출 [링크] [댓글]
AI 자동 생성 콘텐츠
본 콘텐츠는 r/LocalLLaMA의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기