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
add the README and tests
Browse files- README.md +33 -0
- 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)
|