Skip to content

Commit daef7b6

Browse files
authored
vulkan: top_k radix select for k >= 1024 for Qwen 3.8 Flash Next (ggml-org#28032)
* vulkan: add top-k radix sort shader for k >= 1024 * add Qwen 3.8 Flash Next top-k tests * add top-k qsa fusion * clean up code
1 parent 9723942 commit daef7b6

4 files changed

Lines changed: 471 additions & 10 deletions

File tree

ggml/src/ggml-vulkan/ggml-vulkan.cpp

Lines changed: 229 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
17521778
struct 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+
1404614156
static 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+
1711917319
static 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

Comments
 (0)