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

add the README and tests

Browse files
Files changed (2) hide show
  1. README.md +33 -0
  2. tests/test_top_k.py +57 -0
README.md ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: kernels
3
+ license: mit
4
+ tags:
5
+ - kernel
6
+ ---
7
+
8
+ ## topk-metal
9
+
10
+ Top-k over a small row, for a MoE router. One threadgroup per row, one reduction pass per output:
11
+ `k*n` comparisons, but `k` and `n` are small and the launch dominates either way. Optionally softmaxes
12
+ the `k` it selected, which is what a router wants and saves a second dispatch.
13
+
14
+ Not a port. ggml has no top-k of its own — `GGML_OP_TOP_K` dispatches `kernel_argsort_f32_i32_desc`,
15
+ a full bitonic sort — and torch's MPS `topk` is a full sort too. Selecting the largest 8 of 256 logits
16
+ does not need the row ordered: 26 us here against 71 us for either sort, once per layer per token.
17
+
18
+ `indices` comes back as int32, which is what an expert-routed matmul wants, so routing them onward
19
+ costs no cast.
20
+
21
+ ## Usage
22
+
23
+ ```python
24
+ import torch
25
+ from kernels import get_kernel
26
+
27
+ topk = get_kernel("marcsun13/topk-metal", version=1)
28
+
29
+ logits = torch.randn(1, 256, device="mps") # one row of router logits
30
+
31
+ values, indices = topk.top_k(logits, 8) # (1, 8) f32, (1, 8) int32
32
+ weights, experts = topk.top_k(logits, 8, True) # values softmaxed over the selected 8
33
+ ```
tests/test_top_k.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """`top_k` against `torch.topk`, which is what it replaces.
2
+
3
+ The kernel exists to cut a sort down to a selection, so the test that matters is that it picks the
4
+ same elements, in the same order, as the sort it stands in for.
5
+ """
6
+
7
+ import os
8
+
9
+ import pytest
10
+ import torch
11
+
12
+
13
+ DEV = "mps"
14
+ LIB = os.environ.get("TOPK_LOCAL_LIB")
15
+
16
+ if LIB:
17
+ torch.ops.load_library(LIB)
18
+ ops = getattr(torch.ops, os.path.basename(LIB).removesuffix(".so"))
19
+ else:
20
+ from pathlib import Path
21
+
22
+ try:
23
+ from kernels import get_local_kernel
24
+
25
+ ops = get_local_kernel(Path(__file__).resolve().parent.parent, "metal")
26
+ except Exception as error: # pragma: no cover
27
+ pytest.skip(f"no kernel to test ({error})", allow_module_level=True)
28
+
29
+ pytestmark = pytest.mark.skipif(not torch.backends.mps.is_available(), reason="needs mps")
30
+
31
+
32
+ @pytest.mark.parametrize(("rows", "n", "k"), [(1, 256, 8), (1, 128, 4), (7, 512, 8), (1, 64, 1), (3, 320, 16)])
33
+ def test_matches_torch_topk(rows, n, k):
34
+ torch.manual_seed(0)
35
+ logits = torch.randn(rows, n, device=DEV)
36
+ values, indices = ops.top_k(logits, k, False)
37
+ ref_values, ref_indices = torch.topk(logits, k, dim=-1)
38
+ assert torch.equal(indices.long(), ref_indices)
39
+ assert torch.equal(values, ref_values)
40
+ assert indices.dtype == torch.int32
41
+
42
+
43
+ def test_softmax_is_over_the_selected_k():
44
+ torch.manual_seed(0)
45
+ logits = torch.randn(4, 256, device=DEV)
46
+ values, _ = ops.top_k(logits, 8, True)
47
+ reference = torch.softmax(torch.topk(logits, 8, dim=-1).values.float(), dim=-1)
48
+ assert torch.allclose(values, reference, atol=1e-6)
49
+ assert torch.allclose(values.sum(-1), torch.ones(4, device=DEV), atol=1e-6)
50
+
51
+
52
+ def test_k_equal_to_the_row():
53
+ """Degenerate but legal: selecting everything is a sort, and must still agree."""
54
+ torch.manual_seed(0)
55
+ logits = torch.randn(2, 64, device=DEV)
56
+ _, indices = ops.top_k(logits, 64, False)
57
+ assert torch.equal(indices.long(), torch.topk(logits, 64, dim=-1).indices)