Purpose: find where our math diverges from llama.cpp, which produces coherent
output from the identical GGUF. Every claim below is either a code citation or a
measured value from a 5-token prompt ("The capital of France is",
ids [785, 6722, 315, 9621, 374]), greedy.
Reference dump: llama-eval-callback (CPU, -ngl 0) — 4689 tensor dumps.
Ours: QF_DUMP_LAYERS=1 probes in deepseek_v2.py / mla.py.
Config (text_config): 78 layers, hidden 6144, 64 heads, q_lora_rank 2048,
kv_lora_rank 512, qk_nope_head_dim 192, qk_rope_head_dim 64,
v_head_dim 256, intermediate_size 12288, moe_intermediate_size 2048,
256 experts, 8 per token, 1 shared, first_k_dense_replace 3,
routed_scaling_factor 2.5, norm_topk_prob true, scoring_func sigmoid,
n_group 1, topk_group 1, index_topk 2048, index_topk_freq 4,
index_skip_topk_offset 3, indexer_rope_interleave true.
| side | file | role |
|---|---|---|
| llama.cpp | src/models/glm-dsa.cpp |
graph builder for this arch |
| llama.cpp | src/llama-graph.cpp |
build_norm, build_ffn, build_moe_ffn, build_attn |
| llama.cpp | src/llama-model.cpp:2490 |
rope type for LLM_ARCH_GLM_DSA |
| vLLM | vllm/model_executor/models/deepseek_v2.py |
decoder layer, MLP, MoE |
| vLLM | vllm/model_executor/layers/mla.py |
MLA projections wrapper |
| vLLM | vllm/model_executor/layers/attention/mla_attention.py |
absorption, attention |
| plugin | weights_adapter/glm_dsa.py |
GGUF→HF name map, kv_b_proj rebuild |
| plugin | quantization/fused_moe.py |
MoE kernel dispatch |
| plugin | csrc/gguf/moe.cuh, moe_hip.cuh, moe_vec_hip.cuh |
MoE kernels |
llama.cpp glm-dsa.cpp: inpL = build_inp_embd(model.tok_embd) → GET_ROWS.
vLLM: VocabParallelEmbedding → plugin _apply_gguf_embedding:
index_select(qweight, 0, ids) then ggml_dequantize.
| value | |
|---|---|
llama.cpp embd |
0.0198, 0.0044, 0.0005 |
vLLM embd |
0.0198, 0.0044, 0.0005 |
IDENTICAL. Also verified independently: plugin dequant of token_embd
(Q8_0, 154880×6144) matches a CPU gguf.quants.dequantize reference to 1.1e-5.
Tokenizer ids and vocab_size=154880 match the GGUF exactly.
llama.cpp: cur = build_norm(inpL, attn_norm, NULL, LLM_NORM_RMS, il) —
RMS norm then MUL by weight.
vLLM: self.input_layernorm(hidden_states, residual) — fused RMSNorm that
also returns the updated residual.
| value | |
|---|---|
llama.cpp attn_norm-0 |
0.0811, 0.0246, 0.0017 |
vLLM attn_norm-0 |
0.0811, 0.0247, 0.0017 |
IDENTICAL (bf16 noise in the 4th decimal).
llama.cpp (glm-dsa.cpp:246-250, 394):
qr = mul_mat(wq_a, cur) // cb("qr") ← raw
qr = build_norm(qr, attn_q_a_norm) // cb("qr") ← same label, post-norm
q = mul_mat(wq_b, qr)
Note: both pre- and post-norm tensors are labelled qr, so the first
qr-0 in the dump is the raw projection.
vLLM (mla.py:167-192): fused_qkv_a_proj(hidden_states) produces
[q_lora_rank | kv_lora_rank + qk_rope] in one matmul, then splits.
llama.cpp uses two separate matmuls. Structurally different, numerically equal:
| value | |
|---|---|
llama.cpp raw qr |
-0.0178, -0.0584, 0.0134 |
vLLM qkv_lora (q part) |
-0.0179, -0.0583, 0.0134 |
llama.cpp q-0 |
0.0067, -0.0031, 0.0074 |
vLLM q-0 (rank 0) |
0.0067, -0.0032, 0.0074 |
IDENTICAL. The fusion is a valid refactor — not a divergence.
llama.cpp (glm-dsa.cpp:409-434):
kv_cmpr_pe = mul_mat(wkv_a_mqa, cur)
kv_cmpr = view(kv_cmpr_pe, [512, n_tokens])
k_pe = view(kv_cmpr_pe, [64, 1, n_tokens], offset=512)
q_pe = rope(q_pe); k_pe = rope(k_pe)
kv_cmpr = build_norm(kv_cmpr, attn_kv_a_norm) // norm AFTER split+rope
vLLM (mla.py:189-193): same split [kv_lora_rank | qk_rope], same norm.
| value | |
|---|---|
llama.cpp kv_cmpr_pe-0 |
0.1672, -0.0005, -0.1706 |
vLLM kv_cmpr_pe-0 |
0.1670, -0.0008, -0.1709 |
llama.cpp Kcur/Vcur (post-norm) |
0.0041, -0.0000, -0.0042 |
vLLM kv_cmpr-0 (post-norm) |
0.0041, -0.0000, -0.0042 |
IDENTICAL.
llama.cpp: llama-model.cpp:2490 — LLM_ARCH_GLM_DSA → LLAMA_ROPE_TYPE_NORM.
NORM = interleaved pairs (x[2i], x[2i+1]). (NEOX = half-split.)
Indexer rope is explicitly LLAMA_ROPE_TYPE_NORM too (glm-dsa.cpp:276,306).
vLLM: deepseek_v2.py:567 and :1112 — get_rope(..., is_neox_style=False).
is_neox_style=False = interleaved.
IDENTICAL.
llama.cpp (glm-dsa.cpp:438-466):
q_nope = permute(q_nope, 0,2,1,3)
q_nope_absorbed = mul_mat(wk_b, q_nope) // wk_b used DIRECTLY
q_nope_absorbed = permute(..., 0,2,1,3)
Qcur = concat(q_nope_absorbed, q_pe, 0) // [512 | 64]
Kcur = concat(kv_cmpr, k_pe, 0) // [512 | 64]
Vcur = kv_cmpr // [512]
cur = build_attn(..., wo, wo_s, Qcur, Kcur, Vcur, wv_b, top_k, kq_scale)
build_attn (llama-graph.cpp:2455-2500):
kq = mul_mat(k, q); soft_max_ext(kq, mask, kq_scale, bias)
kqv = mul_mat(v, kq)
kqv = mul_mat(v_mla, kqv) // decompress MQA→MHA with wv_b
permute + cont_2d + wo
vLLM: reconstructs a dense kv_b_proj from attn_k_b/attn_v_b
(plugin glm_dsa.py:430-440), then mla_attention.py:971-1007 does
.T → view(kv_lora, heads, nope+v) → split → W_UK/W_UV, and runs the
aiter sparse MQA kernel.
Round-trip verified as a true inverse: adapter emits
(H*(192+256), 512); vLLM applies .T → (512, 28672) (assert at
mla_attention.py:989 passes), then a stride-compatible view(512, 64, 448),
giving [l][h][j] = W[h*448+j, l]. Correct.
Decisive measurement — the post-attention residual:
| value | |
|---|---|
llama.cpp ffn_inp-0 |
0.0185, 0.0042, 0.0042 |
vLLM ffn_inp-0 |
0.0184, 0.0042, 0.0042 |
IDENTICAL. The entire attention block — projections, rope, absorption,
sparse kernel, o_proj, and the forced-MQA path — produces the correct
residual stream. Attention is not the divergence.
(kqv_out-0 appears to differ, but llama.cpp's kqv_out is pre-wo_s:
embd + kqv_out ≠ its own ffn_inp, so the labels are not comparable.)
llama.cpp (glm-dsa.cpp:476-488):
ffn_inp = add(cur, inpSA)
cur = build_norm(ffn_inp, ffn_norm, LLM_NORM_RMS)
cur = build_ffn(cur, ffn_up, ffn_up_s, ffn_gate, ffn_gate_s,
ffn_down, ffn_down_s, NULL, LLM_FFN_SILU, LLM_FFN_PAR)
build_ffn (llama-graph.cpp:1596-1665): tmp = up(cur), cur = gate(cur),
cur = ggml_swiglu_split(cur, tmp) = silu(gate) * up, then down.
*_s scales are null for this checkpoint (verified: 0 scale tensors in GGUF).
vLLM (deepseek_v2.py:292-296): MergedColumnParallelLinear producing
[gate | up], SiluAndMul() = silu(first half) * second half, then
RowParallelLinear down + all-reduce.
| probe | llama.cpp | vLLM | |
|---|---|---|---|
ffn_norm-0 (mlp in) |
0.0461, 0.0096, 0.0110 |
0.0461, 0.0096, 0.0110 |
identical |
ffn_gate-0 |
0.0884, 0.0042, 0.0721 |
0.0884, 0.0039, 0.0713 |
identical |
ffn_up-0 |
-0.0026, 0.0249, 0.0405 |
-0.0027, 0.0248, 0.0403 |
identical |
ffn_swiglu-0 |
-0.0001, 0.0001, 0.0015 |
-0.0001, 0.0000, 0.0015 |
identical |
ffn_out-0 |
-0.0067, -0.0014, -0.0030 |
-0.0043, -0.0029, -0.0043 |
differs |
Gate/up are not swapped. down_proj input is correct, output differs.
down_proj sums 12288 terms of magnitude ~1e-4 into a ~5e-3 result — near
total cancellation. llama.cpp accumulates in f32 on CPU; we accumulate in bf16.
This difference is consistent with precision, not proven to be a bug.
TP sharding of ffn_down verified correct: plugin
params.py:239-247 narrows dim 1 of the byte tensor
(13056 bytes → 6528 = 192 Q8_0 blocks = exactly 6144 logical cols). Block-aligned.
llama.cpp (llama-graph.cpp:1845-1960):
probs = sigmoid(logits)
selection_probs = probs + exp_probs_b // bias for SELECTION only
// comment: "leave probs unbiased as it's later used to
// get expert weights"
selected = argsort_top_k(selection_probs, 8)
weights = probs[selected] // UNBIASED
if (norm_w) weights /= clamp(sum(weights), 6.1e-5, inf)
if (w_scale) weights *= w_scale // 2.5
n_expert_groups = 1, so the group-masking branch is skipped.
ours — aiter biased_grouped_topk
(csrc/kernels/topk_softmax_kernels_group.cu:365-390, 566-610):
gating[i] = 1/(1+exp(-gating[i])); // sigmoid
if (isBiased) gating[i] += bias[i]; // selection score
...
max_val -= correction_bias[max_idx]; // unbias for weight
sum += max_val; // sum over UNBIASED
topk_weights = topk_value * (routed_scaling_factor / sum);
IDENTICAL, including the subtle unbias-before-weighting.
llama.cpp (llama-graph.cpp:1962-2056):
up = mul_mat_id(up_exps, cur, selected), gate = mul_mat_id(gate_exps, ...),
ggml_swiglu_split(gate, up), down = mul_mat_id(down_exps, ...),
× weights, sum over experts. Then + ffn_shexp (shared expert), then
+ ffn_inp residual.
ours (plugin/quantization/fused_moe.py:56-110) — two kernels, chosen by
token count:
if qtypes in MMQ_QUANT_TYPES and x.shape[0] > 64: # > 64 tokens
moe_align_block_size(...); ggml_moe_a8(...) # MMQ tiled path
elif qtypes in MMVQ_QUANT_TYPES: # <= 64 tokens
ggml_moe_a8_vec(...) # MMVQ vector pathExpert stacks unbind correctly: GGUF ffn_gate_exps data is
(256, 2048, 2016) and ffn_down_exps is (256, 6144, 672) — experts
outermost, so weight.unbind() (dim 0) yields correct per-expert slices.
csrc/gguf/moe.cuh:175 and 9 sibling sites, plus 10 in moe_hip.cuh:
const int block_num_y = (tokens_post_padded) / mmq_x; // WRONGvs the correct form used everywhere else (mmq_hip.cuh:144):
const int block_num_y = (ncols_y + mmq_x - 1) / mmq_x;With MOE_X_Q2_K = 8, this drops the tail of the token range, and yields
zero blocks when tokens_post_padded < 8 — i.e. the routed experts
compute nothing at all. Mathematically we evaluate
y[t] = Σ_e w_e·E_e(x[t]) only for t ∈ [0, 8⌊N/8⌋) instead of t ∈ [0, N).
Fixed — all 20 sites now use ceiling division. The kernel already guards
overshoot (if (blockIdx.y * mmq_x > num_tokens_post_padded[0]) return;).
Scope: this path is only taken when x.shape[0] > 64. Our 5-token probe
uses the MMVQ vector path, so D1 is NOT the cause of the observed 5-token
divergence. It IS a real bug for prompts/batches over 64 tokens.
mla_attention.py:1024-1031, gated by VLLM_ROCM_USE_AITER_FP8BMM
(default True): W_UK/W_UV are re-quantized to FP8 with a single
per-tensor scale and attention runs through an FP8 BMM. llama.cpp keeps them at
full precision. On a checkpoint whose routed quant deliberately holds attention
at q8_0, this is a real precision loss.
Disabled (VLLM_ROCM_USE_AITER_FP8BMM=0). Output changed
('H' → '9') but remained wrong, so not the root cause.
Section 7. Not proven to be a bug; expected for a 12288-term cancelling sum.
- tokenizer ids, vocab size
- embedding lookup + Q8_0 dequant (vs CPU reference, 1.1e-5)
- input layernorm, kv layernorm, q_a layernorm
q_a,q_b,kv_aprojections (fused vs split — numerically equal)- RoPE convention (interleaved both sides)
- entire attention block (
ffn_inp-0matches) kv_b_projreconstruct →.T→ view → split round trip (true inverse)- dense MLP gate/up/SwiGLU
- MoE gating: sigmoid, bias-for-selection, unbias-for-weights, renorm, scale
- expert stack unbind (experts outermost)
- TP byte-space sharding of
ffn_down(block-aligned) - MMVQ vector path grid geometry (proper ceiling division)
The 5-token divergence is not yet explained. Everything upstream of the MoE
is byte-identical, and the MoE gating is identical. That leaves the MMVQ
expert GEMM numerics (ggml_moe_a8_vec → moe_vec_q<q2_K>) as the
unverified stage for short prompts.
Proposed, in order:
- Probe
ffn_moe_outandffn_shexpseparately at layer 3 against llama.cpp'sffn_moe_out-3/ffn_shexp-3. This splits routed-expert output from shared-expert output. - If routed output is wrong, dequantize one expert's
ffn_gate_expsslice and compare against a CPUgguf.quants.dequantizereference — the same check that cleared the embedding. - Re-run with a >64-token prompt to exercise the MMQ path and confirm D1's fix independently.