
【#2】ds4.c 해독하기
요약
DeepSeek V4 Flash/Pro 전용 추론 엔진인 DwarfStar(ds4)의 2bit 비대칭 양자화 기술을 분석합니다. 모델 전체가 아닌 라우티드 MoE 엑스퍼트 위주로 압축하여 품질을 유지하며 개인용 머신에서도 거대 모델 구동을 가능하게 합니다.
핵심 포인트
- 비대칭 2bit 양자화로 품질과 압축률 동시 확보
- 라우티드 MoE 엑스퍼트 위주의 선택적 압축 전략
- imatrix를 활용한 활성(Activation) 기반 중요도 통계 적용
- IQ2_XXS 및 Q2_K 레이아웃을 통한 효율적 가중치 저장
본 시리즈는 DeepSeek V4 Flash / Pro 전용 추론 엔진인 DwarfStar(ds4)의 코드를 해독하는 연재입니다.
제2회는 128GB 클래스의 개인용 머신에서 거대 MoE 모델을 구동하기 위한 핵심인 2bit 양자화 (Quantization)와 imatrix를 다룹니다. 대상 리포지토리: https://github.com/antirez/ds4
주요 참조 부분: README.md, gguf-tools/deepseek4-quantize.c, gguf-tools/quants.c, gguf-tools/imatrix/README.md, gguf-tools/mixed/README.md
연재 「DwarfStar(ds4) 읽기」 전 8회
-
제1회 왜 전용 엔진을 작성하는가
-
제2회 비대칭 2bit 양자화와 imatrix (본 기사)
-
제3회 Metal 그래프와 압축 KV
-
제4회 디스크 KV 캐시
-
제5회 서버와 DSML 툴 호출
-
제6회 TCP 파이프라인 분산 추론
-
제7회 네이티브 에이전트
-
제8회 스티어링(Steering)・MTP・평가 기반
-
DS4의 2bit GGUF는 모델 전체를 일률적으로 2bit화하는 것이 아니다. README는 라우티드 MoE 엑스퍼트(Routed MoE Expert)만을 양자화하고, 공유 엑스퍼트(Shared Expert)・어텐션 투영(Attention Projection)・라우팅(Routing) 등은 품질 유지를 위해 남겨둔다고 설명하고 있다.
-
2bit의 주역은 라우티드 엑스퍼트의
gate/up/down이다. 대표적인 q2 레시피에서는gate/up을IQ2_XXS로,down을Q2_K로 설정한다. -
Q2_K는 256 요소를 84바이트에 채워 넣는다. 16 요소마다 스케일/최솟값을 가지며 2bit 코드를 갖는 K-quant 계열 레이아웃이다. -
IQ2_XXS는 256 요소를 66바이트에 채워 넣는다. 8값 단위의 그리드 탐색, 부호 마스크, 그룹 스케일을 사용하며, 구현상 imatrix를 요구한다. -
imatrix는 '가중치 그 자체의 크기'가 아니라, DS4의 실제 추론 그래프에서 라우티드 엑스퍼트의 입력 열이 얼마나 사용되었는지를 수집한 활성(Activation)의 중요도이다. 양자화 오차를 균등하게 다루지 않고, 중요한 열을 우선하기 위한 통계이다.
README의 Model Weights 절에는 2bit 양자화에 대해 상당히 강한 표현이 있습니다.
The 2 bit quantizations provided here are not a joke
이 주장의 전제는 "모든 것을 2bit로 만든다"가 아닙니다. DS4의 2bit는 매우 비대칭적입니다. README는 2bit 양자화에 대해 다음과 같이 설명하고 있습니다.
- 라우티드 MoE 엑스퍼트만을 주로 양자화한다
up/gate는IQ2_XXSdown은Q2_K- 라우티드 엑스퍼트는 모델 사이즈의 대부분을 차지한다
- 공유 엑스퍼트, 투영, 라우팅 등은 품질 확보를 위해 남겨둔다
MoE 모델에서는 전체 파라미터 중 추론 시 매 토큰마다 사용되는 부분과, 루터(Router)에 의해 선택된 엑스퍼트만이 사용하는 부분이 나뉩니다. DeepSeek V4 Flash의 형태는 ds4.c에서 고정값으로 정의되어 있으며, Flash는 43개 층, 각 층 256개의 라우티드 엑스퍼트, 각 토큰당 6개의 엑스퍼트를 사용합니다.
즉, 파일 사이즈의 대부분을 차지하지만 매번 전부가 발화(Fire)하는 것은 아닌 라우티드 엑스퍼트를 강력하게 압축하고, 어텐션이나 공유 엑스퍼트 등 품질에 큰 영향을 미치는 부분은 보다 보수적으로 다루는 설계입니다.
얼마나 '대부분'인지 개략적으로 계산해 보겠습니다. 각 라우티드 엑스퍼트는 gate/up/down의 3개 행렬로 4096 × 2048 × 3 ≈ 25.2M 파라미터를 가집니다. 이것이 256 엑스퍼트 × 43개 층이므로 25.2M × 256 × 43 ≈ 277B가 됩니다. Flash의 총 파라미터 284B급 중, **라우티드 엑스퍼트만으로 약 97%**를 차지하는 계산이 나옵니다. 나머지 3%(어텐션・공유 엑스퍼트・라우팅・임베딩・출력 헤드)는 품질을 위해 보수적으로 남겨둡니다.
파라미터 규모는 **DeepSeek 공식 모델 카드 (HuggingFace, 2026-06-01 취득 시점)**에 기반합니다. 공식 측은 Flash를 284B / 액티브(Active) 13B, Pro를 1.6T / 액티브(Active) 49B, 컨텍스트(Context) 1M, FP4+FP8 Mixed라고 기재하고 있습니다. 모델 카드는 업데이트될 수 있으므로, 인용 시에는 취득일을 병기해야 나중에 차이를 추적할 수 있습니다. 97%는 위의 개산치이며, 레시피에 따라 소수점 자리는 전후합니다.
이러한 절충안이 「284B 급의 MoE를 96/128GB 머신에 올리기」 위한 현실적인 포인트입니다.
양자화(Quantization) 도구의 중심은 gguf-tools/deepseek4-quantize.c입니다.
이는 HF safetensors로부터 DS4 전용 GGUF를 만들기 위한 C 구현체이며, GGML에는 링크되어 있지 않습니다.
양자화 대상의 배분은 policy_type()이 담당합니다. 중요한 점은 텐서(Tensor) 이름을 보고 라우티드 엑스퍼트(Routed Expert)인지 여부, 나아가 gate/down/up 중 어느 것인지를 판정하고 있다는 점입니다.
typedef struct {
ds4q_type routed_w1, routed_w2, routed_w3;
ds4q_type attention_proj, attention, shared, embedding, output, dense;
...
}
parse_expert_tensor()는 ffn_gate_exps, ffn_down_exps, ffn_up_exps와 같은 GGUF 텐서 이름을 해석하여 EXP_W1, EXP_W2, EXP_W3로 분류합니다. policy_type()은 이 분류에 따라 CLI에서 지정된 --routed-w1, --routed-w2, --routed-w3의 타입을 반환합니다.
--routed-w1 TYPE routed gate expert tensor type
--routed-w2 TYPE routed down expert tensor type
--routed-w3 TYPE routed up expert tensor type
...
gguf-tools/README.md의 예시에서는 q2 라우티드 엑스퍼트의 대표 설정으로서 다음과 같은 오버라이드(Override)가 제시되어 있습니다.
--experts iq2_xxs
--routed-w2 q2_k
--attention-proj q8_0
...
여기서 --experts iq2_xxs는 라우티드 엑스퍼트 전체의 기본값을 IQ2_XXS로 설정하고, --routed-w2 q2_k를 통해 down 투영(Projection)만 Q2_K로 되돌리는 형태입니다. DeepSeek 계열의 FFN 명명 규칙에서는 w1 = gate, w2 = down, w3 = up이므로, README의 「up/gate at IQ2_XXS, down at Q2_K」와 대응합니다 (코드 내의 분류 명칭은 EXP_W1/W2/W3이므로, policy_type()을 읽을 때는 「W1=gate, W2=down, W3=up」의 대응 관계를 혼동하지 않도록 주의해야 합니다).
policy_type()의 판정 우선순위를 도식화하면 다음과 같습니다.
양자화는 단순한 「압축률」의 문제가 아니라, 어떤 텐서를 어떤 정밀도(Precision)로 낮출 것인가 하는 정책(Policy)의 문제입니다. DS4는 이 정책을 코드로 명시하여 텐서 이름 기반으로 재현 가능하게 만들었습니다.
gguf-tools/quants.c의 타입 특성(Type trait)을 보면, DS4가 실제로 다루는 양자화 타입과 블록 크기(Block size)가 정리되어 있습니다.
#define QK_K 256
[DS4Q_TYPE_Q2_K] = { "q2_K", QK_K, 84, true, false },
[DS4Q_TYPE_Q4_K] = { "q4_K", QK_K, 144, true, false },
...
Q2_K는 256개 요소를 84바이트로 만드는 형식입니다. 생(Raw) f16이라면 256개 요소에 512바이트이므로, 단순 바이트 비율로는 약 6.1배 작아집니다.
ds4q_write_q2_k_block_ref()의 레이아웃은 다음과 같습니다.
enum { scales_off = 0, qs_off = 16, d_off = 80, dmin_off = 82 };
의미는 다음과 같은 구성입니다.
| 영역 | 바이트 | 역할 |
|---|---|---|
scales | 16 | 16개 요소 서브블록(sub-block)마다의 스케일(scale)/최솟값 니블(nibble) |
qs | 64 | 256개의 2bit 코드 |
d | 2 | 스케일의 f16 기준값 |
dmin | 2 | 최솟값의 f16 기준값 |
| 합계 | 84 | 256개 요소 블록 |
양자화 코드는 16개 요소마다 스케일과 최솟값을 추정하고, 값을 0..3 사이의 2bit 코드로 반올림합니다. 그 후, 4개의 2bit 코드를 1바이트에 패킹(pack)합니다.
qs_out[j / 4 + l] = L[j + l] |
(L[j + l + 32] << 2) |
(L[j + l + 64] << 4) |
...
Q2_K는 requires_imatrix = false이지만, imatrix가 있다면 가중치 버전인 ds4q_write_q2_k_block_weighted()가 사용됩니다. 가중치 버전에서는 열(column)별 중요도를 양자화 오차의 가중치로 사용하여, 스케일과 코드의 선택 방식을 변경합니다.
if (quant_weights) {
ds4q_write_q2_k_block_weighted(x, block, quant_weights + b * QK_K);
} else {
...
}
이 부분이 imatrix가 효과를 발휘하는 지점입니다. 타입은 동일한 Q2_K일지라도, 어떤 오차를 더 중요하게 볼지가 달라집니다.
IQ2_XXS는 256개 요소를 66바이트에 담는, 더욱 공격적인 형식입니다. Q2_K보다 작은 반면, quants.c의 타입 특성에서는 requires_imatrix = true로 설정되어 있습니다.
[DS4Q_TYPE_IQ2_XXS] = { "iq2_xxs", QK_K, 66, true, true }
구현 코멘트에서는 IQ2_XXS의 구조를 다음과 같이 설명하고 있습니다.
- 256개 값의 블록을 8개의 32개 값 그룹으로 취급
- 각 32개 값 그룹은 4개의 8개 값 그리드 인덱스(grid index)와 4개의 7bit 부호 마스크(sign mask)를 가짐
- 블록 스케일은 f16
- 그룹별 4bit 스케일 니블(scale nibble)로 미세 조정
- 모든 2bit 8-tuple을 허용하는 대신, 256개의 작은 그리드를 사용
- 그리드에 없는 튜플은 최근접 그리드 리스트(nearest grid list)에서 탐색
코드상에서도 grid_size = 256, map_size = 43692, neighbour_shells = 2로 설정하여, 허용된 그리드와 근방 탐색표(neighbour search table)를 초기화합니다.
enum { grid_size = 256, map_size = 43692, neighbour_shells = 2 };
IQ2_XXS의 양자화는 ds4q_write_iq2_xxs_block()에서 수행되며, 서두에서 중요도 벡터(importance vector)의 존재를 요구합니다.
static void ds4q_write_iq2_xxs_block(
const float *x,
uint8_t *y,
...
)
이는 설계상 매우 중요합니다. IQ2_XXS는 단순히 "2bit라서 작다"는 형식이 아니라, "어느 열의 오차를 우선적으로 줄일 것인가"라는 가중치가 있어야만 성립하는 형식입니다.
gguf-tools/imatrix/README.md는 DS4의 imatrix 대상을 명확하게 설명하고 있습니다.
현재 imatrix의 대상은 라우티드 MoE(Routed MoE) 경로입니다. Flash 모델에서는 43개 층 × 256개 라우티드 전문가(routed expert), Pro 모델에서는 61개 층 × 384개 라우티드 전문가가 대상이 됩니다. 각 층에는 다음과 같은 3종류의 라우티드 전문가 텐서(routed expert tensor)가 있습니다.
blk.N.ffn_gate_exps.weight
blk.N.ffn_up_exps.weight
blk.N.ffn_down_exps.weight
수집되는 값은 다음과 같습니다.
- gate/up 텐서: FFN 정규화 후의 입력 활성화 (activation)의 제곱
- down 텐서: 라우팅 가중치 (routing weight) 적용 후의 라우팅된 SwiGLU 행의 제곱
즉, imatrix는 가중치 행렬을 보고 "이 열이 크다"라고 판단하는 것이 아닙니다. DS4의 실제 Metal prefill 그래프를 실행하여, MoE 입력으로서 실체화된 활성화를 읽고, 열마다 sum(x[column]^2)를 적산합니다.
importance[column] = sum(x[column]^2)
이 통계는 "실제 DS4 추론에서 해당 열이 얼마나 효과적으로 작용하고 있는가"에 가까운 신호입니다. 양자화 (quantization) 시에는 중요도가 높은 열일수록 오차가 무겁게 평가됩니다.
imatrix의 품질은 무엇을 교정 데이터 (calibration data)로 흘려보내느냐에 강하게 의존합니다. DS4의 데이터셋은 gguf-tools/imatrix/dataset/에 있으며, 재생성 스크립트는 다음과 같습니다.
python3 gguf-tools/imatrix/dataset/build_ds4_imatrix_dataset.py
README에 따르면, 현재 추적 중인 데이터셋은 4,682개의 렌더링된 프롬프트, 대략 2.91M 토큰입니다. 내용은 단순한 Wikipedia 파편이 아니라, DS4의 상정된 유즈케이스 (use case)에 상당히 가깝게 맞춰져 있습니다.
- 이 리포지토리 자체의 C / Metal 소스 리뷰
- 긴 문맥 (long context) 스니펫
- DSML 구문을 사용한 에이전트 / 도구 호출 (tool call) 프롬프트
- 재작성, 요약, 추출, 번역
ds4-eval의 GPQA Diamond, SuperGPQA, AIME2025 프롬프트 - thinking / non-thinking 어시스턴트 접두사 (prefix)
이 구성은 DwarfStar의 설계 사상 그 자체와 일치합니다. DS4는 "로컬에서 코딩 에이전트로 사용할 수 있는가"를 중시하기 때문에, 양자화 교정도 그 용도에 맞추고 있습니다.
단순히 퍼플렉시티 (perplexity)를 낮추기 위한 일반 코퍼스 (corpus)가 아니라, "DS4가 실제로 해결하고자 하는 종류의 작업"을 사용하여 MoE 활성화의 중요도를 측정하고 있다는 점이 흥미로운 부분입니다.
deepseek4-quantize.c는 llama.cpp의 기존 바이너리 .dat imatrix 형식을 읽습니다. 다만 DS4에서는 라우팅된 전문가 (routed expert)가 대량으로 존재하기 때문에, 엔트리(entry)를 보유하는 방식이 DS4 고유입니다.
gguf-tools/imatrix/README.md는 다음과 같이 설명하고 있습니다.
entry length = n_expert * n_columns
텐서 1개당 전문가 벡터 1개가 아니라, 1개의 라우팅된 전문가 텐서 엔트리에 모든 전문가 분량의 중요도 벡터를 연결하여 넣습니다. Flash라면 256 전문가, Pro라면 384 전문가입니다.
deepseek4-quantize.c의 imatrix_find()는 전문가 ID가 지정된 경우, 해당 전문가의 세그먼트를 잘라내어 반환합니다.
if (expert_id >= 0 && n_experts > 0 &&
e->n_values == ncols * n_experts) {
return e->values + expert_id * ncols;
...
이 때문에 파일 형식은 llama.cpp의 기존 imatrix와 호환되더라도, 의미 부여는 DS4의 텐서 명명 규칙과 전문가 패킹 (expert packing)을 이해한 양자화 도구가 아니면 올바르게 사용할 수 없습니다.
IQ2_XXS는 중요도 벡터를 요구합니다. 하지만 초기 2bit GGUF나 테스트 용도에서는 외부 imatrix가 없는 경우도 있습니다. 그 경우 deepseek4-quantize.c는 합성 폴백 (synthetic fallback)을 만듭니다.
gguf-tools/README.md는 다음 식으로 설명합니다.
importance[column] = sum(row[column]^2) over all rows
이것은 "실행 시의 활성화"가 아니라 "가중치의 열 에너지"입니다. 안정적인 열 가중치는 되겠지만, DS4의 실제 추론에서 해당 열이 얼마나 사용되는지를 측정한 것은 아닙니다.
README가 imatrix 버전을 권장하는 이유가 바로 여기에 있습니다. 폴백(Fallback) 방식도 작동은 하지만, 실제 활성(Activation)을 통해 측정한 imatrix가 DS4의 용도에 더 가깝습니다.
양자화(Quantization)의 좋고 나쁨은 최종적으로 모델 출력(Model Output)을 통해 확인할 필요가 있습니다. DS4에는 gguf-tools/quality-testing/이 있으며, 공식 DeepSeek API로부터 지속 생성(Continuous Generation) 데이터를 수집하여, 로컬 GGUF의 정답 토큰에 대한 음의 로그 가능도(NLL, Negative Log-Likelihood)를 측정하는 흐름이 준비되어 있습니다.
python3 gguf-tools/quality-testing/collect_official.py
make -C gguf-tools quality-score
gguf-tools/quality-testing/score_official MODEL.gguf \
...
gguf-tools/imatrix/README.md에는 Q4 imatrix 파일의 평가 결과가 실려 있습니다.
old Q4 avg NLL: 0.177357819
Q4 imatrix avg NLL: 0.173895148
relative NLL change: -1.95%
...
개선 폭만 보면 작게 보일 수도 있습니다. 하지만 거대 모델의 저비트 양자화에서는 "같은 타입, 같은 사이즈에서 NLL이 일관되게 조금씩 낮아진다"는 사실 자체가 가치입니다. 특히 DS4와 같이 코딩 에이전트, 긴 문맥(Long Context), 도구 호출(Tool Calling)을 목표로 하는 경우, 단발성 벤치마크보다 성능이 무너지지 않는 견고함이 중요해집니다.
DS4에는 더욱 실용적인 중간 해답이 있습니다. gguf-tools/mixed/의 스플라이서(Splicer)는 두 개의 호환 가능한 GGUF를 읽어 들여, 선택한 라우티드 엑스퍼트 텐서(Routed Expert Tensor)만을 도너(Donor) GGUF로부터 복사합니다.
중요한 점은 재양자화(Re-quantization)를 하지 않는다는 것입니다. 텐서의 페이로드(Payload)를 바이트 단위로 교체할 뿐입니다.
gguf-tools/mixed/README.md에 따르면, 지금까지의 가장 뛰어난 혼합 Flash 실험은 고정 imatrix를 사용한 2bit GGUF를 베이스로 하고, 마지막 6개 층인 37-42의 라우티드 엑스퍼트만을 고정 imatrix의 Q4 GGUF에서 가져오는 구성입니다.
python3 gguf-tools/mixed/splice_mixed_expert_layers_gguf.py \
--base /path/to/DeepSeek-V4-Flash-IQ2XXS-w2Q2K-...-imatrix-fixed.gguf \
--donor /path/to/DeepSeek-V4-Flash-Q4KExperts-...-imatrix-fixed.gguf \
...
README의 기술에 따르면, 이 혼합 파일은 약 91GB이며, 풀 Q4(Full Q4)는 약 153GB입니다. 출력 일치 체크 결과, 순수 2bit 모델보다 풀 Q4에 가까운 동작을 보였다고 합니다.
이는 "양자화는 단일한 덩어리가 아니다"라는 좋은 사례입니다. 마지막 층일수록 출력 분포에 미치는 영향이 크다면, 그 부분만 고정밀도로 되돌림으로써 사이즈 증가를 제한하면서도 품질을 끌어올릴 가능성이 있습니다.
DwarfStar의 양자화에서 흥미로운 점은 단순히 "2bit로 동작했다"는 이야기가 아닙니다.
- 모델 구조를 이해하여, 사이즈의 대부분을 차지하는 라우티드 MoE 엑스퍼트(Routed MoE Expert)에 타겟을 집중함
gate/up/down에서 타입을 변경함IQ2_XXS와Q2_K의 블록 형식을 로컬 C 구현으로 보유함- 실제 DS4 추론 그래프로부터 활성(Activation)의 중요도를 수집함
- 공식 지속 생성에 대한 NLL로 검증함
- 필요하다면 마지막 층만 Q4로 되돌리는 혼합 GGUF를 생성함
이 일련의 흐름은 DwarfStar가 단순히 "추론 엔진"뿐만 아니라 "그 엔진 전용 GGUF를 어떻게 만들 것인가"까지 포함하여 설계되었음을 보여줍니다.
다음 회차에서는 엔진의 심장부인 Metal 그래프와 압축 KV 구현을 읽어보겠습니다. 제1회에서 개관했던 raw SWA, compressor, indexer, Hyper-Connection, tail-only RoPE가 실제로 어떻게 그래프와 캐시로 구현되는지 추적하겠습니다.
본 기사는 Quick Iterate의 로컬 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가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기