본문 바로가기

[LLM] R9700에서 Qwen3.8-27B를 최대한 빠르게

@이멀젼씨2026. 8. 29. 18:54

최근에 qwen에서 qwen3.8 27b를 출시하였다.

그 외에 Qwen3.8 Next Flash도 출시했는데 이 또한 SOTA 모델급이란 얘기에 드디어 로컬 모델도 많이 발전했다는 걸 느꼈다.

 

이제는 정말 로컬 모델로 사용할 수 있을 것으로 보인다.

 

물론 사용은 할 수 있지만 속도나 품질이 문제일 수 있지만 말이다.

 

어쨌든 Qwen3.8-27B가 나온 김에 최적의 속도가 나오는 효율 구간을 찾아 측정해보았다.

 

품질은 뭐, 위의 표 대로 믿고 가는 거다.

 

물론 측정 이후에 사용하면서 느끼는 건 생각보다 많이 똑똑해졌구나를 느껴서 굳이 품질에 대한 측정은 하지 않았다.

 

측정 환경

  • HW: AMD Radeon AI PRO R9700 (gfx1201 / RDNA4, 32,624MiB), i7-10700 + 32GB RAM, Windows 10 Pro
  • 엔진: llama.cpp b10435 Vulkan (비교용 ROCm은 lemonade b1311 / gfx120X)
  • 모델: Qwen3.8-27B - Unsloth UD-Q4_K_XL / Q5_K_XL / Q6_K_XL

아래 모든 수치는 GGML_VK_DISABLE_HOST_VISIBLE_VIDMEM=1을 켠 상태다.

 

이 보드(MSI MAG B460M MORTAR)는 Resizable BAR가 없어서, 이 변수를 빼면 Vulkan이 3분의 1 속도로 돈다.

 

테스트를 해보면서 몇가지 발견한 점이 있어 함께 공유해보겠다.

 

KV 양자화는 속도를 아끼는 수단이 아니다

이게 제일 크게 뒤집힌 결론이다.

 

보통 KV 캐시를 q8_0으로 양자화하면 메모리도 줄고 대역폭도 줄어서 빨라진다고 생각한다.

 

나도 그렇게 알고 쓰고 있었다.

 

얕은 깊이만 재면 실제로 그렇게 나온다.

 

하지만 실제론 달랐다.

KV MTP d0 10,439 43,209 60,392 121,300
q8_0 off 28.76 27.71 25.92 25.11 22.56
q8_0 n3 58.98 54.57 45.96 43.43 35.62
f16 off 29.14 27.83 25.32 24.17 20.90
f16 n3 52.19 55.56 49.16 48.35 42.47

 

역전되는 구간이 10k 이하다.

 

d0에서만 q8_0이 이기고 그 위로는 전부 f16이다.

 

121k에서 f16 n3(42.47)이 q8_0 n3(35.62)보다 19.2% 빠르다.

 

그런데 여기 재미있는 대비가 하나 있다.

 

MTP를 끄고 보면 121k에서 q8_0이 f16보다 7.9% 빠르다(22.56 vs 20.90).

 

KV가 작으니 대역폭을 덜 먹어서 당연하다.

 

그런데 MTP를 켜는 순간 같은 깊이에서 16.1%로 뒤집힌다.

 

메커니즘은 prefill 수치가 알려준다.

 

MTP off 기준 q8_0의 prefill 손실이 d0에서 -4.2%인데 121k에서 -28.2%로 증가한다.

 

MTP는 수락 토큰당 추가 forward를 돌리고 그때마다 KV 전체를 다시 읽는다.

 

양자화 KV의 디퀀트 비용이 MTP에서 n+1배로 증폭되는 것이다.

 

AR은 토큰당 한 번만 내니 작은 KV의 이득이 살아남고, MTP는 그 이득을 디퀀트 비용으로 다 잃는다.

 

MTP 이득 배율(n3 / off)로 보면 더 선명하다.

 

KV d0 10k 43k 60k 120k
q8_0 2.05배 1.97배 1.77배 1.73배 1.58배
f16 1.79배 2.00배 1.94배 2.00배 2.03배

 

q8_0에서는 MTP 이득이 깊이와 함께 무너지고, f16에서는 2배가 끝까지 유지된다.

 

결국 KV 양자화는 속도를 파는 게 아니라 속도를 희생해서 컨텍스트를 확보하게 되는 것이고, 컨텍스트가 깊을수록 속도는 나빠진다.

 

참고로 bf16도 같이 재봤는데, 얕은 깊이에서 f16과 동등하거나 살짝 나았다.

 

다만 깊이까지 재보면 121k에서 tg -14.3%, prefill -31.0%로 벌어진다.

 

VRAM은 바이트 단위로 같은데 그렇다.

 

f16 쓸 자리에 bf16을 넣을 이유는 없다.

 

컨텍스트가 깊어질수록 높은 양자화의 속도가 양호해진다

Q4 / Q5 / Q6를 파라미터 전부 동일하게 놓고 비교했다.

 

KV = q8_0 기준

MTP 깊이 Q4 Q5 Q6 Q5/Q4 Q6/Q4
off d0 28.76 26.62 21.74 -7.4% -24.4%
n3 d0 58.98 56.40 45.67 -4.4% -22.6%
n4 43k 49.21 52.02 47.30 +5.7% -3.9%
n4 120k 34.67 35.89 34.41 +3.5% -0.7%

 

 

얕은 곳에서는 가중치 크기(16.35 / 18.83 / 24.14GiB)가 그대로 순위로 나온다.

 

Q4 압승이고 Q6는 24% 정도 낮다.

깊은 곳에서는 Q5가 Q4를 앞선다.

 

2.5GiB 더 무거운데 더 빠르다.

 

깊이에서는 토큰당 메모리 트래픽을 KV 읽기가 지배하는데, ctx 131072에 f16이면 KV만 8GiB고 이건 세 quant가 완전히 동일하다.

 

가중치 15% 차이가 전체 트래픽에서 묻힌다.

 

Q6가 깊은 컨텍스트에서 격차를 메우는 이유는 무거운 quant일수록 MTP의 영향을 더 잘 받기 때문이다.

 

quant 120k n3 120k n4
Q4 1.58배 1.54배
Q5 1.67배 1.69배
Q6 1.76배 1.92배

 

AR 기준선이 낮을수록 투기 디코딩이 영향을 줄 여지가 크다.

 

Q6는 MTP 없이 17.95t/s로 처참한데 n4를 켜면 34.41까지 올라와 Q4(34.67)와 0.7% 차이로 붙는다.

 

다만 Q4 대 Q5는 동률로 봐야 한다.

 

n3 기준 42.47 vs 42.82로 노이즈 범위고, n4에서 Q5가 8.3% 앞서 보이지만 그건 단발 측정이다.

 

같은 조합 Q4 n4를 3회 재보니 49.34 / 43.72 / 42.05로 크게 흔들렸다.

 

적재되는 것과 쓸 수 있는 것은 다르다

Q6_K_XL + f16 KV로 ctx 131072를 올리면 VRAM 31.44GB로 로드에 성공하지만 제대로 쓸 수가 없다.

Q6 f16 vs Q6 q8_0 prefill tg
d0 99.9 → 35.2 (−65%) 45.67 → 14.50 (−68%)
43k 520.4 → 56.6 (−89%) 40.85 → 11.26 (−72%)

 

로드는 되는데 왜 이렇게 되는지 좀 풀어서 적어둔다.

VRAM에 들어가야 하는 건 가중치만이 아니다

llama.cpp가 GPU에 올리는 것은 세가지이다.

 

덩어리 성격 Q6 + f16 @ctx131072
가중치 고정. 한 번 올리면 끝 24.14 GiB
KV 캐시 고정. ctx만큼 미리 잡음 f16 = 8.00 GiB
연산 버퍼 매 스텝 읽고 쓰는 작업공간 나머지

 

연산 버퍼는 어텐션 스코어 행렬이나 중간 계산값 같은 걸 담는 자리다.

 

-b 2048 -ub 512가 이 크기를 정한다.

 

앞의 둘과 달리 토큰을 만들 때마다 계속 접근하는 놈이라, 여기가 느려지면 그대로 생성 속도가 된다.

 

셋을 더하면 34.65GiB가 필요한데 이 카드에서 실제로 쓸 수 있는 건 31.86GiB다.

 

2.8GiB가 넘친다.

넘치는데 왜 에러 없이 뜨나

Windows가 VRAM에 자리가 없으면 시스템 RAM 일부를 GPU 메모리인 것처럼 빌려준다.

 

그래서 llama-server가 에러 없이 뜨고, 겉보기엔 정상이다.

 

적재는 되는데 쓸 수 없다가 이 상태다.

 

문제는 그 빌려온 메모리가 그래픽카드 안이 아니라 메인보드 건너편에 있다는 거다.

 

GPU가 거기 접근하려면 PCIe 슬롯을 통과해야 한다.

 

어디서 읽나 속도
VRAM (카드 안) 650 GB/s
시스템 RAM (PCIe 3.0 x16 건너) 약 16 GB/s

 

40배 차이다.

그래서 무슨 일이 벌어지나

연산 버퍼는 토큰 하나 만들 때마다 필요하다. 그게 카드 밖으로 밀려나 있으면, 토큰마다 PCIe를 왕복해야 한다.

 

한 번 옮겨놓고 끝나는 게 아니라 매 토큰 반복된다.

 

시간으로 보면 이렇다.

  토큰 하나에 걸리는 시간
정상 (48.69 t/s) 20.5 ms
넘친 상태 (10.07 t/s) 99.3 ms

 

차이 79ms가 전부 데이터 기다리는 시간이다.

 

계산 자체는 20ms면 끝나는데 데이터가 도착하기를 80ms 기다린다.

 

GPU가 일하는 시간보다 기다리는 시간이 네 배다.

 

이게 병목이 계산이 아니라 데이터 이동이라는 증거가 하나 더 있다.

 

같은 조건에서 -ub 512-ub 128로 낮추면 17.11 → 30.11t/s가 된다. (1.76배)

 

계산량은 똑같고 한 번에 옮기는 덩어리만 작게 쪼갠 건데 이만큼 회복된다.

 

그냥 GPU가 느린 거라면 배치 크기 바꾼다고 이렇게 안 변한다.

 

결론

그래서 결론이 뭐라고 묻는다면 아래와 같이 정리할 수 있다.

 

깊이별로 정리하면 이렇다.

 

전부 Vulkan b10435 + GGML_VK_DISABLE_HOST_VISIBLE_VIDMEM=1 전제다.

 

컨텍스트 KV MTP 실측 비고
~1만 q8_0 n3 약 54~59 t/s 기동 직후 편향 제외한 정상 상태
1만 ~ 13만 f16 n3 42.47 t/s @121k 3회 재현, 산포 0.09%
13만 ~ 26만 q8_0 n3 32.14 t/s @185k f16이 못 담는다

 

컨텍스트 상한은 전부 실제로 채워서 쟀다.

 

Q4 + q8_0이면 네이티브 262,144를 전부 VRAM에 담는다(27.59GB).

 

185k 깊이에서도 32t/s로 쓸 수 있어서 장식이 아니다.

 

다만 prefill이 282.8t/s라 18.5만 토큰을 채우는 데 11분이 걸린다.

 

프롬프트 캐시 없이는 대화형으로 못 쓴다. f16의 실질 상한은 131,072다.

 

편성명 모델 KV MTP d0 121k VRAM
q4-n3 UD-Q4_K_XL f16 n3 52.19 42.47 26.3G
q4-n4 f16 n4 51.43 42.05 26.5G
q5-n4 UD-Q5_K_XL f16 n4 51.98 41.82 29.2G
q6-n4 UD-Q6_K_XL q8_0 n4 48.20 34.41 30.2G

 

 

명령어는 이 형태 하나에서 모델 파일과 n-max만 바뀐다.

set GGML_VK_DISABLE_HOST_VISIBLE_VIDMEM=1

b10435-vulkan\llama-server.exe ^
  -m llm_models\Qwen3.8-27B-UD-Q4_K_XL.gguf ^
  -ngl 99 -c 131072 -ctk f16 -ctv f16 -fa on -b 2048 -ub 512 ^
  --jinja --reasoning-format deepseek -np 1 ^
  --spec-type draft-mtp --spec-draft-n-max 3 ^
  --load-mode none --cache-ram 12288 ^
  --temp 1.0 --top-p 0.95 --top-k 20 --min-p 0.0 ^
  --reasoning-budget 12288 ^
  --reasoning-budget-message "I have enough information. Answer now." ^
  --host 0.0.0.0 --port 8080

 

 

 

RDNA4는 여전히 그냥 켜면 빠른 단계가 아니며 각종 실험과 노력이 들어가야 하는 아키텍쳐이다.

 

다행히 여러 사람들의 노력에 의해 vllm을 사용한다거나 FP8을 사용한다거나 하면 성능이 성능이 올라갈 수 있게 되었다.

 

싱글 R9700에서는 백엔드는 Vulkan로, KV는 f16 고정, MTP는 n3, 나머지는 손대지 않는 게 나을 것 같다.

 

같은 R9700이나 RDNA4 카드로 27B를 굴리는 분들에게, 특히 ReBAR 없는 구형 보드에 꽂아둔 분들에게 이 글이 도움이 되었으면 한다.

 

참고로 나는 Q5_K_XL과 120k의 조합으로 사용중이다.

이멀젼씨
@이멀젼씨 :: 이멀젼씨

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차