DeepSeek 4 MTP implementation - #2216
Conversation
|
Oh, and this is the result from mainline ggml-org/llama.cpp#25784
|
|
Do you use Q4_0 or Q8_0 for the mtp? (sorry if I didn't catch the info!) |
I used |
I ran into this exact thing with openPangu's MTP a few days ago and spent a while chasing it. It turned out not to be the state management, it was batch-shape numerics in the target forward. MTP's change of batch shape is enough to make CUDA pick different kernels and reduction orders, so the same row's logits come out slightly different. The delta is tiny, a fraction of a logprob, but it's enough to flip a close greedy tie and send the text down a different branch. What convinced me: on one MTP-loaded server, compare request-level n_max=0 against n_max=1 with cache_prompt off, then force ub=1. At ub=1 the two go bit-identical (same token IDs and selected logprobs) while drafting stays active with the same accept counts. That pins it to the physical microbatch shape, not acceptance, rollback, or sampler state. Plain no-spec output also changes between ub=2048 and ub=1 on its own, so the effect isn't MTP-specific, which matches your point that mainline shows it too. I built the pre-MTP commit as well and saw the same drift. One thing that tripped me up early: loading the NextN tensors changes the allocation layout and gives a small token-0 difference even with zero drafts. Comparing n_max=0 vs n_max=1 on the same loaded server removes that. I wound up concluding that making MTP output bit-identical to no-spec means serializing the target verify back to one token at a time, which is exactly what kills the batched-verify speedup. So it's lossless or fast, not both, unless you treat the near-tie divergence as expected finite-precision variation and just confirm it isn't changing tokens on real prompts. This is all openPangu, and DS4's CSA/HCA/LID state may be different, so the balance may not be identical. But the ub=1 test should tell you fast whether it's the same batch-shape effect or something in the state handling. |
|
My results are somehow better it seems. I'm getting 28.8 t/s for quicksort (+37%), 28.1 t/s for extract (+34%) and 27.7 t/s for story (+32%), even though acceptance rates are slightly lower than yours (72% for story, 73% for extract, 64% for story). OK, the above was from just a single run each with
Base performance is about 21 t/s on my system, so between 30% (story) and 45% (code). |
|
Concerning the change in logits: up to a point yes, differences are expected due to the non-associativity of floating point operations and the fact that batch size > 1 changes how floating points operations are performed. You need to provide more details on the difference so we can understand if it is within expected limits, or if there is a real bug. |
|
|
||
| const int64_t n_hidden = n_embd * hc; | ||
| ggml_tensor * hidden_state = nullptr; | ||
| if (lctx.cparams.mtp_op_type == MTP_OP_WARMUP || lctx.cparams.mtp_op_type == MTP_OP_UPDATE_ACCEPTED) { |
There was a problem hiding this comment.
I have been wondering about this distinction also in previous MTP implementations. Why is it needed? Isn't it so that when mtp_op_type is not MTP_OP_WARMUP or MTP_OP_UPDATE_ACCEPTED it is MTP_OP_GEN_DRAFT, in which case n_tokens = 1, so just
hidden_state = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, n_hidden, n_tokens);is 100% equivalent.
| } else { | ||
| hidden_state = ggml_new_tensor_1d(ctx0, GGML_TYPE_F32, n_hidden); | ||
| } | ||
| ggml_set_name(hidden_state, "inp_mtp_states"); |
There was a problem hiding this comment.
and not cb(hidden_state, "inp_mtp_states", -1) ?
| const int il_mtp = n_layer - hparams.nextn_predict_layers; | ||
| const auto & mtp_layer = model.layers[il_mtp]; | ||
|
|
||
| ggml_tensor * h_state = ggml_reshape_3d(ctx0, hidden_state, n_embd, hc, n_tokens); |
There was a problem hiding this comment.
I'm not directly comparing with other MTP incarnations, but this seems very familiar. One of these days we should extract this into a function and use that in all MTP graphs.
There was a problem hiding this comment.
All three reviews touch on a pattern that appears in all MTP graphs, in that case I believe it would be better to create a subsequent PR that rewrites them. I also agree that they need to be updated, since they were logic I implemented back in the MVP.
ikawrakow
left a comment
There was a problem hiding this comment.
Let's have a few more people testing this before merging.
|
Oh, please fix the merge conflict. |
# Conflicts: # gguf-py/gguf/constants.py
When I tested the implementation I used the quicksort prompt with args such as: I replayed the exact target verification batch after restoring the pre-verification checkpoint, this replay was bit-identical with |
Moved some repetitive methods from the MTP graph to helper functions following the review in PR ikawrakow#2216. To validate this, I ran smoke tests and benchmarks using llama-spec-bench. Test configuration Tasks: built-in code, extraction, and story prompts Generation: 2000 tokens, n_max=3, p_min=0.0 Sampling: seed 42, temperature 0, Jinja enabled Runtime: Flash Attention (except OpenPangu), GGML_CUDA_NO_PINNED=1, 24 threads, 48 batch threads Standard repetitions: 11 per task/build, with one warm-up and ten measured runs DSV4 repetitions: 6 per task/build, with one warm-up and five measured runs The only one I didn’t test was GLM 4 because I no longer had the model on disk. Throughput and acceptance Model Prompt Runs/build Parent decode PR decode Change Acceptance Qwen3.6 27B Code 11 65.383 63.507 -2.87% 510/624 (81.73%) Qwen3.6 27B Extraction 11 62.113 61.633 -0.77% 1404/1780 (78.88%) Qwen3.6 27B Story 11 51.866 51.316 -1.06% 1286/2133 (60.29%) Gemma4 26B-A4B Code 11 121.281 119.616 -1.37% 770/1002 (76.85%) Gemma4 26B-A4B Extraction 11 120.429 119.557 -0.72% 1207/1557 (77.52%) Gemma4 26B-A4B Story 11 87.028 87.739 +0.82% 1126/2406 (46.80%) OpenPangu Code 11 8.093 7.913 -2.23% 447/633 (70.62%) OpenPangu Extraction 11 9.354 9.249 -1.12% 1414/1749 (80.85%) OpenPangu Story 11 6.304 6.171 -2.10% 1249/2244 (55.66%) DSV4 Flash Code 6 14.114 13.799 -2.23% 198/318 (62.26%) DSV4 Flash Extraction 6 13.946 14.503 +3.99% 282/447 (63.09%) DSV4 Flash Story 6 11.232 11.293 +0.54% 459/1016 (45.18%) I used a difference greater than 2% as the regression criterion, especially since the machine was in use, two models reached this threshold, so I reversed the order of each branch to run the benchmark again: Qwen3.6: The refactor branch remained 0.45%, 0.10%, and 0.79% slower for code, extraction, and story respectively. OpenPangu: reduced refactor-first control for code and story, four measured runs per task. The refactor remained 0.27% to 0.74% slower. Regarding DSV4, I ran tests with fewer repetitions due to the total time required to complete them. This pattern repeated when I re-ran the Openpangu benchmark. Some models initially showed regression, but after performing double validation with new benchmarks, I was able to confirm that there was no regression beyond what I considered to be noise. As for GLM 5.1, I ran smoke tests. Author: SamuelOliveirads
|
I tried to run sweep bench on new ds4-flash around Q2 and with this commit it segfaults. I'm not using MTP and let it allocate pinned memory. Didn't test server yet but assume its the same thing. No error listed besides segmentation fault. I thought it was the quant, but if I back this out, files work. |
Moved some repetitive methods from the MTP graph to helper functions following the review in PR ikawrakow#2216. To validate this, I ran smoke tests and benchmarks using llama-spec-bench. Test configuration Tasks: built-in code, extraction, and story prompts Generation: 2000 tokens, n_max=3, p_min=0.0 Sampling: seed 42, temperature 0, Jinja enabled Runtime: Flash Attention (except OpenPangu), GGML_CUDA_NO_PINNED=1, 24 threads, 48 batch threads Standard repetitions: 11 per task/build, with one warm-up and ten measured runs DSV4 repetitions: 6 per task/build, with one warm-up and five measured runs The only one I didn’t test was GLM 4 because I no longer had the model on disk. Throughput and acceptance Model Prompt Runs/build Parent decode PR decode Change Acceptance Qwen3.6 27B Code 11 65.383 63.507 -2.87% 510/624 (81.73%) Qwen3.6 27B Extraction 11 62.113 61.633 -0.77% 1404/1780 (78.88%) Qwen3.6 27B Story 11 51.866 51.316 -1.06% 1286/2133 (60.29%) Gemma4 26B-A4B Code 11 121.281 119.616 -1.37% 770/1002 (76.85%) Gemma4 26B-A4B Extraction 11 120.429 119.557 -0.72% 1207/1557 (77.52%) Gemma4 26B-A4B Story 11 87.028 87.739 +0.82% 1126/2406 (46.80%) OpenPangu Code 11 8.093 7.913 -2.23% 447/633 (70.62%) OpenPangu Extraction 11 9.354 9.249 -1.12% 1414/1749 (80.85%) OpenPangu Story 11 6.304 6.171 -2.10% 1249/2244 (55.66%) DSV4 Flash Code 6 14.114 13.799 -2.23% 198/318 (62.26%) DSV4 Flash Extraction 6 13.946 14.503 +3.99% 282/447 (63.09%) DSV4 Flash Story 6 11.232 11.293 +0.54% 459/1016 (45.18%) I used a difference greater than 2% as the regression criterion, especially since the machine was in use, two models reached this threshold, so I reversed the order of each branch to run the benchmark again: Qwen3.6: The refactor branch remained 0.45%, 0.10%, and 0.79% slower for code, extraction, and story respectively. OpenPangu: reduced refactor-first control for code and story, four measured runs per task. The refactor remained 0.27% to 0.74% slower. Regarding DSV4, I ran tests with fewer repetitions due to the total time required to complete them. This pattern repeated when I re-ran the Openpangu benchmark. Some models initially showed regression, but after performing double validation with new benchmarks, I was able to confirm that there was no regression beyond what I considered to be noise. As for GLM 5.1, I ran smoke tests. Author: SamuelOliveirads
Moved some repetitive methods from the MTP graph to helper functions following the review in PR ikawrakow#2216. To validate this, I ran smoke tests and benchmarks using llama-spec-bench. Test configuration Tasks: built-in code, extraction, and story prompts Generation: 2000 tokens, n_max=3, p_min=0.0 Sampling: seed 42, temperature 0, Jinja enabled Runtime: Flash Attention (except OpenPangu), GGML_CUDA_NO_PINNED=1, 24 threads, 48 batch threads Standard repetitions: 11 per task/build, with one warm-up and ten measured runs DSV4 repetitions: 6 per task/build, with one warm-up and five measured runs The only one I didn’t test was GLM 4 because I no longer had the model on disk. Throughput and acceptance Model Prompt Runs/build Parent decode PR decode Change Acceptance Qwen3.6 27B Code 11 65.383 63.507 -2.87% 510/624 (81.73%) Qwen3.6 27B Extraction 11 62.113 61.633 -0.77% 1404/1780 (78.88%) Qwen3.6 27B Story 11 51.866 51.316 -1.06% 1286/2133 (60.29%) Gemma4 26B-A4B Code 11 121.281 119.616 -1.37% 770/1002 (76.85%) Gemma4 26B-A4B Extraction 11 120.429 119.557 -0.72% 1207/1557 (77.52%) Gemma4 26B-A4B Story 11 87.028 87.739 +0.82% 1126/2406 (46.80%) OpenPangu Code 11 8.093 7.913 -2.23% 447/633 (70.62%) OpenPangu Extraction 11 9.354 9.249 -1.12% 1414/1749 (80.85%) OpenPangu Story 11 6.304 6.171 -2.10% 1249/2244 (55.66%) DSV4 Flash Code 6 14.114 13.799 -2.23% 198/318 (62.26%) DSV4 Flash Extraction 6 13.946 14.503 +3.99% 282/447 (63.09%) DSV4 Flash Story 6 11.232 11.293 +0.54% 459/1016 (45.18%) I used a difference greater than 2% as the regression criterion, especially since the machine was in use, two models reached this threshold, so I reversed the order of each branch to run the benchmark again: Qwen3.6: The refactor branch remained 0.45%, 0.10%, and 0.79% slower for code, extraction, and story respectively. OpenPangu: reduced refactor-first control for code and story, four measured runs per task. The refactor remained 0.27% to 0.74% slower. Regarding DSV4, I ran tests with fewer repetitions due to the total time required to complete them. This pattern repeated when I re-ran the Openpangu benchmark. Some models initially showed regression, but after performing double validation with new benchmarks, I was able to confirm that there was no regression beyond what I considered to be noise. As for GLM 5.1, I ran smoke tests. Author: SamuelOliveirads
|
I've extracted the MTP head from the original DSv4F, and converted it with ik's converter: https://huggingface.co/philpax/DeepSeek-V4-Flash-MTP-Only-GGUF I can confirm that it is ~compatible with a 0731 quant when run with a recent ik containing this PR, and that it results in a marginal speedup on my hybrid configuration. |
Implementation of MTP for DSV4: For some reason, they removed the MTP layer from the original model again which meant that several published GGUFs would need to be reconverted. To avoid this, I used the GGUF that preserves only the MTP layer, similar to how Gemma 4 works.
Here are the benchmark results:
Arguments used:
Some notes:
convert_hf_to_gguf.py --mtpflag to create a model with only the MTP layer.n_max <= 7to take advantage of the more optimized workflow forspec-ckpt-mode per-step, otherwise you’ll get a fallback warning for the other, slower methods.