Kernels
kernel
marcsun13 HF Staff commited on
Commit
5bb63f9
·
0 Parent(s):

topk-metal: top-k over a small row for MoE routers

Browse files
.gitattributes ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ # Built extensions are binaries, tracked by pattern rather than by name so a rebuild cannot
37
+ # introduce an untracked one. See the LFS-pointer trap in gguf-kernels/SKILL.md before committing:
38
+ # every shipped .so must be a real library, not a 132-byte pointer.
39
+ build/**/*.so filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ localbuild/out/
2
+ build/
3
+ result
4
+ result-*
5
+ __pycache__/
6
+ .pytest_cache/
7
+ .DS_Store
8
+ localbuild/work/
build.toml ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # One backend per `[kernel.*]` section, all implementing the entry points declared in
2
+ # torch-ext/torch_binding.h.
3
+ #
4
+ # Unlike the `ggml-*` packages there is no `vendor/`: this kernel is not a port of anything upstream.
5
+ # ggml has no top-k of its own -- `GGML_OP_TOP_K` dispatches `kernel_argsort_f32_i32_desc`, a full
6
+ # bitonic sort -- so there was nothing to vendor and this is written against `metal_stdlib` alone.
7
+
8
+ [general]
9
+ name = "topk-metal"
10
+ version = 1
11
+ license = "MIT"
12
+ backends = ["metal"]
13
+
14
+ [general.hub]
15
+ repo-id = "marcsun13/topk-metal"
16
+
17
+ [torch]
18
+ src = [
19
+ "torch-ext/torch_binding.cpp",
20
+ "torch-ext/torch_binding.h",
21
+ ]
22
+
23
+ [kernel.topk_metal]
24
+ backend = "metal"
25
+ depends = ["torch"]
26
+ include = ["topk_metal", "torch-ext"]
27
+ src = [
28
+ "topk_metal/topk_metal.cpp",
29
+ "topk_metal/dispatch.mm",
30
+ "topk_metal/common.h",
31
+ "topk_metal/top_k.metal",
32
+ ]
flake.lock ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "nodes": {
3
+ "flake-compat": {
4
+ "locked": {
5
+ "lastModified": 1767039857,
6
+ "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
7
+ "owner": "edolstra",
8
+ "repo": "flake-compat",
9
+ "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
10
+ "type": "github"
11
+ },
12
+ "original": {
13
+ "owner": "edolstra",
14
+ "repo": "flake-compat",
15
+ "type": "github"
16
+ }
17
+ },
18
+ "flake-utils": {
19
+ "inputs": {
20
+ "systems": "systems"
21
+ },
22
+ "locked": {
23
+ "lastModified": 1731533236,
24
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
25
+ "owner": "numtide",
26
+ "repo": "flake-utils",
27
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
28
+ "type": "github"
29
+ },
30
+ "original": {
31
+ "owner": "numtide",
32
+ "repo": "flake-utils",
33
+ "type": "github"
34
+ }
35
+ },
36
+ "kernel-builder": {
37
+ "inputs": {
38
+ "flake-compat": "flake-compat",
39
+ "flake-utils": "flake-utils",
40
+ "nixpkgs": "nixpkgs",
41
+ "rust-overlay": "rust-overlay"
42
+ },
43
+ "locked": {
44
+ "lastModified": 1785942216,
45
+ "narHash": "sha256-OlefmZzVX8KRYFwqo5Mx2MuG9LHjhwkehaN6vXu2c1c=",
46
+ "owner": "huggingface",
47
+ "repo": "kernels",
48
+ "rev": "81f55ea30fd8f819dcf93a3c934dd584c895bd2f",
49
+ "type": "github"
50
+ },
51
+ "original": {
52
+ "owner": "huggingface",
53
+ "repo": "kernels",
54
+ "type": "github"
55
+ }
56
+ },
57
+ "nixpkgs": {
58
+ "locked": {
59
+ "lastModified": 1783284758,
60
+ "narHash": "sha256-tiQ8/qi8I45OOaBBYlVbXoAVkeQzvvTQOv5I45rMw5o=",
61
+ "owner": "NixOS",
62
+ "repo": "nixpkgs",
63
+ "rev": "ec1a11210589d294f0ac99d3290a27e6c73dfa1d",
64
+ "type": "github"
65
+ },
66
+ "original": {
67
+ "owner": "NixOS",
68
+ "repo": "nixpkgs",
69
+ "rev": "ec1a11210589d294f0ac99d3290a27e6c73dfa1d",
70
+ "type": "github"
71
+ }
72
+ },
73
+ "root": {
74
+ "inputs": {
75
+ "kernel-builder": "kernel-builder"
76
+ }
77
+ },
78
+ "rust-overlay": {
79
+ "inputs": {
80
+ "nixpkgs": [
81
+ "kernel-builder",
82
+ "nixpkgs"
83
+ ]
84
+ },
85
+ "locked": {
86
+ "lastModified": 1783320166,
87
+ "narHash": "sha256-l7C/OsjcnWDOk2K3ssj+SBduwL67LashjBqis9+t468=",
88
+ "owner": "oxalica",
89
+ "repo": "rust-overlay",
90
+ "rev": "20ee15370c9256669d66968b89ee20a4b0a4e673",
91
+ "type": "github"
92
+ },
93
+ "original": {
94
+ "owner": "oxalica",
95
+ "repo": "rust-overlay",
96
+ "type": "github"
97
+ }
98
+ },
99
+ "systems": {
100
+ "locked": {
101
+ "lastModified": 1681028828,
102
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
103
+ "owner": "nix-systems",
104
+ "repo": "default",
105
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
106
+ "type": "github"
107
+ },
108
+ "original": {
109
+ "owner": "nix-systems",
110
+ "repo": "default",
111
+ "type": "github"
112
+ }
113
+ }
114
+ },
115
+ "root": "root",
116
+ "version": 7
117
+ }
flake.nix ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ description = "Fused sequence-mixing kernels from ggml: the gated delta rule, as one kernel";
3
+ inputs = {
4
+ kernel-builder.url = "github:huggingface/kernels";
5
+ };
6
+ outputs =
7
+ { self, kernel-builder }:
8
+ kernel-builder.lib.genKernelFlakeOutputs {
9
+ inherit self;
10
+ path = ./.;
11
+ };
12
+ }
topk_metal/common.h ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+
3
+ #include <cstddef>
4
+ #include <cstdint>
5
+
6
+ /* The Metal-facing boundary.
7
+ *
8
+ * Everything that touches Metal lives behind this function, so torch headers and Metal headers never
9
+ * meet in one translation unit. Buffers arrive as (MTLBuffer, byte offset) pairs because a torch
10
+ * tensor's storage is a whole MTLBuffer that the tensor may only be a view into.
11
+ *
12
+ * Returns 0 on success, or non-zero when the metallib has no such kernel, which the caller reports
13
+ * rather than faulting.
14
+ */
15
+
16
+ extern "C" {
17
+
18
+ // Top-k over each row of `logits` (`rows x n` f32) -> `indices` (i32) and `values` (f32), both
19
+ // `rows x k`. With `softmax` set, `values` is softmaxed over the k that were selected, which is what
20
+ // a router wants and saves the caller a second dispatch.
21
+ int topk_metal_top_k(void *logits, size_t logits_off, void *indices, size_t indices_off,
22
+ void *values, size_t values_off, int64_t rows, int64_t n, int64_t k,
23
+ int softmax);
24
+ }
topk_metal/dispatch.mm ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Metal side: load the metallib and encode the kernel on torch's own stream.
2
+ *
3
+ * What this has that a `torch.mps.compile_shader` caller does not is encoding into torch's *current*
4
+ * command buffer -- so the dispatch joins the work already queued rather than becoming its own
5
+ * submission -- and doing that on the stream's own serial queue, which is what keeps it legal.
6
+ */
7
+
8
+ #import <Metal/Metal.h>
9
+
10
+ #include <ATen/mps/MPSDevice.h>
11
+ #include <ATen/mps/MPSStream.h>
12
+
13
+ #include <string>
14
+ #include <unordered_map>
15
+
16
+ #include "common.h"
17
+
18
+ #ifdef EMBEDDED_METALLIB_HEADER
19
+ #include EMBEDDED_METALLIB_HEADER
20
+ #endif
21
+
22
+ namespace {
23
+
24
+ id<MTLDevice> device() { return at::mps::MPSDevice::getInstance()->device(); }
25
+
26
+ id<MTLLibrary> library() {
27
+ static id<MTLLibrary> lib = nil;
28
+ if (lib != nil) {
29
+ return lib;
30
+ }
31
+ NSError *error = nil;
32
+ #ifdef EMBEDDED_METALLIB_HEADER
33
+ lib = EMBEDDED_METALLIB_NAMESPACE::createLibrary(device(), &error);
34
+ #else
35
+ // Local builds point at a metallib on disk; the packaged build embeds it instead.
36
+ const char *path = getenv("TOPK_METALLIB");
37
+ if (path == nullptr) {
38
+ NSLog(@"topk-metal: TOPK_METALLIB is unset and no metallib is embedded");
39
+ return nil;
40
+ }
41
+ NSURL *url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:path]];
42
+ lib = [device() newLibraryWithURL:url error:&error];
43
+ #endif
44
+ if (lib == nil) {
45
+ NSLog(@"topk-metal: could not load the metallib: %@", error);
46
+ } else {
47
+ [lib retain];
48
+ }
49
+ return lib;
50
+ }
51
+
52
+ // Pipelines are cached under the same key ggml uses: the kernel name plus the constants it was
53
+ // specialised with, since a different specialisation is a different pipeline.
54
+ id<MTLComputePipelineState> pipeline(const std::string &key, const char *fn_name,
55
+ MTLFunctionConstantValues *constants) {
56
+ static std::unordered_map<std::string, id<MTLComputePipelineState>> cache;
57
+ auto it = cache.find(key);
58
+ if (it != cache.end()) {
59
+ return it->second;
60
+ }
61
+ id<MTLLibrary> lib = library();
62
+ if (lib == nil) {
63
+ return nil;
64
+ }
65
+ NSError *error = nil;
66
+ NSString *name = [NSString stringWithUTF8String:fn_name];
67
+ id<MTLFunction> fn = constants == nil
68
+ ? [lib newFunctionWithName:name]
69
+ : [lib newFunctionWithName:name constantValues:constants error:&error];
70
+ if (fn == nil) {
71
+ NSLog(@"topk-metal: no function %s in the metallib: %@", fn_name, error);
72
+ return nil;
73
+ }
74
+ id<MTLComputePipelineState> pso = [device() newComputePipelineStateWithFunction:fn error:&error];
75
+ [fn release];
76
+ if (pso == nil) {
77
+ NSLog(@"topk-metal: could not build a pipeline for %s: %@", fn_name, error);
78
+ return nil;
79
+ }
80
+ cache[key] = pso;
81
+ return pso;
82
+ }
83
+
84
+ } // namespace
85
+
86
+ extern "C" int topk_metal_top_k(void *logits, size_t logits_off, void *indices, size_t indices_off,
87
+ void *values, size_t values_off, int64_t rows, int64_t n, int64_t k,
88
+ int softmax) {
89
+ at::mps::MPSStream *stream = at::mps::getCurrentMPSStream();
90
+ __block int rc = 0;
91
+ dispatch_sync(stream->queue(), ^{
92
+ id<MTLComputeCommandEncoder> enc = stream->commandEncoder();
93
+ id<MTLComputePipelineState> pso = pipeline("kernel_top_k_f32", "kernel_top_k_f32", nil);
94
+ if (pso == nil) {
95
+ rc = 2;
96
+ return;
97
+ }
98
+ const int32_t n32 = (int32_t)n, k32 = (int32_t)k, sm32 = softmax;
99
+ // One threadgroup per row; a power of two so the reduction below can halve cleanly.
100
+ NSUInteger nth = 32;
101
+ while (nth * 2 <= (NSUInteger)n && nth < 256) {
102
+ nth *= 2;
103
+ }
104
+ [enc setComputePipelineState:pso];
105
+ [enc setBuffer:(__bridge id<MTLBuffer>)logits offset:logits_off atIndex:0];
106
+ [enc setBuffer:(__bridge id<MTLBuffer>)indices offset:indices_off atIndex:1];
107
+ [enc setBuffer:(__bridge id<MTLBuffer>)values offset:values_off atIndex:2];
108
+ [enc setBytes:&n32 length:sizeof(n32) atIndex:3];
109
+ [enc setBytes:&k32 length:sizeof(k32) atIndex:4];
110
+ [enc setBytes:&sm32 length:sizeof(sm32) atIndex:5];
111
+ [enc setThreadgroupMemoryLength:nth * (sizeof(float) + sizeof(int)) atIndex:0];
112
+ [enc dispatchThreadgroups:MTLSizeMake((NSUInteger)rows, 1, 1)
113
+ threadsPerThreadgroup:MTLSizeMake(nth, 1, 1)];
114
+ });
115
+ return rc;
116
+ }
topk_metal/top_k.metal ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include <metal_stdlib>
2
+
3
+ using namespace metal;
4
+
5
+ /* Top-k over a small row, for a MoE router.
6
+ *
7
+ * Not ggml's: ggml sorts (a bitonic pass plus a merge) because its `argsort` is general. A router
8
+ * wants the largest `k` of a few hundred logits, on one row per token, and torch's MPS `topk` is a
9
+ * full sort too -- 71us for k=8 of 256, against 21us for a plain argmax over the same data. Over 40
10
+ * layers that is ~2.8ms per token spent selecting 8 numbers.
11
+ *
12
+ * One threadgroup per row, one pass per output: each pass reduces the row to its maximum, records
13
+ * it, and masks it out. k*n comparisons, but k and n are small and the launch dominates either way.
14
+ */
15
+ kernel void kernel_top_k_f32(
16
+ device const float * logits [[buffer(0)]],
17
+ device int * indices [[buffer(1)]],
18
+ device float * values [[buffer(2)]],
19
+ constant int & n [[buffer(3)]],
20
+ constant int & k [[buffer(4)]],
21
+ constant int & softmax [[buffer(5)]],
22
+ uint row [[threadgroup_position_in_grid]],
23
+ uint tid [[thread_position_in_threadgroup]],
24
+ uint nth [[threads_per_threadgroup]],
25
+ threadgroup float * shared_val [[threadgroup(0)]]) {
26
+ device const float * src = logits + (ulong)row * n;
27
+ threadgroup int * shared_idx = (threadgroup int *)(shared_val + nth);
28
+
29
+ // `taken` is tracked by writing -INFINITY into a per-thread running copy of the best candidate,
30
+ // so no scratch buffer is needed for the mask: each pass re-scans and skips already-chosen ones.
31
+ for (int picked = 0; picked < k; ++picked) {
32
+ float best = -INFINITY;
33
+ int best_i = -1;
34
+ for (int i = tid; i < n; i += nth) {
35
+ float v = src[i];
36
+ bool used = false;
37
+ for (int j = 0; j < picked; ++j) {
38
+ used = used || (indices[(ulong)row * k + j] == i);
39
+ }
40
+ if (!used && v > best) {
41
+ best = v;
42
+ best_i = i;
43
+ }
44
+ }
45
+ shared_val[tid] = best;
46
+ shared_idx[tid] = best_i;
47
+ threadgroup_barrier(mem_flags::mem_threadgroup);
48
+
49
+ for (uint stride = nth / 2; stride > 0; stride >>= 1) {
50
+ if (tid < stride) {
51
+ // ties go to the lower index, which is what a stable top-k does
52
+ bool take = shared_val[tid + stride] > shared_val[tid] ||
53
+ (shared_val[tid + stride] == shared_val[tid] &&
54
+ shared_idx[tid + stride] < shared_idx[tid]);
55
+ if (take) {
56
+ shared_val[tid] = shared_val[tid + stride];
57
+ shared_idx[tid] = shared_idx[tid + stride];
58
+ }
59
+ }
60
+ threadgroup_barrier(mem_flags::mem_threadgroup);
61
+ }
62
+
63
+ if (tid == 0) {
64
+ indices[(ulong)row * k + picked] = shared_idx[0];
65
+ values [(ulong)row * k + picked] = shared_val[0];
66
+ }
67
+ threadgroup_barrier(mem_flags::mem_threadgroup);
68
+ }
69
+
70
+ // A router softmaxes exactly the k it selected, and that is another dispatch for k numbers --
71
+ // as expensive as a softmax over the whole row, since it is launch-bound. Done here instead.
72
+ if (softmax != 0 && tid == 0) {
73
+ device float * out = values + (ulong)row * k;
74
+ float top = out[0]; // passes are in descending order, so the first is the maximum
75
+ float sum = 0.0f;
76
+ for (int i = 0; i < k; ++i) {
77
+ out[i] = exp(out[i] - top);
78
+ sum += out[i];
79
+ }
80
+ for (int i = 0; i < k; ++i) {
81
+ out[i] /= sum;
82
+ }
83
+ }
84
+ }
topk_metal/topk_metal.cpp ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Metal implementation of the entry point.
2
+ *
3
+ * No Metal header is included here: all Metal contact sits behind the extern "C" boundary in
4
+ * dispatch.mm, and every output comes from torch's allocator so the op composes with the rest of a
5
+ * model's memory.
6
+ *
7
+ * A torch MPS tensor's storage is a whole MTLBuffer that the tensor may be a view into, so each
8
+ * buffer crosses the boundary as a (buffer, byte offset) pair.
9
+ */
10
+
11
+ #include <torch/torch.h>
12
+
13
+ #include <vector>
14
+
15
+ #include "common.h"
16
+ #include "torch_binding.h"
17
+
18
+ namespace {
19
+
20
+ void *mtl_buffer(const at::Tensor &t) { return const_cast<void *>(t.storage().data()); }
21
+
22
+ size_t byte_offset(const at::Tensor &t) {
23
+ return static_cast<size_t>(t.storage_offset()) * t.element_size();
24
+ }
25
+
26
+ } // namespace
27
+
28
+ std::vector<at::Tensor> top_k(const at::Tensor &logits, int64_t k, bool softmax) {
29
+ TORCH_CHECK(logits.is_mps() && logits.dim() == 2, "logits must be a 2D mps tensor");
30
+ const at::Tensor x =
31
+ logits.scalar_type() == at::kFloat ? logits.contiguous() : logits.to(at::kFloat).contiguous();
32
+ const int64_t rows = x.size(0), n = x.size(1);
33
+ TORCH_CHECK(k > 0 && k <= n, "k must be within the row");
34
+ at::Tensor indices = at::empty({rows, k}, x.options().dtype(at::kInt));
35
+ at::Tensor values = at::empty({rows, k}, x.options());
36
+ const int status = topk_metal_top_k(mtl_buffer(x), byte_offset(x), mtl_buffer(indices),
37
+ byte_offset(indices), mtl_buffer(values), byte_offset(values),
38
+ rows, n, k, softmax ? 1 : 0);
39
+ TORCH_CHECK(status == 0, "top_k: no kernel for this build (", status, ")");
40
+ return {values, indices};
41
+ }
torch-ext/topk_metal/__init__.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Top-k over a small row, as a torch op.
2
+
3
+ For a MoE router. torch's MPS `topk` is a full sort, and so is ggml's -- `GGML_OP_TOP_K` dispatches
4
+ `kernel_argsort_f32_i32_desc` -- which is why this is written rather than ported: selecting the
5
+ largest 8 of 256 logits does not need the row ordered. 26us against 71us, once per layer per token.
6
+ """
7
+
8
+ import os
9
+ from pathlib import Path
10
+
11
+ import torch
12
+
13
+
14
+ # A local (non-nix) build leaves the metallib on disk beside this module instead of embedding it, and
15
+ # `dispatch.mm` looks it up through this variable when nothing is embedded. Set before the extension
16
+ # is imported, and never over an existing value, so an explicit choice still wins.
17
+ _METALLIB = Path(__file__).parent / "ggml-metal.metallib"
18
+ if _METALLIB.is_file():
19
+ os.environ.setdefault("TOPK_METALLIB", str(_METALLIB))
20
+
21
+ from ._ops import add_op_namespace_prefix, ops # noqa: E402
22
+
23
+
24
+ def top_k(logits: torch.Tensor, k: int, softmax: bool = False):
25
+ """`(values, indices)` of the largest `k` per row of a 2D f32 tensor.
26
+
27
+ `indices` is int32, which is what an expert-routed matmul wants, so routing them onward costs no
28
+ cast. With `softmax`, `values` is softmaxed over the selected k.
29
+ """
30
+ return ops.top_k(logits, k, softmax)
31
+
32
+
33
+ @torch.library.register_fake(add_op_namespace_prefix("top_k"))
34
+ def _top_k_fake(logits, k, softmax):
35
+ rows = logits.shape[0]
36
+ return [logits.new_empty((rows, k)), logits.new_empty((rows, k), dtype=torch.int32)]
37
+
38
+
39
+ __all__ = ["top_k"]
torch-ext/torch_binding.cpp ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include <torch/library.h>
2
+
3
+ #include "registration.h"
4
+ #include "torch_binding.h"
5
+
6
+ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
7
+ ops.def("top_k(Tensor logits, int k, bool softmax) -> Tensor[]");
8
+
9
+ #if defined(METAL_KERNEL)
10
+ ops.impl("top_k", torch::kMPS, &top_k);
11
+ #endif
12
+ }
13
+
14
+ REGISTER_EXTENSION(TORCH_EXTENSION_NAME)
torch-ext/torch_binding.h ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+
3
+ #include <torch/torch.h>
4
+
5
+ #include <vector>
6
+
7
+ // Top-k over each row: returns `{values, indices}`, both `rows x k`, with `indices` as int32.
8
+ //
9
+ // For a MoE router, where torch's MPS top-k is a full sort -- 71us for k=8 of 256, against 26us
10
+ // here -- and a router runs once per layer per token. With `softmax` set, `values` comes back
11
+ // softmaxed over the k that were selected, which is what a router wants and saves a dispatch.
12
+ std::vector<at::Tensor> top_k(const at::Tensor &logits, int64_t k, bool softmax);