@@ -657,6 +657,21 @@ static constexpr std::initializer_list<ggml_op> snake_pattern { GGM
657657 GGML_OP_SQR, GGML_OP_MUL,
658658 GGML_OP_ADD };
659659
660+ // qwen4 QSA indexer: gather per-block scores to cells + add f16 mask (cast+reshape) + top-k,
661+ // fused into one radix-select. The cast/reshape are elided; the raw f16 mask is read in-shader.
662+ static constexpr std::initializer_list<ggml_op> topk_qsa_pattern { GGML_OP_GET_ROWS, GGML_OP_PERMUTE,
663+ GGML_OP_CONT, GGML_OP_CPY,
664+ GGML_OP_RESHAPE, GGML_OP_ADD,
665+ GGML_OP_TOP_K };
666+ static constexpr std::initializer_list<std::array<int, 3>> topk_qsa_edges {
667+ { 1, 0, 0 }, // permute->src[0] == get_rows
668+ { 2, 0, 1 }, // cont->src[0] == permute
669+ { 4, 0, 3 }, // reshape->src[0] == cpy (mask cast)
670+ { 5, 0, 2 }, // add->src[0] == cont
671+ { 5, 1, 4 }, // add->src[1] == reshape
672+ { 6, 0, 5 }, // top_k->src[0] == add
673+ };
674+
660675//node #978 ( SOFT_MAX): ffn_moe_probs-15 ( 0K) [Vulka ] use=2: ffn_moe_logits-15 ( 0K) [Vulka ]
661676//node #979 ( RESHAPE): ffn_moe_probs-15 (re ( 0K) [Vulka ] use=1: ffn_moe_probs-15 ( 0K) [Vulka ]
662677//node #980 ( ARGSORT): ffn_moe_argsort-15 ( 0K) [Vulka ] use=1: ffn_moe_probs-15 ( 0K) [Vulka ]
@@ -1057,6 +1072,8 @@ struct vk_device_struct {
10571072 vk_pipeline pipeline_argsort_f32[num_argsort_pipelines];
10581073 vk_pipeline pipeline_argsort_large_f32[num_argsort_pipelines];
10591074 vk_pipeline pipeline_topk_f32[num_topk_pipelines];
1075+ vk_pipeline pipeline_topk_radix_f32;
1076+ vk_pipeline pipeline_topk_radix_qsa; // qwen4 QSA indexer fusion (f16 mask)
10601077 vk_pipeline pipeline_sum_rows_f32;
10611078 vk_pipeline pipeline_cross_entropy_loss_f32, pipeline_cross_entropy_loss_f32_wg512;
10621079 vk_pipeline pipeline_cross_entropy_loss_back_f32, pipeline_cross_entropy_loss_back_f32_wg512;
@@ -1749,6 +1766,15 @@ struct vk_op_topk_push_constants {
17491766 uint32_t last_pass;
17501767};
17511768
1769+ struct vk_op_topk_radix_push_constants {
1770+ uint32_t ncols;
1771+ uint32_t k;
1772+ uint32_t nrows;
1773+ uint32_t n_tps; // QSA only
1774+ uint32_t n_blocks; // QSA only
1775+ uint32_t n_stream; // QSA only
1776+ };
1777+
17521778struct vk_op_im2col_push_constants {
17531779 uint64_t dst_addr;
17541780 uint32_t batch_offset; uint32_t offset_delta;
@@ -2439,6 +2465,8 @@ struct ggml_backend_vk_context {
24392465 int fused_ops_write_mask {};
24402466 topk_moe_mode fused_topk_moe_mode {};
24412467 bool fused_topk_moe_scale {};
2468+ // QSA indexer gather+add+top_k fused into one radix-select
2469+ bool fused_topk_qsa {};
24422470
24432471 // for GGML_VK_PERF_LOGGER
24442472 std::unique_ptr<vk_perf_logger> perf_logger;
@@ -5814,6 +5842,14 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
58145842 }
58155843 }
58165844
5845+ // large-k fallback: one workgroup per row, radix-select instead of a full sort. The QSA
5846+ // variant (spec constant 1) additionally gathers the qwen4 indexer input on the fly.
5847+ {
5848+ const uint32_t BLOCK_SIZE = 1u << std::min(10u, device->max_workgroup_size_log2);
5849+ ggml_vk_create_pipeline2(device, device->pipeline_topk_radix_f32, "topk_radix_f32", topk_radix_select_f32_len, topk_radix_select_f32_data, "main", 5, sizeof(vk_op_topk_radix_push_constants), {BLOCK_SIZE, 1, 1}, {BLOCK_SIZE, 0}, 1, true);
5850+ ggml_vk_create_pipeline2(device, device->pipeline_topk_radix_qsa, "topk_radix_qsa", topk_radix_select_f32_len, topk_radix_select_f32_data, "main", 5, sizeof(vk_op_topk_radix_push_constants), {BLOCK_SIZE, 1, 1}, {BLOCK_SIZE, 1}, 1, true);
5851+ }
5852+
58175853 ggml_vk_create_pipeline(device, device->pipeline_argmax_f32, "argmax_f32", argmax_f32_len, argmax_f32_data, "main", 2, sizeof(vk_op_push_constants), {1, 1, 1}, { device->subgroup_size }, 1);
58185854
58195855 ggml_vk_create_pipeline(device, device->pipeline_sum_rows_f32, "sum_rows_f32", sum_rows_f32_len, sum_rows_f32_data, "main", 2, sizeof(vk_op_sum_rows_push_constants), {1, 1, 1}, { device->subgroup_size }, 1);
@@ -13940,6 +13976,31 @@ static void ggml_vk_topk(ggml_backend_vk_context * ctx, vk_context& subctx, cons
1394013976 uint32_t nrows = ggml_nrows(src0);
1394113977 uint32_t k = dst->ne[0];
1394213978
13979+ // tournament path is faster where it fits; use radix-select only past its k limit
13980+ const uint32_t k_min_pipeline = std::max((uint32_t) log2f(float(k)) + 1, ctx->device->subgroup_size_log2);
13981+ if (k_min_pipeline >= num_topk_pipelines || ctx->device->pipeline_topk_f32[k_min_pipeline] == nullptr) {
13982+ vk_pipeline pipeline = ctx->device->pipeline_topk_radix_f32;
13983+ GGML_ASSERT(pipeline != nullptr);
13984+
13985+ if (ctx->prealloc_x_need_sync) {
13986+ ggml_vk_sync_buffers(ctx, subctx);
13987+ }
13988+
13989+ vk_op_topk_radix_push_constants pc { ncols, k, nrows, 0, 0, 0 };
13990+ std::array<uint32_t, 3> elements {
13991+ pipeline->wg_denoms[0],
13992+ std::min(nrows, ctx->device->properties.limits.maxComputeWorkGroupCount[1]),
13993+ 1,
13994+ };
13995+ // the non-QSA path only uses bindings 0/1; bind valid buffers for the unused QSA slots
13996+ vk_subbuffer src0_buf = ggml_vk_tensor_subbuffer(ctx, src0);
13997+ vk_subbuffer dst_buf = ggml_vk_tensor_subbuffer(ctx, dst);
13998+ ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1);
13999+ ggml_vk_dispatch_pipeline(ctx, subctx, pipeline,
14000+ { src0_buf, dst_buf, src0_buf, src0_buf, src0_buf }, pc, elements);
14001+ return;
14002+ }
14003+
1394314004 vk_op_topk_push_constants pc { ncols, ncols, ncols, k, nrows, 0, 0 };
1394414005
1394514006 if (ctx->prealloc_x_need_sync) {
@@ -14043,6 +14104,55 @@ static void ggml_vk_topk(ggml_backend_vk_context * ctx, vk_context& subctx, cons
1404314104 ctx->prealloc_x_need_sync = true;
1404414105}
1404514106
14107+ static void ggml_vk_topk_qsa(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_cgraph * cgraph, int node_idx) {
14108+ const ggml_tensor * get_rows = cgraph->nodes[node_idx + 0];
14109+ const ggml_tensor * add = cgraph->nodes[node_idx + ctx->num_additional_fused_ops - 1];
14110+ ggml_tensor * top_k = cgraph->nodes[node_idx + ctx->num_additional_fused_ops];
14111+
14112+ const ggml_tensor * scores = get_rows->src[0]; // [n_tps, n_blocks, n_stream]
14113+ const ggml_tensor * cell_blk = get_rows->src[1]; // [n_kv, n_stream]
14114+
14115+ // raw f16 mask: follow the reshape/cpy chain back to the materialized input
14116+ const ggml_tensor * mask = add->src[1];
14117+ while (mask->op == GGML_OP_RESHAPE || mask->op == GGML_OP_CPY) {
14118+ mask = mask->src[0];
14119+ }
14120+
14121+ const uint32_t n_tps = scores->ne[0];
14122+ const uint32_t n_blocks = scores->ne[1];
14123+ const uint32_t n_stream = scores->ne[2];
14124+ const uint32_t n_kv = cell_blk->ne[0];
14125+ const uint32_t width = top_k->ne[0];
14126+ const uint32_t nrows = n_tps * n_stream;
14127+
14128+ vk_pipeline pipeline = ctx->device->pipeline_topk_radix_qsa;
14129+ GGML_ASSERT(pipeline != nullptr);
14130+
14131+ // scratch holds the gathered+masked input, materialized once and reused across passes
14132+ const size_t scratch_size = size_t{ n_kv } * nrows * sizeof(float);
14133+ if (ctx->prealloc_size_x < scratch_size) {
14134+ ctx->prealloc_size_x = scratch_size;
14135+ ggml_vk_preallocate_buffers(ctx, subctx);
14136+ }
14137+ if (ctx->prealloc_x_need_sync) {
14138+ ggml_vk_sync_buffers(ctx, subctx);
14139+ }
14140+
14141+ vk_op_topk_radix_push_constants pc { n_kv, width, nrows, n_tps, n_blocks, n_stream };
14142+ std::array<uint32_t, 3> elements {
14143+ pipeline->wg_denoms[0],
14144+ std::min(nrows, ctx->device->properties.limits.maxComputeWorkGroupCount[1]),
14145+ 1,
14146+ };
14147+ vk_subbuffer scratch_buf { ctx->prealloc_x, 0, ctx->prealloc_x->size };
14148+ ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1);
14149+ ggml_vk_dispatch_pipeline(ctx, subctx, pipeline,
14150+ { ggml_vk_tensor_subbuffer(ctx, scores), ggml_vk_tensor_subbuffer(ctx, top_k),
14151+ ggml_vk_tensor_subbuffer(ctx, cell_blk), ggml_vk_tensor_subbuffer(ctx, mask),
14152+ scratch_buf }, pc, elements);
14153+ ctx->prealloc_x_need_sync = true;
14154+ }
14155+
1404614156static void ggml_vk_sum(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * src0, ggml_tensor * dst) {
1404714157 vk_op_sum_rows_push_constants p = vk_op_sum_rows_push_constants_init(src0, dst, ggml_nelements(src0));
1404814158 ggml_vk_op_f32(ctx, subctx, src0, nullptr, nullptr, nullptr, dst, GGML_OP_SUM, p);
@@ -15704,7 +15814,11 @@ static bool ggml_vk_build_graph(ggml_backend_vk_context * ctx, ggml_cgraph * cgr
1570415814
1570515815 break;
1570615816 case GGML_OP_GET_ROWS:
15707- ggml_vk_get_rows(ctx, compute_ctx, src0, src1, node);
15817+ if (ctx->fused_topk_qsa) {
15818+ ggml_vk_topk_qsa(ctx, compute_ctx, cgraph, node_idx);
15819+ } else {
15820+ ggml_vk_get_rows(ctx, compute_ctx, src0, src1, node);
15821+ }
1570815822
1570915823 break;
1571015824 case GGML_OP_GET_ROWS_BACK:
@@ -17116,6 +17230,92 @@ static bool ggml_vk_can_fuse_topk_moe(ggml_backend_vk_context * ctx, const struc
1711617230 return true;
1711717231}
1711817232
17233+ // Manual op-sequence match (ggml_can_fuse_subgraph rejects the mask's external reshape/cpy).
17234+ static bool ggml_vk_match_ops(const struct ggml_cgraph * cgraph, int node_idx,
17235+ const std::initializer_list<ggml_op> & ops) {
17236+ if (node_idx + (int) ops.size() > cgraph->n_nodes) {
17237+ return false;
17238+ }
17239+ for (size_t j = 0; j < ops.size(); ++j) {
17240+ const ggml_tensor * node = cgraph->nodes[node_idx + j];
17241+ if (node->op != ops.begin()[j] ||
17242+ (node->flags & GGML_TENSOR_FLAG_COMPUTE) == 0 ||
17243+ (node->flags & GGML_TENSOR_FLAG_OUTPUT) != 0) {
17244+ return false;
17245+ }
17246+ }
17247+ return true;
17248+ }
17249+
17250+ // True if the qwen4 QSA indexer top-k can be fused at node_idx (the get_rows).
17251+ static bool ggml_vk_can_fuse_topk_qsa(ggml_backend_vk_context * ctx, const struct ggml_cgraph * cgraph, int node_idx) {
17252+ if (ctx->device->disable_fusion || !ctx->device->pipeline_topk_radix_qsa) {
17253+ return false;
17254+ }
17255+
17256+ const int n_ops = topk_qsa_pattern.size();
17257+ if (!ggml_vk_match_ops(cgraph, node_idx, topk_qsa_pattern) ||
17258+ !ggml_check_edges(cgraph, node_idx, topk_qsa_edges)) {
17259+ return false;
17260+ }
17261+
17262+ // elided nodes must be single-use (cpy counts its own src[1] self-reference)
17263+ for (int j = 0; j < n_ops - 1; ++j) {
17264+ const ggml_tensor * node = cgraph->nodes[node_idx + j];
17265+ const int32_t want = node->op == GGML_OP_CPY ? 2 : 1;
17266+ if (ggml_node_get_use_count(cgraph, node_idx + j) != want) {
17267+ return false;
17268+ }
17269+ }
17270+
17271+ const ggml_tensor * get_rows = cgraph->nodes[node_idx + 0];
17272+ const ggml_tensor * add = cgraph->nodes[node_idx + n_ops - 2];
17273+ const ggml_tensor * top_k = cgraph->nodes[node_idx + n_ops - 1];
17274+
17275+ const ggml_tensor * scores = get_rows->src[0]; // [n_tps, n_blocks, n_stream]
17276+ const ggml_tensor * cell_blk = get_rows->src[1]; // [n_kv, n_stream]
17277+ const ggml_tensor * expanded = add->src[0]; // [n_kv, n_tps, n_stream]
17278+
17279+ // raw mask: follow the reshape/cpy chain back to the materialized f16 input
17280+ const ggml_tensor * mask = add->src[1];
17281+ while (mask && (mask->op == GGML_OP_RESHAPE || mask->op == GGML_OP_CPY)) {
17282+ mask = mask->src[0];
17283+ }
17284+ if (!mask || mask->type != GGML_TYPE_F16) {
17285+ return false;
17286+ }
17287+
17288+ if (scores->type != GGML_TYPE_F32 || cell_blk->type != GGML_TYPE_I32 || top_k->type != GGML_TYPE_I32) {
17289+ return false;
17290+ }
17291+ if (!ggml_is_contiguous(scores) || !ggml_is_contiguous(cell_blk) || !ggml_is_contiguous(mask) ||
17292+ !ggml_is_contiguous(expanded) || !ggml_is_contiguous(top_k)) {
17293+ return false;
17294+ }
17295+
17296+ const int64_t n_tps = scores->ne[0];
17297+ const int64_t n_blocks = scores->ne[1];
17298+ const int64_t n_stream = scores->ne[2];
17299+ const int64_t n_kv = cell_blk->ne[0];
17300+ const int64_t width = top_k->ne[0];
17301+
17302+ // pin the indexer layout the shader's addressing assumes
17303+ if (scores->ne[3] != 1 || cell_blk->ne[1] != n_stream || ggml_nrows(cell_blk) != n_stream ||
17304+ ggml_nelements(mask) != n_kv * n_tps * n_stream ||
17305+ expanded->ne[0] != n_kv || expanded->ne[1] != n_tps || expanded->ne[2] != n_stream ||
17306+ top_k->ne[1] != n_tps || top_k->ne[2] != n_stream || top_k->ne[3] != 1 ||
17307+ n_blocks <= 0 || n_kv <= 0 || width <= 0 || width > n_kv) {
17308+ return false;
17309+ }
17310+
17311+ // only worth it in the radix regime; small k uses the faster tournament unfused
17312+ const uint32_t k_min_pipeline = std::max((uint32_t) log2f(float(width)) + 1, ctx->device->subgroup_size_log2);
17313+ if (k_min_pipeline < num_topk_pipelines && ctx->device->pipeline_topk_f32[k_min_pipeline]) {
17314+ return false;
17315+ }
17316+ return true;
17317+ }
17318+
1711917319static bool ggml_vk_can_fuse_rope_set_rows(ggml_backend_vk_context * ctx, const struct ggml_cgraph * cgraph,
1712017320 int node_idx) {
1712117321 GGML_UNUSED(ctx);
@@ -17495,6 +17695,7 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg
1749517695
1749617696 ctx->fused_topk_moe_mode = TOPK_MOE_COUNT;
1749717697 ctx->fused_topk_moe_scale = false;
17698+ ctx->fused_topk_qsa = false;
1749817699 const char *fusion_string {};
1749917700 if (!ctx->device->disable_fusion) {
1750017701 uint32_t num_adds = ggml_vk_fuse_multi_add(ctx, cgraph, i);
@@ -17584,6 +17785,11 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg
1758417785 // with a data dependency on that register. The overlap check still
1758517786 // rejects partial overlaps (different base or size).
1758617787 std::fill_n(op_srcs_fused_elementwise, 5, true);
17788+ } else if (ggml_vk_can_fuse_topk_qsa(ctx, cgraph, i)) {
17789+ ctx->num_additional_fused_ops = topk_qsa_pattern.size() - 1;
17790+ ctx->fused_topk_qsa = true;
17791+ fusion_string = "TOPK_QSA";
17792+ std::fill_n(op_srcs_fused_elementwise, ctx->num_additional_fused_ops + 1, false);
1758717793 } else if (ggml_can_fuse_subgraph(cgraph, i, topk_moe_early_softmax_norm, { i + 3, i + 9 }) &&
1758817794 ggml_check_edges(cgraph, i, topk_moe_early_softmax_norm_edges) &&
1758917795 ggml_vk_can_fuse_topk_moe(ctx, cgraph, i, TOPK_MOE_EARLY_SOFTMAX_NORM)) {
@@ -17700,6 +17906,7 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg
1770017906 ctx->fused_ops_write_mask = 1;
1770117907 ctx->fused_topk_moe_mode = TOPK_MOE_COUNT;
1770217908 ctx->fused_topk_moe_scale = false;
17909+ ctx->fused_topk_qsa = false;
1770317910 }
1770417911 }
1770517912
@@ -17896,6 +18103,9 @@ static void ggml_vk_graph_optimize(ggml_backend_t backend, struct ggml_cgraph *
1789618103 if (keep_pattern(snake_pattern)) {
1789718104 continue;
1789818105 }
18106+ if (keep_pattern(topk_qsa_pattern)) {
18107+ continue;
18108+ }
1789918109
1790018110 // First, grab the next unused node.
1790118111 current_set.push_back(first_unused);
@@ -17914,13 +18124,23 @@ static void ggml_vk_graph_optimize(ggml_backend_t backend, struct ggml_cgraph *
1791418124 if (is_empty(graph->nodes[j])) {
1791518125 continue;
1791618126 }
17917- // Don't pull forward nodes from fusion patterns
18127+ // Protect every interior QSA node (not just the start): the mask branch is
18128+ // independent, so it gets pulled out and breaks keep_pattern otherwise.
18129+ auto const &in_qsa_pattern = [&](int n) -> bool {
18130+ for (int o = 0; o < (int) topk_qsa_pattern.size(); ++o) {
18131+ if (n - o >= 0 && match_pattern(topk_qsa_pattern, n - o)) {
18132+ return true;
18133+ }
18134+ }
18135+ return false;
18136+ };
1791818137 if (match_pattern(topk_moe_early_softmax_norm, j) ||
1791918138 match_pattern(topk_moe_sigmoid_norm_bias, j) ||
1792018139 match_pattern(topk_moe_sqrt_softplus_norm_bias, j) ||
1792118140 match_pattern(topk_moe_early_softmax, j) ||
1792218141 match_pattern(topk_moe_late_softmax, j) ||
17923- match_pattern(snake_pattern, j)) {
18142+ match_pattern(snake_pattern, j) ||
18143+ in_qsa_pattern(j)) {
1792418144 continue;
1792518145 }
1792618146 bool ok = true;
@@ -18723,15 +18943,14 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm
1872318943 if (!ggml_is_contiguous(op) || !ggml_is_contiguous(op->src[0])) {
1872418944 return false;
1872518945 }
18726- // We could potentially support larger, using argsort to sort the
18727- // whole thing. Not clear if this is needed.
18728- uint32_t min_pipeline = (uint32_t)log2f(float(op->ne[0])) + 1;
18729- if (min_pipeline >= num_topk_pipelines ||
18730- !device->pipeline_topk_f32[min_pipeline]) {
18731- return false;
18946+ // large k falls back to radix-select
18947+ const uint32_t min_pipeline =
18948+ std::max((uint32_t) log2f(float(op->ne[0])) + 1, device->subgroup_size_log2);
18949+ if (min_pipeline < num_topk_pipelines && device->pipeline_topk_f32[min_pipeline]) {
18950+ return true;
1873218951 }
18952+ return device->pipeline_topk_radix_f32 != nullptr;
1873318953 }
18734- return true;
1873518954 case GGML_OP_UPSCALE:
1873618955 if (op->op_params[0] & GGML_SCALE_FLAG_ANTIALIAS) {
1873718956 if ((op->op_params[0] & 0xFF) != GGML_SCALE_MODE_BILINEAR) {
0 commit comments