From 63cbb590f3f2743d25bf609d8ae722e5fb23b551 Mon Sep 17 00:00:00 2001 From: mrciffa <49000955+davide221@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:00:39 +0200 Subject: [PATCH 01/20] vendor: grouped-expert MUL_MAT_ID for small MoE verify batches Speculative-verify batches (2-16 tokens) route consecutive draft tokens to heavily overlapping expert sets, but the multi-token MoE MMVQ kernel reads each (token, slot) pair's expert weights independently. Measured on a 256-expert top-8 MoE, an 8-row verify batch reads 64 expert matrices per layer where only 39.4 distinct ones are needed (1.62x duplication); at 15 rows it is 120 vs 58.9 (2.04x). This adds an opt-in path (DFLASH_MMID_GROUPED=1) that rank-sorts the (token, slot) pairs by routed expert in a tiny capture-safe prep kernel, then runs a variant of mul_mat_vec_q_moe whose warp w processes sorted pair blockIdx.y*8+w instead of (slot, token). Same-expert pairs are adjacent after sorting, so the warps of a block share an expert and duplicate weight reads are served from L1/L2 instead of DRAM. Launch shape, per-warp structure, vec_dot sequence and warp reduction are identical to mul_mat_vec_q_moe, so outputs are bit-exact and CUDA-graph capture stays supported. DFLASH_MMID_GROUPED_TYPES bitmask selects quant types: 1 = Q4_K (default), 2 = Q6_K (also lifts the Q6_K MUL_MAT_ID MMVQ ceiling to 16, keeping CUDA graphs enabled for those batches), 4 = Q4_0/Q8_0/Q5_K. Model-agnostic for any MoE with n_expert_used <= 16. Measured on RTX 3090 (sm_86), Laguna XS 2.1 Q4_K_M target + DFlash drafter, greedy HumanEval prompts, outputs byte-identical: verify width 8: 212.0 -> 219.9 tok/s (+3.7%) verify width 15: ~157 -> 173.3 tok/s (~+10%) Gains grow with verify width (more expert overlap to dedup), which is what makes wide/hedged verify configurations cheaper. --- .../deps/llama.cpp/ggml/src/ggml-cuda/mmvq.cu | 297 ++++++++++++++++++ 1 file changed, 297 insertions(+) diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/mmvq.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/mmvq.cu index 78dcf500f..d245c99b8 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/mmvq.cu +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/mmvq.cu @@ -233,8 +233,67 @@ static constexpr __host__ __device__ int get_mmvq_mmid_max_batch_rdna4(ggml_type } } + +// [TAG_MMID_GROUPED] grouped-expert MUL_MAT_ID for small speculative-verify +// batches (2..MMVQ_MAX_MOE_BATCH_SIZE tokens). Consecutive draft tokens route +// to heavily overlapping expert sets, but mul_mat_vec_q_moe reads each +// (token, slot) pair's expert weights independently. This path sorts the +// pairs by expert so that same-expert reads coalesce in cache, cutting expert +// weight traffic toward the union of routed experts. Bit-exact per +// (row, token) vs mul_mat_vec_q_moe (same vec_dot sequence and reduction). +// Model-agnostic: applies to any MoE with n_expert_used*n_tokens <= 256. +// DFLASH_MMID_GROUPED=1 opt-in +// DFLASH_MMID_GROUPED_TYPES bitmask, 1 = Q4_K (default), 2 = Q6_K, +// 4 = Q4_0/Q8_0/Q5_K. Q6_K stays on its tuned +// MMQ route above 5 tokens unless enabled. +#define MMID_GROUPED_MAX_PAIRS 256 +#define MMID_GROUPED_MAX_TPG 8 +#define MMID_META_NG 0 +#define MMID_META_GE 1 +#define MMID_META_GS (MMID_META_GE + MMID_GROUPED_MAX_PAIRS) +#define MMID_META_PT (MMID_META_GS + MMID_GROUPED_MAX_PAIRS + 1) +#define MMID_META_PS (MMID_META_PT + MMID_GROUPED_MAX_PAIRS) +#define MMID_META_INTS (MMID_META_PS + MMID_GROUPED_MAX_PAIRS) + +static bool mmid_grouped_env() { + static const bool on = []() { + const char * e = std::getenv("DFLASH_MMID_GROUPED"); + return e && e[0] == '1' && e[1] == '\0'; + }(); + return on; +} + +static bool mmid_grouped_type_ok(ggml_type type) { + // bit0 = Q4_K, bit1 = Q6_K, bit2 = Q4_0/Q8_0/Q5_K. Default Q4_K only: + // Q6_K keeps its tuned MMQ route above 5 tokens unless explicitly enabled. + static const int mask = []() { + const char * e = std::getenv("DFLASH_MMID_GROUPED_TYPES"); + if (e == nullptr || e[0] == '\0') { + return 1; + } + return atoi(e); + }(); + switch (type) { + case GGML_TYPE_Q4_K: + return (mask & 1) != 0; + case GGML_TYPE_Q6_K: + return (mask & 2) != 0; + case GGML_TYPE_Q4_0: + case GGML_TYPE_Q8_0: + case GGML_TYPE_Q5_K: + return (mask & 4) != 0; + default: + return false; + } +} + // Host function: returns the max batch size for the current arch+type at runtime. int get_mmvq_mmid_max_batch(ggml_type type, int cc) { + // [TAG_MMID_GROUPED] the grouped kernel handles any supported type up to the + // MoE batch ceiling; this also keeps CUDA graphs on for these batches. + if (mmid_grouped_env() && mmid_grouped_type_ok(type) && GGML_CUDA_CC_IS_NVIDIA(cc) && cc >= GGML_CUDA_CC_TURING) { + return MMVQ_MAX_MOE_BATCH_SIZE; + } // Dedicated multi-token MoE kernel: extend the MUL_MAT_ID ceiling to 16 // tokens on NVIDIA Turing+ for types whose base ceiling is already the // maximum. Types with tuned lower ceilings (per PR 20905) keep them. @@ -737,6 +796,225 @@ static __global__ void mul_mat_vec_q_moe( } } + +// [TAG_MMID_GROUPED] prep: rank-sort the (token, slot) pairs of a MUL_MAT_ID +// batch by routed expert. meta holds, per sorted pair p: ge[p] = expert, +// pt[p] = token, ps[p] = slot. Warps processing consecutive sorted pairs then +// share an expert, which is what makes the grouped kernel dedup weight reads. +// Single block, O(np^2) rank sort: np <= 256, ~microseconds, capture-safe. +static __global__ void mmid_group_prep( + const int32_t * __restrict__ ids, int32_t * __restrict__ meta, + const int n_slots, const int n_tok, const int ids_stride) { + __shared__ int sh_expert[MMID_GROUPED_MAX_PAIRS]; + const int np = n_slots*n_tok; + const int i = threadIdx.x; + int e = 0; + if (i < np) { + e = ids[(i % n_slots) + (i / n_slots)*ids_stride]; + sh_expert[i] = e; + } + __syncthreads(); + if (i < np) { + int r = 0; + for (int j = 0; j < np; ++j) { + const int ej = sh_expert[j]; + r += (ej < e || (ej == e && j < i)) ? 1 : 0; + } + meta[MMID_META_GE + r] = e; + meta[MMID_META_PT + r] = i / n_slots; + meta[MMID_META_PS + r] = i % n_slots; + } +} + +// [TAG_MMID_GROUPED] grouped MoE kernel. Identical launch shape and per-warp +// structure to mul_mat_vec_q_moe (block (warp_size, MMID_GROUPED_MAX_TPG), +// c_rows_per_block rows per warp, same vec_dot sequence and warp reduction), +// so results are bit-exact vs that kernel. The only difference: warp w +// handles the expert-SORTED pair blockIdx.y*MMID_GROUPED_MAX_TPG + w instead +// of (slot = blockIdx.y, token = w). Pairs routed to the same expert are +// adjacent after sorting, so the warps of a block mostly share one expert and +// their concurrent weight reads are served once from DRAM, then from L1/L2. +// Weight traffic approaches (union of routed experts) instead of +// (n_expert_used x n_tokens) expert-matrix reads. +template +__launch_bounds__(MMID_GROUPED_MAX_TPG*ggml_cuda_get_physical_warp_size(), 1) +static __global__ void mul_mat_vec_q_moe_grouped( + const void * __restrict__ vx, const void * __restrict__ vy, const int32_t * __restrict__ meta, + const ggml_cuda_mm_fusion_args_device fusion, + float * __restrict__ dst, + const uint32_t np, const uint32_t ncols_x, const uint3 nchannels_y, const uint32_t nrows_x, + const uint32_t stride_row_x, const uint32_t stride_col_y, const uint32_t stride_col_dst, + const uint32_t stride_channel_x, const uint32_t stride_channel_y, const uint32_t stride_channel_dst) { + + constexpr int qk = ggml_cuda_type_traits::qk; + constexpr int qi = ggml_cuda_type_traits::qi; + constexpr int vdr = get_vdr_mmvq(type); + constexpr int warp_size = ggml_cuda_get_physical_warp_size(); + + constexpr vec_dot_q_cuda_t vec_dot_q_cuda = get_vec_dot_q_cuda(type); + + const uint32_t p = blockIdx.y*MMID_GROUPED_MAX_TPG + threadIdx.y; + if (p >= np) { + return; + } + + const int channel_x = meta[MMID_META_GE + p]; + const int tok = meta[MMID_META_PT + p]; + const int slot = meta[MMID_META_PS + p]; + + const int row0 = c_rows_per_block*blockIdx.x; + const int blocks_per_row_x = ncols_x / qk; + constexpr int blocks_per_iter = vdr * warp_size / qi; + + const uint32_t channel_y = fastmodulo((uint32_t) slot, nchannels_y); + const block_q8_1 * y = ((const block_q8_1 *) vy) + channel_y*stride_channel_y + tok*stride_col_y; + const int kbx_offset = channel_x*stride_channel_x + row0*stride_row_x; + + bool use_gate = false; + bool use_bias = false; + bool use_gate_bias = false; + const void * vgate = nullptr; + const float * x_bias = nullptr; + const float * gate_bias = nullptr; + ggml_glu_op active_glu; + + if constexpr (has_fusion) { + use_gate = fusion.gate != nullptr; + use_bias = fusion.x_bias != nullptr; + use_gate_bias = fusion.gate_bias != nullptr && use_gate; + vgate = fusion.gate; + x_bias = (const float *) fusion.x_bias; + gate_bias = (const float *) fusion.gate_bias; + active_glu = fusion.glu_op; + } + + float tmp[c_rows_per_block] = {0.0f}; + float tmp_gate[c_rows_per_block] = {0.0f}; + + for (int kbx = threadIdx.x / (qi/vdr); kbx < blocks_per_row_x; kbx += blocks_per_iter) { + const int kby = kbx * (qk/QK8_1); + const int kqs = vdr * (threadIdx.x % (qi/vdr)); + +#pragma unroll + for (int i = 0; i < c_rows_per_block; ++i) { + tmp[i] += vec_dot_q_cuda(vx, &y[kby], kbx_offset + i*stride_row_x + kbx, kqs); + if constexpr (has_fusion) { + if (use_gate) { + tmp_gate[i] += vec_dot_q_cuda(vgate, &y[kby], kbx_offset + i*stride_row_x + kbx, kqs); + } + } + } + } + +#pragma unroll + for (int i = 0; i < c_rows_per_block; ++i) { + tmp[i] = warp_reduce_sum(tmp[i]); + if constexpr (has_fusion) { + if (use_gate) { + tmp_gate[i] = warp_reduce_sum(tmp_gate[i]); + } + } + } + + if (threadIdx.x < c_rows_per_block && (c_rows_per_block == 1 || uint32_t(row0 + threadIdx.x) < nrows_x)) { + float result = tmp[threadIdx.x]; + if constexpr (has_fusion) { + if (use_bias) { + result += x_bias[channel_x*stride_channel_dst + row0 + threadIdx.x]; + } + if (use_gate) { + float gate_value = tmp_gate[threadIdx.x]; + if (use_gate_bias) { + gate_value += gate_bias[channel_x*stride_channel_dst + row0 + threadIdx.x]; + } + switch (active_glu) { + case GGML_GLU_OP_SWIGLU: + result *= ggml_cuda_op_silu_single(gate_value); + break; + case GGML_GLU_OP_GEGLU: + result *= ggml_cuda_op_gelu_single(gate_value); + break; + case GGML_GLU_OP_SWIGLU_OAI: + result = ggml_cuda_op_swiglu_oai_single(gate_value, result); + break; + default: + result = result * gate_value; + break; + } + } + } + dst[slot*stride_channel_dst + tok*stride_col_dst + row0 + threadIdx.x] = result; + } + + if constexpr (!has_fusion) { + GGML_UNUSED_VARS(use_gate, use_bias, use_gate_bias, vgate, x_bias, gate_bias, active_glu, tmp_gate); + } +} + +template +static void mul_mat_vec_q_moe_grouped_launch( + const void * vx, const void * vy, const int32_t * meta, const ggml_cuda_mm_fusion_args_device & fusion, float * dst, + const uint32_t ncols_x, const uint3 nchannels_y, const uint32_t nrows_x, + const uint32_t stride_row_x, const uint32_t stride_col_y, const uint32_t stride_col_dst, + const uint32_t stride_channel_x, const uint32_t stride_channel_y, const uint32_t stride_channel_dst, + const int np, const int warp_size, cudaStream_t stream) { + + constexpr int rows_per_block = 2; + const int64_t nblocks_rows = (nrows_x + rows_per_block - 1)/rows_per_block; + const dim3 block_nums(nblocks_rows, (np + MMID_GROUPED_MAX_TPG - 1)/MMID_GROUPED_MAX_TPG); + const dim3 block_dims(warp_size, MMID_GROUPED_MAX_TPG); + + const bool has_fusion = fusion.gate != nullptr || fusion.x_bias != nullptr || fusion.gate_bias != nullptr; + if (has_fusion) { + mul_mat_vec_q_moe_grouped<<>>( + vx, vy, meta, fusion, dst, (uint32_t) np, ncols_x, nchannels_y, nrows_x, + stride_row_x, stride_col_y, stride_col_dst, + stride_channel_x, stride_channel_y, stride_channel_dst); + } else { + mul_mat_vec_q_moe_grouped<<>>( + vx, vy, meta, fusion, dst, (uint32_t) np, ncols_x, nchannels_y, nrows_x, + stride_row_x, stride_col_y, stride_col_dst, + stride_channel_x, stride_channel_y, stride_channel_dst); + } +} + +static bool mul_mat_vec_q_grouped_dispatch( + const ggml_type type, const void * vx, const void * vy, const int32_t * meta, + const ggml_cuda_mm_fusion_args_device & fusion, float * dst, + const int ncols_x, const int nrows_x, const int nchannels_y, + const int stride_row_x, const int stride_col_y, const int stride_col_dst, + const int stride_channel_x, const int stride_channel_y, const int stride_channel_dst, + const int max_groups, cudaStream_t stream) { + + const int warp_size = ggml_cuda_info().devices[ggml_cuda_get_device()].warp_size; + const uint3 nchannels_y_fd = init_fastdiv_values((uint32_t) nchannels_y); + + switch (type) { + case GGML_TYPE_Q4_0: + mul_mat_vec_q_moe_grouped_launch(vx, vy, meta, fusion, dst, ncols_x, nchannels_y_fd, nrows_x, + stride_row_x, stride_col_y, stride_col_dst, stride_channel_x, stride_channel_y, stride_channel_dst, max_groups, warp_size, stream); + return true; + case GGML_TYPE_Q8_0: + mul_mat_vec_q_moe_grouped_launch(vx, vy, meta, fusion, dst, ncols_x, nchannels_y_fd, nrows_x, + stride_row_x, stride_col_y, stride_col_dst, stride_channel_x, stride_channel_y, stride_channel_dst, max_groups, warp_size, stream); + return true; + case GGML_TYPE_Q4_K: + mul_mat_vec_q_moe_grouped_launch(vx, vy, meta, fusion, dst, ncols_x, nchannels_y_fd, nrows_x, + stride_row_x, stride_col_y, stride_col_dst, stride_channel_x, stride_channel_y, stride_channel_dst, max_groups, warp_size, stream); + return true; + case GGML_TYPE_Q5_K: + mul_mat_vec_q_moe_grouped_launch(vx, vy, meta, fusion, dst, ncols_x, nchannels_y_fd, nrows_x, + stride_row_x, stride_col_y, stride_col_dst, stride_channel_x, stride_channel_y, stride_channel_dst, max_groups, warp_size, stream); + return true; + case GGML_TYPE_Q6_K: + mul_mat_vec_q_moe_grouped_launch(vx, vy, meta, fusion, dst, ncols_x, nchannels_y_fd, nrows_x, + stride_row_x, stride_col_y, stride_col_dst, stride_channel_x, stride_channel_y, stride_channel_dst, max_groups, warp_size, stream); + return true; + default: + return false; + } +} + template static std::pair calc_launch_params( const int ncols_dst, const int nrows_x, const int nchannels_dst, const int nsamples_or_ntokens, @@ -1298,6 +1576,25 @@ void ggml_cuda_mul_mat_vec_q( const int64_t ids_stride = ids ? ids->nb[1] / ggml_type_size(ids->type) : 0; + // [TAG_MMID_GROUPED] grouped-expert path for small MUL_MAT_ID batches. + if (ids && ncols_dst >= 2 && ncols_dst <= MMVQ_MAX_MOE_BATCH_SIZE && + mmid_grouped_env() && mmid_grouped_type_ok(src0->type)) { + const int np = (int) (nchannels_dst*ncols_dst); + GGML_ASSERT(np <= MMID_GROUPED_MAX_PAIRS && "DFLASH_MMID_GROUPED supports n_expert_used <= 16"); + ggml_cuda_pool_alloc mmid_meta(ctx.pool(), MMID_META_INTS); + mmid_group_prep<<<1, MMID_GROUPED_MAX_PAIRS, 0, stream>>>( + ids_d, mmid_meta.ptr, (int) nchannels_dst, (int) ncols_dst, (int) ids_stride); + CUDA_CHECK(cudaGetLastError()); + if (mul_mat_vec_q_grouped_dispatch( + src0->type, src0->data, src1_q8_d, mmid_meta.ptr, fusion_local, dst_d, + (int) ne00, (int) ne01, (int) nchannels_y, + (int) s01, (int) stride_col_y, (int) stride_col_dst, + (int) s02, (int) stride_channel_y, (int) stride_channel_dst, + np, stream)) { + return; + } + } + mul_mat_vec_q_switch_type( src0->data, src0->type, src1_q8_d, ids_d, fusion_local, dst_d, ne00, ne01, ncols_dst, s01, stride_col_y, stride_col_dst, From c5e61b530e6ecc6d1ae87b0beafc84eefeba33ae Mon Sep 17 00:00:00 2001 From: mrciffa <49000955+davide221@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:21:10 +0200 Subject: [PATCH 02/20] vendor+server: opt-in per-token adaptive expert count for MoE verify batches Speculative-verify tokens differ in difficulty: for most tokens the router mass concentrates in a few experts, and dropping the low-mass tail rarely changes the verified argmax. This adds DFLASH_ADAPTIVE_K_TAU=<0..1>: inside the grouped MUL_MAT_ID prep kernel, each token keeps its leading experts until their cumulative combine weight reaches tau; the remaining ids are sentineled to -1 (skipped by the grouped kernel, exact zero contribution) and the kept weights are renormalized in place. Requires DFLASH_MMID_GROUPED=1 with DFLASH_MMID_GROUPED_TYPES=7 so every routed quant type takes the grouped path. The graph-builder side tags the router ids tensor with the combine-weights tensor via ids->extra (server/src/common/mmid_adaptive_k.h). Wired for laguna (DFlash capture layers 1,13,25,33,39 kept dense by default), qwen35moe and gemma4 (dense layers via DFLASH_ADAPTIVE_K_DENSE). Prefill and single-token decode are untouched. Not bit-exact (near-lossless): tau trades a small per-token risk for speed. Measured on RTX 3090, Laguna XS 2.1 Q4_K_M + DFlash drafter, HumanEval-10 greedy, tau=0.80: 212.0 -> 224.5 tok/s (+6% combined with the grouped kernel), expected_pass 10/10 at every tau tested; tau=0.80 best. tau needs per-model validation; qwen35moe/gemma4 are mechanism-wired but not yet swept. --- .../deps/llama.cpp/ggml/src/ggml-cuda/mmvq.cu | 87 ++++++++++++++++--- server/src/common/mmid_adaptive_k.h | 53 +++++++++++ server/src/gemma4/gemma4_graph.cpp | 2 + server/src/laguna/laguna_target_graph.cpp | 9 +- server/src/qwen35moe/qwen35moe_ffn.cpp | 3 + 5 files changed, 140 insertions(+), 14 deletions(-) create mode 100644 server/src/common/mmid_adaptive_k.h diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/mmvq.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/mmvq.cu index d245c99b8..f5bcd7b68 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/mmvq.cu +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/mmvq.cu @@ -255,6 +255,19 @@ static constexpr __host__ __device__ int get_mmvq_mmid_max_batch_rdna4(ggml_type #define MMID_META_PS (MMID_META_PT + MMID_GROUPED_MAX_PAIRS) #define MMID_META_INTS (MMID_META_PS + MMID_GROUPED_MAX_PAIRS) +// [TAG_MMID_ADAPTIVE_K] per-token expert budget. The graph builder tags the +// mmid ids tensor via ->extra with this struct; the prep kernel then drops +// low-mass slots (cumulative router weight >= tau keeps), sentinels their ids +// to -1 and renormalizes the kept weights in place. Idempotent across the +// gate/up/down calls of one layer (-1 markers). Requires the grouped path for +// every routed expert type (DFLASH_MMID_GROUPED=1, DFLASH_MMID_GROUPED_TYPES=7). +struct mmid_gate_extra { + uint32_t magic; // 0x4D474154 "MGAT" + float tau; + const ggml_tensor * weights; // [n_used, n_tok] f32, combine weights +}; +#define MMID_GATE_MAGIC 0x4D474154u + static bool mmid_grouped_env() { static const bool on = []() { const char * e = std::getenv("DFLASH_MMID_GROUPED"); @@ -803,15 +816,52 @@ static __global__ void mul_mat_vec_q_moe( // share an expert, which is what makes the grouped kernel dedup weight reads. // Single block, O(np^2) rank sort: np <= 256, ~microseconds, capture-safe. static __global__ void mmid_group_prep( - const int32_t * __restrict__ ids, int32_t * __restrict__ meta, - const int n_slots, const int n_tok, const int ids_stride) { + int32_t * __restrict__ ids, int32_t * __restrict__ meta, + const int n_slots, const int n_tok, const int ids_stride, + float * __restrict__ gate_w, const int gate_w_stride, const float gate_tau) { __shared__ int sh_expert[MMID_GROUPED_MAX_PAIRS]; const int np = n_slots*n_tok; const int i = threadIdx.x; + // [TAG_MMID_ADAPTIVE_K] one thread per token: keep slots until cumulative + // combine weight >= tau, sentinel the rest, renormalize kept in place. + // Skips tokens that already contain a -1 (second/third mmid of the layer). + if (gate_w != nullptr && i < n_tok) { + int32_t * idrow = ids + i*ids_stride; + float * wrow = gate_w + i*gate_w_stride; + bool gated = false; + for (int j = 0; j < n_slots; ++j) { + gated = gated || (idrow[j] < 0); + } + if (!gated) { + float cum = 0.0f; + int cut = n_slots; + float total = 0.0f; + for (int j = 0; j < n_slots; ++j) { + total += wrow[j]; + } + for (int j = 0; j < n_slots; ++j) { + cum += wrow[j]; + if (cum >= gate_tau*total) { cut = j + 1; break; } + } + if (cut < n_slots) { + const float inv = total/cum; + for (int j = 0; j < n_slots; ++j) { + if (j < cut) { + wrow[j] *= inv; + } else { + idrow[j] = -1; + wrow[j] = 0.0f; + } + } + } + } + } + __syncthreads(); int e = 0; if (i < np) { e = ids[(i % n_slots) + (i / n_slots)*ids_stride]; - sh_expert[i] = e; + sh_expert[i] = e < 0 ? 0x7FFFFFFF : e; // sentinels sort last + e = sh_expert[i]; } __syncthreads(); if (i < np) { @@ -820,7 +870,7 @@ static __global__ void mmid_group_prep( const int ej = sh_expert[j]; r += (ej < e || (ej == e && j < i)) ? 1 : 0; } - meta[MMID_META_GE + r] = e; + meta[MMID_META_GE + r] = e == 0x7FFFFFFF ? -1 : e; meta[MMID_META_PT + r] = i / n_slots; meta[MMID_META_PS + r] = i % n_slots; } @@ -863,6 +913,15 @@ static __global__ void mul_mat_vec_q_moe_grouped( const int slot = meta[MMID_META_PS + p]; const int row0 = c_rows_per_block*blockIdx.x; + + // [TAG_MMID_ADAPTIVE_K] dropped slot: contribute exact zeros (combine + // weight is zeroed too, but dst must not hold garbage). + if (channel_x < 0) { + if (threadIdx.x < c_rows_per_block && (c_rows_per_block == 1 || uint32_t(row0 + threadIdx.x) < nrows_x)) { + dst[slot*stride_channel_dst + tok*stride_col_dst + row0 + threadIdx.x] = 0.0f; + } + return; + } const int blocks_per_row_x = ncols_x / qk; constexpr int blocks_per_iter = vdr * warp_size / qi; @@ -1518,11 +1577,7 @@ void ggml_cuda_mul_mat_vec_q( const size_t q8_bytes = ne13*ne12 * ne11*ne10_padded * sizeof(block_q8_1)/QK8_1; ggml_cuda_pool_alloc src1_q8_1(ctx.pool()); char * src1_q8_d = nullptr; - // The src1->q8_1 quantization depends only on src0->type and src1's dims/strides, - // not on ids (ids only affects the matmul kernel's channel/dst strides below), so - // the memo is valid for MUL_MAT_ID too: gate/up and the shared expert re-quantize - // the same ffn_norm activation and can share one q8 buffer within an evaluation. - if (luce_q8_memo_on) { + if (luce_q8_memo_on && !ids) { for (const auto & e : ctx.luce_q8_memo) { if (e.src1_node == (const void *) src1 && e.src1_data == (const void *) src1_d && e.src0_type == (int) src0->type && @@ -1534,7 +1589,7 @@ void ggml_cuda_mul_mat_vec_q( } if (src1_q8_d == nullptr) { char * q8_dst; - if (luce_q8_memo_on) { + if (luce_q8_memo_on && !ids) { ggml_backend_cuda_context::luce_q8_memo_entry ent; ent.src1_node = (const void *) src1; ent.src1_data = (const void *) src1_d; @@ -1582,8 +1637,18 @@ void ggml_cuda_mul_mat_vec_q( const int np = (int) (nchannels_dst*ncols_dst); GGML_ASSERT(np <= MMID_GROUPED_MAX_PAIRS && "DFLASH_MMID_GROUPED supports n_expert_used <= 16"); ggml_cuda_pool_alloc mmid_meta(ctx.pool(), MMID_META_INTS); + float * gate_w = nullptr; + int gate_w_stride = 0; + float gate_tau = 0.0f; + const mmid_gate_extra * gx = (const mmid_gate_extra *) ids->extra; // [TAG_MMID_ADAPTIVE_K] + if (gx != nullptr && gx->magic == MMID_GATE_MAGIC && gx->weights != nullptr && gx->weights->data != nullptr) { + gate_w = (float *) gx->weights->data; + gate_w_stride = (int) (gx->weights->nb[1]/sizeof(float)); + gate_tau = gx->tau; + } mmid_group_prep<<<1, MMID_GROUPED_MAX_PAIRS, 0, stream>>>( - ids_d, mmid_meta.ptr, (int) nchannels_dst, (int) ncols_dst, (int) ids_stride); + (int32_t *) ids_d, mmid_meta.ptr, (int) nchannels_dst, (int) ncols_dst, (int) ids_stride, + gate_w, gate_w_stride, gate_tau); CUDA_CHECK(cudaGetLastError()); if (mul_mat_vec_q_grouped_dispatch( src0->type, src0->data, src1_q8_d, mmid_meta.ptr, fusion_local, dst_d, diff --git a/server/src/common/mmid_adaptive_k.h b/server/src/common/mmid_adaptive_k.h new file mode 100644 index 000000000..62ce9be23 --- /dev/null +++ b/server/src/common/mmid_adaptive_k.h @@ -0,0 +1,53 @@ +#pragma once +// [TAG_MMID_ADAPTIVE_K] per-token expert-count gating for small MUL_MAT_ID +// batches (speculative-verify sized, 2..16 tokens). Contract with the grouped +// MUL_MAT_ID path in ggml-cuda mmvq.cu (DFLASH_MMID_GROUPED=1): when a router +// ids tensor's ->extra points at an mmid_gate_extra, the grouped prep kernel +// keeps each token's leading experts until their cumulative combine weight +// reaches tau, sentinels the rest of the token's ids to -1 (skipped, exact +// zero contribution) and renormalizes the kept weights in place. +// DFLASH_ADAPTIVE_K_TAU=<0..1> enables (0/unset = off) +// DFLASH_ADAPTIVE_K_DENSE= layers kept at full top-k. DFlash capture +// layers MUST stay dense so drafter +// conditioning features are unchanged. +#include "ggml.h" + +#include +#include +#include + +struct mmid_gate_extra { + uint32_t magic; // MMID_GATE_MAGIC + float tau; + const ggml_tensor * weights; // [n_used, n_tokens] f32 combine weights +}; +#define MMID_GATE_MAGIC 0x4D474154u + +// il < 0 = layer index unknown for this family: the dense-layer list cannot +// be applied, every MoE layer is gated. Only builds run this; the extra must +// outlive the graph, so it is deliberately never freed. +inline void mmid_adaptive_k_attach(ggml_tensor * ids, const ggml_tensor * weights, + int n_tokens, int il, const char * dense_default) { + static const float tau = []() { + const char * e = std::getenv("DFLASH_ADAPTIVE_K_TAU"); + return e ? (float) std::atof(e) : 0.0f; + }(); + if (tau <= 0.0f || n_tokens < 2 || n_tokens > 16 || ids == nullptr || weights == nullptr) { + return; + } + if (il >= 0) { + const char * e = std::getenv("DFLASH_ADAPTIVE_K_DENSE"); + const std::string str = e ? e : (dense_default ? dense_default : ""); + size_t pos = 0; + while (pos < str.size()) { + const size_t q = str.find(',', pos); + const std::string tok = str.substr(pos, q == std::string::npos ? std::string::npos : q - pos); + if (!tok.empty() && std::atoi(tok.c_str()) == il) { + return; + } + if (q == std::string::npos) break; + pos = q + 1; + } + } + ids->extra = new mmid_gate_extra{MMID_GATE_MAGIC, tau, weights}; +} diff --git a/server/src/gemma4/gemma4_graph.cpp b/server/src/gemma4/gemma4_graph.cpp index f5b310a50..b9a427c67 100644 --- a/server/src/gemma4/gemma4_graph.cpp +++ b/server/src/gemma4/gemma4_graph.cpp @@ -15,6 +15,7 @@ // - Final RMSNorm + lm_head // - Logit softcapping: tanh(logits/cap)*cap +#include "../common/mmid_adaptive_k.h" #include "gemma4_internal.h" #include "common/ggml_graph_precision.h" #include "common/gpu_runtime_compat.h" @@ -105,6 +106,7 @@ static ggml_tensor * build_gemma4_moe_block(ggml_context * ctx, ggml_tensor * at ggml_tensor * probs_3d = ggml_reshape_3d(ctx, probs, 1, n_expert, n_tokens); ggml_tensor * weights = ggml_get_rows(ctx, probs_3d, selected); weights = ggml_reshape_2d(ctx, weights, n_used, n_tokens); + mmid_adaptive_k_attach(selected, weights, n_tokens, -1, nullptr); // [TAG_MMID_ADAPTIVE_K] // Routed expert forward via mul_mat_id with fused gate+up ggml_tensor * cur_3d = ggml_reshape_3d(ctx, cur_moe, n_embd, 1, n_tokens); diff --git a/server/src/laguna/laguna_target_graph.cpp b/server/src/laguna/laguna_target_graph.cpp index 0c7687e4c..48ef55ba1 100644 --- a/server/src/laguna/laguna_target_graph.cpp +++ b/server/src/laguna/laguna_target_graph.cpp @@ -17,6 +17,7 @@ // is tested against our llama.cpp build_laguna (already verified to match HF // for 30+ tokens on B-tree prompt; see Lucebox/Laguna-XS.2-GGUF README). +#include "../common/mmid_adaptive_k.h" #include "laguna_internal.h" #include "../common/moe_hybrid_storage.h" #include "../common/moe_router_graph.h" @@ -387,7 +388,7 @@ static ggml_tensor * build_laguna_dense_ffn(ggml_context * ctx, ggml_tensor * cu // Forward decl for the full MoE block (defined further down). static ggml_tensor * build_laguna_moe_block_full(ggml_context * ctx, ggml_cgraph * gf, ggml_tensor * cur, const LagunaTargetWeights & w, - const LagunaTargetLayer & L); + const LagunaTargetLayer & L, int il); // Forward decl for the hybrid (offload) MoE block (defined further down). static ggml_tensor * build_laguna_moe_block_hybrid(ggml_context * ctx, ggml_cgraph * gf, ggml_tensor * cur, const LagunaTargetWeights & w, @@ -421,7 +422,7 @@ static ggml_tensor * build_laguna_moe_block(ggml_context * ctx, ggml_cgraph * gf hyb->storage->layers[(size_t)il], hyb->lut_all, hyb->vld_all, hyb->sel_all, il - hyb->dense_lead); } - return build_laguna_moe_block_full(ctx, gf, cur, w, L); + return build_laguna_moe_block_full(ctx, gf, cur, w, L, il); } // Phase 2.1: full MoE dispatch (sigmoid + score-correction bias + sum-norm + @@ -429,7 +430,7 @@ static ggml_tensor * build_laguna_moe_block(ggml_context * ctx, ggml_cgraph * gf // the SIGMOID + WEIGHTS_NORM + EXP_PROBS_B configuration that Laguna uses. static ggml_tensor * build_laguna_moe_block_full(ggml_context * ctx, ggml_cgraph * gf, ggml_tensor * cur, const LagunaTargetWeights & w, - const LagunaTargetLayer & L) { + const LagunaTargetLayer & L, int il) { const int n_tokens = (int)cur->ne[1]; const int n_expert = w.n_expert; const int n_used = w.n_expert_used; @@ -447,6 +448,8 @@ static ggml_tensor * build_laguna_moe_block_full(ggml_context * ctx, ggml_cgraph ggml_tensor * selected = router.selected; ggml_tensor * weights_2d = router.weights_2d; ggml_tensor * weights_3d = router.weights_3d; + // [TAG_MMID_ADAPTIVE_K] default dense list = Laguna DFlash capture layers. + mmid_adaptive_k_attach(selected, weights_2d, n_tokens, il, "1,13,25,33,39"); // Per-expert SwiGLU via mul_mat_id. // ffn_gate_exps: [n_embd, n_ff_exp, n_expert] diff --git a/server/src/qwen35moe/qwen35moe_ffn.cpp b/server/src/qwen35moe/qwen35moe_ffn.cpp index 34fe5f0d0..a71a94a4a 100644 --- a/server/src/qwen35moe/qwen35moe_ffn.cpp +++ b/server/src/qwen35moe/qwen35moe_ffn.cpp @@ -1,3 +1,4 @@ +#include "../common/mmid_adaptive_k.h" #include "qwen35moe_ffn.h" #include "qwen35_ops.h" @@ -57,6 +58,8 @@ Qwen35MoeRouterOutputs build_qwen35moe_router( weights = ggml_scale(ctx, weights, w.expert_weights_scale); } + mmid_adaptive_k_attach(selected, weights, n_tokens, -1, nullptr); // [TAG_MMID_ADAPTIVE_K] + Qwen35MoeRouterOutputs out; out.selected = selected; out.weights = weights; From 1fe86bd186f4d894aabeabecd60176a7915da344 Mon Sep 17 00:00:00 2001 From: mrciffa <49000955+davide221@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:05:47 +0200 Subject: [PATCH 03/20] vendor+server: drafter-confidence adaptive verify width for MoE spec decode Every verify row on an MoE target routes to its own top-k experts, so rows cost real weight bandwidth (~1ms/row for a Q4 target on a 3090). A fixed verify width pays that on every step; this trims the batch per step using the drafter's own confidence (keep slot j while the product of top-1 probs through j stays above DFLASH_ADAPTIVE_WIDTH_THETA). Output-exactness is preserved: only the number of speculated slots changes. - ggml-cuda: ggml_backend_cuda_topk_rows, a small top-k (k<=8) + softmax probability reduction over draft-head logits rows, ~KB readback instead of the full vocab logits. Also used to accelerate project_hidden_to_topk (draft-tree candidates) at unit temperature. - common: adaptive_verify_width.h helper + DFlashTarget hook returning per-slot candidate probs alongside draft tokens; domino fused head exposes its corrected logits for the same extraction. - laguna: reference integration (projection extraction, per-width verify graph slots so every width's CUDA graph captures once and replays, trim before verify_batch). Laguna XS 2.1 Q4_K_M + official DFlash drafter, 3090, HE-10 512-tok: theta=0.20 min=4 --verify-width 8 -> 236.0 tok/s vs 226.4 fixed w6 control (+4.2%), commit/step 3.58->3.79, avg rows 5.35, 10/10 pass. Off by default. --- .../deps/llama.cpp/ggml/include/ggml-cuda.h | 6 + .../llama.cpp/ggml/src/ggml-cuda/topk-rows.cu | 145 ++++++++++++++++++ server/src/common/adaptive_verify_width.h | 56 +++++++ server/src/common/dflash_target.h | 15 ++ server/src/common/domino_head.cpp | 34 +++- server/src/common/domino_head.h | 5 +- server/src/laguna/laguna_backend.cpp | 33 +++- server/src/laguna/laguna_dflash_target.cpp | 30 ++++ server/src/laguna/laguna_dflash_target.h | 7 + server/src/laguna/laguna_internal.h | 5 +- server/src/laguna/laguna_target_graph.cpp | 45 +++++- 11 files changed, 370 insertions(+), 11 deletions(-) create mode 100644 server/deps/llama.cpp/ggml/src/ggml-cuda/topk-rows.cu create mode 100644 server/src/common/adaptive_verify_width.h diff --git a/server/deps/llama.cpp/ggml/include/ggml-cuda.h b/server/deps/llama.cpp/ggml/include/ggml-cuda.h index 5436c7ef5..f9693057f 100644 --- a/server/deps/llama.cpp/ggml/include/ggml-cuda.h +++ b/server/deps/llama.cpp/ggml/include/ggml-cuda.h @@ -45,6 +45,12 @@ GGML_BACKEND_API void ggml_backend_cuda_unregister_host_buffer(void * buffer); GGML_BACKEND_API ggml_backend_reg_t ggml_backend_cuda_reg(void); +// [TAG_TOPK_ROWS] top-k (k <= 8) entries + softmax probabilities per row of a +// device-resident contiguous F32 [ncols, nrows] tensor; ~KB of readback. Not +// a graph op: call after the producing graph_compute has returned. +GGML_BACKEND_API bool ggml_backend_cuda_topk_rows(const struct ggml_tensor * logits, int k, + float * probs_out, int32_t * ids_out); + #ifdef __cplusplus } #endif diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/topk-rows.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/topk-rows.cu new file mode 100644 index 000000000..9b7a2cccc --- /dev/null +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/topk-rows.cu @@ -0,0 +1,145 @@ +// [TAG_TOPK_ROWS] Small-k top-k + softmax probabilities over the rows of a +// device-resident contiguous F32 tensor. Serves draft-head candidate +// extraction for speculative decoding (adaptive verify width, draft trees): +// k <= 8 candidates out of a ~100k-entry vocab distribution per drafted +// position, ~KB of readback instead of the full logits. Not a graph op: call +// after the producing graph_compute has returned; runs on the default stream. +#include "common.cuh" +#include "ggml-cuda.h" + +#if defined(GGML_USE_HIP) +#ifndef cudaPointerAttributes +#define cudaPointerAttributes hipPointerAttribute_t +#define cudaPointerGetAttributes hipPointerGetAttributes +#define cudaMemoryTypeDevice hipMemoryTypeDevice +#define cudaMemoryTypeManaged hipMemoryTypeManaged +#endif +#endif + +#define TOPK_ROWS_K 8 +#define TOPK_ROWS_MAX_ROWS 64 +#define TOPK_ROWS_BLOCK 256 + +// One block per row. Each thread keeps a sorted local top-K over its strided +// columns; thread 0 merges the block's candidates. Deterministic: ties break +// toward the lower column index, and the softmax denominator uses a fixed +// shared-memory tree reduction. +static __global__ void topk_rows_kernel(const float * __restrict__ x, const int ncols, + float * __restrict__ out_vals, + int32_t * __restrict__ out_ids) { + constexpr int K = TOPK_ROWS_K; + const float * xr = x + (size_t) blockIdx.x * (size_t) ncols; + + float tv[K]; + int ti[K]; +#pragma unroll + for (int j = 0; j < K; ++j) { tv[j] = -INFINITY; ti[j] = 0x7fffffff; } + for (int c = threadIdx.x; c < ncols; c += blockDim.x) { + const float v = xr[c]; + if (v > tv[K-1] || (v == tv[K-1] && c < ti[K-1])) { + int j = K - 1; + while (j > 0 && (v > tv[j-1] || (v == tv[j-1] && c < ti[j-1]))) { + tv[j] = tv[j-1]; ti[j] = ti[j-1]; --j; + } + tv[j] = v; ti[j] = c; + } + } + + __shared__ float sv[TOPK_ROWS_BLOCK * K]; + __shared__ int si[TOPK_ROWS_BLOCK * K]; +#pragma unroll + for (int j = 0; j < K; ++j) { + sv[threadIdx.x * K + j] = tv[j]; + si[threadIdx.x * K + j] = ti[j]; + } + __syncthreads(); + + __shared__ float s_val[K]; + if (threadIdx.x == 0) { + float bv[K]; int bi[K]; +#pragma unroll + for (int j = 0; j < K; ++j) { bv[j] = -INFINITY; bi[j] = 0x7fffffff; } + for (int e = 0; e < TOPK_ROWS_BLOCK * K; ++e) { + const float v = sv[e]; const int idx = si[e]; + if (idx == 0x7fffffff) continue; + if (v > bv[K-1] || (v == bv[K-1] && idx < bi[K-1])) { + int j = K - 1; + while (j > 0 && (v > bv[j-1] || (v == bv[j-1] && idx < bi[j-1]))) { + bv[j] = bv[j-1]; bi[j] = bi[j-1]; --j; + } + bv[j] = v; bi[j] = idx; + } + } +#pragma unroll + for (int j = 0; j < K; ++j) { + s_val[j] = bv[j]; + out_ids[blockIdx.x * K + j] = bi[j] == 0x7fffffff ? -1 : bi[j]; + } + } + __syncthreads(); + + const float mx = s_val[0]; + float lsum = 0.0f; + for (int c = threadIdx.x; c < ncols; c += blockDim.x) { + lsum += expf(xr[c] - mx); + } + __shared__ float ssum[TOPK_ROWS_BLOCK]; + ssum[threadIdx.x] = lsum; + __syncthreads(); + for (int s = TOPK_ROWS_BLOCK / 2; s > 0; s >>= 1) { + if (threadIdx.x < s) ssum[threadIdx.x] += ssum[threadIdx.x + s]; + __syncthreads(); + } + if (threadIdx.x == 0) { + const float z = ssum[0]; +#pragma unroll + for (int j = 0; j < K; ++j) { + out_vals[blockIdx.x * K + j] = s_val[j] == -INFINITY ? 0.0f : expf(s_val[j] - mx) / z; + } + } +} + +bool ggml_backend_cuda_topk_rows(const struct ggml_tensor * logits, int k, + float * probs_out, int32_t * ids_out) { + if (logits == nullptr || probs_out == nullptr || ids_out == nullptr) return false; + if (logits->type != GGML_TYPE_F32 || !ggml_is_contiguous(logits)) return false; + const int64_t ncols = logits->ne[0]; + const int64_t nrows = ggml_nrows(logits); + if (k < 1 || k > TOPK_ROWS_K || nrows < 1 || + nrows > TOPK_ROWS_MAX_ROWS || ncols < TOPK_ROWS_K) { + return false; + } + cudaPointerAttributes attr{}; + if (cudaPointerGetAttributes(&attr, logits->data) != cudaSuccess || + (attr.type != cudaMemoryTypeDevice && attr.type != cudaMemoryTypeManaged)) { + return false; + } + ggml_cuda_set_device(attr.device); + + // Tiny per-device scratch; decode is single-threaded per process (same + // assumption as the persistent verify-graph slots in the server). + static float * d_vals = nullptr; + static int32_t * d_ids = nullptr; + static int d_dev = -1; + if (d_dev != attr.device) { + if (d_vals != nullptr) { CUDA_CHECK(cudaFree(d_vals)); d_vals = nullptr; } + if (d_ids != nullptr) { CUDA_CHECK(cudaFree(d_ids)); d_ids = nullptr; } + CUDA_CHECK(cudaMalloc(&d_vals, sizeof(float) * TOPK_ROWS_MAX_ROWS * TOPK_ROWS_K)); + CUDA_CHECK(cudaMalloc(&d_ids, sizeof(int32_t) * TOPK_ROWS_MAX_ROWS * TOPK_ROWS_K)); + d_dev = attr.device; + } + + topk_rows_kernel<<<(int) nrows, TOPK_ROWS_BLOCK>>>( + (const float *) logits->data, (int) ncols, d_vals, d_ids); + float h_vals[TOPK_ROWS_MAX_ROWS * TOPK_ROWS_K]; + int32_t h_ids [TOPK_ROWS_MAX_ROWS * TOPK_ROWS_K]; + CUDA_CHECK(cudaMemcpy(h_vals, d_vals, sizeof(float) * (size_t) nrows * TOPK_ROWS_K, cudaMemcpyDeviceToHost)); + CUDA_CHECK(cudaMemcpy(h_ids, d_ids, sizeof(int32_t) * (size_t) nrows * TOPK_ROWS_K, cudaMemcpyDeviceToHost)); + for (int64_t r = 0; r < nrows; ++r) { + for (int j = 0; j < k; ++j) { + probs_out[r * k + j] = h_vals[r * TOPK_ROWS_K + j]; + ids_out [r * k + j] = h_ids [r * TOPK_ROWS_K + j]; + } + } + return true; +} diff --git a/server/src/common/adaptive_verify_width.h b/server/src/common/adaptive_verify_width.h new file mode 100644 index 000000000..6fac0fb15 --- /dev/null +++ b/server/src/common/adaptive_verify_width.h @@ -0,0 +1,56 @@ +#pragma once +// [TAG_ADAPTIVE_WIDTH] Per-step speculative verify-width selection. +// +// On MoE targets every verify row routes to its own top-k experts, so each +// row costs real weight bandwidth (~1ms/row for a Q4 target on a 24GB +// Ampere). A fixed verify width pays that cost even on steps where the +// drafter is unsure and the deep slots almost never commit. This helper trims +// the verify batch per step using the drafter's own confidence: keep +// candidate slot j while the drafter's estimate of the chain surviving +// through j (the product of the top-1 probabilities of slots 1..j) stays +// above theta. Output-exactness is preserved: only the number of speculated +// slots changes, every committed token is still target-verified. +// +// DFLASH_ADAPTIVE_WIDTH_THETA=<0..1> enables (0/unset = off; ~0.20 typical) +// DFLASH_ADAPTIVE_WIDTH_MIN= minimum kept rows incl. seed (default 4) +// +// Model-agnostic: any family loop that has per-slot drafter top-1 +// probabilities (e.g. from ggml_backend_cuda_topk_rows over the draft-head +// logits) can call this before building its verify batch. +#include + +inline float adaptive_verify_width_theta() { + static const float theta = []() { + const char * e = std::getenv("DFLASH_ADAPTIVE_WIDTH_THETA"); + return e ? (float) std::atof(e) : 0.0f; + }(); + return theta; +} + +inline int adaptive_verify_width_min() { + static const int mn = []() { + const char * e = std::getenv("DFLASH_ADAPTIVE_WIDTH_MIN"); + return e ? std::atoi(e) : 4; + }(); + return mn; +} + +// top1_probs[(j-1)*stride]: drafter top-1 probability of candidate slot j. +// Returns the number of rows to verify (seed row 0 included), in +// [min(min_rows, n_rows), n_rows]. +inline int adaptive_verify_width(const float * top1_probs, int stride, + int n_rows, float theta, int min_rows) { + if (theta <= 0.0f || top1_probs == nullptr || n_rows <= 2) { + return n_rows; + } + float reach = 1.0f; + int w = 1; + for (int j = 1; j < n_rows; ++j) { + reach *= top1_probs[(size_t)(j - 1) * (size_t)stride]; + if (w >= min_rows && reach < theta) { + break; + } + ++w; + } + return w; +} diff --git a/server/src/common/dflash_target.h b/server/src/common/dflash_target.h index 902e19c43..aad6428aa 100644 --- a/server/src/common/dflash_target.h +++ b/server/src/common/dflash_target.h @@ -134,6 +134,21 @@ struct DFlashTarget { // Project draft hidden states through the target lm_head and return full // f32 logits with vocab as the fastest-changing dimension. // Default false (unsupported); Domino-capable targets override. + // [TAG_ADAPTIVE_WIDTH] like project_hidden_to_tokens, optionally also + // returning top-cand_k candidate ids + softmax probs covering slots + // 1..n_tokens-1 (entry j-1 <-> slot j). Default: no candidates. + virtual bool project_hidden_to_tokens_topk(const float * hidden, + int n_tokens, + std::vector & tokens_out, + int cand_k, + std::vector * cand_probs, + std::vector * cand_ids) { + (void)cand_k; + if (cand_probs) cand_probs->clear(); + if (cand_ids) cand_ids->clear(); + return project_hidden_to_tokens(hidden, n_tokens, tokens_out); + } + virtual bool project_hidden_to_logits(const float * hidden, int n_tokens, std::vector & logits_out) { diff --git a/server/src/common/domino_head.cpp b/server/src/common/domino_head.cpp index cc60d8279..6d02d393f 100644 --- a/server/src/common/domino_head.cpp +++ b/server/src/common/domino_head.cpp @@ -1,3 +1,4 @@ +#include "ggml-cuda.h" #include "domino_head.h" #include "ggml-alloc.h" @@ -186,7 +187,10 @@ bool domino_correct_greedy_chain_fused(const DraftWeights & dw, const float * local_hidden, int q_len, int32_t last_tok, - std::vector & draft_tok) { + std::vector & draft_tok, + int cand_k, + std::vector * cand_probs, + std::vector * cand_ids) { if (!dw.domino.enabled || q_len <= 1 || !local_hidden || !backend || !lm_head || !embd_table) { return false; @@ -210,7 +214,7 @@ bool domino_correct_greedy_chain_fused(const DraftWeights & dw, static const bool zero_start = std::getenv("DFLASH_DOMINO_ZERO_START") != nullptr; - const size_t arena_size = ggml_tensor_overhead() * (size_t)(96 + 48 * n_cand) + + const size_t arena_size = ggml_tensor_overhead() * (size_t)(96 + 52 * n_cand) + ggml_graph_overhead_custom(1024, false) + 4 * 1024 * 1024; static thread_local std::vector g_arena_fused; if (g_arena_fused.size() < arena_size) g_arena_fused.resize(arena_size); @@ -237,6 +241,7 @@ bool domino_correct_greedy_chain_fused(const DraftWeights & dw, ggml_set_name(prev_embed, "dom_embed_seed"); std::vector toks((size_t)n_cand, nullptr); + std::vector corr((size_t)n_cand, nullptr); for (int i = 0; i < n_cand; ++i) { ggml_tensor * gi = ggml_mul_mat(ctx, dw.domino.gru_w_ih, prev_embed); gi = ggml_add(ctx, gi, ggml_reshape_2d(ctx, dw.domino.gru_b_ih, 3 * H, 1)); @@ -275,6 +280,7 @@ bool domino_correct_greedy_chain_fused(const DraftWeights & dw, ggml_set_output(tok); ggml_build_forward_expand(gf, tok); toks[(size_t)i] = tok; + corr[(size_t)i] = corrected; if (i + 1 < n_cand) { prev_embed = ggml_get_rows(ctx, embd_table, tok); @@ -283,6 +289,18 @@ bool domino_correct_greedy_chain_fused(const DraftWeights & dw, state = h_new; } + // [TAG_ADAPTIVE_WIDTH] optionally expose every position's corrected + // logits as one [vocab, n_cand] output for post-compute top-k extraction. + ggml_tensor * all_corr = nullptr; + if (cand_k > 0 && cand_probs != nullptr && cand_ids != nullptr) { + all_corr = corr[0]; + for (int i = 1; i < n_cand; ++i) { + all_corr = ggml_concat(ctx, all_corr, corr[(size_t)i], 1); + } + ggml_set_output(all_corr); + ggml_build_forward_expand(gf, all_corr); + } + static thread_local ggml_gallocr_t galloc_fused = nullptr; if (!galloc_fused) { galloc_fused = ggml_gallocr_new(ggml_backend_get_default_buffer_type(backend)); @@ -303,6 +321,18 @@ bool domino_correct_greedy_chain_fused(const DraftWeights & dw, return false; } + // [TAG_ADAPTIVE_WIDTH] reduce the corrected logits to top-k ids + + // softmax probs on-device (~KB readback); failure just disables the + // candidate consumers (adaptive width) for this build. + if (all_corr != nullptr) { + cand_probs->assign((size_t)n_cand * (size_t)cand_k, 0.0f); + cand_ids->assign((size_t)n_cand * (size_t)cand_k, -1); + if (!ggml_backend_cuda_topk_rows(all_corr, cand_k, + cand_probs->data(), cand_ids->data())) { + cand_probs->clear(); + cand_ids->clear(); + } + } draft_tok.assign((size_t)q_len, 0); draft_tok[0] = last_tok; // One synchronize instead of n_cand blocking readbacks. diff --git a/server/src/common/domino_head.h b/server/src/common/domino_head.h index 9ce434ee8..78e75ab7f 100644 --- a/server/src/common/domino_head.h +++ b/server/src/common/domino_head.h @@ -20,7 +20,10 @@ bool domino_correct_greedy_chain_fused(const DraftWeights & dw, const float * local_hidden, int q_len, int32_t last_tok, - std::vector & draft_tok); + std::vector & draft_tok, + int cand_k = 0, + std::vector * cand_probs = nullptr, + std::vector * cand_ids = nullptr); bool domino_correct_greedy_chain(const DraftWeights & dw, ggml_backend_t backend, diff --git a/server/src/laguna/laguna_backend.cpp b/server/src/laguna/laguna_backend.cpp index 28c7a57fe..edec2b64e 100644 --- a/server/src/laguna/laguna_backend.cpp +++ b/server/src/laguna/laguna_backend.cpp @@ -30,6 +30,7 @@ #include "common/step_graph.h" #include "ggml-cuda.h" +#include "../common/adaptive_verify_width.h" #include "ggml-alloc.h" #include "common/snapshot_backend.h" @@ -718,6 +719,13 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, sizeof(float) * local_hidden.size()); if (step_prof) prof_draft_ms += prof_lap(); + // [TAG_ADAPTIVE_WIDTH] drafter top-2 candidate probabilities per + // slot, extracted on whichever head produces the draft tokens; used + // below to trim the verify batch per step. + std::vector cand_p; + std::vector cand_i; + const int cand_k = (adaptive_verify_width_theta() > 0.0f && + !sampled_verify && !args_.ddtree_mode && q_len >= 3) ? 2 : 0; bool used_domino = false; if (dw.domino.enabled && q_len > 1 && !sampled_verify && !args_.ddtree_mode) { static std::atomic s_domino_logged{false}; @@ -737,7 +745,9 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, if (domino_correct_greedy_chain_fused( dw, draft_backend_, target->lm_head_tensor(), target->gpu_embd_table(), local_hidden.data(), q_len, - last_tok, draft_tok)) { + last_tok, draft_tok, + cand_k, cand_k > 0 ? &cand_p : nullptr, + cand_k > 0 ? &cand_i : nullptr)) { used_domino = true; } } @@ -799,7 +809,9 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, } } if (!used_domino && !used_dspark) { - if (!target->project_hidden_to_tokens(local_hidden.data(), q_len, draft_tok)) { + if (!target->project_hidden_to_tokens_topk(local_hidden.data(), q_len, draft_tok, + cand_k, cand_k > 0 ? &cand_p : nullptr, + cand_k > 0 ? &cand_i : nullptr)) { std::fprintf(stderr, "[laguna-spec] projection failed\n"); step_graph_destroy(draft_sg); return false; @@ -926,6 +938,23 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, continue; } + // [TAG_ADAPTIVE_WIDTH] trim the verify batch where the drafter's own + // reach-mass says the tail rows almost never commit: every dropped + // row saves one MoE verify row (~1ms of expert reads for a Q4 target + // on a 3090). Output-exactness is preserved: only the number of + // speculated slots changes, every committed token is still verified. + if (cand_k > 0 && !cand_p.empty() && + cand_p.size() >= (size_t)(q_len - 1) * (size_t)cand_k) { + const int w_new = adaptive_verify_width(cand_p.data(), cand_k, q_len, + adaptive_verify_width_theta(), + adaptive_verify_width_min()); + if (w_new < q_len) { + q_len = w_new; + draft_tok.resize((size_t)q_len); + target_tok.resize((size_t)q_len); + } + } + int verify_last_tok = -1; if (step_prof) prof_lap(); if (!target->verify_batch(draft_tok, committed, verify_last_tok, &target_tok)) { diff --git a/server/src/laguna/laguna_dflash_target.cpp b/server/src/laguna/laguna_dflash_target.cpp index a7cda3720..855393f97 100644 --- a/server/src/laguna/laguna_dflash_target.cpp +++ b/server/src/laguna/laguna_dflash_target.cpp @@ -1,5 +1,7 @@ // LagunaDFlashTarget - DFlashTarget adapter for Poolside Laguna-XS.2. +#include "ggml-cuda.h" +#include #include "laguna_dflash_target.h" #include "../common/kvflash_pager.h" #include "../common/ddtree.h" @@ -590,6 +592,17 @@ bool LagunaDFlashTarget::project_hidden_to_tokens( return laguna_project_hidden(backend_, w_, hidden, n_tokens, tokens_out); } +bool LagunaDFlashTarget::project_hidden_to_tokens_topk( + const float * hidden, + int n_tokens, + std::vector & tokens_out, + int cand_k, + std::vector * cand_probs, + std::vector * cand_ids) { + return laguna_project_hidden(backend_, w_, hidden, n_tokens, tokens_out, + cand_k, cand_probs, cand_ids); +} + bool LagunaDFlashTarget::project_hidden_to_logits( const float * hidden, int n_tokens, @@ -675,6 +688,23 @@ bool LagunaDFlashTarget::project_hidden_to_topk( return false; } + // [TAG_TOPK_ROWS] GPU top-k on the resident logits: ~KB readback instead + // of the full vocab logits; falls through to the host path on failure or + // non-unit temperature (the kernel bakes softmax at T=1). + if (temperature == 1.0f && K <= 8) { + std::vector probs_gpu((size_t)n_tokens * (size_t)K); + std::vector ids_gpu((size_t)n_tokens * (size_t)K); + if (ggml_backend_cuda_topk_rows(logits, K, probs_gpu.data(), ids_gpu.data())) { + top_log_probs.resize((size_t)n_tokens * (size_t)K); + top_token_ids.assign(ids_gpu.begin(), ids_gpu.end()); + for (size_t z = 0; z < top_log_probs.size(); ++z) { + top_log_probs[z] = std::log(probs_gpu[z] > 1e-30f ? probs_gpu[z] : 1e-30f); + } + ggml_free(ctx); + return true; + } + } + // CPU top-K via extract_draft_topk (shared with qwen35). The GPU ggml_top_k // path bitonic-argsorts the full vocab, whose shared memory exceeds HIP // shared-mem-per-block on gfx1151 (argsort.cu GGML_ASSERT). extract_draft_topk diff --git a/server/src/laguna/laguna_dflash_target.h b/server/src/laguna/laguna_dflash_target.h index c83380955..361b4eeb2 100644 --- a/server/src/laguna/laguna_dflash_target.h +++ b/server/src/laguna/laguna_dflash_target.h @@ -65,6 +65,13 @@ class LagunaDFlashTarget : public DFlashTarget { int n_tokens, std::vector & logits_out) override; + bool project_hidden_to_tokens_topk(const float * hidden, + int n_tokens, + std::vector & tokens_out, + int cand_k, + std::vector * cand_probs, + std::vector * cand_ids) override; + bool project_hidden_to_topk(const float * hidden, int n_tokens, int K, diff --git a/server/src/laguna/laguna_internal.h b/server/src/laguna/laguna_internal.h index 2855d3a11..adf263fbf 100644 --- a/server/src/laguna/laguna_internal.h +++ b/server/src/laguna/laguna_internal.h @@ -364,7 +364,10 @@ bool laguna_project_hidden( const LagunaTargetWeights & w, const float * hidden, int n_tokens, - std::vector & out_tokens); + std::vector & out_tokens, + int cand_k = 0, // [TAG_ADAPTIVE_WIDTH] + std::vector * cand_probs = nullptr, + std::vector * cand_ids = nullptr); // Forward decl (full definition in common/moe_hybrid_storage.h). struct MoeHybridStorage; diff --git a/server/src/laguna/laguna_target_graph.cpp b/server/src/laguna/laguna_target_graph.cpp index 48ef55ba1..c1ca7239d 100644 --- a/server/src/laguna/laguna_target_graph.cpp +++ b/server/src/laguna/laguna_target_graph.cpp @@ -17,6 +17,7 @@ // is tested against our llama.cpp build_laguna (already verified to match HF // for 30+ tokens on B-tree prompt; see Lucebox/Laguna-XS.2-GGUF README). +#include #include "../common/mmid_adaptive_k.h" #include "laguna_internal.h" #include "../common/moe_hybrid_storage.h" @@ -1612,8 +1613,13 @@ bool laguna_verify_batch( const LagunaTargetCache * cache_id = nullptr; ggml_backend_buffer_t stage_buf = nullptr; }; - static thread_local VerifySlot g_slot_block, g_slot_bonus; - VerifySlot & S = (n_tokens == 1) ? g_slot_bonus : g_slot_block; + // [TAG_ADAPTIVE_WIDTH] one slot per verify row count: adaptive width + // flips n_tokens step to step, and each width needs its own arena so its + // node addresses stay stable and its captured CUDA graph replays instead + // of re-capturing on every flip. + static thread_local std::map g_slots_block; + static thread_local VerifySlot g_slot_bonus; + VerifySlot & S = (n_tokens == 1) ? g_slot_bonus : g_slots_block[n_tokens]; const bool want_logits = out_logits != nullptr; const bool want_feat = cache.target_feat && cache.target_feat_cap > 0; // Reuse requires the kv_idx input path (kv_pad > 0, no PAD_CPY): those @@ -1810,7 +1816,10 @@ bool laguna_project_hidden( const LagunaTargetWeights & w, const float * hidden, int n_tokens, - std::vector & out_tokens) + std::vector & out_tokens, + int cand_k, + std::vector * cand_probs, + std::vector * cand_ids) { if (n_tokens <= 0) return false; @@ -1823,10 +1832,18 @@ bool laguna_project_hidden( ggml_tensor * inp = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, w.n_embd, n_tokens); ggml_set_input(inp); - ggml_tensor * cur = ggml_mul_mat(ctx, w.output, inp); // [vocab, n_tokens] - cur = ggml_argmax(ctx, cur); // [n_tokens] + ggml_tensor * logits = ggml_mul_mat(ctx, w.output, inp); // [vocab, n_tokens] + ggml_tensor * cur = ggml_argmax(ctx, logits); // [n_tokens] ggml_set_output(cur); ggml_build_forward_expand(gf, cur); + // [TAG_ADAPTIVE_WIDTH] keep the logits alive for post-compute top-k + // candidate extraction (~KB readback, no extra projection). + const bool want_cand = + cand_k > 0 && cand_probs != nullptr && cand_ids != nullptr && n_tokens >= 2; + if (want_cand) { + ggml_set_output(logits); + ggml_build_forward_expand(gf, logits); + } static ggml_gallocr_t galloc_proj = nullptr; if (!galloc_proj) galloc_proj = ggml_gallocr_new(ggml_backend_get_default_buffer_type(backend)); @@ -1849,6 +1866,24 @@ bool laguna_project_hidden( ggml_backend_tensor_get(cur, out_tokens.data(), 0, sizeof(int32_t) * (size_t)n_tokens); + // [TAG_ADAPTIVE_WIDTH] slot j uses logits row j; row 0 is the seed slot + // and is dropped, so the outputs cover slots 1..n_tokens-1. + if (want_cand) { + std::vector p_all((size_t)n_tokens * (size_t)cand_k); + std::vector i_all((size_t)n_tokens * (size_t)cand_k); + if (ggml_backend_cuda_topk_rows(logits, cand_k, p_all.data(), i_all.data())) { + cand_probs->assign((size_t)(n_tokens - 1) * (size_t)cand_k, 0.0f); + cand_ids->assign((size_t)(n_tokens - 1) * (size_t)cand_k, -1); + for (size_t z = 0; z < cand_probs->size(); ++z) { + (*cand_probs)[z] = p_all[(size_t)cand_k + z]; + (*cand_ids)[z] = i_all[(size_t)cand_k + z]; + } + } else { + cand_probs->clear(); + cand_ids->clear(); + } + } + ggml_free(ctx); return true; } From 12386290f609722785bf4d61cc133b7ac742675f Mon Sep 17 00:00:00 2001 From: mrciffa <49000955+davide221@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:29:06 +0200 Subject: [PATCH 04/20] server: make validated MoE verify optimizations the default, single flag for the rest Collapse the env sprawl from the previous three commits into a clean surface: - Grouped MUL_MAT_ID: bit-exact, so ON by default on CUDA (HIP stays opt-in, unvalidated). DFLASH_MMID_GROUPED=0 is the kill switch; the type mask defaults to all validated types and is a debug override. - Adaptive verify width: output-exactness-preserving, so ON by default (theta 0.20, min 4). Greedy chains now default to a base width of 8 rows trimmed per step by drafter confidence; the legacy accept-EWMA AUTO remains the fallback for theta=0, sampled verify and --ddtree. - Adaptive expert count (the only output-changing knob): promoted from env to --adaptive-experts [tau] (default 0.80). Explicit env still wins for sweeps. Net: a default server run gets the exact optimizations with zero configuration; one documented flag opts into the near-lossless expert gating. --- .../deps/llama.cpp/ggml/src/ggml-cuda/mmvq.cu | 18 ++++++++++++++---- server/src/common/adaptive_verify_width.h | 5 +++-- server/src/laguna/laguna_backend.cpp | 11 +++++++++-- server/src/server/server_main.cpp | 11 ++++++++++- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/mmvq.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/mmvq.cu index f5bcd7b68..e897e501c 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/mmvq.cu +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/mmvq.cu @@ -269,20 +269,30 @@ struct mmid_gate_extra { #define MMID_GATE_MAGIC 0x4D474154u static bool mmid_grouped_env() { + // Bit-exact and measured equal-or-faster on small MoE verify batches, so + // enabled by default on CUDA; DFLASH_MMID_GROUPED=0 is the kill switch. + // HIP is unvalidated and stays opt-in (DFLASH_MMID_GROUPED=1). static const bool on = []() { const char * e = std::getenv("DFLASH_MMID_GROUPED"); - return e && e[0] == '1' && e[1] == '\0'; + if (e != nullptr) { + return e[0] == '1' && e[1] == '\0'; + } +#ifdef GGML_USE_HIP + return false; +#else + return true; +#endif }(); return on; } static bool mmid_grouped_type_ok(ggml_type type) { - // bit0 = Q4_K, bit1 = Q6_K, bit2 = Q4_0/Q8_0/Q5_K. Default Q4_K only: - // Q6_K keeps its tuned MMQ route above 5 tokens unless explicitly enabled. + // bit0 = Q4_K, bit1 = Q6_K, bit2 = Q4_0/Q8_0/Q5_K. Default: all validated + // types (7); DFLASH_MMID_GROUPED_TYPES is a debug override. static const int mask = []() { const char * e = std::getenv("DFLASH_MMID_GROUPED_TYPES"); if (e == nullptr || e[0] == '\0') { - return 1; + return 7; } return atoi(e); }(); diff --git a/server/src/common/adaptive_verify_width.h b/server/src/common/adaptive_verify_width.h index 6fac0fb15..43195d509 100644 --- a/server/src/common/adaptive_verify_width.h +++ b/server/src/common/adaptive_verify_width.h @@ -11,7 +11,8 @@ // above theta. Output-exactness is preserved: only the number of speculated // slots changes, every committed token is still target-verified. // -// DFLASH_ADAPTIVE_WIDTH_THETA=<0..1> enables (0/unset = off; ~0.20 typical) +// On by default (theta 0.20). Debug overrides: +// DFLASH_ADAPTIVE_WIDTH_THETA=<0..1> 0 disables (legacy fixed/EWMA width) // DFLASH_ADAPTIVE_WIDTH_MIN= minimum kept rows incl. seed (default 4) // // Model-agnostic: any family loop that has per-slot drafter top-1 @@ -22,7 +23,7 @@ inline float adaptive_verify_width_theta() { static const float theta = []() { const char * e = std::getenv("DFLASH_ADAPTIVE_WIDTH_THETA"); - return e ? (float) std::atof(e) : 0.0f; + return e ? (float) std::atof(e) : 0.20f; }(); return theta; } diff --git a/server/src/laguna/laguna_backend.cpp b/server/src/laguna/laguna_backend.cpp index edec2b64e..5c82afd7f 100644 --- a/server/src/laguna/laguna_backend.cpp +++ b/server/src/laguna/laguna_backend.cpp @@ -471,6 +471,7 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, // drafted block; the accept rule is unchanged, so this stays lossless. AUTO // tracks an EWMA of the accepted length (held constant per request so the // verify graph stays CUDA-graph-stable); --verify-width forces a fixed width. + const bool sampled_verify = laguna_sampled_verify_enabled(sampler_, true); int verify_width = args_.verify_width; if (const char * e = std::getenv("DFLASH_LAGUNA_VERIFY_WIDTH")) { const int w = std::atoi(e); @@ -487,8 +488,15 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, const int v = e ? std::atoi(e) : 3; return v > 0 ? v : 3; }(); + // [TAG_ADAPTIVE_WIDTH] default width policy: with the per-step + // drafter-confidence trim active (on by default for greedy chains), run + // from a base of 8 rows and let the trim shrink each step. The legacy + // accept-EWMA AUTO remains the fallback when the trim is off (theta 0) + // and for sampled verify, which has no candidate probabilities. + const bool width_trim = adaptive_verify_width_theta() > 0.0f && + !sampled_verify && !args_.ddtree_mode; int chain_w = adaptive_width - ? std::min((int)(spec_ewma_accept_ + 0.5) + 1, auto_w_max) + ? (width_trim ? 8 : std::min((int)(spec_ewma_accept_ + 0.5) + 1, auto_w_max)) : verify_width; if (chain_w < 2) chain_w = 2; if (chain_w > std::min(block_size, 8)) chain_w = std::min(block_size, 8); @@ -496,7 +504,6 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, const int base_q_len = args_.ddtree_mode ? block_size : chain_w; const bool ignore_eos = (std::getenv("DFLASH_IGNORE_EOS") != nullptr); - const bool sampled_verify = laguna_sampled_verify_enabled(sampler_, true); if (dflash_target_) { dflash_target_->set_keep_verify_logits(sampled_verify); } diff --git a/server/src/server/server_main.cpp b/server/src/server/server_main.cpp index 317fdbed7..1d5d3a2d3 100644 --- a/server/src/server/server_main.cpp +++ b/server/src/server/server_main.cpp @@ -227,7 +227,10 @@ static void print_usage(const char * prog) { " --no-fast-rollback Disable speculative fast rollback, even with --ddtree\n" " --ddtree Enable DDTree speculative decode\n" " --ddtree-budget DDTree budget (default: 22)\n" - " --verify-width laguna chain spec verify width (0=auto; default 0)\n" + " --verify-width laguna chain spec verify width (default: base 8,\n" + " trimmed per step by drafter confidence; N = fixed base)\n" + " --adaptive-experts [tau] MoE expert-count gating on verify batches\n" + " (near-lossless; default tau 0.80 when passed)\n" " --no-cors Disable CORS headers\n" " --think-max-tokens Phase-1 reasoning cap when a request opts in\n" " via thinking:{type:enabled} (default: 15488 =\n" @@ -435,6 +438,12 @@ int main(int argc, char ** argv) { bargs.fast_rollback = true; } else if (std::strcmp(argv[i], "--ddtree-budget") == 0 && i + 1 < argc) { bargs.ddtree_budget = std::atoi(argv[++i]); + } else if (std::strcmp(argv[i], "--adaptive-experts") == 0) { + const char * tau = "0.80"; + if (i + 1 < argc && argv[i + 1][0] != '-') { + tau = argv[++i]; + } + setenv("DFLASH_ADAPTIVE_K_TAU", tau, 0); // explicit env still wins } else if (std::strcmp(argv[i], "--verify-width") == 0 && i + 1 < argc) { bargs.verify_width = std::atoi(argv[++i]); } else if (std::strcmp(argv[i], "--no-fast-rollback") == 0) { From 4a4d519d5facd34d6a3bc1d7989dd9e70a0a9c93 Mon Sep 17 00:00:00 2001 From: mrciffa <49000955+davide221@users.noreply.github.com> Date: Wed, 8 Jul 2026 22:32:32 +0200 Subject: [PATCH 05/20] fix(draft): load and apply Laguna XS 2.1 drafter tensors (attn gates, aux hidden norms, context-KV norms) The official Laguna XS 2.1 DFlash drafter carries per-layer attention gates (blk..attn_gate.weight), per-capture-layer aux hidden norms (dflash.aux_hidden_norm..weight) and a context-KV layer input norm flag. The draft loader on main silently ignored all three, so the drafter ran without its feature norms: the fc projection overflowed in f16 and every draft hidden state came out NaN, killing spec decode on any main build (laguna_verify_batch: embed failed on token id -1 from an all-NaN argmax). Port the loader + draft-graph support so main can run the official drafter. --- server/src/draft/draft_gguf_loader.cpp | 96 +++++++++ server/src/draft/draft_graph.cpp | 270 ++++++++++++++++--------- server/src/internal.h | 4 + 3 files changed, 274 insertions(+), 96 deletions(-) diff --git a/server/src/draft/draft_gguf_loader.cpp b/server/src/draft/draft_gguf_loader.cpp index 2694636ec..657eb0b5c 100644 --- a/server/src/draft/draft_gguf_loader.cpp +++ b/server/src/draft/draft_gguf_loader.cpp @@ -17,6 +17,7 @@ // blk..attn_k.weight [kv_dim, hidden] Q8_0 / F16 // blk..attn_v.weight [kv_dim, hidden] Q8_0 / F16 // blk..attn_output.weight [hidden, q_dim] Q8_0 / F16 +// blk..attn_gate.weight [n_head, hidden] optional Laguna XS 2.1 gate // blk..attn_q_norm.weight [head_dim] F32 // blk..attn_k_norm.weight [head_dim] F32 // blk..ffn_gate.weight [intermediate, hidden] Q8_0 / F16 @@ -64,6 +65,14 @@ int count_swa_layers(const DraftWeights & w) { return n_swa; } +int count_attn_gate_layers(const DraftWeights & w) { + int n_gate = 0; + for (const DraftLayer & layer : w.layers) { + if (layer.attn_gate) n_gate++; + } + return n_gate; +} + bool check_shape_1d(const ggml_tensor * t, int64_t ne0, const char * name, char * buf, size_t buf_sz) { if (!t || t->ne[0] != ne0) { std::snprintf(buf, buf_sz, "draft GGUF: Domino tensor %s shape mismatch: got [%lld], expected [%lld]", @@ -256,6 +265,20 @@ bool load_draft_gguf(const std::string & path, return false; } + out.aux_hidden_norms.clear(); + { + int aux_count = 0; + for (;;) { + char aux_name[128]; + std::snprintf(aux_name, sizeof(aux_name), + "dflash.aux_hidden_norm.%d.weight", aux_count); + ggml_tensor * t = g(aux_name); + if (!t) break; + out.aux_hidden_norms.push_back(t); + aux_count++; + } + } + for (int il = 0; il < out.n_layer; il++) { char name[128]; auto fnd = [&](const char * suffix) -> ggml_tensor * { @@ -270,6 +293,7 @@ bool load_draft_gguf(const std::string & path, L.wk = fnd("attn_k.weight"); L.wv = fnd("attn_v.weight"); L.wo = fnd("attn_output.weight"); + L.attn_gate = fnd("attn_gate.weight"); L.q_norm = fnd("attn_q_norm.weight"); L.k_norm = fnd("attn_k_norm.weight"); L.w_gate = fnd("ffn_gate.weight"); @@ -285,6 +309,17 @@ bool load_draft_gguf(const std::string & path, } } + const int n_gate_layers = count_attn_gate_layers(out); + if (n_gate_layers != 0 && n_gate_layers != out.n_layer) { + char b[160]; + std::snprintf(b, sizeof(b), + "draft GGUF: incomplete attention gate tensors: %d/%d layers", + n_gate_layers, out.n_layer); + set_last_error(b); + gguf_free(gctx); + return false; + } + out.domino = DraftDominoWeights{}; out.domino.start = g("dflash.domino.start"); out.domino.gru_w_ih = g("dflash.domino.gru.weight_ih"); @@ -417,6 +452,8 @@ bool load_draft_gguf(const std::string & path, std::fprintf(stderr, "[draft GGUF] SWA layers: %d/%d (window=%d)\n", n_swa, out.n_layer, out.swa_window); } + const bool meta_context_kv_layer_norm = + read_u32("dflash.context_kv_layer_norm", 0) != 0; // ── 3. Allocate CUDA buffer for all tensors ────────────────────────── out.buf = ggml_backend_alloc_ctx_tensors(meta_ctx, backend); @@ -504,6 +541,65 @@ bool load_draft_gguf(const std::string & path, } } } + if (!out.aux_hidden_norms.empty()) { + const int64_t derived_fc_in = out.fc->ne[0]; + const int derived_layers = + (out.n_embd > 0 && derived_fc_in % out.n_embd == 0) + ? (int)(derived_fc_in / out.n_embd) + : -1; + if ((int)out.aux_hidden_norms.size() != derived_layers) { + char buf[256]; + std::snprintf(buf, sizeof(buf), + "draft GGUF: aux hidden norms count %zu != fc-derived capture count %d", + out.aux_hidden_norms.size(), derived_layers); + set_last_error(buf); + return false; + } + for (size_t i = 0; i < out.aux_hidden_norms.size(); i++) { + const ggml_tensor * t = out.aux_hidden_norms[i]; + if (!t || t->ne[0] != exp_n_embd) { + char buf[256]; + std::snprintf(buf, sizeof(buf), + "draft GGUF: aux hidden norm %zu shape mismatch: got [%lld], expected [%lld]", + i, t ? (long long)t->ne[0] : -1LL, (long long)exp_n_embd); + set_last_error(buf); + return false; + } + } + std::fprintf(stderr, "[draft GGUF] aux hidden norms enabled: %zu capture layers\n", + out.aux_hidden_norms.size()); + } + if (n_gate_layers > 0) { + const int64_t exp_q_dim = (int64_t)out.n_head * out.head_dim; + for (int il = 0; il < out.n_layer; il++) { + DraftLayer & L = out.layers[il]; + const int64_t gate_in = L.attn_gate->ne[0]; + const int64_t gate_out = L.attn_gate->ne[1]; + if (gate_in != exp_n_embd || + (gate_out != out.n_head && gate_out != exp_q_dim)) { + char buf[256]; + std::snprintf(buf, sizeof(buf), + "draft GGUF: blk.%d attn_gate.weight shape mismatch: got [%lld,%lld], expected [%lld,%d] or [%lld,%lld]", + il, (long long)gate_in, (long long)gate_out, + (long long)exp_n_embd, out.n_head, + (long long)exp_n_embd, (long long)exp_q_dim); + set_last_error(buf); + return false; + } + L.attn_gate_per_head = gate_out == out.n_head; + } + std::fprintf(stderr, "[draft GGUF] attention gate enabled: %d/%d layers (%s)\n", + n_gate_layers, out.n_layer, + out.layers[0].attn_gate_per_head ? "per-head" : "per-channel"); + } + out.context_kv_layer_norm = + meta_context_kv_layer_norm || + n_gate_layers > 0 || + !out.aux_hidden_norms.empty(); + if (out.context_kv_layer_norm) { + std::fprintf(stderr, + "[draft GGUF] context K/V layer input norm enabled\n"); + } } char summary[192]; diff --git a/server/src/draft/draft_graph.cpp b/server/src/draft/draft_graph.cpp index e8dcd4460..95a53c8d9 100644 --- a/server/src/draft/draft_graph.cpp +++ b/server/src/draft/draft_graph.cpp @@ -14,6 +14,7 @@ // (the draft has no lm_head of its own, it shares the target's). // // Semantics: +// - optional per-capture RMSNorm on target_hidden_cat slices // - fc @ target_hidden_cat -> rms_norm with hidden_norm -> target_feat // - Per layer: // h_norm = rms_norm(h) * input_layernorm @@ -24,6 +25,7 @@ // V = concat[V_ctx, V_noi] // RoPE(Q, positions_q); RoPE(K, positions_k) (NEOX style) // attn = flash_attn_ext(Q, K, V, mask, scale) SWA=causal, full=non-causal +// optional Laguna XS 2.1 gate: attn *= softplus(attn_gate @ h_norm) // h += wo @ attn // h_norm = rms_norm(h) * post_attention_layernorm // h += w_down @ (silu(w_gate @ h_norm) * (w_up @ h_norm)) @@ -32,7 +34,9 @@ #include "internal.h" #include "draft_graph.h" +#include #include +#include namespace dflash::common { @@ -53,7 +57,33 @@ DraftGraphOutputs build_draft_graph( // fc: [5*hidden, hidden] (ggml: ne[0]=5*hidden, ne[1]=hidden) // target_hidden_cat: [5*hidden, ctx_len, 1] // Result: [hidden, ctx_len, 1] - ggml_tensor * target_feat = ggml_mul_mat(ctx, w.fc, in.target_hidden_cat); + ggml_tensor * target_hidden_cat = in.target_hidden_cat; + static const bool disable_aux_hidden_norms = + std::getenv("DFLASH_DISABLE_DRAFT_AUX_NORMS") != nullptr; + static const bool disable_attn_gate = + std::getenv("DFLASH_DISABLE_DRAFT_ATTN_GATE") != nullptr; + static const bool disable_swa = + std::getenv("DFLASH_DISABLE_DRAFT_SWA") != nullptr; + static const bool disable_attn = + std::getenv("DFLASH_DISABLE_DRAFT_ATTN") != nullptr; + static const bool disable_ffn = + std::getenv("DFLASH_DISABLE_DRAFT_FFN") != nullptr; + + if (!disable_aux_hidden_norms && !w.aux_hidden_norms.empty()) { + ggml_tensor * aux_cat = nullptr; + const size_t elem_sz = ggml_element_size(in.target_hidden_cat); + for (size_t i = 0; i < w.aux_hidden_norms.size(); i++) { + ggml_tensor * slice = ggml_view_3d(ctx, in.target_hidden_cat, + w.n_embd, ctx_len, 1, + in.target_hidden_cat->nb[1], in.target_hidden_cat->nb[2], + i * (size_t)w.n_embd * elem_sz); + slice = ggml_rms_norm(ctx, slice, eps); + slice = ggml_mul(ctx, slice, w.aux_hidden_norms[i]); + aux_cat = aux_cat ? ggml_concat(ctx, aux_cat, slice, 0) : slice; + } + target_hidden_cat = aux_cat; + } + ggml_tensor * target_feat = ggml_mul_mat(ctx, w.fc, target_hidden_cat); target_feat = ggml_rms_norm(ctx, target_feat, eps); target_feat = ggml_mul (ctx, target_feat, w.hidden_norm); ggml_set_name(target_feat, "target_feat"); @@ -63,109 +93,157 @@ DraftGraphOutputs build_draft_graph( for (int il = 0; il < w.n_layer; il++) { const DraftLayer & L = w.layers[il]; + char probe_name[64]; // ── SWA: determine effective context for this layer - const bool use_swa = L.is_swa && w.swa_window > 0 && ctx_len > w.swa_window; + const bool layer_is_swa = L.is_swa && !disable_swa; + const bool use_swa = layer_is_swa && w.swa_window > 0 && ctx_len > w.swa_window; const int eff_ctx = use_swa ? w.swa_window : ctx_len; const int eff_total_k = eff_ctx + q_len; const int ctx_offset = use_swa ? (ctx_len - w.swa_window) : 0; - // ── 2a. Attention pre-norm - ggml_tensor * hn = ggml_rms_norm(ctx, h, eps); - hn = ggml_mul(ctx, hn, L.attn_norm); - - // ── 2b. Q from noise only, then per-head RMSNorm - // wq: [hidden, q_dim=4096] - ggml_tensor * Q = ggml_mul_mat(ctx, L.wq, hn); // [q_dim, q_len, 1] - Q = ggml_reshape_3d(ctx, Q, head_dim, n_head, q_len); // [head_dim, n_head, q_len] - Q = ggml_rms_norm(ctx, Q, eps); // normalize along head_dim - Q = ggml_mul (ctx, Q, L.q_norm); // broadcast [head_dim] - - // ── 2c. K and V from target_feat AND noise, then concat along sequence - // wk, wv: [hidden, kv_dim=1024] - // For SWA layers: window target_feat to last swa_window positions. - ggml_tensor * tf = target_feat; - if (use_swa) { - tf = ggml_view_3d(ctx, target_feat, - w.n_embd, eff_ctx, 1, - target_feat->nb[1], target_feat->nb[2], - target_feat->nb[1] * ctx_offset); + if (!disable_attn) { + // ── 2a. Attention pre-norm + ggml_tensor * hn = ggml_rms_norm(ctx, h, eps); + hn = ggml_mul(ctx, hn, L.attn_norm); + std::snprintf(probe_name, sizeof(probe_name), "draft_l%d_hn", il); + ggml_set_name(hn, probe_name); + + // ── 2b. Q from noise only, then per-head RMSNorm + // wq: [hidden, q_dim=4096] + ggml_tensor * Q = ggml_mul_mat(ctx, L.wq, hn); // [q_dim, q_len, 1] + Q = ggml_reshape_3d(ctx, Q, head_dim, n_head, q_len); // [head_dim, n_head, q_len] + Q = ggml_rms_norm(ctx, Q, eps); // normalize along head_dim + Q = ggml_mul (ctx, Q, L.q_norm); // broadcast [head_dim] + std::snprintf(probe_name, sizeof(probe_name), "draft_l%d_Q_norm", il); + ggml_set_name(Q, probe_name); + + // ── 2c. K and V from target_feat AND noise, then concat along sequence + // wk, wv: [hidden, kv_dim=1024] + // For SWA layers: window target_feat to last swa_window positions. + ggml_tensor * tf = target_feat; + if (use_swa) { + tf = ggml_view_3d(ctx, target_feat, + w.n_embd, eff_ctx, 1, + target_feat->nb[1], target_feat->nb[2], + target_feat->nb[1] * ctx_offset); + } + ggml_tensor * tf_kv = tf; + if (w.context_kv_layer_norm) { + tf_kv = ggml_rms_norm(ctx, tf_kv, eps); + tf_kv = ggml_mul(ctx, tf_kv, L.attn_norm); + } + ggml_tensor * Kctx = ggml_mul_mat(ctx, L.wk, tf_kv); // [kv_dim, eff_ctx, 1] + ggml_tensor * Kn = ggml_mul_mat(ctx, L.wk, hn); // [kv_dim, q_len, 1] + ggml_tensor * Vctx = ggml_mul_mat(ctx, L.wv, tf_kv); + ggml_tensor * Vn = ggml_mul_mat(ctx, L.wv, hn); + std::snprintf(probe_name, sizeof(probe_name), "draft_l%d_Kctx", il); + ggml_set_name(Kctx, probe_name); + std::snprintf(probe_name, sizeof(probe_name), "draft_l%d_Kn", il); + ggml_set_name(Kn, probe_name); + std::snprintf(probe_name, sizeof(probe_name), "draft_l%d_Vctx", il); + ggml_set_name(Vctx, probe_name); + std::snprintf(probe_name, sizeof(probe_name), "draft_l%d_Vn", il); + ggml_set_name(Vn, probe_name); + + // concat along ne[1] (sequence) — ggml_concat second arg dim=1 + ggml_tensor * K = ggml_concat(ctx, Kctx, Kn, 1); // [kv_dim, eff_total_k, 1] + ggml_tensor * V = ggml_concat(ctx, Vctx, Vn, 1); + + // Per-head k_norm + K = ggml_reshape_3d(ctx, K, head_dim, n_kv, eff_total_k); + K = ggml_rms_norm(ctx, K, eps); + K = ggml_mul (ctx, K, L.k_norm); + std::snprintf(probe_name, sizeof(probe_name), "draft_l%d_K_norm", il); + ggml_set_name(K, probe_name); + + V = ggml_reshape_3d(ctx, V, head_dim, n_kv, eff_total_k); + std::snprintf(probe_name, sizeof(probe_name), "draft_l%d_V", il); + ggml_set_name(V, probe_name); + + // ── 2d. RoPE (NEOX, theta=10M) + // Q: positions_q [q_len] values [ctx_len..ctx_len+q_len-1] + // K: positions_k [eff_total_k] — for SWA, starts from ctx_offset + ggml_tensor * pk = in.positions_k; + if (use_swa) { + pk = ggml_view_1d(ctx, in.positions_k, eff_total_k, + ctx_offset * ggml_element_size(in.positions_k)); + } + Q = ggml_rope_ext(ctx, Q, in.positions_q, /*freq_factors=*/nullptr, + head_dim, GGML_ROPE_TYPE_NEOX, /*n_ctx_orig=*/0, + rope_base, /*freq_scale=*/1.0f, + /*ext_factor=*/0.0f, /*attn_factor=*/1.0f, + /*beta_fast=*/0.0f, /*beta_slow=*/0.0f); + K = ggml_rope_ext(ctx, K, pk, nullptr, + head_dim, GGML_ROPE_TYPE_NEOX, 0, + rope_base, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); + + // ── 2e. Permute into the layout flash_attn_ext wants + // q: [n_embd_k=head_dim, n_batch=q_len, n_head, ne3] + // k: [n_embd_k=head_dim, n_kv=eff_total_k, n_head_kv, ne3] + // v: [n_embd_v=head_dim, n_kv=eff_total_k, n_head_kv, ne3] (not transposed) + Q = ggml_permute(ctx, Q, 0, 2, 1, 3); // [head_dim, q_len, n_head, 1] + Q = ggml_cont (ctx, Q); + K = ggml_permute(ctx, K, 0, 2, 1, 3); // [head_dim, eff_total_k, n_kv, 1] + K = ggml_cont (ctx, K); + V = ggml_permute(ctx, V, 0, 2, 1, 3); // [head_dim, eff_total_k, n_kv, 1] + V = ggml_cont (ctx, V); + + // ── 2f. Attention: causal for SWA layers, non-causal for full layers. + const float scale = 1.0f / std::sqrt((float)head_dim); + ggml_tensor * mask = layer_is_swa + ? (in.causal_mask_swa ? in.causal_mask_swa : nullptr) + : in.pad_mask_full; + ggml_tensor * attn = ggml_flash_attn_ext(ctx, Q, K, V, mask, + scale, /*max_bias=*/0.0f, + /*logit_softcap=*/0.0f); + std::snprintf(probe_name, sizeof(probe_name), "draft_l%d_attn", il); + ggml_set_name(attn, probe_name); + // attn result: [n_embd_v=head_dim, n_head, n_batch=q_len, 1] + if (!disable_attn_gate && L.attn_gate) { + ggml_tensor * gate = ggml_mul_mat(ctx, L.attn_gate, hn); // [n_head|q_dim, q_len] + gate = ggml_softplus(ctx, gate); + std::snprintf(probe_name, sizeof(probe_name), "draft_l%d_gate", il); + ggml_set_name(gate, probe_name); + if (L.attn_gate_per_head) { + gate = ggml_reshape_3d(ctx, gate, 1, n_head, q_len); + } else { + gate = ggml_reshape_3d(ctx, gate, head_dim, n_head, q_len); + } + gate = ggml_cast(ctx, gate, attn->type); + attn = ggml_mul(ctx, attn, gate); + } + attn = ggml_reshape_2d(ctx, attn, head_dim * n_head, q_len); + // attn: [q_dim, q_len] + + // ── 2g. Output projection + residual + // wo: [q_dim, hidden] (ne[0]=q_dim, ne[1]=hidden) + ggml_tensor * attn_out = ggml_mul_mat(ctx, L.wo, attn); // [hidden, q_len] + std::snprintf(probe_name, sizeof(probe_name), "draft_l%d_attn_out", il); + ggml_set_name(attn_out, probe_name); + h = ggml_add(ctx, h, attn_out); + std::snprintf(probe_name, sizeof(probe_name), "draft_l%d_h_after_attn", il); + ggml_set_name(h, probe_name); } - ggml_tensor * Kctx = ggml_mul_mat(ctx, L.wk, tf); // [kv_dim, eff_ctx, 1] - ggml_tensor * Kn = ggml_mul_mat(ctx, L.wk, hn); // [kv_dim, q_len, 1] - ggml_tensor * Vctx = ggml_mul_mat(ctx, L.wv, tf); - ggml_tensor * Vn = ggml_mul_mat(ctx, L.wv, hn); - - // concat along ne[1] (sequence) — ggml_concat second arg dim=1 - ggml_tensor * K = ggml_concat(ctx, Kctx, Kn, 1); // [kv_dim, eff_total_k, 1] - ggml_tensor * V = ggml_concat(ctx, Vctx, Vn, 1); - - // Per-head k_norm - K = ggml_reshape_3d(ctx, K, head_dim, n_kv, eff_total_k); - K = ggml_rms_norm(ctx, K, eps); - K = ggml_mul (ctx, K, L.k_norm); - - V = ggml_reshape_3d(ctx, V, head_dim, n_kv, eff_total_k); - - // ── 2d. RoPE (NEOX, theta=10M) - // Q: positions_q [q_len] values [ctx_len..ctx_len+q_len-1] - // K: positions_k [eff_total_k] — for SWA, starts from ctx_offset - ggml_tensor * pk = in.positions_k; - if (use_swa) { - pk = ggml_view_1d(ctx, in.positions_k, eff_total_k, - ctx_offset * ggml_element_size(in.positions_k)); + + if (!disable_ffn) { + // ── 2h. FFN pre-norm + ggml_tensor * hf = ggml_rms_norm(ctx, h, eps); + hf = ggml_mul(ctx, hf, L.ffn_norm); + + // ── 2i. SwiGLU: down(silu(gate(x)) * up(x)) + // w_gate, w_up: [hidden, intermediate] + // w_down: [intermediate, hidden] + ggml_tensor * g = ggml_mul_mat(ctx, L.w_gate, hf); // [inter, q_len] + g = ggml_silu(ctx, g); + ggml_tensor * u = ggml_mul_mat(ctx, L.w_up, hf); // [inter, q_len] + ggml_tensor * gu = ggml_mul(ctx, g, u); + ggml_tensor * ffn_out = ggml_mul_mat(ctx, L.w_down, gu); // [hidden, q_len] + + h = ggml_add(ctx, h, ffn_out); + std::snprintf(probe_name, sizeof(probe_name), "draft_l%d_h_after_ffn", il); + ggml_set_name(h, probe_name); } - Q = ggml_rope_ext(ctx, Q, in.positions_q, /*freq_factors=*/nullptr, - head_dim, GGML_ROPE_TYPE_NEOX, /*n_ctx_orig=*/0, - rope_base, /*freq_scale=*/1.0f, - /*ext_factor=*/0.0f, /*attn_factor=*/1.0f, - /*beta_fast=*/0.0f, /*beta_slow=*/0.0f); - K = ggml_rope_ext(ctx, K, pk, nullptr, - head_dim, GGML_ROPE_TYPE_NEOX, 0, - rope_base, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); - - // ── 2e. Permute into the layout flash_attn_ext wants - // q: [n_embd_k=head_dim, n_batch=q_len, n_head, ne3] - // k: [n_embd_k=head_dim, n_kv=eff_total_k, n_head_kv, ne3] - // v: [n_embd_v=head_dim, n_kv=eff_total_k, n_head_kv, ne3] (not transposed) - Q = ggml_permute(ctx, Q, 0, 2, 1, 3); // [head_dim, q_len, n_head, 1] - Q = ggml_cont (ctx, Q); - K = ggml_permute(ctx, K, 0, 2, 1, 3); // [head_dim, eff_total_k, n_kv, 1] - K = ggml_cont (ctx, K); - V = ggml_permute(ctx, V, 0, 2, 1, 3); // [head_dim, eff_total_k, n_kv, 1] - V = ggml_cont (ctx, V); - - // ── 2f. Attention: causal for SWA layers, non-causal for full layers. - const float scale = 1.0f / std::sqrt((float)head_dim); - ggml_tensor * mask = L.is_swa - ? (in.causal_mask_swa ? in.causal_mask_swa : nullptr) - : in.pad_mask_full; - ggml_tensor * attn = ggml_flash_attn_ext(ctx, Q, K, V, mask, - scale, /*max_bias=*/0.0f, - /*logit_softcap=*/0.0f); - // attn result: [n_embd_v=head_dim, n_head, n_batch=q_len, 1] - attn = ggml_reshape_2d(ctx, attn, head_dim * n_head, q_len); - // attn: [q_dim, q_len] - - // ── 2g. Output projection + residual - // wo: [q_dim, hidden] (ne[0]=q_dim, ne[1]=hidden) - ggml_tensor * attn_out = ggml_mul_mat(ctx, L.wo, attn); // [hidden, q_len] - h = ggml_add(ctx, h, attn_out); - - // ── 2h. FFN pre-norm - ggml_tensor * hf = ggml_rms_norm(ctx, h, eps); - hf = ggml_mul(ctx, hf, L.ffn_norm); - - // ── 2i. SwiGLU: down(silu(gate(x)) * up(x)) - // w_gate, w_up: [hidden, intermediate] - // w_down: [intermediate, hidden] - ggml_tensor * g = ggml_mul_mat(ctx, L.w_gate, hf); // [inter, q_len] - g = ggml_silu(ctx, g); - ggml_tensor * u = ggml_mul_mat(ctx, L.w_up, hf); // [inter, q_len] - ggml_tensor * gu = ggml_mul(ctx, g, u); - ggml_tensor * ffn_out = ggml_mul_mat(ctx, L.w_down, gu); // [hidden, q_len] - - h = ggml_add(ctx, h, ffn_out); } // ── 3. Final norm diff --git a/server/src/internal.h b/server/src/internal.h index 9cb36b11e..ea43482fc 100644 --- a/server/src/internal.h +++ b/server/src/internal.h @@ -240,12 +240,14 @@ struct DraftLayer { ggml_tensor * wk; ggml_tensor * wv; ggml_tensor * wo; + ggml_tensor * attn_gate = nullptr; // optional Laguna XS 2.1 attention gate ggml_tensor * q_norm; ggml_tensor * k_norm; ggml_tensor * w_gate; ggml_tensor * w_up; ggml_tensor * w_down; bool is_swa = false; // true for SWA layers (Qwen3.6 pattern) + bool attn_gate_per_head = false; }; struct DraftDominoWeights { @@ -284,6 +286,8 @@ struct DraftWeights { ggml_tensor * fc = nullptr; // [5*hidden, hidden] ggml_tensor * hidden_norm = nullptr; // [hidden] + std::vector aux_hidden_norms; // optional [hidden] per captured target layer + bool context_kv_layer_norm = false; // Laguna DFlash: per-layer input norm before context K/V std::vector layers; // size = n_layer ggml_tensor * out_norm = nullptr; // [hidden] From 61b6abd44357eeeb91188dfa84f50f7c7749c694 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Thu, 9 Jul 2026 13:42:36 +0200 Subject: [PATCH 06/20] perf(spec): device-resident domino head, persistent draft graph, drafter requant tool - fused Domino head reads the draft graph's device hidden in place (hidden_dev param); the 64KB D2H hidden readback is now lazy and only paid by fallback heads / ddtree / sampled-verify paths - build_draft_step skips the per-step rebuild while ctx stays inside the same 64-aligned pad bucket (copy-mode builds only); laguna passes copy-mode by default, kill with DFLASH_DRAFT_PERSIST=0 - scripts/requant_dflash_draft.py: GGUF->GGUF drafter requant (q8/q4), metadata verbatim; q4 drafter measured +3% end-to-end on Laguna XS 2.1 (3090), acceptance unchanged - drafter is latency-bound, not bandwidth-bound - step-prof: commit/build laps close the loop-top blind spot --- server/scripts/requant_dflash_draft.py | 102 +++++++++++++++++++++++ server/src/common/dflash_draft_graph.cpp | 40 +++++++++ server/src/common/domino_head.cpp | 25 ++++-- server/src/common/domino_head.h | 6 +- server/src/laguna/laguna_backend.cpp | 52 +++++++++--- 5 files changed, 207 insertions(+), 18 deletions(-) create mode 100644 server/scripts/requant_dflash_draft.py diff --git a/server/scripts/requant_dflash_draft.py b/server/scripts/requant_dflash_draft.py new file mode 100644 index 000000000..86e80ec73 --- /dev/null +++ b/server/scripts/requant_dflash_draft.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +""" +Requantize a DFlash drafter GGUF in place of llama-quantize (which does not +know the drafter arch). Big 2D projection weights are re-encoded; norms, +gates, aux heads and all metadata are copied verbatim, so any drafter GGUF +the engine loads today keeps loading. + +Verification keeps the committed text greedy-exact regardless of drafter +precision; the only quantity at risk is acceptance length. Measured on +Laguna XS 2.1 + official DFlash (RTX 3090, HE screen): q4 drafter = same +acceptance within noise, ~+3% end-to-end (the drafter is latency-bound, +not bandwidth-bound, so gains are modest). + +Usage: + python3 requant_dflash_draft.py IN.gguf OUT.gguf [--variant q8|q4] + + q8: all projection mats Q8_0 (~2x smaller, lossless in practice) + q4: projection mats Q4_0, dflash.fc kept Q8_0 (~3.4x smaller; fc is the + feature projection with known overflow sensitivity, keep it higher) +""" +import argparse +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "deps" / "llama.cpp" / "gguf-py")) + +import gguf +from gguf import GGUFReader, GGUFWriter, GGMLQuantizationType +from gguf.quants import dequantize, quantize + +QUANT_SUBSTR = ("attn_q.weight", "attn_k.weight", "attn_v.weight", + "attn_output.weight", "ffn_gate.weight", "ffn_up.weight", + "ffn_down.weight", "dflash.fc.weight") + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("src") + ap.add_argument("dst") + ap.add_argument("--variant", choices=("q8", "q4"), default="q4") + args = ap.parse_args() + + r = GGUFReader(args.src) + arch = None + for f in r.fields.values(): + if f.name == "general.architecture": + arch = str(bytes(f.parts[f.data[0]]), "utf-8") + if not arch: + sys.exit("no general.architecture in source gguf") + w = GGUFWriter(args.dst, arch) + + skip = {"general.architecture", "GGUF.version", "GGUF.tensor_count", "GGUF.kv_count"} + for f in r.fields.values(): + if f.name in skip: + continue + ftype = f.types[0] + if ftype == gguf.GGUFValueType.STRING: + w.add_string(f.name, str(bytes(f.parts[f.data[0]]), "utf-8")) + elif ftype == gguf.GGUFValueType.ARRAY: + itype = f.types[1] + if itype == gguf.GGUFValueType.STRING: + vals = [str(bytes(f.parts[i]), "utf-8") for i in f.data] + else: + vals = [f.parts[i].tolist()[0] for i in f.data] + w.add_array(f.name, vals) + else: + val = f.parts[f.data[0]].tolist()[0] + adders = { + gguf.GGUFValueType.UINT8: w.add_uint8, gguf.GGUFValueType.INT8: w.add_int8, + gguf.GGUFValueType.UINT16: w.add_uint16, gguf.GGUFValueType.INT16: w.add_int16, + gguf.GGUFValueType.UINT32: w.add_uint32, gguf.GGUFValueType.INT32: w.add_int32, + gguf.GGUFValueType.UINT64: w.add_uint64, gguf.GGUFValueType.INT64: w.add_int64, + gguf.GGUFValueType.FLOAT32: w.add_float32, gguf.GGUFValueType.FLOAT64: w.add_float64, + gguf.GGUFValueType.BOOL: w.add_bool, + } + adders[ftype](f.name, val) + + n_q = 0 + for t in r.tensors: + shape = [int(x) for x in t.shape] + do_q = (len(shape) == 2 and any(s in t.name for s in QUANT_SUBSTR) + and shape[0] % 256 == 0) + if do_q: + f32 = dequantize(t.data, t.tensor_type).reshape(shape[::-1]) + qt = GGMLQuantizationType.Q8_0 + if args.variant == "q4" and "dflash.fc" not in t.name: + qt = GGMLQuantizationType.Q4_0 + w.add_tensor(t.name, quantize(f32, qt), raw_dtype=qt) + n_q += 1 + else: + w.add_tensor(t.name, t.data, raw_dtype=t.tensor_type) + + w.write_header_to_file() + w.write_kv_data_to_file() + w.write_tensors_to_file() + w.close() + print(f"requantized {n_q} tensors -> {args.dst} " + f"({Path(args.dst).stat().st_size / 1e6:.0f} MB)") + + +if __name__ == "__main__": + main() diff --git a/server/src/common/dflash_draft_graph.cpp b/server/src/common/dflash_draft_graph.cpp index da2920988..c957bbb8a 100644 --- a/server/src/common/dflash_draft_graph.cpp +++ b/server/src/common/dflash_draft_graph.cpp @@ -145,6 +145,46 @@ bool build_draft_step( int committed, int /*ctx_len_max*/, bool pad_ctx) { + // [TAG_FUSED_LOOP] persistent-graph fast path: when the previous build + // used the padded COPY mode (no mirror view baked into the topology) and + // ctx_len still lands in the same 64-aligned bucket, every tensor address + // and the graph topology are identical — skip the rebuild and only + // refresh the two ctx_len-dependent masks. Feature contents, positions + // and noise embeds are re-uploaded by the caller every step regardless, + // and the pad feature rows beyond ctx_len were zeroed at bucket build. + if (sg.gf && sg.ctx_alloc > 0 && !mirror && pad_ctx && + sg.ctx_alloc == ((ctx_len + 63) & ~63)) { + const int q_len = dw.block_size; + const int ctx_alloc = sg.ctx_alloc; + static constexpr uint16_t ZERO = 0x0000; + static constexpr uint16_t NEG_INF = 0xFC00; + if (sg.pad_mask_full) { + const int kv_pad = mask_align_up(ctx_alloc + q_len, MASK_KV_PAD); + std::vector mask_data((size_t)kv_pad * q_len, NEG_INF); + for (int q = 0; q < q_len; q++) { + for (int k = 0; k < ctx_len; k++) + mask_data[(size_t)q * kv_pad + k] = ZERO; + for (int j = 0; j < q_len; j++) + mask_data[(size_t)q * kv_pad + (ctx_alloc + j)] = ZERO; + } + ggml_backend_tensor_set(sg.pad_mask_full, mask_data.data(), 0, + sizeof(uint16_t) * mask_data.size()); + } + if (sg.attn_mask) { + const int kv_pad = mask_align_up(ctx_alloc + q_len, MASK_KV_PAD); + std::vector mask_data((size_t)kv_pad * q_len, NEG_INF); + for (int q = 0; q < q_len; q++) { + for (int k = 0; k < ctx_len; k++) + mask_data[(size_t)q * kv_pad + k] = ZERO; + for (int j = 0; j <= q; j++) + mask_data[(size_t)q * kv_pad + (ctx_alloc + j)] = ZERO; + } + ggml_backend_tensor_set(sg.attn_mask, mask_data.data(), 0, + sizeof(uint16_t) * mask_data.size()); + } + return true; + } + step_graph_free(sg); if (!sg.alloc) { diff --git a/server/src/common/domino_head.cpp b/server/src/common/domino_head.cpp index 6d02d393f..7e01d2fb7 100644 --- a/server/src/common/domino_head.cpp +++ b/server/src/common/domino_head.cpp @@ -190,11 +190,16 @@ bool domino_correct_greedy_chain_fused(const DraftWeights & dw, std::vector & draft_tok, int cand_k, std::vector * cand_probs, - std::vector * cand_ids) { - if (!dw.domino.enabled || q_len <= 1 || !local_hidden || + std::vector * cand_ids, + ggml_tensor * hidden_dev) { + if (!dw.domino.enabled || q_len <= 1 || (!local_hidden && !hidden_dev) || !backend || !lm_head || !embd_table) { return false; } + // [TAG_FUSED_LOOP] device path needs enough rows in the draft output + if (hidden_dev && (hidden_dev->ne[0] != dw.n_embd || hidden_dev->ne[1] < q_len)) { + return false; + } const int hidden = dw.n_embd; const int H = dw.domino.gru_hidden_dim; const int E = dw.domino.emb_dim; @@ -227,9 +232,15 @@ bool domino_correct_greedy_chain_fused(const DraftWeights & dw, if (!ctx) return false; ggml_cgraph * gf = ggml_new_graph_custom(ctx, 1024, false); - ggml_tensor * inp_hidden = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, hidden, n_cand); + // [TAG_FUSED_LOOP] with hidden_dev, read candidate rows 1..q_len-1 of the + // draft graph's device-resident hidden in place (same backend/stream, so + // ordering is guaranteed and the arena address is stable across steps). + ggml_tensor * inp_hidden = hidden_dev + ? ggml_view_2d(ctx, hidden_dev, hidden, n_cand, + hidden_dev->nb[1], hidden_dev->nb[1]) + : ggml_new_tensor_2d(ctx, GGML_TYPE_F32, hidden, n_cand); ggml_tensor * inp_seed = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, 1); - ggml_set_input(inp_hidden); + if (!hidden_dev) ggml_set_input(inp_hidden); ggml_set_input(inp_seed); // Base logits for every candidate in one matmul: [vocab, n_cand]. @@ -311,8 +322,10 @@ bool domino_correct_greedy_chain_fused(const DraftWeights & dw, return false; } - ggml_backend_tensor_set(inp_hidden, local_hidden + (size_t)hidden, 0, - sizeof(float) * (size_t)hidden * (size_t)n_cand); + if (!hidden_dev) { + ggml_backend_tensor_set(inp_hidden, local_hidden + (size_t)hidden, 0, + sizeof(float) * (size_t)hidden * (size_t)n_cand); + } ggml_backend_tensor_set(inp_seed, &last_tok, 0, sizeof(int32_t)); if (ggml_backend_graph_compute(backend, gf) != GGML_STATUS_SUCCESS) { diff --git a/server/src/common/domino_head.h b/server/src/common/domino_head.h index 78e75ab7f..d5d4ba9fb 100644 --- a/server/src/common/domino_head.h +++ b/server/src/common/domino_head.h @@ -13,6 +13,9 @@ namespace dflash::common { // token feedback. Requires the target to expose its lm_head and a GPU (f16) // token-embedding table. Runs on a dedicated CUDA backend instance so the // ggml-cuda graph cache can replay it across steps. +// [TAG_FUSED_LOOP] When hidden_dev is set (the draft graph's device-resident +// hidden_states on the same backend), the head reads rows 1..q_len-1 in place +// and local_hidden may be null: no D2H hidden readback, no H2D re-upload. bool domino_correct_greedy_chain_fused(const DraftWeights & dw, ggml_backend_t backend, ggml_tensor * lm_head, @@ -23,7 +26,8 @@ bool domino_correct_greedy_chain_fused(const DraftWeights & dw, std::vector & draft_tok, int cand_k = 0, std::vector * cand_probs = nullptr, - std::vector * cand_ids = nullptr); + std::vector * cand_ids = nullptr, + ggml_tensor * hidden_dev = nullptr); bool domino_correct_greedy_chain(const DraftWeights & dw, ggml_backend_t backend, diff --git a/server/src/laguna/laguna_backend.cpp b/server/src/laguna/laguna_backend.cpp index 5c82afd7f..3d0dd7da3 100644 --- a/server/src/laguna/laguna_backend.cpp +++ b/server/src/laguna/laguna_backend.cpp @@ -626,6 +626,10 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, auto t_dec0 = std::chrono::steady_clock::now(); static const bool step_prof = std::getenv("DFLASH_LAGUNA_STEP_PROF") != nullptr; double prof_draft_ms = 0.0, prof_heads_ms = 0.0, prof_verify_ms = 0.0; + // [TAG_FUSED_LOOP] blind-spot laps: commit = verify-end -> loop-top + // (accept/commit/emit/feature-sync), build = loop-top -> draft-input + // upload (noise embed on host, build_draft_step, feature copy). + double prof_commit_ms = 0.0, prof_build_ms = 0.0; auto prof_now = std::chrono::steady_clock::now(); auto prof_lap = [&]() { auto t = std::chrono::steady_clock::now(); @@ -635,6 +639,7 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, }; while (n_generated < n_gen) { + if (step_prof) prof_commit_ms += prof_lap(); // [TAG_FUSED_LOOP] int q_len = base_q_len; draft_tok.resize((size_t)q_len); target_tok.resize((size_t)q_len); @@ -685,8 +690,17 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, const char * e = std::getenv("DFLASH_LAGUNA_DRAFT_PAD"); return !(e && e[0] == '0' && e[1] == '\0'); }(); + // [TAG_FUSED_LOOP] persistent draft graph: force the feature-COPY + // build (D2D peer copy, ~0.1ms) so the topology carries no per-step + // ring-view offsets and build_draft_step can skip the rebuild while + // ctx stays inside the same 64-aligned bucket. Kill: DFLASH_DRAFT_PERSIST=0. + static const bool draft_persist = []() { + const char * e = std::getenv("DFLASH_DRAFT_PERSIST"); + return !(e && e[0] == '0' && e[1] == '\0'); + }(); + const bool want_view = use_mirror_view && !draft_persist; if (!build_draft_step(draft_sg, dw, /*lm_head=*/nullptr, draft_backend_, - draft_ctx, use_mirror_view ? &feature_mirror_ : nullptr, + draft_ctx, want_view ? &feature_mirror_ : nullptr, committed, std::min(ring_cap, std::max(DRAFT_CTX_MAX_DEFAULT, args_.draft_ctx_max)), draft_pad)) { @@ -694,7 +708,7 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, step_graph_destroy(draft_sg); return false; } - if (!use_mirror_view && + if (!want_view && !copy_feature_ring_range_to_tensor(feature_mirror_, draft_sg.target_hidden_cat, draft_start, draft_ctx)) { std::fprintf(stderr, "[laguna-spec] feature copy failed\n"); @@ -709,7 +723,7 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, for (int i = 0; i < block_size; i++) pos_q[(size_t)i] = draft_ctx + i; for (int i = 0; i < kctx; i++) pos_k[(size_t)i] = (i < draft_ctx) ? i : 0; for (int j = 0; j < block_size; j++) pos_k[(size_t)kctx + j] = draft_ctx + j; - if (step_prof) prof_lap(); + if (step_prof) prof_build_ms += prof_lap(); // [TAG_FUSED_LOOP] ggml_backend_tensor_set(draft_sg.positions, pos_q.data(), 0, sizeof(int32_t) * pos_q.size()); ggml_backend_tensor_set(draft_sg.positions_k, pos_k.data(), 0, @@ -721,9 +735,18 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, return false; } - local_hidden.resize((size_t)hidden * (size_t)q_len); - ggml_backend_tensor_get(draft_sg.hidden_states, local_hidden.data(), 0, - sizeof(float) * local_hidden.size()); + // [TAG_FUSED_LOOP] the 64KB draft-hidden D2H readback is lazy: the + // shipping path (fused Domino) reads the device tensor in place, so + // only fallback heads and ddtree/dspark pay for the transfer. + bool hidden_on_host = false; + auto fetch_hidden = [&]() { + if (hidden_on_host) return; + local_hidden.resize((size_t)hidden * (size_t)q_len); + ggml_backend_tensor_get(draft_sg.hidden_states, local_hidden.data(), 0, + sizeof(float) * local_hidden.size()); + hidden_on_host = true; + }; + if (args_.ddtree_mode || sampled_verify) fetch_hidden(); if (step_prof) prof_draft_ms += prof_lap(); // [TAG_ADAPTIVE_WIDTH] drafter top-2 candidate probabilities per @@ -749,18 +772,21 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, if (fused_domino) { // Run on the draft backend: same stream as the draft forward, // second graph key in the multi-key CUDA-graph cache. + // [TAG_FUSED_LOOP] pass the device hidden; no D2H/H2D hop. if (domino_correct_greedy_chain_fused( dw, draft_backend_, target->lm_head_tensor(), - target->gpu_embd_table(), local_hidden.data(), q_len, + target->gpu_embd_table(), nullptr, q_len, last_tok, draft_tok, cand_k, cand_k > 0 ? &cand_p : nullptr, - cand_k > 0 ? &cand_i : nullptr)) { + cand_k > 0 ? &cand_i : nullptr, + draft_sg.hidden_states)) { used_domino = true; } } if (used_domino) { // fused path done - } else if (domino_correct_greedy_chain(dw, draft_backend_, *target, + } else if (fetch_hidden(), + domino_correct_greedy_chain(dw, draft_backend_, *target, local_hidden.data(), q_len, last_tok, draft_tok)) { used_domino = true; @@ -788,6 +814,7 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, return !(e && e[0] == '0' && e[1] == '\0'); }(); bool ds_ok = false; + fetch_hidden(); // [TAG_FUSED_LOOP] if (fused_dspark && laguna_dspark_confidence_threshold() <= 0.0f) { // One graph on the draft stream: lm_head + markov chain + // in-graph argmax; no host logits round-trip. @@ -816,6 +843,7 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, } } if (!used_domino && !used_dspark) { + fetch_hidden(); // [TAG_FUSED_LOOP] if (!target->project_hidden_to_tokens_topk(local_hidden.data(), q_len, draft_tok, cand_k, cand_k > 0 ? &cand_p : nullptr, cand_k > 0 ? &cand_i : nullptr)) { @@ -1110,10 +1138,12 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, if (step_prof && n_draft_steps > 0) { std::fprintf(stderr, "[step-prof] per-step ms: draft=%.2f heads=%.2f verify=%.2f " - "other=%.2f total=%.2f (steps=%d)\n", + "commit=%.2f build=%.2f other=%.2f total=%.2f (steps=%d)\n", prof_draft_ms / n_draft_steps, prof_heads_ms / n_draft_steps, prof_verify_ms / n_draft_steps, - (decode_s * 1000.0 - prof_draft_ms - prof_heads_ms - prof_verify_ms) / n_draft_steps, + prof_commit_ms / n_draft_steps, prof_build_ms / n_draft_steps, + (decode_s * 1000.0 - prof_draft_ms - prof_heads_ms - prof_verify_ms - + prof_commit_ms - prof_build_ms) / n_draft_steps, decode_s * 1000.0 / n_draft_steps, n_draft_steps); } std::fprintf(stderr, "[laguna-spec] tokens=%d time=%.3f s speed=%.2f tok/s " From 746a81b452d245407cbae80de235bedc5f6f0b6e Mon Sep 17 00:00:00 2001 From: mrciffa <49000955+davide221@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:08:54 +0200 Subject: [PATCH 07/20] feat(spec): drafter context-KV ring cache (target-agnostic) DFlash drafters re-encoded their whole feature window (up to draft_ctx_max tokens of target hidden states) every step: ~10ms/step once the window fills, on every model family, previously hidden behind slower targets. common/dflash_draft_kv.{h,cpp}: per-layer head-major F16 ring caches (slot = pos % cap) with absolute-position RoPE, so entries stay valid as the window slides. One fixed-topology step graph (fold-in append of newly committed rows via set_rows with a trash slot for pads, then the noise forward flash-attending over the ring) built once, CUDA-graph replayed forever; bulk append after prefill. All inputs live in a dedicated buffer so gallocr never recycles them. draft/draft_graph.cpp: build_draft_kv_append/build_draft_kv_step next to the legacy one-shot builder, sharing draft_fuse_features. The per-position k_norm and RoPE are separable, so append/step splitting matches the legacy concat math exactly (f16 storage is the only numeric difference). Any backend adopts it with ~20 lines: draft_kv_init once, then per step draft_kv_begin_step + upload noise embed + compute. Measured (laguna XS 2.1, RTX 3090): draft step 9.9 -> 2.4ms, flat at any context; acceptance parity. --- server/CMakeLists.txt | 1 + server/src/common/dflash_draft_kv.cpp | 308 ++++++++++++++++++++++++++ server/src/common/dflash_draft_kv.h | 97 ++++++++ server/src/draft/draft_graph.cpp | 236 ++++++++++++++++++-- server/src/draft/draft_graph.h | 55 +++++ 5 files changed, 679 insertions(+), 18 deletions(-) create mode 100644 server/src/common/dflash_draft_kv.cpp create mode 100644 server/src/common/dflash_draft_kv.h diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 805d91c09..aa251a349 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -280,6 +280,7 @@ add_library(dflash_common STATIC src/common/dflash_draft_ipc_daemon.cpp src/common/pflash_drafter_ipc.cpp src/common/dflash_draft_graph.cpp + src/common/dflash_draft_kv.cpp src/common/dflash_spec_decode.cpp src/common/layer_split_backend.cpp src/common/layer_split_runtime.cpp diff --git a/server/src/common/dflash_draft_kv.cpp b/server/src/common/dflash_draft_kv.cpp new file mode 100644 index 000000000..564a5941c --- /dev/null +++ b/server/src/common/dflash_draft_kv.cpp @@ -0,0 +1,308 @@ +#include "dflash_draft_kv.h" + +#include +#include +#include +#include + +namespace dflash::common { + +static constexpr int MASK_KV_PAD = 32; +static inline int mask_align_up(int x, int a) { return ((x + a - 1) / a) * a; } + +static constexpr uint16_t F16_ZERO = 0x0000; +static constexpr uint16_t F16_NEG_INF = 0xFC00; + +bool draft_kv_init(DraftKvState & st, + const DraftWeights & dw, + ggml_backend_t backend, + int cap, + ggml_tensor * lm_head) { + if (cap <= 0 || dw.block_size <= 0) return false; + static const bool disable_swa = + std::getenv("DFLASH_DISABLE_DRAFT_SWA") != nullptr; + + st.cap = cap; + st.q_len = dw.block_size; + st.a_step = 2 * dw.block_size + 2; + st.trash_slot = cap + dw.block_size; + st.kv_total = mask_align_up(cap + dw.block_size + 1, MASK_KV_PAD); + st.fc_in = dw.n_target_layers * dw.n_embd; + st.any_full = st.any_swa = false; + for (int i = 0; i < dw.n_layer; i++) { + if (dw.layers[i].is_swa && !disable_swa) st.any_swa = true; + else st.any_full = true; + } + + // ── persistent memory: caches + inputs (outside gallocr, stable, zeroed) + const size_t n_mem_tensors = 2 * (size_t)dw.n_layer + 10; + ggml_init_params ip{}; + ip.mem_size = ggml_tensor_overhead() * n_mem_tensors; + ip.no_alloc = true; + st.mem_ctx = ggml_init(ip); + if (!st.mem_ctx) return false; + + const int64_t kv_row = (int64_t)dw.head_dim * dw.n_head_kv; + st.cache.kv_total = st.kv_total; + st.cache.k.resize(dw.n_layer); + st.cache.v.resize(dw.n_layer); + for (int il = 0; il < dw.n_layer; il++) { + st.cache.k[il] = ggml_new_tensor_2d(st.mem_ctx, GGML_TYPE_F16, kv_row, st.kv_total); + st.cache.v[il] = ggml_new_tensor_2d(st.mem_ctx, GGML_TYPE_F16, kv_row, st.kv_total); + } + st.inp_embed = ggml_new_tensor_2d(st.mem_ctx, GGML_TYPE_F32, dw.n_embd, st.q_len); + st.pos_q = ggml_new_tensor_1d(st.mem_ctx, GGML_TYPE_I32, st.q_len); + st.noise_rows = ggml_new_tensor_1d(st.mem_ctx, GGML_TYPE_I32, st.q_len); + if (st.any_full) + st.mask_full = ggml_new_tensor_2d(st.mem_ctx, GGML_TYPE_F16, st.kv_total, st.q_len); + if (st.any_swa) + st.mask_swa = ggml_new_tensor_2d(st.mem_ctx, GGML_TYPE_F16, st.kv_total, st.q_len); + st.ap_feat = ggml_new_tensor_2d(st.mem_ctx, GGML_TYPE_F32, st.fc_in, st.a_step); + st.ap_pos = ggml_new_tensor_1d(st.mem_ctx, GGML_TYPE_I32, st.a_step); + st.ap_rows = ggml_new_tensor_1d(st.mem_ctx, GGML_TYPE_I32, st.a_step); + + st.mem_buf = ggml_backend_alloc_ctx_tensors(st.mem_ctx, backend); + if (!st.mem_buf) { + std::fprintf(stderr, "[draft-kv] cache alloc failed\n"); + return false; + } + // Zero everything: empty/pad cache slots are read by FA (masked -inf) and + // must be finite; pad feature rows must be finite for the trash-slot rows. + ggml_backend_buffer_clear(st.mem_buf, 0); + + // ── build the fixed-topology step graph once + const size_t arena_sz = 16u * 1024 * 1024; + st.meta_arena.resize(arena_sz); + ggml_init_params gp{}; + gp.mem_size = st.meta_arena.size(); + gp.mem_buffer = st.meta_arena.data(); + gp.no_alloc = true; + st.g_ctx = ggml_init(gp); + if (!st.g_ctx) return false; + st.gf = ggml_new_graph_custom(st.g_ctx, 4096, false); + + DraftKvAppendInputs ai{}; + ai.n_rows = st.a_step; + ai.feat = st.ap_feat; + ai.positions = st.ap_pos; + ai.rows = st.ap_rows; + if (!build_draft_kv_append(st.g_ctx, st.gf, dw, st.cache, ai)) return false; + + DraftKvStepInputs si{}; + si.noise_embed = st.inp_embed; + si.positions_q = st.pos_q; + si.noise_rows = st.noise_rows; + si.mask_full = st.mask_full; + si.mask_swa = st.mask_swa; + si.lm_head = lm_head; + DraftGraphOutputs go = build_draft_kv_step(st.g_ctx, st.gf, dw, st.cache, si); + if (!go.hidden_states) return false; + st.hidden_states = go.hidden_states; + st.logits = go.logits; + ggml_set_output(st.hidden_states); + ggml_build_forward_expand(st.gf, st.hidden_states); + if (st.logits) { + ggml_set_output(st.logits); + ggml_build_forward_expand(st.gf, st.logits); + } + + st.galloc = ggml_gallocr_new(ggml_backend_get_default_buffer_type(backend)); + if (!st.galloc || !ggml_gallocr_alloc_graph(st.galloc, st.gf)) { + std::fprintf(stderr, "[draft-kv] graph alloc failed\n"); + return false; + } + + // static noise scratch slots + std::vector nrows((size_t)st.q_len); + for (int i = 0; i < st.q_len; i++) nrows[(size_t)i] = st.cap + i; + ggml_backend_tensor_set(st.noise_rows, nrows.data(), 0, + sizeof(int32_t) * nrows.size()); + + st.built_for = &dw; + st.slot_pos.assign((size_t)st.cap, -1); + st.next_pos = 0; + std::fprintf(stderr, + "[draft-kv] ctx-KV ring active: cap=%d kv_total=%d a_step=%d " + "layers=%d f16 cache %.1f MiB\n", + st.cap, st.kv_total, st.a_step, dw.n_layer, + (double)(2ull * dw.n_layer * (size_t)kv_row * st.kv_total * 2) / (1024.0 * 1024.0)); + return true; +} + +void draft_kv_reset(DraftKvState & st) { + std::fill(st.slot_pos.begin(), st.slot_pos.end(), -1); + st.next_pos = 0; +} + +void draft_kv_free(DraftKvState & st) { + if (st.galloc) { ggml_gallocr_free(st.galloc); st.galloc = nullptr; } + if (st.g_ctx) { ggml_free(st.g_ctx); st.g_ctx = nullptr; } + st.gf = nullptr; + if (st.mem_buf) { ggml_backend_buffer_free(st.mem_buf); st.mem_buf = nullptr; } + if (st.mem_ctx) { ggml_free(st.mem_ctx); st.mem_ctx = nullptr; } + st.meta_arena.clear(); + st.meta_arena.shrink_to_fit(); + st.hidden_states = st.logits = nullptr; + st.cache.k.clear(); + st.cache.v.clear(); + st.slot_pos.clear(); + st.next_pos = 0; + st.built_for = nullptr; +} + +// One-shot append of [start, start+n) via temporary exact-size graphs +// (used to fill the window after prefill or a rewind; runs once per request). +static bool draft_kv_bulk_append(DraftKvState & st, + const DraftWeights & dw, + ggml_backend_t backend, + const DraftFeatureMirror & ring, + int start, int n) { + constexpr int A_BULK = 1024; + std::vector pos, rows; + while (n > 0) { + const int c = std::min(n, A_BULK); + ggml_init_params tp{}; + tp.mem_size = ggml_tensor_overhead() * 8; + tp.no_alloc = true; + ggml_context * tctx = ggml_init(tp); + if (!tctx) return false; + ggml_tensor * feat = ggml_new_tensor_2d(tctx, GGML_TYPE_F32, st.fc_in, c); + ggml_tensor * tpos = ggml_new_tensor_1d(tctx, GGML_TYPE_I32, c); + ggml_tensor * trow = ggml_new_tensor_1d(tctx, GGML_TYPE_I32, c); + ggml_backend_buffer_t tbuf = ggml_backend_alloc_ctx_tensors(tctx, backend); + if (!tbuf) { ggml_free(tctx); return false; } + + bool ok = copy_feature_ring_range_to_tensor(ring, feat, start, c); + if (ok) { + pos.resize((size_t)c); + rows.resize((size_t)c); + for (int i = 0; i < c; i++) { + const int p = start + i; + pos[(size_t)i] = p; + rows[(size_t)i] = p % st.cap; + st.slot_pos[(size_t)(p % st.cap)] = p; + } + ggml_backend_tensor_set(tpos, pos.data(), 0, sizeof(int32_t) * pos.size()); + ggml_backend_tensor_set(trow, rows.data(), 0, sizeof(int32_t) * rows.size()); + + std::vector arena(16u * 1024 * 1024); + ggml_init_params gp{}; + gp.mem_size = arena.size(); + gp.mem_buffer = arena.data(); + gp.no_alloc = true; + ggml_context * gctx = ggml_init(gp); + ok = gctx != nullptr; + if (ok) { + ggml_cgraph * g = ggml_new_graph_custom(gctx, 4096, false); + DraftKvAppendInputs ai{c, feat, tpos, trow}; + ok = build_draft_kv_append(gctx, g, dw, st.cache, ai); + if (ok) { + ggml_gallocr_t ga = + ggml_gallocr_new(ggml_backend_get_default_buffer_type(backend)); + ok = ga && ggml_gallocr_alloc_graph(ga, g) && + ggml_backend_graph_compute(backend, g) == GGML_STATUS_SUCCESS; + if (ga) ggml_gallocr_free(ga); + } + ggml_free(gctx); + } + } + ggml_backend_buffer_free(tbuf); + ggml_free(tctx); + if (!ok) return false; + start += c; + n -= c; + } + return true; +} + +bool draft_kv_begin_step(DraftKvState & st, + const DraftWeights & dw, + ggml_backend_t backend, + const DraftFeatureMirror & ring, + int committed) { + if (!st.gf || committed <= 0) return false; + // Rewind (prefix-cache restore / new shorter request): stale slots would + // shadow live window positions, so rebuild from scratch. + if (st.next_pos > committed) draft_kv_reset(st); + + const int win = std::min(committed, st.cap); + const int lo = committed - win; + int64_t start = std::max(st.next_pos, lo); + int n_new = (int)(committed - start); + if (n_new > st.a_step) { + if (!draft_kv_bulk_append(st, dw, backend, ring, (int)start, n_new)) { + std::fprintf(stderr, "[draft-kv] bulk append failed\n"); + return false; + } + start = committed; + n_new = 0; + } + + // fold-in append inputs: real rows then trash-slot pads + st.i32_hbuf.assign((size_t)st.a_step * 2, 0); + int32_t * ap_pos = st.i32_hbuf.data(); + int32_t * ap_rows = st.i32_hbuf.data() + st.a_step; + for (int i = 0; i < st.a_step; i++) { + if (i < n_new) { + const int p = (int)start + i; + ap_pos[i] = p; + ap_rows[i] = p % st.cap; + st.slot_pos[(size_t)(p % st.cap)] = p; + } else { + ap_pos[i] = 0; + ap_rows[i] = st.trash_slot; + } + } + if (n_new > 0 && + !copy_feature_ring_range_to_tensor(ring, st.ap_feat, (int)start, n_new)) { + std::fprintf(stderr, "[draft-kv] feature copy failed\n"); + return false; + } + ggml_backend_tensor_set(st.ap_pos, ap_pos, 0, sizeof(int32_t) * (size_t)st.a_step); + ggml_backend_tensor_set(st.ap_rows, ap_rows, 0, sizeof(int32_t) * (size_t)st.a_step); + st.next_pos = committed; + + // noise positions (absolute) + std::vector pq((size_t)st.q_len); + for (int i = 0; i < st.q_len; i++) pq[(size_t)i] = committed + i; + ggml_backend_tensor_set(st.pos_q, pq.data(), 0, sizeof(int32_t) * pq.size()); + + // masks: ctx columns share one row template; noise columns differ only + // in causality (full = block-visible, SWA = causal). + const size_t mask_elems = (size_t)st.kv_total * st.q_len; + if (st.mask_full) { + st.mask_hbuf.assign(mask_elems, F16_NEG_INF); + for (int s = 0; s < st.cap; s++) { + const int32_t p = st.slot_pos[(size_t)s]; + if (p >= lo && p < committed) st.mask_hbuf[(size_t)s] = F16_ZERO; + } + for (int j = 0; j < st.q_len; j++) + st.mask_hbuf[(size_t)(st.cap + j)] = F16_ZERO; + for (int q = 1; q < st.q_len; q++) + std::memcpy(st.mask_hbuf.data() + (size_t)q * st.kv_total, + st.mask_hbuf.data(), sizeof(uint16_t) * (size_t)st.kv_total); + ggml_backend_tensor_set(st.mask_full, st.mask_hbuf.data(), 0, + sizeof(uint16_t) * mask_elems); + } + if (st.mask_swa) { + const int eff_win = (dw.swa_window > 0 && win > dw.swa_window) + ? dw.swa_window : win; + const int swa_lo = committed - eff_win; + st.mask_hbuf.assign(mask_elems, F16_NEG_INF); + for (int s = 0; s < st.cap; s++) { + const int32_t p = st.slot_pos[(size_t)s]; + if (p >= swa_lo && p < committed) st.mask_hbuf[(size_t)s] = F16_ZERO; + } + for (int q = 1; q < st.q_len; q++) + std::memcpy(st.mask_hbuf.data() + (size_t)q * st.kv_total, + st.mask_hbuf.data(), sizeof(uint16_t) * (size_t)st.kv_total); + for (int q = 0; q < st.q_len; q++) + for (int j = 0; j <= q; j++) + st.mask_hbuf[(size_t)q * st.kv_total + (size_t)(st.cap + j)] = F16_ZERO; + ggml_backend_tensor_set(st.mask_swa, st.mask_hbuf.data(), 0, + sizeof(uint16_t) * mask_elems); + } + return true; +} + +} // namespace dflash::common diff --git a/server/src/common/dflash_draft_kv.h b/server/src/common/dflash_draft_kv.h new file mode 100644 index 000000000..e40872a56 --- /dev/null +++ b/server/src/common/dflash_draft_kv.h @@ -0,0 +1,97 @@ +// dflash_draft_kv.h — drafter context-KV ring cache (target-agnostic). +// +// The DFlash drafter recomputes K/V over its whole feature window (up to +// draft_ctx_max tokens) every step, which dominates draft latency once the +// window fills (~10ms at 2048 on RTX 3090 vs ~3ms on short prompts). The +// window is append-only per commit, so this module keeps per-layer K/V ring +// caches on the draft backend and only computes the newly committed rows. +// +// One fixed-topology step graph is built at init: a fold-in append stage +// (up to a_step new rows per step, padded rows land in a trash slot) followed +// by the noise-token forward flash-attending over the cache. Every input +// tensor lives in a dedicated backend buffer (never gallocr-recycled), so the +// graph replays as a CUDA graph indefinitely — no per-step rebuilds at all. +// +// RoPE uses absolute positions; scores depend only on position differences, +// so results match the legacy rebased-window math while cached entries stay +// valid as the window slides (ring slot = position % cap). + +#pragma once + +#include "dflash_feature_ring.h" +#include "draft/draft_graph.h" +#include "internal.h" // DraftWeights + +#include "ggml.h" +#include "ggml-alloc.h" +#include "ggml-backend.h" + +#include +#include + +namespace dflash::common { + +struct DraftKvState { + // geometry + int cap = 0; // ctx ring slots (== drafter feature window cap) + int q_len = 0; // noise block size + int kv_total = 0; // cache rows: cap + q_len + trash + mask alignment + int a_step = 0; // fold-in append capacity per step + int trash_slot = 0; // destination for padded append rows + int fc_in = 0; + bool any_full = false, any_swa = false; + + // persistent device memory: caches + every graph input. + ggml_context * mem_ctx = nullptr; + ggml_backend_buffer_t mem_buf = nullptr; + DraftKvCacheRefs cache; + ggml_tensor * inp_embed = nullptr; // [hidden, q_len] f32 (caller fills) + ggml_tensor * pos_q = nullptr; // [q_len] i32 + ggml_tensor * noise_rows = nullptr; // [q_len] i32 (static) + ggml_tensor * mask_full = nullptr; // [kv_total, q_len] f16 + ggml_tensor * mask_swa = nullptr; // [kv_total, q_len] f16 + ggml_tensor * ap_feat = nullptr; // [fc_in, a_step] f32 + ggml_tensor * ap_pos = nullptr; // [a_step] i32 + ggml_tensor * ap_rows = nullptr; // [a_step] i32 + + // the once-built step graph (fold-in append + noise forward) + std::vector meta_arena; + ggml_context * g_ctx = nullptr; + ggml_cgraph * gf = nullptr; + ggml_gallocr_t galloc = nullptr; + ggml_tensor * hidden_states = nullptr; + ggml_tensor * logits = nullptr; // iff lm_head passed at init + + // host bookkeeping + const void * built_for = nullptr; // DraftWeights the graph was built against + int64_t next_pos = 0; // first ctx position not yet appended + std::vector slot_pos; // absolute position per ring slot, -1 empty + std::vector mask_hbuf; + std::vector i32_hbuf; +}; + +// Allocate caches + inputs and build the step graph. `cap` is the drafter +// feature-window capacity (ring size). lm_head may be null (hidden-only). +bool draft_kv_init(DraftKvState & st, + const DraftWeights & dw, + ggml_backend_t backend, + int cap, + ggml_tensor * lm_head); + +// Invalidate all cached rows (new/rewound request). Cheap; the next +// begin_step bulk-appends the live window from the feature ring. +void draft_kv_reset(DraftKvState & st); + +void draft_kv_free(DraftKvState & st); + +// Bring the cache up to date with `committed` (bulk-appending from the +// feature ring if more than a_step rows are missing) and fill all step +// inputs (append rows, positions, masks). After this the caller uploads +// inp_embed and computes st.gf; the draft hidden is st.hidden_states. +bool draft_kv_begin_step(DraftKvState & st, + const DraftWeights & dw, + ggml_backend_t backend, + const DraftFeatureMirror & ring, + int committed); + +} // namespace dflash::common diff --git a/server/src/draft/draft_graph.cpp b/server/src/draft/draft_graph.cpp index 95a53c8d9..472c214c9 100644 --- a/server/src/draft/draft_graph.cpp +++ b/server/src/draft/draft_graph.cpp @@ -40,6 +40,38 @@ namespace dflash::common { +// Feature fusion shared by the legacy one-shot graph and the cached-KV +// builders: optional per-capture RMSNorm slices, fc projection, hidden_norm. +// Row-independent, so it is bit-identical whether run over the full window +// or over appended rows only. +static ggml_tensor * draft_fuse_features( + ggml_context * ctx, + const DraftWeights & w, + ggml_tensor * target_hidden_cat, + int n_rows, + bool disable_aux_hidden_norms) { + const float eps = DFLASH27B_RMS_EPS; + ggml_tensor * thc = target_hidden_cat; + if (!disable_aux_hidden_norms && !w.aux_hidden_norms.empty()) { + ggml_tensor * aux_cat = nullptr; + const size_t elem_sz = ggml_element_size(target_hidden_cat); + for (size_t i = 0; i < w.aux_hidden_norms.size(); i++) { + ggml_tensor * slice = ggml_view_3d(ctx, target_hidden_cat, + w.n_embd, n_rows, 1, + target_hidden_cat->nb[1], target_hidden_cat->nb[2], + i * (size_t)w.n_embd * elem_sz); + slice = ggml_rms_norm(ctx, slice, eps); + slice = ggml_mul(ctx, slice, w.aux_hidden_norms[i]); + aux_cat = aux_cat ? ggml_concat(ctx, aux_cat, slice, 0) : slice; + } + thc = aux_cat; + } + ggml_tensor * target_feat = ggml_mul_mat(ctx, w.fc, thc); + target_feat = ggml_rms_norm(ctx, target_feat, eps); + target_feat = ggml_mul (ctx, target_feat, w.hidden_norm); + return target_feat; +} + DraftGraphOutputs build_draft_graph( ggml_context * ctx, const DraftWeights & w, @@ -57,7 +89,6 @@ DraftGraphOutputs build_draft_graph( // fc: [5*hidden, hidden] (ggml: ne[0]=5*hidden, ne[1]=hidden) // target_hidden_cat: [5*hidden, ctx_len, 1] // Result: [hidden, ctx_len, 1] - ggml_tensor * target_hidden_cat = in.target_hidden_cat; static const bool disable_aux_hidden_norms = std::getenv("DFLASH_DISABLE_DRAFT_AUX_NORMS") != nullptr; static const bool disable_attn_gate = @@ -69,23 +100,8 @@ DraftGraphOutputs build_draft_graph( static const bool disable_ffn = std::getenv("DFLASH_DISABLE_DRAFT_FFN") != nullptr; - if (!disable_aux_hidden_norms && !w.aux_hidden_norms.empty()) { - ggml_tensor * aux_cat = nullptr; - const size_t elem_sz = ggml_element_size(in.target_hidden_cat); - for (size_t i = 0; i < w.aux_hidden_norms.size(); i++) { - ggml_tensor * slice = ggml_view_3d(ctx, in.target_hidden_cat, - w.n_embd, ctx_len, 1, - in.target_hidden_cat->nb[1], in.target_hidden_cat->nb[2], - i * (size_t)w.n_embd * elem_sz); - slice = ggml_rms_norm(ctx, slice, eps); - slice = ggml_mul(ctx, slice, w.aux_hidden_norms[i]); - aux_cat = aux_cat ? ggml_concat(ctx, aux_cat, slice, 0) : slice; - } - target_hidden_cat = aux_cat; - } - ggml_tensor * target_feat = ggml_mul_mat(ctx, w.fc, target_hidden_cat); - target_feat = ggml_rms_norm(ctx, target_feat, eps); - target_feat = ggml_mul (ctx, target_feat, w.hidden_norm); + ggml_tensor * target_feat = draft_fuse_features( + ctx, w, in.target_hidden_cat, ctx_len, disable_aux_hidden_norms); ggml_set_name(target_feat, "target_feat"); // ── 2. Decoder layers @@ -264,4 +280,188 @@ DraftGraphOutputs build_draft_graph( return og; } +// ── Cached drafter context-KV builders ────────────────────────────── +// +// The per-(head,position) k_norm and RoPE are separable, so computing the ctx +// K/V rows here (append) and the noise K/V rows in the step graph matches the +// legacy concat-then-normalize math exactly; the only numeric difference is +// the F16 cache storage (legacy keeps K/V in F32 for the one shot). + +// Per-layer ctx-side K/V rows in the head-major cache layout. +// K: wk @ tf_kv → per-head k_norm → RoPE(positions) → [head_dim*n_kv, n] rows. +// V: wv @ tf_kv, already row-shaped. +static void draft_ctx_kv_rows( + ggml_context * ctx, + const DraftWeights & w, + const DraftLayer & L, + ggml_tensor * target_feat, // [hidden, n] + ggml_tensor * positions, // [n] i32 absolute + int n, + ggml_tensor ** k_rows_out, + ggml_tensor ** v_rows_out) { + const float eps = DFLASH27B_RMS_EPS; + ggml_tensor * tf_kv = target_feat; + if (w.context_kv_layer_norm) { + tf_kv = ggml_rms_norm(ctx, tf_kv, eps); + tf_kv = ggml_mul(ctx, tf_kv, L.attn_norm); + } + ggml_tensor * K = ggml_mul_mat(ctx, L.wk, tf_kv); // [kv_dim, n] + K = ggml_reshape_3d(ctx, K, w.head_dim, w.n_head_kv, n); + K = ggml_rms_norm(ctx, K, eps); + K = ggml_mul (ctx, K, L.k_norm); + K = ggml_rope_ext(ctx, K, positions, /*freq_factors=*/nullptr, + w.head_dim, GGML_ROPE_TYPE_NEOX, /*n_ctx_orig=*/0, + w.rope_theta, /*freq_scale=*/1.0f, + /*ext_factor=*/0.0f, /*attn_factor=*/1.0f, + /*beta_fast=*/0.0f, /*beta_slow=*/0.0f); + // rope output is contiguous [head_dim, n_kv, n] → head-major rows view + *k_rows_out = ggml_view_2d(ctx, K, (int64_t)w.head_dim * w.n_head_kv, n, + K->nb[2], 0); + *v_rows_out = ggml_mul_mat(ctx, L.wv, tf_kv); // [kv_dim, n] +} + +bool build_draft_kv_append( + ggml_context * ctx, + ggml_cgraph * gf, + const DraftWeights & w, + const DraftKvCacheRefs & cache, + const DraftKvAppendInputs & in) { + if (!in.feat || !in.positions || !in.rows || in.n_rows <= 0) return false; + static const bool disable_aux_hidden_norms = + std::getenv("DFLASH_DISABLE_DRAFT_AUX_NORMS") != nullptr; + + ggml_tensor * target_feat = draft_fuse_features( + ctx, w, in.feat, in.n_rows, disable_aux_hidden_norms); + ggml_set_name(target_feat, "draft_kv_append_feat"); + + for (int il = 0; il < w.n_layer; il++) { + ggml_tensor * Krows = nullptr; + ggml_tensor * Vrows = nullptr; + draft_ctx_kv_rows(ctx, w, w.layers[il], target_feat, in.positions, + in.n_rows, &Krows, &Vrows); + ggml_build_forward_expand(gf, ggml_set_rows(ctx, cache.k[il], Krows, in.rows)); + ggml_build_forward_expand(gf, ggml_set_rows(ctx, cache.v[il], Vrows, in.rows)); + } + return true; +} + +DraftGraphOutputs build_draft_kv_step( + ggml_context * ctx, + ggml_cgraph * gf, + const DraftWeights & w, + const DraftKvCacheRefs & cache, + const DraftKvStepInputs & in) { + const int q_len = w.block_size; + const int n_head = w.n_head; + const int n_kv = w.n_head_kv; + const int head_dim = w.head_dim; + const float eps = DFLASH27B_RMS_EPS; + const float rope_base = w.rope_theta; + const int kv_total = cache.kv_total; + + static const bool disable_attn_gate = + std::getenv("DFLASH_DISABLE_DRAFT_ATTN_GATE") != nullptr; + static const bool disable_swa = + std::getenv("DFLASH_DISABLE_DRAFT_SWA") != nullptr; + + ggml_tensor * h = in.noise_embed; // [hidden, q_len] + char probe_name[64]; + + for (int il = 0; il < w.n_layer; il++) { + const DraftLayer & L = w.layers[il]; + const bool layer_is_swa = L.is_swa && !disable_swa; + + // ── attention pre-norm + ggml_tensor * hn = ggml_rms_norm(ctx, h, eps); + hn = ggml_mul(ctx, hn, L.attn_norm); + + // ── Q from noise, per-head RMSNorm, RoPE at absolute positions + ggml_tensor * Q = ggml_mul_mat(ctx, L.wq, hn); + Q = ggml_reshape_3d(ctx, Q, head_dim, n_head, q_len); + Q = ggml_rms_norm(ctx, Q, eps); + Q = ggml_mul (ctx, Q, L.q_norm); + Q = ggml_rope_ext(ctx, Q, in.positions_q, nullptr, + head_dim, GGML_ROPE_TYPE_NEOX, 0, + rope_base, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); + + // ── noise K/V into the scratch cache slots + ggml_tensor * Kn = ggml_mul_mat(ctx, L.wk, hn); + Kn = ggml_reshape_3d(ctx, Kn, head_dim, n_kv, q_len); + Kn = ggml_rms_norm(ctx, Kn, eps); + Kn = ggml_mul (ctx, Kn, L.k_norm); + Kn = ggml_rope_ext(ctx, Kn, in.positions_q, nullptr, + head_dim, GGML_ROPE_TYPE_NEOX, 0, + rope_base, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); + ggml_tensor * Kn_rows = ggml_view_2d(ctx, Kn, + (int64_t)head_dim * n_kv, q_len, Kn->nb[2], 0); + ggml_tensor * Vn_rows = ggml_mul_mat(ctx, L.wv, hn); // [kv_dim, q_len] + ggml_build_forward_expand(gf, + ggml_set_rows(ctx, cache.k[il], Kn_rows, in.noise_rows)); + ggml_build_forward_expand(gf, + ggml_set_rows(ctx, cache.v[il], Vn_rows, in.noise_rows)); + + // ── flash attention over the full cache span (masks gate validity). + // The set_rows nodes were expanded above, so graph order guarantees + // the scratch slots hold this step's noise K/V before the FA reads. + ggml_tensor * Qfa = ggml_permute(ctx, Q, 0, 2, 1, 3); // [hd, q_len, n_head] + Qfa = ggml_cont(ctx, Qfa); + ggml_tensor * Kview = ggml_view_3d(ctx, cache.k[il], + head_dim, n_kv, kv_total, + ggml_row_size(cache.k[il]->type, head_dim), cache.k[il]->nb[1], 0); + ggml_tensor * Vview = ggml_view_3d(ctx, cache.v[il], + head_dim, n_kv, kv_total, + ggml_row_size(cache.v[il]->type, head_dim), cache.v[il]->nb[1], 0); + ggml_tensor * Kfa = ggml_permute(ctx, Kview, 0, 2, 1, 3); // [hd, kv_total, n_kv] + ggml_tensor * Vfa = ggml_permute(ctx, Vview, 0, 2, 1, 3); + + const float scale = 1.0f / std::sqrt((float)head_dim); + ggml_tensor * mask = layer_is_swa ? in.mask_swa : in.mask_full; + ggml_tensor * attn = ggml_flash_attn_ext(ctx, Qfa, Kfa, Vfa, mask, + scale, /*max_bias=*/0.0f, + /*logit_softcap=*/0.0f); + std::snprintf(probe_name, sizeof(probe_name), "draft_kv_l%d_attn", il); + ggml_set_name(attn, probe_name); + + if (!disable_attn_gate && L.attn_gate) { + ggml_tensor * gate = ggml_mul_mat(ctx, L.attn_gate, hn); + gate = ggml_softplus(ctx, gate); + if (L.attn_gate_per_head) { + gate = ggml_reshape_3d(ctx, gate, 1, n_head, q_len); + } else { + gate = ggml_reshape_3d(ctx, gate, head_dim, n_head, q_len); + } + gate = ggml_cast(ctx, gate, attn->type); + attn = ggml_mul(ctx, attn, gate); + } + attn = ggml_reshape_2d(ctx, attn, head_dim * n_head, q_len); + + ggml_tensor * attn_out = ggml_mul_mat(ctx, L.wo, attn); + h = ggml_add(ctx, h, attn_out); + + // ── FFN + ggml_tensor * hf = ggml_rms_norm(ctx, h, eps); + hf = ggml_mul(ctx, hf, L.ffn_norm); + ggml_tensor * g = ggml_mul_mat(ctx, L.w_gate, hf); + g = ggml_silu(ctx, g); + ggml_tensor * u = ggml_mul_mat(ctx, L.w_up, hf); + ggml_tensor * gu = ggml_mul(ctx, g, u); + ggml_tensor * ffn_out = ggml_mul_mat(ctx, L.w_down, gu); + h = ggml_add(ctx, h, ffn_out); + } + + ggml_tensor * out = ggml_rms_norm(ctx, h, eps); + out = ggml_mul(ctx, out, w.out_norm); + ggml_set_name(out, "draft_kv_hidden_out"); + + DraftGraphOutputs og{}; + og.hidden_states = out; + og.logits = nullptr; + if (in.lm_head) { + ggml_tensor * logits = ggml_mul_mat(ctx, in.lm_head, out); + ggml_set_name(logits, "draft_kv_logits"); + og.logits = logits; + } + return og; +} + } // namespace dflash::common diff --git a/server/src/draft/draft_graph.h b/server/src/draft/draft_graph.h index e186de289..b89429dac 100644 --- a/server/src/draft/draft_graph.h +++ b/server/src/draft/draft_graph.h @@ -3,6 +3,8 @@ #include "ggml.h" +#include + namespace dflash::common { struct DraftWeights; // fwd @@ -37,4 +39,57 @@ DraftGraphOutputs build_draft_graph( const DraftWeights & w, const DraftGraphInputs & in); +// ── Cached drafter context-KV ──────────────────────────────────────── +// The ctx side of the draft forward (feature fusion → per-layer K/V → +// k_norm → RoPE) is row-independent, so committed rows can be computed once +// and kept in per-layer ring caches; the step graph then only processes the +// q_len noise rows and flash-attends over the cache. RoPE uses ABSOLUTE +// positions (attention scores only depend on position differences, so this +// matches the legacy rebased-window math) which keeps cached entries valid +// as the feature window slides. +// +// Cache layout (head-major, mirrors the target KV cache): one row per +// position holding all KV heads fused, [head_dim*n_head_kv, kv_total] F16. +// Rows [0, cap) are the ctx ring (position p → slot p % cap), rows +// [cap, cap+q_len) are the per-step noise K/V scratch, row cap+q_len is a +// trash slot for padded append rows, and the remainder is mask alignment. + +struct DraftKvCacheRefs { + int kv_total = 0; // cache rows incl. noise scratch + trash + pad + std::vector k; // per layer [head_dim*n_head_kv, kv_total] f16 + std::vector v; // per layer [head_dim*n_head_kv, kv_total] f16 +}; + +// Fuse `n_rows` feature rows and set_rows their per-layer K/V into the caches. +struct DraftKvAppendInputs { + int n_rows = 0; + ggml_tensor * feat = nullptr; // [n_target_layers*hidden, n_rows] f32 + ggml_tensor * positions = nullptr; // [n_rows] i32, absolute + ggml_tensor * rows = nullptr; // [n_rows] i32, destination cache slots +}; +bool build_draft_kv_append( + ggml_context * ctx, + ggml_cgraph * gf, + const DraftWeights & w, + const DraftKvCacheRefs & cache, + const DraftKvAppendInputs & in); + +// One draft step over the cached context KV. Noise K/V are written into the +// scratch slots and the flash attention reads the full kv_total span; the +// masks carry window membership, causality and slot validity. +struct DraftKvStepInputs { + ggml_tensor * noise_embed = nullptr; // [hidden, q_len] f32 + ggml_tensor * positions_q = nullptr; // [q_len] i32, absolute + ggml_tensor * noise_rows = nullptr; // [q_len] i32, scratch cache slots + ggml_tensor * mask_full = nullptr; // [kv_total, q_len] f16 (non-SWA layers) + ggml_tensor * mask_swa = nullptr; // [kv_total, q_len] f16 (SWA layers) + ggml_tensor * lm_head = nullptr; // optional fused projection +}; +DraftGraphOutputs build_draft_kv_step( + ggml_context * ctx, + ggml_cgraph * gf, + const DraftWeights & w, + const DraftKvCacheRefs & cache, + const DraftKvStepInputs & in); + } // namespace dflash::common From b645e4844cb26f8e3f462d4aa741e54a3c8c1bd6 Mon Sep 17 00:00:00 2001 From: mrciffa <49000955+davide221@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:09:09 +0200 Subject: [PATCH 08/20] feat(laguna): flat long-context serving under KVFlash Laguna XS 2.1 Q4_K_M + q4 drafter on RTX 3090, cold-certified: decode 154.6 tok/s @30K and 152.3 @256K (8K pool, was 84.8/22.1), prefill 240K tokens in 67.3s (was 26.4 min), acceptance parity, ~1GB VRAM freed. - SWA ring caches [TAG_SWA_RING]: 30/40 layers attend a 512-token window but flash-attended the whole pooled span with 97% masked out. Window layers now keep a position-indexed ring (slot = pos % ring, ring sized from chunk+window) and bypass the pager entirely; the pager only pages full-attention layers. Verify 19.4 -> 13.7ms, prefill -27%. Snapshots are skipped when rings are active (they hold only the trailing window); kv_cap probes pick a full-attention layer. Kill: DFLASH_LAGUNA_SWA_RING=0. - Drafter ring-cache wiring behind DFLASH_DRAFT_KV (default on, =0 legacy; rebuilt on drafter-variant switch). - Pooled prefill batches multiple pager chunks per target step and the feature capture appends via set_rows with ring-row indices as input data, so prefill graphs CUDA-replay; --chunk 1024 feeds the MoE expert GEMMs better (-19% prefill wall at 240K). - Env-gated profilers: DFLASH_LAGUNA_STEP_PROF (commit/build/dwait laps), DFLASH_LAGUNA_VERIFY_PROF and DFLASH_LAGUNA_PREFILL_PROF (sub-phase laps), GGML_CUDA_GRAPH_STATS_EVERY + first-mismatch logging in ggml-cuda. Measured: the entire KVFlash host machinery costs 0.08ms/step; verify is 99.5% GPU kernel time. --- .../llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu | 14 +- server/src/laguna/laguna_backend.cpp | 278 ++++++++++++----- server/src/laguna/laguna_backend.h | 5 + server/src/laguna/laguna_internal.h | 20 +- server/src/laguna/laguna_target_graph.cpp | 289 +++++++++++++++--- 5 files changed, 483 insertions(+), 123 deletions(-) diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu index feb9104a9..07175145b 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu @@ -3265,6 +3265,14 @@ static bool ggml_cuda_graph_update_required(ggml_backend_cuda_context * cuda_ctx } if (res || memcmp(&graph->node_props[i], &prop, sizeof(prop)) != 0) { + // [TAG_FUSED_LOOP] with GGML_CUDA_GRAPH_STATS, name the first node + // whose properties changed — the reason a graph re-captures. + static const bool dbg = getenv("GGML_CUDA_GRAPH_STATS") != nullptr; + if (dbg && !res) { + GGML_LOG_INFO("[graph-mismatch] key=%p node=%d op=%s name=%s\n", + graph_key, i, ggml_op_name(cgraph->nodes[i]->op), + cgraph->nodes[i]->name); + } graph->node_props[i] = prop; res = true; } @@ -4380,7 +4388,11 @@ static enum ggml_status ggml_backend_cuda_graph_compute(ggml_backend_t backend, if (use_cuda_graph && !cuda_graph_update_required) g->stat_replay++; else if (use_cuda_graph) g->stat_capture++; else g->stat_eager++; - if (g->stat_total % 200 == 0) { + static const int stats_every = [](){ + const char * e = getenv("GGML_CUDA_GRAPH_STATS_EVERY"); + return e ? atoi(e) : 200; + }(); + if (g->stat_total % stats_every == 0) { GGML_LOG_INFO("[cuda-graph-stats] key=%p n_nodes=%d total=%llu replay=%llu capture=%llu eager=%llu enabled=%d\n", graph_key, cgraph->n_nodes, (unsigned long long)g->stat_total, (unsigned long long)g->stat_replay, diff --git a/server/src/laguna/laguna_backend.cpp b/server/src/laguna/laguna_backend.cpp index 3d0dd7da3..dac541ea3 100644 --- a/server/src/laguna/laguna_backend.cpp +++ b/server/src/laguna/laguna_backend.cpp @@ -174,8 +174,19 @@ bool LagunaBackend::init() { "[laguna] auto-enabled head-major KV layout " "(disable with DFLASH_LAGUNA_AUTO_HEAD_MAJOR=0)\n"); } + // [TAG_SWA_RING] pooled mode: SWA layers on position rings sized for the + // sliding window + the largest batch this backend issues. Kill switch: + // DFLASH_LAGUNA_SWA_RING=0. + static const bool swa_ring_env = []() { + const char * e = std::getenv("DFLASH_LAGUNA_SWA_RING"); + return !(e && e[0] == '0' && e[1] == '\0'); + }(); + const int swa_ring_rows = + (kvflash_tokens_ > 0 && !hybrid_mode_ && swa_ring_env) + ? std::max(2048, args_.chunk + w_.sliding_window + 64) + : 0; if (!create_laguna_target_cache(w_, args_.max_ctx, backend_, cache_, - kvflash_tokens_)) { + kvflash_tokens_, swa_ring_rows)) { std::fprintf(stderr, "cache failed: %s\n", dflash27b_last_error()); free_laguna_target_weights(w_); ggml_backend_free(backend_); backend_ = nullptr; @@ -296,7 +307,20 @@ bool LagunaBackend::kvflash_attach() { if (!kvflash_active()) return true; KvFlashConfig pc = kvflash_config(); pc.pool_tokens = kvflash_tokens_; - if (!kvflash_pager_.attach(pc, cache_.attn_k, cache_.attn_v)) { + // [TAG_SWA_RING] SWA layers live on position rings outside the pool, so + // the pager only pages the full-attention layers' K/V. + std::vector pool_k = cache_.attn_k; + std::vector pool_v = cache_.attn_v; + if (cache_.swa_ring_rows > 0) { + pool_k.clear(); + pool_v.clear(); + for (int il = 0; il < w_.n_layer; ++il) { + if (!laguna_is_full_attn_layer(w_, il)) continue; + pool_k.push_back(cache_.attn_k[(size_t)il]); + pool_v.push_back(cache_.attn_v[(size_t)il]); + } + } + if (!kvflash_pager_.attach(pc, pool_k, pool_v)) { std::fprintf(stderr, "kvflash: pager attach failed (pool=%d)\n", kvflash_tokens_); return false; @@ -374,8 +398,16 @@ bool LagunaBackend::unpark(const std::string & what) { "[laguna] auto-enabled head-major KV layout " "(disable with DFLASH_LAGUNA_AUTO_HEAD_MAJOR=0)\n"); } + static const bool unpark_swa_ring_env = []() { + const char * e = std::getenv("DFLASH_LAGUNA_SWA_RING"); + return !(e && e[0] == '0' && e[1] == '\0'); + }(); + const int unpark_swa_ring = + (kvflash_tokens_ > 0 && !hybrid_mode_ && unpark_swa_ring_env) + ? std::max(2048, args_.chunk + w_.sliding_window + 64) + : 0; if (!create_laguna_target_cache(w_, args_.max_ctx, backend_, cache_, - kvflash_tokens_)) { + kvflash_tokens_, unpark_swa_ring)) { std::fprintf(stderr, "[unpark] cache: %s\n", dflash27b_last_error()); return false; } @@ -410,10 +442,13 @@ bool LagunaBackend::ensure_slot(int slot) { bool LagunaBackend::snapshot_save(int slot) { // kvflash: snapshots copy rows assuming identity layout, which breaks - // after the first page-out relocates a chunk. - if (kvflash_active() && !kvflash_pager_.is_identity()) { + // after the first page-out relocates a chunk. [TAG_SWA_RING] ring-cached + // SWA layers hold only the trailing window, so prefix snapshots are + // impossible there too. + if (kvflash_active() && + (!kvflash_pager_.is_identity() || cache_.swa_ring_rows > 0)) { std::fprintf(stderr, "[kvflash] snapshot skipped: pool has relocated " - "chunks (page-table serialization not implemented)\n"); + "chunks or SWA layers are ring-cached\n"); return false; } if (!ensure_slot(slot)) return false; @@ -623,13 +658,37 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, (void)kvflash_alloc_span(0, committed); } + // [TAG_DRAFT_KV] drafter context-KV ring cache: compute the ctx-side + // K/V once per committed row instead of re-fusing the whole feature + // window every step (draft ~10ms -> ~3ms once the window fills). + // Kill switch: DFLASH_DRAFT_KV=0 restores the legacy one-shot graph. + constexpr int DRAFT_CTX_MAX_DEFAULT = 2048; + static const bool draft_kv_on = []() { + const char * e = std::getenv("DFLASH_DRAFT_KV"); + return !(e && e[0] == '0' && e[1] == '\0'); + }(); + bool use_draft_kv = draft_kv_on && feature_mirror_.target_feat != nullptr; + if (use_draft_kv && draft_kv_.gf && draft_kv_.built_for != (const void *)&dw) { + draft_kv_free(draft_kv_); // drafter variant switched; graph holds old weights + } + if (use_draft_kv && !draft_kv_.gf) { + const int kv_cap = std::min(feature_mirror_.cap, + std::max(DRAFT_CTX_MAX_DEFAULT, args_.draft_ctx_max)); + if (!draft_kv_init(draft_kv_, dw, draft_backend_, kv_cap, nullptr)) { + draft_kv_free(draft_kv_); + use_draft_kv = false; + std::fprintf(stderr, + "[laguna-spec] draft-kv init failed; using legacy draft path\n"); + } + } + auto t_dec0 = std::chrono::steady_clock::now(); static const bool step_prof = std::getenv("DFLASH_LAGUNA_STEP_PROF") != nullptr; double prof_draft_ms = 0.0, prof_heads_ms = 0.0, prof_verify_ms = 0.0; // [TAG_FUSED_LOOP] blind-spot laps: commit = verify-end -> loop-top // (accept/commit/emit/feature-sync), build = loop-top -> draft-input // upload (noise embed on host, build_draft_step, feature copy). - double prof_commit_ms = 0.0, prof_build_ms = 0.0; + double prof_commit_ms = 0.0, prof_build_ms = 0.0, prof_dwait_ms = 0.0; auto prof_now = std::chrono::steady_clock::now(); auto prof_lap = [&]() { auto t = std::chrono::steady_clock::now(); @@ -677,62 +736,94 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, return false; } - constexpr int DRAFT_CTX_MAX_DEFAULT = 2048; - const int ring_cap = feature_mirror_.cap; - const int draft_ctx = std::min(committed, - std::min(ring_cap, std::max(DRAFT_CTX_MAX_DEFAULT, args_.draft_ctx_max))); - const int draft_start = committed - draft_ctx; - int mirror_slot0 = 0; - const bool use_mirror_view = - draft_feature_mirror_can_view(feature_mirror_, committed, draft_ctx, mirror_slot0); - - static const bool draft_pad = []() { - const char * e = std::getenv("DFLASH_LAGUNA_DRAFT_PAD"); - return !(e && e[0] == '0' && e[1] == '\0'); - }(); - // [TAG_FUSED_LOOP] persistent draft graph: force the feature-COPY - // build (D2D peer copy, ~0.1ms) so the topology carries no per-step - // ring-view offsets and build_draft_step can skip the rebuild while - // ctx stays inside the same 64-aligned bucket. Kill: DFLASH_DRAFT_PERSIST=0. - static const bool draft_persist = []() { - const char * e = std::getenv("DFLASH_DRAFT_PERSIST"); - return !(e && e[0] == '0' && e[1] == '\0'); - }(); - const bool want_view = use_mirror_view && !draft_persist; - if (!build_draft_step(draft_sg, dw, /*lm_head=*/nullptr, draft_backend_, - draft_ctx, want_view ? &feature_mirror_ : nullptr, - committed, - std::min(ring_cap, std::max(DRAFT_CTX_MAX_DEFAULT, args_.draft_ctx_max)), - draft_pad)) { - std::fprintf(stderr, "[laguna-spec] draft build failed\n"); - step_graph_destroy(draft_sg); - return false; - } - if (!want_view && - !copy_feature_ring_range_to_tensor(feature_mirror_, draft_sg.target_hidden_cat, - draft_start, draft_ctx)) { - std::fprintf(stderr, "[laguna-spec] feature copy failed\n"); - step_graph_destroy(draft_sg); - return false; - } + ggml_tensor * draft_hidden = nullptr; + if (use_draft_kv) { + // [TAG_DRAFT_KV] append newly committed rows to the ctx-KV ring + // and refresh positions/masks; the step graph itself never + // rebuilds (fixed topology, CUDA-graph replay from step 2). + if (!draft_kv_begin_step(draft_kv_, dw, draft_backend_, + feature_mirror_, committed)) { + std::fprintf(stderr, "[laguna-spec] draft-kv step prep failed\n"); + step_graph_destroy(draft_sg); + return false; + } + ggml_backend_tensor_set(draft_kv_.inp_embed, noise_embed.data(), 0, + sizeof(float) * noise_embed.size()); + if (step_prof) prof_build_ms += prof_lap(); // [TAG_FUSED_LOOP] + if (step_prof) { + ggml_backend_synchronize(draft_backend_); + prof_dwait_ms += prof_lap(); + } + if (ggml_backend_graph_compute(draft_backend_, draft_kv_.gf) != + GGML_STATUS_SUCCESS) { + std::fprintf(stderr, "[laguna-spec] draft-kv compute failed\n"); + step_graph_destroy(draft_sg); + return false; + } + draft_hidden = draft_kv_.hidden_states; + } else { + const int ring_cap = feature_mirror_.cap; + const int draft_ctx = std::min(committed, + std::min(ring_cap, std::max(DRAFT_CTX_MAX_DEFAULT, args_.draft_ctx_max))); + const int draft_start = committed - draft_ctx; + int mirror_slot0 = 0; + const bool use_mirror_view = + draft_feature_mirror_can_view(feature_mirror_, committed, draft_ctx, mirror_slot0); + + static const bool draft_pad = []() { + const char * e = std::getenv("DFLASH_LAGUNA_DRAFT_PAD"); + return !(e && e[0] == '0' && e[1] == '\0'); + }(); + // [TAG_FUSED_LOOP] persistent draft graph: force the feature-COPY + // build (D2D peer copy, ~0.1ms) so the topology carries no per-step + // ring-view offsets and build_draft_step can skip the rebuild while + // ctx stays inside the same 64-aligned bucket. Kill: DFLASH_DRAFT_PERSIST=0. + static const bool draft_persist = []() { + const char * e = std::getenv("DFLASH_DRAFT_PERSIST"); + return !(e && e[0] == '0' && e[1] == '\0'); + }(); + const bool want_view = use_mirror_view && !draft_persist; + if (!build_draft_step(draft_sg, dw, /*lm_head=*/nullptr, draft_backend_, + draft_ctx, want_view ? &feature_mirror_ : nullptr, + committed, + std::min(ring_cap, std::max(DRAFT_CTX_MAX_DEFAULT, args_.draft_ctx_max)), + draft_pad)) { + std::fprintf(stderr, "[laguna-spec] draft build failed\n"); + step_graph_destroy(draft_sg); + return false; + } + if (!want_view && + !copy_feature_ring_range_to_tensor(feature_mirror_, draft_sg.target_hidden_cat, + draft_start, draft_ctx)) { + std::fprintf(stderr, "[laguna-spec] feature copy failed\n"); + step_graph_destroy(draft_sg); + return false; + } - ggml_backend_tensor_set(draft_sg.inp_embed, noise_embed.data(), 0, - sizeof(float) * noise_embed.size()); - const int kctx = (draft_sg.ctx_alloc > 0) ? draft_sg.ctx_alloc : draft_ctx; - pos_k.resize((size_t)kctx + (size_t)block_size); - for (int i = 0; i < block_size; i++) pos_q[(size_t)i] = draft_ctx + i; - for (int i = 0; i < kctx; i++) pos_k[(size_t)i] = (i < draft_ctx) ? i : 0; - for (int j = 0; j < block_size; j++) pos_k[(size_t)kctx + j] = draft_ctx + j; - if (step_prof) prof_build_ms += prof_lap(); // [TAG_FUSED_LOOP] - ggml_backend_tensor_set(draft_sg.positions, pos_q.data(), 0, - sizeof(int32_t) * pos_q.size()); - ggml_backend_tensor_set(draft_sg.positions_k, pos_k.data(), 0, - sizeof(int32_t) * pos_k.size()); - - if (ggml_backend_graph_compute(draft_backend_, draft_sg.gf) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "[laguna-spec] draft compute failed\n"); - step_graph_destroy(draft_sg); - return false; + ggml_backend_tensor_set(draft_sg.inp_embed, noise_embed.data(), 0, + sizeof(float) * noise_embed.size()); + const int kctx = (draft_sg.ctx_alloc > 0) ? draft_sg.ctx_alloc : draft_ctx; + pos_k.resize((size_t)kctx + (size_t)block_size); + for (int i = 0; i < block_size; i++) pos_q[(size_t)i] = draft_ctx + i; + for (int i = 0; i < kctx; i++) pos_k[(size_t)i] = (i < draft_ctx) ? i : 0; + for (int j = 0; j < block_size; j++) pos_k[(size_t)kctx + j] = draft_ctx + j; + if (step_prof) prof_build_ms += prof_lap(); // [TAG_FUSED_LOOP] + ggml_backend_tensor_set(draft_sg.positions, pos_q.data(), 0, + sizeof(int32_t) * pos_q.size()); + ggml_backend_tensor_set(draft_sg.positions_k, pos_k.data(), 0, + sizeof(int32_t) * pos_k.size()); + + // [TAG_FUSED_LOOP] split pending-work wait from execution when profiling + if (step_prof) { + ggml_backend_synchronize(draft_backend_); + prof_dwait_ms += prof_lap(); + } + if (ggml_backend_graph_compute(draft_backend_, draft_sg.gf) != GGML_STATUS_SUCCESS) { + std::fprintf(stderr, "[laguna-spec] draft compute failed\n"); + step_graph_destroy(draft_sg); + return false; + } + draft_hidden = draft_sg.hidden_states; } // [TAG_FUSED_LOOP] the 64KB draft-hidden D2H readback is lazy: the @@ -742,7 +833,7 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, auto fetch_hidden = [&]() { if (hidden_on_host) return; local_hidden.resize((size_t)hidden * (size_t)q_len); - ggml_backend_tensor_get(draft_sg.hidden_states, local_hidden.data(), 0, + ggml_backend_tensor_get(draft_hidden, local_hidden.data(), 0, sizeof(float) * local_hidden.size()); hidden_on_host = true; }; @@ -779,7 +870,7 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, last_tok, draft_tok, cand_k, cand_k > 0 ? &cand_p : nullptr, cand_k > 0 ? &cand_i : nullptr, - draft_sg.hidden_states)) { + draft_hidden)) { used_domino = true; } } @@ -1138,12 +1229,13 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, if (step_prof && n_draft_steps > 0) { std::fprintf(stderr, "[step-prof] per-step ms: draft=%.2f heads=%.2f verify=%.2f " - "commit=%.2f build=%.2f other=%.2f total=%.2f (steps=%d)\n", + "commit=%.2f build=%.2f dwait=%.2f other=%.2f total=%.2f (steps=%d)\n", prof_draft_ms / n_draft_steps, prof_heads_ms / n_draft_steps, prof_verify_ms / n_draft_steps, prof_commit_ms / n_draft_steps, prof_build_ms / n_draft_steps, + prof_dwait_ms / n_draft_steps, (decode_s * 1000.0 - prof_draft_ms - prof_heads_ms - prof_verify_ms - - prof_commit_ms - prof_build_ms) / n_draft_steps, + prof_commit_ms - prof_build_ms - prof_dwait_ms) / n_draft_steps, decode_s * 1000.0 / n_draft_steps, n_draft_steps); } std::fprintf(stderr, "[laguna-spec] tokens=%d time=%.3f s speed=%.2f tok/s " @@ -1207,14 +1299,18 @@ GenerateResult LagunaBackend::generate_impl(const GenerateRequest & req, return result; } - // kvflash: prefill rows land identity-mapped, so the prompt must fit the - // pool with one chunk of decode headroom (decode then evicts LRU live). - if (kvflash_active() && - N > kvflash_tokens_ - kvflash_pager_.chunk_tokens()) { - std::fprintf(stderr, "[kvflash] prompt (%d) exceeds pool %d; raise " - "--kvflash\n", N, kvflash_tokens_); - result.error = "kvflash: prompt exceeds resident pool"; - return result; + // kvflash: prompts that fit the pool prefill identity-mapped. Larger + // prompts take the pooled path: pager-chunk-sized batches allocated via + // slot_for (evicting as the pool fills), slot-mapped KV writes and + // slot-space masks via kvflash_fill_rows_and_masks — same recipe as the + // qwen35 pooled prefill. Constant VRAM, linear time. + const bool kvf_paged = kvflash_active() && + N > kvflash_tokens_ - kvflash_pager_.chunk_tokens(); + if (kvf_paged) { + std::printf("[kvflash] pooled prefill: %d tokens through a %d-token " + "pool (%d-token chunks, evicting)\n", + N, kvflash_tokens_, kvflash_pager_.chunk_tokens()); + std::fflush(stdout); } reset_laguna_target_cache(cache_); @@ -1231,11 +1327,32 @@ GenerateResult LagunaBackend::generate_impl(const GenerateRequest & req, auto t_pf0 = std::chrono::steady_clock::now(); std::vector last_logits; bool ok = true; - const int n_chunks = (N + args_.chunk - 1) / args_.chunk; + // Pooled batches span multiple pager chunks (graphs replay, so big + // batches are pure win); each batch is chunk-aligned for slot_for. + const int pf_pool_batch = std::max(kvflash_pager_.chunk_tokens(), + (args_.chunk / std::max(1, kvflash_pager_.chunk_tokens())) * + std::max(1, kvflash_pager_.chunk_tokens())); + const int pf_chunk = kvf_paged ? pf_pool_batch : args_.chunk; + const int n_chunks = (N + pf_chunk - 1) / pf_chunk; for (int c = 0; c < n_chunks && ok; ++c) { - const int kv_start = c * args_.chunk; - const int n_tok = std::min(args_.chunk, N - c * args_.chunk); - ok = kvflash_alloc_span(kv_start, n_tok) && + const int kv_start = c * pf_chunk; + const int n_tok = std::min(pf_chunk, N - c * pf_chunk); + if (kvf_paged) { + // Pooled path: allocate this chunk's slots up front, evicting the + // lowest-priority resident chunk once the pool fills. laguna_step + // then slot-maps the KV writes + masks via the shared helper. + for (int i = 0; i < n_tok && ok; i++) { + ok = kvflash_pager_.slot_for(kv_start + i) >= 0; + } + if (!ok) { + std::fprintf(stderr, + "[kvflash] pooled prefill: slot alloc failed @%d\n", kv_start); + break; + } + } else { + ok = kvflash_alloc_span(kv_start, n_tok); + } + ok = ok && laguna_step(backend_, w_, cache_, embed_pf.data() + (size_t)kv_start * w_.n_embd, n_tok, kv_start, no_mask, last_logits, kvf, @@ -1249,9 +1366,9 @@ GenerateResult LagunaBackend::generate_impl(const GenerateRequest & req, // kvflash: snapshots copy rows [0, snap_pos) assuming identity layout, // which holds until the first page-out relocates a chunk. if (kvflash_active() && req.snap_slot >= 0 && - !kvflash_pager_.is_identity()) { + (!kvflash_pager_.is_identity() || cache_.swa_ring_rows > 0)) { std::fprintf(stderr, "[kvflash] snapshot skipped: pool has relocated " - "chunks (page-table serialization not implemented)\n"); + "chunks or SWA layers are ring-cached\n"); } else if (req.snap_slot >= 0 && req.snap_pos > 0 && req.snap_pos <= N) { if (ensure_slot(req.snap_slot) && @@ -3232,6 +3349,7 @@ bool LagunaBackend::select_decode_draft(const std::string & name) { void LagunaBackend::free_decode_draft() { delete dflash_target_; dflash_target_ = nullptr; + draft_kv_free(draft_kv_); draft_feature_mirror_free(feature_mirror_); free_laguna_target_feat(cache_); for (LagunaDraftVariant & variant : draft_variants_) { diff --git a/server/src/laguna/laguna_backend.h b/server/src/laguna/laguna_backend.h index 24090f966..9bbf005b4 100644 --- a/server/src/laguna/laguna_backend.h +++ b/server/src/laguna/laguna_backend.h @@ -11,6 +11,7 @@ #include "laguna_dflash_target.h" #include "common/dflash_feature_ring.h" #include "common/dflash_draft_graph.h" +#include "common/dflash_draft_kv.h" #include "placement/placement_config.h" #include "qwen3_drafter.h" #include "kvflash_pager.h" @@ -109,6 +110,10 @@ class LagunaBackend : public ModelBackend { DraftWeights * active_dw_ = nullptr; std::string default_draft_variant_ = "base"; DraftFeatureMirror feature_mirror_{}; + // [TAG_DRAFT_KV] drafter context-KV ring cache (lazy-init on first spec + // decode; kill with DFLASH_DRAFT_KV=0). Replaces the per-step full-window + // K/V recompute once the feature window fills. + DraftKvState draft_kv_{}; LagunaDFlashTarget * dflash_target_ = nullptr; bool draft_parked_ = false; // [TAG_LAGUNA_VERIFY_WIDTH] EWMA of the accepted block length, persisted diff --git a/server/src/laguna/laguna_internal.h b/server/src/laguna/laguna_internal.h index adf263fbf..88d4d2aae 100644 --- a/server/src/laguna/laguna_internal.h +++ b/server/src/laguna/laguna_internal.h @@ -183,6 +183,14 @@ struct LagunaTargetCache { std::vector attn_k; // size = n_layer std::vector attn_v; + // [TAG_SWA_RING] When > 0 (pooled/kvflash mode), SWA layers' K/V tensors + // are position-indexed rings of this many rows (slot = pos % rows) + // instead of pool-sized tensors: a window-W layer never needs positions + // older than W back, so it bypasses the pager entirely. Full-attention + // layers keep pool-sized tensors + pager slot mapping. Cuts the SWA + // layers' attention span from pool width to ring width (prefill + verify). + int swa_ring_rows = 0; + // DFlash feature capture ring buffer (BF16, allocated when draft is active). ggml_tensor * target_feat = nullptr; // [n_capture_layers*n_embd, target_feat_cap] int target_feat_cap = 0; @@ -195,18 +203,22 @@ struct LagunaTargetCache { // `ctx_alloc` (kvflash): when > 0 and < max_ctx, the per-layer K/V tensors // are allocated at ctx_alloc rows (the resident pool) while cache.max_ctx // keeps the logical bound. 0 = allocate at max_ctx (default). +// `swa_ring_rows` ([TAG_SWA_RING]): when > 0 (requires ctx_alloc > 0 and a +// full layer range), SWA layers allocate ring-sized K/V instead of the pool. bool create_laguna_target_cache(const LagunaTargetWeights & w, int max_ctx, ggml_backend_t backend, LagunaTargetCache & out, - int ctx_alloc = 0); + int ctx_alloc = 0, + int swa_ring_rows = 0); bool create_laguna_target_cache_partial(const LagunaTargetWeights & w, int max_ctx, ggml_backend_t backend, int layer_begin, int layer_end, LagunaTargetCache & out, - int ctx_alloc = 0); + int ctx_alloc = 0, + int swa_ring_rows = 0); void free_laguna_target_cache(LagunaTargetCache & c); void reset_laguna_target_cache(LagunaTargetCache & c); @@ -290,6 +302,10 @@ struct LagunaGraphInputs { // CUDA-graph cache replays instead of re-launching every kernel. int kv_pad = 0; // 0 = legacy exact-length views + cpy append ggml_tensor * kv_idx = nullptr; // [n_tokens] I32 cache row indices (graph input) + // [TAG_SWA_RING] SWA ring rows (pos % cache.swa_ring_rows). When non-null, + // SWA layers write K/V through these indices and flash-attend over the + // ring span; attn_mask_swa must then be [swa_ring_rows, n_tokens]. + ggml_tensor * kv_idx_swa = nullptr; // [n_tokens] I32 bool output_logits = true; bool logits_are_output = true; bool output_hidden_states = false; diff --git a/server/src/laguna/laguna_target_graph.cpp b/server/src/laguna/laguna_target_graph.cpp index c1ca7239d..a93b79528 100644 --- a/server/src/laguna/laguna_target_graph.cpp +++ b/server/src/laguna/laguna_target_graph.cpp @@ -17,6 +17,7 @@ // is tested against our llama.cpp build_laguna (already verified to match HF // for 30+ tokens on B-tree prompt; see Lucebox/Laguna-XS.2-GGUF README). +#include #include #include "../common/mmid_adaptive_k.h" #include "laguna_internal.h" @@ -49,10 +50,11 @@ bool create_laguna_target_cache(const LagunaTargetWeights & w, int max_ctx, ggml_backend_t backend, LagunaTargetCache & out, - int ctx_alloc) { + int ctx_alloc, + int swa_ring_rows) { return create_laguna_target_cache_partial( w, max_ctx, backend, /*layer_begin=*/0, /*layer_end=*/w.n_layer, out, - ctx_alloc); + ctx_alloc, swa_ring_rows); } bool create_laguna_target_cache_partial(const LagunaTargetWeights & w, @@ -61,7 +63,8 @@ bool create_laguna_target_cache_partial(const LagunaTargetWeights & w, int layer_begin, int layer_end, LagunaTargetCache & out, - int ctx_alloc) { + int ctx_alloc, + int swa_ring_rows) { if (layer_begin < 0) layer_begin = 0; if (layer_end < 0) layer_end = w.n_layer; if (layer_begin > layer_end || layer_end > w.n_layer) { @@ -97,24 +100,40 @@ bool create_laguna_target_cache_partial(const LagunaTargetWeights & w, out.base_ctx = ggml_init(ip); if (!out.base_ctx) { set_last_error("laguna cache: ggml_init failed"); return false; } + // [TAG_SWA_RING] ring-sized SWA caches only make sense in pooled mode + // with the full layer range (layer-split shards fill masks in pool space). + out.swa_ring_rows = 0; + if (swa_ring_rows > 0 && ctx_alloc > 0 && + layer_begin == 0 && layer_end == w.n_layer) { + out.swa_ring_rows = ((swa_ring_rows + kKvFaPad - 1) / kKvFaPad) * kKvFaPad; + } + out.attn_k.resize(w.n_layer, nullptr); out.attn_v.resize(w.n_layer, nullptr); for (int il = 0; il < w.n_layer; ++il) { if (il < layer_begin || il >= layer_end) continue; + const int rows = (out.swa_ring_rows > 0 && !laguna_is_full_attn_layer(w, il)) + ? out.swa_ring_rows : ctx_phys; char nm[32]; std::snprintf(nm, sizeof(nm), "k_l%d", il); ggml_tensor * k = out.kv_head_major - ? ggml_new_tensor_2d(out.base_ctx, k_type, w.head_dim * w.n_head_kv, ctx_phys) - : ggml_new_tensor_3d(out.base_ctx, k_type, w.head_dim, ctx_phys, w.n_head_kv); + ? ggml_new_tensor_2d(out.base_ctx, k_type, w.head_dim * w.n_head_kv, rows) + : ggml_new_tensor_3d(out.base_ctx, k_type, w.head_dim, rows, w.n_head_kv); ggml_set_name(k, nm); std::snprintf(nm, sizeof(nm), "v_l%d", il); ggml_tensor * v = out.kv_head_major - ? ggml_new_tensor_2d(out.base_ctx, v_type, w.head_dim * w.n_head_kv, ctx_phys) - : ggml_new_tensor_3d(out.base_ctx, v_type, w.head_dim, ctx_phys, w.n_head_kv); + ? ggml_new_tensor_2d(out.base_ctx, v_type, w.head_dim * w.n_head_kv, rows) + : ggml_new_tensor_3d(out.base_ctx, v_type, w.head_dim, rows, w.n_head_kv); ggml_set_name(v, nm); out.attn_k[il] = k; out.attn_v[il] = v; } + if (out.swa_ring_rows > 0) { + std::fprintf(stderr, + "[laguna][swa-ring] SWA layers on %d-row position rings " + "(window=%d, pool=%d rows stays full-layer only)\n", + out.swa_ring_rows, w.sliding_window, ctx_phys); + } out.base_buf = ggml_backend_alloc_ctx_tensors(out.base_ctx, backend); if (!out.base_buf) { @@ -659,12 +678,16 @@ static ggml_tensor * build_laguna_attn_block( int n_tokens, bool is_full, int kv_pad = 0, - ggml_tensor * kv_idx = nullptr) + ggml_tensor * kv_idx = nullptr, + ggml_tensor * kv_idx_swa = nullptr) { const int head_dim = w.head_dim; const int n_head = w.n_head_arr[il]; const int n_head_kv = w.n_head_kv; const int q_dim = n_head * head_dim; + // [TAG_SWA_RING] SWA layer on a position-indexed ring: K/V land at + // pos % ring via kv_idx_swa and the FA span is the (constant) ring size. + const bool swa_ring = !is_full && kv_idx_swa != nullptr; // ---- Q/K/V projections --- // Fused path: ONE matmul for Q|K (loader-fused adjacent weights), then a @@ -794,7 +817,8 @@ static ggml_tensor * build_laguna_attn_block( Vcur_rows = ggml_permute(ctx, Vcur, 0, 2, 1, 3); } - if (kv_idx) { + ggml_tensor * write_idx = swa_ring ? kv_idx_swa : kv_idx; + if (write_idx) { // CUDA-graph-stable append: the destination is the WHOLE cache tensor // (stable data pointer) and the row index is a graph input whose DATA // changes per step but whose pointer doesn't. A kv_start-offset view @@ -804,8 +828,8 @@ static ggml_tensor * build_laguna_attn_block( // stores all KV heads for a token in one row [head_dim*n_head_kv]. ggml_tensor * Krows = cache_head_major ? Kcur_rows : ggml_cont(ctx, Kcur_rows); ggml_tensor * Vrows = cache_head_major ? Vcur_rows : ggml_cont(ctx, Vcur_rows); - ggml_build_forward_expand(gf, ggml_set_rows(ctx, cache_k, Krows, kv_idx)); - ggml_build_forward_expand(gf, ggml_set_rows(ctx, cache_v, Vrows, kv_idx)); + ggml_build_forward_expand(gf, ggml_set_rows(ctx, cache_k, Krows, write_idx)); + ggml_build_forward_expand(gf, ggml_set_rows(ctx, cache_v, Vrows, write_idx)); } else { if (cache_head_major) { ggml_tensor * k_slot = ggml_view_2d(ctx, cache_k, @@ -841,8 +865,10 @@ static ggml_tensor * build_laguna_attn_block( // every downstream FA node's properties) stays constant across decode // steps; the mask carries -inf for [kv_len, kv_pad) and the cache buffer // is zero-initialised, so the padded tail contributes exactly nothing. + // [TAG_SWA_RING] ring layers always span the whole (constant) ring. const int win_start = 0; - const int win_len = kv_pad > 0 ? kv_pad : kv_len; + const int win_len = swa_ring ? (int)cache_k->ne[1] // rows in both layouts + : (kv_pad > 0 ? kv_pad : kv_len); ggml_tensor * Qfa = ggml_permute(ctx, Qcur, 0, 2, 1, 3); Qfa = ggml_cont(ctx, Qfa); @@ -891,6 +917,24 @@ static ggml_tensor * build_laguna_attn_block( return ggml_mul_mat(ctx, L.wo, attn); // [n_embd, n_tokens] } +// [TAG_SWA_RING] host fill for ring-mode SWA layers: row indices (pos % ring) +// plus the windowed causal mask in ring space. Position-derived only — no +// pager needed — and valid as long as ring >= sliding_window + n_tok (all +// visible positions occupy distinct ring slots and were written this request). +static void laguna_fill_swa_ring(int kv_start, int n_tok, int ring, int W, + std::vector & rows, + std::vector & mask) { + rows.resize((size_t)n_tok); + for (int i = 0; i < n_tok; ++i) rows[(size_t)i] = (kv_start + i) % ring; + mask.assign((size_t)ring * n_tok, -INFINITY); + for (int q = 0; q < n_tok; ++q) { + const int abs_q = kv_start + q; + const int lo = std::max(0, abs_q - W + 1); + for (int p = lo; p <= abs_q; ++p) + mask[(size_t)q * ring + (p % ring)] = 0.0f; + } +} + // ---- Layer dispatch ---------------------------------------------------- static ggml_tensor * build_laguna_layer( @@ -907,7 +951,8 @@ static ggml_tensor * build_laguna_layer( ggml_tensor * attn_mask_swa, const LagunaHybridMoe * hyb = nullptr, int kv_pad = 0, - ggml_tensor * kv_idx = nullptr) + ggml_tensor * kv_idx = nullptr, + ggml_tensor * kv_idx_swa = nullptr) { const LagunaTargetLayer & L = w.layers[il]; ggml_tensor * inp_f32 = graph_tensor_f32(ctx, inp); @@ -920,7 +965,7 @@ static ggml_tensor * build_laguna_layer( cur = build_laguna_attn_block(ctx, gf, w, L, il, cur, positions, cache.attn_k[il], cache.attn_v[il], attn_mask, attn_mask_swa, kv_start, n_tokens, is_full, - kv_pad, kv_idx); + kv_pad, kv_idx, kv_idx_swa); // Residual ggml_tensor * ffn_inp = ggml_add(ctx, cur, inp_f32); @@ -1114,7 +1159,8 @@ LagunaGraphOutputs build_laguna_graph( for (int il = 0; il < w.n_layer; ++il) { cur = build_laguna_layer(ctx, gf, w, cache, il, cur, in.positions, in.attn_mask, in.kv_start, in.n_tokens, - in.attn_mask_swa, in.hybrid, in.kv_pad, in.kv_idx); + in.attn_mask_swa, in.hybrid, in.kv_pad, in.kv_idx, + in.kv_idx_swa); // Feature capture for DFlash spec-decode: write residual-stream layer // outputs into the BF16 target feature ring. @@ -1261,6 +1307,9 @@ bool laguna_step( static const bool g_pad_cpy = (std::getenv("DFLASH_LAGUNA_PAD_CPY") != nullptr); int kv_cap = 0; for (int il = 0; il < w.n_layer; ++il) { + // [TAG_SWA_RING] ring-cached SWA tensors are smaller than the pool; + // the pool capacity must come from a full-attention layer. + if (!laguna_is_full_attn_layer(w, il)) continue; if (cache.attn_k[(size_t)il]) { kv_cap = (int)cache.attn_k[(size_t)il]->ne[1]; break; } } const int kv_pad = (!g_no_kvpad && kv_cap > 0) @@ -1415,6 +1464,22 @@ bool laguna_step( return true; } + // [TAG_PREFILL_PROF] batch-path sub-phase laps: DFLASH_LAGUNA_PREFILL_PROF=1. + // build = graph rebuild+alloc, fill = host mask/row fills, up = tensor_set + // uploads, gpu = graph_compute, read = logit/argmax readback. + static const bool g_pfprof = std::getenv("DFLASH_LAGUNA_PREFILL_PROF") != nullptr; + static thread_local double pf_build = 0, pf_fill = 0, pf_up = 0, pf_gpu = 0, + pf_read = 0; + static thread_local int pf_n = 0; + std::chrono::steady_clock::time_point pf_t; + auto pf_lap = [&]() { + auto t = std::chrono::steady_clock::now(); + double ms = std::chrono::duration(t - pf_t).count(); + pf_t = t; + return ms; + }; + if (g_pfprof) pf_t = std::chrono::steady_clock::now(); + // Same CUDA-graph-replay treatment as laguna_step_hybrid: persistent // arena (stable node addresses -> stable graph key), stride-padded KV // span, and set_rows K/V append (index is an input, so node properties @@ -1443,17 +1508,45 @@ bool laguna_step( ggml_set_input(kvi); } + // [TAG_SWA_RING] ring-mode SWA layers take their own (constant-width) + // mask and row indices; the ring needs masks plus batch headroom. + const int swa_ring = cache.swa_ring_rows; + if (swa_ring > 0 && (no_mask || n_tok + w.sliding_window > swa_ring)) { + std::fprintf(stderr, "laguna_step: swa-ring requires masks and " + "n_tok(%d) + window(%d) <= ring(%d)\n", + n_tok, w.sliding_window, swa_ring); + ggml_free(ctx); + return false; + } + const int mk_w_swa = swa_ring > 0 ? swa_ring : mk_w; + ggml_tensor * kvi_swa = nullptr; + if (swa_ring > 0) { + kvi_swa = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, n_tok); + ggml_set_input(kvi_swa); + } + ggml_tensor * mk_full = nullptr, * mk_full_cnv = nullptr; ggml_tensor * mk_swa = nullptr, * mk_swa_cnv = nullptr; if (!no_mask) { mk_full = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, mk_w, n_tok, 1, 1); ggml_set_input(mk_full); mk_full_cnv = ggml_cast(ctx, mk_full, GGML_TYPE_F16); - mk_swa = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, mk_w, n_tok, 1, 1); + mk_swa = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, mk_w_swa, n_tok, 1, 1); ggml_set_input(mk_swa); mk_swa_cnv = ggml_cast(ctx, mk_swa, GGML_TYPE_F16); } + // [TAG_FUSED_LOOP] feature append via set_rows with the ring row as INPUT + // data. The legacy offset-view append bakes the write offset into the + // graph, so node properties change every step and the ggml-cuda graph + // cache re-captures forever (measured: 728/728 evals eager on + // `laguna_target_feat` during pooled prefill). + ggml_tensor * feat_rows = nullptr; + if (capture && cache.target_feat && cache.target_feat_cap > 0) { + feat_rows = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, n_tok); + ggml_set_input(feat_rows); + } + LagunaGraphInputs gi{}; gi.inp_embed = ie; gi.positions = pp; @@ -1463,7 +1556,9 @@ bool laguna_step( gi.kv_start = kv_start; gi.kv_pad = kv_pad; gi.kv_idx = kvi; + gi.kv_idx_swa = kvi_swa; gi.capture_features = capture; + gi.target_feat_rows = feat_rows; gi.output_last_only = true; gi.output_logits = read_logits || out_argmax; gi.logits_are_output = read_logits; @@ -1484,11 +1579,13 @@ bool laguna_step( ggml_free(ctx); return false; } + if (g_pfprof) pf_build += pf_lap(); ggml_backend_tensor_set(ie, embed, 0, ggml_nbytes(ie)); std::vector pos((size_t)n_tok); for (int i = 0; i < n_tok; ++i) pos[i] = kv_start + i; ggml_backend_tensor_set(pp, pos.data(), 0, ggml_nbytes(pp)); + if (g_pfprof) pf_up += pf_lap(); if (kvflash) { if (!kvi) { @@ -1500,13 +1597,24 @@ bool laguna_step( std::vector rows; std::vector mfull, mswa; if (!kvflash_fill_rows_and_masks(*kvflash, kv_start, n_tok, mk_w, - w.sliding_window, rows, &mfull, &mswa)) { + w.sliding_window, rows, &mfull, + swa_ring > 0 ? nullptr : &mswa)) { ggml_free(ctx); return false; } + std::vector rows_swa; + if (swa_ring > 0) { + laguna_fill_swa_ring(kv_start, n_tok, swa_ring, w.sliding_window, + rows_swa, mswa); + } + if (g_pfprof) pf_fill += pf_lap(); ggml_backend_tensor_set(kvi, rows.data(), 0, ggml_nbytes(kvi)); + if (kvi_swa) { + ggml_backend_tensor_set(kvi_swa, rows_swa.data(), 0, ggml_nbytes(kvi_swa)); + } ggml_backend_tensor_set(mk_full, mfull.data(), 0, ggml_nbytes(mk_full)); ggml_backend_tensor_set(mk_swa, mswa.data(), 0, ggml_nbytes(mk_swa)); + if (g_pfprof) pf_up += pf_lap(); } else { if (kvi) { ggml_backend_tensor_set(kvi, pos.data(), 0, ggml_nbytes(kvi)); @@ -1524,24 +1632,42 @@ bool laguna_step( } ggml_backend_tensor_set(mk_full, mfull.data(), 0, ggml_nbytes(mk_full)); - std::vector mswa((size_t)mk_w * n_tok, -INFINITY); - const int W = w.sliding_window; - for (int q = 0; q < n_tok; ++q) { - const int abs_q = kv_start + q; - const int win_lo = std::max(0, abs_q - W + 1); - for (int k = win_lo; k <= abs_q && k < kv_len; ++k) { - mswa[(size_t)q * mk_w + k] = 0.0f; + std::vector mswa; + if (swa_ring > 0) { + std::vector rows_swa; + laguna_fill_swa_ring(kv_start, n_tok, swa_ring, w.sliding_window, + rows_swa, mswa); + ggml_backend_tensor_set(kvi_swa, rows_swa.data(), 0, + ggml_nbytes(kvi_swa)); + } else { + mswa.assign((size_t)mk_w * n_tok, -INFINITY); + const int W = w.sliding_window; + for (int q = 0; q < n_tok; ++q) { + const int abs_q = kv_start + q; + const int win_lo = std::max(0, abs_q - W + 1); + for (int k = win_lo; k <= abs_q && k < kv_len; ++k) { + mswa[(size_t)q * mk_w + k] = 0.0f; + } } } ggml_backend_tensor_set(mk_swa, mswa.data(), 0, ggml_nbytes(mk_swa)); } } + if (feat_rows) { + std::vector fr((size_t)n_tok); + const int cap = cache.target_feat_cap; + for (int i = 0; i < n_tok; ++i) fr[(size_t)i] = (kv_start + i) % cap; + ggml_backend_tensor_set(feat_rows, fr.data(), 0, ggml_nbytes(feat_rows)); + } + if (g_pfprof) pf_up += pf_lap(); + if (ggml_backend_graph_compute(backend, gf) != GGML_STATUS_SUCCESS) { std::fprintf(stderr, "laguna_step: graph_compute failed\n"); ggml_free(ctx); return false; } + if (g_pfprof) pf_gpu += pf_lap(); if (out_argmax) { ggml_backend_tensor_get(argmax, out_argmax, 0, sizeof(int32_t)); @@ -1553,6 +1679,16 @@ bool laguna_step( } else { out_logits.clear(); } + if (g_pfprof) { + pf_read += pf_lap(); + if (n_tok > 1 && ++pf_n % 32 == 0) { + std::fprintf(stderr, + "[prefill-prof] per-batch ms (n=%d, n_tok=%d, mk_w=%d): " + "build=%.2f fill=%.2f up=%.2f gpu=%.2f read=%.2f\n", + pf_n, n_tok, mk_w, pf_build / pf_n, pf_fill / pf_n, + pf_up / pf_n, pf_gpu / pf_n, pf_read / pf_n); + } + } cache.cur_pos = kv_len; if (out_argmax) cache.last_tok = *out_argmax; @@ -1580,6 +1716,9 @@ bool laguna_verify_batch( static const bool g_pad_cpy = (std::getenv("DFLASH_LAGUNA_PAD_CPY") != nullptr); int kv_cap = 0; for (int il = 0; il < w.n_layer; ++il) { + // [TAG_SWA_RING] ring-cached SWA tensors are smaller than the pool; + // the pool capacity must come from a full-attention layer. + if (!laguna_is_full_attn_layer(w, il)) continue; if (cache.attn_k[(size_t)il]) { kv_cap = (int)cache.attn_k[(size_t)il]->ne[1]; break; } } const int kv_pad = (!g_no_kvpad && kv_cap > 0) @@ -1602,6 +1741,7 @@ bool laguna_verify_batch( ggml_cgraph * gf = nullptr; ggml_gallocr_t galloc = nullptr; ggml_tensor *ie = nullptr, *pp = nullptr, *kvi = nullptr; + ggml_tensor *kvi_swa = nullptr; // [TAG_SWA_RING] ggml_tensor *mk_full = nullptr, *mk_swa = nullptr, *feat_rows = nullptr; ggml_tensor *argmax = nullptr, *logits = nullptr; int n_tokens = 0, mk_w = 0, kv_pad = 0, kv_cap = 0; @@ -1653,10 +1793,18 @@ bool laguna_verify_batch( ggml_set_input(kvi); } + // [TAG_SWA_RING] ring-mode SWA layers: own row indices + ring-width mask. + ggml_tensor * kvi_swa = nullptr; + if (cache.swa_ring_rows > 0) { + kvi_swa = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, n_tokens); + ggml_set_input(kvi_swa); + } + const int mk_w_swa = cache.swa_ring_rows > 0 ? cache.swa_ring_rows : mk_w; + ggml_tensor * mk_full = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, mk_w, n_tokens, 1, 1); ggml_set_input(mk_full); ggml_tensor * mk_full_cnv = ggml_cast(ctx, mk_full, GGML_TYPE_F16); - ggml_tensor * mk_swa = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, mk_w, n_tokens, 1, 1); + ggml_tensor * mk_swa = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, mk_w_swa, n_tokens, 1, 1); ggml_set_input(mk_swa); ggml_tensor * mk_swa_cnv = ggml_cast(ctx, mk_swa, GGML_TYPE_F16); @@ -1675,6 +1823,7 @@ bool laguna_verify_batch( gi.kv_start = kv_start; gi.kv_pad = kv_pad; gi.kv_idx = kvi; + gi.kv_idx_swa = kvi_swa; gi.output_last_only = false; gi.output_logits = true; gi.logits_are_output = out_logits != nullptr; @@ -1699,7 +1848,7 @@ bool laguna_verify_batch( return false; } S.ctx = ctx; S.gf = gf; - S.ie = ie; S.pp = pp; S.kvi = kvi; + S.ie = ie; S.pp = pp; S.kvi = kvi; S.kvi_swa = kvi_swa; S.mk_full = mk_full; S.mk_swa = mk_swa; S.feat_rows = feat_rows; S.argmax = argmax; S.logits = go.logits; S.n_tokens = n_tokens; S.mk_w = mk_w; S.kv_pad = kv_pad; S.kv_cap = kv_cap; @@ -1711,16 +1860,37 @@ bool laguna_verify_batch( ggml_tensor * ie = S.ie; ggml_tensor * pp = S.pp; ggml_tensor * kvi = S.kvi; + ggml_tensor * kvi_swa = S.kvi_swa; ggml_tensor * mk_full = S.mk_full; ggml_tensor * mk_swa = S.mk_swa; ggml_tensor * feat_rows = S.feat_rows; ggml_tensor * argmax = S.argmax; + const int swa_ring = cache.swa_ring_rows; + + // [TAG_VERIFY_PROF] sub-phase laps: DFLASH_LAGUNA_VERIFY_PROF=1. + // prep = stage+embed/pos fills, mask = kvflash rows+mask fill+memcpy, + // upwait = sync after async uploads (isolates upload cost from compute), + // gpu = graph_compute, read = argmax/logits readback. + static const bool g_vprof = std::getenv("DFLASH_LAGUNA_VERIFY_PROF") != nullptr; + static thread_local double vp_prep = 0, vp_mask = 0, vp_up = 0, vp_gpu = 0, + vp_read = 0; + static thread_local int vp_n = 0; + std::chrono::steady_clock::time_point vp_t; + auto vp_lap = [&]() { + auto t = std::chrono::steady_clock::now(); + double ms = std::chrono::duration(t - vp_t).count(); + vp_t = t; + return ms; + }; + if (g_vprof) vp_t = std::chrono::steady_clock::now(); const size_t embed_sz = ggml_nbytes(ie); const size_t pos_sz = (size_t)n_tokens * sizeof(int32_t); const size_t mask_sz = (size_t)mk_w * n_tokens * sizeof(float); + const size_t mswa_sz = (size_t)(swa_ring > 0 ? swa_ring : mk_w) * + n_tokens * sizeof(float); uint8_t * st = laguna_host_stage(backend, S.stage_buf, - embed_sz + 3 * pos_sz + 2 * mask_sz); + embed_sz + 4 * pos_sz + mask_sz + mswa_sz); if (!st) { std::fprintf(stderr, "laguna_verify_batch: host stage alloc failed\n"); return false; @@ -1729,8 +1899,9 @@ bool laguna_verify_batch( int32_t * st_pos = (int32_t *) (st + embed_sz); int32_t * st_kvi = (int32_t *) (st + embed_sz + pos_sz); int32_t * st_feat = (int32_t *) (st + embed_sz + 2 * pos_sz); - float * st_mfull = (float *) (st + embed_sz + 3 * pos_sz); - float * st_mswa = (float *) (st + embed_sz + 3 * pos_sz + mask_sz); + int32_t * st_kvi_s = (int32_t *) (st + embed_sz + 3 * pos_sz); + float * st_mfull = (float *) (st + embed_sz + 4 * pos_sz); + float * st_mswa = (float *) (st + embed_sz + 4 * pos_sz + mask_sz); std::memcpy(st_embed, embed, embed_sz); ggml_backend_tensor_set_async(backend, ie, st_embed, 0, embed_sz); @@ -1743,6 +1914,7 @@ bool laguna_verify_batch( ggml_backend_tensor_set_async(backend, feat_rows, st_feat, 0, pos_sz); } + if (g_vprof) vp_prep += vp_lap(); if (kvflash) { if (!kvi) { std::fprintf(stderr, "laguna_verify_batch: kvflash requires the kv_pad " @@ -1753,16 +1925,24 @@ bool laguna_verify_batch( std::vector rows; std::vector mfull, mswa; if (!kvflash_fill_rows_and_masks(*kvflash, kv_start, n_tokens, mk_w, - w.sliding_window, rows, &mfull, &mswa)) { + w.sliding_window, rows, &mfull, + swa_ring > 0 ? nullptr : &mswa)) { ggml_free(S.ctx); S.ctx = nullptr; return false; } + if (swa_ring > 0) { + std::vector rows_swa; + laguna_fill_swa_ring(kv_start, n_tokens, swa_ring, w.sliding_window, + rows_swa, mswa); + std::memcpy(st_kvi_s, rows_swa.data(), pos_sz); + ggml_backend_tensor_set_async(backend, kvi_swa, st_kvi_s, 0, pos_sz); + } std::memcpy(st_kvi, rows.data(), pos_sz); std::memcpy(st_mfull, mfull.data(), mask_sz); - std::memcpy(st_mswa, mswa.data(), mask_sz); + std::memcpy(st_mswa, mswa.data(), mswa_sz); ggml_backend_tensor_set_async(backend, kvi, st_kvi, 0, pos_sz); ggml_backend_tensor_set_async(backend, mk_full, st_mfull, 0, mask_sz); - ggml_backend_tensor_set_async(backend, mk_swa, st_mswa, 0, mask_sz); + ggml_backend_tensor_set_async(backend, mk_swa, st_mswa, 0, mswa_sz); } else { if (kvi) { ggml_backend_tensor_set_async(backend, kvi, st_pos, 0, pos_sz); @@ -1777,16 +1957,31 @@ bool laguna_verify_batch( } ggml_backend_tensor_set_async(backend, mk_full, st_mfull, 0, mask_sz); - std::fill(st_mswa, st_mswa + (size_t)mk_w * n_tokens, -INFINITY); - const int W = w.sliding_window; - for (int q = 0; q < n_tokens; ++q) { - const int abs_q = kv_start + q; - const int win_lo = std::max(0, abs_q - W + 1); - for (int k = win_lo; k <= abs_q && k < kv_len; ++k) { - st_mswa[(size_t)q * mk_w + k] = 0.0f; + if (swa_ring > 0) { + std::vector rows_swa; + std::vector mswa_ring; + laguna_fill_swa_ring(kv_start, n_tokens, swa_ring, w.sliding_window, + rows_swa, mswa_ring); + std::memcpy(st_kvi_s, rows_swa.data(), pos_sz); + std::memcpy(st_mswa, mswa_ring.data(), mswa_sz); + ggml_backend_tensor_set_async(backend, kvi_swa, st_kvi_s, 0, pos_sz); + } else { + std::fill(st_mswa, st_mswa + (size_t)mk_w * n_tokens, -INFINITY); + const int W = w.sliding_window; + for (int q = 0; q < n_tokens; ++q) { + const int abs_q = kv_start + q; + const int win_lo = std::max(0, abs_q - W + 1); + for (int k = win_lo; k <= abs_q && k < kv_len; ++k) { + st_mswa[(size_t)q * mk_w + k] = 0.0f; + } } } - ggml_backend_tensor_set_async(backend, mk_swa, st_mswa, 0, mask_sz); + ggml_backend_tensor_set_async(backend, mk_swa, st_mswa, 0, mswa_sz); + } + if (g_vprof) { + vp_mask += vp_lap(); + ggml_backend_synchronize(backend); + vp_up += vp_lap(); } if (ggml_backend_graph_compute(backend, gf) != GGML_STATUS_SUCCESS) { @@ -1794,6 +1989,7 @@ bool laguna_verify_batch( ggml_free(S.ctx); S.ctx = nullptr; return false; } + if (g_vprof) vp_gpu += vp_lap(); out_argmax.resize((size_t)n_tokens); ggml_backend_tensor_get(argmax, out_argmax.data(), 0, @@ -1804,6 +2000,16 @@ bool laguna_verify_batch( ggml_backend_tensor_get(S.logits, out_logits->data(), 0, sizeof(float) * out_logits->size()); } + if (g_vprof) { + vp_read += vp_lap(); + if (++vp_n % 100 == 0) { + std::fprintf(stderr, + "[verify-prof] per-call ms (n=%d, w=%d): prep=%.2f mask=%.2f " + "upwait=%.2f gpu=%.2f read=%.2f\n", + vp_n, n_tokens, vp_prep / vp_n, vp_mask / vp_n, vp_up / vp_n, + vp_gpu / vp_n, vp_read / vp_n); + } + } cache.cur_pos = kv_len; cache.last_tok = out_argmax.empty() ? -1 : out_argmax.back(); @@ -1938,6 +2144,9 @@ bool laguna_step_hybrid( static const bool g_no_kvpad = (std::getenv("DFLASH_LAGUNA_NO_KVPAD") != nullptr); int kv_cap = 0; for (int il = 0; il < w.n_layer; ++il) { + // [TAG_SWA_RING] ring-cached SWA tensors are smaller than the pool; + // the pool capacity must come from a full-attention layer. + if (!laguna_is_full_attn_layer(w, il)) continue; if (cache.attn_k[(size_t)il]) { kv_cap = (int)cache.attn_k[(size_t)il]->ne[1]; break; } } const int kv_pad = (!g_no_kvpad && kv_cap > 0) From 1b5a5fb51f51a4323da55fe87256b970f1b2a9b3 Mon Sep 17 00:00:00 2001 From: mrciffa <49000955+davide221@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:49:05 +0200 Subject: [PATCH 09/20] feat(qwen35): adopt the drafter context-KV ring cache Wires the shared common/dflash_draft_kv module into the qwen35 backend's local-draft path (same pattern as laguna: lazy init, built_for guard on drafter switch, DFLASH_DRAFT_KV=0 kill switch, legacy path preserved). Remote-draft (IPC) requests keep the legacy path. Also adds distinct error messages to the bulk-append failure paths in dflash_draft_kv.cpp; a mismatched drafter now fails gracefully with the offending dims where the legacy path hit a GGML_ASSERT OOB write. A/B on Qwen3.6-27B Q4_K_M + dflash-draft-3.6-q4_k_m (RTX 3090): short 83.7 -> 96.3 tok/s (accept 38.0 -> 41.2%), 8K real code 26.8 -> 31.6 tok/s (19.3 -> 18.7%), outputs coherent both legs. --- server/src/common/dflash_draft_kv.cpp | 10 ++- server/src/qwen35/qwen35_backend.cpp | 108 ++++++++++++++++++-------- server/src/qwen35/qwen35_backend.h | 4 + 3 files changed, 88 insertions(+), 34 deletions(-) diff --git a/server/src/common/dflash_draft_kv.cpp b/server/src/common/dflash_draft_kv.cpp index 564a5941c..3494894fc 100644 --- a/server/src/common/dflash_draft_kv.cpp +++ b/server/src/common/dflash_draft_kv.cpp @@ -170,9 +170,15 @@ static bool draft_kv_bulk_append(DraftKvState & st, ggml_tensor * tpos = ggml_new_tensor_1d(tctx, GGML_TYPE_I32, c); ggml_tensor * trow = ggml_new_tensor_1d(tctx, GGML_TYPE_I32, c); ggml_backend_buffer_t tbuf = ggml_backend_alloc_ctx_tensors(tctx, backend); - if (!tbuf) { ggml_free(tctx); return false; } + if (!tbuf) { + std::fprintf(stderr, "[draft-kv] bulk: input alloc failed (n=%d fc_in=%d)\n", c, st.fc_in); + ggml_free(tctx); + return false; + } bool ok = copy_feature_ring_range_to_tensor(ring, feat, start, c); + if (!ok) std::fprintf(stderr, "[draft-kv] bulk: ring copy failed (start=%d n=%d ring_cap=%d fc_in=%d ring_type=%d)\n", + start, c, ring.cap, st.fc_in, (int)ring.storage_type); if (ok) { pos.resize((size_t)c); rows.resize((size_t)c); @@ -196,11 +202,13 @@ static bool draft_kv_bulk_append(DraftKvState & st, ggml_cgraph * g = ggml_new_graph_custom(gctx, 4096, false); DraftKvAppendInputs ai{c, feat, tpos, trow}; ok = build_draft_kv_append(gctx, g, dw, st.cache, ai); + if (!ok) std::fprintf(stderr, "[draft-kv] bulk: append build failed\n"); if (ok) { ggml_gallocr_t ga = ggml_gallocr_new(ggml_backend_get_default_buffer_type(backend)); ok = ga && ggml_gallocr_alloc_graph(ga, g) && ggml_backend_graph_compute(backend, g) == GGML_STATUS_SUCCESS; + if (!ok) std::fprintf(stderr, "[draft-kv] bulk: graph alloc/compute failed (n=%d)\n", c); if (ga) ggml_gallocr_free(ga); } ggml_free(gctx); diff --git a/server/src/qwen35/qwen35_backend.cpp b/server/src/qwen35/qwen35_backend.cpp index d37c1b758..a27ed0979 100644 --- a/server/src/qwen35/qwen35_backend.cpp +++ b/server/src/qwen35/qwen35_backend.cpp @@ -730,6 +730,7 @@ void Qwen35Backend::shutdown() { step_graph_destroy(draft_sg_); step_graph_destroy(proj_sg_); remote_draft_.close(); + draft_kv_free(draft_kv_); draft_feature_mirror_free(feature_mirror_); for (int i = 0; i < PREFIX_SLOTS; i++) { free_prefix_snapshot(prefix_snapshots_[i]); @@ -1999,42 +2000,83 @@ bool Qwen35Backend::do_spec_decode(int committed, int n_gen, return false; } } else { - if (!build_draft_step(draft_sg, dw_, /*lm_head=*/nullptr, draft_backend_, - draft_ctx, use_mirror_view ? &feature_mirror_ : nullptr, - committed, - /*ctx_len_max=*/std::min(ring_cap, std::max(DRAFT_CTX_MAX_DEFAULT, cfg_.draft_ctx_max)))) { - std::fprintf(stderr, "spec-decode: draft build failed\n"); - step_graph_destroy(draft_sg); - return false; - } - if (!use_mirror_view && - !copy_feature_ring_range_to_tensor(feature_mirror_, draft_sg.target_hidden_cat, - draft_start, draft_ctx)) { - std::fprintf(stderr, "spec-decode: feature copy failed\n"); - step_graph_destroy(draft_sg); - return false; + // [TAG_DRAFT_KV] ring-cached drafter context KV: append newly + // committed rows instead of re-encoding the whole feature window. + static const bool draft_kv_on = []() { + const char * e = std::getenv("DFLASH_DRAFT_KV"); + return !(e && e[0] == '0' && e[1] == '\0'); + }(); + bool use_draft_kv = draft_kv_on && feature_mirror_.target_feat != nullptr; + if (use_draft_kv && draft_kv_.gf && + draft_kv_.built_for != (const void *)&dw_) { + draft_kv_free(draft_kv_); } - ggml_backend_tensor_set(draft_sg.inp_embed, noise_embed.data(), 0, - sizeof(float) * noise_embed.size()); - pos_k.resize((size_t)draft_ctx + q_len); - for (int i = 0; i < q_len; i++) pos_q[i] = draft_ctx + i; - for (int i = 0; i < draft_ctx + q_len; i++) pos_k[i] = i; - ggml_backend_tensor_set(draft_sg.positions, pos_q.data(), 0, - sizeof(int32_t) * pos_q.size()); - ggml_backend_tensor_set(draft_sg.positions_k, pos_k.data(), 0, - sizeof(int32_t) * pos_k.size()); - - auto st = ggml_backend_graph_compute(draft_backend_, draft_sg.gf); - if (st != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "spec-decode: draft compute failed\n"); - step_graph_destroy(draft_sg); - return false; + if (use_draft_kv && !draft_kv_.gf) { + const int kv_cap = std::min(ring_cap, + std::max(DRAFT_CTX_MAX_DEFAULT, cfg_.draft_ctx_max)); + if (!draft_kv_init(draft_kv_, dw_, draft_backend_, kv_cap, nullptr)) { + draft_kv_free(draft_kv_); + use_draft_kv = false; + std::fprintf(stderr, + "spec-decode: draft-kv init failed; using legacy draft path\n"); + } } + if (use_draft_kv) { + if (!draft_kv_begin_step(draft_kv_, dw_, draft_backend_, + feature_mirror_, committed)) { + std::fprintf(stderr, "spec-decode: draft-kv step prep failed\n"); + step_graph_destroy(draft_sg); + return false; + } + ggml_backend_tensor_set(draft_kv_.inp_embed, noise_embed.data(), 0, + sizeof(float) * noise_embed.size()); + if (ggml_backend_graph_compute(draft_backend_, draft_kv_.gf) != + GGML_STATUS_SUCCESS) { + std::fprintf(stderr, "spec-decode: draft-kv compute failed\n"); + step_graph_destroy(draft_sg); + return false; + } + local_hidden.resize((size_t)hidden * q_len); + ggml_backend_tensor_get(draft_kv_.hidden_states, local_hidden.data(), 0, + sizeof(float) * local_hidden.size()); + } else { + if (!build_draft_step(draft_sg, dw_, /*lm_head=*/nullptr, draft_backend_, + draft_ctx, use_mirror_view ? &feature_mirror_ : nullptr, + committed, + /*ctx_len_max=*/std::min(ring_cap, std::max(DRAFT_CTX_MAX_DEFAULT, cfg_.draft_ctx_max)))) { + std::fprintf(stderr, "spec-decode: draft build failed\n"); + step_graph_destroy(draft_sg); + return false; + } + if (!use_mirror_view && + !copy_feature_ring_range_to_tensor(feature_mirror_, draft_sg.target_hidden_cat, + draft_start, draft_ctx)) { + std::fprintf(stderr, "spec-decode: feature copy failed\n"); + step_graph_destroy(draft_sg); + return false; + } + ggml_backend_tensor_set(draft_sg.inp_embed, noise_embed.data(), 0, + sizeof(float) * noise_embed.size()); + pos_k.resize((size_t)draft_ctx + q_len); + for (int i = 0; i < q_len; i++) pos_q[i] = draft_ctx + i; + for (int i = 0; i < draft_ctx + q_len; i++) pos_k[i] = i; + ggml_backend_tensor_set(draft_sg.positions, pos_q.data(), 0, + sizeof(int32_t) * pos_q.size()); + ggml_backend_tensor_set(draft_sg.positions_k, pos_k.data(), 0, + sizeof(int32_t) * pos_k.size()); + + auto st = ggml_backend_graph_compute(draft_backend_, draft_sg.gf); + if (st != GGML_STATUS_SUCCESS) { + std::fprintf(stderr, "spec-decode: draft compute failed\n"); + step_graph_destroy(draft_sg); + return false; + } - // Read draft hidden states to host for LM-head projection. - local_hidden.resize((size_t)hidden * q_len); - ggml_backend_tensor_get(draft_sg.hidden_states, local_hidden.data(), 0, - sizeof(float) * local_hidden.size()); + // Read draft hidden states to host for LM-head projection. + local_hidden.resize((size_t)hidden * q_len); + ggml_backend_tensor_get(draft_sg.hidden_states, local_hidden.data(), 0, + sizeof(float) * local_hidden.size()); + } } // 3. Project draft hidden → token IDs via target LM head diff --git a/server/src/qwen35/qwen35_backend.h b/server/src/qwen35/qwen35_backend.h index 53e5b6915..5fb25bfb3 100644 --- a/server/src/qwen35/qwen35_backend.h +++ b/server/src/qwen35/qwen35_backend.h @@ -19,6 +19,7 @@ #include "step_graph.h" #include "ddtree.h" #include "dflash_feature_ring.h" +#include "common/dflash_draft_kv.h" #include "internal.h" // TargetWeights, TargetCache, DraftWeights, PrefixSnapshot #include "qwen3/qwen3_drafter.h" // DrafterContext, load_drafter, free_drafter, drafter_score_and_compress #include "kvflash_pager.h" // bounded KV residency pool @@ -233,6 +234,9 @@ class Qwen35Backend : public ModelBackend { // ── Draft feature mirror (cross-GPU feature transfer) ──────────── DraftFeatureMirror feature_mirror_; + // [TAG_DRAFT_KV] drafter context-KV ring cache (lazy-init; kill with + // DFLASH_DRAFT_KV=0). Shared module: common/dflash_draft_kv.h. + DraftKvState draft_kv_; DFlashDraftIpcClient remote_draft_; // ── Prefix cache (snapshots) ───────────────────────────────────── From bf3846152b510f554b04cdbd3ff3a50ca7ed6a5b Mon Sep 17 00:00:00 2001 From: mrciffa <49000955+davide221@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:04:56 +0200 Subject: [PATCH 10/20] fix(spec): address review findings on the drafter ring cache - Reset the drafter ring at the start of every spec-decode request in both backends: the state persists across requests, so a new conversation could draft against the previous request's cached rows for positions below next_pos. The first begin_step re-appends the live window from the feature mirror (~10ms once per request). Validated: identical speed, acceptance and output across back-to-back requests. - StepGraph records built_view; the copy-mode persistent fast path refuses graphs whose topology reads features through a mirror ring view (reachable with DFLASH_DRAFT_PERSIST=0). - DraftKvState is now non-copyable: it owns raw ggml contexts, buffers and graphs, so a copy would double-free. - Document that the drafter SWA window is anchored at committed for all noise rows on purpose: it replicates the legacy one-shot graph's single windowed view, which is the trained drafter's validated attention pattern. --- server/src/common/dflash_draft_graph.cpp | 3 ++- server/src/common/dflash_draft_kv.cpp | 5 +++++ server/src/common/dflash_draft_kv.h | 5 +++++ server/src/common/step_graph.h | 5 +++++ server/src/laguna/laguna_backend.cpp | 4 ++++ server/src/qwen35/qwen35_backend.cpp | 4 ++++ 6 files changed, 25 insertions(+), 1 deletion(-) diff --git a/server/src/common/dflash_draft_graph.cpp b/server/src/common/dflash_draft_graph.cpp index c957bbb8a..9b719ea71 100644 --- a/server/src/common/dflash_draft_graph.cpp +++ b/server/src/common/dflash_draft_graph.cpp @@ -152,7 +152,7 @@ bool build_draft_step( // refresh the two ctx_len-dependent masks. Feature contents, positions // and noise embeds are re-uploaded by the caller every step regardless, // and the pad feature rows beyond ctx_len were zeroed at bucket build. - if (sg.gf && sg.ctx_alloc > 0 && !mirror && pad_ctx && + if (sg.gf && sg.ctx_alloc > 0 && !sg.built_view && !mirror && pad_ctx && sg.ctx_alloc == ((ctx_len + 63) & ~63)) { const int q_len = dw.block_size; const int ctx_alloc = sg.ctx_alloc; @@ -243,6 +243,7 @@ bool build_draft_step( return false; } sg.ctx_alloc = do_pad ? ctx_alloc : 0; + sg.built_view = use_view; if (!ggml_gallocr_alloc_graph(sg.alloc, sg.gf)) { return false; diff --git a/server/src/common/dflash_draft_kv.cpp b/server/src/common/dflash_draft_kv.cpp index 3494894fc..05ec25aca 100644 --- a/server/src/common/dflash_draft_kv.cpp +++ b/server/src/common/dflash_draft_kv.cpp @@ -293,6 +293,11 @@ bool draft_kv_begin_step(DraftKvState & st, sizeof(uint16_t) * mask_elems); } if (st.mask_swa) { + // The window is anchored at `committed` for ALL noise rows on purpose: + // this replicates the legacy one-shot drafter graph, which windows the + // ctx via a single view [ctx_len - swa_window, ctx_len) shared by every + // query row. A per-row lower bound would change the trained drafter's + // attention pattern (and measured acceptance). const int eff_win = (dw.swa_window > 0 && win > dw.swa_window) ? dw.swa_window : win; const int swa_lo = committed - eff_win; diff --git a/server/src/common/dflash_draft_kv.h b/server/src/common/dflash_draft_kv.h index e40872a56..888f9b46b 100644 --- a/server/src/common/dflash_draft_kv.h +++ b/server/src/common/dflash_draft_kv.h @@ -32,6 +32,11 @@ namespace dflash::common { struct DraftKvState { + DraftKvState() = default; + // Owns raw ggml contexts/buffers/graphs: copying would double-free. + DraftKvState(const DraftKvState &) = delete; + DraftKvState & operator=(const DraftKvState &) = delete; + // geometry int cap = 0; // ctx ring slots (== drafter feature window cap) int q_len = 0; // noise block size diff --git a/server/src/common/step_graph.h b/server/src/common/step_graph.h index 3eefe62c5..b09a91341 100644 --- a/server/src/common/step_graph.h +++ b/server/src/common/step_graph.h @@ -42,6 +42,10 @@ struct StepGraph { // When >0, the draft graph was built with the ctx dimension padded to this // size (stable topology for CUDA-graph replay); noise keys start here. int ctx_alloc = 0; + // True when the built topology reads features through a mirror ring VIEW + // (no target_hidden_cat input tensor). A view-built graph must never be + // reused by the copy-mode persistent fast path. + bool built_view = false; ggml_tensor * hidden_input = nullptr; // lm-head projection only // [n_tokens,n_head_kv] i64; step-invariant KV write (carries kv_start). Null on non-graph paths. ggml_tensor * kv_write_rows = nullptr; @@ -71,6 +75,7 @@ inline void step_graph_free(StepGraph & sg) { sg.target_hidden_cat = sg.positions_k = nullptr; sg.pad_mask_full = nullptr; sg.ctx_alloc = 0; + sg.built_view = false; sg.hidden_input = nullptr; sg.parent_ids = nullptr; sg.kv_write_rows = nullptr; diff --git a/server/src/laguna/laguna_backend.cpp b/server/src/laguna/laguna_backend.cpp index dac541ea3..ee9cafa0e 100644 --- a/server/src/laguna/laguna_backend.cpp +++ b/server/src/laguna/laguna_backend.cpp @@ -681,6 +681,10 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, "[laguna-spec] draft-kv init failed; using legacy draft path\n"); } } + // The ring persists across requests but its rows belong to the previous + // conversation; start every request from an empty ring (the first + // begin_step bulk-appends the live window from the feature mirror). + if (use_draft_kv) draft_kv_reset(draft_kv_); auto t_dec0 = std::chrono::steady_clock::now(); static const bool step_prof = std::getenv("DFLASH_LAGUNA_STEP_PROF") != nullptr; diff --git a/server/src/qwen35/qwen35_backend.cpp b/server/src/qwen35/qwen35_backend.cpp index a27ed0979..6869f4b3c 100644 --- a/server/src/qwen35/qwen35_backend.cpp +++ b/server/src/qwen35/qwen35_backend.cpp @@ -1797,6 +1797,10 @@ bool Qwen35Backend::do_spec_decode(int committed, int n_gen, bool * degenerate_close_out) { out_accept_rate = 0.0f; out_spec_ran = false; + // [TAG_DRAFT_KV] the drafter ring persists across requests but its rows + // belong to the previous conversation; start every request empty (the + // first begin_step bulk-appends the live window from the feature mirror). + if (draft_kv_.gf) draft_kv_reset(draft_kv_); const int hidden = w_.n_embd; // First token: use the argmax that do_prefill already sampled and stored. From 0942a2da8bd37f6cae78e0fb3a53eef57953db33 Mon Sep 17 00:00:00 2001 From: mrciffa <49000955+davide221@users.noreply.github.com> Date: Fri, 10 Jul 2026 17:42:20 +0200 Subject: [PATCH 11/20] fix(review): harden the mmid/adaptive-K/ring surfaces for merge - Adaptive-K gates a scratch COPY of the router ids: the -1 drop sentinels never reach consumers on the non-grouped path (a sibling weight with a non-grouped quant type would cast them to uint32_t and read OOB). The shared zeroed gate weights keep every consumer bit-correct, and the already-gated marker is now weights-based since ids copies are per-op. - Grouped MUL_MAT_ID falls back to the legacy kernel above MMID_GROUPED_MAX_PAIRS instead of aborting on a hard assert. - q8 memoization re-enabled for MUL_MAT_ID (quantization is ids-independent and the memo key never included ids). - SWA ring guards: build_laguna_graph asserts ring => kv_idx_swa; laguna_layer_step refuses ring caches (unwired path fails loudly, not OOB). - Adaptive-K extras pooled per tensor address (was one leak per graph build); tau/DENSE/theta/min env knobs and --adaptive-experts get strict validated parsing; il<0 families warn once that dense exclusions cannot apply yet (layer-index threading tracked as follow-up). - Draft GGUF loader frees the metadata arena on all early validation failures; requant tool refuses src==dst (in-place corruption). - GGML_CUDA_GRAPH_STATS_EVERY clamped positive (division by zero); topk-rows sync contract + output buffer sizes documented. Validated: laguna ring A/B per-step parity (17.2/22.8 ms per step vs 17.3/23.1 pre-fix), opt-in grouped+adaptive-K leg 230 tok/s short / 126.7 @8K, coherent output, acceptance healthy. --- .../deps/llama.cpp/ggml/include/ggml-cuda.h | 6 ++- .../llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu | 3 +- .../deps/llama.cpp/ggml/src/ggml-cuda/mmvq.cu | 28 ++++++++--- .../llama.cpp/ggml/src/ggml-cuda/topk-rows.cu | 3 ++ server/scripts/requant_dflash_draft.py | 5 ++ server/src/common/adaptive_verify_width.h | 22 +++++++- server/src/common/dflash_target.h | 5 +- server/src/common/mmid_adaptive_k.h | 50 ++++++++++++++++--- server/src/draft/draft_gguf_loader.cpp | 18 +++++++ server/src/laguna/laguna_target_graph.cpp | 15 +++++- server/src/server/server_main.cpp | 7 +++ 11 files changed, 142 insertions(+), 20 deletions(-) diff --git a/server/deps/llama.cpp/ggml/include/ggml-cuda.h b/server/deps/llama.cpp/ggml/include/ggml-cuda.h index f9693057f..091e91297 100644 --- a/server/deps/llama.cpp/ggml/include/ggml-cuda.h +++ b/server/deps/llama.cpp/ggml/include/ggml-cuda.h @@ -46,8 +46,10 @@ GGML_BACKEND_API void ggml_backend_cuda_unregister_host_buffer(void * buffer); GGML_BACKEND_API ggml_backend_reg_t ggml_backend_cuda_reg(void); // [TAG_TOPK_ROWS] top-k (k <= 8) entries + softmax probabilities per row of a -// device-resident contiguous F32 [ncols, nrows] tensor; ~KB of readback. Not -// a graph op: call after the producing graph_compute has returned. +// device-resident contiguous F32 [ncols, nrows] tensor. probs_out and ids_out +// must each hold k * nrows elements (row-major: entry [r*k + j] = rank-j of +// row r). Not a graph op: call only after the SYNCHRONOUS +// ggml_backend_graph_compute() producing `logits` has returned. GGML_BACKEND_API bool ggml_backend_cuda_topk_rows(const struct ggml_tensor * logits, int k, float * probs_out, int32_t * ids_out); diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu index 07175145b..3aa2eb481 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu @@ -4390,7 +4390,8 @@ static enum ggml_status ggml_backend_cuda_graph_compute(ggml_backend_t backend, else g->stat_eager++; static const int stats_every = [](){ const char * e = getenv("GGML_CUDA_GRAPH_STATS_EVERY"); - return e ? atoi(e) : 200; + const int v = e ? atoi(e) : 200; + return v > 0 ? v : 200; // guard % 0 on malformed input }(); if (g->stat_total % stats_every == 0) { GGML_LOG_INFO("[cuda-graph-stats] key=%p n_nodes=%d total=%llu replay=%llu capture=%llu eager=%llu enabled=%d\n", diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/mmvq.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/mmvq.cu index e897e501c..ec6288f97 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/mmvq.cu +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/mmvq.cu @@ -834,13 +834,15 @@ static __global__ void mmid_group_prep( const int i = threadIdx.x; // [TAG_MMID_ADAPTIVE_K] one thread per token: keep slots until cumulative // combine weight >= tau, sentinel the rest, renormalize kept in place. - // Skips tokens that already contain a -1 (second/third mmid of the layer). + // A row is "already gated" (second/third mmid of the layer) when a slot + // weight is exactly 0.0f: `ids` is now a per-op scratch copy, so the + // shared zeroed weights are the only marker that survives across ops. if (gate_w != nullptr && i < n_tok) { int32_t * idrow = ids + i*ids_stride; float * wrow = gate_w + i*gate_w_stride; bool gated = false; for (int j = 0; j < n_slots; ++j) { - gated = gated || (idrow[j] < 0); + gated = gated || (idrow[j] < 0) || (wrow[j] == 0.0f); } if (!gated) { float cum = 0.0f; @@ -1587,7 +1589,7 @@ void ggml_cuda_mul_mat_vec_q( const size_t q8_bytes = ne13*ne12 * ne11*ne10_padded * sizeof(block_q8_1)/QK8_1; ggml_cuda_pool_alloc src1_q8_1(ctx.pool()); char * src1_q8_d = nullptr; - if (luce_q8_memo_on && !ids) { + if (luce_q8_memo_on) { for (const auto & e : ctx.luce_q8_memo) { if (e.src1_node == (const void *) src1 && e.src1_data == (const void *) src1_d && e.src0_type == (int) src0->type && @@ -1599,7 +1601,7 @@ void ggml_cuda_mul_mat_vec_q( } if (src1_q8_d == nullptr) { char * q8_dst; - if (luce_q8_memo_on && !ids) { + if (luce_q8_memo_on) { ggml_backend_cuda_context::luce_q8_memo_entry ent; ent.src1_node = (const void *) src1; ent.src1_data = (const void *) src1_d; @@ -1643,9 +1645,11 @@ void ggml_cuda_mul_mat_vec_q( // [TAG_MMID_GROUPED] grouped-expert path for small MUL_MAT_ID batches. if (ids && ncols_dst >= 2 && ncols_dst <= MMVQ_MAX_MOE_BATCH_SIZE && + (int) (nchannels_dst*ncols_dst) <= MMID_GROUPED_MAX_PAIRS && mmid_grouped_env() && mmid_grouped_type_ok(src0->type)) { + // Batches above MMID_GROUPED_MAX_PAIRS fall through to the legacy + // per-expert kernel instead of aborting the request. const int np = (int) (nchannels_dst*ncols_dst); - GGML_ASSERT(np <= MMID_GROUPED_MAX_PAIRS && "DFLASH_MMID_GROUPED supports n_expert_used <= 16"); ggml_cuda_pool_alloc mmid_meta(ctx.pool(), MMID_META_INTS); float * gate_w = nullptr; int gate_w_stride = 0; @@ -1656,8 +1660,20 @@ void ggml_cuda_mul_mat_vec_q( gate_w_stride = (int) (gx->weights->nb[1]/sizeof(float)); gate_tau = gx->tau; } + // Gate on a scratch COPY of ids: consumers of the same ids tensor that + // take the non-grouped path (e.g. a sibling weight whose quant type is + // not grouped-capable) must never see the -1 drop sentinels, which the + // legacy kernel would cast to uint32_t and read out of bounds. The + // in-place gate-weight renorm stays shared: dropped slots get weight + // 0.0f, which keeps any legacy consumer bit-correct (it just computes + // an expert that contributes nothing). + ggml_cuda_pool_alloc ids_gated(ctx.pool(), (size_t) (nchannels_dst*ncols_dst)); + CUDA_CHECK(cudaMemcpy2DAsync(ids_gated.ptr, nchannels_dst*sizeof(int32_t), + ids_d, ids_stride*sizeof(int32_t), + nchannels_dst*sizeof(int32_t), ncols_dst, + cudaMemcpyDeviceToDevice, stream)); mmid_group_prep<<<1, MMID_GROUPED_MAX_PAIRS, 0, stream>>>( - (int32_t *) ids_d, mmid_meta.ptr, (int) nchannels_dst, (int) ncols_dst, (int) ids_stride, + ids_gated.ptr, mmid_meta.ptr, (int) nchannels_dst, (int) ncols_dst, (int) nchannels_dst, gate_w, gate_w_stride, gate_tau); CUDA_CHECK(cudaGetLastError()); if (mul_mat_vec_q_grouped_dispatch( diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/topk-rows.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/topk-rows.cu index 9b7a2cccc..d8ad5a25c 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/topk-rows.cu +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/topk-rows.cu @@ -4,6 +4,9 @@ // k <= 8 candidates out of a ~100k-entry vocab distribution per drafted // position, ~KB of readback instead of the full logits. Not a graph op: call // after the producing graph_compute has returned; runs on the default stream. +// CONTRACT: callers must produce `logits` with the synchronous +// ggml_backend_graph_compute() (which drains the backend stream) - the +// async variant would race this helper's default-stream reads. #include "common.cuh" #include "ggml-cuda.h" diff --git a/server/scripts/requant_dflash_draft.py b/server/scripts/requant_dflash_draft.py index 86e80ec73..9e118ec43 100644 --- a/server/scripts/requant_dflash_draft.py +++ b/server/scripts/requant_dflash_draft.py @@ -19,6 +19,7 @@ feature projection with known overflow sensitivity, keep it higher) """ import argparse +import os import sys from pathlib import Path @@ -40,6 +41,10 @@ def main() -> None: ap.add_argument("--variant", choices=("q8", "q4"), default="q4") args = ap.parse_args() + if os.path.realpath(args.src) == os.path.realpath(args.dst): + sys.exit("src and dst are the same file: the writer truncates dst " + "while tensor data is still mmap-read from src") + r = GGUFReader(args.src) arch = None for f in r.fields.values(): diff --git a/server/src/common/adaptive_verify_width.h b/server/src/common/adaptive_verify_width.h index 43195d509..b7a0cb954 100644 --- a/server/src/common/adaptive_verify_width.h +++ b/server/src/common/adaptive_verify_width.h @@ -18,12 +18,22 @@ // Model-agnostic: any family loop that has per-slot drafter top-1 // probabilities (e.g. from ggml_backend_cuda_topk_rows over the draft-head // logits) can call this before building its verify batch. +#include #include inline float adaptive_verify_width_theta() { static const float theta = []() { const char * e = std::getenv("DFLASH_ADAPTIVE_WIDTH_THETA"); - return e ? (float) std::atof(e) : 0.20f; + if (!e) return 0.20f; + char * end = nullptr; + const float v = std::strtof(e, &end); + if (end == e || *end != '\0' || v < 0.0f || v > 1.0f) { + std::fprintf(stderr, "[adaptive-width] ignoring " + "DFLASH_ADAPTIVE_WIDTH_THETA=\"%s\" " + "(want a float in [0,1]); using 0.20\n", e); + return 0.20f; + } + return v; }(); return theta; } @@ -31,7 +41,15 @@ inline float adaptive_verify_width_theta() { inline int adaptive_verify_width_min() { static const int mn = []() { const char * e = std::getenv("DFLASH_ADAPTIVE_WIDTH_MIN"); - return e ? std::atoi(e) : 4; + if (!e) return 4; + const int v = std::atoi(e); + if (v <= 0) { + std::fprintf(stderr, "[adaptive-width] ignoring " + "DFLASH_ADAPTIVE_WIDTH_MIN=\"%s\" " + "(want a positive int); using 4\n", e); + return 4; + } + return v; }(); return mn; } diff --git a/server/src/common/dflash_target.h b/server/src/common/dflash_target.h index aad6428aa..af7cd756f 100644 --- a/server/src/common/dflash_target.h +++ b/server/src/common/dflash_target.h @@ -134,9 +134,12 @@ struct DFlashTarget { // Project draft hidden states through the target lm_head and return full // f32 logits with vocab as the fastest-changing dimension. // Default false (unsupported); Domino-capable targets override. + // [TAG_ADAPTIVE_WIDTH] like project_hidden_to_tokens, optionally also // returning top-cand_k candidate ids + softmax probs covering slots - // 1..n_tokens-1 (entry j-1 <-> slot j). Default: no candidates. + // 1..n_tokens-1 (entry j-1 <-> slot j). The default implementation + // SUCCEEDS by delegating to project_hidden_to_tokens and returning no + // candidates (the "default false" above refers to logits support only). virtual bool project_hidden_to_tokens_topk(const float * hidden, int n_tokens, std::vector & tokens_out, diff --git a/server/src/common/mmid_adaptive_k.h b/server/src/common/mmid_adaptive_k.h index 62ce9be23..a5f023161 100644 --- a/server/src/common/mmid_adaptive_k.h +++ b/server/src/common/mmid_adaptive_k.h @@ -13,8 +13,10 @@ #include "ggml.h" #include +#include #include #include +#include struct mmid_gate_extra { uint32_t magic; // MMID_GATE_MAGIC @@ -24,17 +26,35 @@ struct mmid_gate_extra { #define MMID_GATE_MAGIC 0x4D474154u // il < 0 = layer index unknown for this family: the dense-layer list cannot -// be applied, every MoE layer is gated. Only builds run this; the extra must -// outlive the graph, so it is deliberately never freed. +// be applied, every MoE layer is gated. inline void mmid_adaptive_k_attach(ggml_tensor * ids, const ggml_tensor * weights, int n_tokens, int il, const char * dense_default) { static const float tau = []() { const char * e = std::getenv("DFLASH_ADAPTIVE_K_TAU"); - return e ? (float) std::atof(e) : 0.0f; + if (!e) return 0.0f; + char * end = nullptr; + const float v = std::strtof(e, &end); + if (end == e || *end != '\0' || v < 0.0f || v > 1.0f) { + std::fprintf(stderr, "[adaptive-k] ignoring DFLASH_ADAPTIVE_K_TAU=\"%s\"" + " (want a float in [0,1])\n", e); + return 0.0f; + } + return v; }(); if (tau <= 0.0f || n_tokens < 2 || n_tokens > 16 || ids == nullptr || weights == nullptr) { return; } + if (il < 0) { + static const bool warned = []() { + std::fprintf(stderr, + "[adaptive-k] WARNING: this model family does not thread layer " + "indices into the router yet, so DFLASH_ADAPTIVE_K_DENSE cannot " + "be honored and ALL MoE layers are gated - including DFlash " + "capture layers, which can degrade drafter acceptance.\n"); + return true; + }(); + (void) warned; + } if (il >= 0) { const char * e = std::getenv("DFLASH_ADAPTIVE_K_DENSE"); const std::string str = e ? e : (dense_default ? dense_default : ""); @@ -42,12 +62,30 @@ inline void mmid_adaptive_k_attach(ggml_tensor * ids, const ggml_tensor * weight while (pos < str.size()) { const size_t q = str.find(',', pos); const std::string tok = str.substr(pos, q == std::string::npos ? std::string::npos : q - pos); - if (!tok.empty() && std::atoi(tok.c_str()) == il) { - return; + if (!tok.empty()) { + char * end = nullptr; + const long v = std::strtol(tok.c_str(), &end, 10); + if (end == tok.c_str() || *end != '\0') { + std::fprintf(stderr, "[adaptive-k] ignoring malformed " + "DFLASH_ADAPTIVE_K_DENSE entry \"%s\"\n", + tok.c_str()); + } else if ((int) v == il) { + return; + } } if (q == std::string::npos) break; pos = q + 1; } } - ids->extra = new mmid_gate_extra{MMID_GATE_MAGIC, tau, weights}; + // The extra must outlive graph evals, and builds run repeatedly in a + // server (ggml_new_tensor zeroes ->extra even when the persistent arena + // hands back the same address). Keep one allocation per distinct tensor + // address in a process-lifetime pool: bounded by the arenas' stable + // addresses instead of leaking one allocation per rebuild. + static std::unordered_map pool; + mmid_gate_extra *& gx = pool[ids]; + if (gx == nullptr) gx = new mmid_gate_extra{MMID_GATE_MAGIC, tau, weights}; + gx->tau = tau; + gx->weights = weights; + ids->extra = gx; } diff --git a/server/src/draft/draft_gguf_loader.cpp b/server/src/draft/draft_gguf_loader.cpp index 657eb0b5c..e5a04721c 100644 --- a/server/src/draft/draft_gguf_loader.cpp +++ b/server/src/draft/draft_gguf_loader.cpp @@ -261,6 +261,8 @@ bool load_draft_gguf(const std::string & path, if (!out.fc || !out.hidden_norm || !out.out_norm) { set_last_error("draft GGUF: missing top-level tensors " "(dflash.fc|dflash_fc / dflash.hidden_norm|dflash_hidden_norm / output_norm)"); + ggml_free(meta_ctx); // out.ctx: free the metadata arena on early failure + out.ctx = nullptr; gguf_free(gctx); return false; } @@ -304,6 +306,8 @@ bool load_draft_gguf(const std::string & path, char b[128]; std::snprintf(b, sizeof(b), "draft GGUF: layer %d missing tensors", il); set_last_error(b); + ggml_free(meta_ctx); // out.ctx: free the metadata arena on early failure + out.ctx = nullptr; gguf_free(gctx); return false; } @@ -316,6 +320,8 @@ bool load_draft_gguf(const std::string & path, "draft GGUF: incomplete attention gate tensors: %d/%d layers", n_gate_layers, out.n_layer); set_last_error(b); + ggml_free(meta_ctx); // out.ctx: free the metadata arena on early failure + out.ctx = nullptr; gguf_free(gctx); return false; } @@ -343,6 +349,8 @@ bool load_draft_gguf(const std::string & path, out.domino.head_b1 && out.domino.head_w2 && out.domino.head_b2; if (!domino_all) { set_last_error("draft GGUF: incomplete Domino aux-head tensors"); + ggml_free(meta_ctx); // out.ctx: free the metadata arena on early failure + out.ctx = nullptr; gguf_free(gctx); return false; } @@ -368,6 +376,8 @@ bool load_draft_gguf(const std::string & path, !check_shape_2d(out.domino.head_w2, E, V, "head.w2", shape_err, sizeof(shape_err)) || !check_shape_1d(out.domino.head_b2, V, "head.b2", shape_err, sizeof(shape_err))) { set_last_error(shape_err); + ggml_free(meta_ctx); // out.ctx: free the metadata arena on early failure + out.ctx = nullptr; gguf_free(gctx); return false; } @@ -391,6 +401,8 @@ bool load_draft_gguf(const std::string & path, if (dspark_any) { if (!out.dspark.markov_w1 || !out.dspark.markov_w2) { set_last_error("draft GGUF: incomplete DSpark Markov tensors"); + ggml_free(meta_ctx); // out.ctx: free the metadata arena on early failure + out.ctx = nullptr; gguf_free(gctx); return false; } @@ -405,6 +417,8 @@ bool load_draft_gguf(const std::string & path, if (!check_shape_2d(out.dspark.markov_w1, R, V, "dspark.markov.w1", shape_err, sizeof(shape_err)) || !check_shape_2d(out.dspark.markov_w2, R, V, "dspark.markov.w2", shape_err, sizeof(shape_err))) { set_last_error(shape_err); + ggml_free(meta_ctx); // out.ctx: free the metadata arena on early failure + out.ctx = nullptr; gguf_free(gctx); return false; } @@ -413,6 +427,8 @@ bool load_draft_gguf(const std::string & path, if (conf_any) { if (!out.dspark.confidence_w || !out.dspark.confidence_b) { set_last_error("draft GGUF: incomplete DSpark confidence tensors"); + ggml_free(meta_ctx); // out.ctx: free the metadata arena on early failure + out.ctx = nullptr; gguf_free(gctx); return false; } @@ -422,6 +438,8 @@ bool load_draft_gguf(const std::string & path, if (!check_shape_2d(out.dspark.confidence_w, C, 1, "dspark.confidence.weight", shape_err, sizeof(shape_err)) || !check_shape_1d(out.dspark.confidence_b, 1, "dspark.confidence.bias", shape_err, sizeof(shape_err))) { set_last_error(shape_err); + ggml_free(meta_ctx); // out.ctx: free the metadata arena on early failure + out.ctx = nullptr; gguf_free(gctx); return false; } diff --git a/server/src/laguna/laguna_target_graph.cpp b/server/src/laguna/laguna_target_graph.cpp index a93b79528..0b580f7ad 100644 --- a/server/src/laguna/laguna_target_graph.cpp +++ b/server/src/laguna/laguna_target_graph.cpp @@ -25,6 +25,7 @@ #include "../common/moe_router_graph.h" #include "../common/kvflash_pager.h" #include "common/ggml_graph_precision.h" +#include "common/prof_env.h" #include "internal.h" #include "dflash27b.h" @@ -1016,6 +1017,11 @@ bool build_laguna_layer_step( laguna_layer_step_graph_free(sg); if (layer_idx < 0 || layer_idx >= w.n_layer) return false; if (!cache.attn_k[layer_idx] || !cache.attn_v[layer_idx]) return false; + if (cache.swa_ring_rows > 0) { + std::fprintf(stderr, "laguna_layer_step: SWA ring caches are not " + "wired for the layer-split path\n"); + return false; + } if (kvflash && std::getenv("DFLASH_LAGUNA_NO_KVPAD")) return false; ggml_init_params ip{}; @@ -1156,6 +1162,11 @@ LagunaGraphOutputs build_laguna_graph( capture_slices.assign((size_t)cache.n_capture_layers, nullptr); } + // [TAG_SWA_RING] ring-sized SWA caches make pool-width indices/views on + // SWA layers out of bounds: every builder reaching here must supply ring + // row indices. Fail loudly at build time rather than corrupt memory. + GGML_ASSERT(cache.swa_ring_rows == 0 || in.kv_idx_swa != nullptr); + for (int il = 0; il < w.n_layer; ++il) { cur = build_laguna_layer(ctx, gf, w, cache, il, cur, in.positions, in.attn_mask, in.kv_start, in.n_tokens, @@ -1467,7 +1478,7 @@ bool laguna_step( // [TAG_PREFILL_PROF] batch-path sub-phase laps: DFLASH_LAGUNA_PREFILL_PROF=1. // build = graph rebuild+alloc, fill = host mask/row fills, up = tensor_set // uploads, gpu = graph_compute, read = logit/argmax readback. - static const bool g_pfprof = std::getenv("DFLASH_LAGUNA_PREFILL_PROF") != nullptr; + static const bool g_pfprof = dflash_prof_enabled("prefill"); static thread_local double pf_build = 0, pf_fill = 0, pf_up = 0, pf_gpu = 0, pf_read = 0; static thread_local int pf_n = 0; @@ -1871,7 +1882,7 @@ bool laguna_verify_batch( // prep = stage+embed/pos fills, mask = kvflash rows+mask fill+memcpy, // upwait = sync after async uploads (isolates upload cost from compute), // gpu = graph_compute, read = argmax/logits readback. - static const bool g_vprof = std::getenv("DFLASH_LAGUNA_VERIFY_PROF") != nullptr; + static const bool g_vprof = dflash_prof_enabled("verify"); static thread_local double vp_prep = 0, vp_mask = 0, vp_up = 0, vp_gpu = 0, vp_read = 0; static thread_local int vp_n = 0; diff --git a/server/src/server/server_main.cpp b/server/src/server/server_main.cpp index 1d5d3a2d3..a1f12dc9d 100644 --- a/server/src/server/server_main.cpp +++ b/server/src/server/server_main.cpp @@ -443,6 +443,13 @@ int main(int argc, char ** argv) { if (i + 1 < argc && argv[i + 1][0] != '-') { tau = argv[++i]; } + char * end = nullptr; + const double tv = std::strtod(tau, &end); + if (end == tau || *end != '\0' || tv <= 0.0 || tv > 1.0) { + std::fprintf(stderr, + "--adaptive-experts: tau must be a float in (0,1], got \"%s\"\n", tau); + return 1; + } setenv("DFLASH_ADAPTIVE_K_TAU", tau, 0); // explicit env still wins } else if (std::strcmp(argv[i], "--verify-width") == 0 && i + 1 < argc) { bargs.verify_width = std::atoi(argv[++i]); From 8c4f08a5a42b828ad87810b49b27e21ae9f32380 Mon Sep 17 00:00:00 2001 From: mrciffa <49000955+davide221@users.noreply.github.com> Date: Fri, 10 Jul 2026 17:42:33 +0200 Subject: [PATCH 12/20] refactor(env): consolidate profilers into DFLASH_PROF, document the env surface The server had accumulated 153 distinct environment variables. Policy going forward (docs/ENVIRONMENT.md): features ship as CLI flags or defaults; env vars are reserved for burn-in kill switches (deleted after soak) and debug instrumentation. - DFLASH_PROF=step,verify,prefill replaces DFLASH_LAGUNA_{STEP,VERIFY, PREFILL}_PROF (all three were new this branch; no users to migrate). - docs/ENVIRONMENT.md documents the load-bearing variables (default, purpose, lifecycle) and carries the full generated inventory; the promotion/deletion audit of the legacy surface is tracked as follow-up. --- server/docs/ENVIRONMENT.md | 164 +++++++++++++++++++++++++++ server/src/common/prof_env.h | 32 ++++++ server/src/laguna/laguna_backend.cpp | 3 +- 3 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 server/docs/ENVIRONMENT.md create mode 100644 server/src/common/prof_env.h diff --git a/server/docs/ENVIRONMENT.md b/server/docs/ENVIRONMENT.md new file mode 100644 index 000000000..45d80ceee --- /dev/null +++ b/server/docs/ENVIRONMENT.md @@ -0,0 +1,164 @@ +# Server environment variables + +Policy (2026-07): **new features ship as CLI flags or defaults, not env vars.** +Environment variables are reserved for two cases: + +1. **Burn-in kill switches** for freshly landed defaults - documented here with + the intent to delete them once the feature has soaked. +2. **Debug instrumentation** (profilers, stats) - zero-cost when unset, never + required for correct serving. + +Anything else in the inventory below is legacy surface: prefer the CLI flag +where one exists, and treat undocumented variables as internal. The +consolidation of this list into CLI flags is tracked as follow-up work. + +## Documented variables + +| Variable | Default | Purpose | +|---|---|---| +| `DFLASH_DRAFT_KV` | 1 | KILL SWITCH (remove after burn-in): =0 restores the legacy per-step drafter window recompute instead of the ring cache. | +| `DFLASH_LAGUNA_SWA_RING` | 1 | KILL SWITCH (remove after burn-in): =0 keeps SWA layers on pool-sized caches under KVFlash. | +| `DFLASH_PROF` | unset | DEBUG: comma list of profilers (step,verify,prefill). Replaces DFLASH_LAGUNA_{STEP,VERIFY,PREFILL}_PROF. | +| `GGML_CUDA_GRAPH_STATS` | unset | DEBUG: per-graph CUDA-graph replay/capture/eager counters. | +| `GGML_CUDA_GRAPH_STATS_EVERY` | 200 | DEBUG: print period for the stats above (clamped to >=1). | +| `DFLASH_ADAPTIVE_K_TAU` | 0 = off | Prefer the CLI: --adaptive-experts [tau]. Cumulative combine-weight threshold for per-token expert gating. | +| `DFLASH_ADAPTIVE_K_DENSE` | per-model default | CSV of MoE layers kept dense under adaptive-K (DFlash capture layers). Warned-inert on families that do not thread layer indices yet. | +| `DFLASH_MMID_GROUPED` | unset | Grouped MUL_MAT_ID kernel for small verify batches; candidate for CLI promotion. | +| `DFLASH_KVFLASH` | unset | Prefer the CLI: --kvflash . | + +## Full inventory (generated) + +`grep -rE 'getenv\("[A-Z0-9_]+"\)' server/src` - regenerate when adding or removing variables. + +- `DFLASH27B_CHUNKED` - qwen35_target_graph.cpp +- `DFLASH27B_DRAFT_FP16` - draft_safetensors_loader.cpp +- `DFLASH27B_DRAFT_SWA` - server_main.cpp +- `DFLASH27B_KV_F16` - kv_quant.cpp +- `DFLASH27B_KV_K` - kv_quant.cpp, laguna_backend.cpp +- `DFLASH27B_KV_Q4` - kv_quant.cpp +- `DFLASH27B_KV_TQ3` - kv_quant.cpp, qwen3_drafter.cpp +- `DFLASH27B_KV_V` - kv_quant.cpp, laguna_backend.cpp +- `DFLASH27B_LM_HEAD_FIX` - http_server.cpp +- `DFLASH27B_PREFILL_UBATCH` - layer_split_daemon.cpp, qwen35_backend.cpp, qwen35_layer_split_adapter.cpp +- `DFLASH_ADAPTIVE_K_DENSE` - mmid_adaptive_k.h +- `DFLASH_ADAPTIVE_K_TAU` - mmid_adaptive_k.h +- `DFLASH_ADAPTIVE_WIDTH_MIN` - adaptive_verify_width.h +- `DFLASH_ADAPTIVE_WIDTH_THETA` - adaptive_verify_width.h +- `DFLASH_COLD_THREADS` - moe_expert_compute_cpu.cpp +- `DFLASH_DISABLE_DRAFT_ATTN` - draft_graph.cpp +- `DFLASH_DISABLE_DRAFT_ATTN_GATE` - draft_graph.cpp +- `DFLASH_DISABLE_DRAFT_AUX_NORMS` - draft_graph.cpp +- `DFLASH_DISABLE_DRAFT_FFN` - draft_graph.cpp +- `DFLASH_DISABLE_DRAFT_SWA` - dflash_draft_kv.cpp, draft_graph.cpp +- `DFLASH_DOMINO_ZERO_START` - domino_head.cpp +- `DFLASH_DRAFT_IPC_SHARED_BYTES` - dflash_draft_ipc.cpp +- `DFLASH_DRAFT_IPC_TRANSPORT` - dflash_draft_ipc.cpp +- `DFLASH_DRAFT_KV` - laguna_backend.cpp, qwen35_backend.cpp +- `DFLASH_DRAFT_PERSIST` - laguna_backend.cpp +- `DFLASH_DROP_COLD` - qwen35moe_backend.cpp, qwen35moe_pipelined_decode.cpp +- `DFLASH_DS4_TIMING` - deepseek4_target_shard_ipc_daemon.cpp +- `DFLASH_EXPERT_BUDGET_MB` - deepseek4_backend.cpp, laguna_backend.cpp, qwen35moe_backend.cpp +- `DFLASH_EXPERT_BUDGET_PCT` - laguna_backend.cpp +- `DFLASH_FEATURE_DTYPE` - dflash_feature_ring.cpp +- `DFLASH_FP_ALPHA` - http_server.cpp, qwen3_graph.cpp, server_main.cpp +- `DFLASH_FP_CHUNK_S` - qwen3_graph.cpp +- `DFLASH_FP_DEBUG_LAYER0` - qwen3_graph.cpp +- `DFLASH_FP_DUMP_COUNTS` - flashprefill.cpp +- `DFLASH_FP_HIP_ROW` - flashprefill_kernels.cu +- `DFLASH_FP_NOPE_TAIL` - qwen3_graph.cpp +- `DFLASH_FP_PROFILE` - flashprefill.cpp +- `DFLASH_FP_SKIP_PREWARM` - qwen3_drafter.cpp +- `DFLASH_FP_USE_BSA` - flashprefill.cpp, http_server.cpp, server_main.cpp +- `DFLASH_G4_BSA_CHUNK` - gemma4_graph.cpp +- `DFLASH_GEMMA4_LAYER_SPLIT_UBATCH` - gemma4_layer_split_adapter.cpp +- `DFLASH_GEMMA4_NO_KVPAD` - gemma4_graph.cpp +- `DFLASH_GPU_ARGMAX` - qwen35_backend.cpp +- `DFLASH_GPU_DRAFT_TOPK` - qwen35_dflash_target.cpp +- `DFLASH_GPU_SAMPLE` - geometric_sampler_cuda.cu +- `DFLASH_GPU_VERIFY_ARGMAX` - qwen35_dflash_target.cpp +- `DFLASH_IGNORE_EOS` - laguna_backend.cpp +- `DFLASH_KVFLASH` - gemma4_backend.cpp, gemma4_layer_split_adapter.cpp, kvflash_pager.h, laguna_backend.cpp, laguna_layer_split_adapter.cpp, qwen35_backend.cpp, qwen35_layer_split_adapter.cpp +- `DFLASH_KVFLASH_DRAFTER` - kvflash_pager.h +- `DFLASH_KVFLASH_MAX_POOL` - kvflash_pager.h +- `DFLASH_KVFLASH_POLICY` - kvflash_pager.h +- `DFLASH_KVFLASH_TAU` - gemma4_backend.cpp, gemma4_layer_split_adapter.cpp, laguna_backend.cpp, laguna_layer_split_adapter.cpp, qwen35_layer_split_adapter.cpp +- `DFLASH_LAGUNA_AUTO_HEAD_MAJOR` - laguna_backend.cpp +- `DFLASH_LAGUNA_CACHE_SLOTS` - laguna_backend.cpp +- `DFLASH_LAGUNA_DRAFT_PAD` - laguna_backend.cpp +- `DFLASH_LAGUNA_DSPARK` - laguna_backend.cpp +- `DFLASH_LAGUNA_DSPARK_CONFIDENCE_THRESHOLD` - laguna_backend.cpp +- `DFLASH_LAGUNA_DSPARK_TREE` - laguna_backend.cpp +- `DFLASH_LAGUNA_EXPERT_CACHE` - moe_hybrid_ffn_eval.cpp +- `DFLASH_LAGUNA_FUSED_DOMINO` - laguna_backend.cpp +- `DFLASH_LAGUNA_FUSED_DSPARK` - laguna_backend.cpp +- `DFLASH_LAGUNA_FUSED_QK` - laguna_target_loader.cpp +- `DFLASH_LAGUNA_FUSE_FFN` - laguna_backend.cpp +- `DFLASH_LAGUNA_GPU_ARGMAX` - laguna_backend.cpp +- `DFLASH_LAGUNA_GPU_REMAP` - moe_hybrid_ffn_eval.cpp +- `DFLASH_LAGUNA_HOTNESS` - laguna_backend.cpp +- `DFLASH_LAGUNA_KV_HEAD_MAJOR` - laguna_backend.cpp, laguna_target_graph.cpp +- `DFLASH_LAGUNA_LAYER_SPLIT_UBATCH` - laguna_layer_split_adapter.cpp +- `DFLASH_LAGUNA_MOE_FUSED_COMBINE` - laguna_target_graph.cpp +- `DFLASH_LAGUNA_MOE_STUB` - laguna_target_graph.cpp +- `DFLASH_LAGUNA_NEXT_PLACEMENT_OUT` - laguna_backend.cpp +- `DFLASH_LAGUNA_NO_KVPAD` - laguna_dflash_target.cpp, laguna_target_graph.cpp +- `DFLASH_LAGUNA_NO_SINGLE_GRAPH` - laguna_backend.cpp +- `DFLASH_LAGUNA_PAD_CPY` - laguna_dflash_target.cpp, laguna_target_graph.cpp +- `DFLASH_LAGUNA_PERSIST_VERIFY` - laguna_target_graph.cpp +- `DFLASH_LAGUNA_PREGATE_MAX` - laguna_backend.cpp +- `DFLASH_LAGUNA_PREGATE_TRACE` - laguna_backend.cpp +- `DFLASH_LAGUNA_PROFILE` - laguna_backend.cpp +- `DFLASH_LAGUNA_SWAP_MAX` - laguna_backend.cpp +- `DFLASH_LAGUNA_SWAP_MIN_GAIN` - laguna_backend.cpp +- `DFLASH_LAGUNA_SWA_RING` - laguna_backend.cpp +- `DFLASH_LAGUNA_TELEMETRY` - laguna_backend.cpp +- `DFLASH_LAGUNA_VERIFY_WIDTH` - laguna_backend.cpp +- `DFLASH_LAGUNA_VERIFY_WIDTH_MAX` - laguna_backend.cpp +- `DFLASH_MAX_CONTEXT` - laguna_backend.cpp, qwen35moe_backend.cpp +- `DFLASH_MMQ_FULL_BATCH_MIN` - moe_hybrid_ffn_eval.cpp +- `DFLASH_MMQ_SUB_BATCH` - moe_hybrid_ffn_eval.cpp +- `DFLASH_MODEL_CARDS_DIR` - model_card.cpp +- `DFLASH_MOE_COLD_BACKEND` - deepseek4_loader.cpp +- `DFLASH_MOE_EXPERT_COMPUTE_IPC_BATCH_CAPACITY` - moe_expert_compute_ipc.cpp +- `DFLASH_MOE_EXPERT_COMPUTE_IPC_DTYPE` - moe_expert_compute_ipc.cpp +- `DFLASH_MOE_EXPERT_COMPUTE_IPC_MODE` - moe_hybrid_ffn_eval.cpp +- `DFLASH_MOE_EXPERT_COMPUTE_IPC_PROFILE` - moe_expert_compute_ipc.cpp +- `DFLASH_MOE_EXPERT_COMPUTE_IPC_SHARED_BYTES` - moe_expert_compute_ipc.cpp +- `DFLASH_MOE_EXPERT_COMPUTE_IPC_TRANSPORT` - moe_expert_compute_ipc.cpp +- `DFLASH_MOE_EXPERT_COMPUTE_THREADS` - moe_expert_compute_cpu.cpp +- `DFLASH_MOE_FIXED_SLOT_GRAPHS` - moe_hybrid_ffn_eval.cpp +- `DFLASH_MOE_FIXED_SLOT_MAX` - moe_hybrid_ffn_eval.cpp +- `DFLASH_MOE_PREFILL_HOT_SUB_BATCH` - moe_hybrid_ffn_eval.cpp +- `DFLASH_NO_MASK` - laguna_backend.cpp +- `DFLASH_NO_MOE_ROUTER_FUSE` - qwen35moe_ffn.cpp +- `DFLASH_NO_MOE_SWIGLU_FUSE` - qwen35moe_ffn.cpp +- `DFLASH_NO_PREAD` - deepseek4_loader.cpp +- `DFLASH_PROF` - prof_env.h +- `DFLASH_QWEN35MOE_CACHE_SLOTS` - qwen35moe_backend.cpp +- `DFLASH_QWEN35MOE_HOTNESS` - qwen35moe_backend.cpp +- `DFLASH_QWEN35MOE_NEXT_PLACEMENT_OUT` - qwen35moe_backend.cpp +- `DFLASH_QWEN35MOE_NO_KVPAD` - qwen35moe_pipelined_decode.cpp +- `DFLASH_QWEN35MOE_NO_ROUTED` - qwen35moe_pipelined_decode.cpp +- `DFLASH_QWEN35MOE_RUNTIME_STATS_OUT` - qwen35moe_backend.cpp +- `DFLASH_QWEN35MOE_SWAP_MAX` - qwen35moe_backend.cpp +- `DFLASH_QWEN35MOE_SWAP_MIN_GAIN` - qwen35moe_backend.cpp +- `DFLASH_QWEN35MOE_TELEMETRY` - qwen35moe_backend.cpp +- `DFLASH_QWEN35_NO_KVPAD` - graph_builders.cpp +- `DFLASH_SAMPLED_VERIFY` - laguna_backend.cpp, qwen35_backend.cpp +- `DFLASH_SHARE_DIR` - http_server.cpp +- `DFLASH_SPARK` - laguna_backend.cpp, qwen35moe_backend.cpp +- `DFLASH_SPARK_VRAM_MB` - laguna_backend.cpp, qwen35moe_backend.cpp +- `DFLASH_SV_DEBUG` - qwen35_backend.cpp +- `DFLASH_TARGET_SHARD_IPC_SHARED_BYTES` - target_shard_ipc.cpp +- `DFLASH_TARGET_SHARD_IPC_TRANSPORT` - target_shard_ipc.cpp +- `DFLASH_TOPK_PROFILE` - geometric_draft_topk_cuda.cu +- `DFLASH_TOPK_SPLIT` - geometric_draft_topk_cuda.cu +- `DFLASH_VERIFY_WIDTH` - qwen35moe_backend.cpp +- `FAST_ROLLBACK_DIAG` - qwen35_dflash_target.cpp +- `HOME` - spark_corpus.cpp +- `LUCE_QK_FUSE_LAYERS` - laguna_target_graph.cpp +- `LUCE_QK_FUSE_MODE` - laguna_target_graph.cpp +- `PFLASH_DRAFTER_EARLY_EXIT_N` - qwen3_graph.cpp +- `PFLASH_DRAFTER_SCORE_LAYERS` - qwen3_graph.cpp +- `PFLASH_FREEZE_HOT_WINDOW` - http_server.cpp +- `TMPDIR` - backend_ipc.cpp, moe_expert_compute_ipc.cpp diff --git a/server/src/common/prof_env.h b/server/src/common/prof_env.h new file mode 100644 index 000000000..0609d90af --- /dev/null +++ b/server/src/common/prof_env.h @@ -0,0 +1,32 @@ +// prof_env.h — single profiling switch for the serving hot paths. +// +// DFLASH_PROF is a comma-separated list of profiler names, replacing the +// per-profiler env-var sprawl. Example: DFLASH_PROF=step,verify,prefill +// step per-step decode laps (draft/verify/heads/commit/build) +// verify verify_batch sub-phases (prep/mask/upwait/gpu/read) +// prefill prefill batch sub-phases (build/fill/up/gpu/read) +// Unknown names are ignored. Profilers are debug instrumentation: zero cost +// when off, never required for correct serving. + +#pragma once + +#include +#include + +namespace dflash::common { + +inline bool dflash_prof_enabled(const char * name) { + const char * e = std::getenv("DFLASH_PROF"); + if (!e) return false; + const size_t n = std::strlen(name); + for (const char * p = e; *p; ) { + const char * q = std::strchr(p, ','); + const size_t len = q ? (size_t)(q - p) : std::strlen(p); + if (len == n && std::strncmp(p, name, n) == 0) return true; + if (!q) break; + p = q + 1; + } + return false; +} + +} // namespace dflash::common diff --git a/server/src/laguna/laguna_backend.cpp b/server/src/laguna/laguna_backend.cpp index ee9cafa0e..da2e336a6 100644 --- a/server/src/laguna/laguna_backend.cpp +++ b/server/src/laguna/laguna_backend.cpp @@ -15,6 +15,7 @@ #include "common/dspark_head.h" #include "common/dflash_feature_ring.h" #include "common/dflash_draft_graph.h" +#include "common/prof_env.h" #include "kv_quant.h" #include @@ -687,7 +688,7 @@ bool LagunaBackend::do_spec_decode(int committed, int n_gen, if (use_draft_kv) draft_kv_reset(draft_kv_); auto t_dec0 = std::chrono::steady_clock::now(); - static const bool step_prof = std::getenv("DFLASH_LAGUNA_STEP_PROF") != nullptr; + static const bool step_prof = dflash_prof_enabled("step"); double prof_draft_ms = 0.0, prof_heads_ms = 0.0, prof_verify_ms = 0.0; // [TAG_FUSED_LOOP] blind-spot laps: commit = verify-end -> loop-top // (accept/commit/emit/feature-sync), build = loop-top -> draft-input From 4b859031b2110352506c2367e9b9311846c19ee9 Mon Sep 17 00:00:00 2001 From: mrciffa <49000955+davide221@users.noreply.github.com> Date: Fri, 10 Jul 2026 19:06:13 +0200 Subject: [PATCH 13/20] polish(review): log-spam guard, honest comments, doc rendering Self-review pass over the review-fix batch: - malformed DFLASH_ADAPTIVE_K_DENSE entries warn once, not per graph build - the adaptive-K extras pool documents its single-threaded-builder assumption (same contract as the thread_local builder arenas) and states the boundedness argument honestly - two profiler comments still referenced the pre-consolidation env names - ENVIRONMENT.md: a raw pipe inside a table cell broke markdown rendering Validated on a full clean recompile: DFLASH_PROF=step emits the step profile (the rename's runtime behavior, previously masked by a stale object: rsync -a preserves source mtimes older than build objects, so make skipped the recompile - box-tree-only issue, the pushed branch was always verified via the fresh-clone build), opt-in grouped+adaptive leg byte-identical behavior. --- server/docs/ENVIRONMENT.md | 2 +- server/src/common/mmid_adaptive_k.h | 17 ++++++++++++----- server/src/laguna/laguna_target_graph.cpp | 4 ++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/server/docs/ENVIRONMENT.md b/server/docs/ENVIRONMENT.md index 45d80ceee..f711a920c 100644 --- a/server/docs/ENVIRONMENT.md +++ b/server/docs/ENVIRONMENT.md @@ -24,7 +24,7 @@ consolidation of this list into CLI flags is tracked as follow-up work. | `DFLASH_ADAPTIVE_K_TAU` | 0 = off | Prefer the CLI: --adaptive-experts [tau]. Cumulative combine-weight threshold for per-token expert gating. | | `DFLASH_ADAPTIVE_K_DENSE` | per-model default | CSV of MoE layers kept dense under adaptive-K (DFlash capture layers). Warned-inert on families that do not thread layer indices yet. | | `DFLASH_MMID_GROUPED` | unset | Grouped MUL_MAT_ID kernel for small verify batches; candidate for CLI promotion. | -| `DFLASH_KVFLASH` | unset | Prefer the CLI: --kvflash . | +| `DFLASH_KVFLASH` | unset | Prefer the CLI: `--kvflash` (token count or `auto`). | ## Full inventory (generated) diff --git a/server/src/common/mmid_adaptive_k.h b/server/src/common/mmid_adaptive_k.h index a5f023161..864dc32a1 100644 --- a/server/src/common/mmid_adaptive_k.h +++ b/server/src/common/mmid_adaptive_k.h @@ -66,9 +66,13 @@ inline void mmid_adaptive_k_attach(ggml_tensor * ids, const ggml_tensor * weight char * end = nullptr; const long v = std::strtol(tok.c_str(), &end, 10); if (end == tok.c_str() || *end != '\0') { - std::fprintf(stderr, "[adaptive-k] ignoring malformed " - "DFLASH_ADAPTIVE_K_DENSE entry \"%s\"\n", - tok.c_str()); + static bool warned_bad_dense = false; + if (!warned_bad_dense) { + warned_bad_dense = true; + std::fprintf(stderr, "[adaptive-k] ignoring malformed " + "DFLASH_ADAPTIVE_K_DENSE entry \"%s\"\n", + tok.c_str()); + } } else if ((int) v == il) { return; } @@ -80,8 +84,11 @@ inline void mmid_adaptive_k_attach(ggml_tensor * ids, const ggml_tensor * weight // The extra must outlive graph evals, and builds run repeatedly in a // server (ggml_new_tensor zeroes ->extra even when the persistent arena // hands back the same address). Keep one allocation per distinct tensor - // address in a process-lifetime pool: bounded by the arenas' stable - // addresses instead of leaking one allocation per rebuild. + // address in a process-lifetime pool: with persistent-arena builders the + // address set is small and stable, so this bounds the former + // one-allocation-per-rebuild leak. Unsynchronized on purpose: graph + // builds are single-threaded (same assumption as the thread_local + // builder arenas). static std::unordered_map pool; mmid_gate_extra *& gx = pool[ids]; if (gx == nullptr) gx = new mmid_gate_extra{MMID_GATE_MAGIC, tau, weights}; diff --git a/server/src/laguna/laguna_target_graph.cpp b/server/src/laguna/laguna_target_graph.cpp index 0b580f7ad..b52f0e461 100644 --- a/server/src/laguna/laguna_target_graph.cpp +++ b/server/src/laguna/laguna_target_graph.cpp @@ -1475,7 +1475,7 @@ bool laguna_step( return true; } - // [TAG_PREFILL_PROF] batch-path sub-phase laps: DFLASH_LAGUNA_PREFILL_PROF=1. + // [TAG_PREFILL_PROF] batch-path sub-phase laps: DFLASH_PROF=prefill. // build = graph rebuild+alloc, fill = host mask/row fills, up = tensor_set // uploads, gpu = graph_compute, read = logit/argmax readback. static const bool g_pfprof = dflash_prof_enabled("prefill"); @@ -1878,7 +1878,7 @@ bool laguna_verify_batch( ggml_tensor * argmax = S.argmax; const int swa_ring = cache.swa_ring_rows; - // [TAG_VERIFY_PROF] sub-phase laps: DFLASH_LAGUNA_VERIFY_PROF=1. + // [TAG_VERIFY_PROF] sub-phase laps: DFLASH_PROF=verify. // prep = stage+embed/pos fills, mask = kvflash rows+mask fill+memcpy, // upwait = sync after async uploads (isolates upload cost from compute), // gpu = graph_compute, read = argmax/logits readback. From 5469578773c2292ce1331388bdab94b5cf927c8b Mon Sep 17 00:00:00 2001 From: mrciffa <49000955+davide221@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:29:55 +0200 Subject: [PATCH 14/20] fix(deploy): no tq3_0 or finite fa-window in any default path Found by running the hermes harness against laguna: harness defaults forced tq3_0 KV (garbles laguna output) and fa-window 2048 (drops the system prompt, breaks tool calls). - server: the max_ctx>6144 auto-tq3_0 policy is removed; KV types come from each family's default (laguna q8_0, base q4_0), tq3_0 only via an explicit --cache-type flag - laguna: explicit tq3_0/q4_0 KV now prints a loud garbling warning - harness/clients/common.sh: no KV override exported unless the user sets one; FA_WINDOW defaults to 0 (full attention) Validated: hermes harness run against laguna shows family-default KV and coherent output (the previous run produced gibberish). --- harness/clients/common.sh | 22 ++++++++++++++-------- server/src/laguna/laguna_backend.cpp | 7 +++++++ server/src/server/server_main.cpp | 18 +++++++----------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/harness/clients/common.sh b/harness/clients/common.sh index 34c3005e8..07e9d8ea8 100755 --- a/harness/clients/common.sh +++ b/harness/clients/common.sh @@ -51,11 +51,16 @@ LLAMA_UPSTREAM_PORT="${LLAMA_UPSTREAM_PORT:-$((PORT + 1))}" MAX_CTX="${MAX_CTX:-16384}" VERIFY_MODE="${VERIFY_MODE:-seq}" BUDGET="${BUDGET:-22}" -FA_WINDOW="${FA_WINDOW:-2048}" -CACHE_TYPE_K="${CACHE_TYPE_K:-tq3_0}" -CACHE_TYPE_V="${CACHE_TYPE_V:-tq3_0}" -LLAMA_CACHE_TYPE_K="${LLAMA_CACHE_TYPE_K:-$CACHE_TYPE_K}" -LLAMA_CACHE_TYPE_V="${LLAMA_CACHE_TYPE_V:-$CACHE_TYPE_V}" +# FA_WINDOW: 0 = full attention. A finite window is known to break tool +# calling (Qwen3.6) - only set it explicitly for experiments. +FA_WINDOW="${FA_WINDOW:-0}" +# CACHE_TYPE_K/V: empty = let the server pick the model family's default +# (qwen auto-selects tq3_0 above 6K ctx; laguna requires q8_0 - forcing +# tq3_0/q4_0 on laguna garbles its output). Set explicitly to experiment. +CACHE_TYPE_K="${CACHE_TYPE_K:-}" +CACHE_TYPE_V="${CACHE_TYPE_V:-}" +LLAMA_CACHE_TYPE_K="${LLAMA_CACHE_TYPE_K:-${CACHE_TYPE_K:-q8_0}}" +LLAMA_CACHE_TYPE_V="${LLAMA_CACHE_TYPE_V:-${CACHE_TYPE_V:-q8_0}}" MAX_TOKENS="${MAX_TOKENS:-2048}" EXTRA_SERVER_ARGS="${EXTRA_SERVER_ARGS:-}" @@ -155,9 +160,10 @@ start_dflash_native_server() { if [[ -n "$FA_WINDOW" ]] && [[ "$FA_WINDOW" != "0" ]]; then fa_args=(--fa-window "$FA_WINDOW") fi - # Export KV cache type env vars for the C++ server to pick up. - export DFLASH27B_KV_K="$CACHE_TYPE_K" - export DFLASH27B_KV_V="$CACHE_TYPE_V" + # Export KV cache type env vars for the C++ server to pick up (only when + # explicitly requested: the per-axis envs override family defaults). + if [[ -n "$CACHE_TYPE_K" ]]; then export DFLASH27B_KV_K="$CACHE_TYPE_K"; fi + if [[ -n "$CACHE_TYPE_V" ]]; then export DFLASH27B_KV_V="$CACHE_TYPE_V"; fi "$DFLASH_SERVER_BIN" "$TARGET" \ "${draft_args[@]}" \ --host "$HOST" \ diff --git a/server/src/laguna/laguna_backend.cpp b/server/src/laguna/laguna_backend.cpp index da2e336a6..de7a3074f 100644 --- a/server/src/laguna/laguna_backend.cpp +++ b/server/src/laguna/laguna_backend.cpp @@ -78,6 +78,13 @@ static void resolve_laguna_kv_types(const LagunaBackendArgs & args, dflash::validate_kv_pair_or_abort(k_type, v_type, "[laguna]"); std::fprintf(stderr, "[laguna] KV cache types overridden: K=%s V=%s\n", dflash::kv_type_name(k_type), dflash::kv_type_name(v_type)); + if (k_type == GGML_TYPE_TQ3_0 || v_type == GGML_TYPE_TQ3_0 || + k_type == GGML_TYPE_Q4_0 || v_type == GGML_TYPE_Q4_0) { + std::fprintf(stderr, + "[laguna] WARNING: tq3_0/q4_0 KV caches are known to GARBLE " + "laguna output. Use q8_0 (the default) unless you are " + "debugging quantization itself.\n"); + } } } diff --git a/server/src/server/server_main.cpp b/server/src/server/server_main.cpp index a1f12dc9d..354b7cd47 100644 --- a/server/src/server/server_main.cpp +++ b/server/src/server/server_main.cpp @@ -259,7 +259,7 @@ static void print_usage(const char * prog) { #ifdef GGML_USE_HIP " Default: q4_0 (HIP builds; tq3_0 fattn unsupported)\n" #else - " Default: tq3_0 when max_ctx>6144, else q4_0\n" + " Default: per model family (laguna q8_0, else q4_0)\n" #endif "\n" "PFlash (speculative prefill compression):\n" @@ -700,14 +700,10 @@ int main(int argc, char ** argv) { setenv("DFLASH27B_KV_V", cache_type_v.c_str(), 1); } - // Auto-select TQ3_0 KV cache for large contexts (saves ~40% VRAM). - // Q4_0 remains default for short contexts where quality matters more. - // HIP build skips this: tq3_0 fattn unsupported (ggml-cuda/fattn.cu). -#ifndef GGML_USE_HIP - if (sconfig.max_ctx > 6144 && cache_type_k.empty() && cache_type_v.empty()) { - setenv("DFLASH27B_KV_TQ3", "1", 0); // don't overwrite user env - } -#endif + // TQ3_0 KV auto-selection was removed (2026-07): tq3_0 saved ~40% VRAM on + // large qwen contexts but is quality-risky and garbles laguna outright. + // KV types now come from each family's default (q8_0 for laguna, q4_0 + // base) unless the user passes --cache-type-k/v explicitly. // PFlash performance defaults: BSA kernel + sparse alpha + full attention window. bool pflash_enabled = (sconfig.pflash_mode != ServerConfig::PflashMode::OFF); @@ -1092,13 +1088,13 @@ int main(int argc, char ** argv) { #ifdef GGML_USE_HIP cache_type_k.empty() ? "q4_0 (default, HIP)" : cache_type_k.c_str()); #else - cache_type_k.empty() ? (sconfig.max_ctx > 6144 ? "tq3_0 (auto)" : "q4_0 (default)") : cache_type_k.c_str()); + cache_type_k.empty() ? "family default" : cache_type_k.c_str()); #endif std::fprintf(stderr, "[server] │ cache_type_v = %s\n", #ifdef GGML_USE_HIP cache_type_v.empty() ? "q4_0 (default, HIP)" : cache_type_v.c_str()); #else - cache_type_v.empty() ? (sconfig.max_ctx > 6144 ? "tq3_0 (auto)" : "q4_0 (default)") : cache_type_v.c_str()); + cache_type_v.empty() ? "family default" : cache_type_v.c_str()); #endif std::fprintf(stderr, "[server] │ pflash = %s\n", sconfig.pflash_mode == ServerConfig::PflashMode::AUTO ? "auto" : From 9bfd1190637f8bef97cbb5594a84427dcda70d4a Mon Sep 17 00:00:00 2001 From: mrciffa <49000955+davide221@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:57:58 +0200 Subject: [PATCH 15/20] feat(laguna): tool calling end to end Laguna emits poolside's tool dialect - NAME with KV pairs - where the wrapper tags are special tokens that detokenization strips. Nothing in the pipeline knew the format, so agent harnesses saw plain text and dead-ended after one turn. - chat_template: the laguna system block renders the tools declaration per the GGUF-embedded template (### Tools, schemas as raw JSON inside , calling instruction with the arg_key/arg_value example, thinking + non-thinking variants). Fixes the model inventing tool/arg names it could not see. - tool_parser: pattern 8 parses both the wrapped and the stripped form (name walked back from the anchor; values typed through the declared schema like the other seven dialects). - sse_emitter: added to the tool triggers with a name rewind, and the safe-prefix flush holds back a trailing identifier run (<=64 chars) when tools are declared so a streaming split cannot separate the tool name from its first tag. Validated on RTX 3090: the probe returns finish_reason=tool_calls with {"name":"get_weather","arguments":{"city":"Paris"}} and clean content; parser verified standalone against the exact model output. --- server/src/server/chat_template.cpp | 42 ++++++++++- server/src/server/sse_emitter.cpp | 33 +++++++++ server/src/server/tool_parser.cpp | 111 ++++++++++++++++++++++++++++ 3 files changed, 185 insertions(+), 1 deletion(-) diff --git a/server/src/server/chat_template.cpp b/server/src/server/chat_template.cpp index ec8f30509..939386bab 100644 --- a/server/src/server/chat_template.cpp +++ b/server/src/server/chat_template.cpp @@ -191,7 +191,47 @@ std::string render_chat_template( if (!system_content.empty() || has_tools) { result += "\n"; if (!system_content.empty()) result += system_content; - (void)tools_json; // TODO: tools block per upstream template + if (has_tools) { + // Tools block per the upstream template: each tool schema as + // raw JSON inside , then the calling + // instruction with the // + // example (thinking and non-thinking variants). + result += "\n\n### Tools\n\n" + "You may call functions to assist with the user query.\n" + "All available function signatures are listed below:\n" + "\n"; + try { + const nlohmann::json tools = nlohmann::json::parse(tools_json); + for (const auto & t : tools) { + result += t.dump(); + result += "\n"; + } + } catch (const std::exception &) { + result += tools_json; + result += "\n"; + } + result += "\n\n"; + if (enable_thinking) { + result += "Wrap your thinking in '', '' tags, " + "followed by a function call. For each function call, " + "return an unescaped XML-like object with function name " + "and arguments within '' and '' " + "tags, like here:\n" + " your thoughts here \n" + "function-name\n" + "argument-key\n" + "value-of-argument-key\n" + ""; + } else { + result += "For each function call, return an unescaped XML-like " + "object with function name and arguments within " + "'' and '' tags, like here:\n" + "function-name\n" + "argument-key\n" + "value-of-argument-key\n" + ""; + } + } result += "\n\n"; } diff --git a/server/src/server/sse_emitter.cpp b/server/src/server/sse_emitter.cpp index cab9a820c..5c03672c2 100644 --- a/server/src/server/sse_emitter.cpp +++ b/server/src/server/sse_emitter.cpp @@ -16,6 +16,10 @@ static const char THINK_CLOSE[] = ""; static const char TOOL_OPEN[] = ""; static const char FUNCTION_OPEN[] = "KV` with the +// wrapper as special tokens that detokenization strips, so the +// visible trigger is and the tool name sits immediately before it. +static const char ARG_KEY_OPEN[] = ""; static constexpr size_t THINK_OPEN_LEN = 7; static constexpr size_t THINK_CLOSE_LEN = 8; @@ -43,6 +47,20 @@ static bool find_tool_start(const std::string & text, size_t & pos) { pos = idx; return true; } + if (text.compare(idx, sizeof(ARG_KEY_OPEN) - 1, ARG_KEY_OPEN) == 0) { + // Rewind over the tool name so it lands in the tool buffer + // (the parser extracts it from just before ). + size_t start = idx; + auto is_ident = [](char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_' || c == '-'; + }; + while (start > 0 && is_ident(text[start - 1])) start--; + if (start < idx) { // require a non-empty name + pos = start; + return true; + } + } idx = text.find('<', idx + 1); } return false; @@ -441,6 +459,21 @@ std::vector SseEmitter::emit_token(const std::string & raw_piece) { // No tags found — emit safe prefix if (window_.size() > std::max(BASE_HOLDBACK, stop_holdback_)) { size_t cut = utf8_safe_len(window_, window_.size() - std::max(BASE_HOLDBACK, stop_holdback_)); + // When tools are declared, a trailing identifier run may be a + // Laguna tool name whose has not streamed in yet (the + // wrapper is a stripped special token). Hold it back + // (bounded to 64 chars, the OpenAI function-name limit) so the + // name is still in the window when the trigger fires. + if (cut > 0 && has_request_tools(tools_)) { + auto is_ident = [](char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_' || c == '-'; + }; + size_t name_hold = cut; + while (name_hold > 0 && cut - name_hold < 64 && + is_ident(window_[name_hold - 1])) name_hold--; + if (cut - name_hold < 64) cut = name_hold; + } if (cut > 0) { std::string safe = window_.substr(0, cut); accumulated_content_ += safe; diff --git a/server/src/server/tool_parser.cpp b/server/src/server/tool_parser.cpp index ac7fc7930..2592aad41 100644 --- a/server/src/server/tool_parser.cpp +++ b/server/src/server/tool_parser.cpp @@ -586,6 +586,117 @@ ToolParseResult parse_tool_calls(const std::string & text, const json & tools) { removals.push_back({start, end}); }; + // Pattern 8 (Laguna): NAME\nK\n + // V...\n. Values are raw strings or + // JSON (the template emits non-strings via tojson); coerce via the + // declared tool schema like the other patterns. Checked before pattern 1 + // so the shared wrapper is not half-consumed by the Qwen + // regexes. + { + size_t pos = 0; + while ((pos = text.find("", pos)) != std::string::npos) { + const size_t body_start = pos + 11; + const size_t close = text.find("", body_start); + if (close == std::string::npos) break; + const std::string body = text.substr(body_start, close - body_start); + const size_t first_key = body.find(""); + // Only claim bodies in the Laguna shape: bare name then arg tags + // (or a bare name alone for zero-arg calls); leave + // bodies to the Qwen patterns below. + if (body.find("", kpos); + if (kend == std::string::npos) break; + const std::string key = + trim(body.substr(kpos + 9, kend - (kpos + 9))); + const size_t vpos = body.find("", kend); + if (vpos == std::string::npos) break; + const size_t vend = body.find("", vpos); + if (vend == std::string::npos) break; + const std::string val = + trim(body.substr(vpos + 11, vend - (vpos + 11))); + if (!key.empty()) { + args[key] = convert_param_value(val, key, props); + } + kpos = body.find("", vend); + } + add_call(name, args, pos, close + 12); + } + } + pos = close + 12; + } + + // Stripped-wrapper variant: / are SPECIAL + // tokens in the laguna vocab and detokenization removes them, so the + // visible text is `NAMEKV…`. + // Anchor on and walk back over identifier chars for the + // name. No other family emits bare , so this cannot + // misfire cross-family. + size_t apos = 0; + while ((apos = text.find("", apos)) != std::string::npos) { + if (overlaps(removals, apos)) { apos += 9; continue; } + size_t name_end = apos; + size_t name_start = name_end; + auto is_ident = [](char c) { + // OpenAI-shape function names: [A-Za-z0-9_-] only. '.' must + // stay out or prose immediately before the name gets eaten + // ("...the weather tool.get_weather"). + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_' || c == '-'; + }; + while (name_start > 0 && is_ident(text[name_start - 1])) name_start--; + const std::string name = text.substr(name_start, name_end - name_start); + if (name.empty()) { apos += 9; continue; } + const json props = find_tool_properties(tools, name); + json args = json::object(); + size_t kpos = apos; + size_t span_end = apos; + while (kpos != std::string::npos && kpos == span_end) { + const size_t kend = text.find("", kpos); + if (kend == std::string::npos) break; + const size_t vpos = text.find("", kend); + if (vpos == std::string::npos) break; + const size_t vend = text.find("", vpos); + if (vend == std::string::npos) break; + auto trim = [](std::string v) { + const char * ws = " \t\r\n"; + const size_t a = v.find_first_not_of(ws); + if (a == std::string::npos) return std::string(); + const size_t b = v.find_last_not_of(ws); + return v.substr(a, b - a + 1); + }; + const std::string key = trim(text.substr(kpos + 9, kend - (kpos + 9))); + const std::string val = trim(text.substr(vpos + 11, vend - (vpos + 11))); + if (!key.empty()) args[key] = convert_param_value(val, key, props); + span_end = vend + 12; + // consume whitespace between pairs, then check for the next key + size_t nxt = span_end; + while (nxt < text.size() && (text[nxt] == '\n' || text[nxt] == ' ' || + text[nxt] == '\t' || text[nxt] == '\r')) nxt++; + kpos = (text.compare(nxt, 9, "") == 0) ? nxt : std::string::npos; + if (kpos != std::string::npos) span_end = nxt; + } + if (!args.empty()) { + add_call(name, args, name_start, span_end); + } + apos = span_end > apos ? span_end : apos + 9; + } + } + // Pattern 1: ......params...... { auto begin = std::sregex_iterator(text.begin(), text.end(), re_tool_call_complete()); From b71c23c6e7eaf7b38011350be8c1f7eaad2ca915 Mon Sep 17 00:00:00 2001 From: mrciffa <49000955+davide221@users.noreply.github.com> Date: Fri, 10 Jul 2026 22:15:17 +0200 Subject: [PATCH 16/20] refactor(tools): hoist the duplicated trim lambda into trim_ws Pure refactor from the self-review pass; parser behavior verified identical on the standalone test (get_weather/{city:Paris}, clean text). --- server/src/server/tool_parser.cpp | 32 +++++++++++++------------------ 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/server/src/server/tool_parser.cpp b/server/src/server/tool_parser.cpp index 2592aad41..e8235ff15 100644 --- a/server/src/server/tool_parser.cpp +++ b/server/src/server/tool_parser.cpp @@ -29,6 +29,14 @@ namespace dflash::common { // ─── Helpers ──────────────────────────────────────────────────────────── +static std::string trim_ws(const std::string & v) { + const char * ws = " \t\r\n"; + const size_t a = v.find_first_not_of(ws); + if (a == std::string::npos) return std::string(); + const size_t b = v.find_last_not_of(ws); + return v.substr(a, b - a + 1); +} + static std::string generate_call_id() { static std::mutex rng_mu; static std::mt19937_64 rng(std::random_device{}()); @@ -605,14 +613,7 @@ ToolParseResult parse_tool_calls(const std::string & text, const json & tools) { // bodies to the Qwen patterns below. if (body.find("", kpos); if (kend == std::string::npos) break; const std::string key = - trim(body.substr(kpos + 9, kend - (kpos + 9))); + trim_ws(body.substr(kpos + 9, kend - (kpos + 9))); const size_t vpos = body.find("", kend); if (vpos == std::string::npos) break; const size_t vend = body.find("", vpos); if (vend == std::string::npos) break; const std::string val = - trim(body.substr(vpos + 11, vend - (vpos + 11))); + trim_ws(body.substr(vpos + 11, vend - (vpos + 11))); if (!key.empty()) { args[key] = convert_param_value(val, key, props); } @@ -672,15 +673,8 @@ ToolParseResult parse_tool_calls(const std::string & text, const json & tools) { if (vpos == std::string::npos) break; const size_t vend = text.find("", vpos); if (vend == std::string::npos) break; - auto trim = [](std::string v) { - const char * ws = " \t\r\n"; - const size_t a = v.find_first_not_of(ws); - if (a == std::string::npos) return std::string(); - const size_t b = v.find_last_not_of(ws); - return v.substr(a, b - a + 1); - }; - const std::string key = trim(text.substr(kpos + 9, kend - (kpos + 9))); - const std::string val = trim(text.substr(vpos + 11, vend - (vpos + 11))); + const std::string key = trim_ws(text.substr(kpos + 9, kend - (kpos + 9))); + const std::string val = trim_ws(text.substr(vpos + 11, vend - (vpos + 11))); if (!key.empty()) args[key] = convert_param_value(val, key, props); span_end = vend + 12; // consume whitespace between pairs, then check for the next key From a0704e588194a129fefe31823d4223e2d03a3689 Mon Sep 17 00:00:00 2001 From: mrciffa <49000955+davide221@users.noreply.github.com> Date: Fri, 10 Jul 2026 22:18:40 +0200 Subject: [PATCH 17/20] fix(harness): purge finite fa-window and tq3_0 defaults everywhere Follow-up sweep after common.sh: run_openclaw overrode FA_WINDOW back to 2048, the lucebox-vs-llamacpp benchmark defaulted 2048 + tq3_0 KV, and client_test_runner's launch profiles hardcoded both. All now default to full attention and family-default (or q8_0) KV; tq3_0 remains only in the explicit cache-type comparison matrix, which tests it on purpose. --- harness/benchmarks/run_lucebox_vs_llamacpp.sh | 13 ++++++---- harness/client_test_runner.py | 26 +++++++++---------- harness/clients/common.sh | 4 +-- harness/clients/run_openclaw.sh | 2 +- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/harness/benchmarks/run_lucebox_vs_llamacpp.sh b/harness/benchmarks/run_lucebox_vs_llamacpp.sh index 4e35fddc8..c08af2be6 100755 --- a/harness/benchmarks/run_lucebox_vs_llamacpp.sh +++ b/harness/benchmarks/run_lucebox_vs_llamacpp.sh @@ -18,9 +18,12 @@ MAX_CTX="${MAX_CTX:-32768}" MAX_TOKENS="${MAX_TOKENS:-256}" BUDGET="${BUDGET:-22}" VERIFY_MODE="${VERIFY_MODE:-ddtree}" -FA_WINDOW="${FA_WINDOW:-2048}" -CACHE_TYPE_K="${CACHE_TYPE_K:-tq3_0}" -CACHE_TYPE_V="${CACHE_TYPE_V:-tq3_0}" +# 0 = full attention; finite windows break tool calls and are opt-in only. +FA_WINDOW="${FA_WINDOW:-0}" +# Empty = the server picks the model family's KV default (tq3_0/q4_0 garble +# laguna); set explicitly to experiment. +CACHE_TYPE_K="${CACHE_TYPE_K:-}" +CACHE_TYPE_V="${CACHE_TYPE_V:-}" EXTRA_SERVER_ARGS="${EXTRA_SERVER_ARGS:---lazy-draft}" LLAMA_N_GPU_LAYERS="${LLAMA_N_GPU_LAYERS:-999}" MODEL_ID="${MODEL_ID:-luce-dflash}" @@ -124,8 +127,8 @@ local_fa_args=() if [[ -n "$FA_WINDOW" ]] && [[ "$FA_WINDOW" != "0" ]]; then local_fa_args=(--fa-window "$FA_WINDOW") fi -export DFLASH27B_KV_K="$CACHE_TYPE_K" -export DFLASH27B_KV_V="$CACHE_TYPE_V" +if [[ -n "$CACHE_TYPE_K" ]]; then export DFLASH27B_KV_K="$CACHE_TYPE_K"; fi +if [[ -n "$CACHE_TYPE_V" ]]; then export DFLASH27B_KV_V="$CACHE_TYPE_V"; fi "$DFLASH_SERVER_BIN" "$TARGET" \ --draft "$DRAFT" \ --host "$HOST" \ diff --git a/harness/client_test_runner.py b/harness/client_test_runner.py index ac4693b8e..7763ae909 100755 --- a/harness/client_test_runner.py +++ b/harness/client_test_runner.py @@ -127,7 +127,7 @@ class ServerProfile: "--budget", "22", "--verify-mode", "ddtree", "--max-ctx", "4096", - "--fa-window", "1024", + "--fa-window", "0", "--cache-type-k", "q8_0", "--cache-type-v", "q8_0", "--prefix-cache-slots", "0", @@ -140,9 +140,9 @@ class ServerProfile: "--budget", "22", "--verify-mode", "ddtree", "--max-ctx", "8192", - "--fa-window", "2048", - "--cache-type-k", "tq3_0", - "--cache-type-v", "tq3_0", + "--fa-window", "0", + "--cache-type-k", "q8_0", + "--cache-type-v", "q8_0", "--prefix-cache-slots", "0", "--prefill-cache-slots", "0", ), @@ -153,9 +153,9 @@ class ServerProfile: "--budget", "22", "--verify-mode", "ddtree", "--max-ctx", "16384", - "--fa-window", "2048", - "--cache-type-k", "tq3_0", - "--cache-type-v", "tq3_0", + "--fa-window", "0", + "--cache-type-k", "q8_0", + "--cache-type-v", "q8_0", "--prefix-cache-slots", "0", "--prefill-cache-slots", "0", ), @@ -166,9 +166,9 @@ class ServerProfile: "--budget", "16", "--verify-mode", "ddtree", "--max-ctx", "32768", - "--fa-window", "2048", - "--cache-type-k", "tq3_0", - "--cache-type-v", "tq3_0", + "--fa-window", "0", + "--cache-type-k", "q8_0", + "--cache-type-v", "q8_0", "--prefix-cache-slots", "0", "--prefill-cache-slots", "0", "--lazy-draft", @@ -181,9 +181,9 @@ class ServerProfile: "--budget", "16", "--verify-mode", "ddtree", "--max-ctx", "32768", - "--fa-window", "2048", - "--cache-type-k", "tq3_0", - "--cache-type-v", "tq3_0", + "--fa-window", "0", + "--cache-type-k", "q8_0", + "--cache-type-v", "q8_0", "--prefix-cache-slots", "0", "--prefill-cache-slots", "0", "--prefill-compression", "auto", diff --git a/harness/clients/common.sh b/harness/clients/common.sh index 07e9d8ea8..3a5edf15f 100755 --- a/harness/clients/common.sh +++ b/harness/clients/common.sh @@ -55,8 +55,8 @@ BUDGET="${BUDGET:-22}" # calling (Qwen3.6) - only set it explicitly for experiments. FA_WINDOW="${FA_WINDOW:-0}" # CACHE_TYPE_K/V: empty = let the server pick the model family's default -# (qwen auto-selects tq3_0 above 6K ctx; laguna requires q8_0 - forcing -# tq3_0/q4_0 on laguna garbles its output). Set explicitly to experiment. +# (laguna q8_0, q4_0 base; forcing tq3_0/q4_0 on laguna garbles its +# output). Set explicitly to experiment. CACHE_TYPE_K="${CACHE_TYPE_K:-}" CACHE_TYPE_V="${CACHE_TYPE_V:-}" LLAMA_CACHE_TYPE_K="${LLAMA_CACHE_TYPE_K:-${CACHE_TYPE_K:-q8_0}}" diff --git a/harness/clients/run_openclaw.sh b/harness/clients/run_openclaw.sh index e409b9894..0a1cf3d70 100755 --- a/harness/clients/run_openclaw.sh +++ b/harness/clients/run_openclaw.sh @@ -5,7 +5,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" : "${MAX_CTX:=204800}" : "${BUDGET:=22}" : "${VERIFY_MODE:=ddtree}" -: "${FA_WINDOW:=2048}" +# FA_WINDOW stays 0 (common.sh): finite windows break tool calls. : "${EXTRA_SERVER_ARGS:=--lazy-draft}" source "$SCRIPT_DIR/common.sh" From 871bbf66f4e32ab61f6a2f8764143961edf6936c Mon Sep 17 00:00:00 2001 From: mrciffa <49000955+davide221@users.noreply.github.com> Date: Fri, 10 Jul 2026 22:29:11 +0200 Subject: [PATCH 18/20] fix(tools): laguna dialect review findings - Wrapped Laguna bodies whose holds JSON (the template serializes non-string args via tojson) were rejected by the '{' guard meant for Qwen JSON bodies; bodies containing pairs are now accepted. - convert_param_value assumed a string "type"; JSON Schema array types (["integer","null"]) threw out of the parser. First non-null string entry is used instead - hardens all eight dialects. - Zero-argument calls in the stripped form (bare declared tool name at end of output, wrapper tokens removed by detokenization) are recognized at finish and re-wrapped for the parser. Fourth reviewer suggestion (drop the tool-call instruction from the prompt) dismissed: the instruction is a verbatim copy of the model's own GGUF-embedded chat template; removing it would deviate from the trained format. Validated standalone (JSON-object args, array-typed schema, zero-arg) and live on RTX 3090: get_weather/{city:Paris} regression clean, zero-arg list_files/{} returns finish_reason=tool_calls. --- server/src/server/sse_emitter.cpp | 39 +++++++++++++++++++++++++++++-- server/src/server/tool_parser.cpp | 20 ++++++++++++++-- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/server/src/server/sse_emitter.cpp b/server/src/server/sse_emitter.cpp index 5c03672c2..f4d7bf1df 100644 --- a/server/src/server/sse_emitter.cpp +++ b/server/src/server/sse_emitter.cpp @@ -547,8 +547,43 @@ std::vector SseEmitter::emit_finish(int completion_tokens, default: break; } } else if (mode_ == StreamMode::CONTENT && !window_.empty()) { - accumulated_content_ += window_; - emit_content_delta(out, window_); + // Zero-argument Laguna calls in the stripped form are just the bare + // declared tool name at end of output (the wrapper is + // special tokens the detokenizer removed, and with no there + // is no trigger). If the output ENDS on exactly a declared tool name, + // treat it as a zero-arg call instead of trailing prose. + bool zero_arg_call = false; + if (has_request_tools(tools_)) { + auto is_ident = [](char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_' || c == '-'; + }; + size_t name_start = window_.size(); + while (name_start > 0 && is_ident(window_[name_start - 1])) name_start--; + const std::string tail = window_.substr(name_start); + if (!tail.empty()) { + for (const auto & t : tools_) { + if (t.contains("function") && t["function"].value("name", "") == tail) { + std::string pre = window_.substr(0, name_start); + if (!pre.empty()) { + accumulated_content_ += pre; + emit_content_delta(out, pre); + } + // Re-wrap in the canonical form the parser's wrapped + // path accepts (it emits name-only bodies as + // zero-argument calls). + tool_buffer_ = "" + tail + ""; + mode_ = StreamMode::TOOL_BUFFER; + zero_arg_call = true; + break; + } + } + } + } + if (!zero_arg_call) { + accumulated_content_ += window_; + emit_content_delta(out, window_); + } } else if (mode_ == StreamMode::TOOL_BUFFER) { tool_buffer_ += window_; } diff --git a/server/src/server/tool_parser.cpp b/server/src/server/tool_parser.cpp index e8235ff15..35442515b 100644 --- a/server/src/server/tool_parser.cpp +++ b/server/src/server/tool_parser.cpp @@ -84,7 +84,19 @@ static json convert_param_value(const std::string & val, const std::string & key const auto & cfg = props[key]; std::string ptype = "string"; if (cfg.is_object() && cfg.contains("type")) { - ptype = cfg["type"].get(); + const auto & t = cfg["type"]; + if (t.is_string()) { + ptype = t.get(); + } else if (t.is_array()) { + // JSON Schema allows "type": ["string","null"]; take the first + // non-null string entry instead of throwing. + for (const auto & e : t) { + if (e.is_string() && e.get() != "null") { + ptype = e.get(); + break; + } + } + } } // string types @@ -611,8 +623,12 @@ ToolParseResult parse_tool_calls(const std::string & text, const json & tools) { // Only claim bodies in the Laguna shape: bare name then arg tags // (or a bare name alone for zero-arg calls); leave // bodies to the Qwen patterns below. + // Laguna bodies are `NAME...` (values may contain JSON — + // the template serializes non-string args via tojson). Only leave + // and pure-JSON bodies to the Qwen patterns. if (body.find(" Date: Fri, 10 Jul 2026 22:33:31 +0200 Subject: [PATCH 19/20] fix(server): status report no longer claims the removed tq3_0 auto-default The /status model card still computed 'tq3_0 above 6144 ctx' for the KV type when no explicit --cache-type was passed, describing the auto policy that 5469578 deleted. Report 'family default' like the startup banner. --- server/src/server/server_main.cpp | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/server/src/server/server_main.cpp b/server/src/server/server_main.cpp index 354b7cd47..fe0ba31f5 100644 --- a/server/src/server/server_main.cpp +++ b/server/src/server/server_main.cpp @@ -1127,22 +1127,11 @@ int main(int argc, char ** argv) { sconfig.ddtree_budget = bargs.ddtree_budget; sconfig.speculative_enabled = bargs.ddtree_mode; sconfig.target_sharding = bargs.device.is_layer_split(); - // KV type: report the operator's choice if set, else the auto-default - // the daemon picks. Matches the printed table above. - sconfig.kv_cache_k = cache_type_k.empty() -#ifdef GGML_USE_HIP - ? "q4_0" -#else - ? (sconfig.max_ctx > 6144 ? "tq3_0" : "q4_0") -#endif - : cache_type_k; - sconfig.kv_cache_v = cache_type_v.empty() -#ifdef GGML_USE_HIP - ? "q4_0" -#else - ? (sconfig.max_ctx > 6144 ? "tq3_0" : "q4_0") -#endif - : cache_type_v; + // KV type: report the operator's choice if set, else the family default + // the backend resolves (the tq3_0 auto policy was removed; laguna uses + // q8_0, base default q4_0). Matches the printed table above. + sconfig.kv_cache_k = cache_type_k.empty() ? "family default" : cache_type_k; + sconfig.kv_cache_v = cache_type_v.empty() ? "family default" : cache_type_v; sconfig.runtime_backend = #ifdef GGML_USE_HIP "hip"; From a96edf2f486753f08a9d189a0e863ab3199df3cd Mon Sep 17 00:00:00 2001 From: mrciffa <49000955+davide221@users.noreply.github.com> Date: Fri, 10 Jul 2026 22:33:50 +0200 Subject: [PATCH 20/20] docs(laguna): drop stale reference to the removed tq3 auto policy --- server/src/laguna/laguna_backend.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/laguna/laguna_backend.cpp b/server/src/laguna/laguna_backend.cpp index de7a3074f..22ffdab21 100644 --- a/server/src/laguna/laguna_backend.cpp +++ b/server/src/laguna/laguna_backend.cpp @@ -50,9 +50,9 @@ namespace dflash::common { namespace { // Laguna honors only the explicit per-axis --cache-type-k/v overrides. -// The DFLASH27B_KV_F16/_Q4/_TQ3 shorthands are qwen-family toggles - the -// server auto-sets _KV_TQ3 for max_ctx > 6144 - and must not displace -// laguna's Q8_0 default (a TQ3_0/Q4_0 KV cache garbles laguna output). +// The DFLASH27B_KV_F16/_Q4/_TQ3 shorthands are qwen-family toggles and +// must not displace laguna's Q8_0 default (a TQ3_0/Q4_0 KV cache garbles +// laguna output). static void resolve_laguna_kv_types(const LagunaBackendArgs & args, ggml_type & k_type, ggml_type & v_type) {