Documents
Home>Documents>AI>Inference

Token Healing 없이 Prefix Caching을 쓰면 생기는 일: 토크나이저 분절이 캐시와 생성 품질을 동시에 깨는 방식

11 min readSep 11, 2026Sep 11, 2026

프롬프트가 http:로 끝나면 모델은 뒤에 ://가 올 수 없다는 결론을 내린다. 토크나이저가 : 하나를 별도 토큰(ID 27)으로 분절했기 때문이다. 그 ID가 컨텍스트 마지막에 있으면 ://에 해당하는 토큰(ID 1358)은 다음 위치에서 나타날 수 없다 — 모델이 훈련에서 그렇게 학습했다. Microsoft guidance 라이브러리 문서에서 검증된 예시다.

Prefix caching을 쓰는 서빙에서 동일한 현상이 캐시 키를 바꿔버린다. 두 문제의 원인이 같고, Token Healing이 하나의 수술로 두 곳을 동시에 고친다.

Prefix Cache가 어떻게 깨지는가

vLLM의 Automatic Prefix Caching(APC)SGLang의 RadixAttention은 토큰 ID 시퀀스를 캐시 키로 쓴다. 요청의 입력을 블록(보통 16~32토큰)으로 나눠 이미 계산된 KV 블록이 있으면 재활용한다. 전제 조건이 하나 있다: 동일한 자연어 prefix는 항상 동일한 토큰 ID 시퀀스를 낸다. BPE 토크나이저가 이 전제를 깨는 지점이 경계다.

RAG 패턴의 전형적인 구조를 보자.

[고정 system prompt] + [동적 retrieved chunk] + [user query]

많은 구현체에서 system prompt 부분의 KV를 미리 계산해 캐시에 올려둔다. 이때 캐시 키는 system prompt를 단독으로 토크나이즈한 토큰 ID 시퀀스다.

SentencePiece 기반 토크나이저(Llama 2/3, Mistral, Gemma 등)는 문자열 전체를 한 번에 보고 greedy merge를 적용한다. "system prompt" 단독 토크나이즈 결과와 "system prompt + chunk" 전체 토크나이즈 결과에서, 경계 지점의 토큰 ID가 달라질 수 있다. SentencePiece는 공백을 다음 단어에 붙여(▁-prefix 방식) 처리하기 때문에, 뒤에 어떤 문자가 오느냐에 따라 현재 위치 공백의 분절 방식이 바뀐다.

system prompt가 "Context:\n"으로 끝날 때를 예로 들면:

# system prompt 단독 토크나이즈
tokenizer.encode("Context:\n")
# → [..., ▁Context, :, ▁]   ← 공백이 별도 토큰

# retrieved chunk를 이어붙인 전체 토크나이즈
tokenizer.encode("Context:\n The answer is...")
# → [..., ▁Context, :, ▁The, ...]   ← 공백이 "The"와 병합

앞 블록의 마지막 토큰 ID가 바뀌었다. 캐시에 저장된 블록과 매칭되지 않는다 — 캐시 미스. 동일한 자연어 프롬프트인데도 suffix가 달라진다는 이유만으로 prefix의 캐시 키가 무효화된다.

tiktoken(cl100k_base)은 regex pre-tokenization으로 단어 경계를 먼저 분리하기 때문에 이 현상이 상대적으로 드물다. OpenAI 계열 모델을 API로 쓸 때 문제가 덜 튀는 이유다. OSS 서빙의 주력인 Llama/Mistral/Gemma 패밀리에서 더 자주 발생하는데, 하필 현재 self-hosted 서빙에서 가장 많이 쓰이는 모델 계열 전부다.

Token Boundary Bias가 출력을 왜곡하는 경로

캐시 미스는 성능 문제다. token boundary bias는 출력 품질 문제다. 그리고 원인이 같다.

http: 예시를 다시 보면, 모델이 : 이후를 생성할 때 어휘 확률 분포가 왜곡된다. ://는 훈련 데이터에서 : 토큰 다음에 자주 등장했지만, 추론 시 : 토큰이 이미 입력에 있으면 //를 생성해야 하는데 //가 문장 도입부에 단독으로 오는 케이스는 훈련 분포에서 드물다. 결과적으로 모델이 http: //www.google.com처럼 공백이 낀 잘못된 URL을 생성한다. 토크나이즈가 올바르게 됐다면 http://youtube.com 같은 유효한 URL이 나왔을 것이다.

이 편향이 얼마나 광범위한가. guidance 팀 분석에 따르면 StableLM 기준 상위 1만 개 토큰 중 약 70%가 더 긴 토큰으로 확장 가능하다. 임의의 위치에서 프롬프트가 끊기면 10번 중 7번은 마지막 토큰이 불완전한 접두사로 작동한다는 뜻이다.

코드 생성에서 이 문제가 특히 두드러진다. 프롬프트가 def calculate_로 끝나는 경우, _가 다음 단어의 일부로 병합되어야 할 토큰이라면 모델이 함수명 패턴을 올바르게 이어가지 못한다. LaTeX 수식(\frac{처럼 명령어 중간에 끊기는 경우)이나 CJK 문자와 영문이 교차하는 다국어 경계에서 동일한 왜곡이 나타난다. 이들은 그냥 이상한 출력처럼 보이고, 원인이 토크나이저 경계라는 걸 추적하기가 쉽지 않다.

Token Healing의 원리와 비용

원리는 단순하다. 프롬프트 끝에서 토큰 1개(때로는 2개)를 제거하고, 첫 생성 토큰을 "제거된 토큰의 바이트 접두사로 시작하는 것들"로 제한한다.

http: 케이스:

  1. : (ID 27) 제거
  2. 생성 가능한 첫 토큰을 : 바이트로 시작하는 것들로 제한 → : (27), :// (1358), :/
  3. 모델이 :// (1358)를 선택 → 올바른 URL 생성

prefix caching 관점에서는, 마지막 토큰을 걷어낸 prefix가 캐시 키가 된다. suffix가 무엇이든 경계 전 블록의 토큰 ID가 안정적으로 유지된다. suffix에 의한 마지막 토큰 변형 문제가 없어진다.

비용은 추가 prefill 연산이다.

healing_cost  = N_rollback × C_prefill_per_token + C_logit_mask

cache_benefit = N_cache_prefix × C_prefill_per_token  (캐시 히트 시 절약분)

healing이 이득인 조건:
  (캐시 히트율 개선 × N_cache_prefix) > N_rollback + logit_mask_overhead

N_rollback은 보통 12토큰이다. N_cache_prefix가 system prompt + 문서 컨텍스트이면 수백수천 토큰이다. 이 비율이 1:수백이면 healing 비용은 무시할 수 있다.

배치 크기가 커지면 달라진다. continuous batching 환경에서 배치 내 각 요청이 서로 다른 rollback 토큰을 가지면, logit masking을 요청별로 별도 적용해야 한다. 현재 vLLM의 배치 처리 구현에서 이를 효율적으로 처리하는 경로가 없다. 배치 크기 32 이상에서는 healing이 prefill throughput을 눈에 띄게 낮출 수 있고, 그 시점부터는 캐시 히트율 이득과 직접 비교해야 한다.

서빙 패턴별 판단 기준

서빙 패턴prefix 재사용률Token Healing 권고핵심 이유
챗봇 (고정 system prompt)높음켜기캐시 히트율 + 생성 품질 동시 개선
RAG (고정 SP + 동적 chunk)SP만 고정켜기SP 경계 캐시 안정화
코드 자동완성 (임의 prefix)낮음켜기생성 품질 왜곡이 핵심 문제
다국어 혼합 프롬프트중간켜기CJK/영문 경계 바이어스 강함
완전 동적 프롬프트없음끄기캐시 이득 없음, 순비용
배치 크기 32+ 고처리량높아도 무관측정 후 결정throughput 저하 vs 캐시 이득 비교 필요

코드 자동완성은 특수한 경우다. prefix 재사용률이 낮아 캐시 이득은 거의 없지만, 불완전한 토큰 경계에서 모델이 잘못된 방향으로 분기하는 일이 반복되기 때문에 healing을 켜야 한다. 이 패턴에서는 캐시가 아니라 생성 품질이 healing의 정당화 근거다.

RAG에서는 healing이 "system prompt까지의 prefix"를 안정적으로 캐싱하는 데 효과적이다. 다만 retrieved chunk 자체 내부의 경계 문제는 healing이 닿지 않는다. healing은 prefix 마지막 토큰 1~2개만 롤백하므로, chunk 중간에 경계 왜곡이 있다면 별도 대응이 필요하다.

완전 동적 프롬프트 — 매 요청이 완전히 새로운 텍스트인 경우 — 에서는 healing이 캐시 히트율을 개선할 여지가 전혀 없다. 추가 prefill 비용만 남아서 순비용이 된다.

프레임워크 현황

guidance: 현재 가장 완성된 구현이다. LlamaCpp와 Transformers 백엔드에서 기본 지원하며, token healing의 원리를 정립한 라이브러리이기도 하다. structured generation 라이브러리라는 성격상 standalone 서빙 엔진으로 그대로 쓰기엔 제약이 많다.

llama.cpp: 2024년 2월 feature request(issue #5765)가 "good first issue"로 열렸고, 관련 PR #24247이 있지만 병합 미완료 상태다.

vLLM: 공식 token healing API가 없다. prefix caching은 아래 플래그로 활성화한다.

vllm serve meta-llama/Llama-3.1-8B-Instruct \
  --enable-prefix-caching \
  --max-model-len 32768

경계 안정성 확보는 프롬프트 템플릿 레벨 우회를 쓴다. suffix가 붙는 지점을 \n---\n 같은 구분자로 끝내되, 그 구분자가 항상 동일한 토큰 ID 시퀀스로 끝나도록 사전에 검증하는 방식이다. 완전하진 않지만 추가 compute 없이 경계 안정성을 높인다.

SGLang: RadixAttention이 기본 활성화되어 있고, 별도 healing 메커니즘 없이 prefix 정렬을 요청 라우팅 레벨에서 권고한다.

TGI: prefix caching 지원 자체가 제한적이고, token healing 관련 구현 논의는 아직 없다.

세 서빙 프레임워크 모두 token boundary를 구조적으로 해결하는 경로가 없다. guidance가 가장 앞서 있지만 서빙 엔진이 아니다. llama.cpp가 뒤를 따르는 중이고, vLLM과 SGLang은 프롬프트 설계로 회피하길 기대한다. 운영자 입장에서 현재 할 수 있는 가장 확실한 방법은 — 프레임워크 지원을 기다리기보다 — system prompt 끝을 토큰 경계에서 안정적으로 끝나도록 사전 검증해두는 것이다.

Tags
LLMInferencevLLMKV 캐시서빙RAGTTFT