RTX 5090에서 Qwen 3.6 27B를 140 tok/s로 굴리기 — llama.cpp 패치부터 MTP 함정까지
20시간의 실사용 데이터가 말해주는 것
로컬 LLM 추론 속도가 초당 140토큰이라니, 보고도 내 눈을 의심했다.
r/LocalLLM에 올라온 글 제목을 보자마자 마시던 커피를 뿜을 뻔했다. “qwen3.6 27b q6 + 5090 maximum llamacpp optimization: 100-233tok/s, average 140.” 평균 140 tok/s, 최대 피크 233 tok/s라니. 27B 파라미터짜리 모델을 집구석 로컬에서 굴리는데 이 속도가 나온다면 눈이 뒤집히지 않을 개발자가 없다.
하드웨어 스펙부터 심상치 않다. Ryzen 7 9800X3D, 시스템 RAM 64GB, 그리고 대망의 끝판왕 RTX 5090 32GB(물론 5090 살 돈이 먼저 있어야 한다는 사소한(?) 문제가 있다). VRAM을 한 방울이라도 쥐어짜기 위해 우분투를 GUI 없는 순수 텍스트 모드로 부팅했다. 여기에 Qwen 3.6 27B Q6_K 양자화 모델을 올리고, 약 20시간 동안 실전 코딩 에이전트와 디버깅, 문서 요약 작업을 빡세게 굴리며 벤치마크 데이터를 뽑아냈다.
총 6,454개 샘플을 측정한 결과, 중앙값 134.9 tok/s, 평균 140.7 tok/s를 찍었다. 100 tok/s 아래로 떨어진 구간은 전체의 고작 6% 남짓. 대부분 120~150 사이를 쾌적하게 유지했고, 200을 돌파한 샘플도 169개나 됐다. 이 정도면 클라우드 API를 쓰면서 “아 왜 이렇게 응답이 굼뜨냐”고 투덜댈 일이 아예 없다. 그냥 글자가 폭포수처럼 쏟아지는 수준이다.
근데 왜 기본 설정으로는 안 될까
하지만 순정 llama.cpp에 모델을 그냥 얹으면 절대 이 속도가 안 나온다.
Qwen 3.6은 하이브리드 어텐션(Hybrid Attention)과 슬라이딩 윈도우(Sliding Window Attention, SWA) 아키텍처를 섞어 쓴다. 이게 llama.cpp의 기존 캐시 메커니즘과 정면으로 충돌한다. 로그를 뜯어보면 아주 복장 터지는 에러 메시지가 줄줄이 찍힌다.
forcing full prompt re-processing due to lack of cache data
(likely due to SWA or hybrid/recurrent memory)
한마디로 캐시를 전혀 못 써먹고, 매 턴마다 프롬프트를 맨 처음부터 무식하게 다시 연산하고 자빠진 거다. 멀티턴 대화를 돌리는데 턴이 넘어갈 때마다 묘하게 버벅대고 로딩이 길어진다? 십중팔구 이 캐시 미스 문제다.
글쓴이는 이걸 해결하려고 llama.cpp 소스코드를 직접 까서 패치 두 개를 만들어 붙였다.
첫 번째 패치 — 하이브리드/순환 모델에서 체크포인트 검색이 바보가 되는 버그 수정. cur.pos.min < pos_min_thold 조건식이 무조건 “체크포인트 없음”을 뱉어내며 캐시를 날려먹던 현상을 잡았다(관련 업스트림 이슈 #22384, #20225, #24055).
두 번째 패치 — 프롬프트 캐시 연산을 위한 recurrent_shrink/expand API 추가. 업스트림 PR #24785를 기반으로 삼되, 불필요해진 needs_reeval 땜질 코드를 걷어냈다.
이 두 패치를 먹이고 빌드하자 지긋지긋하던 SWA 리프로세싱 에러가 싹 사라졌다. 캐시가 제구실을 하기 시작하니 기본 속도부터 수직 상승한다.
MTP: 속도를 두 배 가까이 올려주는 스위치
여기서 만족하지 않고 MTP(Multi-Token Prediction, 다중 토큰 예측) 초안 디코딩(Speculative Decoding)까지 끼얹었다. 세팅 값은 이렇다.
- 초안 토큰(Draft tokens) 10개,
spec-draft-p-min = 0.5 - q8 KV 캐시, 컨텍스트 192k
- 배치 사이즈 512 (VRAM 다이어트용)
초안 토큰을 10개까지 땡기면 예측 수락률은 살짝 깎이지만, 한번 맞아떨어졌을 때 치고 나가는 부스트가 워낙 막강해서 전체 처리량(Throughput)이 확 뛴다. 6개로 낮추면 수락률은 안정적인 대신 속도가 15~20 tok/s 정도 빠진다. p_min 값은 시스템마다 환경을 타니까 직접 돌려보며 스윕하는 걸 추천한단다.
풀로드 시 VRAM은 32,768MB 중 32,036MB를 집어삼킨다. 5090 32GB 메모리를 그야말로 마지막 1MB까지 알뜰살뜰하게 쥐어짜 쓴 셈이다.
그런데 도구 호출이 망가진다
자, 속도 자랑은 여기까지다. 진짜 중요한 반전은 지금부터다.
다른 유저가 OpenCode + LM Studio 환경에서 똑같은 Qwen 3.6 27B 모델에 MTP 설정을 바꿔가며 실전 에이전트 테스트를 돌렸다. 똑같은 RTX 5090, 131k 컨텍스트 환경에서 C#과 Raylib를 써서 데스크톱 액션 RPG를 만드는 풀오토 코딩 에이전트 작업이었다.
그리고 충격적인 결과가 나왔다.
| 설정 | 속도 | 도구 호출 | 서브에이전트 |
|---|---|---|---|
| MTP 없음 | 30~40 tok/s | 안정적 | 정상 작동 |
| 초안 2~4 | 100~110 tok/s | 완전히 break | 거의 생성 안 됨 |
| 초안 1~2 | ~94 tok/s | 정상 | 정상 작동 |
초안 토큰을 2~4개로 올리자마자 모델이 도구를 실행하지 못하고, 터미널에 <tool_call> ... </tool_call> 태그를 그냥 일반 생텍스트로 뱉어내기 시작했다. OpenCode 파서가 이걸 인식 못 하니 에이전트 루프가 그 자리에서 뻗어버린다. 서브에이전트 스폰도 먹통이 됐다.
그런데 초안을 1~2개로 확 낮추니까 귀신같이 도구 호출이 정상화되고 서브에이전트들도 부활했다. 지형 렌더링, 캐릭터 스프라이트, 사망 UI 구현이라는 세 가지 독립 작업을 서브에이전트 셋이서 척척 나눠 맡아 빌드를 돌렸다. 심지어 한 서브에이전트는 남은 컴파일 에러가 자기 담당 구역 밖이라는 것까지 칼같이 파악했다.
레딧 댓글에 아주 날카로운 분석이 달렸다. 이론상 투기적 디코딩은 타겟 모델이 초안 토큰을 일일이 검증하기 때문에 출력 퀄리티가 원본과 100% 동일해야 정상이다. 그런데 초안 깊이에 따라 도구 호출 문법이 깨진다는 건, 수락 알고리즘의 거부 샘플링(Rejection Sampling)이 완벽하지 않아 타겟 모델이 원래라면 선택하지 않았을 쓰레기 토큰이 일부 슬쩍 통과되고 있다는 뜻이다.
일반 줄글 텍스트야 조사 하나 틀려도 대충 문맥으로 읽히지만, JSON이나 XML 같은 도구 호출 규격은 괄호 하나, 따옴표 하나만 삐끗해도 파서가 즉사한다. 그래서 에이전트 워크플로우에서 가장 먼저 대참사가 터진 거다.
속도와 안정성 사이의 줄타기
결국 판을 정리하면 이렇다.
llama.cpp 소스코드 패치 두 개로 Qwen 3.6의 고질적인 캐시 낭비를 잡는 건 무조건 이득이다. 여기에 MTP 초안 디코딩을 살짝 얹으면 100 tok/s 고지를 가볍게 밟을 수 있다. 하지만 속도 욕심에 초안 토큰을 무지성으로 10개씩 때려 박았다간, 에이전트가 도구 호출도 못 하고 멍청이가 되어버린다(경험담 맞다).
현시점에서 실전 코딩 에이전트를 돌릴 거라면 초안 1~2개 설정이 가장 이상적인 스위트 스팟이다. 초당 94토큰이면 눈으로 따라가기도 벅찰 만큼 충분히 빠르고, 도구 호출과 멀티에이전트 루프도 깨지지 않고 든든하게 돌아간다. 140 tok/s라는 숫자는 문서 요약이나 단순 채팅에서나 안전하게 써먹을 수 있는 일종의 오버클럭 영역이다.
로컬 LLM이 실무에 쓸 만한 수준으로 올라왔다는 건 이제 의심할 여지가 없다. 다만 무작정 빠르게 달리는 설정과 단단하게 일하는 설정이 아직 완벽히 일치하진 않는다. 토큰 속도 딸깍질과 도구 호출 안정성 사이에서 줄타기를 잘하는 것, 이게 요즘 로컬 AI 튜닝의 진짜 손맛이다.