Skip to content

Port Deepseek v4 - #2110

Closed
SamuelOliveirads wants to merge 34 commits into
ikawrakow:mainfrom
SamuelOliveirads:feat-deepseek-v4-port
Closed

SamuelOliveirads wants to merge 34 commits into
ikawrakow:mainfrom
SamuelOliveirads:feat-deepseek-v4-port

Conversation

@SamuelOliveirads

Copy link
Copy Markdown
Collaborator

This implementation is based on the mainline PR and uses DeepSeek-V4-Flash-MXFP4 for validation.

The main reason I created a draft is because the output diverges from the mainline when FA is enabled. For testing purposes, I disabled the IQK FA using the GGML_FLASH_ATTN_EXT_IQK_DISABLED flag, which aligns the model’s output with the mainline, however, the same issue occurs when using CUDA MMA FA path, which produces finite but incorrect logits for the DSV4 512/512/512, GQA 64:1 case, and here it’s a bit more complicated to disable the current implementation for testing. Since I don’t know exactly what to adjust in the current FA implementation in the engine, @ikawrakow I’d like to know the best course of action. I can disable FA for now so it can be fixed in the future, in that case I would revert the main changes made to ggml, concat.cu and GGML_FLASH_ATTN_EXT_IQK_DISABLED path.

As for performance, it’s a work in progress, partly because I can’t achieve a complete offload to the GPU, so I haven’t run a full benchmark yet. Using sweep-bench with a humble -c 256 -n 32 I’ve observed the following performance:

  • CPU non-FA: 3.83 tok/s.
  • CPU generic FA: 4.00 tok/s.
  • CUDA hybrid non-FA: 2.59 tok/s
  • CUDA FA: 3.20 tok/s

There are other limitations I haven’t addressed yet in this PR, such as context shift, quantized KV, -sm graph, and low GPU performance, for example the routing of l_out but I believe they may come up later.

@ikawrakow

Copy link
Copy Markdown
Owner

I'll look into it.

But as a general observation, I wasn't expecting a DS4 PR will add over 3k LOC. We now have the main ingredients (indexer via the GLM-DSA PRs, HC and SWA via the openPangu PR) on the main branch, so please reuse as much of that as possible (you will probably need to extract the components from build_deepseek2.cpp and build_openpangu.cpp and move a level up so they can be reused by DS4).

@ikawrakow

Copy link
Copy Markdown
Owner

Your branch is quite a bit behind the main branch. Please rebase, fix conflicts, and check what has happened on the main branch. Do not reinvent the wheel with sparse attention, HC, sinkhorn, etc.

I have one of the antirez ds4 GGUFs. Mainline works with that, this PR crashes. Not sure if I want to download yet another 160 GB to test the PR and see what can be done about the issues you are seeing. Can you make it work with antirez GGUFs?

OK, one issue is that the GGUF I have has the MTP layer included, and this is not taken into account when building the graph. This leads to a crash in build_deepseek4. After fixing that, I run into an illegal memory access in k_bin_bcast<op_add>. I ran with -no-fa due to the issues you mentioned.

Better luck running CPU-only. Better in the sense of no crash (after fixing the MTP issue). But running llama-perplexity I get

perplexity: calculating perplexity over 140 chunks, n_ctx=2048, batch_size=2048, n_seq=1
perplexity: 24.78 seconds per pass - ETA 57.80 minutes
[1]25.9507,[2]17.3224,[3]21.6921,[4]47.4972, etc.

PPL values are about the same with or without -no-fa (but the no-fa run is quite a bit slower with 32.8 seconds per pass).

In contrast, here is what I get with mainline

.39.346.600 I system_info: n_threads = 64 (n_threads_batch = 64) / 128 | CPU : SSE3 = 1 | SSSE3 = 1 | AVX = 1 | AVX2 = 1 | F16C = 1 | FMA = 1 | BMI2 = 1 | LLAMAFILE = 1 | OPENMP = 1 | REPACK = 1 | 
0.39.346.616 I perplexity: tokenizing the input ..
0.39.981.243 I perplexity: tokenization took 634.62 ms
0.39.981.412 I perplexity: calculating perplexity over 140 chunks, n_ctx=2048, batch_size=2048, n_seq=1
1.18.973.364 I perplexity: 38.99 seconds per pass - ETA 1 hours 30.97 minutes
[1]1.7743,[2]1.6264,[3]1.9530,[4]2.3941, etc.

Oh, initially I tried to run perplexity for a context of 512 but using u_batch = 2048 (so 4 sequences). I see in the code that there is all this stuff with n_stream which is supposed to provide support for multiple sequences. But with that, it completely disintegrates:

perplexity: calculating perplexity over 561 chunks, n_ctx=512, batch_size=2048, n_seq=4
perplexity: 21.77 seconds per pass - ETA 50.87 minutes
[1]79.8919,[2]5778.6920,[3]11001.4778,[4]16637.9307, etc.

So, it looks like it simply does not work yet, and is not related to issues with IQK flash attention.

# Conflicts:
#	convert_hf_to_gguf.py
#	ggml/src/ggml.c
#	src/CMakeLists.txt
#	src/llama-build-context.cpp
#	src/llama-build-context.h
#	src/llama-hparams.cpp
#	src/llama-load-tensors.cpp
@SamuelOliveirads

Copy link
Copy Markdown
Collaborator Author

The problem with the Antirez GGUF arose mainly because I make use of some of the GLM_DSA’s logic and as a result relied on the generic management of nextn_predict_layers which is not present in the DPV4 GGUF models. I downloaded the IQ2XXS and obtained the following results:

  • Antirez IQ2XXS CPU non-FA:
    • fused MoE: 3.1934, 2.9367
    • unfused MoE: 3.2104, 2.9414
    • mainline: 3.1485, 2.9126
  • MXFP4 CPU non-FA: 2.9514, 2.8147

With n_seq=4, the results are:

  • branch: 3.1678, 2.9091
  • mainline: 3.1697, 2.9091

As for CUDA non-FA:

  • CUDA non-FA: 3.1310

Note that for perplexity I used prompts/mnemonics.txt and compared the results here with those in the mainline.

Regarding the rebase, I was going to wait until the logic was working, but now that you sugested I did it now to reuse what had already been done for HC, SWA, Indexer, and Sinkhorn, hopefully I was able to reuse everything that made sense.

Back to FA for CPU, the perplexity is similar but the token selection differs from the mainline. This is most striking in CUDA where attn_csa-2 produces 5.289 NaNs and the output can become more incoherent quickly.

@ikawrakow

Copy link
Copy Markdown
Owner

On the latest PR version, CPU-only

./bin/llama-perplexity -m DeepSeek-V4-Flash-Q4KExperts-F16HC-F16Compressor-F16Indexer-Q8Attn-Q8Shared-Q8Out-chat-v2-imatrix.gguf \
    -f wiki.test.raw -t 64 -ub 2048 -no-fa -c 2048
perplexity: calculating perplexity over 140 chunks, n_ctx=2048, batch_size=2048, n_seq=1
Segmentation fault

This after fixing the build error due to the duplicate instantiation of llama_model_loader::get_arr<uint32_t> in llama-model-loader.cpp, and fixing the still missing handling of hparams.nextn_predict_layers in build_deepseek4()

-    for (int il = 0; il < n_layer; ++il) {
+    int n_active_layer = n_layer - hparams.nextn_predict_layers;
+
+    for (int il = 0; il < n_active_layer; ++il) {

Another try

./bin/llama-perplexity -m $same_model -f wiki.test.raw -t 64 -ub 1024 -no-fa -c 1024
perplexity: calculating perplexity over 280 chunks, n_ctx=1024, batch_size=2048, n_seq=2
perplexity: 28.43 seconds per pass - ETA 1 hours 6.33 minutes
[1]3.3205,[2]1010.3598,[3]113.9258,[4]604.1925,^C

CUDA (with --cpu-moe)

  • Similar nonsense PPL for -c 1024 -ub 1024
  • Crash with Illegal memory access for -c 2048 -ub 2048

Comment thread src/graphs/build_deepseek4.cpp Outdated
0);
}

// Keep DSV4 normalization explicit. The generic helper may fuse RMS

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this is required because? The generic llm_build_norm is used in absolutely all models with absolutely zero issues, but suddenly DS4 requires that the RMS norm is not fused?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At one point during the port, using the mainline RMS helped keep the output consistent. To be fair I ran a test with norm again and didn't encounter any problems so I imagine it was an unintended side effect during development.

Comment thread src/graphs/build_deepseek4.cpp Outdated
return cur;
}

static ggml_tensor * dsv4_build_raw_k_from_idxs(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not used, discard.

Comment thread src/graphs/build_deepseek4.cpp Outdated
dsv4_new_i64_input(ctx, &inputs.state_write_idxs, (int64_t) plan.state_write_idxs.size(), (std::string(tag) + "_state_write").c_str());
dsv4_new_i32_input(ctx, &inputs.state_write_pos, (int64_t) plan.state_write_pos.size(), (std::string(tag) + "_write_pos").c_str());
dsv4_new_mask_input(ctx, &inputs.kq_mask, std::max<int64_t>(1, plan.n_kv), n_tokens, (std::string(tag) + "_kq_mask").c_str());
if (std::strcmp(tag, "dsv4_lid") == 0) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

llama.cpp style nonsense is not tolerated in this repository. We do have ggml_hadamard, and this is what you need to use.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that's right! My bad, the plan was to test their code and then adapt it to what we have here but I ended up forgetting to get to that part.

Comment thread src/graphs/build_deepseek4.cpp Outdated
const int64_t n_indexer_head = hparams.indexer_n_head;
const int64_t n_tokens = cur->ne[1];
const int64_t n_lid = llm.lctx.dsv4.lid_plan.n_kv;
ggml_tensor * k_rot = llm.lctx.dsv4.inputs.lid.k_rot;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove

Comment thread src/graphs/build_deepseek4.cpp Outdated
hparams.dsv4_compress_rope_base, llm.freq_scale,
llm.ext_factor, dsv4_rope_attn_factor(llm.freq_scale, llm.ext_factor), llm.beta_fast, llm.beta_slow);
indexer_q = ggml_concat(ctx0, indexer_q_nope, indexer_q_pe, 0);
indexer_q = ggml_mul_mat(ctx0, k_rot, indexer_q);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace with ggml_hadamard

Comment thread src/graphs/build_deepseek4.cpp Outdated
hparams.indexer_head_size,
il,
"lid_state_compress");
if (lctx.dsv4.inputs.lid.k_rot != nullptr) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace with ggml_hadamard

Comment thread src/llama-context.h Outdated
struct ggml_tensor * state_write_idxs = nullptr;
struct ggml_tensor * state_write_pos = nullptr;
struct ggml_tensor * kq_mask = nullptr;
struct ggml_tensor * k_rot = nullptr;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discard

Comment thread src/llama-context.h Outdated
std::vector<float> csa_mask_data;
std::vector<float> hca_mask_data;
std::vector<float> lid_mask_data;
std::vector<float> lid_k_rot_data;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discard

Comment thread src/graphs/build_deepseek4.cpp Outdated
return *dst;
}

static int64_t dsv4_k_rot_size(int64_t n_embd) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discard and see how the Hadamard block size is determined in the standard ik_llama.cpp Hadamard transform usage.

Comment thread src/graphs/build_deepseek4.cpp Outdated
llm.cb(indexer_k, "lid_k_stream", il);

ggml_tensor * lid_mask = dsv4_build_raw_mask_view(ctx0,
llm.lctx.dsv4.inputs.lid.kq_mask, n_lid, n_tokens);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what way is lid.kq_mask different from the standard attention mask that we have been building forever?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first glance it's semantically similar, the only difference is in the structure of the raw attention mask since the key axis is the compressed LID cache. I reused inputs.csa.kq_mask and removed duplicate inputs in the latest commits.

Comment thread src/graphs/build_deepseek4.cpp Outdated
return dsv4_concat_named(ctx, t, row, 1, "dsv4_append_zero_row");
}

static ggml_tensor * dsv4_with_zero_dep(ggml_context * ctx, ggml_tensor * t, ggml_tensor * dep) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this supposed to do?

We sum the elements of dep, to create tensor zero that has 1 element.
We then scale the result stored in zero with 0, to obtain a tensor containing exactly one zero.
We then use ggml_add1 to add this zero to the elements of t.

So, basically, we are wasting a bunch of CPU or GPU cycles to obtain the exact same tensor as we had before? Is this the new way of making a copy of a tensor?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a comment above the function for future reference, but in short this forces compression/cache work to complete before state persistence or dependent cache reads can execute.

The graph would originally have had this dependency:

dep -> compression/cache write
t   -> state persistence or dependent cache read

Since the branches are independent, we need to create a tensor dependency:

  dep -> sum -> scale(0) -> add1(t, zero) -> state persistence/read
   \________________________________________/

The output is numerically equal to t but it now depends on dep.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, forever we were able to make sure that writing to the KV cache has completed before using it, but now suddenly we cannot?

Just put

            if (hca_dep) {
                ggml_build_forward_expand(gf, hca_dep);
            }

before

ggml_tensor * hca_persist_kv = ggml_get_rows(ctx0, hca_state_kv, lctx.dsv4.inputs.hca.state_persist_src_idxs);

discard all occurrences of dsv4_with_zero_dep, and lets see how that goes.

if (raw_mask->type != hca_mask->type) {
raw_mask = ggml_cast(ctx0, raw_mask, hca_mask->type);
}
ggml_tensor * k_all = dsv4_concat_named(ctx0, raw_k, hca_k, 2, "dsv4_raw_plus_hca_k");

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comment to the above. Why on Earth are we concatenating the regular cache/mask with the HC cache/mask?


#include "concat.cuh"

// contiguous kernels

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are changes being made to concat.cu? If the reason is that it was not the same as concat.cu in llama.cpp, that is not a valid reason to copy/paste from there. The concat ops here can serve different purposes not exercised in mainline.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the first version I wrote I was unable to run it using non-FA CUDA so the change was based on the assumption that DSV4 required a more generic F16/BF16 strided concatenation path. However, after making the most recent changes I ran new tests and the same problem did not occur so I reverted the code.

Comment thread ggml/src/ggml.c Outdated
return result;
}

struct ggml_tensor * ggml_rope_ext_back(

@ikawrakow ikawrakow Jul 15, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We absolutely do not need this function here. We have ggml_rope_back instead.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I probably copied this from the mainline port.

Reverted.

Comment thread ggml/include/ggml.h Outdated
float beta_fast,
float beta_slow);

GGML_API struct ggml_tensor * ggml_rope_ext_back(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discard this and use ggml_rope_back instead.

ggml_tensor * normed = ggml_rms_norm(ctx0, flat, hparams.f_norm_rms_eps);
normed = ggml_mul(ctx0, normed, gamma); // [S*H, T]
ggml_tensor * mixes = ggml_mul_mat(ctx0, phi, normed); // [(S+2)*S, T]
ggml_tensor * mixes = build_mhc_pre_projection(Rin, phi, gamma,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is obviously useful to try to unify DS4 and OpenPangu as there is basically the same thing being done. But after merging #2132, this change results in a ~10% performance penalty for openPangu compared to the main branch. I didn't investigate if it is the combined effect of build_mhc_pre_projection, build_mhc_weighted_sum and build_mhc_post or if it is one of those causing the performance penalty.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I could tell by comparing it with main the regression occurs in build_mhc_post. I added conditions to use ggml_sum_rows_ext via OpenPangu, while DSV4 uses the old approach and this restored performance.

@ikawrakow

Copy link
Copy Markdown
Owner

Will continue in a ~2-3 days.

Comment thread src/llama-build-context.cpp Outdated
int64_t n_stream) {
const int64_t n_tokens = x->ne[2];
ggml_tensor * out = nullptr;

@ikawrakow ikawrakow Jul 15, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function assumes that x->ne[3] == 1 and weights->ne[3] == 1. Also, n_stream is not actually necessary as argument because n_stream must be the same as x->ne[1] and weights->ne[0], else it will fail. Also, n_embd must be the same as x->ne[0], no?

Given this, the implementation can be replaced with

        auto w = ggml_reshape_4d(ctx0, weights, 1, weights->ne[0], weights->ne[1], weights->ne[2]);
        return ggml_mul_multi_add(ctx0, x, w);

If we are worried that the above conditions are not met, we can say

if (x->ne[3] == 1 && weights->ne[3] == 1 && x->ne[0] == n_embd && x->ne[1] == n_stream &&
    x->ne[1] == weights->ne[0] && x->ne[2] == weights->ne[1] && weights->ne[2] == 1) {
    auto w = ggml_reshape_4d(ctx0, weights, 1, weights->ne[0], weights->ne[1], weights->ne[2]);
    return ggml_mul_multi_add(ctx0, x, w);
}

It is not a big deal, it gives just 1-2% performance boost. But still, why wouldn't we use the ik_llama.cpp abilities to their full potential?

The ggml_mul_multi_add op was specifically added to sum up the contributions of the selected experts multiplied with the experts weights. These are not experts, but the operations being performed are exactly the same.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, I applied ggml_mul_multi_add directly and no longer materializes weights. The retained dimensions are used only to assert the shared DSV4/OpenPangu caller contract. The reshape produces [1, streams, tokens, 1], equivalent to the proposed 4D representation. I also made the ne[3] and source weight dimensions explicit so an incompatible caller fails at the helper boundary.


*comb = ggml_cont(ctx0, dsv4_view_2d(ctx0, mixes, hc*hc, nt, 2*hc));
*comb = dsv4_hc_affine(ctx0, *comb, scale_comb, base_comb);
*comb = ggml_reshape_3d(ctx0, *comb, hc, hc, nt);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace this and the next line with

*comb = ggml_sinkhorn(ctx0, *comb, hc, hparams.dsv4_hc_sinkhorn_iters, hparams.dsv4_hc_eps, false);

Then remove the build_hc_sinkhorn function.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested the idea, on the CPU non-FA IQ2XXS c2048 one-chunk control, the existing DSV4 path produced 3.3172 PPL, while the direct ggml_sinkhorn replacement produced 4.5795. An earlier bounded test also changed the selected token so I reverted the experiment. This is not an FA artifact, but I have not yet determined whether the mismatch is the caller’s flattened matrix orientation or the shared operation’s expected layout so I didn't applied the private path for now.

@SamuelOliveirads

Copy link
Copy Markdown
Collaborator Author

The latest version crashes with an assert that the mask is not f16 when attempting to run with FA on (yesterday it was working).

The more worrying thing is the Wikitext2 perplexity is systematically higher than what I get with llama.cpp: 3.9695 vs 3.9354 for context of 4096 tokens (so we actually exercise the indexer) after a full run of 70 chunks. Such a difference is way too high to be caused by numerical round-off, so something is still not quite right. Oh, it is slower than llama.cpp as well, so as a user why would I use this instead of just using mainline?

I fixed the CPU FA mask assertion in commit abf5c96.

On the Antirez IQ2XXS model at c4096, branch non-FA was 3.747916869 versus mainline 3.717200623. Disabling fused MoE improved the branch to 3.731602469, removing approximately 53% of that bounded gap. -fidx, graph reuse, and MoE chunk size were neutral, while sampled LID top-k overlap was effectively identical.

Tensor probing found a very small initial branch/mainline difference at layer-0 normalization/latent projection input which is amplified by the quantized Q/K path. The fused-MoE path is a measurable contributor, but the sampled tensors do not yet prove that its kernel is the original owner so a same-input fused-expert replay is still required.
I paused that deeper attribution to address the review so I am not claiming the PPL issue is closed.

@ikawrakow

Copy link
Copy Markdown
Owner

Things are moving in the wrong direction. I observe a massive TG performance drop (~50%) compared to when I last checked (I believe commit 3d6acd3) with the few optimizations that I proposed added on top. That's running on 2x3090+Ryzen-3995WX with --cpu-moe. You can compare to #2147, and here is what I get on my system

This PR

PP TG N_KV T_PP s S_PP t/s T_TG s S_TG t/s
2048 64 0 10.190 200.98 8.426 7.60
2048 64 2048 10.654 192.22 9.089 7.04
2048 64 4096 11.244 182.14 8.369 7.65
2048 64 6144 11.943 171.48 8.545 7.49
2048 64 8192 12.604 162.49 8.814 7.26
2048 64 10240 13.302 153.97 8.802 7.27
2048 64 12288 13.994 146.35 8.964 7.14
2048 64 14336 14.723 139.10 9.032 7.09
2048 64 16384 15.406 132.93 9.421 6.79
2048 64 18432 16.137 126.91 9.550 6.70
2048 64 20480 16.731 122.41 9.511 6.73
2048 64 22528 17.717 115.60 9.640 6.64
2048 64 24576 18.441 111.06 9.775 6.55
2048 64 26624 19.128 107.07 9.927 6.45
2048 64 28672 19.882 103.01 10.108 6.33
2048 64 30720 20.635 99.25 10.233 6.25

PR #2147

PP TG N_KV T_PP s S_PP t/s T_TG s S_TG t/s
2048 64 0 10.017 204.46 5.605 11.42
2048 64 2048 10.505 194.96 5.793 11.05
2048 64 4096 11.123 184.13 6.124 10.45
2048 64 6144 11.820 173.27 5.900 10.85
2048 64 8192 12.475 164.17 6.071 10.54
2048 64 10240 13.185 155.33 6.249 10.24
2048 64 12288 13.860 147.77 6.415 9.98
2048 64 14336 14.550 140.75 6.652 9.62
2048 64 16384 15.230 134.47 6.730 9.51
2048 64 18432 15.948 128.42 6.793 9.42
2048 64 20480 16.596 123.40 6.987 9.16
2048 64 22528 17.526 116.85 7.106 9.01
2048 64 24576 18.327 111.74 7.240 8.84
2048 64 26624 18.987 107.86 7.529 8.50
2048 64 28672 19.781 103.54 7.553 8.47
2048 64 30720 20.477 100.01 7.677 8.34

return dsv4_build_mask_stream_view(ctx, base, std::max<int64_t>(1, n_stream), n_tokens);
}

ggml_tensor * mask_t = ggml_cont(ctx, ggml_transpose(ctx, mask));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain what this is supposed to accomplish and why we need it?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raw K is gathered by cache-slot indices while the original mask is position-oriented. The transpose lets ggml_get_rows() apply the identical row permutation to the mask, and ggml_cont() materializes the non-contiguous transpose for correct strides. Mainline has a specific style of raw/SWA cache, where K and the mask are generated in the same bounded order, so they don't apply this there.

@cora4

cora4 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Does anyone know why AVX-512 MXFP4 CPU-only token generation is slower than llama.cpp?
Even when running other models requantized to MXFP4
I get 18.66 token/s vs 25.06 token/s
Whereas both are 20+ when running with Q8_0

@usrlocalben

usrlocalben commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

@cora4 can you give a config/invocation example?

btw there is an Edit feature, it would avoid sending 4 email notices that are sent from new/delete pairs.

@ikawrakow

Copy link
Copy Markdown
Owner

Please do not misuse this PR for conversations about totally unrelated stuff.

@cora4

cora4 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

it would avoid sending 4 email notices

Apologies

Please do not misuse this PR for conversations about totally unrelated stuff.

It is related.
Deepseek v4 is MXFP4 format

CPU non-FA: 3.83 tok/s.
CPU generic FA: 4.00 tok/s.

@cora4

cora4 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

You can run

llama-quantize --imatrix imatrix.dat --allow-requantize --token-embedding-type q8_0 --attn-k-type mxfp4 --attn-v-type mxfp4 --custom-q ffn_down_exps=mxfp4 --ffn-down-type mxfp4 model-name-Q8_0.gguf model-name-MXFP4.gguf mxfp4

I have also tested it with smaller models

@ikawrakow

Copy link
Copy Markdown
Owner

This PR is very much WIP. You can try #2147 instead.

I don't have an MXP4 version of DS4, but for GPT-OSS-20B-MXFP4 on my Ryzen-7950X CPU I observe the following

llama.cpp

PP TG N_KV T_PP s S_PP t/s T_TG s S_TG t/s
2048 64 0 12.307 166.41 2.921 21.91
2048 64 2048 14.152 144.72 3.145 20.35
2048 64 4096 15.266 134.15 3.342 19.15
2048 64 6144 17.014 120.37 3.462 18.49
2048 64 8192 17.447 117.38 3.578 17.89
2048 64 10240 19.396 105.59 3.709 17.25
2048 64 12288 20.384 100.47 3.838 16.67
2048 64 14336 21.593 94.85 3.988 16.05
2048 64 16384 22.957 89.21 4.097 15.62
2048 64 18432 24.226 84.54 4.250 15.06
2048 64 20480 27.679 73.99 4.453 14.37
2048 64 22528 28.584 71.65 4.553 14.06
2048 64 24576 30.877 66.33 4.710 13.59
2048 64 26624 32.816 62.41 4.933 12.97
2048 64 28672 35.277 58.05 5.027 12.73
2048 64 30720 37.420 54.73 5.181 12.35

ik_llama.cpp

PP TG N_KV T_PP s S_PP t/s T_TG s S_TG t/s
2048 64 0 4.314 474.70 2.975 21.51
2048 64 2048 4.893 418.57 3.085 20.75
2048 64 4096 5.389 380.04 3.103 20.62
2048 64 6144 5.952 344.06 3.168 20.20
2048 64 8192 6.652 307.86 3.239 19.76
2048 64 10240 7.270 281.71 3.463 18.48
2048 64 12288 7.611 269.09 3.482 18.38
2048 64 14336 8.308 246.51 3.460 18.50
2048 64 16384 8.781 233.22 3.555 18.00
2048 64 18432 9.407 217.72 3.570 17.93
2048 64 20480 10.106 202.65 3.651 17.53
2048 64 22528 10.785 189.89 3.761 17.02
2048 64 24576 11.461 178.69 3.740 17.11
2048 64 26624 12.168 168.31 3.936 16.26
2048 64 28672 12.656 161.82 3.873 16.52
2048 64 30720 13.375 153.12 3.981 16.08

@ikawrakow

Copy link
Copy Markdown
Owner

@cora4 Your results are with this PR or with #2147 ?

@cora4

cora4 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

No improvement with #2147's fb26910
Moved to issue #2155


Part of the speed difference is because it is repacked as block_mxfp4x8 in llama.cpp

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants