Select DeepSeek V4.1 index candidates with a radix select on Metal

This commit is contained in:
Tarjei Mandt committed 2026-09-18 23:32:47 +10:00
1 parent 4b1b1519ed
commit af7c02b594
3 files changed
+433 -1

No files matched your search

+95
View File
@@ -680,6 +680,9 @@ static id<MTLBuffer> g_router_selection_buffer;
static id<MTLBuffer> g_router_weight_sum_buffer;
static id<MTLBuffer> g_indexer_head_scores_buffer;
static id<MTLBuffer> g_indexer_topk_buffer;
static id<MTLBuffer> g_topk_select_buffer;
static NSUInteger g_topk_select_bytes;
static uint32_t g_topk_select_parity;
static id<MTLBuffer> g_indexed_topk_buffer;
static id<MTLBuffer> g_f16_round_scratch_buffer;
static id<MTLBuffer> g_q8_prefill_scratch_buffer;
@@ -6408,6 +6411,15 @@ typedef struct {
uint32_t block_top_k;
} ds4_gpu_kargs_argsort_merge;
typedef struct {
uint32_t n;
uint32_t rows;
uint32_t top_k;
uint32_t pass;
uint32_t parity;
uint32_t n_tg;
} ds4_gpu_kargs_topk_select;
typedef struct {
int64_t ne00;
int64_t ne01;
@@ -19151,6 +19163,84 @@ int ds4_gpu_indexer_scores_decode_batch_tensor(
scale);
}
/* Radix select, deterministic compaction and one in-place sort of the winners
* (kernel_topk_select_*): every dispatch spans the GPU, where the merge ladder
* ends on two or three threadgroups. Returns 0 without encoding anything when
* the pipelines are unavailable, so the caller keeps the ladder. */
static int ds4_gpu_indexer_topk_select(id<MTLBuffer> selbuf, NSUInteger sel_off,
id<MTLBuffer> scorebuf, NSUInteger score_off,
uint32_t n, uint32_t rows, uint32_t top_k) {
id<MTLComputePipelineState> hist = ds4_gpu_get_pipeline("kernel_topk_select_hist");
id<MTLComputePipelineState> count = ds4_gpu_get_pipeline("kernel_topk_select_count");
id<MTLComputePipelineState> write = ds4_gpu_get_pipeline("kernel_topk_select_write");
id<MTLComputePipelineState> sort = ds4_gpu_get_pipeline("kernel_topk_select_sort");
id<MTLComputePipelineState> zero = ds4_gpu_get_pipeline("kernel_topk_select_zero");
if (!hist || !count || !write || !sort || !zero ||
hist.maxTotalThreadsPerThreadgroup < 1024u || count.maxTotalThreadsPerThreadgroup < 1024u ||
write.maxTotalThreadsPerThreadgroup < 1024u || sort.maxTotalThreadsPerThreadgroup < 1024u) return 0;
const uint32_t n_tg = (n + 2047u) / 2048u;
/* sized for the widest row (2^23 keys), so the scratch is allocated once per row count */
const NSUInteger state_off = (NSUInteger)rows * 4u * 2048u * sizeof(uint32_t);
const NSUInteger counts_off = state_off + (NSUInteger)rows * 4u * sizeof(uint32_t);
const NSUInteger bytes = counts_off + (NSUInteger)rows * 4096u * 2u * sizeof(uint32_t);
id<MTLBuffer> before = g_topk_select_buffer;
if (!ds4_gpu_ensure_scratch_buffer(&g_topk_select_buffer, &g_topk_select_bytes, bytes,
"ds4_topk_select")) return 0;
ds4_gpu_kargs_topk_select args = {n, rows, top_k, 0, g_topk_select_parity, n_tg};
g_topk_select_parity ^= 1u;
int owned = 0;
id<MTLCommandBuffer> cb = ds4_gpu_command_buffer(&owned);
if (!cb) return 0;
id<MTLComputeCommandEncoder> enc;
if (g_topk_select_buffer != before) { /* a fresh scratch starts with empty histograms */
const uint32_t words = (uint32_t)(g_topk_select_bytes / sizeof(uint32_t));
enc = ds4_gpu_compute_encoder(cb);
[enc setComputePipelineState:zero];
[enc setBuffer:g_topk_select_buffer offset:0 atIndex:0];
[enc setBytes:&words length:sizeof(words) atIndex:1];
[enc dispatchThreadgroups:MTLSizeMake((words + 255u) / 256u, 1, 1)
threadsPerThreadgroup:MTLSizeMake(256, 1, 1)];
ds4_gpu_end_compute_encoder(cb, enc);
}
for (args.pass = 0; args.pass < 3u; args.pass++) {
enc = ds4_gpu_compute_encoder(cb);
[enc setComputePipelineState:hist];
[enc setBytes:&args length:sizeof(args) atIndex:0];
[enc setBuffer:scorebuf offset:score_off atIndex:1];
[enc setBuffer:g_topk_select_buffer offset:0 atIndex:2];
[enc setThreadgroupMemoryLength:2048u * sizeof(uint32_t) atIndex:0];
[enc dispatchThreadgroups:MTLSizeMake(n_tg, rows, 1) threadsPerThreadgroup:MTLSizeMake(1024, 1, 1)];
ds4_gpu_end_compute_encoder(cb, enc);
}
enc = ds4_gpu_compute_encoder(cb);
[enc setComputePipelineState:count];
[enc setBytes:&args length:sizeof(args) atIndex:0];
[enc setBuffer:scorebuf offset:score_off atIndex:1];
[enc setBuffer:g_topk_select_buffer offset:0 atIndex:2];
[enc setBuffer:g_topk_select_buffer offset:state_off atIndex:3];
[enc setBuffer:g_topk_select_buffer offset:counts_off atIndex:4];
[enc dispatchThreadgroups:MTLSizeMake(n_tg, rows, 1) threadsPerThreadgroup:MTLSizeMake(1024, 1, 1)];
ds4_gpu_end_compute_encoder(cb, enc);
enc = ds4_gpu_compute_encoder(cb);
[enc setComputePipelineState:write];
[enc setBytes:&args length:sizeof(args) atIndex:0];
[enc setBuffer:scorebuf offset:score_off atIndex:1];
[enc setBuffer:g_topk_select_buffer offset:state_off atIndex:2];
[enc setBuffer:g_topk_select_buffer offset:counts_off atIndex:3];
[enc setBuffer:selbuf offset:sel_off atIndex:4];
[enc dispatchThreadgroups:MTLSizeMake(n_tg, rows, 1) threadsPerThreadgroup:MTLSizeMake(1024, 1, 1)];
ds4_gpu_end_compute_encoder(cb, enc);
enc = ds4_gpu_compute_encoder(cb);
[enc setComputePipelineState:sort];
[enc setBytes:&args length:sizeof(args) atIndex:0];
[enc setBuffer:scorebuf offset:score_off atIndex:1];
[enc setBuffer:selbuf offset:sel_off atIndex:2];
[enc setThreadgroupMemoryLength:2u * 2048u * sizeof(uint32_t) atIndex:0];
[enc dispatchThreadgroups:MTLSizeMake(rows, 1, 1) threadsPerThreadgroup:MTLSizeMake(1024, 1, 1)];
ds4_gpu_end_compute_encoder(cb, enc);
return ds4_gpu_finish_command_buffer(cb, owned, "indexer top-k select") ? 1 : 0;
}
static int ds4_gpu_indexer_topk_tensor_impl(
ds4_gpu_tensor *selected,
const ds4_gpu_tensor *scores,
@@ -19182,6 +19272,11 @@ static int ds4_gpu_indexer_topk_tensor_impl(
fprintf(stderr, "ds4: Metal graph indexer top-k received undersized buffers\n");
return 0;
}
/* wide rows: the merge ladder's last rounds run on two or three threadgroups */
if (!causal_ratio && top_k <= 2048u && n_comp >= 16384u && n_comp <= (1u << 23) &&
!getenv("DS4_METAL_DISABLE_V41_TOPK_SELECT") &&
ds4_gpu_indexer_topk_select(selbuf, ds4_gpu_tensor_offset(selected), scorebuf,
ds4_gpu_tensor_offset(scores), n_comp, n_tokens, top_k)) return 1;
NSUInteger max_threads = g_argsort_f32_i32_desc_pipeline.maxTotalThreadsPerThreadgroup;
if (max_threads == 0) max_threads = 256;
int32_t nth = 1;
+267
View File
@@ -318,3 +318,270 @@ kernel void kernel_argsort_merge_f32_i32(
template [[host_name("kernel_argsort_merge_f32_i32_desc")]] kernel argsort_merge_t kernel_argsort_merge_f32_i32<DS4_SORT_ORDER_DESC>;
template [[host_name("kernel_argsort_merge_f32_i32_desc_causal")]] kernel argsort_merge_t kernel_argsort_merge_f32_i32<DS4_SORT_ORDER_DESC, true>;
template [[host_name("kernel_argsort_merge_f32_i32_desc_causal_prefix")]] kernel argsort_merge_t kernel_argsort_merge_f32_i32<DS4_SORT_ORDER_DESC, true, true>;
// ---- Whole-GPU top-k for wide score rows -----------------------------------
//
// kernel_argsort_f32_i32 and its merge ladder finish a 131072-wide index row
// with two or three threadgroups doing the last merges, so one owner's
// selection costs several hundred microseconds at long context. This path
// keeps every core busy: a radix select over the order-preserving key bits
// finds the k-th largest score in three histogram passes, a deterministic
// compaction writes the winners by ascending id, and one threadgroup sorts
// them by (score descending, id ascending). Values by rank equal the merge
// path's; only exactly tied keys can change places. All kernels run with
// 1024 threads; the host falls back to the merge ladder otherwise.
struct ds4_metal_args_topk_select {
uint n; // scores per row
uint rows;
uint top_k; // at most 2048
uint pass; // histogram pass 0, 1, 2 over 11 + 11 + 10 key bits
uint parity; // pass-0 histogram slot this call fills; the other is cleared
uint n_tg; // element threadgroups per row
};
#define DS4_TOPK_BINS 2048u
#define DS4_TOPK_PER_TG 2048u
#define DS4_TOPK_HIST_WORDS (4u * DS4_TOPK_BINS) // hist0[2], hist1, hist2
// Unsigned key with the float's order: larger score, larger key.
static inline uint ds4_topk_key(float f) {
const uint u = as_type<uint>(f);
return (u & 0x80000000u) ? ~u : (u | 0x80000000u);
}
// The bin holding the k-th largest key and the count of keys above that bin.
static inline uint2 ds4_topk_scan(device atomic_uint *h, uint bins, uint k,
threadgroup uint *scratch,
uint tid, uint lane, uint simd) {
const uint per = bins / 1024u;
uint c[2] = {0u, 0u}, local = 0u;
for (uint q = 0; q < per; q++) {
c[q] = atomic_load_explicit(h + bins - 1u - (tid * per + q), memory_order_relaxed);
local += c[q];
}
const uint excl = simd_prefix_exclusive_sum(local);
if (lane == 31u) scratch[simd] = excl + local;
threadgroup_barrier(mem_flags::mem_threadgroup);
if (tid == 0u) {
uint run = 0u;
for (uint s = 0; s < 32u; s++) { const uint t = scratch[s]; scratch[s] = run; run += t; }
}
threadgroup_barrier(mem_flags::mem_threadgroup);
uint before = scratch[simd] + excl;
for (uint q = 0; q < per; q++) {
if (before < k && before + c[q] >= k) {
scratch[32] = bins - 1u - (tid * per + q);
scratch[33] = before;
}
before += c[q];
}
threadgroup_barrier(mem_flags::mem_threadgroup);
const uint2 r = uint2(scratch[32], scratch[33]);
threadgroup_barrier(mem_flags::mem_threadgroup);
return r;
}
// Walk the finished passes: (key prefix, count still wanted inside it, keys
// above it). After three passes that is (threshold, wanted ties, greater).
static inline uint3 ds4_topk_prefix(device atomic_uint *hist, uint parity, uint passes,
uint top_k, threadgroup uint *scratch,
uint tid, uint lane, uint simd) {
uint prefix = 0u, k = top_k, above = 0u;
if (passes > 0u) {
const uint2 r = ds4_topk_scan(hist + parity * DS4_TOPK_BINS, 2048u, k, scratch, tid, lane, simd);
prefix = r.x; k -= r.y; above += r.y;
}
if (passes > 1u) {
const uint2 r = ds4_topk_scan(hist + 2u * DS4_TOPK_BINS, 2048u, k, scratch, tid, lane, simd);
prefix = (prefix << 11) | r.x; k -= r.y; above += r.y;
}
if (passes > 2u) {
const uint2 r = ds4_topk_scan(hist + 3u * DS4_TOPK_BINS, 1024u, k, scratch, tid, lane, simd);
prefix = (prefix << 10) | r.x; k -= r.y; above += r.y;
}
return uint3(prefix, k, above);
}
kernel void kernel_topk_select_zero(
device uint * words,
constant uint & count,
uint gid [[thread_position_in_grid]]) {
if (gid < count) words[gid] = 0u;
}
// One histogram pass. Pass 0 also clears the other pass-0 slot and hist1 for
// the next call and the next pass; pass 1 clears hist2.
kernel void kernel_topk_select_hist(
constant ds4_metal_args_topk_select & args,
device const float * scores,
device atomic_uint * hist,
threadgroup atomic_uint * bins [[threadgroup(0)]],
uint2 tgpig [[threadgroup_position_in_grid]],
uint tid [[thread_index_in_threadgroup]],
uint lane [[thread_index_in_simdgroup]],
uint simd [[simdgroup_index_in_threadgroup]]) {
threadgroup uint scratch[34];
const uint row = tgpig.y;
device atomic_uint * h = hist + row * DS4_TOPK_HIST_WORDS;
const uint3 sel = ds4_topk_prefix(h, args.parity, args.pass, args.top_k, scratch, tid, lane, simd);
if (args.pass == 0u) {
for (uint i = tgpig.x * 1024u + tid; i < 2u * DS4_TOPK_BINS; i += args.n_tg * 1024u) {
atomic_store_explicit(h + (i < DS4_TOPK_BINS ? (args.parity ^ 1u) * DS4_TOPK_BINS + i :
DS4_TOPK_BINS + i), 0u, memory_order_relaxed);
}
} else if (args.pass == 1u) {
for (uint i = tgpig.x * 1024u + tid; i < DS4_TOPK_BINS; i += args.n_tg * 1024u)
atomic_store_explicit(h + 3u * DS4_TOPK_BINS + i, 0u, memory_order_relaxed);
}
for (uint i = tid; i < DS4_TOPK_BINS; i += 1024u) atomic_store_explicit(bins + i, 0u, memory_order_relaxed);
threadgroup_barrier(mem_flags::mem_threadgroup);
device const float * s = scores + (ulong)row * args.n;
for (uint j = 0; j < DS4_TOPK_PER_TG / 1024u; j++) {
const uint i = tgpig.x * DS4_TOPK_PER_TG + j * 1024u + tid;
if (i >= args.n) break;
const uint key = ds4_topk_key(s[i]);
uint bin;
if (args.pass == 0u) bin = key >> 21;
else if (args.pass == 1u) { if ((key >> 21) != sel.x) continue; bin = (key >> 10) & 0x7ffu; }
else { if ((key >> 10) != sel.x) continue; bin = key & 0x3ffu; }
atomic_fetch_add_explicit(bins + bin, 1u, memory_order_relaxed);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
device atomic_uint * out = h + (args.pass == 0u ? args.parity : args.pass + 1u) * DS4_TOPK_BINS;
for (uint i = tid; i < DS4_TOPK_BINS; i += 1024u) {
const uint c = atomic_load_explicit(bins + i, memory_order_relaxed);
if (c) atomic_fetch_add_explicit(out + i, c, memory_order_relaxed);
}
}
// Threshold, greater count and wanted ties for the row, plus this
// threadgroup's greater/equal counts for the compaction offsets.
kernel void kernel_topk_select_count(
constant ds4_metal_args_topk_select & args,
device const float * scores,
device atomic_uint * hist,
device uint * state,
device uint * counts,
uint2 tgpig [[threadgroup_position_in_grid]],
uint tid [[thread_index_in_threadgroup]],
uint lane [[thread_index_in_simdgroup]],
uint simd [[simdgroup_index_in_threadgroup]]) {
threadgroup uint scratch[34], part[64];
const uint row = tgpig.y;
const uint3 sel = ds4_topk_prefix(hist + row * DS4_TOPK_HIST_WORDS, args.parity, 3u, args.top_k,
scratch, tid, lane, simd);
if (tgpig.x == 0u && tid == 0u) {
state[row * 4u] = sel.x;
state[row * 4u + 1u] = sel.z;
state[row * 4u + 2u] = sel.y;
}
device const float * s = scores + (ulong)row * args.n;
uint gt = 0u, eq = 0u;
for (uint j = 0; j < DS4_TOPK_PER_TG / 1024u; j++) {
const uint i = tgpig.x * DS4_TOPK_PER_TG + j * 1024u + tid;
if (i >= args.n) break;
const uint key = ds4_topk_key(s[i]);
gt += key > sel.x;
eq += key == sel.x;
}
gt = simd_sum(gt); eq = simd_sum(eq);
if (lane == 0u) { part[simd] = gt; part[32u + simd] = eq; }
threadgroup_barrier(mem_flags::mem_threadgroup);
if (tid == 0u) {
uint tg_gt = 0u, tg_eq = 0u;
for (uint q = 0; q < 32u; q++) { tg_gt += part[q]; tg_eq += part[32u + q]; }
counts[(row * args.n_tg + tgpig.x) * 2u] = tg_gt;
counts[(row * args.n_tg + tgpig.x) * 2u + 1u] = tg_eq;
}
}
// Winners by ascending id: keys above the threshold first, then the wanted
// number of keys equal to it.
kernel void kernel_topk_select_write(
constant ds4_metal_args_topk_select & args,
device const float * scores,
device const uint * state,
device const uint * counts,
device int * selected,
uint2 tgpig [[threadgroup_position_in_grid]],
uint tid [[thread_index_in_threadgroup]],
uint lane [[thread_index_in_simdgroup]],
uint simd [[simdgroup_index_in_threadgroup]]) {
threadgroup uint part[66];
const uint row = tgpig.y;
const uint threshold = state[row * 4u], above = state[row * 4u + 1u], wanted = state[row * 4u + 2u];
uint gt_before = 0u, eq_before = 0u;
for (uint t = 0; t < tgpig.x; t++) {
gt_before += counts[(row * args.n_tg + t) * 2u];
eq_before += counts[(row * args.n_tg + t) * 2u + 1u];
}
device const float * s = scores + (ulong)row * args.n;
device int * out = selected + (ulong)row * args.top_k;
for (uint j = 0; j < DS4_TOPK_PER_TG / 1024u; j++) {
const uint i = tgpig.x * DS4_TOPK_PER_TG + j * 1024u + tid;
const bool live = i < args.n;
const uint key = live ? ds4_topk_key(s[i]) : 0u;
const uint gt = live && key > threshold, eq = live && key == threshold;
const uint gt_x = simd_prefix_exclusive_sum(gt), eq_x = simd_prefix_exclusive_sum(eq);
if (lane == 31u) { part[simd] = gt_x + gt; part[32u + simd] = eq_x + eq; }
threadgroup_barrier(mem_flags::mem_threadgroup);
if (tid == 0u) {
uint run_gt = 0u, run_eq = 0u;
for (uint q = 0; q < 32u; q++) {
const uint a = part[q], b = part[32u + q];
part[q] = run_gt; part[32u + q] = run_eq;
run_gt += a; run_eq += b;
}
part[64] = run_gt; part[65] = run_eq;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
if (gt) out[gt_before + part[simd] + gt_x] = (int)i;
if (eq) {
const uint r = eq_before + part[32u + simd] + eq_x;
if (r < wanted) out[above + r] = (int)i;
}
gt_before += part[64]; eq_before += part[65];
threadgroup_barrier(mem_flags::mem_threadgroup);
}
}
// Sort the row's winners in place: score descending, ties by ascending id.
kernel void kernel_topk_select_sort(
constant ds4_metal_args_topk_select & args,
device const float * scores,
device int * selected,
threadgroup uint * keys [[threadgroup(0)]],
uint row [[threadgroup_position_in_grid]],
uint tid [[thread_index_in_threadgroup]]) {
threadgroup uint * ids = keys + 2048u;
device const float * s = scores + (ulong)row * args.n;
device int * out = selected + (ulong)row * args.top_k;
for (uint i = tid; i < 2048u; i += 1024u) {
if (i < args.top_k) {
const uint id = (uint)out[i];
keys[i] = ds4_topk_key(s[id]);
ids[i] = id;
} else {
keys[i] = 0u;
ids[i] = 0xffffffffu;
}
}
threadgroup_barrier(mem_flags::mem_threadgroup);
for (uint k = 2u; k <= 2048u; k <<= 1) {
for (uint j = k >> 1; j > 0u; j >>= 1) {
for (uint i = tid; i < 2048u; i += 1024u) {
const uint o = i ^ j;
if (o <= i) continue;
const bool first = keys[i] > keys[o] || (keys[i] == keys[o] && ids[i] < ids[o]);
if (((i & k) == 0u) != first) {
const uint tk = keys[i], ti = ids[i];
keys[i] = keys[o]; ids[i] = ids[o];
keys[o] = tk; ids[o] = ti;
}
}
threadgroup_barrier(mem_flags::mem_threadgroup);
}
}
for (uint i = tid; i < args.top_k; i += 1024u) out[i] = (int)ids[i];
}
+71 -1
View File
@@ -1085,6 +1085,66 @@ static int check_general_topk(void) {
return 1;
}
/* The radix-select path against the merge ladder: same values by rank, same
* ids and order when the selected values are unique, ties by ascending id. */
static int check_topk_select(void) {
const uint32_t widths[] = {16384, 65536, 131072, 262144};
const uint32_t counts[] = {1, 512, 2048};
for (size_t wi = 0; wi < sizeof(widths) / sizeof(*widths); wi++) {
const uint32_t width = widths[wi], rows = 2;
ds4_gpu_tensor *scores = upload(NULL, (size_t)width * rows * 4);
ds4_gpu_tensor *fast = upload(NULL, (size_t)2048 * rows * 4);
ds4_gpu_tensor *slow = upload(NULL, (size_t)2048 * rows * 4);
topk_entry *reference = malloc((size_t)width * rows * sizeof(*reference));
bool *seen = calloc(width, sizeof(*seen));
CHECK(scores && fast && slow && reference && seen);
float *s = ds4_gpu_tensor_contents(scores);
for (uint32_t pattern = 0; pattern < 4; pattern++) {
for (uint32_t t = 0; t < rows; t++) {
for (uint32_t i = 0; i < width; i++) {
/* 0: coarse random values with many ties; 1: unique; 2: unique
* with seven of eight blocks masked like the candidate filter;
* 3: fewer finite values than 2048, so ties at -inf are selected */
const float unique = (float)((i * 7919u + t * 1009u) % 1048573u) - 500000.0f;
s[(size_t)t * width + i] = pattern == 0 ? random_value() : pattern == 1 ? unique :
pattern == 2 ? (i / 8 % 8 ? -INFINITY : unique) : (i % 1361 ? -INFINITY : unique);
reference[(size_t)t * width + i] = (topk_entry){s[(size_t)t * width + i], i};
}
qsort(reference + (size_t)t * width, width, sizeof(*reference), compare_topk_entry);
}
for (size_t ki = 0; ki < sizeof(counts) / sizeof(*counts); ki++) {
const uint32_t k = counts[ki];
CHECK(unsetenv("DS4_METAL_DISABLE_V41_TOPK_SELECT") == 0);
CHECK(ds4_gpu_indexer_topk_tensor(fast, scores, width, rows, k));
CHECK(setenv("DS4_METAL_DISABLE_V41_TOPK_SELECT", "1", 1) == 0);
CHECK(ds4_gpu_indexer_topk_tensor(slow, scores, width, rows, k));
CHECK(unsetenv("DS4_METAL_DISABLE_V41_TOPK_SELECT") == 0);
CHECK(ds4_gpu_synchronize());
const int32_t *a = ds4_gpu_tensor_contents(fast), *b = ds4_gpu_tensor_contents(slow);
for (uint32_t t = 0; t < rows; t++) {
const float *row = s + (size_t)t * width;
const topk_entry *ref = reference + (size_t)t * width;
memset(seen, 0, width * sizeof(*seen));
for (uint32_t i = 0; i < k; i++) {
const int32_t id = a[t * k + i];
CHECK(id >= 0 && (uint32_t)id < width && !seen[id]);
seen[id] = true;
CHECK(row[id] == ref[i].value);
CHECK(row[id] == row[b[t * k + i]]);
if (i && row[id] == row[a[t * k + i - 1]]) CHECK(a[t * k + i - 1] < id);
}
if (pattern == 1 || pattern == 2)
CHECK(!memcmp(a + t * k, b + t * k, k * sizeof(*a)));
}
}
}
fprintf(stderr, "V4.1 radix top-k width=%u: values by rank, unique-ID and tie-order oracle PASS\n", width);
ds4_gpu_tensor_free(scores); ds4_gpu_tensor_free(fast); ds4_gpu_tensor_free(slow);
free(reference); free(seen);
}
return 1;
}
static int check_causal_topk(void) {
const uint32_t frontiers[] = {513, 514, 1023, 1024, 1025, 1535, 2047, 2048, 4095, 16383, 32767, 65535};
const uint32_t counts[] = {1, 2, 31, 32, 33, 127, 128, 129};
@@ -1111,6 +1171,10 @@ static int check_causal_topk(void) {
}
for (size_t ci = 0; ci < sizeof(counts) / sizeof(*counts); ci++) {
const uint32_t rows = counts[ci];
/* The causal kernels are the ladder's variants and agree with it
* tie for tie; the radix path orders ties by id, so the reference
* stays on the ladder. */
CHECK(setenv("DS4_METAL_DISABLE_V41_TOPK_SELECT", "1", 1) == 0);
CHECK(ds4_gpu_begin_commands());
for (uint32_t t = 0; t < rows; t++) {
const uint32_t visible = (start + t + 1u) / ratio;
@@ -1120,6 +1184,7 @@ static int check_causal_topk(void) {
ds4_gpu_tensor_free(out); ds4_gpu_tensor_free(sr);
}
CHECK(ds4_gpu_end_commands());
CHECK(unsetenv("DS4_METAL_DISABLE_V41_TOPK_SELECT") == 0);
ids[rows * 512u] = 123456;
CHECK(ds4_gpu_dsv41_indexer_topk_batch(selected, scores, width, rows, start, ratio));
CHECK(ds4_gpu_synchronize());
@@ -1486,6 +1551,11 @@ int main(int argc, char **argv) {
ds4_gpu_cleanup();
return ok ? 0 : 1;
}
if (argc == 2 && !strcmp(argv[1], "--topk-select")) {
const int ok = ds4_gpu_init() && check_topk_select();
ds4_gpu_cleanup();
return ok ? 0 : 1;
}
if (argc == 2 && !strcmp(argv[1], "--general-topk")) {
const int ok = ds4_gpu_init() && check_general_topk();
ds4_gpu_cleanup();
@@ -1500,7 +1570,7 @@ int main(int argc, char **argv) {
int ok = ds4_gpu_init() && check_router() && check_quantization() && check_engram() && check_rope_stride() && check_pool() &&
check_candidates() && check_candidates_batch(16449, 17, 16400) &&
check_candidates_batch(17017, 17, 17000) && check_sparse_gather() && check_indexer_batch() && check_index_score_wide() &&
check_embedding() && check_index_projection() && check_general_topk() && check_causal_topk() && check_indexer_all() && check_compact_carry() && check_attention_output(false) &&
check_embedding() && check_index_projection() && check_general_topk() && check_topk_select() && check_causal_topk() && check_indexer_all() && check_compact_carry() && check_attention_output(false) &&
check_tp_attention();
ds4_gpu_cleanup();
return ok ? 0 : 1;