본문으로 건너뛰기

© 2026 Molayo

Qiita헤드라인2026. 06. 03. 12:40

【#4】ds4.c 분석하기

요약

DeepSeek V4 Flash/Pro 전용 추론 엔진인 DwarfStar(ds4)의 디스크 KV 캐시 구현 방식을 분석합니다. 스테이트리스 API 환경에서 프리픽스 재사용을 위해 SHA1 기반의 파일 식별과 정교한 페이로드 구조를 사용하는 설계 원리를 다룹니다.

핵심 포인트

  • SHA1 기반 파일 식별로 토크나이저 경계 문제 해결
  • KVC 엔벨로프와 DSV4 페이로드의 이중 구조 설계
  • 단순 LRU가 아닌 토큰 밀도와 히트 수를 고려한 퇴피 로직
  • 디스크 KV를 통한 세션 간 프리픽스 prefill 재사용

본 시리즈는 DeepSeek V4 Flash / Pro 전용 추론 엔진인 DwarfStar(ds4)의 코드를 읽어 내려가는 연재입니다.

제4회는 README의 핵심 사상인 "KV cache is a first-class disk citizen"이 어떤 파일 형식과 재사용 로직으로 구현되어 있는지 살펴봅니다. 주요 참조 지점:

README.md, ds4.h, ds4.c, ds4_kvstore.c, ds4_kvstore.h

관전 포인트: "파일명이 토큰 열이 아니라 렌더링 후 바이트 열의 SHA1"이라는 점이 스테이트리스(Stateless) API에서 프리픽스(Prefix)를 재사용할 수 있는 열쇠가 됩니다.

연재 「DwarfStar(ds4) 읽기」 전 8회

  • 제1회 왜 전용 엔진을 작성하는가

  • 제2회 비대칭 2bit 양자화와 imatrix

  • 제3회 Metal 그래프와 압축 KV
    제4회 디스크 KV 캐시 (본 기사)

  • 제5회 서버와 DSML 툴 호출

  • 제6회 TCP 파이프라인 분산 추론

  • 제7회 네이티브 에이전트

  • 제8회 스티어링(Steering)・MTP・평가 기반

  • DS4의 디스크 KV 캐시는 단순한 프롬프트 문자열 캐시가 아니다. 체크포인트의 토큰 ID, logits, raw KV, 압축 KV, compressor/indexer의 frontier까지 저장한다.

  • 파일은 외부의 KVC 엔벨로프(Envelope)와 내부의 DSV4 세션 페이로드(Payload)로 나뉜다.

  • KVC의 키는 토큰 ID 열이 아니라, 렌더링 후 바이트 열 프리픽스의 SHA1이다. 이는 동일한 바이트 열이 다음 토크나이저(Tokenizer)에 의해 다른 토큰 경계로 나뉘어 읽힐 가능성을 흡수하기 위함이다.

  • DSV4 페이로드는 그래프 상태의 실체로, 13개의 u32 헤더, 체크포인트 토큰, 다음 토큰 logits, 층(Layer)별 raw/압축/indexer 상태를 가진다.

  • cold 저장 시에는 마지막 32개 토큰을 잘라내고, 2048 토큰 경계에 정렬한다. BPE 경계의 흔들림과 compressor 경계를 피하기 위한 실용적인 설계이다.

  • 퇴피(Eviction) 스코어는 토큰 밀도, 히트(Hit) 수, 6시간 반감기, 앵커(Anchor) 이유를 조합한다. 단순한 LRU가 아니다.

OpenAI/Anthropic 호환 채팅/보완 API는 기본적으로 스테이트리스(Stateless)입니다. 클라이언트는 매 요청마다 대화 이력 전체를 다시 보냅니다.

로컬 LLM 서버에게 이것은 상당히 무거운 문제입니다. 긴 시스템 프롬프트, 툴 정의, 과거 대화, 파일 내용 등이 매번 재전송되기 때문입니다.

DS4의 README는 이 문제에 대해 다음과 같은 방침을 취합니다.

  • 라이브 메모리 캐시는 현재 1개 세션용
  • 무관한 세션으로 전환하면 RAM 상의 KV는 교체됨
  • 교체된 세션을 재 prefill 없이 복원하려면 디스크 KV 캐시가 필요함

즉, 디스크 KV는 "메모리가 부족할 때의 퇴피"가 아니라, 여러 세션과 서버 재부팅을 가로질러 프리픽스의 prefill을 재사용하기 위한 주 기능입니다.

README의 말을 빌리자면, 압축 KV와 고속 SSD의 시대에는 KV 캐시는 RAM만의 것이 아닙니다.

DS4의 디스크 캐시 파일은 크게 2개 층으로 나뉩니다.

KVC fixed header
rendered text length
rendered text bytes
...

외부의 KVC는 캐시의 룩업(Lookup)과 관리를 위한 엔벨로프(Envelope)입니다.

내부의 DSV4 페이로드는 실제 그래프 체크포인트입니다. 여기에 토큰 ID, logits, KV 텐서가 들어갑니다.

ds4_kvstore.c의 서두 주석은 이 분리를 명확히 하고 있습니다.

/* Shared disk KV checkpoint file support.
*
* The low-level file layout and payload helpers are intentionally shared.
...

서버는 자동 캐시 정책을 가집니다. 반면, 네이티브 에이전트는 동일한 영구 포맷을 명시적인 세션 저장에 사용합니다. 파일 형식을 공유하고, 정책은 사용자마다 나누는 구조입니다.

README에 따르면, KVC

고정 헤더 (Fixed header)는 48 바이트입니다. 코드에는 다음과 같은 정의가 있습니다.

#define DS4_KVSTORE_FIXED_HEADER 48u
#define KV_CACHE_MAGIC0 'K'
#define KV_CACHE_MAGIC1 'V'
...

헤더의 주요 필드는 다음과 같습니다.

오프셋필드역할
0magic "KVC"캐시 파일 판별
3versionKVC 엔벨로프 (Envelope) 버전
4quant bits루티드 엑스퍼트 (Routed Expert)의 양자화 비트, 2 또는 4
5reasoncold / continued / evict / shutdown 등
...

코드상에서는 ds4_kvstore_fill_header()가 리틀 엔디안 (Little-endian) 방식으로 채웁니다.

h[0] = KV_CACHE_MAGIC0;
h[1] = KV_CACHE_MAGIC1;
h[2] = KV_CACHE_MAGIC2;
...

KVC는 "이 파일의 프리픽스(Prefix)가 무엇인가", "현재 런타임에서 읽을 수 있는가", "어느 것을 퇴피(Evict)해야 하는가"를 판단하기 위한 메타데이터를 가지고 있습니다.

이 부분이 DS4 디스크 KV 캐시의 중요한 설계입니다.

파일명은 <sha1>.kv입니다. 이 SHA1은 체크포인트의 토큰 ID가 아니라, 렌더링 후 바이트열 프리픽스(Byte sequence prefix)에 대해 계산됩니다.

ds4_kvstore.h의 주석에는 그 이유가 명확하게 적혀 있습니다.

/* The file name is the rendered byte prefix, not the token sequence.
* The payload still carries the exact tokens and graph state; the hash only
* answers "does this checkpoint represent the bytes at the front of the
...

왜 토큰 ID가 아니라 바이트열인가?

채팅 클라이언트(Chat client)는 모델이 이전에 생성한 텍스트를 다음 요청 시 JSON 히스토리로 다시 보냅니다. 이때, 겉보기에 동일한 텍스트라도 토크나이저 (Tokenizer)의 경계가 달라질 수 있습니다. 예를 들어, 이전에는 1개의 토큰으로 생성되었던 문자열이, 정준적인(Canonical) 프롬프트 렌더링 후에는 2개의 토큰으로 나뉠 수 있습니다.

DS4는 렌더링 후 바이트열의 프리픽스가 일치하는지를 먼저 확인합니다. 일치한다면, 페이로드 (Payload) 내의 정확한 체크포인트 토큰 ID와 그래프 상태를 정답으로 간주하여 복원하고, 나머지 서픽스 (Suffix) 부분만 토크나이즈합니다.

즉:

  • 룩업 (Lookup)은 렌더링 후 바이트열 기준
  • 복원 후의 상태는 페이로드 내의 정확한 토큰
  • 추가된 부분만 렌더링 서픽스에서 토크나이즈

이 설계를 통해 BPE 경계의 변동으로 인한 캐시 미스 (Cache miss)를 줄이고 있습니다. 새로운 프롬프트가 들어왔을 때의 판정 흐름은 다음과 같습니다.

내부 세션 페이로드는 ds4.h에 정의되어 있습니다.

#define DS4_SESSION_PAYLOAD_MAGIC UINT32_C(0x34565344) /* "DSV4" */
#define DS4_SESSION_PAYLOAD_VERSION UINT32_C(2)
#define DS4_SESSION_PAYLOAD_U32_FIELDS 13u

ds4.c의 Session Snapshot Payloads 주석이 저장 대상을 설명하고 있습니다.

* The payload is model-specific rather than self-describing.
* The fixed header records enough shape information to reject a file written
* for a different DS4 runtime, then the body writes: checkpoint tokens,
...

헤더의 13개 필드는 다음과 같습니다.

인덱스필드
0매직 넘버 (magic) DSV4
1페이로드 버전 (payload version)
2저장된 컨텍스트 크기 (saved context size)
3프리필 청크 크기 (prefill chunk size)
...

페이로드 본체는 다음 순서입니다.

u32[token_count]

체크포인트 토큰 ID -
float32[vocab_size]

다음 토큰 로짓 (logits) -
u32[layer_count]

압축된 어텐션 행 (compressed attention rows)의 개수 -
u32[layer_count]

ratio-4 인덱서 행 (indexer rows)의 개수 - 레이어별 raw SWA 행

  • 압축된 레이어의 어텐션 압축 행
  • compressor의 frontier 상태
  • ratio-4 레이어의 indexer 압축 행
  • indexer의 frontier 상태

로짓 (logits)까지 저장하고 있다는 점이 중요합니다. 복원 후에 "다음 토큰을 샘플링하기 위해서만 한 번 디코딩을 진행할" 필요가 없습니다. 프리픽스 (prefix)를 프리필 (prefill)한 직후의 다음 토큰 분포로부터 그대로 계속할 수 있습니다.

raw KV는 링 버퍼 (ring buffer)이므로, 메모리상에서는 물리적으로 되돌아갔을(wrap-around) 가능성이 있습니다. 하지만 페이로드는 물리적 배치가 아닌 논리적 순서로 작성합니다.

GPU 경로의 저장 구현에는 다음과 같은 주석이 있습니다.

/* Write the raw ring in logical position order. The file does not care
* where the rows happened to live physically in the source graph. */

이 설계 덕분에 저장 원본 런타임의 링 물리 상태를 파일 형식에 노출하지 않으면서, 로드 시에는 현재 세션 캐시(session cache)에 순서대로 복원할 수 있습니다.

반면, 압축 행은 추가 전용(append-only)이며, 행 0부터 라이브 카운트 (live count)까지 연속됩니다. 희소 어텐션 (sparse attention)은 프리픽스 전체의 압축 행 중에서 선택할 가능성이 있기 때문에, 라이브 행 수까지 저장합니다.

압축된 KV 행만 저장하면 충분하다는 식으로는 작동하지 않습니다. compressor는 ratio 경계에 도달하지 않은 중간 윈도우 (intermediate window)를 가지고 있기 때문입니다.

DSV4

페이로드는 압축 행 외에도 compressor의 frontier 텐서 (tensor)를 저장합니다.

attn_state_kv

attn_state_score

  • ratio-4 레이어에서는
    index_state_kv

  • ratio-4 레이어에서는
    index_state_score

이것들이 없으면 체크포인트의 다음 토큰으로부터 compressor를 올바르게 지속할 수 없습니다. 특히 ratio 4 / ratio 128 경계에 있는 경우, 다음에 압축 행이 생성되는 타이밍과 내용이 어긋나게 됩니다.

DS4의 페이로드는 "이미 완성된 KV 행"뿐만 아니라 "다음 압축 행을 만들기 위한 중간 상태"까지 저장하기 때문에, 로드 후에 프리필의 중간 단계부터라도 정확하게 이어갈 수 있습니다.

디스크 KV 캐시는 저장하면 할수록 좋은 것은 아닙니다. 프롬프트의 끝부분은 다음 요청에서 서픽스 (suffix)가 붙었을 때 BPE의 병합 경계 (merge boundary)가 바뀌기 쉽기 때문입니다.

DS4의 기본값은 다음과 같습니다.

#define KV_CACHE_DEFAULT_MIN_TOKENS 512
#define KV_CACHE_DEFAULT_COLD_MAX_TOKENS 30000
#define KV_CACHE_DEFAULT_BOUNDARY_TRIM_TOKENS 32
...

ds4_kvstore_store_len()은 저장 후보 토큰 수에서 32 토큰을 잘라낸(trim) 후, 그 다음 2048 경계로 내림(floor)합니다.

int stable = tokens - trim;
if (align > 0) stable -= stable % align;

이 2048 정렬 (alignment)에는 토크나이저뿐만 아니라 백엔드의 프리필 청크 스케줄링 (prefill chunk scheduling)과의 정합성도 고려되어 있습니다. 주석에는 2048 정렬이 compressor 행의 확정을 cold 상태의 전체 프롬프트와 일치시킨다고 적혀 있습니다.

저장할 프리픽스를 약간 짧게 만듦으로써, 다음 바이트 프리픽스 (byte prefix)의 히트율과 그래프 지속의 안정성을 높이는 것입니다.

README는 캐시 체크포인트 (cache checkpoint)가 저장되는 타이밍을 4가지로 나누고 있습니다.

이유역할
cold긴 첫 번째 프롬프트 (prompt)가 안정적인 프리픽스 (prefix)에 도달한 후, 생성 전
continuedprefill/생성이 정렬 프론티어 (alignment frontier)에 도달했을 때
evict무관한 요청이 라이브 세션 (live session)을 대체하기 전
shutdown서버가 깔끔하게 종료될 때

코드상의 enum에는 에이전트 (agent)를 위한 이유도 추가되어 있습니다.

DS4_KVSTORE_REASON_AGENT_SYSTEM = 5,
DS4_KVSTORE_REASON_AGENT_SESSION = 6,

서버의 자동 캐시와 에이전트의 명시적인 세션 저장이 동일한 저수준 (low-level) 형식 위에 올라와 있다는 점이 여기서도 드러납니다.

캐시 디렉토리에는 용량 예산이 있습니다. ds4_kvstore_open()의 기본값은 4096MB이며, 서버 기동 시 --kv-disk-space-mb로 지정할 수 있습니다.

어떤 파일을 삭제할지는 단순한 LRU (Least Recently Used)가 아닙니다. ds4_kvstore_entry_eviction_score()는 히트 수 (hit count), 경과 시간, 토큰 수, 파일 크기, 이유를 조합합니다.

effective_hits *= exp2(-elapsed / DS4_KVSTORE_HIT_HALF_LIFE_SECONDS);
score = (effective_hits + 1.0) * tokens / file_size;

DS4_KVSTORE_HIT_HALF_LIFE_SECONDS는 6시간입니다.

#define DS4_KVSTORE_HIT_HALF_LIFE_SECONDS (6ull * 60ull * 60ull)

나아가 cold / evict / shutdown은 앵커 이유 (anchor reason)로서, 점수에 소프트한 사전 가중치 (soft prior weight)가 부여됩니다.

#define KV_CACHE_ANCHOR_REASON_SCORE_FACTOR 2.0

continued 체크포인트는 긴 생성 중간의 중계 지점이므로, 프리픽스 관계나 최근의 히트 여부에 따라 가중치가 변합니다.

이 설계는 실용적입니다. 긴 프리픽스일수록 재사용 가치가 높은 반면, 너무 거대한 파일은 용량 효율이 떨어집니다. 최근에 히트된 캐시는 남기고 싶지만, 오래된 히트는 워크로드 (workload)가 변했을 가능성이 있습니다. DS4는 이를 점수화하여 처리합니다.

서버에서는 툴 호출 (tool call)의 정확한 DSML replay도 디스크 캐시와 관련이 있습니다.

README에 따르면, 옵션인 tool-id map 섹션은 KTM 매직을 가지며, API 툴 호출 ID로부터 모델이 실제로 샘플링한 DSML 블록에 대한 대응 관계를 저장합니다.

이는 모델 상태가 아니라 보조적인 재생 메모리 (replay memory)입니다.

왜 필요하냐면, 클라이언트는 다음 턴에 정규화된 JSON을 다시 보냅니다. 하지만 모델이 이전 턴에서 생성한 DSML 텍스트와 단 1바이트라도 다르면, 렌더링 후의 프롬프트가 바뀌어 KV 체크포인트와 일치하지 않게 됩니다.

tool-id map을 통해 서버 재기동 후에도 툴 호출 ID를 보고 "당시 모델이 내뱉었던 정확한 DSML 블록"을 복원할 수 있습니다. 이는 제5회에서 다룰 DSML 툴 처리의 기반입니다.

README는 KVC 파일을 일반적인 read / write I/O로 다루며, mmap 하지 않는다고 설명합니다.

이유는 DS4 프로세스가 이미 거대한 모델 GGUF를 mmap 하고 있기 때문입니다. 복원할 때마다 캐시 파일을 mmap 하면 VM 매핑 (VM mapping) 압력이 증가합니다. DS4는 페이로드 (payload) 바이트열을 기존의 Metal 텐서 (tensor) / CPU 버퍼에 복사하여 되돌리는 설계를 선택했습니다.

ds4.c의 페이로드 주석에도 복원은 체크포인트의 바이트열을 기존의 그래프 버퍼 (graph buffer)에 복사한다고 적혀 있습니다.

* This payload is intentionally not mmaped: restoring a checkpoint copies
* bytes back into the already allocated Metal tensors ...

이것은 macOS의 거대한 mmap과 VM 압력을 의식한, 상당히 구현 측면을 고려한 판단입니다.

분산 추론 (Distributed Inference)에서는 각 워커 (Worker)가 자신의 레이어 슬라이스 (Layer Slice)에 대한 KV를 가집니다. 하지만 README는 저장되는 DSV4 페이로드가 토폴로지 비의존적 (Topology-agnostic)이라고 설명합니다.

저장 시에는 코디네이터 (Coordinator)가 워커가 소유한 레이어 텐서 (Layer Tensor)를 수집하여 일반적인 레이어 순서의 텐서 스트림 (Tensor Stream)으로 통합합니다. 로드 시에는 현재의 루트 (Route)에 맞춰 다시 워커들에게 배분합니다.

즉, 디스크 파일은 "2대 구성이었는지" 혹은 "3대 구성이었는지"를 기억하지 않습니다. 기억하는 것은 DS4의 모델 레이아웃 (Model Layout)에 대한 레이어별 세션 상태 (Session State)입니다.

이러한 분리 덕분에 분산 구성을 변경하더라도 호환 가능한 런타임 (Runtime)이라면 동일한 캐시 파일을 읽을 수 있습니다.

지금까지 살펴본 바와 같이, KVC 파일에는 "rendered text bytes", 즉 캐시 대상 프리픽스 (Prefix)의 토크나이저 (Tokenizer) 복호화 텍스트가 그대로 들어갑니다. README에서도 KV 캐시 파일에는 프롬프트가 verbatim (축자적/있는 그대로)로 저장되므로, 동작이 이상할 때는 캐시 디렉터리 전체를 삭제해도 되며 (디렉터리는 disposable 함), hexdump로 내용을 확인할 수 있다고 설명합니다.

이는 설계상으로는 합리적이지만 (바이트 프리픽스 매칭을 위한 동일성 확보 및 인간이 내용을 확인할 수 있음), 운영 측면에서는 프라이버시 및 정보 관리상의 주의 사항이 됩니다.

디스크 KV 캐시 (기본 --kv-disk-dir 하위, 에이전트(Agent)는 ~/.ds4/kvcache)에는 시스템 프롬프트, 도구 정의 (Tool Definition), 대화 내용, 읽어들인 파일 내용 등이 축자적 텍스트로 남습니다. 기밀 정보를 다루는 운영 환경에서는 저장 위치의 배치, 접근 권한, 암호화 볼륨 사용, 불필요한 캐시 삭제 방침을 일반적인 로그나 임시 파일과 동일하거나 그 이상으로 관리해야 합니다. 공유 머신이나 외부 반출용 단말기에서는 특히 주의가 필요합니다.

DwarfStar의 디스크 KV 캐시는 흔히 볼 수 있는 "프롬프트 텍스트를 캐시하는" 메커니즘보다 훨씬 깊이가 있습니다.

  • 룩업 (Lookup)의 동일성은 렌더링 후 바이트열의 SHA1
  • 정확한 상태는 DSV4 페이로드 내의 토큰 ID (Token ID)와 그래프 텐서 (Graph Tensor) - Raw KV는 논리적 순서, 압축된 KV는 라이브 행 (Live Row), Frontier 상태도 저장
  • 다음 토큰 로짓 (Next Token Logits)도 저장하여, 복원 후 즉시 샘플링 (Sampling) 가능
  • BPE 경계 대책으로 32 토큰 절단 (Truncation) 및 2048 정렬 (Alignment) 수행
  • 퇴출 (Eviction)은 6시간 반감기의 히트 스코어 (Hit Score)와 토큰 밀도를 조합
  • 도구의 exact replay를 위한 트레일러 (Trailer)도 동일한 파일 엔벨로프 (File Envelope)에 실릴 수 있음

이러한 설계가 있기 때문에, 스테이트리스 (Stateless)한 OpenAI/Anthropic 호환 클라이언트로부터 긴 대화 이력이 여러 번 전송되더라도, DS4는 프리픽스의 prefill을 재사용할 수 있습니다.

다음 회차에서는 그 서버 계층을 살펴보겠습니다. OpenAI /v1/chat/completions, OpenAI Responses /v1/responses, Anthropic /v1/messages를 받으면서, DeepSeek V4의 DSML 도구 호출을 어떻게 exact replay 하고 있는지 추적하겠습니다.

본 기사는 Quick Iterate의 로컬 LLM 연구의 일환으로, 공개 리포지토리 antirez/ds4의 코드를 분석한 것입니다. 행 번호, 상수, 벤치마크 값은 열람 커밋 ba00a8a (2026-05-30) / README 취득일 2026-06-01 시점의 것입니다. ds4-agent는 alpha 단계이며, 엔진 본체는 beta 품질로 활발하게 변화하고 있으므로, 인용된 부분은 각자 최신 README 및 소스 코드를 통해 다시 확인하시기 바랍니다.

Quick Iterate Co., Ltd. ― IoT / 전력 모니터링 / AI / 위성·무선 통신 / 시스템 통합

로컬 LLM 및 에이전트 기반에 관한 문의는 언제든 환영합니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0