From ce2325aa561c0b575f21f2a55b1622f6338284ae Mon Sep 17 00:00:00 2001 From: dusterbloom <32869278+dusterbloom@users.noreply.github.com> Date: Tue, 30 Jun 2026 21:57:05 +0200 Subject: [PATCH 1/9] perf(qwen35): use in-place GDN state write in pure AR --- server/src/qwen35/qwen35_target_graph.cpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/server/src/qwen35/qwen35_target_graph.cpp b/server/src/qwen35/qwen35_target_graph.cpp index ea4684e09..df132c9da 100644 --- a/server/src/qwen35/qwen35_target_graph.cpp +++ b/server/src/qwen35/qwen35_target_graph.cpp @@ -918,6 +918,7 @@ static ggml_tensor * build_delta_net_block( ggml_tensor * output = nullptr; ggml_tensor * new_state = nullptr; + const bool pure_ar = !use_chunked && parent_ids == nullptr && cap == nullptr && persist_inter == nullptr; if (use_chunked) { auto r = build_delta_net_chunked(ctx, q_c, k_c, v_c, g_tensor, beta, s); @@ -933,12 +934,12 @@ static ggml_tensor * build_delta_net_block( ? ggml_gated_delta_net_tree_persist(ctx, q_c, k_c, v_c, g_tensor, beta, s, parent_ids, persist_inter) : ggml_gated_delta_net_tree(ctx, q_c, k_c, v_c, g_tensor, beta, s, parent_ids); } else { - // Non-tree (chain/prefill). When capture is requested, set src[7] so - // the kernel writes per-token intermediates directly to the persistent - // cache buffer — same mechanism as _tree_persist, but without tree - // parent_ids. Avoids the legacy result-region cpy (and the OOB it - // could cause if the result tensor has no embedded intermediate region). - result = ggml_gated_delta_net(ctx, q_c, k_c, v_c, g_tensor, beta, s); + // Non-tree (chain/prefill). Pure AR writes the final recurrent state + // in-place; capture paths keep the regular op and attach src[7] so the + // kernel writes per-token intermediates directly to persistent cache. + result = pure_ar + ? ggml_gated_delta_net_inplace(ctx, q_c, k_c, v_c, g_tensor, beta, s) + : ggml_gated_delta_net(ctx, q_c, k_c, v_c, g_tensor, beta, s); if (persist_inter) { result->src[7] = persist_inter; } @@ -965,8 +966,11 @@ static ggml_tensor * build_delta_net_block( S_v * S_v * H_v * r_elt, S_v * H_v * n_seq_tokens * n_seqs * r_elt); - // Persist new_state back to cache - ggml_build_forward_expand(gf, ggml_cpy(ctx, new_state, ssm_state)); + // In pure AR, ggml_gated_delta_net_inplace writes the final recurrent state + // directly into ssm_state, so the explicit copy is redundant. + if (!pure_ar) { + ggml_build_forward_expand(gf, ggml_cpy(ctx, new_state, ssm_state)); + } // Expose per-step intermediate states for spec-decode rollback. The patched // ggml_gated_delta_net kernel appends an intermediate-states region to the From e1ce9c732524939f70617109092324a65bfb6827 Mon Sep 17 00:00:00 2001 From: dusterbloom <32869278+dusterbloom@users.noreply.github.com> Date: Thu, 25 Jun 2026 03:03:49 +0200 Subject: [PATCH 2/9] perf(decode): fuse silu+mul -> swiglu_split in DeltaNet gated output In build_delta_net_block, replace the two-op silu(z_4d) + mul(output, z_silu) pattern with ggml_swiglu_split(z_4d, output_n). swiglu_split(a,b)=silu(a)*b, mathematically identical. Removes 30 kernel launches per AR decode step (UNARY-30, MUL-30, GLU+30; graph 2744->2714). Token-identical at temp=0 confirmed. Part 1 of launch-count gap closure (30/162 = 18.5% of gap). --- server/src/qwen35/qwen35_target_graph.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/qwen35/qwen35_target_graph.cpp b/server/src/qwen35/qwen35_target_graph.cpp index df132c9da..c5b7457b9 100644 --- a/server/src/qwen35/qwen35_target_graph.cpp +++ b/server/src/qwen35/qwen35_target_graph.cpp @@ -1009,12 +1009,12 @@ static ggml_tensor * build_delta_net_block( ggml_build_forward_expand(gf, ggml_cpy(ctx, new_state, s)); } - // ── Gated output norm: rms_norm(output) * silu(z_4d) + // ── Gated output norm: rms_norm(output) * weight * silu(z_4d) + // fused: swiglu_split(z_4d, normed) = silu(z_4d) * normed (1 GLU kernel vs 2) ggml_tensor * z_4d = ggml_reshape_4d(ctx, z, head_v_dim, num_v_heads, n_seq_tokens, n_seqs); ggml_tensor * output_n = ggml_rms_norm(ctx, rms_norm_input_f32(ctx, output), w.rms_eps); output_n = ggml_mul(ctx, output_n, L.ssm_norm); - ggml_tensor * z_silu = ggml_silu(ctx, z_4d); - output_n = ggml_mul(ctx, output_n, z_silu); + output_n = ggml_swiglu_split(ctx, z_4d, output_n); // Reshape to [d_inner, n_tokens] ggml_tensor * flat = ggml_reshape_3d(ctx, output_n, From 96fd3ea9e7ed6d002e81e221b1d77394af87c53e Mon Sep 17 00:00:00 2001 From: dusterbloom <32869278+dusterbloom@users.noreply.github.com> Date: Sat, 27 Jun 2026 18:29:15 +0200 Subject: [PATCH 3/9] perf(qwen35): gate FWHT rotation for q4_0 KV + skip repeat_back in pure AR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FWHT gate: DFLASH_NO_WHT=1 disables the K/Q rotation that costs ~16 extra kernels/token (~3% decode). q4_0 KV doesn't need the outlier spreading on Ampere. Default off for TQ3_0 (WHT baked into quant). Repeat_back skip: in pure AR (no tree/capture), the SSM kernel's broadcast handles the head mismatch natively — skip the repeat_back copy per DeltaNet layer per step (~1% decode). --- server/src/qwen35/qwen35_target_graph.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/server/src/qwen35/qwen35_target_graph.cpp b/server/src/qwen35/qwen35_target_graph.cpp index c5b7457b9..6d2e16840 100644 --- a/server/src/qwen35/qwen35_target_graph.cpp +++ b/server/src/qwen35/qwen35_target_graph.cpp @@ -864,9 +864,14 @@ static ggml_tensor * build_delta_net_block( q_c = ggml_l2_norm(ctx, q_c, w.rms_eps); k_c = ggml_l2_norm(ctx, k_c, w.rms_eps); - // Repeat Q and K from num_k_heads to num_v_heads so they match V's layout - // (only needed if not using the fused op's broadcast support). - if (num_k_heads != num_v_heads) { + // Repeat Q and K from num_k_heads to num_v_heads so they match V's layout. + // In pure AR (cap==nullptr, parent_ids==nullptr) the kernel's fastmodulo + // broadcast handles the head mismatch natively — skip the repeat to save + // one tensor allocation and a memory-bandwidth copy per SSM layer per step. + // In tree/capture mode keep the repeat: the DFS parent-reload in the kernel + // indexes intermediate states by h_idx (v-head), so Q/K must already be + // in the expanded [num_v_heads] layout before the kernel runs. + if (num_k_heads != num_v_heads && (cap != nullptr || parent_ids != nullptr)) { q_c = ggml_repeat_4d(ctx, q_c, head_k_dim, num_v_heads, n_seq_tokens, n_seqs); k_c = ggml_repeat_4d(ctx, k_c, head_k_dim, num_v_heads, n_seq_tokens, n_seqs); } From 5fd7c3fb06ed9ae306609b027b4d9effa60107a2 Mon Sep 17 00:00:00 2001 From: dusterbloom <32869278+dusterbloom@users.noreply.github.com> Date: Sat, 27 Jun 2026 18:30:15 +0200 Subject: [PATCH 4/9] perf(qwen35): cont concat inputs for fast concat_cont path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DeltaNet conv_input concat (conv_states_r, qkv_T) hit the slow concat_f32_dim0 kernel (15us) because both inputs were non-contiguous (reshape + transpose). Wrapping in ggml_cont routes to the fast concat_cont path (3.3us) — ~0.5ms/token saved in the 30-layer serial DeltaNet recurrence. --- server/src/qwen35/qwen35_target_graph.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/qwen35/qwen35_target_graph.cpp b/server/src/qwen35/qwen35_target_graph.cpp index 6d2e16840..32060ef4c 100644 --- a/server/src/qwen35/qwen35_target_graph.cpp +++ b/server/src/qwen35/qwen35_target_graph.cpp @@ -791,7 +791,7 @@ static ggml_tensor * build_delta_net_block( // [n_tokens, conv_channels, n_seqs] to concat on dim 0. ggml_tensor * qkv_T = ggml_transpose(ctx, qkv_mixed); - ggml_tensor * conv_input = ggml_concat(ctx, conv_states_r, qkv_T, 0); + ggml_tensor * conv_input = ggml_concat(ctx, ggml_cont(ctx, conv_states_r), ggml_cont(ctx, qkv_T), 0); // conv_input: [kernel-1 + n_tokens, conv_channels, n_seqs] // For spec-decode rollback: copy the full conv_input into the persistent From a5915e29cc6ba441285d918c97e9e6a5c741e3d6 Mon Sep 17 00:00:00 2001 From: dusterbloom <32869278+dusterbloom@users.noreply.github.com> Date: Sat, 27 Jun 2026 19:01:26 +0200 Subject: [PATCH 5/9] =?UTF-8?q?perf(qwen35):=20graph=20reuse=20in=20AR=20d?= =?UTF-8?q?ecode=20=E2=80=94=20skip=20rebuild=20within=20256-token=20FA=20?= =?UTF-8?q?bucket?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build_target_step rebuilds the 2744-node cgraph every decode step, costing ~0.38ms/token and resetting ggml-cuda's CUDA-graph warmup counter. The only per-step topology change is win_len_padded = round_up(committed+1, 256), which only advances every 256 steps. Gate the rebuild on committed/256; within a bucket reuse the cached graph and update only mutable inputs. DFLASH_AR_NO_REUSE=1 restores the per-step rebuild. Measured: +6% decode tok/s (106→113) on Q3_K_XL, q4_0 KV, 16K ctx. --- server/src/qwen35/qwen35_backend.cpp | 41 ++++++++++++++++++++-------- server/src/qwen35/qwen35_backend.h | 3 ++ 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/server/src/qwen35/qwen35_backend.cpp b/server/src/qwen35/qwen35_backend.cpp index d37c1b758..89ad8db3f 100644 --- a/server/src/qwen35/qwen35_backend.cpp +++ b/server/src/qwen35/qwen35_backend.cpp @@ -1326,7 +1326,7 @@ void Qwen35Backend::kvflash_upload_mask() { const size_t need = (size_t)sg_.attn_mask->ne[0] * sg_.attn_mask->ne[1]; if (kvflash_mask_buf_.size() != need || kvflash_pager_.epoch() != kvflash_mask_epoch_) { kvflash_mask_buf_.assign(need, F16_NEG_INF); - kvflash_pager_.fill_slot_mask(kvflash_mask_buf_.data()); // q row 0 + kvflash_pager_.fill_slot_mask_cached(kvflash_mask_buf_.data()); // q row 0; pager caches kvflash_mask_epoch_ = kvflash_pager_.epoch(); } // Upload before EVERY compute: the input tensor's buffer region is @@ -1536,6 +1536,17 @@ bool Qwen35Backend::do_ar_decode(int committed, int n_gen, } // AR decode loop for remaining tokens + // + // Graph reuse: build_target_step rebuilds the ggml cgraph every step, + // which resets the ggml-cuda CUDA-graph warmup counter and prevents + // replay. The only shape that varies across steps is the FA view window + // (win_len_padded = round_up(committed+1, 256)), which only advances + // every 256 decode steps. So we rebuild only when committed/256 crosses + // a boundary; within a bucket we reuse the cached graph and just update + // the mutable input tensors. + const bool ar_graph_reuse = std::getenv("DFLASH_AR_NO_REUSE") == nullptr; + ar_decode_fa_bucket_ = -1; // force first-step build + for (int i = initial_emitted; i < n_gen; i++) { int32_t tok = out_tokens.back(); @@ -1547,17 +1558,23 @@ bool Qwen35Backend::do_ar_decode(int committed, int n_gen, // kvflash: graph carries a slot-validity mask alongside the // step-invariant set_rows write; the FA span clamps to the pool. const bool pool = kvflash_active(); - if (!build_target_step(sg_, w_, cache_, target_backend_, - /*kv_start=*/committed, /*n_tokens=*/1, - /*with_mask=*/pool, /*capture=*/false, - /*capture_delta_intermediate=*/false, - /*fa_window=*/0, - /*last_token_logits_only=*/false, - cfg_.kq_stride_pad, - should_capture_moe_router(), - /*kvflash_mask=*/pool, - /*capture_qk=*/pool && kvflash_qk_policy_)) { - return false; + + const int fa_bucket = committed >> 8; // committed / 256 + const bool need_rebuild = !ar_graph_reuse || (fa_bucket != ar_decode_fa_bucket_); + if (need_rebuild) { + if (!build_target_step(sg_, w_, cache_, target_backend_, + /*kv_start=*/committed, /*n_tokens=*/1, + /*with_mask=*/pool, /*capture=*/false, + /*capture_delta_intermediate=*/false, + /*fa_window=*/0, + /*last_token_logits_only=*/false, + cfg_.kq_stride_pad, + should_capture_moe_router(), + /*kvflash_mask=*/pool, + /*capture_qk=*/pool && kvflash_qk_policy_)) { + return false; + } + ar_decode_fa_bucket_ = fa_bucket; } // Fill kv_write_rows with this step's cache slot for set_rows: diff --git a/server/src/qwen35/qwen35_backend.h b/server/src/qwen35/qwen35_backend.h index 53e5b6915..a7e864dd2 100644 --- a/server/src/qwen35/qwen35_backend.h +++ b/server/src/qwen35/qwen35_backend.h @@ -231,6 +231,9 @@ class Qwen35Backend : public ModelBackend { StepGraph draft_sg_; // draft forward StepGraph proj_sg_; // lm-head projection (remote-lm-head mode) + int ar_decode_fa_bucket_ = -1; + static const bool ar_graph_reuse; + // ── Draft feature mirror (cross-GPU feature transfer) ──────────── DraftFeatureMirror feature_mirror_; DFlashDraftIpcClient remote_draft_; From a8253730c7834bb3fd59807faf07e2b03507c3dd Mon Sep 17 00:00:00 2001 From: dusterbloom <32869278+dusterbloom@users.noreply.github.com> Date: Sat, 27 Jun 2026 19:28:02 +0200 Subject: [PATCH 6/9] =?UTF-8?q?perf(qwen35):=20reduce=20concat=20cont=20ov?= =?UTF-8?q?erhead=20=E2=80=94=20materialize=20qkv=5FT=20once?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move ggml_cont from the concat call to the qkv_T transpose definition. This makes qkv_T contiguous once (1 cont per DeltaNet layer) instead of wrapping both concat inputs (2 conts per layer). conv_states_r is already contiguous (reshape of contiguous cache tensor). Net: -30 graph nodes (2704→2674), compute -24µs/step. --- server/src/qwen35/qwen35_target_graph.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server/src/qwen35/qwen35_target_graph.cpp b/server/src/qwen35/qwen35_target_graph.cpp index 32060ef4c..c1a4a5833 100644 --- a/server/src/qwen35/qwen35_target_graph.cpp +++ b/server/src/qwen35/qwen35_target_graph.cpp @@ -789,9 +789,12 @@ static ggml_tensor * build_delta_net_block( // qkv_mixed currently is [conv_channels, n_tokens, n_seqs]; we need // [n_tokens, conv_channels, n_seqs] to concat on dim 0. - ggml_tensor * qkv_T = ggml_transpose(ctx, qkv_mixed); + // Materialize the transpose once so both concat inputs are contiguous + // → concat_cont fast path fires (saves 1 ggml_cont per layer vs wrapping + // both inputs individually). + ggml_tensor * qkv_T = ggml_cont(ctx, ggml_transpose(ctx, qkv_mixed)); - ggml_tensor * conv_input = ggml_concat(ctx, ggml_cont(ctx, conv_states_r), ggml_cont(ctx, qkv_T), 0); + ggml_tensor * conv_input = ggml_concat(ctx, conv_states_r, qkv_T, 0); // conv_input: [kernel-1 + n_tokens, conv_channels, n_seqs] // For spec-decode rollback: copy the full conv_input into the persistent From 9d4506242ad49431ddedcfa72718af7719d104eb Mon Sep 17 00:00:00 2001 From: dusterbloom <32869278+dusterbloom@users.noreply.github.com> Date: Wed, 1 Jul 2026 02:02:05 +0200 Subject: [PATCH 7/9] perf(qwen35): recover dense AR replay fast path --- server/src/qwen35/qwen35_backend.cpp | 17 +++++++++++- server/src/qwen35/qwen35_graph_options.h | 30 +++++++++++++++++++++ server/src/qwen35/qwen35_target_graph.cpp | 9 ++++--- server/src/qwen35/scoped_skip_props_check.h | 26 ++++++++++++++++++ 4 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 server/src/qwen35/qwen35_graph_options.h create mode 100644 server/src/qwen35/scoped_skip_props_check.h diff --git a/server/src/qwen35/qwen35_backend.cpp b/server/src/qwen35/qwen35_backend.cpp index 89ad8db3f..0ee2f851d 100644 --- a/server/src/qwen35/qwen35_backend.cpp +++ b/server/src/qwen35/qwen35_backend.cpp @@ -1,5 +1,6 @@ #include "qwen35_backend.h" #include "placement/skip_park_guard.h" +#include "qwen35/scoped_skip_props_check.h" #include "qwen35_dflash_target.h" #include "graph_builders.h" #include "dflash_feature_ring.h" @@ -37,6 +38,12 @@ #include "kv_quant.h" +#if defined(DFLASH27B_BACKEND_CUDA) || defined(DFLASH27B_BACKEND_HIP) || defined(GGML_USE_HIP) +extern "C" void ggml_cuda_set_skip_props_check(bool skip); +#else +static void ggml_cuda_set_skip_props_check(bool) {} +#endif + namespace dflash::common { namespace { @@ -1326,7 +1333,7 @@ void Qwen35Backend::kvflash_upload_mask() { const size_t need = (size_t)sg_.attn_mask->ne[0] * sg_.attn_mask->ne[1]; if (kvflash_mask_buf_.size() != need || kvflash_pager_.epoch() != kvflash_mask_epoch_) { kvflash_mask_buf_.assign(need, F16_NEG_INF); - kvflash_pager_.fill_slot_mask_cached(kvflash_mask_buf_.data()); // q row 0; pager caches + kvflash_pager_.fill_slot_mask(kvflash_mask_buf_.data()); // q row 0; pager caches kvflash_mask_epoch_ = kvflash_pager_.epoch(); } // Upload before EVERY compute: the input tensor's buffer region is @@ -1547,6 +1554,9 @@ bool Qwen35Backend::do_ar_decode(int committed, int n_gen, const bool ar_graph_reuse = std::getenv("DFLASH_AR_NO_REUSE") == nullptr; ar_decode_fa_bucket_ = -1; // force first-step build + dflash::qwen35::ScopedSkipPropsCheck skip_props_guard( + &ggml_cuda_set_skip_props_check, ar_graph_reuse); + for (int i = initial_emitted; i < n_gen; i++) { int32_t tok = out_tokens.back(); @@ -1596,7 +1606,12 @@ bool Qwen35Backend::do_ar_decode(int committed, int n_gen, } if (pool) kvflash_upload_mask(); + // A graph rebuild must run the normal property check so ggml-cuda can + // reset warmup and recapture. Stable replay steps can skip the O(nodes) + // property scan after warmup. + if (need_rebuild) skip_props_guard.set(false); auto st = ggml_backend_graph_compute(target_backend_, sg_.gf); + if (need_rebuild) skip_props_guard.set(ar_graph_reuse); if (st != GGML_STATUS_SUCCESS) return false; after_target_compute(sg_, committed, 1); diff --git a/server/src/qwen35/qwen35_graph_options.h b/server/src/qwen35/qwen35_graph_options.h new file mode 100644 index 000000000..d98af6fa8 --- /dev/null +++ b/server/src/qwen35/qwen35_graph_options.h @@ -0,0 +1,30 @@ +#pragma once + +#include "ggml.h" + +#include +#include + +namespace dflash::common { + +inline bool qwen35_env_flag_is_one(const char * name) { + const char * value = std::getenv(name); + return value != nullptr && std::strcmp(value, "1") == 0; +} + +inline bool qwen35_should_use_graph_wht_k_rotation(ggml_type kv_k_type) { + if (std::getenv("DFLASH_NO_WHT") != nullptr) { + return false; + } + if (kv_k_type == GGML_TYPE_TQ3_0) { + return false; + } + if (qwen35_env_flag_is_one("DFLASH_FORCE_WHT")) { + return true; + } + // q4_0 on Ampere is already fast enough without graph-level FWHT; avoiding + // it removes extra prefill/decode kernels in the parity path. + return kv_k_type != GGML_TYPE_Q4_0; +} + +} // namespace dflash::common diff --git a/server/src/qwen35/qwen35_target_graph.cpp b/server/src/qwen35/qwen35_target_graph.cpp index c1a4a5833..d002400b8 100644 --- a/server/src/qwen35/qwen35_target_graph.cpp +++ b/server/src/qwen35/qwen35_target_graph.cpp @@ -33,6 +33,7 @@ #include "internal.h" #include "delta_net_chunked.h" #include "kv_quant.h" +#include "qwen35/qwen35_graph_options.h" #include "qwen35_ops.h" #include "qwen35moe_ffn.h" @@ -128,10 +129,10 @@ bool create_target_cache_partial(const TargetWeights & w, out.kv_k_type = kv_k_type; out.kv_v_type = kv_v_type; - // Graph-level FWHT K-rotation (TurboQuant-style outlier spreading with - // standard quant types that keep fast FA kernel paths on all arches). - // Skip for TQ3_0 K cache — that type already applies WHT during quantization. - out.kv_k_rotated = (kv_k_type != GGML_TYPE_TQ3_0); + // Graph-level FWHT K-rotation (TurboQuant-style outlier spreading). + // q4_0 on Ampere skips it by default for parity with llama.cpp's leaner + // q4_0 path; DFLASH_FORCE_WHT=1 restores the old graph-level rotation. + out.kv_k_rotated = qwen35_should_use_graph_wht_k_rotation(kv_k_type); const bool needs_256_stride = kv_k_type == GGML_TYPE_TQ3_0 || kv_v_type == GGML_TYPE_TQ3_0; diff --git a/server/src/qwen35/scoped_skip_props_check.h b/server/src/qwen35/scoped_skip_props_check.h new file mode 100644 index 000000000..d90bab023 --- /dev/null +++ b/server/src/qwen35/scoped_skip_props_check.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +namespace dflash::qwen35 { + +// Keeps ggml-cuda's thread-local graph fast path scoped to one decode request. +template +class ScopedSkipPropsCheck { +public: + ScopedSkipPropsCheck(SetFn set, bool skip) : set_(std::move(set)) { set_(skip); } + ~ScopedSkipPropsCheck() { set_(false); } + + ScopedSkipPropsCheck(const ScopedSkipPropsCheck &) = delete; + ScopedSkipPropsCheck & operator=(const ScopedSkipPropsCheck &) = delete; + + void set(bool skip) { set_(skip); } + +private: + SetFn set_; +}; + +template +ScopedSkipPropsCheck(SetFn, bool) -> ScopedSkipPropsCheck; + +} // namespace dflash::qwen35 From 42b0fd6ecac495e0e96c93157689676b87302d1e Mon Sep 17 00:00:00 2001 From: dusterbloom <32869278+dusterbloom@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:48:02 +0200 Subject: [PATCH 8/9] fix(qwen35): scope AR props-skip to target compute; gate reuse to dense; K-basis in disk-cache salt; CPU GDN inplace guard --- .../deps/llama.cpp/ggml/src/ggml-cpu/ops.cpp | 8 ++++++ server/src/qwen35/qwen35_backend.cpp | 25 +++++++++++-------- server/src/qwen35/qwen35_backend.h | 5 +++- server/src/qwen35moe/qwen35moe_backend.h | 3 +++ server/src/server/http_server.cpp | 14 ++++++++++- 5 files changed, 43 insertions(+), 12 deletions(-) diff --git a/server/deps/llama.cpp/ggml/src/ggml-cpu/ops.cpp b/server/deps/llama.cpp/ggml/src/ggml-cpu/ops.cpp index c8f811406..88d95426f 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cpu/ops.cpp +++ b/server/deps/llama.cpp/ggml/src/ggml-cpu/ops.cpp @@ -10604,6 +10604,14 @@ void ggml_compute_forward_gated_delta_net( ggml_tensor * dst) { const ggml_tensor * src0 = dst->src[0]; + // The in-place SSM state write (op_params[1]) is a CUDA-only fast path: the + // CPU kernel always writes the final state to dst+attn_score_elems and never + // reads it back, so honoring the flag here would silently freeze SSM state. + // Abort like TURBO_WHT rather than run a wrong-but-quiet CPU fallback. + if (ggml_get_op_params_i32(dst, 1) != 0) { + GGML_ABORT("gated_delta_net inplace state write is CUDA-only"); + } + switch (src0->type) { case GGML_TYPE_F32: { diff --git a/server/src/qwen35/qwen35_backend.cpp b/server/src/qwen35/qwen35_backend.cpp index 0ee2f851d..59e220381 100644 --- a/server/src/qwen35/qwen35_backend.cpp +++ b/server/src/qwen35/qwen35_backend.cpp @@ -1551,12 +1551,10 @@ bool Qwen35Backend::do_ar_decode(int committed, int n_gen, // every 256 decode steps. So we rebuild only when committed/256 crosses // a boundary; within a bucket we reuse the cached graph and just update // the mutable input tensors. - const bool ar_graph_reuse = std::getenv("DFLASH_AR_NO_REUSE") == nullptr; + const bool ar_graph_reuse = supports_ar_graph_reuse() + && std::getenv("DFLASH_AR_NO_REUSE") == nullptr; ar_decode_fa_bucket_ = -1; // force first-step build - dflash::qwen35::ScopedSkipPropsCheck skip_props_guard( - &ggml_cuda_set_skip_props_check, ar_graph_reuse); - for (int i = initial_emitted; i < n_gen; i++) { int32_t tok = out_tokens.back(); @@ -1606,12 +1604,19 @@ bool Qwen35Backend::do_ar_decode(int committed, int n_gen, } if (pool) kvflash_upload_mask(); - // A graph rebuild must run the normal property check so ggml-cuda can - // reset warmup and recapture. Stable replay steps can skip the O(nodes) - // property scan after warmup. - if (need_rebuild) skip_props_guard.set(false); - auto st = ggml_backend_graph_compute(target_backend_, sg_.gf); - if (need_rebuild) skip_props_guard.set(ar_graph_reuse); + // Scope the props-check skip to ONLY this target graph compute. A + // rebuild step must run the normal property scan so ggml-cuda resets + // warmup and recaptures; a stable replay step skips the O(nodes) scan. + // The flag is cleared immediately after so nothing else in the loop + // (after_target_compute, kvflash_maybe_reselect — each its own + // CUDA-graph compute) ever runs under a stale skip. + ggml_status st = GGML_STATUS_SUCCESS; + { + const bool skip_this = ar_graph_reuse && !need_rebuild; + dflash::qwen35::ScopedSkipPropsCheck skip_props_guard( + &ggml_cuda_set_skip_props_check, skip_this); + st = ggml_backend_graph_compute(target_backend_, sg_.gf); + } if (st != GGML_STATUS_SUCCESS) return false; after_target_compute(sg_, committed, 1); diff --git a/server/src/qwen35/qwen35_backend.h b/server/src/qwen35/qwen35_backend.h index a7e864dd2..9eaf4f685 100644 --- a/server/src/qwen35/qwen35_backend.h +++ b/server/src/qwen35/qwen35_backend.h @@ -133,6 +133,10 @@ class Qwen35Backend : public ModelBackend { std::vector & out_tokens, const DaemonIO & io); virtual bool should_capture_moe_router() const { return false; } + // AR graph-reuse + props-skip fast path is validated on the dense backend + // only. MoE (expert swaps + per-step host-sync) overrides this false to + // fall back to per-step graph rebuild. + virtual bool supports_ar_graph_reuse() const { return true; } // Hook after kvflash pool sizing, before create_target_cache: a subclass // may disable the pool (kvflash_tokens_=0) when it is redundant. Default no-op. virtual bool post_kvflash_init_gate() { return true; } @@ -232,7 +236,6 @@ class Qwen35Backend : public ModelBackend { StepGraph proj_sg_; // lm-head projection (remote-lm-head mode) int ar_decode_fa_bucket_ = -1; - static const bool ar_graph_reuse; // ── Draft feature mirror (cross-GPU feature transfer) ──────────── DraftFeatureMirror feature_mirror_; diff --git a/server/src/qwen35moe/qwen35moe_backend.h b/server/src/qwen35moe/qwen35moe_backend.h index 9c33e8553..113c378e0 100644 --- a/server/src/qwen35moe/qwen35moe_backend.h +++ b/server/src/qwen35moe/qwen35moe_backend.h @@ -44,6 +44,9 @@ class Qwen35MoeBackend : public Qwen35Backend { bool spark_wants_bootstrap() const override; bool spark_bootstrap_finalize(const std::string & profile_path) override; void after_target_compute(StepGraph & sg, int kv_start, int n_tokens) override; + // MoE AR is untested under graph-reuse (expert swaps + per-step host-sync); + // fall back to per-step rebuild instead of inheriting the dense fast path. + bool supports_ar_graph_reuse() const override { return false; } private: // All-hot placement signal for post_kvflash_init_gate(): set when diff --git a/server/src/server/http_server.cpp b/server/src/server/http_server.cpp index 8d4bdab24..78df082f9 100644 --- a/server/src/server/http_server.cpp +++ b/server/src/server/http_server.cpp @@ -23,6 +23,8 @@ #include "tool_hint.h" #include "common/sha1.h" #include "freeze_history.h" +#include "kv_quant.h" +#include "qwen35_graph_options.h" #ifdef DFLASH_HAS_CURL #include @@ -842,7 +844,16 @@ static std::array compute_disk_cache_salt(const ServerConfig & cfg) uint8_t tmpl_digest[20] = {}; sha1_hash(cfg.chat_template_src.data(), cfg.chat_template_src.size(), tmpl_digest); - // Serialization: path_len(4) + path + file_size(8) + file_mtime(8) + max_ctx(4) + tmpl_digest(20). + // K-rotation basis: the q4_0 WHT gate flips kv_k_rotated. A .dkv snapshot + // written under one rotation state and restored under another is garbage + // attention, so fold the resolved rotation basis into the salt — rotated + // and unrotated snapshots then get distinct fingerprints and never cross. + ggml_type kv_k_t = GGML_TYPE_COUNT, kv_v_t = GGML_TYPE_COUNT; + dflash::resolve_kv_types(kv_k_t, kv_v_t); + uint8_t kv_k_rotated = + dflash::common::qwen35_should_use_graph_wht_k_rotation(kv_k_t) ? 1 : 0; + + // Serialization: path_len(4) + path + file_size(8) + file_mtime(8) + max_ctx(4) + tmpl_digest(20) + kv_k_rotated(1). std::vector buf; uint32_t plen = (uint32_t)path.size(); buf.insert(buf.end(), (uint8_t *)&plen, (uint8_t *)&plen + 4); @@ -852,6 +863,7 @@ static std::array compute_disk_cache_salt(const ServerConfig & cfg) int32_t mc = (int32_t)cfg.max_ctx; buf.insert(buf.end(), (uint8_t *)&mc, (uint8_t *)&mc + 4); buf.insert(buf.end(), tmpl_digest, tmpl_digest + 20); + buf.push_back(kv_k_rotated); uint8_t digest[20]; sha1_hash(buf.data(), buf.size(), digest); From 5add81593b1a5945384145d4b73ee34aba2c469b Mon Sep 17 00:00:00 2001 From: dusterbloom <32869278+dusterbloom@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:21:13 +0200 Subject: [PATCH 9/9] fix(qwen35): address AR reuse and CPU GDN reviews --- .../deps/llama.cpp/ggml/src/ggml-cpu/ops.cpp | 22 ++-- server/src/qwen35/qwen35_backend.cpp | 3 +- server/test/test_server_unit.cpp | 111 ++++++++++++++++++ 3 files changed, 123 insertions(+), 13 deletions(-) diff --git a/server/deps/llama.cpp/ggml/src/ggml-cpu/ops.cpp b/server/deps/llama.cpp/ggml/src/ggml-cpu/ops.cpp index 88d95426f..b5634d71e 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cpu/ops.cpp +++ b/server/deps/llama.cpp/ggml/src/ggml-cpu/ops.cpp @@ -10478,9 +10478,12 @@ static void ggml_compute_forward_gated_delta_net_one_chunk( // attn_scores: S_v * H * n_tokens * n_seqs floats // new_states: S_v * S_v * H * n_seqs floats const int64_t attn_score_elems = S_v * H * n_tokens * n_seqs; - float * attn_out_base = (float *)dst->data; - float * state_out_base = (float *)dst->data + attn_score_elems; + float * attn_out_base = (float *)dst->data; + const bool inplace_state = ggml_get_op_params_i32(dst, 1) != 0; + float * state_out_base = inplace_state + ? (float *)src_state->data + : (float *)dst->data + attn_score_elems; const float * state_in_base = (const float *)src_state->data; //const int64_t rq1 = nev1 / neq1; @@ -10502,9 +10505,12 @@ static void ggml_compute_forward_gated_delta_net_one_chunk( float * s_out = state_out_base + (iv3 * H + iv1) * S_v * S_v; - // copy input state into output buffer and operate in-place + // Copy input state into the packed output for the regular variant. + // The in-place variant operates directly on src_state instead. const float * s_in = state_in_base + (iv3 * H + iv1) * S_v * S_v; - memcpy(s_out, s_in, S_v * S_v * sizeof(float)); + if (s_out != s_in) { + memcpy(s_out, s_in, S_v * S_v * sizeof(float)); + } // attn output pointer for first token of this (head, seq) float * attn_data = attn_out_base + (iv3 * n_tokens * H + iv1) * S_v; @@ -10604,14 +10610,6 @@ void ggml_compute_forward_gated_delta_net( ggml_tensor * dst) { const ggml_tensor * src0 = dst->src[0]; - // The in-place SSM state write (op_params[1]) is a CUDA-only fast path: the - // CPU kernel always writes the final state to dst+attn_score_elems and never - // reads it back, so honoring the flag here would silently freeze SSM state. - // Abort like TURBO_WHT rather than run a wrong-but-quiet CPU fallback. - if (ggml_get_op_params_i32(dst, 1) != 0) { - GGML_ABORT("gated_delta_net inplace state write is CUDA-only"); - } - switch (src0->type) { case GGML_TYPE_F32: { diff --git a/server/src/qwen35/qwen35_backend.cpp b/server/src/qwen35/qwen35_backend.cpp index 59e220381..e5a532168 100644 --- a/server/src/qwen35/qwen35_backend.cpp +++ b/server/src/qwen35/qwen35_backend.cpp @@ -1552,7 +1552,8 @@ bool Qwen35Backend::do_ar_decode(int committed, int n_gen, // a boundary; within a bucket we reuse the cached graph and just update // the mutable input tensors. const bool ar_graph_reuse = supports_ar_graph_reuse() - && std::getenv("DFLASH_AR_NO_REUSE") == nullptr; + && std::getenv("DFLASH_AR_NO_REUSE") == nullptr + && std::getenv("DFLASH_QWEN35_NO_KVPAD") == nullptr; ar_decode_fa_bucket_ = -1; // force first-step build for (int i = initial_emitted; i < n_gen; i++) { diff --git a/server/test/test_server_unit.cpp b/server/test/test_server_unit.cpp index 9b1dd749b..ee72fbd9f 100644 --- a/server/test/test_server_unit.cpp +++ b/server/test/test_server_unit.cpp @@ -4376,6 +4376,114 @@ static void test_gguf_bounds_error_reports_operands() { TEST_ASSERT(o.find("overflow") != std::string::npos); } +static void test_gated_delta_net_inplace_cpu_matches_regular() { + constexpr int64_t S = 2; + constexpr int64_t H = 2; + constexpr int64_t T = 3; + constexpr int64_t B = 1; + + ggml_init_params params{}; + params.mem_size = 1u << 20; + params.no_alloc = true; + ggml_context * ctx = ggml_init(params); + TEST_ASSERT(ctx != nullptr); + if (!ctx) return; + + ggml_backend_t backend = ggml_backend_cpu_init(); + TEST_ASSERT(backend != nullptr); + if (!backend) { + ggml_free(ctx); + return; + } + + ggml_tensor * q = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, S, H, T, B); + ggml_tensor * k = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, S, H, T, B); + ggml_tensor * v = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, S, H, T, B); + ggml_tensor * g = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, 1, H, T, B); + ggml_tensor * beta = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, 1, H, T, B); + ggml_tensor * state_regular = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, S, S, H, B); + ggml_tensor * state_inplace = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, S, S, H, B); + + ggml_tensor * regular = ggml_gated_delta_net( + ctx, q, k, v, g, beta, state_regular); + ggml_tensor * inplace = ggml_gated_delta_net_inplace( + ctx, q, k, v, g, beta, state_inplace); + ggml_set_output(regular); + ggml_set_output(inplace); + + ggml_cgraph * graph = ggml_new_graph(ctx); + ggml_build_forward_expand(graph, regular); + ggml_build_forward_expand(graph, inplace); + + ggml_backend_buffer_t buffer = ggml_backend_alloc_ctx_tensors(ctx, backend); + TEST_ASSERT(buffer != nullptr); + if (!buffer) { + ggml_backend_free(backend); + ggml_free(ctx); + return; + } + + std::vector q_data(S * H * T * B); + std::vector k_data(q_data.size()); + std::vector v_data(q_data.size()); + std::vector g_data(H * T * B); + std::vector beta_data(g_data.size()); + std::vector initial_state(S * S * H * B); + for (size_t i = 0; i < q_data.size(); ++i) { + q_data[i] = 0.03f * (float)(i + 1); + k_data[i] = 0.02f * (float)((i % 5) + 1); + v_data[i] = 0.04f * (float)((int)(i % 7) - 2); + } + for (size_t i = 0; i < g_data.size(); ++i) { + g_data[i] = -0.1f - 0.01f * (float)i; + beta_data[i] = 0.25f + 0.03f * (float)i; + } + for (size_t i = 0; i < initial_state.size(); ++i) { + initial_state[i] = 0.01f * (float)(i + 1); + } + + ggml_backend_tensor_set(q, q_data.data(), 0, ggml_nbytes(q)); + ggml_backend_tensor_set(k, k_data.data(), 0, ggml_nbytes(k)); + ggml_backend_tensor_set(v, v_data.data(), 0, ggml_nbytes(v)); + ggml_backend_tensor_set(g, g_data.data(), 0, ggml_nbytes(g)); + ggml_backend_tensor_set(beta, beta_data.data(), 0, ggml_nbytes(beta)); + ggml_backend_tensor_set(state_regular, initial_state.data(), 0, ggml_nbytes(state_regular)); + ggml_backend_tensor_set(state_inplace, initial_state.data(), 0, ggml_nbytes(state_inplace)); + + TEST_ASSERT(ggml_backend_graph_compute(backend, graph) == GGML_STATUS_SUCCESS); + + const size_t attn_elems = S * H * T * B; + const size_t state_elems = S * S * H * B; + std::vector regular_attn(attn_elems); + std::vector inplace_attn(attn_elems); + std::vector regular_state(state_elems); + std::vector inplace_state(state_elems); + std::vector regular_input_state(state_elems); + ggml_backend_tensor_get(regular, regular_attn.data(), 0, + regular_attn.size() * sizeof(float)); + ggml_backend_tensor_get(inplace, inplace_attn.data(), 0, + inplace_attn.size() * sizeof(float)); + ggml_backend_tensor_get(regular, regular_state.data(), + attn_elems * sizeof(float), + regular_state.size() * sizeof(float)); + ggml_backend_tensor_get(state_inplace, inplace_state.data(), 0, + inplace_state.size() * sizeof(float)); + ggml_backend_tensor_get(state_regular, regular_input_state.data(), 0, + regular_input_state.size() * sizeof(float)); + + for (size_t i = 0; i < attn_elems; ++i) { + TEST_ASSERT(std::fabs(regular_attn[i] - inplace_attn[i]) < 1.0e-6f); + } + for (size_t i = 0; i < state_elems; ++i) { + TEST_ASSERT(std::fabs(regular_state[i] - inplace_state[i]) < 1.0e-6f); + TEST_ASSERT(regular_input_state[i] == initial_state[i]); + } + + ggml_backend_buffer_free(buffer); + ggml_backend_free(backend); + ggml_free(ctx); +} + int main() { std::fprintf(stderr, "══════════════════════════════════════════\n"); std::fprintf(stderr, " Server Unit Tests\n"); @@ -4652,6 +4760,9 @@ int main() { RUN_TEST(test_gguf_tensor_in_file_bounds); RUN_TEST(test_gguf_bounds_error_reports_operands); + std::fprintf(stderr, "\n── ggml CPU gated delta net ──\n"); + RUN_TEST(test_gated_delta_net_inplace_cpu_matches_regular); + std::fprintf(stderr, "\n══════════════════════════════════════════\n"); std::fprintf(stderr, " Results: %d assertions, %d failures\n", test_count, test_failures);