
llama.cpp의 Context Checkpoint를 단순히 “KV 캐시 적중률을 높이는 기능”이라고 설명하면 조금 빗나갑니다.
핵심은 이것입니다.
SWA나 Recurrent/Hybrid 모델처럼 현재 상태에서 과거 상태로 단순 rollback하기 어려운 경우, 이전 계산 상태를 저장해 두었다가 복원
하여 불필요한 Prompt 재처리를 줄이는 기능입니다.
여기서 가장 중요한 전제가 하나 있습니다.
Checkpoint는 Prompt가 변경된 이후의 계산을 재사용하지 않습니다.
변경 이후는 다시 계산해야 합니다.
Checkpoint가 줄여주는 것은 변경 지점 이전의 유효한 상태까지 돌아가기 위해 다시 계산해야 하는 Prefix 구간입니다.
기존 Prompt가 다음과 같다고 하겠습니다.
A B C D E
새 요청은 다음과 같습니다.
A B C F F
공통 Prefix는 ABC입니다.
일반적인 Full Attention Transformer에서는 ABC까지 계산한 KV Cache를 유지한 뒤 DE 부분을 버리고 FF부터 다시 계산할 수 있습니다.
기존 A B C D E
새 요청 A B C F F
↑
여기부터 재계산
100K token Prompt에서 20K부터 내용이 달라졌다면 개념적으로 다음과 같습니다.
0 20K 100K
├──────────────────┼────────────────────────────────┤
재사용 다시 계산
20K 이후의 80K는 다시 계산해야 합니다.
20K에서 token이 달라지면 그 이후 hidden state와 Attention의 K/V도 영향을 받기 때문입니다.
따라서 Context Checkpoint가 있다고 뒤쪽 80K가 살아나는 것은 아닙니다.
Full Attention 모델처럼 KV Cache를 원하는 Prefix 위치까지 정상적으로 truncate할 수 있다면 기존 Prefix Cache만으로 rollback이 가능합니다.
현재 모델이 100K까지 처리했다고 하겠습니다.
원하는 동작은 다음과 같습니다.
현재 100K
↓
20K 이후 제거
↓
정확한 20K 상태
Full Attention에서는 가능한 경우가 많지만 다음 구조에서는 문제가 생길 수 있습니다.
llama.cpp의 Context Checkpoint 역시 처음에는 SWA의 이런 문제를 해결하기 위해 도입됐습니다.
PR #15293에서는 SWA memory 상태를 checkpoint로 저장하여 context 전체를 다시 처리하는 상황을 줄이는 기능이 추가됐습니다.
이후 PR #16382에서 Recurrent/Hybrid 모델까지 범위가 확대되면서 --swa-checkpoints가 보다 일반적인 --ctx-checkpoints로 확장됐습니다.
Sliding Window Attention의 window 크기를 4라고 단순화해보겠습니다.
A B C D
B C D E
C D E F
D E F G
새 token이 들어오면 오래된 KV는 window 밖으로 밀려날 수 있습니다.
따라서 현재 100K까지 처리했다고 해서 20K 당시의 SWA 상태가 그대로 남아 있다고 볼 수 없습니다.
새 Prompt와 기존 Prompt의 공통 Prefix가 20K라고 해도,
20K 이후만 삭제
해서 정확한 20K 상태로 돌아가지 못할 수 있습니다.
그래서 과거 상태 자체를 별도 checkpoint로 저장합니다.
SWA에서는 --swa-full이라는 대안도 있습니다.
전체 Context 범위에 해당하는 SWA memory를 유지하면 과거 위치에서 분기하기 쉬워지는 대신 더 많은 메모리를 사용합니다.
즉 여기서도 결국 계산량과 메모리의 trade-off가 생깁니다.
Recurrent 구조를 단순화하면 다음과 같습니다.
S0 --A--> S1 --B--> S2 --C--> S3 --D--> S4 --E--> S5
현재 상태는 이전 상태로부터 만들어집니다.
S(t) = f(S(t-1), token(t))
ABCDE를 처리한 현재 상태가 S5인데 새 Prompt가
ABCFF
로 바뀌었다고 하겠습니다.
필요한 것은 ABC 직후 상태인 S3입니다.
하지만 일반적으로
S5 - D - E = S3
와 같은 역연산은 할 수 없습니다.
현재 state에서 특정 token의 영향만 제거해 과거 state를 복원할 수 없기 때문입니다.
따라서 S3에 가까운 상태를 별도로 저장하지 않았다면 훨씬 앞쪽부터 다시 계산해야 합니다.
Context Checkpoint가 이 과거 상태를 보존합니다.
100K token Prompt가 있고 20K에서 Prompt가 달라졌다고 하겠습니다.
20K까지 KV Cache를 재사용합니다.
20K cached
80K re-process
20K 상태로 rollback할 방법이 없다면 다음처럼 될 수 있습니다.
0부터 다시 계산해야 할 수 있음
최악의 경우 100K 전체 Prefill이 다시 발생합니다.
16K 상태를 복원할 수 있다면
16K checkpoint restore
↓
16K → 20K 재계산
↓
20K 이후 새로운 Prompt 계산
20K 이후가 80K라면 다시 계산해야 할 양은 대략
4K + 80K = 84K
입니다.
Checkpoint가 절약한 것은 16K입니다.
80K를 살린 것이 아닙니다.
Context Checkpoint는 변경 이후의 계산 결과를 재사용하는 기능이 아니라, 변경 지점 이전의 유효한 상태까지 돌아가기 위해 다시 처리해야 하는 Prefix를 줄이는 기능입니다.
일반 채팅은 기존 Prompt 뒤에 새로운 메시지가 추가되는 경우가 많습니다.
하지만 Coding Agent는 다릅니다.
첫 요청이 다음과 같았다가
System
User
Tool
File A v1
Tool Result
Assistant
...
다음 요청에서는 중간이 바뀔 수 있습니다.
System
User
Tool
File A v2
Tool Result
Assistant
...
File A 이후 상태는 다시 계산해야 합니다.
Context Checkpoint가 그 이후 계산을 살려주는 것은 아닙니다.
문제는 Recurrent/Hybrid 모델에서 File A 직전 상태로 돌아갈 방법까지 없을 때입니다.
Checkpoint가 없다면 Prompt 시작부터 다시 Prefill해야 할 수도 있지만, 가까운 과거 checkpoint가 남아 있다면 그곳에서 다시 시작할 수 있습니다.
즉 Agent 환경에서는 다음 의미를 갖습니다.
Agent가 Prompt 중간을 변경해 rollback이 필요해졌을 때, rollback이 어려운 모델에서 Context 전체를 처음부터 다시 처리하는 상황을 줄여준다.
llama.cpp Issue #27813에서는 Hybrid/Recurrent 모델의 slot restore 문제를 분석했습니다.
사용 모델은 Qwen3.8-Flash-Next이며 이슈 작성자는 hybrid attention + SSM 구조라고 설명합니다.
5,892-token Prompt에서 결과는 다음과 같았습니다.
| 상태 | Prompt 처리 | 시간 |
|---|---|---|
| Cold | 5,892 tokens | 19,057 ms |
| Disk restore | 5,892 tokens | 19,238 ms |
| In-memory warm | 4 tokens | 496 ms |
Disk restore 자체는 성공했지만 slot.prompt.checkpoints는 state file에 저장되지 않았습니다.
따라서 restore 이후 Hybrid/Recurrent state를 필요한 Prefix 위치로 rollback할 checkpoint가 사라졌고 5,892 token 전체를 다시 처리했습니다.
반대로 checkpoint가 메모리에 남아 있던 warm slot에서는 새로운 4 token만 처리했습니다.
다만 이것을
--ctx-checkpoints 0 vs 32
의 단순 성능 비교라고 해석하면 안 됩니다.
Checkpoint가 유실된 restore 경로와 정상적인 in-memory 경로의 차이를 보여주는 사례입니다.
llama.cpp server의 timing 정보에서는 다음 값을 확인할 수 있습니다.
{
"cache_n": 20000,
"prompt_n": 32,
"prompt_ms": 120.5,
"prompt_per_second": 265.5
}
최소한 다음 값은 같이 봐야 합니다.
| 값 | 의미 |
|---|---|
cache_n | 기존 Context에서 재사용한 Prompt token |
prompt_n | 이번 요청에서 실제 처리한 Prompt token |
prompt_ms | Prompt 처리 시간 |
prompt_per_second | Prompt 처리 속도 |
그리고 시스템의 Host RAM 사용량도 함께 측정하는 것이 좋습니다.
Checkpoint는 계산량을 줄이는 대신 상태를 메모리에 저장하기 때문입니다.
단순히 다음 두 서버를 실행한다고 실험이 끝나는 것은 아닙니다.
llama-server \
-m model.gguf \
--ctx-checkpoints 0
llama-server \
-m model.gguf \
--ctx-checkpoints 32
중요한 것은 Prompt가 변경되는 위치 이전에 실제로 복원 가능한 checkpoint가 남아 있어야 한다는 것입니다.
그런데 checkpoint는 무제한 저장되지 않습니다.
현재 master README 기준 --ctx-checkpoints 기본값은 32, --checkpoint-min-step은 8192입니다.
현재 구현에서도 checkpoint 수가 최대치에 도달하면 목록의 앞쪽, 즉 오래된 checkpoint부터 제거합니다.
로그에는 다음 메시지가 출력됩니다.
erasing old context checkpoint
코드에서도 checkpoints.front()를 선택한 뒤 제거하는 흐름을 확인할 수 있습니다.
단순한 감각값으로 계산하면
32 checkpoints × 8192 tokens
≈ 262,144 tokens
≈ 256K tokens
입니다.
따라서 checkpoint가 대략 최소 간격 수준으로 계속 생성되고 있다고 가정하면, 세션이 100K 정도 진행됐다고 곧바로 8K나 16K checkpoint가 FIFO 때문에 밀려나는 그림은 자연스럽지 않습니다.
기본 설정에서 오래된 checkpoint 삭제가 눈에 띄기 시작하는 것은 보다 긴 세션일 가능성이 큽니다.
예를 들어 개념적으로는 다음과 같습니다.
초기
8K 16K 24K ... 248K
CP CP CP CP
↓ 계속 새로운 checkpoint 생성
256K 264K 272K ...
CP CP CP
↓ 최대 개수를 초과
오래된 8K, 16K ... checkpoint부터 제거
단, 이 32 × 8192 ≈ 256K를 정확히 256K까지 복원이 보장된다는 의미로 이해하면 안 됩니다.
checkpoint-min-step은 최소 간격입니다.
현재 코드에서는 user message 위치, 현재 task, Prompt 끝부분 등의 조건도 checkpoint 생성에 관여하기 때문에 실제 checkpoint 간격은 더 넓어질 수 있습니다.
즉 256K는 어디까지나 설정을 이해하기 위한 규모감입니다.
checkpoint-min-step을 줄이면 생기는 또 다른 Trade-off여기가 운영할 때 꽤 중요합니다.
다음처럼 설정한다고 생각해보겠습니다.
--ctx-checkpoints 32
--checkpoint-min-step 8192
최소 간격 기준 규모는 대략
32 × 8192 ≈ 256K
입니다.
그런데 Agent workload에서 더 촘촘한 rollback 지점을 얻기 위해 다음처럼 설정할 수도 있습니다.
--ctx-checkpoints 32
--checkpoint-min-step 512
실제로 llama.cpp 사용자 이슈에서도 512를 사용한 설정들을 확인할 수 있습니다.
checkpoint가 최소 간격 수준으로 자주 생성된다고 단순 가정하면
32 × 512
≈ 16K
규모입니다.
128을 사용하는 사례도 있습니다.
32 × 128
≈ 4K
입니다.
물론 이것도 정확한 retention window 계산식은 아닙니다.
실제 checkpoint 간격은 최소값보다 더 넓을 수 있습니다.
중요한 것은 방향입니다.
Checkpoint를 더 촘촘하게 만들 수 있음
↓
변경 위치에 가까운 상태를 잡을 가능성 증가
↓
Rollback 후 재계산량 감소 가능
하지만 고정된 checkpoint 개수 안에서 checkpoint가 실제로 더 자주 만들어진다면,
새 Checkpoint가 빠르게 증가
↓
--ctx-checkpoints 최대치에 빨리 도달
↓
오래된 Checkpoint가 더 빨리 삭제
↓
먼 과거로 돌아가기 어려워질 수 있음
이라는 반대 효과가 생깁니다.
즉 설정에는 두 축이 있습니다.
| 설정 | 작게/적게 | 크게/많게 |
|---|---|---|
checkpoint-min-step | 촘촘한 rollback 가능, 오래된 CP가 빨리 밀릴 가능성 | 성긴 checkpoint, 더 넓은 시간축 보존 가능 |
ctx-checkpoints | RAM 절약, 보존 지점 적음 | 더 많은 rollback 지점, RAM 증가 |
따라서 무조건 checkpoint-min-step을 낮추는 것이 좋은 것은 아닙니다.
가까운 과거를 정밀하게 복원할 것인지, 더 먼 과거까지 rollback 가능성을 유지할 것인지를 workload에 맞춰 선택해야 합니다.
이건 긴 Agent Session에서는 특히 중요합니다.
현재 llama.cpp에는 checkpoint를 제거하는 경로가 여러 개 있습니다.
erasing old context checkpoint
최대 보관 개수를 넘겨 오래된 checkpoint를 제거하는 경우입니다.
erasing context checkpoint too close to an earlier one
목록이 가득 차는 과정에서 이전 checkpoint와 지나치게 가까운 항목을 정리하는 경로입니다.
erased invalidated context checkpoint
현재 Prompt와 더 이상 일치하지 않는 미래 쪽 checkpoint를 제거하는 경우입니다.
현재 server-context.cpp에 각각 별도 로직으로 존재합니다.
그래서 디버깅할 때 단순히
checkpoint가 사라졌다
만 보면 부족합니다.
왜 삭제됐는지 로그 문구까지 구분해서 봐야 합니다.
테스트 순서는 다음처럼 잡는 것이 좋습니다.
1. SWA 또는 Recurrent/Hybrid 모델 선택
2. Checkpoint 활성화
3. 긴 multi-turn Agent Session 생성
4. created context checkpoint 로그 확인
5. 실제 checkpoint 위치 확인
6. erasing old context checkpoint 발생 여부 확인
7. 남아 있는 checkpoint 이후에서 Prompt 분기
8. restored context checkpoint 확인
9. OFF/ON 조건의 timings와 RAM 비교
Checkpoint가 존재하지 않으면 다음 경로로 갈 수 있습니다.
forcing full prompt re-processing due to lack of cache data
반대로 복원이 성공하면 다음 로그를 확인할 수 있습니다.
restored context checkpoint
현재 master 코드에 두 경로가 모두 존재합니다.
따라서 숫자를 보기 전에 먼저
Checkpoint가 생성됐는가?
↓
아직 남아 있는가?
↓
실제로 복원됐는가?
를 확인해야 합니다.
그렇지 않으면 ON인데 왜 차이가 없지?라는 잘못된 결론을 내릴 수 있습니다.
아래 템플릿으로 직접 A/B Test를 기록해볼 수 있습니다.
| 조건 | cache_n | prompt_n | prompt_ms | Host RAM |
|---|---|---|---|---|
| Cold | ||||
| Checkpoint OFF | ||||
| Checkpoint ON |
실험 환경도 함께 남기는 것이 좋습니다.
llama.cpp build/commit :
Model :
Quantization :
Context size :
GPU :
KV cache type :
ctx-checkpoints :
checkpoint-min-step :
cache-ram :
Prompt 분기 위치 :
생성된 checkpoint 위치 :
삭제된 checkpoint 위치 :
복원된 checkpoint 위치 :
이 정도까지 기록해야 단순히
Checkpoint ON이 빨랐다
가 아니라
어느 checkpoint에서 복원했고
몇 token의 Prefill을 줄였으며
그 대신 RAM을 얼마나 사용했는가
까지 설명할 수 있습니다.
현재 llama.cpp master README 기준 주요 설정은 다음과 같습니다.
--ctx-checkpoints 32
--checkpoint-min-step 8192
--cache-ram 8192 MiB
하지만 llama.cpp는 변화가 빠릅니다.
실제로 이슈와 배포 환경에서는 checkpoint-min-step을 1024, 512, 128 등으로 조정해 사용하는 사례도 확인할 수 있습니다.
따라서 인터넷 글의 숫자를 그대로 복사하기보다 자신이 사용하는 binary에서 확인하는 것이 가장 안전합니다.
llama-server --version
llama-server --help
Context Checkpoint 설정을 단순히
많으면 좋다
라고 생각하면 안 됩니다.
실제로는 세 요소가 엮입니다.
ctx-checkpoints ↑
→ 더 많은 과거 상태 유지
→ rollback 가능성 증가
→ RAM 증가
checkpoint-min-step ↓
→ 더 촘촘한 checkpoint 허용
→ 가까운 위치에서 rollback 가능
→ checkpoint 생성이 잦다면 오래된 상태가 빨리 밀릴 수 있음
짧은 Agent Session에서 최근 몇 천 token 안에서 Prompt가 자주 바뀐다면 촘촘한 checkpoint가 유리할 수 있습니다.
반대로 수십만 token짜리 장기 Agent Session에서 초반 Tool 결과나 파일 내용까지 다시 변경할 가능성이 있다면 너무 촘촘한 checkpoint는 고정된 개수 안에서 오래된 checkpoint를 빨리 밀어낼 수 있습니다.
결국 설정할 때 봐야 하는 것은 다음입니다.
복원 정밀도
vs
복원 가능한 과거 거리
vs
RAM 사용량
이 세 가지를 workload에 맞춰 조절해야 합니다.
Context Checkpoint의 역할은 다음 한 문장으로 정리할 수 있습니다.
SWA·Recurrent·Hybrid 모델에서 과거 Context 상태로 직접 rollback하기 어려울 때, 저장해 둔 과거 상태를 복원하여 변경 지점까지 처음부터 다시 Prefill해야 하는 비용을 줄이는 기능입니다.
일반적인 Full Attention에서는 공통 Prefix까지 KV Cache를 남기고 이후만 다시 계산하면 됩니다.
하지만 SWA에서는 오래된 KV가 사라질 수 있고, Recurrent 모델에서는 현재 state에서 과거 state를 역산하기 어렵습니다.
그래서 Context Checkpoint가 필요합니다.
Prompt가 20K에서 변경됨
Checkpoint 없음
0
│
└────────────────────────────→
0부터 다시 계산해야 할 수 있음
16K Checkpoint 존재
16K
│
└────────────→
16K부터 다시 계산
하지만
20K 이후의 변경된 부분
─────────────────────────────→
결국 다시 계산해야 함
그리고 또 하나 중요합니다.
과거에 Checkpoint가 생성됐다
와
지금도 그 Checkpoint가 남아 있다
는 같은 말이 아닙니다.
--ctx-checkpoints에는 개수 제한이 있고, 최대치에 도달하면 오래된 checkpoint부터 제거됩니다.
따라서 긴 Agent Session에서는 앞쪽 Prompt가 변경됐을 때 사용할 checkpoint가 이미 사라져 Full Prefill이 발생할 수도 있습니다.
또 checkpoint-min-step을 낮추면 더 촘촘하게 rollback할 가능성은 생기지만, 실제 checkpoint 생성 빈도가 증가한다면 고정된 개수 안에서 오래된 checkpoint가 더 빠르게 밀려날 수 있습니다.
그래서 Context Checkpoint를 검증할 때는 단순히
빨라졌다 / 느려졌다
만 보면 부족합니다.
최소한
checkpoint 생성 위치
checkpoint 삭제 원인
checkpoint 복원 위치
Prompt 분기 위치
cache_n
prompt_n
prompt_ms
Host RAM
을 같이 봐야 합니다.
어느 상태로 복원했고, 그 결과 몇 token의 Prefill을 피했으며, 그 대가로 얼마의 메모리를 사용했는가.
여기까지 확인해야 Context Checkpoint가 실제 Agent workload에서 어떤 효과를 내는지 제대로 판단할 수 있습니다.
이번 내용을 정리하면서 가장 크게 느낀 점은 KV Cache와 Context Checkpoint를 단순히 같은 종류의 캐시 최적화 기술로 보면 안 된다는 것이었습니다.
처음에는 Context Checkpoint가 Agent가 기존 Prefix를 변경했을 때 뒤쪽 계산을 최대한 재사용하기 위한 기능이라고 생각했습니다.
하지만 llama.cpp의 구현과 관련 PR, 이슈를 확인하면서 실제 핵심은 달랐습니다.
변경된 지점 이후의 계산은 결국 다시 해야 하고, Checkpoint가 줄여주는 것은 SWA나 Recurrent·Hybrid 모델에서 과거의 유효한 상태까지 되돌아가기 위해 다시 계산해야 하는 비용이었습니다.
특히 --ctx-checkpoints와 --checkpoint-min-step의 관계가 흥미로웠습니다. Checkpoint를 촘촘하게 생성하면 가까운 위치에서 복원할 가능성은 높아지지만, 보관 가능한 개수가 한정되어 있기 때문에 오래된 Checkpoint가 더 빨리 밀려날 수도 있습니다.
결국 복원 정밀도, 과거 상태를 보관하는 범위, 메모리 사용량 사이에서 적절한 균형이 필요합니다.
이번 글을 작성하면서 문서의 옵션 설명만 보는 것보다 실제 소스 코드의 생성·삭제·복원 경로와 로그를 함께 확인하는 것이 얼마나 중요한지도 다시 느꼈습니다.
특히 llama.cpp처럼 변화가 빠른 프로젝트는 블로그나 과거 이슈의 기본값을 그대로 믿기보다 현재 사용하는 빌드의 --help와 코드를 기준으로 확인하는 습관이 필요해 보입니다.
다음에는 실제 Hybrid 모델을 대상으로 Context Checkpoint를 켠 경우와 끈 경우를 비교하고, cache_n, prompt_n, prompt_ms, Host RAM 사용량까지 직접 측정해보고 싶습니다.
개념을 이해하는 것에서 끝내지 않고 실제 수치로 확인해보는 것이 이번 주제의 마지막 단계라고 생각합니다.