metal : add top-k radix implementation (#28073)
Assisted-by: DeepSeek-v4-Flash-0731
This commit is contained in:
@@ -5091,7 +5091,9 @@ int ggml_metal_op_argsort(ggml_metal_op_t ctx, int idx) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ggml_metal_op_top_k(ggml_metal_op_t ctx, int idx) {
|
||||
// bitonic-sort + merge fallback: efficient when k is small and there are few rows,
|
||||
// where the single-workgroup-per-row radix-select cannot reach enough parallelism
|
||||
static void ggml_metal_op_top_k_bitonic(ggml_metal_op_t ctx, int idx) {
|
||||
ggml_tensor * op = ctx->node(idx);
|
||||
|
||||
ggml_metal_library_t lib = ctx->lib;
|
||||
@@ -5199,6 +5201,74 @@ int ggml_metal_op_top_k(ggml_metal_op_t ctx, int idx) {
|
||||
|
||||
len <<= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// radix-select: one workgroup per row. Maps each float to an order-preserving unsigned
|
||||
// key, finds the k-th largest via 4 radix-8 histogram passes, then compacts the top-k
|
||||
// indices. Fast for large k and/or many rows.
|
||||
static void ggml_metal_op_top_k_radix(ggml_metal_op_t ctx, int idx) {
|
||||
ggml_tensor * op = ctx->node(idx);
|
||||
|
||||
ggml_metal_library_t lib = ctx->lib;
|
||||
ggml_metal_encoder_t enc = ctx->enc;
|
||||
|
||||
GGML_ASSERT(ggml_is_contiguous_rows(op->src[0]));
|
||||
|
||||
GGML_TENSOR_LOCALS( int32_t, ne0, op->src[0], ne);
|
||||
GGML_TENSOR_LOCALS(uint64_t, nb0, op->src[0], nb);
|
||||
|
||||
auto pipeline = ggml_metal_library_get_pipeline_top_k_radix(lib, op);
|
||||
|
||||
// one workgroup per row; radix-select the k-th largest value
|
||||
const int nth = std::min(1024, ggml_metal_pipeline_max_theads_per_threadgroup(pipeline));
|
||||
|
||||
ggml_metal_kargs_top_k args = {
|
||||
/*.ne00 =*/ ne00,
|
||||
/*.ne01 =*/ ne01,
|
||||
/*.ne02 =*/ ne02,
|
||||
/*.ne03 =*/ ne03,
|
||||
/*.nb01 =*/ nb01,
|
||||
/*.nb02 =*/ nb02,
|
||||
/*.nb03 =*/ nb03,
|
||||
/*.top_k =*/ (int32_t) op->ne[0],
|
||||
};
|
||||
|
||||
// shared memory: 256-entry histogram + bucket/above scalars + output counter
|
||||
const size_t smem_histo = GGML_PAD(256*sizeof(uint32_t), 16);
|
||||
const size_t smem_bucket = GGML_PAD( sizeof(uint32_t), 16);
|
||||
const size_t smem_above = GGML_PAD( sizeof(uint32_t), 16);
|
||||
const size_t smem_out = GGML_PAD( sizeof(uint32_t), 16);
|
||||
|
||||
ggml_metal_encoder_set_pipeline(enc, pipeline);
|
||||
ggml_metal_encoder_set_bytes (enc, &args, sizeof(args), 0);
|
||||
ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op->src[0]), 1);
|
||||
ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op), 2);
|
||||
|
||||
ggml_metal_encoder_set_threadgroup_memory_size(enc, smem_histo, 0);
|
||||
ggml_metal_encoder_set_threadgroup_memory_size(enc, smem_bucket, 1);
|
||||
ggml_metal_encoder_set_threadgroup_memory_size(enc, smem_above, 2);
|
||||
ggml_metal_encoder_set_threadgroup_memory_size(enc, smem_out, 3);
|
||||
|
||||
ggml_metal_encoder_dispatch_threadgroups(enc, ne01, ne02, ne03, nth, 1, 1);
|
||||
}
|
||||
|
||||
int ggml_metal_op_top_k(ggml_metal_op_t ctx, int idx) {
|
||||
ggml_tensor * op = ctx->node(idx);
|
||||
|
||||
// radix-select has a fixed single-workgroup-per-row cost (~50-60us) that is only
|
||||
// amortized for long rows, many rows, or a large k; otherwise the bitonic path wins
|
||||
const int ncols = op->src[0]->ne[0];
|
||||
const int k = op->ne[0];
|
||||
const int nrows = ggml_nrows(op->src[0]);
|
||||
|
||||
const bool use_radix =
|
||||
ncols > 2048 && (k > 64 || (nrows > 4 && ncols >= 8192));
|
||||
|
||||
if (use_radix) {
|
||||
ggml_metal_op_top_k_radix(ctx, idx);
|
||||
} else {
|
||||
ggml_metal_op_top_k_bitonic(ctx, idx);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user