thinking-token reasoning-budget llama.cpp Gemma-4 Qwen3

🔍 발단 — 번역 결과가 왜 이 모양인가
Atelier 블로그 번역 결과가 계속 부실했다. 문장이 어설프고 중간에 끊기는 현상이 반복되었다. 처음에는 모델 자체의 품질 문제로 판단했으나, 추적 결과 원인은 다른 곳에 있었다.
llama.cpp는 max_tokens 하나로 thinking 토큰과 답변(content) 토큰을 통합 관리한다. thinking이 활성화된 상태에서 모델이 사소한 작업에 과도하게 thinking 토큰을 소모하면, 정작 답변(content) 생성에 쓸 예산이 남지 않는다. 즉, 번역이 잘린 것은 모델의 품질 문제가 아니라 예산 부족 문제였다.
max_tokens 예산을 thinking 토큰과 content 토큰이 나누어 쓴다. 모델이 사소한 작업에 과도하게 생각(thinking)하면, content 생성 전에 예산이 소진되어 finish_reason=length가 반환된다.
📊 측정 — 3슬롯 매트릭스
DGX의 3개 슬롯을 대상으로 측정을 진행했다. 각 슬롯에 사소한 작업(1줄 번역)과 추론 작업(수학 문제)을 던져 thinking 토큰 소비량을 측정했다. 조건은 max_tokens=2048, finish_reason=stop이다.
| 슬롯 | 모델 | 사소 작업 (1줄 번역) | 추론 작업 (수학) |
|---|---|---|---|
| model1 | Gemma-4-26B-A4B | think 1,601c / 5.4s | think 742c / 4.0s |
| model2 | Gemma-4-E4B | think 943c / 2.3s | think 253c / 1.4s |
| model3 | Qwen3.6-27B | think 4,498c / 63.5s | think 906c / 23.4s |
Qwen3.6(model3)는 단 한 줄의 번역을 위해 63.5초를 소모했다. 수학 문제도 아닌 단순 번역에 1분이 넘는 시간이 걸리는 것이다. 이는 Qwen3 계열의 전형적인 실패 모드인 ‘over-thinking’ 현상이다.

🔬 커뮤니티 크로스체크
측정 데이터가 모델의 스펙과 일치하는지 검증했다. 직접 측정한 수치가 모델의 설계 의도와 맞는지 확인하는 단계다.
Gemma 4 — native thinking, enable_thinking 플래그
Gemma 4는 native thinking 모드를 지원하며, enable_thinking 플래그로 제어한다. 공식 문서에는 별도의 budget 가이드가 명시되어 있지 않으나, 커뮤니티 관례에 따라 운영된다. 멀티턴 대화 시 이전 thinking 블록을 제거하는 것이 공식 요구 사항이다.
enable_thinking=false 설정에도 빈 thought block을 출력한다. E4B 모델만 이 블록이 완전히 생략된다.
Qwen3 — 하이브리드 thinking, 과사고 심각
Qwen3 계열은 하이브리드 thinking 구조를 가진다. enable_thinking=false 또는 /no_think 시스템 프롬프트로 끌 수 있지만, 완전히 끄면 tool-calling 성능이 저하된다. Agentic 경로에서는 thinking을 완전히 끄기보다 budget 캡을 설정하는 것이 권장된다는 것이 공식 thinking_budget 문서의 결론이다.
llama.cpp — –reasoning-budget 스펙
공식 README 기준 --reasoning-budget 값의 범위는 다음과 같다: -1(무제한) / 0(즉시 종료) / N > 0(토큰 캡). 기본값은 -1이다.
중요한 우선순위 규칙이 있다. GH disc 21445에 따르면, 서버 설정이 --reasoning-budget=-1일 때만 per-request thinking_budget_tokens 필드가 적용된다. 서버 플래그가 0이나 양수이면 per-request 설정은 완전히 무시된다.
</think>를 삽입하고 응답을 이어가는 transition message를 추가하면 89%까지 회복된다.

🛠️ 실증 — per-request 제어 검증 (build b9743)
이론을 확인한 후, 실제로 per-request 제어가 작동하는지 검증했다. 대상은 model3(Qwen3.6-27B)와 model2(Gemma-4-E4B)이며, 재시작 없이 설정을 적용했다.
| 조건 | content | reasoning | 소요 시간 | 단축률 |
|---|---|---|---|---|
| model3 baseline | 106c | 7,262c | 102.7s | — |
model3 thinking_budget_tokens=256 |
106c | 827c | 16.1s | 6.4× |
model3 enable_thinking=false |
109c | 0c | 2.4s | 43× |
model2 enable_thinking=false |
110c | 0c | 0.7s | — |
per-request 제어가 정상적으로 작동함을 확인했다. model3 baseline 102.7초가 budget cap 적용 후 16.1초로 단축되었다. thinking을 완전히 끄면 2.4초까지 줄어든다. content 토큰 수는 세 조건 모두 106~109c로 동일했다. 이는 thinking 토큰 소비가 문제였을 뿐, 모델 자체가 문제는 아니었음을 증명한다.
⚙️ 구현 — terry-llm v0.4.5 (커밋 220b85a1)
실증 결과를 바탕으로 terry-llm 게이트웨이에 반영했다. 주요 변경 사항은 다음과 같다.
1. providers/router.py — per-request budget 패싱
클라이언트가 보낸 thinking_budget_tokens를 extra_body를 통해 llama.cpp 서버로 전달한다.
# providers/router.py
if request.thinking_budget_tokens is not None:
extra_body["thinking_budget_tokens"] = request.thinking_budget_tokens
2. server.py — agentic 경로 budget 캡
Agentic loop에서 thinking 토큰이 무제한으로 소비되는 것을 방지하기 위해, 설정값에서 캡(cap)을 읽어 적용한다. 기본값은 4096 토큰이다.
# server.py _do_agent()
budget = cfg.defaults.agent_thinking_budget_tokens
# config.yaml:
# agent_thinking_budget_tokens: 4096
3. agent/loop.py — 빈 content 정직 처리
finish_reason=length(잘림)와 finish_reason=stop(정상 종료)를 구분한다. content가 비어 있는 경우 truncated 플래그를 붙여 반환한다. 이전에는 빈 문자열을 정상 응답처럼 반환하는 문제가 있었다.
4. run_http.py — 세션 독립 HTTP 서비스 (:8090)
기존 MCP 세션에 종속되지 않고 :8090 포트에서 독립적으로 실행되는 standalone HTTP 서비스다. 세션이 종료되어도 HTTP 엔드포인트는 안정적으로 유지된다.
✅ 3슬롯 최종 상태
| 슬롯 | 모델 | 상태 | 처리 방식 |
|---|---|---|---|
| model1 | Gemma-4-26B-A4B | ✅ 최적화 완료 | 게이트웨이 budget 캡(4096) + 단발 경로 enable_thinking=false 적용 |
| model2 | Gemma-4-E4B | ✅ 최적화 상태 | terryragsys가 enable_thinking=false 및 GBNF 제약 디코딩 적용 중 |
| model3 | Qwen3.6-27B | ✅ 의도적 보존 | 코딩 작업 전담 모델로, thinking 기능을 유지함 |
실질적인 변경 사항은 model1에 대해서만 적용했다. model2는 이미 최적화되어 있었고, model3는 코딩 작업을 위해 thinking 기능이 필수적이어서 보존했다.
⚠️ Pitfalls
- DGX 서버
--reasoning-budget=-1유지 필수: 이 설정이 -1이 아니면 per-requestthinking_budget_tokens가 무시된다. 서버 설정을 변경하면 모든 per-request 제어가 한꺼번에 무력화된다. - 양의 budget 캡 사용 시 transition message 권장: thinking을 강제로 종료할 때
</think>를 삽입하지 않으면 성능이 급격히 떨어진다. Qwen3 기준 HumanEval 점수가 약 16%p 하락하므로, 라이브러리에서 지원하지 않는다면 직접 구현이 필요하다. - Gemma 멀티턴 — 이전 thinking 제거 필수: 공식 요구 사항이다. 멀티턴 파이프라인 구성 시 이전 대화의 thinking 블록을 반드시 처리해야 한다.
- E4B 외 Gemma — 빈 thought block 출력:
enable_thinking=false설정에도<|channel>thought와 같은 빈 블록이 출력될 수 있다. 파싱 로직에서 이에 대한 처리가 필요하다.
📚 참고 자료
- Gemma 4 thinking 공식 문서 — Google AI for Developers
- Gemma 4 model card — E4B thinking 비활성화 동작 공식 스펙
- llama.cpp server README —
--reasoning-budget공식 스펙 - GH disc 21445 — per-request
thinking_budget_tokens우선순위 규칙 - GH disc 21338 — Gemma 4 E4B thinking disable 버그 보고
- r/LocalLLaMA — HumanEval 벤치마크 및 transition message 효과
- Qwen3 thinking_budget 공식 문서 — QwenLM
관련 DGX 인프라 운영 기록은 인프라 아카이브에서 확인할 수 있다. 이전 모델 혼용 사례와 레이턴시 개선 결과도 정리되어 있다.
✅ 요약
문제는 단순했다. thinking-on 상태에서 max_tokens 예산을 thinking과 content가 공유하는데, 모델이 사소한 작업에 과도하게 thinking하면 content가 잘린다. Qwen3 기준 1줄 번역에 63.5초를 소모했으며, baseline reasoning 토큰은 7,262c에 달했다.
해결책 역시 명확했다. DGX 서버의 --reasoning-budget를 -1로 유지하고, 게이트웨이에서 per-request thinking_budget_tokens를 전달하며, agentic 경로에 4096 토큰 캡을 적용했다. 재시작 없이 model1 경로에만 적용하여 문제를 해결했다.