Support for DeepseekV32ForCausalLM with DeepSeek Sparse Attention (DSA)#21149
Support for DeepseekV32ForCausalLM with DeepSeek Sparse Attention (DSA)#21149fairydreaming wants to merge 80 commits into
Conversation
…e attention). Needs manual change of add_bos_token to true in tokenizer_config.json before conversion.
…I think it's best not to quantize them.
…er implementation
…indexer implementation since the former fails for large tensors even when using CCCL.
… of llama_kv_cache and new llama_ik_cache (lightning indexer key cache). model : used new llama_kv_cache_dsa instead of modified llama_kv_cache with indexer keys in DeepseekV32ForCausalLM model : removed non-MLA path in DeepseekV32ForCausalLM
…lar to torch scatter_ operation.
…e can get rid of ggml_cast() calls in sparse attention implementation
…rm implementations
…orCausalLM-based models.
…lash attention mma kernel.
…it does not improve the performance
…() to restrict cases where fattn-mma-f16 top_k optimization may be used.
| if name.startswith("language_model."): | ||
| name = name.replace("language_model.", "") | ||
|
|
||
| # rename e_score_correction_bias tensors | ||
| if name.endswith("e_score_correction_bias"): | ||
| name = name.replace("e_score_correction_bias", "e_score_correction.bias") | ||
|
|
||
| # skip Multi-Token Prediction (MTP) layers |
There was a problem hiding this comment.
| if name.startswith("language_model."): | |
| name = name.replace("language_model.", "") | |
| # rename e_score_correction_bias tensors | |
| if name.endswith("e_score_correction_bias"): | |
| name = name.replace("e_score_correction_bias", "e_score_correction.bias") | |
| # skip Multi-Token Prediction (MTP) layers | |
| # skip Multi-Token Prediction (MTP) layers |
No longer needed after #22597
| if (num_nextn_predict_layers := self.hparams.get("num_nextn_predict_layers")) is not None: | ||
| self.gguf_writer.add_nextn_predict_layers(num_nextn_predict_layers) |
There was a problem hiding this comment.
| if (num_nextn_predict_layers := self.hparams.get("num_nextn_predict_layers")) is not None: | |
| self.gguf_writer.add_nextn_predict_layers(num_nextn_predict_layers) | |
| if not self.skip_mtp: | |
| if (num_nextn_predict_layers := self.hparams.get("num_nextn_predict_layers")) is not None: | |
| self.gguf_writer.add_nextn_predict_layers(num_nextn_predict_layers) |
There was a problem hiding this comment.
I don't think it's a good idea, DeepSeek V3.2 model C++ code uses hparams.nextn_predict_layers to calculate number of non-mtp layers. Many other models does that, they all have in convert script:
self.block_count = self.hparams["num_hidden_layers"] + self.hparams.get("num_nextn_predict_layers", 0)
so total number of layers includes MTP layers regardless of skip_mtp value. Then in C++ code:
int effective_n_layers = hparams.n_layer - hparams.nextn_predict_layers;
or similar.
Why DeepSeek V3.2 code shall behave differently?
There was a problem hiding this comment.
Because skip_mtp strips those layers?
There was a problem hiding this comment.
Point is you are mis-reporting the number of layers included in the GGUF.
| self.block_count = self.hparams["num_hidden_layers"] + self.hparams.get("num_nextn_predict_layers", 0) | ||
| self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count) |
There was a problem hiding this comment.
| self.block_count = self.hparams["num_hidden_layers"] + self.hparams.get("num_nextn_predict_layers", 0) | |
| self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count) | |
| if not self.skip_mtp: | |
| self.block_count = self.hparams["num_hidden_layers"] + self.hparams.get("num_nextn_predict_layers", 0) | |
| self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count) |
Sorry, I added this earlier, then second-guessed it because of the additional check you had, but I think that check should go instead.
|
Forgive my ignorance. Does a model need to be re-quantized to use this PR? ie GLM 5.1, which makes use of DSA. |
@whoisjeremylam As far as I know existing GLM 5.0/5.1 GGUFs already contain weights for indexer tensors, so it's only a matter of using them in glm-dsa.cpp implementation. I think there will be no need to requantize GGUFs to run them with DSA. However, lightning indexer output may be sensitive to quantization of indexer tensors and for this reason it may be necessary to change their quantization level in the future. |
DeepSeek V3.2 / V4-Flash use a sparse-attention 'lightning indexer' that scores compressed K vectors against per-head Q vectors via a fused mul_mat -> relu -> weighted-sum-over-heads pipeline. The graph emitted by build_attn_v4 today materializes that sequence as four discrete ggml ops (mul_mat, relu, mul, sum_rows), which costs multiple kernel launches per layer per token at decode and an intermediate [n_comp, n_heads, n_batch] score tensor that scales linearly with both context length and ubatch. This commit imports the WMMA + vector CUDA kernel originally written by Stanislaw Szymczyk for ggml-org/llama.cpp PR ggml-org#21149 (V3.2 DSA), later kept available on cchuter/llama.cpp (feat/v4-port). It does not yet wire the op into src/models/deepseek4.cpp -- the V4 indexer has three distinct shape regimes (decode, collapsed-q prefill, per-query prefill) that each need their own reshape adapter -- so this commit only: * adds GGML_OP_LIGHTNING_INDEXER to the op enum and bumps GGML_OP_COUNT/static_asserts to 98 * adds the ggml_lightning_indexer constructor in ggml.c with shape and dtype guards matching fairydreaming's reference * adds the CUDA dispatcher case + supports() entry. The supports() check restricts to the V3.2/V4 indexer config (n_embd=128, n_heads=64) and to the K dtypes the kernel actually instantiates (F32/F16/BF16/Q4_0/Q4_1/Q5_0/Q5_1/Q8_0). Other shapes return false so the scheduler keeps those ops on a backend that can run them. * imports the kernel implementation (WMMA path on Ampere+ NVIDIA, vector path on everything else, including HIP/MUSA stubs). Build clean; existing smoke tests still pass since the op isn't called yet. Co-authored-by: Stanislaw Szymczyk <sszymczy@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ggerganov
left a comment
There was a problem hiding this comment.
Edit: I just realized that by forcing ncols1 to 1 there will be no problem with mixing Q vectors from different tokens in one FA kernel Q tile, so top_k optimization should work for prompt processing as well. Will test it soon!
Nice. I suppose this is a sort of a stopgap solution until we implement a more optimized large-batch top-k kernel? Or do you think it is already the right approach for the prefill?
Also, I guess it works quite well with small batches (BS <= 8), for example for parallel decoding?
| GGML_API struct ggml_tensor * ggml_lightning_indexer( | ||
| struct ggml_context * ctx, | ||
| struct ggml_tensor * q, | ||
| struct ggml_tensor * k, | ||
| struct ggml_tensor * weights, | ||
| float scale_embd, | ||
| float scale_heads); | ||
|
|
There was a problem hiding this comment.
Can this OP be used in DeepSeek v4?
@ggerganov I think it's more like a stopgap solution - best performing one currently achievable with minimal changes. I tried to optimize it further by copying topk tiles to shared memory, but it reduced the performance. I'm hardly a CUDA expert, so waiting for @JohannesGaessler opinion on this. I see that in V4 there's even more complicated approach - a dense part of attention with SWA (128 tokens) and a sparse top-k (1024) based part that uses compressed KV cache. But maybe a sparse kernel still could be used by always including the dense part in top-k indices. |
|
|
||
| // store indexer keys to KV cache | ||
| const auto * mctx_lid = inp_attn_dsa->mctx->get_lid(); | ||
| const auto & k_idxs_lid = inp_attn_dsa->get_k_idxs_lid(); |
There was a problem hiding this comment.
| const auto & k_idxs_lid = inp_attn_dsa->get_k_idxs_lid(); | |
| const auto * k_idxs_lid = inp_attn_dsa->get_k_idxs_lid(); |
|
|
||
| GGML_API void ggml_flash_attn_ext_add_top_k( | ||
| struct ggml_tensor * a, | ||
| struct ggml_tensor * top_k); |
There was a problem hiding this comment.
Semantically, is it important here that these are "Top-K" indices? If I understand correctly, this is more generic than top-k - it's just a list of any indices.
If yes, I think the name should reflect that. Instead of top_k, consider using ggml_flash_attn_ext_add_idxs().
My opinion is that you should initially only add CPU support as is clearly laid out in the contributing guidelines and add CUDA support in a follow-up PR. It is way more work for me to review the changes if they're in this PR. |
Oh, sorry. I don't want to be a burden. Closing then. |
Overview
This PR adds support for DeepseekV32ForCausalLM (DeepSeek V3.2 Exp, DeepSeek V3.2, DeepSeek V3.2 Speciale) models. It contains implementation of the lightning indexer and DeepSeek Sparse Attention (DSA) - both implemented in the simplest possible way as a proof of concept. So far only CPU and CUDA backends are supported.
Due to the way it's currently implemented it doesn't improve long context performance yet, more work is needed for this.Long context performance was improved by using sparse top_k indices in CUDA flash attention MMA kernel.Some GGUFs for testing are available here (-light models), I uploaded Q8_0/Q4_K_M quants, so you need over 700GB/400GB of RAM/VRAM to run them.
I also created a 16GB baby DeepSeek V3.2 GGUF for VRAM-deprived people. It outputs incoherent gibberish, but should be useful for testing and optimizing this implementation even with limited resources.
I really could use some help with verifying the implementation correctness. If you have large GPU cluster and can run some benchmarks to compare results with official reported benchmark results for DeepSeek V3.2 models then go for it. More details in #21183.
Fixes #16331, #20363
Additional information
Decisions I made when implementing this:
DEEPSEEK32was added (mostly a copy of existingGLM_DSAarch),for this purpose I added new GGML opreplaced by SET_ROWS with 1-element rowsGGML_OP_SCATTERthat works similar to torch scatter_ operation but is currently limited to setting tensor elements at specified indices to a given scalar value,was added as another new GGML opimplementation from llama : rotate activations for better quantization #21038 was used in lightning indexerGGML_OP_HADAMARDwith implementation borrowed from ik_llama.cpp (thx @ikawrakow),llama_kv_cache_dsaclass which aggregatesthe usualtwo instances ofllama_kv_cachethat caches MLA latent representations (same as before for DeepSeek V3) and another newllama_ik_cacheclass (basically a copy of llama_kv_cache stripped of code related to V vector) that caches lightning indexer keys,llama_kv_cache- one for caching MLA latent representations, second for caching lightning indexer keyssince there are no official jinja templates for V3.2 and V3.2 Speciale, I simply decided to ignore this problem for now. You have to explicitly set chat template for these models (using jinja template from V3.2 Exp with these models will allow you to chat but tool calls won't work correctly).PR chat: dedicated DeepSeek v3.2 parser + "official" template #21785 added DeepSeek V3.2 chat template that you can use with--chat-template-file models/templates/deepseek-ai-DeepSeek-V3.2.jinjaRequirements
Due to limitations of the current CUDAggml_top_k()implementation NVIDIA CUDA CCCL library (version >3.2) and enabling GGML_CUDA_USE_CUB during CUDA backend compilation is needed, otherwise the CUDA implementation will crash for context sizes larger than (I think) 1024 tokens. I use it with CUDA 13.2 and CCCL 13.2.27.Bug in
ggml_top_k()is now fixed, fix is merged, so it should work even on 2.[89] CUDA without CCCL.Also if you want to convert the model by yourself, set
add_bos_tokento true intokenizer_config.jsonbefore the model conversion - this is needed for DeepSeek V3.2 and DeepSeek V3.2 Speciale. The conversion script has assert that checks this.Next Steps