
【#6】ds4.c 분석하기
요약
DeepSeek V4 Flash/Pro 전용 추론 엔진인 DwarfStar(ds4)의 분산 추론 메커니즘을 분석합니다. 모델을 레이어 슬라이스로 나누어 여러 머신에 분산 탑재하는 방식과 prefill/생성 단계의 성능 특성을 다룹니다.
핵심 포인트
- 레이어 슬라이싱을 통한 대규모 모델의 다중 머신 분산 탑재
- Prefill 단계의 파이프라인화를 통한 속도 향상 효과
- 자기회귀적 특성으로 인한 생성(Generation) 단계의 속도 저하
- 워커 간 직접 활성화 전달 및 와이어 프로토콜 구조
본 시리즈는 DeepSeek V4 Flash / Pro 전용 추론 엔진인 DwarfStar(ds4)의 코드를 분석하는 연재입니다.
제6회는 모델을 레이어 슬라이스(layer slice)로 나누어 여러 대의 머신에서 구동하는 분산 추론(distributed inference)을 다룹니다. 주요 참조 부분:
README.md, ds4_distributed.c, ds4_distributed.h, ds4.h
관전 포인트: 분산 환경에서 "prefill은 빠르고, 생성(generation)은 느리다"라는 비대칭이 발생하는 이유와, 그럼에도 불구하고 여러 대의 머신으로 구동할 가치가 어디에 있는지가 핵심입니다.
- DS4의 분산 추론은 거대한 GGUF를 여러 머신에 분할하여 탑재하기 위한 메커니즘입니다. 각 프로세스는 동일한 GGUF를 참조하지만,
--layers를 통해 자신의 레이어 슬라이스(layer slice)만 매핑합니다. - 코디네이터(coordinator)는 토큰화(tokenization), 샘플링(sampling), 프롬프트(prompt), 그리고 첫 번째 레이어 슬라이스를 가집니다. 워커(worker)는 후속 레이어 슬라이스와 자신의 KV 캐시(KV cache)를 가집니다. - 활성화(activation)는 코디네이터를 거치지 않고 워커 간에 직접 흘릴 수 있습니다. 전형적인 경로는
A -> B -> C -> A입니다. - prefill은 파이프라인화(pipeline)할 수 있기 때문에 빨라집니다. README의 Thunderbolt 5 / 2대의 M5 Max 환경에서는 63,819 토큰의 프롬프트에서 1.85배 빨라졌습니다. - 생성은 토큰마다 로짓(logits)과 샘플링을 기다려야 하는 자기회귀(autoregressive) 경로이므로 파이프라인화할 수 없습니다. 동일한 구성에서 19.4% 느려진다는 실측치가 기재되어 있습니다. - 와이어 프로토콜(wire protocol)은
DS4D매직,HELLO/WORK/RESULT/SNAPSHOT_*프레임을 사용합니다.WORK에는 세션 ID, 토큰 스팬(token span), prefix/result 해시, 루트(root), 은닉 상태(hidden state) 페이로드가 포함됩니다. - 워커는 로링(rolling) 64bit 토큰 프리픽스 해시를 검증하여, 재시작 후 위치 N의 작업을 잘못 받는 일을 방지합니다. - 영구 KV(persistent KV)는 단일 머신과 동일하게
DSV4페이로드로 집약됩니다. 저장 시에는 워커가 소유한 레이어 텐서(layer tensor)를 코디네이터가 수집합니다.
분산 추론이라고 하면 우선 속도 향상을 떠올립니다. 하지만 DS4의 README는 목적을 두 가지로 나누고 있습니다.
- 한 대의 머신에 다 올라가지 않는 모델/양자화(quantization)를 여러 머신에 나누어 탑재
- 긴 prefill을 여러 GPU에서 파이프라인화하여 빠르게 수행
대표적인 예로, 풀 4bit Flash 양자화를 2대의 128GB MacBook에 분할하는 구성이 언급되어 있습니다. 각 프로세스는 동일한 GGUF를 가지지만, --layers로 읽어들일 텐서 슬라이스(tensor slice)를 한정합니다.
# Machine A: coordinator, layers 0..19
./ds4 \
-m gguf/DeepSeek-V4-Flash-Q4KExperts-...gguf \
...
20:output은 레이어 20부터 최종 레이어까지에 더해 출력 헤드(output head)도 워커가 가진다는 의미입니다. 최종 워커가 로짓(logits)을 직접 반환할 수 있기 때문에, prefill 이후에 거대한 은닉 상태 배치(hidden state batch)를 코디네이터로 되돌릴 필요가 없습니다.
README의 멘탈 모델(mental model)은 다음과 같습니다.
- GGUF는 모든 머신에 둔다
- 단, 각 머신은
--layers로 부분 집합만 매핑한다 - 코디네이터는 프롬프트, 토큰화, 샘플링, 일반적인 CLI/API 동작을 가진다
- 워커는 자신의 레이어 슬라이스와 KV 캐시를 가진다
- 활성화의 흐름은 코디네이터를 거치는 것에 국한되지 않고 워커 간에 흐른다
ds4_distributed.c의 서두에 있는 주석에서도 워커가 로컬 엔진과 동일한 그래프 슬라이스(graph slice) 엔트리 포인트(entry point)를 사용한다고 설명하고 있습니다.
/* Workers execute contiguous model slices with the same graph-slice entry
* points used by the local engine. KV snapshots remain topology-independent:
* save gathers worker-owned layer tensors into the normal DSV4 payload, and
...
분산 전용의 별도 그래프를 만드는 것이 아니라, 기존의 레이어 슬라이스 API를 네트워크 전송으로 연결하고 있는 것입니다.
긴 프롬프트의 prefill (prefill)은 다수의 토큰을 일괄적으로 처리할 수 있습니다. DS4는 이를 청크 (chunk) 단위로 나누어 파이프라인화 (pipeline) 합니다.
README에는 "코디네이터 (coordinator)가 청크 N+1을 처리하는 동안 워커 (worker)가 청크 N을 처리할 수 있다"라고 설명되어 있습니다. 이는 조립 라인과 같습니다.
2대 구성으로 생각하면 다음과 같습니다.
time 1: A processes chunk 0 layers 0-19
time 2: B processes chunk 0 layers 20-output | A processes chunk 1 layers 0-19
time 3: B processes chunk 1 layers 20-output | A processes chunk 2 layers 0-19
...
각 청크는 순차적으로 레이어 슬라이스 (layer slice)를 통과해야 하지만, 서로 다른 청크는 서로 다른 머신 위의 서로 다른 스테이지 (stage)에 배치될 수 있습니다.
README의 실측 데이터에서는 M5 Max 128GB 2대, Thunderbolt 5, Q4 Flash GGUF, 4096 토큰 분산 prefill 청크 조건에서 다음과 같은 수치가 기록되어 있습니다. 여기서 주의할 점은, 이 표가 엄격하게 동일한 조건에서의 비교는 아니라는 점입니다. 분산 측 (Two MacBooks)은 Q4 Flash GGUF를 사용했지만, single-process reference 열은 Q2 GGUF를 사용한 단일 기기 실행이므로, Routed MoE가 작은 만큼 참조 측이 본래 약간 더 유리합니다. 즉, 아래의 Speedup (가속도)은 "동일한 양자화(quantization)를 1대 vs 2대로 비교한 순수한 스케일링 (scaling)"이 아니라, 양자화 차이를 포함한 참고치로 읽어야 합니다.
| Prompt | Single-process reference (Q2) | Two MacBooks (Q4) | Speedup |
|---|---|---|---|
| 9,421 tokens | 421.70 t/s | 582.22 t/s | 1.38x |
| ... |
프롬프트가 길수록 파이프라인이 채워지며 가속 효과가 커집니다.
이 표는 README에 게재된 실측값입니다. 재현성을 위해 인용할 때는 commit / OS / GGUF 파일명 / 청크 사이즈 / 전력(발열) 상태 / single-run 여부 또는 평균값 등을 각주에 고정하는 것이 안전합니다 (README 원문에서는 2대 M5 Max 128GB · Thunderbolt 5 · Q4 Flash · 4096 토큰 청크로 읽히지만, commit이나 발열 조건까지는 명시되어 있지 않습니다). 본 기사에서는 "공개된 참고치"로 취급해 주십시오.
반면, 디코딩 (decoding)/생성 (generation)은 엄격하게 자기회귀 (autoregressive) 방식입니다.
토큰 N의 logits (로짓)이 나오고 샘플링 (sampling)을 통해 토큰 N+1이 결정될 때까지 다음 토큰의 계산을 시작할 수 없습니다. prefill처럼 청크 N+1을 미리 진행시킬 수 없습니다.
분산 생성 시에는 매 토큰의 활성화 값 (activation)이 네트워크 홉 (network hop)을 거쳐야 합니다.
coordinator slice -> worker slice -> logits -> coordinator sampling
그렇기 때문에 README에서는 동일한 Thunderbolt 구성에서 12k 컨텍스트의 대조 실행 시 30.59 t/s에서 24.67 t/s로 떨어져, 19.4%의 손실이 발생했다고 보고하고 있습니다.
분산 추론은 디코딩을 빠르게 하기 위한 목적이 아니라,
- 1대에서는 올릴 수 없는 모델 / 양자화를 구동하기 위해
- 긴 prefill을 빠르게 하기 위해
사용하는 기능으로 보아야 합니다. prefill과 생성의 비대칭성을 도식화하면 명확해집니다.
ds4_distributed.c의 프로토콜 (protocol) 상수는 다음과 같습니다.
#define DS4_DIST_MAGIC 0x44533444u /* DS4D */
#define DS4_DIST_MSG_HELLO 1u
#define DS4_DIST_MSG_ERROR 2u
...
프레임 헤더 (frame header)는 매직 (magic), 타입 (type), 바이트 수 (byte count)만 포함하는 고정 형식입니다.
typedef struct {
uint32_t magic;
uint32_t type;
...
프로토콜은 릴리스 안정화 단계가 아니며, README에서도 동일한 commit으로부터 빌드한 신뢰할 수 있는 머신에서 사용할 것을 전제로 설명하고 있습니다. 암호화나 인증도 없습니다.
워커는 코디네이터에 제어 연결을 맺고, HELLO
를 보냅니다.
ds4_dist_hello_fixed
는 다음과 같은 정보를 가집니다.
typedef struct {
uint32_t model_id;
uint32_t quant_bits;
...
코디네이터(Coordinator)는 이 등록 정보를 보고, 모든 층(Layer)을 연속적으로 덮는 루트(Route)를 생성합니다. 모델 ID, 양자화 프로파일(Quantization Profile), 컨텍스트 용량, 층 범위가 일치하지 않는 워커(Worker)는 루트에 포함될 수 없습니다.
루트는 코디네이터의 로컬 슬라이스(Local Slice) 다음부터 시작되는 연속된 체인(Chain)입니다.
분산 처리의 중심은 WORK 프레임입니다. ds4_dist_work_fixed에는 세션/요청 ID, 토큰 위치, 해시(Hash), 루트, 페이로드 크기(Payload Size)가 들어갑니다.
typedef struct {
uint32_t model_id;
uint32_t session_hi;
...
여기서 prefix_hash는 작업 적용 전, result_hash는 토큰 스팬(Token Span) 적용 후의 롤링 토큰 프리픽스 해시(Rolling Token Prefix Hash)입니다.
flags에는 다음과 같은 것들이 있습니다.
#define DS4_DIST_WORK_F_INPUT_HC 0x00000001u
#define DS4_DIST_WORK_F_OUTPUT_LOGITS 0x00000002u
#define DS4_DIST_WORK_F_RESET_SESSION 0x00000004u
...
최종 워커가 출력 헤드(Output Head)를 가지는 경우 logits를 반환합니다. 프리필(Prefill) 파이프라인의 중간 청크(Chunk)에서는 ACK만 보내는 경우도 있습니다.
워커로부터의 RESULT는 유형을 가집니다.
#define DS4_DIST_RESULT_ACK 0u
#define DS4_DIST_RESULT_HIDDEN_STATE 1u
#define DS4_DIST_RESULT_LOGITS 2u
ds4_dist_result_fixed에는 요청 ID, 스팬 후 해시, 상태, 결과 유형, 텔레메트리(Telemetry), 페이로드 크기 등이 들어갑니다.
텔레메트리에는 층 범위, 토큰 스팬, 로컬 평가 시간, 다운스트림 대기 시간, 전송 송신 시간, 입출력 바이트 수가 포함됩니다.
README의 --debug 설명에 있는 "per-hop telemetry"가 바로 이 정보입니다. 층 분할의 균형이나 네트워크 병목(Bottleneck)을 확인하기 위한 구현입니다.
통상적으로 그래프 슬라이스(Graph-slice) API는 float 버퍼를 교환합니다. 분산 전송에서는 와이어(Wire) 상의 활성화(Activation) 폭을 변경할 수 있습니다.
ds4_distributed.c의 주석:
* The graph-slice APIs exchange float buffers. Distributed transport can leave
* those buffers as 32-bit floats or pack them to 16/8 bits on the wire; workers
* decode back to float before executing the next slice.
유효한 비트 폭은 32, 16, 8입니다.
return bits == 32u || bits == 16u || bits == 8u;
16bit는 f16 변환, 8bit는 E4M3 방식의 f8 변환입니다.
CLI에서는 --dist-activation-bits 16 또는 --dist-activation-bits 8을 사용합니다.
README에서는 16bit 전송은 트래픽을 절반으로 줄이는 첫 번째 선택지이며, 8bit는 근사적/실험적이라고 설명되어 있습니다. 다만 실험 결과, 활성화 크기(Activation Size)의 축소가 큰 개선을 보여주지 못해 향후 삭제될 수도 있다고 적혀 있습니다.
분산 추론에서 가장 두려운 것은 워커의 KV 상태가 코디네이터의 토큰 이력과 어긋나는 것입니다.
DS4는 작업마다 롤링 64bit 토큰 프리픽스 해시를 검증합니다. 구현 방식은 FNV-1a입니다.
#define DS4_DIST_TOKEN_HASH_INIT 1469598103934665603ull
#define DS4_DIST_TOKEN_HASH_PRIME 1099511628211ull
주석에서는 이것이 보안 요소가 아니라 세션 불변 조건 (session invariant)이라고 설명하고 있습니다.
/* 리틀 엔디언 (little-endian) 토큰 ID에 대한 FNV-1a. 이것은 보안 프리미티브 (security primitive)가 아니라,
* 분산된 워커 (worker)들이 레이어 작업을 수행하기 전에, 위치는 같지만 접두사 (prefix)가 다른
* KV 상태를 거부할 수 있도록 하는 컴팩트한 세션 불변 조건 (session invariant)입니다. */
워커는 "자신이 위치 N에 있다"고 생각하더라도, 접두사 해시 (prefix hash)가 다르면 작업을 거부합니다. 재부팅된 워커가 빈 KV 상태인 채로 위치 N의 작업을 묵묵히 받아들이는 것을 방지할 수 있습니다.
README에서는 워커의 해시 불일치는 토큰 이력 재생 (token history replay)을 통해 복구할 수 있는 반면, 전송 장애는 경로를 폐기하고 호환 가능한 워커의 재연결을 기다린다고 설명되어 있습니다.
제4회에서 보았던 DSV4
페이로드 (payload)는 분산 구성에서도 동일합니다.
ds4_distributed.c
의 서두 주석:
* KV 스냅샷은 토폴로지 독립적 (topology-independent)입니다:
* save는 워커가 소유한 레이어 텐서 (layer tensors)를 일반적인 DSV4 페이로드로 수집하며,
* load는 일반적인 DSV4 페이로드를 현재 등록된 경로 (route)에 따라 분할합니다.
프로토콜에는 스냅샷 프레임이 있습니다.
DS4_DIST_MSG_SNAPSHOT_SAVE_REQ
DS4_DIST_MSG_SNAPSHOT_BEGIN
DS4_DIST_MSG_SNAPSHOT_CHUNK
...
청크 (chunk) 크기는 8MiB입니다.
#define DS4_DIST_SNAPSHOT_CHUNK_BYTES (8u * 1024u * 1024u)
저장 시에는 코디네이터 (coordinator)가 워커의 데이터 연결을 열어 워커가 소유한 레이어 텐서를 가져온 뒤, 일반적인 레이어 순서의 DSV4 페이로드로 통합합니다. 로드 시에는 현재의 경로 (route)에 따라 레이어 텐서를 분배합니다.
파일에는 "이 세션은 2대 구성으로 저장했다"와 같은 토폴로지 정보가 남지 않습니다. 남는 것은 모델 레이아웃 (model layout)에 대한 레이어별 상태입니다.
README의 네트워크 비교는 분산 추론 (distributed inference)의 성능 특성을 잘 보여줍니다.
동일한 2대의 M5 Max, 동일한 91GB Flash 양자화 (quantization), 8192 토큰의 프롬프트, 128 생성 토큰 조건에서:
| Link | Ping avg | Prefill | Generation |
|---|---|---|---|
| Thunderbolt 5 | 0.45 ms | 582.99 t/s | 25.09 t/s |
| ... |
prefill은 큰 청크를 흘려보낼 수 있기 때문에 레이턴시 (latency)의 영향을 어느 정도 상쇄할 수 있습니다. 생성 (generation)은 토큰마다 왕복 (round-trip)이 발생하므로 레이턴시에 거의 직접적으로 영향을 받습니다.
따라서 대화형 코딩 에이전트 (coding agent)로 사용하려면 Thunderbolt 또는 고속 Ethernet이 현실적입니다. Internet/VPN은 "작동하지 않는 모델을 공동으로 들여다보는" 용도라면 의미가 있다는 위치 선정입니다.
DS4의 분산 추론은 텐서 병렬 (tensor parallelism)이 아니라 레이어 파이프라인 (layer pipeline)입니다.
- 각 머신은 연속된 레이어 슬라이스 (layer slice)를 가짐
- 워커는 자신의 슬라이스에 대한 KV 캐시를 유지함
- 활성화 (activation)는 TCP 프레임으로 다음 홉 (hop)으로 흘려보냄
- prefill은 청크 파이프라인 (chunk pipeline)을 통해 빨라짐
- 생성은 자기회귀 (autoregressive) 방식이므로 빨라지지 않으며, 네트워크 레이턴시만큼 느려짐
- 접두사 해시 (prefix hash)로 워커 KV의 정합성을 검증함
- 스냅샷은 토폴로지 비의존적인
DSV4페이로드로 저장됨
이 설계는 MacBook을 여러 대 연결하여 "1대에는 올라가지 않는 양자화 모델을 구동하기" 위한 구현으로서 상당히 현실적입니다. 특히 4bit Flash를 128GB급의 여러 머신에서 사용하는 용도에는 README의 수치로부터 그 의미가 드러납니다.
다음 회차에서는 HTTP 서버를 거치지 않고 엔진을 직접 조작하는 네이티브 코딩 에이전트를 읽습니다. 여기서는 세션 자체가 온디스크 (on-disk) KV 캐시가 되며, DSML과 JSON의 변환 경계도 더욱 얇아집니다.
본 기사는 Quick Iterate 주식회사 (Quick Iterate Co., Ltd.)의 로컬 LLM 연구의 일환으로서,
공개 리포지토리(repository) antirez/ds4의 코드를 분석한 것입니다. 행 번호, 상수, 벤치마크 값은 열람 커밋(commit) ba00a8a
(2026-05-30) / README 취득일 2026-06-01 시점의 것입니다. ds4-agent는 alpha, 엔진 본체는 beta 품질로 활발하게 변화하므로, 인용된 부분은 각자 최신 README / 소스(source)를 참조하여 다시 확인하시기 바랍니다.
Quick Iterate 주식회사
IoT / 전력 감시 / AI / 위성·무선 통신 / 시스템 통합 (System Integration)/
로컬 LLM·에이전트 기반에 관한 문의는 언제든 편하게 해주시기 바랍니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기