본문으로 건너뛰기

© 2026 Molayo

Qiita헤드라인2026. 06. 03. 17:56

【#5】ds4.c 분석하기

요약

DeepSeek V4 Flash/Pro 전용 추론 엔진인 DwarfStar(ds4)의 서버 코드를 분석합니다. 특히 OpenAI 및 Anthropic 호환 API를 제공하면서 모델의 DSML 도구 호출과 KV 캐시 간의 정합성을 유지하는 메커니즘을 다룹니다.

핵심 포인트

  • DSML 텍스트와 클라이언트 JSON 간의 바이트 불일치 문제 해결
  • Exact replay를 통한 KV 캐시 손상 방지 전략
  • 도구 호출 시 구문 부분의 temperature=0 설정 기법
  • OpenAI, Anthropic, Codex CLI 대응 엔드포인트 구현

본 시리즈는 DeepSeek V4 Flash / Pro 전용 추론 엔진인 DwarfStar (ds4)의 코드를 분석하는 연재입니다.

제5회는 ds4-server가 OpenAI Chat Completions, OpenAI Responses, Anthropic Messages를 받으면서, DeepSeek V4의 DSML 도구 호출(tool calling)과 KV 캐시를 어떻게 정합시키는지 읽어봅니다. 주요 참조 부분:

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

관전 포인트: 호환 API의 본질적인 난관은 HTTP가 아니라, 모델이 출력한 DSML과 클라이언트가 반환하는 정규화된 JSON 사이의 바이트 차이가 KV를 손상시키는 문제이며, 이를 exact replay로 어떻게 방지하는가입니다.

ds4-server는 OpenAI/Anthropic 호환 API를 제공하지만, 내부 모델은 DeepSeek V4의 DSML 도구 호출 텍스트를 생성합니다. - HTTP 요청 파싱과 소켓 I/O는 클라이언트 스레드가 수행합니다. 추론은 단일 그래프 워커(graph worker)로 직렬화되며, 라이브 ds4_session과 KV 상태는 워커가 소유합니다. - 대응 엔드포인트는 /v1/chat/completions, /v1/responses, /v1/messages 등입니다. Codex CLI에는 Responses가, Claude Code 계열에는 Anthropic 엔드포인트가 상정되어 있습니다. - 가장 큰 문제는 모델이 생성한 DSML 텍스트와 다음 요청에서 클라이언트가 다시 보내는 정규화된 JSON이 바이트 단위로 일치하지 않는다는 점입니다.

  • 제1 방어선은 exact replay입니다. 도구 호출 ID로부터 모델이 실제로 샘플링한 DSML 블록을 다시 불러옵니다.
  • exact replay가 없는 경우에만 결정적인 DSML 정규화 (canonicalization) 및 KV 체크포인트의 재작성 / 디스크로의 폴백 (fallback)을 사용합니다.
  • 도구 호출 생성 중에는 DSML의 구문 부분만 temperature=0으로 설정하고, 인자 페이로드(argument payload)는 일반적인 샘플링으로 되돌립니다.

ds4-server는 README 상에서 OpenAI/Anthropic 호환 서버로 소개되고 있습니다.

./ds4-server --ctx 100000 --kv-disk-dir /tmp/ds4-kv --kv-disk-space-mb 8192

대응 엔드포인트는 다음과 같습니다.

GET /v1/models
GET /v1/models/deepseek-v4-flash
GET /v1/models/deepseek-v4-pro
...

각각의 역할은 다음과 같이 정리할 수 있습니다.

엔드포인트상정 클라이언트특징
/v1/chat/completionsOpenAI Chat Completions 호환 클라이언트OpenAI 형식의 messages / tools
/v1/responsesCodex CLIResponses 이벤트 라이프사이클
/v1/messagesClaude Code 계열 클라이언트Anthropic tool_use / thinking
/v1/completions기존의 보완 (completion)원형에 가까운 보완

단, 내부의 DeepSeek V4는 OpenAI의 JSON 도구 호출을 직접 생성하는 것이 아닙니다. 모델은 DSML 텍스트를 생성합니다.

DSML의 형태는 대략 다음과 같은 XML 스타일의 텍스트입니다.

<|DSML|tool_calls>
<|DSML|invoke name="tool_name">
<|DSML|parameter name="path" string="true">README.md</|DSML|parameter>
...

서버의 역할은 클라이언트로부터 오는 OpenAI/Anthropic/Responses의 JSON 세계와 모델이 다루는 DSML 텍스트 세계를, KV 캐시를 손상시키지 않고 왕복시키는 것입니다.

ds4_server.c

ds4_server.c 서두의 주석은 서버의 병렬 모델 (concurrency model)을 간결하게 설명하고 있습니다.

/* HTTP is intentionally simple: each client connection is handled by a small
* blocking thread that parses one request, then queues a job to the single
* Metal worker. The worker owns the ds4_session and therefore owns all live KV
...

클라이언트 스레드는 요청을 파싱하여 작업 큐 (job queue)에 쌓습니다. 추론 자체는 단일 워커 (single worker)가 수행합니다.

이는 처리량 (throughput) 측면에서는 보수적인 방식입니다. README에서도 현재 서버는 여러 독립적인 요청을 배치 (batch) 처리하지 않는다고 설명하고 있습니다.

반면, 라이브 KV 상태를 한곳에 가두는 효과가 있습니다.

ds4_session
을 여러 스레드가 동시에 변경하지 않음

  • 프리픽스 (prefix) 재사용과 디스크 체크포인트 (disk checkpoint) 판단을 워커로 집약할 수 있음
  • 향후 배치 처리를 도입하더라도 그래프 변경의 책임이 분산되지 않음

로컬 코딩 에이전트용 서버로서는, 우선 긴 세션을 올바르게 유지하는 것이 우선순위입니다.

Chat/Responses/Anthropic 클라이언트는 기본적으로 매번 트랜스크립트 (transcript) 전체를 보냅니다. DS4 서버 측은 이전의 라이브 세션과 이번 프롬프트의 프리픽스를 비교합니다.

ds4.h에는 이를 위한 API가 준비되어 있습니다.

int ds4_session_common_prefix(ds4_session *s, const ds4_tokens *prompt);
ds4_session_rewrite_result ds4_session_rewrite_from_common(...);
int ds4_session_sync(ds4_session *s, const ds4_tokens *prompt, ...);

ds4_server.c에서는 요청 처리 중에 다음과 같은 흐름을 따릅니다.

  • 라이브 세션과 새 프롬프트의 토큰 레벨 (token level) 공통 프리픽스를 확인
  • 라이브 체크포인트를 사용할 수 있다면 서픽스 (suffix)만
    ds4_session_sync()
  • 라이브가 실패(miss)라면 디스크 KV 캐시에서 렌더링 후 바이트 프리픽스 (rendered byte prefix)를 탐색
  • 디스크 히트 (disk hit) 시
    DSV4
    페이로드를 복원하고, 서픽스만 토크나이즈 (tokenize)/prefill - 그래도 안 된다면 토큰 0부터 prefill

여기서 4회차에서 다룬 디스크 KV가 효과를 발휘합니다. 토큰 ID 프리픽스가 일치하지 않더라도, 렌더링 후 바이트 프리픽스가 일치하면 체크포인트를 복원할 수 있기 때문입니다.

도구 호출 (tool call)에서 문제가 되는 것은, 클라이언트가

가 API 스타일에 따라 프리픽스(prefix)를 변경합니다.

const char *prefix = api == API_ANTHROPIC ? "toolu_" : "call_";

그 후, 서버는 다음 대응을 기억합니다.

tool id -> exact sampled DSML block

tool_calls 구조체에는 raw_dsml이 있으며, 프롬프트 렌더러(prompt renderer)는 이것이 존재할 경우 정준적인(canonical) 재생성이 아니라 로우(raw) DSML을 그대로 출력합니다.

static void append_dsml_tool_calls_text(buf *b, const tool_calls *calls) {
if (calls->raw_dsml && calls->raw_dsml[0]) {
buf_puts(b, calls->raw_dsml);
...

이를 통해 클라이언트가 정규화된 JSON을 다시 보내더라도, 서버는 툴 ID(tool ID)를 보고 당시 모델이 실제로 샘플링한 DSML 바이트 열(byte sequence)을 복원할 수 있습니다.

나아가 제4회에서 보았듯이, 이 tool-id map은 KV 캐시 파일의 옵션 트레일러(optional trailer)에도 저장할 수 있습니다. 서버 재시작 후에도 캐시된 이력에 대해 정확한 리플레이(exact replay)가 가능해집니다.

다음 턴에 툴 결과가 왔을 때의 판정은 다음과 같습니다.

정확한 DSML 블록을 찾을 수 없는 경우, 서버는 JSON으로부터 결정론적인(deterministic) DSML을 생성합니다. 이것이 정준화(canonicalization)입니다.

README에는 정준화가 백업 경로라고 명확히 적혀 있습니다.

이유는 간단합니다. 정준화는 "앞으로는 이 형식에 맞추겠다"는 처리이지, "과거에 모델이 샘플링한 바이트 열과 동일하다"는 것을 보장하지 않기 때문입니다.

ds4_server.c에는 툴 체크포인트의 정준화 처리가 있습니다. 흐름은 다음과 같습니다.

  • 라이브 샘플링된 토큰 스트림과 정준 프롬프트(canonical prompt)의 공통 프리픽스(common prefix)를 추출
  • 완전 일치하면 아무것도 하지 않음
  • 차이가 작다면 ds4_session_rewrite_from_common()으로 라이브 체크포인트를 재작성
  • 필요하다면 디스크 KV에서 오래된 체크포인트를 로드하여 서픽스(suffix)만 재생
  • 그래도 안 된다면 재구축

해당 부분에서는 다음 API가 사용됩니다.

const int common = ds4_session_common_prefix(s->session, &canonical);
ds4_session_rewrite_from_common(s->session, &canonical, common, ...);

이 설계는 상태가 없는(stateless) JSON 트랜스크립트와 상태가 있는(stateful) 샘플링된 KV 사이의 차이를 가능한 한 국소적으로 수정하기 위한 것입니다.

DSML은 텍스트이므로, 모델이 설명문 안에서 DSML 예시를 들 수도 있습니다. thinking 내에 DSML처럼 보이는 텍스트가 나올 수도 있습니다.

ds4_server.c의 생성 메시지 파서(generation message parser)는 </think> 이후, 즉 실행 가능한 어시스턴트 표면(assistant surface)에 들어온 DSML만을 툴 호출(tool call)로 취급하도록 설계되어 있습니다.

주석에는 thinking이 닫히지 않은 경우 reasoning 내의 DSML을 무시한다고 적혀 있습니다.

/* Model did not close thinking, ignore any DSML in reasoning */

또한, DSML이 중간에 끊겼을 경우의 복구 기능도 있습니다.

/* Try to repair a truncated DSML block.
* DSML nesting order is: tool_calls > invoke > parameter.
*/

툴 호출 파싱은 사용자에게 보이는 텍스트를 파싱하는 것이 아니라, 실행 가능한 프로토콜 표면을 판정하는 것입니다. 여기서 실수하면 단순한 설명문을 툴 호출로 실행해 버리게 됩니다.

OpenAI나 Anthropic의 스트리밍 API에서는 클라이언트가 툴 호출 JSON 델타(delta)를 기대합니다. 하지만 DS4의 모델은 DSML 바이트 열을 생성하고 있습니다.

ds4_server.c의 주석은 이 변환을 설명하고 있습니다.

/* 프로토콜별 DSML 스트림 프로젝션(projections)을 위한 공유 상태. 모델은
* 여전히 DSML을 샘플링하지만, 이 상태들은 이미 샘플링된 바이트를
* OpenAI / Anthropic 와이어 이벤트(wire events)로 변환할 뿐입니다... */

즉, 와이어 이벤트(wire event)는 DSML로부터의 프로젝션(projection)입니다.

  • OpenAI Chat Completions에서는
    tool_calls[].function.arguments

의 델타(delta)로 출력

  • Anthropic에서는 최종적으로
    tool_use

블록으로 출력

  • Responses에서는
    response.function_call_arguments.delta

와 같은 라이프사이클 이벤트(lifecycle event)로 출력

모델의 샘플링 스트림(sampling stream)은 DSML 상태로 유지되므로, 나중에 정확한 재생(exact replay)에 사용할 수 있습니다. 와이어 프로토콜(wire protocol)의 외관만 클라이언트에 맞춰주는 것입니다.

도구 호출(tool call) 생성에서 더욱 흥미로운 점은, 샘플링 정책이 DSML 상태에 따라 달라진다는 점입니다.

ds4_server.c에는 DSML 디코딩 상태가 있습니다.

typedef enum {
DSML_DECODE_OUTSIDE,
DSML_DECODE_STRUCTURAL,
...

페이로드 샘플링(payload sampling)을 사용해도 되는 상태는 문자열 본체뿐입니다.

static bool dsml_decode_state_uses_payload_sampling(dsml_decode_state state) {
return state == DSML_DECODE_STRING_BODY ||
state == DSML_DECODE_JSON_STRING;
...

생성 루프(generation loop)에서는, 도구 호출 중이면서 페이로드 샘플링 상태가 아니라면 temperature를 0으로 설정합니다.

if (in_tool_call && !dsml_decode_state_uses_payload_sampling(dsml_state)) {
temperature = 0.0f;
}

이를 통해 DSML 태그, 파라미터 헤더, JSON 구분자, 종료 마커와 같은 구문(syntax) 부분은 결정론적(deterministic)이 됩니다. 반면, 파일 내용이나 편집 텍스트와 같은 긴 인자 페이로드(argument payload)는 일반적인 샘플링을 유지합니다.

README에서도 이 분리가 중요하다고 설명합니다. 구문을 결정론적으로 만드는 것은 파싱 가능성(parsability)에 도움이 되지만, 긴 코드/파일 본체까지 탐욕적으로(greedily) 결정론적으로 만들면 반복(repetition)을 초래할 수 있기 때문입니다.

Responses API나 Anthropic Messages에는 도구 결과(tool result)만 다음 요청에 포함되는 라이브 지속 경로(live continuation path)가 있습니다.

ds4_server.c에는 Responses를 위한 다음과 같은 플래그가 있습니다.

bool responses_requires_live_tool_state;
bool responses_requires_live_reasoning;

Anthropic 측에도 유사하게 다음과 같은 항목이 있습니다.

bool anthropic_requires_live_tool_state;
stop_list anthropic_live_call_ids;
char *anthropic_live_suffix_text;

이는 눈에 보이는 트랜스크립트(transcript)만으로는 복원할 수 없는 숨겨진 사고(thinking)나 이미 샘플링된 DSML 상태가 라이브 세션에 남아 있는 경우, 도구 결과의 지속을 라이브 상태(live state)로 묶기 위함입니다.

라이브 상태가 남아 있으면 고속 경로를 사용합니다. 남아 있지 않으면 클라이언트에게 전체 이력의 재생을 요청하거나, 디스크 KV / exact replay를 통해 복원을 시도합니다.

로컬 에이전트 서버에서는 이 "클라이언트가 보고 있는 트랜스크립트"와 "모델 내부에서 샘플링된 숨겨진 상태"가 일치하지 않는 경우가 많은데, DS4 서버는 이 부분을 세심하게 처리한다는 점이 특징입니다.

README는 thinking 모드에서 reasoning을 최종 텍스트에 섞지 않고, API별 네이티브 형태(native shape)로 스트리밍한다고 설명합니다.

  • Chat Completions: 도구 호출 (tool call) 델타 또는 content 델타
  • Responses:
    response.output_text.delta

함수 호출 (function-call) 인자 이벤트, 종료 시의 completed/incomplete/failed 이벤트 - Anthropic: thinking/text를 라이브 스트림(live stream)하고, 완료 후에 구조화된 tool_use

ds4_server.c

에는 responses_sse_* 함수군이 나열되어 있으며, Codex가 기대하는 시퀀스 번호(sequence number)나 라이프사이클을 생성합니다.

여기서도 내부 표현은 DSML과 샘플링된 토큰이며, 와이어 프로토콜 (wire protocol)은 투영 (projection)입니다.

지금까지의 메커니즘은 기존의 로컬 코딩 에이전트에서 그대로 사용할 수 있습니다. 원칙적으로 --ctx로 기동한 값 이하로 클라이언트 측의 컨텍스트 상한을 설정해야 합니다.

다음의 접속 절차는 README에서 변경되기 쉬운 부분입니다. 환경 변수 이름이나 플래그는 업데이트될 수 있으므로, 실제로 설정할 때는 최신 README의 해당 절(commit ba00a8a 시점을 기준으로 함)을 확인하십시오.

여기서는 "개념 예시"로 읽어 주시기 바랍니다.

Claude Code:

(Anthropic 호환 엔드포인트로 향하게 하는 래퍼 (wrapper) 예시. README의 ~/bin/claude-ds4에 해당)

#!/bin/sh
unset ANTHROPIC_API_KEY
export ANTHROPIC_BASE_URL="${DS4_ANTHROPIC_BASE_URL:-http://127.0.0.1:8000}"
...

Sonnet/Haiku/Opus의 에일리어스 (alias)를 모두 deepseek-v4-flash로 향하게 하고, 서브 에이전트 모델도 동일하게 맞춥니다. CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFICCLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK으로 로컬 서버가 다루지 않는 경로를 무효화하고, CLAUDE_STREAM_IDLE_TIMEOUT_MS로 로컬 추론의 긴 무음 시간에 대비합니다.

Codex CLI (Responses 와이어 API):

[model_providers.ds4]
name = "DS4"
base_url = "http://127.0.0.1:8000/v1"
...
codex --model deepseek-v4-flash -c model_provider=ds4

opencode / Pi용 프로바이더 설정 예시는 README에 정리되어 있습니다. Claude Code는 첫 실행 시 25k 토큰 규모의 큰 프롬프트를 보낼 수 있으므로, --kv-disk-dir을 활성화해 두면 첫 번째의 고비용 prefill을 후속 세션에서 재사용할 수 있습니다 (제4회 참조).

ds4-server의 어려움은 "HTTP 서버를 작성하는 것"이 아닙니다.

어려운 점은 다음 세 가지를 동시에 만족하는 것입니다.

  • OpenAI/Responses/Anthropic의 스테이트리스 (stateless) JSON API와 호환되는 것처럼 보이게 할 것
  • DeepSeek V4의 DSML 도구 호출 텍스트를 모델이 그대로 다루게 할 것
  • 라이브 KV 체크포인트와 렌더링 후 프롬프트의 바이트열 (byte sequence)을 깨뜨리지 않을 것

이를 위한 설계는 다음과 같습니다.

  • 단일 그래프 워커 (single graph worker)
  • 공통 프리픽스 (common prefix) 재사용
  • 렌더링 후 바이트의 SHA1을 이용한 디스크 KV
  • 도구 ID를 통한 정확한 (exact) 샘플링된 DSML replay
  • 백업으로서의 정준화 (canonicalization) / 체크포인트 쓰기
  • DSML 구문만 탐욕적 (greedy)으로 하는 샘플링 전환
  • API별 SSE 투영 (projection)

입니다.

다음 회차에서는 한 대의 머신을 넘어서는 이야기로 진행합니다. DS4의 분산 추론은 코디네이터가 모든 처리를 중계하는 것이 아니라, 워커가 연속된 레이어 슬라이스 (layer slice)를 가지고 은닉 상태 (hidden state)를 TCP로 파이프라이닝 (pipelining)하는 구성입니다.

본 기사는 퀵이테레이트 주식회사 (Quick Iterate Co., Ltd.)의 로컬 LLM 연구의 일환으로, 공개 리포지토리 antirez/ds4의 코드를 분석한 것입니다. 행 번호, 상수, 벤치마크 값은 열람 커밋 ba00a8a를 기준으로 합니다.

(2026-05-30)/README 획득일 2026-06-01 시점의 것입니다. ds4-agent는 alpha, 엔진 본체는 beta 품질로 활발하게 변화하므로, 인용된 부분은 각자 최신 README / 소스 코드를 확인하여 재확인하시기 바랍니다.

Quick Iterate 주식회사

IoT / 전력 모니터링 / AI / 위성·무선 통신 / 시스템 통합 (System Integration)/

로컬 LLM · 에이전트 (Agent) 기반에 관한 문의는 언제든 편하게 해주세요.

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0