Instructions to use marcsun13/topk with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Kernels
How to use marcsun13/topk with Kernels:
# !pip install kernels from kernels import get_kernel # a version (or an explicit revision) is required; see the "Files and versions" tab for the available ones kernel = get_kernel("marcsun13/topk", version=1) - Notebooks
- Google Colab
- Kaggle
Commit ·
5bb63f9
0
Parent(s):
topk-metal: top-k over a small row for MoE routers
Browse files- .gitattributes +39 -0
- .gitignore +8 -0
- build.toml +32 -0
- flake.lock +117 -0
- flake.nix +12 -0
- topk_metal/common.h +24 -0
- topk_metal/dispatch.mm +116 -0
- topk_metal/top_k.metal +84 -0
- topk_metal/topk_metal.cpp +41 -0
- torch-ext/topk_metal/__init__.py +39 -0
- torch-ext/torch_binding.cpp +14 -0
- torch-ext/torch_binding.h +12 -0
.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);
|