mjolnir-kernel 0.1.1__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/PKG-INFO +31 -4
  2. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/README.md +30 -3
  3. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/mjolnir/__init__.py +1 -1
  4. mjolnir_kernel-0.2.0/mjolnir/arch/__init__.py +7 -0
  5. mjolnir_kernel-0.2.0/mjolnir/arch/thor.py +42 -0
  6. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/mjolnir/backend.py +21 -3
  7. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/mjolnir/calibrate.py +2 -2
  8. mjolnir_kernel-0.2.0/mjolnir/platforms.py +103 -0
  9. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/.gitignore +0 -0
  10. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/LICENSE +0 -0
  11. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/NOTICE +0 -0
  12. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/mjolnir/banner.py +0 -0
  13. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/mjolnir/draftvocab.py +0 -0
  14. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/mjolnir/kvfp6.py +0 -0
  15. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/mjolnir/kvscales.py +0 -0
  16. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/mjolnir/lmhead.py +0 -0
  17. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/mjolnir/py.typed +0 -0
  18. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/mjolnir/qpack.py +0 -0
  19. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/mjolnir/quant.py +0 -0
  20. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/mjolnir/runner.py +0 -0
  21. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/mjolnir/t5_call.py +0 -0
  22. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/mjolnir/t5_const.py +0 -0
  23. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/mjolnir/t5_decode.py +0 -0
  24. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/mjolnir/t5_harness.py +0 -0
  25. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/mjolnir/t5_plan.py +0 -0
  26. {mjolnir_kernel-0.1.1 → mjolnir_kernel-0.2.0}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mjolnir-kernel
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: Mjolnir: a paged decode-attention kernel for NVIDIA Jetson AGX Thor (sm_110) on tcgen05 / TMEM / TMA (CuTe DSL), with fp8 and 6-bit (e2m3) KV caches — packaged as the vLLM MJOLNIR attention backend.
5
5
  Project-URL: Homepage, https://github.com/thor-arena/mjolnir
6
6
  Project-URL: Documentation, https://github.com/thor-arena/mjolnir/tree/main/docs/kernel
@@ -141,7 +141,7 @@ runs on the fallback backend.
141
141
  | `MJOLNIR_ATTN_DRAFT_T5` | `1` | `0`: the drafter's layers (MTP / EAGLE) run on the fallback — isolates the draft-step cost (`mjolnir verify profile`). fp8 KV only: refused on fp6 KV, whose fallback cannot be CUDA-graphed |
142
142
  | `MJOLNIR_ATTN_NONCAUSAL` | `1` | `0`: a parallel drafter's non-causal block steps (DSpark / DFlash) go to the fallback instead of the decode kernel — fp8 KV only for the same reason as above |
143
143
  | `MJOLNIR_ATTN_FUSED_DRAFT` | `1` | `0`: tell vLLM the backend cannot advance draft metadata in place → one graph replay per draft step instead of the fused draft loop |
144
- | `MJOLNIR_LM_HEAD` | – | `fp8`: quantize the lm_head to e4m3 per row at load, Triton fp8-weight GEMM for the verify and every draft (`mjolnir.lmhead`; config `NVFP4_MJOLNIR_E2M3_LMH8`); the bf16 copy is freed automatically (untied heads only, +2.5 GB KV) |
144
+ | `MJOLNIR_LM_HEAD` | – | `fp8`: quantize the lm_head to e4m3 per row at load, Triton fp8-weight GEMM for the verify and every draft (`mjolnir.lmhead`; config `NVFP4_MJOLNIR_E2M3_LMH8_VT64K`); the bf16 copy is freed automatically (untied heads only, +2.5 GB KV) |
145
145
  | `MJOLNIR_DRAFT_VOCAB` | – | path to a `draft_vocab.json` (`mjolnir calibrate --draft-vocab`): the drafter's logits only over those tokens (VocabTrim); `MJOLNIR_DRAFT_VOCAB_SIZE` keeps the first N (a shorter file is padded with the lowest unused ids, with a warning). The kept head stays in the drafter's native quant (NVFP4 4-bit in-kernel, FP8 row-slice) — never up-scaled to bf16; a bf16 head is the one exception (fp8 only when `MJOLNIR_LM_HEAD=fp8` is set). `MJOLNIR_DRAFT_VOCAB_NATIVE=0` forces the dequant→fp8 path |
146
146
  | `MJOLNIR_ATTN_VERBOSE` | – | `1`: print the kernel's compile-time notes (ring stages, TMEM columns) — the standalone harness always does |
147
147
  | `MJOLNIR_ATTN_BANNER` | `1` | `0`: skip the Mjolnir hammer banner in the vLLM boot log (`mjolnir.banner.emit`, once per start, coloured with the server's formatter) |
@@ -166,8 +166,8 @@ expands the pages a prefill / mixed step reads to e4m3 for the FA4 fallback
166
166
  (channel scales applied, one descale per head). The scales come from a calibration run on fp8 KV:
167
167
 
168
168
  ```bash
169
- python -m mjolnir.calibrate --config <serve.yaml> --out kv_scales.json # in the image
170
- MJOLNIR_ATTN_KV_SCALES=kv_scales.json vllm serve ... --kv-cache-dtype fp6_e2m3 --attention-backend MJOLNIR
169
+ python -m mjolnir.calibrate --config <serve.yaml> --out kv_scales/e2m3.json # in the image
170
+ MJOLNIR_ATTN_KV_SCALES=kv_scales/e2m3.json vllm serve ... --kv-cache-dtype fp6_e2m3 --attention-backend MJOLNIR
171
171
  ```
172
172
 
173
173
  (`mjolnir calibrate` / `mjolnir serve` do both for you —
@@ -185,6 +185,31 @@ How they compare in speed and accuracy:
185
185
  [`quant`](https://github.com/thor-arena/mjolnir/blob/main/docs/kernel/quant.md).
186
186
  Wiring a vLLM cache-write path for fp6 is an open contribution.
187
187
 
188
+ ## Platforms and architectures
189
+
190
+ The kernel is written for one architecture today — Thor (`sm_110`) — but the
191
+ package is organized by platform: `mjolnir/arch/` is the implemented-kernel
192
+ registry (`mjolnir.arch.thor` re-exports the exact runner surface the backend
193
+ consumes), and `mjolnir/platforms.py` is the torch-free leaf that maps a
194
+ compute capability to it. Thor is implemented; DGX Spark (`sm_121`) and Jetson
195
+ Orin (`sm_87`) are declared research — named in the matrix, no kernel yet.
196
+
197
+ `kernel_for((11, 0))` returns `mjolnir.arch.thor`; any other compute
198
+ capability raises `PlatformNotSupported` naming the requested `sm_XX` and the
199
+ shipped platforms. The vLLM backend never sees that exception: its metadata
200
+ builder consults the pure `unsupported_reason()` and takes the normal
201
+ ineligible-config path — that layer's decode steps run on the fallback
202
+ backend, the serve continues. On a host without a usable CUDA device,
203
+ `MJOLNIR_ARCH` (`sm_110` or `11.0`) pins the architecture instead of the
204
+ torch read (the standalone harness).
205
+
206
+ There is no remembered platform state anywhere in the stack: the CLI side
207
+ resolves its platform per invocation (flag > `$MJOLNIR_PLATFORM` > host
208
+ detection — four source classes with provenance, the `/tmp/nv_jetson_model`
209
+ model copy corroboration-only), so the kernel is always resolved from the
210
+ device the run actually happens on. See
211
+ [docs/faq/platforms.md](../../docs/faq/platforms.md).
212
+
188
213
  ## Tests
189
214
 
190
215
  From a checkout, on a Thor:
@@ -213,6 +238,8 @@ is printed as information.
213
238
  | `mjolnir/t5_decode.py` | the CuTe-DSL kernel, the split-KV merge, the harness and the emulator |
214
239
  | `mjolnir/runner.py` | plan buffers, compile cache, launch, KV views (no vLLM import) |
215
240
  | `mjolnir/backend.py` | the vLLM backend, metadata builder, impl |
241
+ | `mjolnir/platforms.py` | arch → kernel resolution (`kernel_for`, `PlatformNotSupported`), `MJOLNIR_ARCH` detection — torch-free |
242
+ | `mjolnir/arch/thor.py` | the Thor (`sm_110`) kernel surface: re-exports of the runner |
216
243
  | `mjolnir/__init__.py` | `register()` — the vLLM plugin entry point |
217
244
 
218
245
  Apache-2.0, except `mjolnir/t5_decode.py`, which is derived from an NVIDIA
@@ -113,7 +113,7 @@ runs on the fallback backend.
113
113
  | `MJOLNIR_ATTN_DRAFT_T5` | `1` | `0`: the drafter's layers (MTP / EAGLE) run on the fallback — isolates the draft-step cost (`mjolnir verify profile`). fp8 KV only: refused on fp6 KV, whose fallback cannot be CUDA-graphed |
114
114
  | `MJOLNIR_ATTN_NONCAUSAL` | `1` | `0`: a parallel drafter's non-causal block steps (DSpark / DFlash) go to the fallback instead of the decode kernel — fp8 KV only for the same reason as above |
115
115
  | `MJOLNIR_ATTN_FUSED_DRAFT` | `1` | `0`: tell vLLM the backend cannot advance draft metadata in place → one graph replay per draft step instead of the fused draft loop |
116
- | `MJOLNIR_LM_HEAD` | – | `fp8`: quantize the lm_head to e4m3 per row at load, Triton fp8-weight GEMM for the verify and every draft (`mjolnir.lmhead`; config `NVFP4_MJOLNIR_E2M3_LMH8`); the bf16 copy is freed automatically (untied heads only, +2.5 GB KV) |
116
+ | `MJOLNIR_LM_HEAD` | – | `fp8`: quantize the lm_head to e4m3 per row at load, Triton fp8-weight GEMM for the verify and every draft (`mjolnir.lmhead`; config `NVFP4_MJOLNIR_E2M3_LMH8_VT64K`); the bf16 copy is freed automatically (untied heads only, +2.5 GB KV) |
117
117
  | `MJOLNIR_DRAFT_VOCAB` | – | path to a `draft_vocab.json` (`mjolnir calibrate --draft-vocab`): the drafter's logits only over those tokens (VocabTrim); `MJOLNIR_DRAFT_VOCAB_SIZE` keeps the first N (a shorter file is padded with the lowest unused ids, with a warning). The kept head stays in the drafter's native quant (NVFP4 4-bit in-kernel, FP8 row-slice) — never up-scaled to bf16; a bf16 head is the one exception (fp8 only when `MJOLNIR_LM_HEAD=fp8` is set). `MJOLNIR_DRAFT_VOCAB_NATIVE=0` forces the dequant→fp8 path |
118
118
  | `MJOLNIR_ATTN_VERBOSE` | – | `1`: print the kernel's compile-time notes (ring stages, TMEM columns) — the standalone harness always does |
119
119
  | `MJOLNIR_ATTN_BANNER` | `1` | `0`: skip the Mjolnir hammer banner in the vLLM boot log (`mjolnir.banner.emit`, once per start, coloured with the server's formatter) |
@@ -138,8 +138,8 @@ expands the pages a prefill / mixed step reads to e4m3 for the FA4 fallback
138
138
  (channel scales applied, one descale per head). The scales come from a calibration run on fp8 KV:
139
139
 
140
140
  ```bash
141
- python -m mjolnir.calibrate --config <serve.yaml> --out kv_scales.json # in the image
142
- MJOLNIR_ATTN_KV_SCALES=kv_scales.json vllm serve ... --kv-cache-dtype fp6_e2m3 --attention-backend MJOLNIR
141
+ python -m mjolnir.calibrate --config <serve.yaml> --out kv_scales/e2m3.json # in the image
142
+ MJOLNIR_ATTN_KV_SCALES=kv_scales/e2m3.json vllm serve ... --kv-cache-dtype fp6_e2m3 --attention-backend MJOLNIR
143
143
  ```
144
144
 
145
145
  (`mjolnir calibrate` / `mjolnir serve` do both for you —
@@ -157,6 +157,31 @@ How they compare in speed and accuracy:
157
157
  [`quant`](https://github.com/thor-arena/mjolnir/blob/main/docs/kernel/quant.md).
158
158
  Wiring a vLLM cache-write path for fp6 is an open contribution.
159
159
 
160
+ ## Platforms and architectures
161
+
162
+ The kernel is written for one architecture today — Thor (`sm_110`) — but the
163
+ package is organized by platform: `mjolnir/arch/` is the implemented-kernel
164
+ registry (`mjolnir.arch.thor` re-exports the exact runner surface the backend
165
+ consumes), and `mjolnir/platforms.py` is the torch-free leaf that maps a
166
+ compute capability to it. Thor is implemented; DGX Spark (`sm_121`) and Jetson
167
+ Orin (`sm_87`) are declared research — named in the matrix, no kernel yet.
168
+
169
+ `kernel_for((11, 0))` returns `mjolnir.arch.thor`; any other compute
170
+ capability raises `PlatformNotSupported` naming the requested `sm_XX` and the
171
+ shipped platforms. The vLLM backend never sees that exception: its metadata
172
+ builder consults the pure `unsupported_reason()` and takes the normal
173
+ ineligible-config path — that layer's decode steps run on the fallback
174
+ backend, the serve continues. On a host without a usable CUDA device,
175
+ `MJOLNIR_ARCH` (`sm_110` or `11.0`) pins the architecture instead of the
176
+ torch read (the standalone harness).
177
+
178
+ There is no remembered platform state anywhere in the stack: the CLI side
179
+ resolves its platform per invocation (flag > `$MJOLNIR_PLATFORM` > host
180
+ detection — four source classes with provenance, the `/tmp/nv_jetson_model`
181
+ model copy corroboration-only), so the kernel is always resolved from the
182
+ device the run actually happens on. See
183
+ [docs/faq/platforms.md](../../docs/faq/platforms.md).
184
+
160
185
  ## Tests
161
186
 
162
187
  From a checkout, on a Thor:
@@ -185,6 +210,8 @@ is printed as information.
185
210
  | `mjolnir/t5_decode.py` | the CuTe-DSL kernel, the split-KV merge, the harness and the emulator |
186
211
  | `mjolnir/runner.py` | plan buffers, compile cache, launch, KV views (no vLLM import) |
187
212
  | `mjolnir/backend.py` | the vLLM backend, metadata builder, impl |
213
+ | `mjolnir/platforms.py` | arch → kernel resolution (`kernel_for`, `PlatformNotSupported`), `MJOLNIR_ARCH` detection — torch-free |
214
+ | `mjolnir/arch/thor.py` | the Thor (`sm_110`) kernel surface: re-exports of the runner |
188
215
  | `mjolnir/__init__.py` | `register()` — the vLLM plugin entry point |
189
216
 
190
217
  Apache-2.0, except `mjolnir/t5_decode.py`, which is derived from an NVIDIA
@@ -24,7 +24,7 @@ from __future__ import annotations
24
24
  import os
25
25
 
26
26
  __all__ = ["BACKEND_NAME", "BACKEND_PATH", "__version__", "register"]
27
- __version__ = "0.1.1"
27
+ __version__ = "0.2.0"
28
28
 
29
29
  BACKEND_NAME = "MJOLNIR"
30
30
  BACKEND_PATH = "mjolnir.backend.MjolnirAttentionBackend"
@@ -0,0 +1,7 @@
1
+ """Implemented-kernel registry: one subpackage per supported architecture
2
+ (``mjolnir.arch.thor``).
3
+
4
+ ``mjolnir.platforms.kernel_for`` maps a compute capability to a dotted module
5
+ path here. Importing a subpackage pulls the CUTLASS DSL (heavy) — only do it
6
+ when a device is real.
7
+ """
@@ -0,0 +1,42 @@
1
+ """The Thor (sm_110) kernel: re-exports of the exact runner surface the vLLM
2
+ backend consumes. Adding an architecture = adding a sibling subpackage with
3
+ the same surface and a ``_SHIPPED`` row in ``mjolnir.platforms``:
4
+
5
+ * ``Runner`` — the decode-runner class the backend instantiates; the one
6
+ name a family subpackage must define itself (all others are re-exports of
7
+ shared, family-independent machinery from :mod:`mjolnir.runner`);
8
+ * the shared helpers every family consumes unchanged: ``StepPlan``,
9
+ ``SUPPORTED_PAGE_SIZES``, ``detect_kv_layout``, ``env_flag``,
10
+ ``split_kv_cache``.
11
+
12
+ The backend resolves the class through :func:`mjolnir.platforms.load_kernel`
13
+ at build time — it never imports a family module directly, so landing a
14
+ family edits nothing but ``platforms._SHIPPED`` and its own subpackage.
15
+ Importing this module pulls the CUTLASS DSL (heavy): only do it when a
16
+ device is real. The compiled-kernel cache fingerprint
17
+ (``runner._cache_fingerprint``) hashes the kernel module the shared runner
18
+ imports — family-blind until the runner itself is split per family: a
19
+ second family must extend the fingerprint (or key its own cache) or it
20
+ would reuse this family's objects.
21
+ """
22
+
23
+ from mjolnir.runner import (
24
+ SUPPORTED_PAGE_SIZES,
25
+ StepPlan,
26
+ T5DecodeRunner,
27
+ detect_kv_layout,
28
+ env_flag,
29
+ split_kv_cache,
30
+ )
31
+
32
+ Runner = T5DecodeRunner
33
+
34
+ __all__ = [
35
+ "SUPPORTED_PAGE_SIZES",
36
+ "Runner",
37
+ "StepPlan",
38
+ "T5DecodeRunner",
39
+ "detect_kv_layout",
40
+ "env_flag",
41
+ "split_kv_cache",
42
+ ]
@@ -35,20 +35,27 @@ from __future__ import annotations
35
35
 
36
36
  import copy
37
37
  import logging
38
- from typing import Any, ClassVar
38
+ from typing import TYPE_CHECKING, Any, ClassVar
39
39
 
40
40
  import torch
41
41
 
42
42
  from . import banner
43
+ from .platforms import detect, load_kernel, unsupported_reason
43
44
  from .runner import (
44
45
  SUPPORTED_PAGE_SIZES,
45
46
  StepPlan,
46
- T5DecodeRunner,
47
47
  detect_kv_layout,
48
48
  env_flag,
49
49
  split_kv_cache,
50
50
  )
51
51
 
52
+ if TYPE_CHECKING:
53
+ # The annotations name the Thor runner; a second family's runner duck-types
54
+ # the same surface and arrives through its own arch subpackage. At runtime
55
+ # the class is resolved via platforms.load_kernel — never by importing a
56
+ # family module here (see mjolnir.arch).
57
+ from .runner import T5DecodeRunner
58
+
52
59
  logger = logging.getLogger("vllm.mjolnir") # under vLLM's handler
53
60
 
54
61
  # --------------------------------------------------------------------------
@@ -238,6 +245,13 @@ def ineligible_reason(kv_cache_spec, layer_names, vllm_config) -> str | None:
238
245
  return None
239
246
 
240
247
 
248
+ def _unsupported_arch_reason(device) -> str | None:
249
+ """None on a supported compute capability; else the platform registry's
250
+ verdict (torch-free leaf), routed into the same t5=None fallback as every
251
+ ineligible config."""
252
+ return unsupported_reason(torch.cuda.get_device_capability(device))
253
+
254
+
241
255
  # --------------------------------------------------------------------------
242
256
  # fp6 (e2m3) KV: preconditions and per-channel scales
243
257
  # --------------------------------------------------------------------------
@@ -395,6 +409,10 @@ class MjolnirMetadataBuilder(_BaseBuilder): # type: ignore[misc, valid-type]
395
409
  self._fp6 = self._kv_format == "e2m3"
396
410
  self._page = kv_cache_spec.block_size
397
411
  self._t5_reason = ineligible_reason(kv_cache_spec, layer_names, vllm_config)
412
+ if self._t5_reason is None:
413
+ # platform guard (surgical): a GPU without a Mjolnir kernel
414
+ # degrades exactly like any other ineligible config
415
+ self._t5_reason = _unsupported_arch_reason(device)
398
416
  # Diagnosis knobs for the draft path (MTP / EAGLE drafter layers):
399
417
  # MJOLNIR_ATTN_DRAFT_T5=0 runs every drafter step on the fallback;
400
418
  # MJOLNIR_ATTN_FUSED_DRAFT=0 makes vLLM rebuild the draft metadata
@@ -438,7 +456,7 @@ class MjolnirMetadataBuilder(_BaseBuilder): # type: ignore[misc, valid-type]
438
456
  q_mode = {"bf16": "bf16", "bfloat16": "bf16", "fp8": "fp8", "e4m3": "fp8"}[q_mode]
439
457
  if self._fp6 and q_mode != "fp8":
440
458
  raise ValueError("fp6_e2m3 KV runs on the fp8-Q path only: unset MJOLNIR_ATTN_Q_DTYPE")
441
- self.t5 = T5DecodeRunner(
459
+ self.t5 = load_kernel(detect()).Runner(
442
460
  num_heads=num_heads,
443
461
  num_kv_heads=kv_cache_spec.num_kv_heads,
444
462
  head_size=kv_cache_spec.head_size,
@@ -9,14 +9,14 @@ contexts), and the result is written as a ``mjolnir.kv_scales/2`` file —
9
9
  what ``MJOLNIR_ATTN_KV_SCALES`` points ``--kv-cache-dtype fp6_e2m3`` at.
10
10
 
11
11
  python -m mjolnir.calibrate --config /configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR/config.yaml \\
12
- --out /out/kv_scales.json --download-dir /data/models/huggingface
12
+ --out /out/e2m3.json --download-dir /data/models/huggingface
13
13
  python -m mjolnir.calibrate --model Qwen/Qwen3-0.6B --out /tmp/scales.json # small models / tests
14
14
 
15
15
  The scales cover every attention layer that runs on the Mjolnir backend,
16
16
  drafters included (drafting writes its KV too): the built-in MTP head, and an
17
17
  external drafter whose ``speculative-config.attention_backend`` is MJOLNIR
18
18
  (DSpark: its 5 layers come out as layers 64-68). ``mjolnir calibrate`` runs this in the image and writes a config
19
- with its own drafter to ``<config>.kv_scales.json``.
19
+ with its own drafter to ``kv_scales/<method>.kv_scales.json``.
20
20
  """
21
21
 
22
22
  from __future__ import annotations
@@ -0,0 +1,103 @@
1
+ """Kernel-side platform leaf: which architecture is this GPU, and which kernel
2
+ implements it.
3
+
4
+ Import-safe everywhere (torch is imported inside :func:`detect` only, never at
5
+ module level), so the vLLM backend can consult it and any host can read the
6
+ registry without pulling the GPU stack.
7
+
8
+ ``kernel_for`` resolves a compute capability to a dotted module path under
9
+ :mod:`mjolnir.arch`; :func:`load_kernel` imports it — heavy (the CUTLASS DSL
10
+ comes with it), so only call it when a device is real.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ from types import ModuleType
17
+
18
+ # The platforms Mjolnir ships support for: (name, sm, implemented). A row
19
+ # without a kernel (spark / orin) exists so the unsupported-arch error can
20
+ # name the whole matrix; its kernel arrives as mjolnir/arch/<name>.py. The
21
+ # kernel package cannot import the CLI's registry, so the sm rows are
22
+ # mirrored here — the two sides are pinned in sync by
23
+ # tests/cli/test_kernel_platforms.py::test_kernel_and_cli_registries_agree_on_the_sm_rows.
24
+ _SHIPPED: tuple[tuple[str, tuple[int, int], bool], ...] = (
25
+ ("thor", (11, 0), True),
26
+ ("spark", (12, 1), False),
27
+ ("orin", (8, 7), False),
28
+ )
29
+
30
+
31
+ class PlatformNotSupported(RuntimeError):
32
+ """The GPU's compute capability has no Mjolnir kernel."""
33
+
34
+
35
+ def detect() -> tuple[int, int]:
36
+ """The device's compute capability: ``$MJOLNIR_ARCH`` (``sm_110`` or
37
+ ``11.0``) wins, else torch reads it off the CUDA device."""
38
+ raw = os.environ.get("MJOLNIR_ARCH", "").strip()
39
+ if raw:
40
+ return _parse_arch(raw)
41
+ try:
42
+ import torch
43
+ except ImportError as e:
44
+ raise RuntimeError(
45
+ "Mjolnir cannot detect the GPU compute capability (torch is not importable): "
46
+ "set MJOLNIR_ARCH, e.g. MJOLNIR_ARCH=sm_110"
47
+ ) from e
48
+ if not torch.cuda.is_available():
49
+ raise RuntimeError(
50
+ "Mjolnir cannot detect the GPU compute capability (no CUDA device): "
51
+ "set MJOLNIR_ARCH, e.g. MJOLNIR_ARCH=sm_110"
52
+ )
53
+ cap = torch.cuda.get_device_capability()
54
+ return int(cap[0]), int(cap[1])
55
+
56
+
57
+ def _parse_arch(raw: str) -> tuple[int, int]:
58
+ """``sm_110`` / ``11.0`` / ``8.7`` -> (major, minor)."""
59
+ s = raw.strip().lower().removeprefix("sm_")
60
+ if s.isdigit(): # the sm_ form packs the minor into the last digit: sm_110 = 11.0
61
+ return int(s[:-1]), int(s[-1])
62
+ major, sep, minor = s.partition(".")
63
+ if sep and major.isdigit() and minor.isdigit():
64
+ return int(major), int(minor)
65
+ raise RuntimeError(f"MJOLNIR_ARCH={raw!r}: expected 'sm_<major><minor>' (e.g. sm_110) or '<major>.<minor>'")
66
+
67
+
68
+ def kernel_for(cc: tuple[int, int]) -> str:
69
+ """The dotted module path of the kernel implementing ``cc`` — currently
70
+ only ``mjolnir.arch.thor``; anything else raises :class:`PlatformNotSupported`."""
71
+ for name, sm, implemented in _SHIPPED:
72
+ if sm == tuple(cc):
73
+ if implemented:
74
+ return f"mjolnir.arch.{name}"
75
+ break # shipped, but the kernel has not landed: unsupported for now
76
+ raise PlatformNotSupported(_unsupported_msg(cc))
77
+
78
+
79
+ def load_kernel(cc: tuple[int, int]) -> ModuleType:
80
+ """Import and return the kernel module for ``cc`` (heavy: pulls the
81
+ CUTLASS DSL — only call this when a device is real)."""
82
+ import importlib
83
+
84
+ return importlib.import_module(kernel_for(cc))
85
+
86
+
87
+ def unsupported_reason(cc: tuple[int, int]) -> str | None:
88
+ """None when ``cc`` has a kernel; else the reason the vLLM backend routes
89
+ into its ineligible-config degrade path (names the requested sm and the
90
+ fallback that takes over). Pure — no device access."""
91
+ for _name, sm, implemented in _SHIPPED:
92
+ if sm == tuple(cc) and implemented:
93
+ return None
94
+ return _unsupported_msg(cc) + " Decode steps run on the fallback backend."
95
+
96
+
97
+ def _unsupported_msg(cc: tuple[int, int]) -> str:
98
+ impl = [f"{n} (sm_{maj}{minr})" for n, (maj, minr), ok in _SHIPPED if ok]
99
+ research = [f"{n} (sm_{maj}{minr})" for n, (maj, minr), ok in _SHIPPED if not ok]
100
+ msg = f"Mjolnir has no kernel for sm_{cc[0]}{cc[1]} — implemented: {', '.join(impl)}"
101
+ if research:
102
+ msg += f"; research (no kernel yet): {', '.join(research)}"
103
+ return msg
File without changes
File without changes