From f014bfef8b0870f257abcbceb9d8d2c4f2c684d7 Mon Sep 17 00:00:00 2001 From: miyan <1138989048@qq.com> Date: Tue, 8 Sep 2026 15:34:12 +0800 Subject: [PATCH 01/12] Fix Vulkan-Hpp handle usage on 32-bit targets. (#22892) On 32-bit platforms, Vulkan non-dispatchable handles such as VkBuffer are represented as uint64_t, and Vulkan-Hpp disables implicit conversions for type safety. This exposes two issues in ggml-vulkan: 1. vk::Buffer is streamed directly into std::ostream in debug/memory logs. 2. vk::Buffer is cast to VkBuffer before being passed to Vulkan-Hpp CommandBuffer::copyBuffer APIs. Fix these by add the operator<< for vk::Buffer, and by passing vk::Buffer directly to Vulkan-Hpp copyBuffer calls. --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 75132c0b5..738e7cd92 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -95,6 +95,14 @@ typedef struct VkPhysicalDeviceCooperativeMatrixDecodeVectorFeaturesNV { #include "ggml-vulkan-shaders.hpp" +// On 32-bit platforms, Vulkan non-dispatchable handles such as VkBuffer are represented as uint64_t, +// and Vulkan-Hpp disables implicit conversions for type safety. +namespace { +inline std::ostream & operator<<(std::ostream & os, vk::Buffer buffer) { + return os << static_cast(buffer); +} +} + // remove this once it's more widely available in the SDK #if !defined(VK_KHR_shader_bfloat16) @@ -8742,7 +8750,7 @@ static bool ggml_vk_buffer_write_2d_async(vk_context subctx, vk_buffer& dst, siz } ggml_vk_sync_buffers(nullptr, subctx); - subctx->s->buffer->buf.copyBuffer((VkBuffer)staging_buffer->buffer, (VkBuffer)dst->buffer, slices); + subctx->s->buffer->buf.copyBuffer(staging_buffer->buffer, dst->buffer, slices); if (width == spitch) { deferred_memcpy((uint8_t *)staging_buffer->ptr, src, staging_size, &subctx->in_memcpys); From 64e9bceb2c3a856efed96feda784a50947049feb Mon Sep 17 00:00:00 2001 From: Ankit Khandelwal Date: Tue, 8 Sep 2026 13:05:02 +0530 Subject: [PATCH 02/12] vulkan : fuse UNARY(GELU|SIGMOID|SILU|SOFTPLUS) + MUL (#27220) * vulkan : fuse UNARY(SIGMOID|SILU|SOFTPLUS) + MUL * vulkan : fuse UNARY(SIGMOID|SILU|SOFTPLUS) + MUL - implement fusion in unary.comp behind UNARY_MUL_FUSION ifdef, specialized pipelines per op instead of runtime branching - fuse adjacent nodes only, ordering handled by graph_optimize - drop runtime consumer scan and pending_unary_mul deferral * vulkan : fuse UNARY(GELU|SIGMOID|SILU|SOFTPLUS) + MUL 1. GELU: gelu_mul_f32/f16 pipelines registered, CREATE_UNARY_MUL(gelu), GELU in dispatch + fuse gate + perf fusion name 2. Renamed/moved: gate is now ggml_vk_can_fuse_unary_mul(cgraph, unary_idx, mul_idx), placed with the other can-fuse helpers 3. norepeat both variants: each op gets plain (spec {0}) + _norepeat (spec {1}) pipelines from the same SPIR-V, selected via ggml_are_same_shape(src0, src1); the shape gate now allows broadcast (other dims equal-or-1) 4. graph_optimize: lambda deleted; standard "// UNARY + MUL: pull the consuming MUL forward" block added alongside the SSM_CONV/ROPE/MUL_MAT reorderings, with the same "other src must be weights or already processed" readiness check * vulkan : align unary_mul fusion with binary kernel layout, relax gelu test tolerance - schedule the fused kernel like mul.comp (256 threads x 2 unrolled iterations), recovering a 10-18% prompt-processing regression - allow 5e-7 f32 error for gelu_mul: the shader evaluates gelu with an exp-based tanh identity while the CPU reference uses tanhf (~1 ulp) * vulkan : use ggml_can_repeat in UNARY+MUL fusion shape check The fused kernel indexes src1 via per-dim fastmod (generic_binary_head.glsl), which is exact whenever the other operand tiles into the unary result -- not just when its dims are equal or 1. Replace the hand-rolled loop with ggml_can_repeat(other, unary) so the check matches the kernel's actual capability and reuses the standard helper. Argument order matters: reversed, it would wrongly admit graphs where the unary result is mul->src[1] and the other operand is larger, producing truncated output. Also add a rep_ne0 layout to the fused unary+mul backend tests covering a non-1 repeat factor along dim 0. * vulkan : fuse UNARY+MUL pairs separated by zero-compute nodes gemma4's per-layer embedding gating builds gelu -> view_2d_slice -> mul, where the intervening view is a zero-compute node aliasing an input that was computed much earlier. Strict adjacency requirements meant neither CUDA nor the vulkan unary+mul fusion handled this pattern. Extend ggml_vk_graph_optimize to detect a UNARY whose consuming MUL is separated only by unscheduled zero-compute nodes (GGML_OP_NONE, VIEW, RESHAPE, TRANSPOSE, PERMUTE) and schedule those nodes ahead of the pair, making it adjacent so the existing fusion applies. The reorder is guarded by ggml_vk_can_fuse_unary_mul, a source-availability check for every interleaved node, and the protected fusion patterns (topk_moe*, snake); if fusion is later rejected the reordered graph still executes correctly, just unfused. Add a view_mid layout to the fused unary+mul backend tests replicating the gemma4 pattern. * vulkan : support OP-on-B in UNARY+MUL fusion Some models apply the unary activation to the smaller MUL operand, e.g. qwen3next/qwen35moe shared-expert gating builds ffn_shexp * sigmoid(gate) with a [1,n_tokens] gate tensor. This shape was correctly rejected before: the fused kernel derives its iteration extent from the unary tensor and would leave most of the destination unwritten, and the generic same-shape requirement in ggml_can_fuse blocked the pair outright. Add UNARY_MUL_B_FUSION shader variants computing dst = src0 * OP(src1): the OP operand rides the existing per-dim fastmod indexing, while the iteration extent now comes from mul. Route {UNARY, MUL} pairs through a local can-fuse variant that drops the generic same-shape rule and instead requires the unary result to tile into mul->src[0] (ggml_can_repeat); pairs with the unary as src0 keep the previous direction check, and equal-shape pairs keep using the original pipelines. Add a "gate" layout to the fused unary+mul backend tests covering the shared-expert gate shape for gelu/sigmoid/silu/softplus in f32 and f16. * vulkan : fold unary+mul view-hoisting into graph_optimize dep checks Replace the dedicated UNARY + EMPTY* + MUL scanning block with two small extensions to the existing scheduling logic: - a consuming MUL may now join its in-set UNARY across a gap of unused zero-compute nodes (NONE/VIEW/RESHAPE/TRANSPOSE/PERMUTE), instead of requiring strict adjacency - while doing so, such zero-compute blockers are ignored for this pair Fusion validity is still decided later by ggml_vk_can_fuse at dispatch time, so a rejected pair simply executes adjacent-but-unfused. Note the relaxation must stay scoped to this pattern: exempting zero-compute blockers globally reproduces silent output corruption on gemma3n. * vulkan : select unary_mul OP-on-B via specialization constant Replace the UNARY_MUL_B_FUSION compile-time shader variants with an op_on_b specialization constant on the existing unary_mul SPIR-V, mirroring how the norepeat flag is handled. The four {op}_mul_b_{f32,f16} shader artifacts are gone - the OP-on-B pipelines reuse the base SPIR-V with two-entry {norepeat, op_on_b} spec lists - and the duplicated store expression is collapsed into a single runtime branch that the driver prunes per specialization. The constant is declared only under UNARY_MUL_FUSION so every other binary pipeline keeps its single-entry specialization list. * vulkan : replace unary_mul pipeline switches with a lookup table Collapse the four nested selection switches in ggml_vk_unary_mul into a single indexed lookup against a pipeline_unary_mul[4][2][2][2] table ([unary op][f16][norepeat][op_on_b]), whose trailing dims mirror the {norepeat, op_on_b} spec constant list. The op axis uses a small shared index helper that also replaces the switch in ggml_vk_can_fuse_unary_mul, making it the only place that maps ops to the table. Pipeline names are unchanged. Adding another supported op now requires one macro invocation line and one helper case instead of edits in four separate switches. * vulkan : use ggml_can_fuse_subgraph for unary_mul pairs Replace the hand-rolled pair validation in ggml_vk_can_fuse_unary_mul_pair (bounds, op match, compute flags, single-use elision) with the shared ggml_can_fuse_subgraph helper; backend-specific shape/type rules remain in ggml_vk_can_fuse_unary_mul. Unlike ggml_can_fuse, the subgraph helper has no same-shape requirement, so it covers both operand slots including OP-on-B gates, and additionally rejects intermediates flagged as graph outputs and validates view-source confinement. The outputs parameter takes absolute node indices into the cgraph. * Fix Whitespace * vulkan : drop redundant unary_mul gap check in graph_optimize The zero-compute nodes separating a UNARY from its consuming MUL are already scheduled ahead of the pair by pass 2 of an earlier optimization window, so the scoped gap tolerance added for this pattern is unreachable in practice - disabling it leaves gemma-3n dispatch counts unchanged (841 GELU_MUL per pass). Remove the flag, the empty blocker exemption, and the now-unused gap helper, restoring the strict adjacency requirement of the UNARY -> MUL pull-forward. Keep the relaxation scoped out entirely: generalizing "zero-compute nodes never block" beyond this pattern previously reproduced silent output corruption on gemma3n. * vulkan: fix whitespace (tab in indent) * vulkan: fix whitespace (extra blank line) * vulkan : move op_on_b spec constant to unary.comp op_on_b is only used by the fused unary*mul path. Keep generic_binary_head.glsl generic by defining it in unary.comp instead. Same constant_id=1 and guard, no functional change. * vulkan : make RMS_NORM/UNARY fusion gap-tolerant for views Strict j==c+1 blocked RMS_NORM->MUL and UNARY->MUL when a VIEW sits between (e.g. rms_norm -> view -> mul). Allow c==back() with an empty-or-scheduled gap, matching the review suggestion to check src linkage instead of adjacency. Scoped to the two blessed pairs; safe because gaps can only contain zero-compute nodes. * vulkan : trim comments in UNARY+MUL fusion Assisted-by: Muse Spark --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 168 +++++++++++++++++- .../src/ggml-vulkan/vulkan-shaders/unary.comp | 39 +++- .../vulkan-shaders/vulkan-shaders-gen.cpp | 9 + tests/test-backend-ops.cpp | 47 ++++- 4 files changed, 251 insertions(+), 12 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 738e7cd92..8f37f65b8 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -1071,6 +1071,9 @@ struct vk_device_struct { vk_pipeline pipeline_trunc[2]; vk_pipeline pipeline_sgn[2]; + // fused UNARY+MUL pipelines: [op][f16][norepeat][op_on_b] + vk_pipeline pipeline_unary_mul[4][2][2][2]; + vk_pipeline pipeline_add1_f16_f16; vk_pipeline pipeline_add1_f16_f32; vk_pipeline pipeline_add1_f32_f32; @@ -5930,6 +5933,26 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { CREATE_UNARY(expm1) #undef CREATE_UNARY +// spec constants: {norepeat, op_on_b} +#define CREATE_UNARY_MUL(name, idx) \ + for (int dt = 0; dt < 2; ++dt) { \ + const size_t len_ = dt ? name ## _mul_f16_len : name ## _mul_f32_len; \ + const unsigned char * data_ = dt ? name ## _mul_f16_data : name ## _mul_f32_data; \ + const std::string dts_ = dt ? "f16" : "f32"; \ + for (int ob = 0; ob < 2; ++ob) \ + for (int nr = 0; nr < 2; ++nr) \ + ggml_vk_create_pipeline(device, device->pipeline_unary_mul[(idx)][dt][nr][ob], \ + (#name "_mul" + std::string(ob ? "_b" : "") + "_" + dts_ + (nr ? "_norepeat" : "")).c_str(), \ + len_, data_, "main", 3, sizeof(vk_op_binary_push_constants), {512, 1, 1}, \ + { (uint32_t) nr, (uint32_t) ob }, 1); \ + } + + CREATE_UNARY_MUL(gelu, 0) + CREATE_UNARY_MUL(sigmoid, 1) + CREATE_UNARY_MUL(silu, 2) + CREATE_UNARY_MUL(softplus, 3) +#undef CREATE_UNARY_MUL + ggml_vk_create_pipeline(device, device->pipeline_add1_f16_f16, "add1_f16_f16", add1_f16_f16_len, add1_f16_f16_data, "main", 3, sizeof(vk_op_binary_push_constants), {512, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_add1_f16_f32, "add1_f16_f32", add1_f16_f32_len, add1_f16_f32_data, "main", 3, sizeof(vk_op_binary_push_constants), {512, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_add1_f32_f32, "add1_f32_f32", add1_f32_f32_len, add1_f32_f32_data, "main", 3, sizeof(vk_op_binary_push_constants), {512, 1, 1}, {}, 1); @@ -12416,7 +12439,7 @@ template <> void init_pushconst_tensor_offsets(ggml_backend_vk_context * ctx, vk } template -static void ggml_vk_op_f32(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * src2, const ggml_tensor * src3, ggml_tensor * dst, ggml_op op, PC&& pc) { +static void ggml_vk_op_f32(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * src2, const ggml_tensor * src3, ggml_tensor * dst, ggml_op op, PC&& pc, vk_pipeline pipeline_override = nullptr) { VK_LOG_DEBUG("ggml_vk_op_f32((" << src0 << ", name=" << src0->name << ", type=" << src0->type << ", ne0=" << src0->ne[0] << ", ne1=" << src0->ne[1] << ", ne2=" << src0->ne[2] << ", ne3=" << src0->ne[3] << ", nb0=" << src0->nb[0] << ", nb1=" << src0->nb[1] << ", nb2=" << src0->nb[2] << ", nb3=" << src0->nb[3]; if (src1 != nullptr) { std::cerr << "), (" << src1 << ", name=" << src1->name << ", type=" << src1->type << ", ne0=" << src1->ne[0] << ", ne1=" << src1->ne[1] << ", ne2=" << src1->ne[2] << ", ne3=" << src1->ne[3] << ", nb0=" << src1->nb[0] << ", nb1=" << src1->nb[1] << ", nb2=" << src1->nb[2] << ", nb3=" << src1->nb[3]; @@ -12447,7 +12470,12 @@ static void ggml_vk_op_f32(ggml_backend_vk_context * ctx, vk_context& subctx, co init_pushconst_fastdiv(pc); - vk_pipeline pipeline = ggml_vk_op_get_pipeline(ctx, src0, src1, src2, dst, op); + vk_pipeline pipeline; + if (pipeline_override) { + pipeline = pipeline_override; + } else { + pipeline = ggml_vk_op_get_pipeline(ctx, src0, src1, src2, dst, op); + } if (pipeline == nullptr) { std::cerr << "ggml_vulkan: Error: Missing op: " << ggml_op_name(op) << " for " << ggml_type_name(src0->type); @@ -13032,6 +13060,52 @@ static void ggml_vk_mul(ggml_backend_vk_context * ctx, vk_context& subctx, const }); } +// index into device->pipeline_unary_mul for the supported unary ops, or -1 +static int ggml_vk_unary_mul_op_index(ggml_unary_op op) { + switch (op) { + case GGML_UNARY_OP_GELU: return 0; + case GGML_UNARY_OP_SIGMOID: return 1; + case GGML_UNARY_OP_SILU: return 2; + case GGML_UNARY_OP_SOFTPLUS: return 3; + default: return -1; + } +} + +static void ggml_vk_unary_mul(ggml_backend_vk_context * ctx, vk_context& subctx, const struct ggml_cgraph * cgraph, int node_idx) { + const ggml_tensor * unary = cgraph->nodes[node_idx]; + ggml_tensor * mul = cgraph->nodes[node_idx + 1]; + + // unary on src1 that tiles into src0 + const bool op_on_b = mul->src[1] == unary && + !ggml_are_same_shape(unary->src[0], mul->src[0]) && + ggml_can_repeat(unary, mul->src[0]); + + const ggml_tensor * src0 = op_on_b ? mul->src[0] : unary->src[0]; + const ggml_tensor * src1 = op_on_b ? unary->src[0] : + ((mul->src[0] == unary) ? mul->src[1] : mul->src[0]); + + const bool f16 = src0->type == GGML_TYPE_F16; + const bool norepeat = ggml_are_same_shape(src0, src1); + const int oi = ggml_vk_unary_mul_op_index(ggml_get_unary_op(unary)); + if (oi < 0) { + GGML_ABORT("fatal error"); + } + vk_pipeline pipeline = ctx->device->pipeline_unary_mul[oi][f16][norepeat][op_on_b]; + + const uint32_t src0_type_size = ggml_type_size(src0->type); + const uint32_t src1_type_size = ggml_type_size(src1->type); + const uint32_t dst_type_size = ggml_type_size(mul->type); + + ggml_vk_op_f32(ctx, subctx, src0, src1, nullptr, nullptr, mul, GGML_OP_UNARY, { + (uint32_t)ggml_nelements(op_on_b ? mul : src0), + (uint32_t)src0->ne[0], (uint32_t)src0->ne[1], (uint32_t)src0->ne[2],(uint32_t)src0->ne[3], (uint32_t)src0->nb[0] / src0_type_size, (uint32_t)src0->nb[1] / src0_type_size, (uint32_t)src0->nb[2] / src0_type_size, (uint32_t)src0->nb[3] / src0_type_size, + (uint32_t)src1->ne[0], (uint32_t)src1->ne[1], (uint32_t)src1->ne[2],(uint32_t)src1->ne[3], (uint32_t)src1->nb[0] / src1_type_size, (uint32_t)src1->nb[1] / src1_type_size, (uint32_t)src1->nb[2] / src1_type_size, (uint32_t)src1->nb[3] / src1_type_size, + (uint32_t) mul->ne[0], (uint32_t) mul->ne[1], (uint32_t) mul->ne[2],(uint32_t) mul->ne[3], (uint32_t) mul->nb[0] / dst_type_size, (uint32_t) mul->nb[1] / dst_type_size, (uint32_t) mul->nb[2] / dst_type_size, (uint32_t) mul->nb[3] / dst_type_size, + 0, + 0.0f, 0.0f, 0, + }, pipeline); +} + static void ggml_vk_div(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst) { const uint32_t src0_type_size = ggml_type_size(src0->type); const uint32_t src1_type_size = ggml_type_size(src1->type); @@ -16314,6 +16388,10 @@ static bool ggml_vk_build_graph(ggml_backend_vk_context * ctx, ggml_cgraph * cgr ggml_vk_topk_moe(ctx, compute_ctx, cgraph, node_idx); break; } + if (ctx->num_additional_fused_ops) { + ggml_vk_unary_mul(ctx, compute_ctx, cgraph, node_idx); + break; + } switch (ggml_get_unary_op(node)) { case GGML_UNARY_OP_ELU: @@ -17275,7 +17353,48 @@ static bool ggml_vk_is_empty(ggml_tensor * node) { return ggml_is_empty(node) || node->op == GGML_OP_NONE || node->op == GGML_OP_RESHAPE || node->op == GGML_OP_TRANSPOSE || node->op == GGML_OP_VIEW || node->op == GGML_OP_PERMUTE; } +static bool ggml_vk_can_fuse_unary_mul(const struct ggml_cgraph * cgraph, int unary_idx, int mul_idx) { + const ggml_tensor * unary = cgraph->nodes[unary_idx]; + const ggml_tensor * mul = cgraph->nodes[mul_idx]; + + if (ggml_vk_unary_mul_op_index(ggml_get_unary_op(unary)) < 0) { + return false; + } + if (unary->type != GGML_TYPE_F32 && unary->type != GGML_TYPE_F16) { + return false; + } + if (unary->type != mul->type) { + return false; + } + if (mul->src[0] != unary && mul->src[1] != unary) { + return false; + } + const ggml_tensor * other = (mul->src[0] == unary) ? mul->src[1] : mul->src[0]; + if (other == nullptr || other->type != unary->type) { + return false; + } + if (!ggml_is_contiguous_1(other) || !ggml_is_contiguous_1(unary->src[0])) { + return false; + } + // fastmod needs src to tile into dst + if (mul->src[0] == unary) { + return ggml_can_repeat(other, unary); + } + return ggml_can_repeat(unary, mul->src[0]); +} + +static bool ggml_vk_can_fuse_unary_mul_pair(const struct ggml_cgraph * cgraph, int node_idx) { + const enum ggml_op ops[] = { GGML_OP_UNARY, GGML_OP_MUL }; + const int outputs[] = { node_idx + 1 }; + return ggml_can_fuse_subgraph(cgraph, node_idx, 2, ops, outputs, 1) && + ggml_vk_can_fuse_unary_mul(cgraph, node_idx, node_idx + 1); +} + static bool ggml_vk_can_fuse(const ggml_backend_vk_context * ctx, const struct ggml_cgraph * cgraph, int node_idx, std::initializer_list ops) { + if (ops.size() == 2 && ops.begin()[0] == GGML_OP_UNARY && ops.begin()[1] == GGML_OP_MUL) { + return ggml_vk_can_fuse_unary_mul_pair(cgraph, node_idx); + } + if (!ggml_can_fuse(cgraph, node_idx, ops)) { return false; } @@ -17341,6 +17460,7 @@ static bool ggml_vk_can_fuse(const ggml_backend_vk_context * ctx, const struct g } } } + auto const &mm_add_ok = [&](const ggml_tensor *mul, const ggml_tensor *add) { const ggml_tensor *bias = add->src[0] == mul ? add->src[1] : add->src[0]; @@ -18209,6 +18329,16 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg // they are overwritten, and one workgroup per row. So close enough. op_srcs_fused_elementwise[0] = true; op_srcs_fused_elementwise[1] = true; + } else if (ggml_vk_can_fuse(ctx, cgraph, i, { GGML_OP_UNARY, GGML_OP_MUL })) { + ctx->num_additional_fused_ops = 1; + switch (ggml_get_unary_op(cgraph->nodes[i])) { + case GGML_UNARY_OP_GELU: fusion_string = "GELU_MUL"; break; + case GGML_UNARY_OP_SIGMOID: fusion_string = "SIGMOID_MUL"; break; + case GGML_UNARY_OP_SILU: fusion_string = "SILU_MUL"; break; + default: fusion_string = "SOFTPLUS_MUL"; break; + } + op_srcs_fused_elementwise[0] = true; + op_srcs_fused_elementwise[1] = true; } else if (ggml_vk_can_fuse_ssm_conv(ctx, cgraph, i, 2)) { ctx->num_additional_fused_ops = 2; fusion_string = "SSM_CONV_BIAS_SILU"; @@ -18507,6 +18637,16 @@ static void ggml_vk_graph_optimize(ggml_backend_t backend, struct ggml_cgraph * std::set used_node_set; int first_unused = 0; + + // scheduled or zero-compute nodes in [lo, hi) + auto const &empty_or_scheduled_between = [&](int lo, int hi) -> bool { + for (int v = lo; v < hi; ++v) { + if (!used[v] && !is_empty(graph->nodes[v])) { + return false; + } + } + return true; + }; while (first_unused < graph->n_nodes) { std::vector current_set; @@ -18622,7 +18762,8 @@ static void ggml_vk_graph_optimize(ggml_backend_t backend, struct ggml_cgraph * for (int c = first_unused; c < j; ++c) { if (!used[c] && is_src_of(graph->nodes[j], graph->nodes[c]) && - !(j == c+1 && c == current_set.back() && graph->nodes[c]->op == GGML_OP_RMS_NORM && graph->nodes[j]->op == GGML_OP_MUL) && + !(c == current_set.back() && graph->nodes[c]->op == GGML_OP_RMS_NORM && graph->nodes[j]->op == GGML_OP_MUL && empty_or_scheduled_between(c+1, j)) && + !(c == current_set.back() && graph->nodes[c]->op == GGML_OP_UNARY && graph->nodes[j]->op == GGML_OP_MUL && empty_or_scheduled_between(c+1, j)) && !(j == c+1 && c == current_set.back() && graph->nodes[c]->op == GGML_OP_MUL_MAT && graph->nodes[j]->op == GGML_OP_ADD) && !(j == c+1 && c == current_set.back() && graph->nodes[c]->op == GGML_OP_MUL_MAT_ID && graph->nodes[j]->op == GGML_OP_ADD_ID) && !(j == c+1 && c == current_set.back() && graph->nodes[c]->op == GGML_OP_MUL_MAT_ID && graph->nodes[j]->op == GGML_OP_MUL) && @@ -18735,6 +18876,27 @@ static void ggml_vk_graph_optimize(ggml_backend_t backend, struct ggml_cgraph * } } } + // UNARY + MUL: pull the consuming MUL forward + if (j > 0 && + graph->nodes[j]->op == GGML_OP_UNARY) { + for (int k = j + 1; k < std::min(j + 15, graph->n_nodes); ++k) { + ggml_tensor * mul = graph->nodes[k]; + if (mul->op != GGML_OP_MUL || (mul->src[0] != graph->nodes[j] && mul->src[1] != graph->nodes[j])) { + continue; + } + ggml_tensor * other = (mul->src[0] == graph->nodes[j]) ? mul->src[1] : mul->src[0]; + // the other src must either be weights or already processed + if (!(other->op == GGML_OP_NONE || used_node_set.find(other) != used_node_set.end())) { + continue; + } + if (!ggml_vk_can_fuse_unary_mul(graph, j, k)) { + continue; + } + current_set.push_back(k); + used[k] = true; + break; + } + } } } // Second pass grabs view nodes. diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/unary.comp b/ggml/src/ggml-vulkan/vulkan-shaders/unary.comp index 5ee5275d2..9ee7769ba 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/unary.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/unary.comp @@ -1,9 +1,23 @@ #version 450 #include "types.glsl" +#if defined(UNARY_MUL_FUSION) +#include "generic_binary_head.glsl" +#else #include "generic_unary_head.glsl" +#endif +#if defined(UNARY_MUL_FUSION) +// OP on src1 +layout(constant_id = 1) const bool op_on_b = false; +#endif + +#if defined(UNARY_MUL_FUSION) +layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; +const uint num_threads = 256; +#else layout(local_size_x = 512, local_size_y = 1, local_size_z = 1) in; +#endif float op_abs(float x) { return abs(x); @@ -123,6 +137,7 @@ float op_gelu_erf(float a) { return 0.5f * a * (1.0f + sign_x * y); } +#if !defined(UNARY_MUL_FUSION) float op_xielu(float x) { const float alpha_n = p.param1; const float alpha_p = p.param2; @@ -136,6 +151,7 @@ float op_xielu(float x) { const float min_x_eps = min(x, eps); return (op_expm1(min_x_eps) - x) * alpha_n + beta * x; } +#endif float op_floor(float x) { return floor(x); @@ -155,8 +171,28 @@ float op_trunc(float x) { } void main() { - const uint idx = get_idx(); + uint idx = get_idx(); +#if defined(UNARY_MUL_FUSION) + // keep total threads at 512 + [[unroll]] for (uint iter = 0; iter < 2; ++iter) { + if (idx >= p.ne) { + continue; + } + uint i00, i01, i02, i03; + get_indices(idx, i00, i01, i02, i03); + + if (op_on_b) { + data_d[get_doffset() + dst_idx(i00, i01, i02, i03)] = + D_TYPE(FLOAT_TYPE(OP(float(data_b[get_boffset() + src1_idx(i00, i01, i02, i03)]))) * FLOAT_TYPE(data_a[get_aoffset() + src0_idx(i00, i01, i02, i03)])); + } else { + data_d[get_doffset() + dst_idx(i00, i01, i02, i03)] = + D_TYPE(FLOAT_TYPE(OP(float(data_a[get_aoffset() + src0_idx(i00, i01, i02, i03)]))) * FLOAT_TYPE(data_b[get_boffset() + src1_idx(i00, i01, i02, i03)])); + } + + idx += num_threads; + } +#else if (idx >= p.ne) { return; } @@ -165,4 +201,5 @@ void main() { const uint d_idx = get_doffset() + dst_idx(idx); data_d[d_idx] = D_TYPE(OP(float(data_a[a_idx]))); +#endif } diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp index 2daafdf43..cb1128dcc 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -966,6 +966,15 @@ void process_shaders() { string_to_spv("softplus_f16", "unary.comp", {{"A_TYPE", "float16_t"}, {"D_TYPE", "float16_t"}, {"OP", "op_softplus"}}); string_to_spv("softplus_f32", "unary.comp", {{"A_TYPE", "float"}, {"D_TYPE", "float"}, {"OP", "op_softplus"}}); + string_to_spv("gelu_mul_f32", "unary.comp", {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"OP", "op_gelu"}, {"UNARY_MUL_FUSION", "1"}}); + string_to_spv("gelu_mul_f16", "unary.comp", {{"A_TYPE", "float16_t"}, {"B_TYPE", "float16_t"}, {"D_TYPE", "float16_t"}, {"FLOAT_TYPE", "float"}, {"OP", "op_gelu"}, {"UNARY_MUL_FUSION", "1"}}); + string_to_spv("sigmoid_mul_f32", "unary.comp", {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"OP", "op_sigmoid"}, {"UNARY_MUL_FUSION", "1"}}); + string_to_spv("sigmoid_mul_f16", "unary.comp", {{"A_TYPE", "float16_t"}, {"B_TYPE", "float16_t"}, {"D_TYPE", "float16_t"}, {"FLOAT_TYPE", "float"}, {"OP", "op_sigmoid"}, {"UNARY_MUL_FUSION", "1"}}); + string_to_spv("silu_mul_f32", "unary.comp", {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"OP", "op_silu"}, {"UNARY_MUL_FUSION", "1"}}); + string_to_spv("silu_mul_f16", "unary.comp", {{"A_TYPE", "float16_t"}, {"B_TYPE", "float16_t"}, {"D_TYPE", "float16_t"}, {"FLOAT_TYPE", "float"}, {"OP", "op_silu"}, {"UNARY_MUL_FUSION", "1"}}); + string_to_spv("softplus_mul_f32","unary.comp", {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"OP", "op_softplus"}, {"UNARY_MUL_FUSION", "1"}}); + string_to_spv("softplus_mul_f16","unary.comp", {{"A_TYPE", "float16_t"}, {"B_TYPE", "float16_t"}, {"D_TYPE", "float16_t"}, {"FLOAT_TYPE", "float"}, {"OP", "op_softplus"}, {"UNARY_MUL_FUSION", "1"}}); + string_to_spv("add1_f16_f16", "add1.comp", {{"A_TYPE", "float16_t"}, {"B_TYPE", "float16_t"}, {"D_TYPE", "float16_t"}, {"FLOAT_TYPE", "float"}}); string_to_spv("add1_f16_f32", "add1.comp", {{"A_TYPE", "float16_t"}, {"B_TYPE", "float"}, {"D_TYPE", "float16_t"}, {"FLOAT_TYPE", "float"}}); string_to_spv("add1_f32_f32", "add1.comp", {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}}); diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 5121578ff..01e72041b 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -3908,8 +3908,7 @@ struct test_relu_sqr : public test_case { } }; -// GGML_OP_UNARY(SILU|SIGMOID|SOFTPLUS) + GGML_OP_MUL (fused operation). -// `layout` and `tail` are used for fallback cases where fusion must be skipped +// GGML_OP_UNARY(GELU|SILU|SIGMOID|SOFTPLUS) + GGML_OP_MUL (fused operation). struct test_unary_mul : public test_case { const ggml_unary_op op; const ggml_type type; @@ -3930,7 +3929,8 @@ struct test_unary_mul : public test_case { // performs; relax the tolerance to match that drift switch (type) { case GGML_TYPE_F16: return 5e-5; - default: return 1e-7; + // gelu shader uses exp form, CPU uses tanhf + default: return op == GGML_UNARY_OP_GELU ? 5e-7 : 1e-7; } } @@ -3989,17 +3989,45 @@ struct test_unary_mul : public test_case { } else if (layout == "bcast") { a = ggml_new_tensor(ctx, type, 4, ne.data()); b = ggml_new_tensor_4d(ctx, type, ne[0], 1, 1, 1); + } else if (layout == "rep_ne0") { + // repeat on dim 0 + a = ggml_new_tensor(ctx, type, 4, ne.data()); + std::array ne_b = ne; + ne_b[0] /= 4; + b = ggml_new_tensor(ctx, type, 4, ne_b.data()); + } else if (layout == "view_mid") { + // VIEW between UNARY and MUL + a = ggml_new_tensor(ctx, type, 4, ne.data()); + b = nullptr; + } else if (layout == "gate") { + // small gate on src1 + const std::array ne_gate = { 1, ne[1], ne[2], ne[3] }; + a = ggml_new_tensor(ctx, type, 4, ne_gate.data()); + b = ggml_new_tensor(ctx, type, 4, ne.data()); } else { GGML_ABORT("unknown layout %s", layout.c_str()); } - ggml_set_name(a, "a"); - ggml_set_name(b, "b"); + if (a != nullptr) { + ggml_set_name(a, "a"); + } + if (b != nullptr) { + ggml_set_name(b, "b"); + } ggml_tensor * u = ggml_unary(ctx, a, op); ggml_set_name(u, "unary"); // a broadcasting operand can only be the second one - const bool second = swap && layout != "bcast"; + const bool second = layout == "gate" || (swap && layout != "bcast" && layout != "view_mid"); + if (layout == "view_mid") { + std::array ne_base = ne; + ne_base[0] *= 2; + ggml_tensor * base = ggml_new_tensor(ctx, type, 4, ne_base.data()); + ggml_set_name(base, "base"); + b = ggml_view_4d(ctx, base, ne[0], ne[1], ne[2], ne[3], + base->nb[1], base->nb[2], base->nb[3], 0); + ggml_set_name(b, "b"); + } ggml_tensor * out = second ? ggml_mul(ctx, b, u) : ggml_mul(ctx, u, b); if (tail == "reuse") { @@ -8815,7 +8843,7 @@ static std::vector> make_test_cases_eval() { } // fused unary + mul (gated activations that are not expressed as GGML_OP_GLU) - for (ggml_unary_op op : { GGML_UNARY_OP_SILU, GGML_UNARY_OP_SIGMOID, GGML_UNARY_OP_SOFTPLUS }) { + for (ggml_unary_op op : { GGML_UNARY_OP_GELU, GGML_UNARY_OP_SILU, GGML_UNARY_OP_SIGMOID, GGML_UNARY_OP_SOFTPLUS }) { for (ggml_type type : { GGML_TYPE_F16, GGML_TYPE_F32 }) { for (bool swap : { false, true }) { test_cases.emplace_back(new test_unary_mul(op, type, { 128, 2, 2, 2 }, swap)); @@ -8826,9 +8854,12 @@ static std::vector> make_test_cases_eval() { test_cases.emplace_back(new test_unary_mul(op, type, { 128, 2, 2, 2 }, true, "pad_other")); test_cases.emplace_back(new test_unary_mul(op, type, { 128, 2, 2, 2 }, true, "halves")); test_cases.emplace_back(new test_unary_mul(op, type, { 128, 2, 2, 2 }, false, "packed", "consumer")); + test_cases.emplace_back(new test_unary_mul(op, type, { 128, 2, 2, 2 }, false, "bcast")); + test_cases.emplace_back(new test_unary_mul(op, type, { 128, 2, 2, 2 }, false, "rep_ne0")); + test_cases.emplace_back(new test_unary_mul(op, type, { 128, 2, 2, 2 }, false, "view_mid")); + test_cases.emplace_back(new test_unary_mul(op, type, { 128, 2, 2, 2 }, false, "gate")); // must not fuse test_cases.emplace_back(new test_unary_mul(op, type, { 128, 2, 2, 2 }, false, "strided_dim1")); - test_cases.emplace_back(new test_unary_mul(op, type, { 128, 2, 2, 2 }, false, "bcast")); test_cases.emplace_back(new test_unary_mul(op, type, { 128, 2, 2, 2 }, false, "packed", "reuse")); } } From ca86fb222e0080d4d102f4390d5c7279dffb3277 Mon Sep 17 00:00:00 2001 From: Pepper Gray Date: Tue, 8 Sep 2026 12:59:53 +0200 Subject: [PATCH 03/12] llama : add missing headers (#28566) * fix compile-error: add missing header Bug: #28557 Signed-off-by: Pepper Gray * fix compile-error: add missing header Bug: #28559 Signed-off-by: Pepper Gray * fix compile-error: add missing header Bug: #28560 Signed-off-by: Pepper Gray * fix compile-error: add missing header Bug: #28561 Signed-off-by: Pepper Gray * fix compile-error: add missing header Bug: #28562 Signed-off-by: Pepper Gray * fix compile-error: add missing header Bug: #28564 Signed-off-by: Pepper Gray --------- Signed-off-by: Pepper Gray --- ggml/src/gguf.cpp | 1 + src/llama-graph.h | 1 + src/llama-mmap.cpp | 1 + src/llama-vocab.cpp | 1 + tests/test-sampling.cpp | 1 + tools/mtmd/deprecation-warning.cpp | 1 + 6 files changed, 6 insertions(+) diff --git a/ggml/src/gguf.cpp b/ggml/src/gguf.cpp index 6c7b58178..144a8edf8 100644 --- a/ggml/src/gguf.cpp +++ b/ggml/src/gguf.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include diff --git a/src/llama-graph.h b/src/llama-graph.h index b486578c1..cc4110639 100644 --- a/src/llama-graph.h +++ b/src/llama-graph.h @@ -6,6 +6,7 @@ #include "llama-adapter.h" #include +#include #include #include #include diff --git a/src/llama-mmap.cpp b/src/llama-mmap.cpp index 4d183cbc9..715a6e354 100644 --- a/src/llama-mmap.cpp +++ b/src/llama-mmap.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include diff --git a/src/llama-vocab.cpp b/src/llama-vocab.cpp index a69801f08..ee65faf23 100644 --- a/src/llama-vocab.cpp +++ b/src/llama-vocab.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include diff --git a/tests/test-sampling.cpp b/tests/test-sampling.cpp index d727ab632..353a5a1a1 100644 --- a/tests/test-sampling.cpp +++ b/tests/test-sampling.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include diff --git a/tools/mtmd/deprecation-warning.cpp b/tools/mtmd/deprecation-warning.cpp index 2b31a9d8b..615d7577b 100644 --- a/tools/mtmd/deprecation-warning.cpp +++ b/tools/mtmd/deprecation-warning.cpp @@ -1,5 +1,6 @@ #include #include +#include #include int main(int argc, char** argv) { From 1744c6bde8d687ce9774b3b54e688eee0bfdf5b7 Mon Sep 17 00:00:00 2001 From: Daniel Bevenius Date: Tue, 8 Sep 2026 13:36:03 +0200 Subject: [PATCH 04/12] ci : add PYTEST_WORKERS=1 to fix server-self-hosted job (#28603) * ci : add PYTEST_WORKERS=1 to fix server-self-hosted job This commit adds the `PYTEST_WORKERS=1` environment variable to the hf-jobs-t4-small:cuda13 runner steps. This is an attempt to address CI failure of this job that I might have introduced in Commit 42f0225fea945b24e92a0ce716e59b7c13e9b819 ("server : use pytest-xdist for server tests (#28298)"). Refs: https://github.com/ggml-org/llama.cpp/actions/runs/34126971262/job/101757819134 * apply same changes to server-metal steps --- .github/workflows/server-self-hosted.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/server-self-hosted.yml b/.github/workflows/server-self-hosted.yml index d9ad2fcd0..de30d1a74 100644 --- a/.github/workflows/server-self-hosted.yml +++ b/.github/workflows/server-self-hosted.yml @@ -72,7 +72,7 @@ jobs: run: | cd tools/server/tests source venv/bin/activate - ./tests.sh + PYTEST_WORKERS=1 ./tests.sh - name: Tests (GPUx1, backend-sampling) id: server_integration_tests_backend_sampling @@ -81,7 +81,7 @@ jobs: cd tools/server/tests source venv/bin/activate export LLAMA_ARG_BACKEND_SAMPLING=1 - ./tests.sh + PYTEST_WORKERS=1 ./tests.sh - name: Tests (GPUx2) id: server_integration_tests_gpu2 @@ -90,7 +90,7 @@ jobs: cd tools/server/tests source venv/bin/activate export GGML_METAL_DEVICES=2 - ./tests.sh + PYTEST_WORKERS=1 ./tests.sh - name: Tests (GPUx2, backend-sampling) id: server_integration_tests_gpu2_backend_sampling @@ -99,7 +99,7 @@ jobs: cd tools/server/tests source venv/bin/activate export GGML_METAL_DEVICES=2 LLAMA_ARG_BACKEND_SAMPLING=1 - ./tests.sh + PYTEST_WORKERS=1 ./tests.sh server-cuda: runs-on: "hf-jobs-t4-small:cuda13" @@ -162,7 +162,7 @@ jobs: run: | cd tools/server/tests source venv/bin/activate - ./tests.sh + PYTEST_WORKERS=1 ./tests.sh - name: Tests (GPUx1, backend-sampling) id: server_integration_tests_backend_sampling @@ -171,7 +171,7 @@ jobs: cd tools/server/tests source venv/bin/activate export LLAMA_ARG_BACKEND_SAMPLING=1 - ./tests.sh + PYTEST_WORKERS=1 ./tests.sh - name: Tests (GPUx2) id: server_integration_tests_gpu2 @@ -180,7 +180,7 @@ jobs: cd tools/server/tests source venv/bin/activate export GGML_CUDA_DEVICES=2 - ./tests.sh + PYTEST_WORKERS=1 ./tests.sh - name: Tests (GPUx2, backend-sampling) id: server_integration_tests_gpu2_backend_sampling @@ -189,7 +189,7 @@ jobs: cd tools/server/tests source venv/bin/activate export GGML_CUDA_DEVICES=2 LLAMA_ARG_BACKEND_SAMPLING=1 - ./tests.sh + PYTEST_WORKERS=1 ./tests.sh server-kleidiai: runs-on: ah-ubuntu_22_04-c8g_8x From 03fa73cb27f5c251b9528489b18d303b1366aca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigbj=C3=B8rn=20Skj=C3=A6ret?= Date: Tue, 8 Sep 2026 14:06:15 +0200 Subject: [PATCH 05/12] ci : disable npm gha cache (#28600) * disable npm gha cache * lies --- .github/workflows/ui-build-self-hosted.yml | 5 +++-- .github/workflows/ui-build.yml | 5 +++-- .github/workflows/ui.yml | 10 ++++++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ui-build-self-hosted.yml b/.github/workflows/ui-build-self-hosted.yml index 390a2f35f..e93a89003 100644 --- a/.github/workflows/ui-build-self-hosted.yml +++ b/.github/workflows/ui-build-self-hosted.yml @@ -17,8 +17,9 @@ jobs: uses: actions/setup-node@v6 with: node-version: "24" - cache: "npm" - cache-dependency-path: "tools/ui/package-lock.json" + # cache: "npm" + # cache-dependency-path: "tools/ui/package-lock.json" + package-manager-cache: false - name: Install dependencies run: npm ci diff --git a/.github/workflows/ui-build.yml b/.github/workflows/ui-build.yml index 3fbd90c11..cbadaa9e7 100644 --- a/.github/workflows/ui-build.yml +++ b/.github/workflows/ui-build.yml @@ -33,8 +33,9 @@ jobs: uses: actions/setup-node@v6 with: node-version: "24" - cache: "npm" - cache-dependency-path: "tools/ui/package-lock.json" + # cache: "npm" + # cache-dependency-path: "tools/ui/package-lock.json" + package-manager-cache: false - name: Install dependencies run: npm ci diff --git a/.github/workflows/ui.yml b/.github/workflows/ui.yml index 00a0804af..f395c0b52 100644 --- a/.github/workflows/ui.yml +++ b/.github/workflows/ui.yml @@ -57,8 +57,9 @@ jobs: uses: actions/setup-node@v6 with: node-version: "24" - cache: "npm" - cache-dependency-path: "tools/ui/package-lock.json" + # cache: "npm" + # cache-dependency-path: "tools/ui/package-lock.json" + package-manager-cache: false - name: Download built UI artifacts uses: actions/download-artifact@v6 @@ -114,8 +115,9 @@ jobs: uses: actions/setup-node@v6 with: node-version: "24" - cache: "npm" - cache-dependency-path: "tools/ui/package-lock.json" + # cache: "npm" + # cache-dependency-path: "tools/ui/package-lock.json" + package-manager-cache: false - name: Install dependencies id: setup From 415e909d84334a7b1f582229c166aa98be6c4678 Mon Sep 17 00:00:00 2001 From: Aman Gupta Date: Tue, 8 Sep 2026 20:44:33 +0800 Subject: [PATCH 06/12] spec: single device drafter should create meta backend wrapper (#28390) --- common/arg.cpp | 10 ++++++++-- common/speculative.cpp | 13 ++++++++++++- src/llama-context.cpp | 3 +++ tools/cli/README.md | 4 ++-- tools/server/README.md | 4 ++-- 5 files changed, 27 insertions(+), 7 deletions(-) diff --git a/common/arg.cpp b/common/arg.cpp index 015196ca1..74241f931 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -894,6 +894,12 @@ static bool common_params_parse_ex(int argc, char ** argv, common_params_context postprocess_cpu_params(params.speculative.draft.cpuparams, ¶ms.cpuparams); postprocess_cpu_params(params.speculative.draft.cpuparams_batch, ¶ms.cpuparams_batch); + // default the mmproj device to the global device selection if not set explicitly with -mmdev + if (params.mmproj_use_gpu && params.mmproj_device == nullptr && !params.devices.empty()) { + params.mmproj_device = params.devices.front(); + params.mmproj_use_gpu = params.mmproj_device != nullptr; + } + if (params.prompt_cache_all && (params.interactive || params.interactive_first)) { throw std::invalid_argument("error: --prompt-cache-all not supported in interactive mode yet\n"); } @@ -2610,7 +2616,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex add_opt(common_arg( // note: "-mmdev" must sort after "--rpc" in the preset map, else RPC devices are not registered yet {"-mmdev", "--mmproj-device"}, "DEVICE", - "device to use for multimodal projector (none = don't offload, default: auto)\n" + "device to use for multimodal projector (none = don't offload, default: follows --device)\n" "use --list-devices to see a list of available devices", [](common_params & params, const std::string & value) { if (value == "none") { @@ -4229,7 +4235,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex ).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_SPEC_DRAFT_BACKEND_SAMPLING")); add_opt(common_arg( {"--spec-draft-device", "-devd", "--device-draft"}, "", - "comma-separated list of devices to use for offloading the draft model (none = don't offload)\n" + "comma-separated list of devices to use for offloading the draft model (none = don't offload, default: follows --device)\n" "use --list-devices to see a list of available devices", [](common_params & params, const std::string & value) { params.speculative.draft.devices = parse_device_list(value); diff --git a/common/speculative.cpp b/common/speculative.cpp index 851a47b9a..2db381d58 100644 --- a/common/speculative.cpp +++ b/common/speculative.cpp @@ -2467,11 +2467,22 @@ common_params common_base_params_to_speculative(const common_params & params) { result.pooling_type = LLAMA_POOLING_TYPE_UNSPECIFIED; if (has_draft) { - result.devices = params_spec.devices; + // default to global devices value + if (!params_spec.devices.empty()) { + result.devices = params_spec.devices; + } result.model = params_spec.mparams; result.n_gpu_layers = params_spec.n_gpu_layers; result.tensor_buft_overrides = params_spec.tensor_buft_overrides; + // a draft pinned to a single device doesn't need the meta wrapper an inherited -sm tensor would give it + // (the device list is null-terminated, so a single device means size 2) + const size_t n_devs = std::count_if(params_spec.devices.begin(), params_spec.devices.end(), + [](ggml_backend_dev_t d) { return d != nullptr; }); + if (n_devs == 1) { + result.split_mode = LLAMA_SPLIT_MODE_LAYER; + } + if (params_spec.cpuparams.n_threads > 0) { result.cpuparams.n_threads = params_spec.cpuparams.n_threads; result.cpuparams_batch.n_threads = params_spec.cpuparams_batch.n_threads; diff --git a/src/llama-context.cpp b/src/llama-context.cpp index c1ef12f56..21501574a 100644 --- a/src/llama-context.cpp +++ b/src/llama-context.cpp @@ -3689,6 +3689,9 @@ llama_context * llama_init_from_model( LLAMA_LOG_ERROR("%s: SPLIT_MODE_TENSOR requires flash_attn to be enabled\n", __func__); return nullptr; } + if (model->get_split_state_ud.n_devices == 1) { + LLAMA_LOG_WARN("%s: SPLIT_MODE_TENSOR being used for a single device is not recommended\n", __func__); + } } if ((model->hparams.is_mla() || model->arch == LLM_ARCH_DEEPSEEK4) && params.type_k != params.type_v) { diff --git a/tools/cli/README.md b/tools/cli/README.md index b667d341d..efe653494 100644 --- a/tools/cli/README.md +++ b/tools/cli/README.md @@ -164,7 +164,7 @@ | `-mmu, --mmproj-url URL` | URL to a multimodal projector file. see tools/mtmd/README.md
(env: LLAMA_ARG_MMPROJ_URL) | | `--mmproj-auto, --no-mmproj, --no-mmproj-auto` | whether to use multimodal projector file (if available), useful when using -hf (default: enabled)
(env: LLAMA_ARG_MMPROJ_AUTO) | | `--mmproj-offload, --no-mmproj-offload` | whether to enable GPU offloading for multimodal projector (default: enabled)
(env: LLAMA_ARG_MMPROJ_OFFLOAD) | -| `-mmdev, --mmproj-device DEVICE` | device to use for multimodal projector (none = don't offload, default: auto)
use --list-devices to see a list of available devices
(env: MTMD_BACKEND_DEVICE) | +| `-mmdev, --mmproj-device DEVICE` | device to use for multimodal projector (none = don't offload, default: follows --device)
use --list-devices to see a list of available devices
(env: MTMD_BACKEND_DEVICE) | | `--image, --audio, --video FILE` | path to an image, audio, or video file. use with multimodal models, use comma-separated values for multiple files | | `--image-min-tokens N` | minimum number of tokens each image can take, only used by vision models with dynamic resolution (default: read from model)
(env: LLAMA_ARG_IMAGE_MIN_TOKENS) | | `--image-max-tokens N` | maximum number of tokens each image can take, only used by vision models with dynamic resolution (default: read from model)
(env: LLAMA_ARG_IMAGE_MAX_TOKENS) | @@ -207,7 +207,7 @@ | `--spec-draft-p-split, --draft-p-split P` | speculative decoding split probability (default: 0.10)
(env: LLAMA_ARG_SPEC_DRAFT_P_SPLIT) | | `--spec-draft-p-min, --draft-p-min P` | minimum speculative decoding probability (greedy) (default: 0.00)
(env: LLAMA_ARG_SPEC_DRAFT_P_MIN) | | `--spec-draft-backend-sampling, --no-spec-draft-backend-sampling` | offload draft sampling to the backend (default: enabled)
(env: LLAMA_ARG_SPEC_DRAFT_BACKEND_SAMPLING) | -| `--spec-draft-device, -devd, --device-draft ` | comma-separated list of devices to use for offloading the draft model (none = don't offload)
use --list-devices to see a list of available devices | +| `--spec-draft-device, -devd, --device-draft ` | comma-separated list of devices to use for offloading the draft model (none = don't offload, default: follows --device)
use --list-devices to see a list of available devices | | `--spec-draft-ngl, -ngld, --gpu-layers-draft, --n-gpu-layers-draft N` | max. number of draft model layers to store in VRAM, either an exact number, 'auto', or 'all' (default: auto)
(env: LLAMA_ARG_N_GPU_LAYERS_DRAFT) | | `--spec-draft-model, -md, --model-draft FNAME` | draft model for speculative decoding (default: unused)
(env: LLAMA_ARG_SPEC_DRAFT_MODEL) | | `--spec-type none,draft-simple,draft-eagle3,draft-mtp,draft-dflash,draft-dspark,ngram-simple,ngram-map-k,ngram-map-k4v,ngram-mod,ngram-cache` | comma-separated list of types of speculative decoding to use (default: none)

(env: LLAMA_ARG_SPEC_TYPE) | diff --git a/tools/server/README.md b/tools/server/README.md index 952d31e75..71ebb9543 100644 --- a/tools/server/README.md +++ b/tools/server/README.md @@ -182,7 +182,7 @@ For the full list of features, please refer to [server's changelog](https://gith | `-mmu, --mmproj-url URL` | URL to a multimodal projector file. see tools/mtmd/README.md
(env: LLAMA_ARG_MMPROJ_URL) | | `--mmproj-auto, --no-mmproj, --no-mmproj-auto` | whether to use multimodal projector file (if available), useful when using -hf (default: enabled)
(env: LLAMA_ARG_MMPROJ_AUTO) | | `--mmproj-offload, --no-mmproj-offload` | whether to enable GPU offloading for multimodal projector (default: enabled)
(env: LLAMA_ARG_MMPROJ_OFFLOAD) | -| `-mmdev, --mmproj-device DEVICE` | device to use for multimodal projector (none = don't offload, default: auto)
use --list-devices to see a list of available devices
(env: MTMD_BACKEND_DEVICE) | +| `-mmdev, --mmproj-device DEVICE` | device to use for multimodal projector (none = don't offload, default: follows --device)
use --list-devices to see a list of available devices
(env: MTMD_BACKEND_DEVICE) | | `--image-min-tokens N` | minimum number of tokens each image can take, only used by vision models with dynamic resolution (default: read from model)
(env: LLAMA_ARG_IMAGE_MIN_TOKENS) | | `--image-max-tokens N` | maximum number of tokens each image can take, only used by vision models with dynamic resolution (default: read from model)
(env: LLAMA_ARG_IMAGE_MAX_TOKENS) | | `--mtmd-batch-max-tokens N` | maximum number of image tokens per batch when encoding images (default: 1024)
(env: LLAMA_ARG_MTMD_BATCH_MAX_TOKENS) | @@ -268,7 +268,7 @@ For the full list of features, please refer to [server's changelog](https://gith | `--spec-draft-p-split, --draft-p-split P` | speculative decoding split probability (default: 0.10)
(env: LLAMA_ARG_SPEC_DRAFT_P_SPLIT) | | `--spec-draft-p-min, --draft-p-min P` | minimum speculative decoding probability (greedy) (default: 0.00)
(env: LLAMA_ARG_SPEC_DRAFT_P_MIN) | | `--spec-draft-backend-sampling, --no-spec-draft-backend-sampling` | offload draft sampling to the backend (default: enabled)
(env: LLAMA_ARG_SPEC_DRAFT_BACKEND_SAMPLING) | -| `--spec-draft-device, -devd, --device-draft ` | comma-separated list of devices to use for offloading the draft model (none = don't offload)
use --list-devices to see a list of available devices | +| `--spec-draft-device, -devd, --device-draft ` | comma-separated list of devices to use for offloading the draft model (none = don't offload, default: follows --device)
use --list-devices to see a list of available devices | | `--spec-draft-ngl, -ngld, --gpu-layers-draft, --n-gpu-layers-draft N` | max. number of draft model layers to store in VRAM, either an exact number, 'auto', or 'all' (default: auto)
(env: LLAMA_ARG_N_GPU_LAYERS_DRAFT) | | `--spec-draft-model, -md, --model-draft FNAME` | draft model for speculative decoding (default: unused)
(env: LLAMA_ARG_SPEC_DRAFT_MODEL) | | `--spec-type none,draft-simple,draft-eagle3,draft-mtp,draft-dflash,draft-dspark,ngram-simple,ngram-map-k,ngram-map-k4v,ngram-mod,ngram-cache` | comma-separated list of types of speculative decoding to use (default: none)

(env: LLAMA_ARG_SPEC_TYPE) | From 88ada91c18cd026388be742838d9f27fc12673bc Mon Sep 17 00:00:00 2001 From: Foad Abo Dahood <32059146+masterFoad@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:54:42 +0300 Subject: [PATCH 07/12] metal : fix idle threads in mul_mv_iq3_xxs for ne00 < 1024 (#28086) * metal : fix half-idle simdgroup in kernel_mul_mv_iq3_xxs_f32 for ne00 < 1024 * metal : keep N_R0_IQ3_XXS = 4, dispatch a separate 8-row split kernel for ne00/32 < 32 The plain kernel is unchanged from master (4 rows per simdgroup, one thread per chunk). The row-split mapping now lives in a separate kernel_mul_mv_iq3_xxs_f32_split instantiation with N_R0_IQ3_XXS_SPLIT = 8, and the host selects it only when ne00/32 < 32 and divides 32, so wide matrices keep the master kernel bit for bit. * metal : select the iq3_xxs row split with a function constant instead of a separate kernel --- ggml/src/ggml-metal/ggml-metal-device.cpp | 24 +++++++++++-- ggml/src/ggml-metal/ggml-metal-impl.h | 1 + ggml/src/ggml-metal/kernels/mul_mv.metal | 44 ++++++++++++++++++----- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/ggml/src/ggml-metal/ggml-metal-device.cpp b/ggml/src/ggml-metal/ggml-metal-device.cpp index c296d17b1..1137c5f6d 100644 --- a/ggml/src/ggml-metal/ggml-metal-device.cpp +++ b/ggml/src/ggml-metal/ggml-metal-device.cpp @@ -839,6 +839,8 @@ ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_mul_mv(ggml_meta const char * suffix = ""; + bool split = false; + // use custom matrix x vector kernel switch (tsrc0) { case GGML_TYPE_F32: @@ -942,6 +944,13 @@ ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_mul_mv(ggml_meta nsg = N_SG_IQ3_XXS; nr0 = N_R0_IQ3_XXS; smem = 256*4+128; + + // split the rows across threads when there are fewer than 32 chunks per row + const int nb32 = ne00/32; + if (nb32 < 32 && (32 % nb32) == 0) { + nr0 = N_R0_IQ3_XXS_SPLIT; + split = true; + } } break; case GGML_TYPE_IQ3_S: { @@ -993,7 +1002,7 @@ ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_mul_mv(ggml_meta const int16_t r3 = (int16_t) (ne13 / ne03); snprintf(base, 256, "kernel_mul_mv_%s_%s%s", ggml_type_name(tsrc0), ggml_type_name(tsrc1), suffix); - snprintf(name, 256, "%s_nsg=%d_ne12=%d_r2=%d_r3=%d", base, nsg, ne12, r2, r3); + snprintf(name, 256, "%s_nsg=%d_ne12=%d_r2=%d_r3=%d_split=%d", base, nsg, ne12, r2, r3, split); ggml_metal_pipeline_with_params res = ggml_metal_library_get_pipeline(lib, name); if (!res.pipeline) { @@ -1003,6 +1012,7 @@ ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_mul_mv(ggml_meta ggml_metal_cv_set_int16(cv, (int16_t) ne12, FC_MUL_MV + 2); ggml_metal_cv_set_int16(cv, r2, FC_MUL_MV + 3); ggml_metal_cv_set_int16(cv, r3, FC_MUL_MV + 4); + ggml_metal_cv_set_bool (cv, split, FC_MUL_MV + 5); res = ggml_metal_library_compile_pipeline(lib, base, name, cv); @@ -1081,6 +1091,8 @@ ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_mul_mv_id(ggml_m const char * suffix = ""; + bool split = false; + // use custom matrix x vector kernel switch (tsrc0) { case GGML_TYPE_F32: @@ -1177,6 +1189,13 @@ ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_mul_mv_id(ggml_m nsg = N_SG_IQ3_XXS; nr0 = N_R0_IQ3_XXS; smem = 256*4+128; + + // split the rows across threads when there are fewer than 32 chunks per row + const int nb32 = ne00/32; + if (nb32 < 32 && (32 % nb32) == 0) { + nr0 = N_R0_IQ3_XXS_SPLIT; + split = true; + } } break; case GGML_TYPE_IQ3_S: { @@ -1224,7 +1243,7 @@ ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_mul_mv_id(ggml_m }; snprintf(base, 256, "kernel_mul_mv_id_%s_%s%s", ggml_type_name(tsrc0), ggml_type_name(tsrc1), suffix); - snprintf(name, 256, "%s_nsg=%d", base, nsg); + snprintf(name, 256, "%s_nsg=%d_split=%d", base, nsg, split); ggml_metal_pipeline_with_params res = ggml_metal_library_get_pipeline(lib, name); if (!res.pipeline) { @@ -1234,6 +1253,7 @@ ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_mul_mv_id(ggml_m ggml_metal_cv_set_int16(cv, 1, FC_MUL_MV + 2); ggml_metal_cv_set_int16(cv, 1, FC_MUL_MV + 3); ggml_metal_cv_set_int16(cv, 1, FC_MUL_MV + 4); + ggml_metal_cv_set_bool (cv, split, FC_MUL_MV + 5); res = ggml_metal_library_compile_pipeline(lib, base, name, cv); diff --git a/ggml/src/ggml-metal/ggml-metal-impl.h b/ggml/src/ggml-metal/ggml-metal-impl.h index 30e40f527..1fe947633 100644 --- a/ggml/src/ggml-metal/ggml-metal-impl.h +++ b/ggml/src/ggml-metal/ggml-metal-impl.h @@ -77,6 +77,7 @@ #define N_R0_IQ3_XXS 4 #define N_SG_IQ3_XXS 2 +#define N_R0_IQ3_XXS_SPLIT 8 #define N_R0_IQ3_S 4 #define N_SG_IQ3_S 2 diff --git a/ggml/src/ggml-metal/kernels/mul_mv.metal b/ggml/src/ggml-metal/kernels/mul_mv.metal index d1800313e..fbe8398ea 100644 --- a/ggml/src/ggml-metal/kernels/mul_mv.metal +++ b/ggml/src/ggml-metal/kernels/mul_mv.metal @@ -213,6 +213,7 @@ constant short FC_mul_mv_nxpsg [[function_constant(FC_MUL_MV + 1)]]; constant short FC_mul_mv_ne12 [[function_constant(FC_MUL_MV + 2)]]; constant short FC_mul_mv_r2 [[function_constant(FC_MUL_MV + 3)]]; constant short FC_mul_mv_r3 [[function_constant(FC_MUL_MV + 4)]]; +constant bool FC_mul_mv_split [[function_constant(FC_MUL_MV + 5)]]; template void mul_vec_q_n_f32_impl( @@ -2092,6 +2093,7 @@ kernel void kernel_mul_mv_iq2_xs_f32( kernel_mul_mv_iq2_xs_f32_impl(args, src0, src1, dst, shmem, tgpig, tiisg, sgitg); } +// FC_mul_mv_split: for nb32 < 32 (nb32 divides 32), 32/nb32 threads share each chunk and each takes a slice of the rows template void kernel_mul_mv_iq3_xxs_f32_impl( args_t args, @@ -2138,11 +2140,18 @@ void kernel_mul_mv_iq3_xxs_f32_impl( threadgroup_barrier(mem_flags::mem_threadgroup); } - const int ix = tiisg; + const short ntx = FC_mul_mv_split ? nb32 : 32; + const short nrep = 32 / ntx; + + const short ix = tiisg % ntx; + const short irep = tiisg / ntx; + + const short row0 = (nr0 * irep ) / nrep; + const short row1 = (nr0 * (irep + 1)) / nrep; device const float * y4 = y + 32 * ix; - for (int ib32 = ix; ib32 < nb32; ib32 += 32) { + for (int ib32 = ix; ib32 < nb32; ib32 += ntx) { for (short i = 0; i < 32; ++i) { yl[i] = y4[i]; } @@ -2151,11 +2160,11 @@ void kernel_mul_mv_iq3_xxs_f32_impl( const int ib = ib32 % (QK_K / 32); device const block_iq3_xxs * xr = x + ibl; - device const uint8_t * q3 = xr->qs + 8 * ib; - device const uint16_t * gas = (device const uint16_t *)(xr->qs + QK_K/4) + 2 * ib; - device const half * dh = &xr->d; + device const uint8_t * q3 = xr->qs + 8 * ib + (uint64_t) row0*args.nb01; + device const uint16_t * gas = (device const uint16_t *)(xr->qs + QK_K/4) + 2 * ib + (uint64_t) row0*args.nb01/2; + device const half * dh = &xr->d + (uint64_t) row0*args.nb01/2; - for (short row = 0; row < nr0; row++) { + for (short row = row0; row < row1; row++) { const float db = dh[0]; const uint32_t aux32 = gas[0] | (gas[1] << 16); const float d = db * (0.5f + (aux32 >> 28)); @@ -2177,7 +2186,7 @@ void kernel_mul_mv_iq3_xxs_f32_impl( gas += args.nb01/2; } - y4 += 32 * 32; + y4 += 32 * ntx; } device float * dst_f32 = (device float *) dst + (uint64_t)im*args.ne0*args.ne1 + (uint64_t)r1*args.ne0; @@ -2190,6 +2199,23 @@ void kernel_mul_mv_iq3_xxs_f32_impl( } } +template +void kernel_mul_mv_iq3_xxs_f32_disp( + args_t args, + device const char * src0, + device const char * src1, + device char * dst, + threadgroup char * shmem, + uint3 tgpig, + ushort tiisg, + ushort sgitg) { + if (FC_mul_mv_split) { + kernel_mul_mv_iq3_xxs_f32_impl(args, src0, src1, dst, shmem, tgpig, tiisg, sgitg); + } else { + kernel_mul_mv_iq3_xxs_f32_impl(args, src0, src1, dst, shmem, tgpig, tiisg, sgitg); + } +} + [[host_name("kernel_mul_mv_iq3_xxs_f32")]] kernel void kernel_mul_mv_iq3_xxs_f32( constant ggml_metal_kargs_mul_mv & args, @@ -2201,7 +2227,7 @@ kernel void kernel_mul_mv_iq3_xxs_f32( ushort tiisg[[thread_index_in_simdgroup]], ushort sgitg[[simdgroup_index_in_threadgroup]]) { - kernel_mul_mv_iq3_xxs_f32_impl(args, src0, src1, dst, shmem, tgpig, tiisg, sgitg); + kernel_mul_mv_iq3_xxs_f32_disp(args, src0, src1, dst, shmem, tgpig, tiisg, sgitg); } template @@ -3217,7 +3243,7 @@ template [[host_name("kernel_mul_mv_id_iq1_s_f32")]] kernel kernel_mul_mv_id_t template [[host_name("kernel_mul_mv_id_iq1_m_f32")]] kernel kernel_mul_mv_id_t kernel_mul_mv_id>>; template [[host_name("kernel_mul_mv_id_iq2_xxs_f32")]] kernel kernel_mul_mv_id_t kernel_mul_mv_id>>; template [[host_name("kernel_mul_mv_id_iq2_xs_f32")]] kernel kernel_mul_mv_id_t kernel_mul_mv_id>>; -template [[host_name("kernel_mul_mv_id_iq3_xxs_f32")]] kernel kernel_mul_mv_id_t kernel_mul_mv_id>>; +template [[host_name("kernel_mul_mv_id_iq3_xxs_f32")]] kernel kernel_mul_mv_id_t kernel_mul_mv_id>>; template [[host_name("kernel_mul_mv_id_iq3_s_f32")]] kernel kernel_mul_mv_id_t kernel_mul_mv_id>>; template [[host_name("kernel_mul_mv_id_iq2_s_f32")]] kernel kernel_mul_mv_id_t kernel_mul_mv_id>>; template [[host_name("kernel_mul_mv_id_iq4_nl_f32")]] kernel kernel_mul_mv_id_t kernel_mul_mv_id>>; From 5d806aa2575e01e126651fd69ab1ab6cefff861d Mon Sep 17 00:00:00 2001 From: Foad Abo Dahood <32059146+masterFoad@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:01:03 +0300 Subject: [PATCH 08/12] server : apply checkpoint min-step eviction only when the checkpoint list is full (#28302) The spacing eviction in create_checkpoint() keeps the oldest checkpoint and erases every later one within checkpoint_min_step of it. For prompts shorter than checkpoint_min_step this drops the checkpoint at n_tokens - 4 that the next request resumes from, so hybrid/recurrent models re-prefill from the previous checkpoint instead. Apply the spacing rule only once the list is at n_ctx_checkpoints, and replace an existing checkpoint at the same n_tokens instead of appending a duplicate. --- tools/server/server-context.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp index f78cfb36d..fe068d3e9 100644 --- a/tools/server/server-context.cpp +++ b/tools/server/server-context.cpp @@ -2311,8 +2311,11 @@ private: // evict checkpoints within min-step of a previous checkpoint, unless they were // created by the current task + // only when the list is full, otherwise short prompts keep just the oldest checkpoint int64_t last = -1; - for (auto it = slot.prompt.checkpoints.begin(); it != slot.prompt.checkpoints.end(); ) { + for (auto it = slot.prompt.checkpoints.begin(); + slot.prompt.checkpoints.size() + 1 >= (size_t) params_base.n_ctx_checkpoints && + it != slot.prompt.checkpoints.end(); ) { if (it->id_task != id_task && last >= 0 && it->n_tokens <= last + params_base.checkpoint_min_step) { SLT_TRC(slot, "erasing context checkpoint too close to an earlier one (pos_min = %d, pos_max = %d, n_tokens = %" PRId64 ", size = %.3f MiB)\n", it->pos_min, it->pos_max, it->n_tokens, (float) it->size() / 1024 / 1024); @@ -2335,6 +2338,19 @@ private: slot.prompt.checkpoints.erase(slot.prompt.checkpoints.begin()); } + // replace an existing checkpoint at the same n_tokens instead of appending a duplicate + { + const int64_t n_tokens_new = slot.prompt.n_tokens() - n_tokens_cur; + for (auto it = slot.prompt.checkpoints.begin(); it != slot.prompt.checkpoints.end(); ) { + if (it->n_tokens == n_tokens_new) { + SLT_TRC(slot, "superseding context checkpoint at n_tokens = %" PRId64 "\n", it->n_tokens); + it = slot.prompt.checkpoints.erase(it); + } else { + ++it; + } + } + } + auto & cur = slot.prompt.checkpoints.emplace_back(); cur.id_task = id_task; From d4389a4dd920d24c9592f1dc3badbd69be23bd09 Mon Sep 17 00:00:00 2001 From: uvos Date: Tue, 8 Sep 2026 16:19:53 +0200 Subject: [PATCH 09/12] Revert "ggml-cuda : restore prop.integrated on HIP builds (#24233)" (#28604) This reverts commit c7d8722922a2599dc4d77f8808d8e6c2fde5e7a2. --- ggml/src/ggml-cuda/ggml-cuda.cu | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index 0e6601f03..38bd4c9a0 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -304,11 +304,7 @@ static ggml_cuda_device_info ggml_cuda_init() { info.default_tensor_split[id] = total_vram; total_vram += device_vram; -#if defined(GGML_USE_HIP) - info.devices[id].integrated = prop.integrated; -#else info.devices[id].integrated = false; // Temporarily disabled due to issues with corrupted output (e.g. #15034) -#endif info.devices[id].nsm = prop.multiProcessorCount; info.devices[id].smpb = prop.sharedMemPerBlock; info.devices[id].warp_size = prop.warpSize; From 9113cc1880763bf590774490f51a661bf22403a4 Mon Sep 17 00:00:00 2001 From: Sarah Wu Date: Tue, 8 Sep 2026 07:40:26 -0700 Subject: [PATCH 10/12] ggml : fix msvc+clang ggml_vld1q_u32 (#28284) --- ggml/src/ggml-cpu/ggml-cpu-impl.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ggml/src/ggml-cpu/ggml-cpu-impl.h b/ggml/src/ggml-cpu/ggml-cpu-impl.h index 5d1ca5ffc..5dd9ec8e6 100644 --- a/ggml/src/ggml-cpu/ggml-cpu-impl.h +++ b/ggml/src/ggml-cpu/ggml-cpu-impl.h @@ -78,7 +78,7 @@ struct ggml_compute_params { #if defined(__ARM_NEON) // ref: https://github.com/ggml-org/llama.cpp/pull/5404 -#ifdef _MSC_VER +#if defined(_MSC_VER) && !defined(__clang__) #define ggml_vld1q_u32(w,x,y,z) { ((w) + ((uint64_t)(x) << 32)), ((y) + ((uint64_t)(z) << 32)) } #else #define ggml_vld1q_u32(w,x,y,z) { (w), (x), (y), (z) } From f3f1a8f2760f28325a5ec20c05b171e5b7c83a29 Mon Sep 17 00:00:00 2001 From: Ruben Ortlam Date: Tue, 8 Sep 2026 18:05:09 +0200 Subject: [PATCH 11/12] llama: disable lazy tensor loading by default on iGPUs (#28326) * llama: add lazy mode auto, fix iGPU regression * revert changes except disabling lazy load on iGPUs in AUTO --- src/llama-model.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/llama-model.cpp b/src/llama-model.cpp index ffedf89e6..0adc07449 100644 --- a/src/llama-model.cpp +++ b/src/llama-model.cpp @@ -1422,6 +1422,18 @@ bool llama_model_base::load_tensors(llama_model_loader & ml) { } } + // resolve AUTO on systems without mmap support (e.g. iGPUs): fall back to OFF; see #28160 + if (ml.lazy.mode == LLAMA_LAZY_MODE_AUTO) { + for (const auto & dev : devices) { + ggml_backend_dev_props props; + ggml_backend_dev_get_props(dev.dev, &props); + if (!props.caps.mmap_support) { + ml.lazy.mode = LLAMA_LAZY_MODE_OFF; + break; + } + } + } + const char * load_mode_name = params.load_mode == LLAMA_LOAD_MODE_AUTO ? llama_load_mode_name(ml.use_mmap ? LLAMA_LOAD_MODE_MMAP : LLAMA_LOAD_MODE_NONE) : llama_load_mode_name(params.load_mode); From 304665fe7ac957df95e3ff8c8c4ffdf92dd6ffa3 Mon Sep 17 00:00:00 2001 From: cwriter Date: Wed, 9 Sep 2026 03:25:41 +0200 Subject: [PATCH 12/12] Add IQ type handling for MoE (#28476) Co-authored-by: cwriter --- ggml/src/ggml-sycl/mmvq.cpp | 73 ++++++++++++++++++++++++++++++++++ ggml/src/ggml-sycl/vecdotq.hpp | 21 ++++++++++ 2 files changed, 94 insertions(+) diff --git a/ggml/src/ggml-sycl/mmvq.cpp b/ggml/src/ggml-sycl/mmvq.cpp index 933bc77d2..32903431b 100644 --- a/ggml/src/ggml-sycl/mmvq.cpp +++ b/ggml/src/ggml-sycl/mmvq.cpp @@ -2671,6 +2671,34 @@ void ggml_sycl_op_mul_mat_vec_q(ggml_backend_sycl_context & ctx, const ggml_tens GGML_UNUSED(ctx); } +// vec_dot_q_sycl_t adapters for the IQ vec_dots that take their codebook tables as extra +// arguments: bind the constant tables here (as vec_dot_iq2_s_q8_1 / vec_dot_iq1_m_q8_1 already do +// internally) so they can be used as template arguments of mul_mat_vec_q_moe. +static __dpct_inline__ float vec_dot_iq2_xxs_q8_1_moe(const void * __restrict__ vbq, + const block_q8_1 * __restrict__ bq8_1, const int & iqs) { + return vec_dot_iq2_xxs_q8_1(vbq, bq8_1, iqs, iq2xxs_grid, ksigns_iq2xs, kmask_iq2xs); +} + +static __dpct_inline__ float vec_dot_iq2_xs_q8_1_moe(const void * __restrict__ vbq, + const block_q8_1 * __restrict__ bq8_1, const int & iqs) { + return vec_dot_iq2_xs_q8_1(vbq, bq8_1, iqs, iq2xs_grid, ksigns64); +} + +static __dpct_inline__ float vec_dot_iq3_xxs_q8_1_moe(const void * __restrict__ vbq, + const block_q8_1 * __restrict__ bq8_1, const int & iqs) { + return vec_dot_iq3_xxs_q8_1(vbq, bq8_1, iqs, iq3xxs_grid, ksigns64); +} + +static __dpct_inline__ float vec_dot_iq3_s_q8_1_moe(const void * __restrict__ vbq, + const block_q8_1 * __restrict__ bq8_1, const int & iqs) { + return vec_dot_iq3_s_q8_1(vbq, bq8_1, iqs, iq3s_grid); +} + +static __dpct_inline__ float vec_dot_iq1_s_q8_1_moe(const void * __restrict__ vbq, + const block_q8_1 * __restrict__ bq8_1, const int & iqs) { + return vec_dot_iq1_s_q8_1(vbq, bq8_1, iqs, iq1s_grid_gpu); +} + // src1_row_stride: 0 for shared src1 (gate/up proj), else per-expert stride (down proj). template static void mul_mat_vec_q_moe( @@ -2822,6 +2850,51 @@ bool ggml_sycl_mul_mat_vec_q_id( vx_base, vy, ids_dev, dst_base, ncols, nrows, n_experts_used, expert_weight_stride, dst_row_stride, src1_row_stride, stream); return true; + case GGML_TYPE_IQ2_XXS: + launch_mul_mat_vec_q_moe( + vx_base, vy, ids_dev, dst_base, ncols, nrows, n_experts_used, + expert_weight_stride, dst_row_stride, src1_row_stride, stream); + return true; + case GGML_TYPE_IQ2_XS: + launch_mul_mat_vec_q_moe( + vx_base, vy, ids_dev, dst_base, ncols, nrows, n_experts_used, + expert_weight_stride, dst_row_stride, src1_row_stride, stream); + return true; + case GGML_TYPE_IQ2_S: + launch_mul_mat_vec_q_moe( + vx_base, vy, ids_dev, dst_base, ncols, nrows, n_experts_used, + expert_weight_stride, dst_row_stride, src1_row_stride, stream); + return true; + case GGML_TYPE_IQ3_XXS: + launch_mul_mat_vec_q_moe( + vx_base, vy, ids_dev, dst_base, ncols, nrows, n_experts_used, + expert_weight_stride, dst_row_stride, src1_row_stride, stream); + return true; + case GGML_TYPE_IQ3_S: + launch_mul_mat_vec_q_moe( + vx_base, vy, ids_dev, dst_base, ncols, nrows, n_experts_used, + expert_weight_stride, dst_row_stride, src1_row_stride, stream); + return true; + case GGML_TYPE_IQ1_S: + launch_mul_mat_vec_q_moe( + vx_base, vy, ids_dev, dst_base, ncols, nrows, n_experts_used, + expert_weight_stride, dst_row_stride, src1_row_stride, stream); + return true; + case GGML_TYPE_IQ1_M: + launch_mul_mat_vec_q_moe( + vx_base, vy, ids_dev, dst_base, ncols, nrows, n_experts_used, + expert_weight_stride, dst_row_stride, src1_row_stride, stream); + return true; + case GGML_TYPE_IQ4_NL: + launch_mul_mat_vec_q_moe( + vx_base, vy, ids_dev, dst_base, ncols, nrows, n_experts_used, + expert_weight_stride, dst_row_stride, src1_row_stride, stream); + return true; + case GGML_TYPE_IQ4_XS: + launch_mul_mat_vec_q_moe( + vx_base, vy, ids_dev, dst_base, ncols, nrows, n_experts_used, + expert_weight_stride, dst_row_stride, src1_row_stride, stream); + return true; default: return false; } diff --git a/ggml/src/ggml-sycl/vecdotq.hpp b/ggml/src/ggml-sycl/vecdotq.hpp index ed5fd7de8..909f7a789 100644 --- a/ggml/src/ggml-sycl/vecdotq.hpp +++ b/ggml/src/ggml-sycl/vecdotq.hpp @@ -1398,6 +1398,11 @@ vec_dot_q6_K_q8_1(const void *__restrict__ vbq, } +// NOTE: the VDR_IQ*_Q8_1_MMVQ values deliberately differ from the identically named CUDA constants +// (vecdotq.cuh): the SYCL kernels pair them with a halved qi (e.g. QI3_S/2), so the values are not +// interchangeable and must not be copied across backends. +#define VDR_IQ2_XXS_Q8_1_MMVQ 1 + static __dpct_inline__ float vec_dot_iq2_xxs_q8_1(const void *__restrict__ vbq, const block_q8_1 *__restrict__ bq8_1, const int &iqs, @@ -1429,6 +1434,8 @@ vec_dot_iq2_xxs_q8_1(const void *__restrict__ vbq, #endif } +#define VDR_IQ2_XS_Q8_1_MMVQ 1 + static __dpct_inline__ float vec_dot_iq2_xs_q8_1(const void *__restrict__ vbq, const block_q8_1 *__restrict__ bq8_1, const int &iqs, @@ -1479,6 +1486,8 @@ vec_dot_iq2_xs_q8_1(const void *__restrict__ vbq, #endif } +#define VDR_IQ2_S_Q8_1_MMVQ 1 + static __dpct_inline__ float vec_dot_iq2_s_q8_1(const void *__restrict__ vbq, const block_q8_1 *__restrict__ bq8_1, const int &iqs) { @@ -1531,6 +1540,8 @@ vec_dot_iq2_s_q8_1(const void *__restrict__ vbq, #endif } +#define VDR_IQ3_XXS_Q8_1_MMVQ 1 + static __dpct_inline__ float vec_dot_iq3_xxs_q8_1(const void *__restrict__ vbq, const block_q8_1 *__restrict__ bq8_1, const int &iqs, @@ -1571,6 +1582,8 @@ vec_dot_iq3_xxs_q8_1(const void *__restrict__ vbq, #endif } +#define VDR_IQ3_S_Q8_1_MMVQ 1 + static __dpct_inline__ float vec_dot_iq3_s_q8_1(const void *__restrict__ vbq, const block_q8_1 *__restrict__ bq8_1, const int &iqs, @@ -1609,6 +1622,8 @@ vec_dot_iq3_s_q8_1(const void *__restrict__ vbq, #endif } +#define VDR_IQ1_S_Q8_1_MMVQ 1 + static __dpct_inline__ float vec_dot_iq1_s_q8_1(const void *__restrict__ vbq, const block_q8_1 *__restrict__ bq8_1, const int &iqs, @@ -1637,6 +1652,8 @@ vec_dot_iq1_s_q8_1(const void *__restrict__ vbq, #endif } +#define VDR_IQ1_M_Q8_1_MMVQ 1 + static __dpct_inline__ float vec_dot_iq1_m_q8_1(const void *__restrict__ vbq, const block_q8_1 *__restrict__ bq8_1, const int &iqs) { @@ -1671,6 +1688,8 @@ vec_dot_iq1_m_q8_1(const void *__restrict__ vbq, } +#define VDR_IQ4_NL_Q8_1_MMVQ 2 + static __dpct_inline__ float vec_dot_iq4_nl_q8_1(const void *__restrict__ vbq, const block_q8_1 *__restrict__ bq8_1, const int &iqs) { @@ -1696,6 +1715,8 @@ vec_dot_iq4_nl_q8_1(const void *__restrict__ vbq, } +#define VDR_IQ4_XS_Q8_1_MMVQ 1 + static __dpct_inline__ float vec_dot_iq4_xs_q8_1(const void *__restrict__ vbq, const block_q8_1 *__restrict__ bq8_1, const int &iqs) {