mjolnir-kernel 0.1.0__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 (27) hide show
  1. {mjolnir_kernel-0.1.0 → mjolnir_kernel-0.2.0}/.gitignore +6 -0
  2. {mjolnir_kernel-0.1.0 → mjolnir_kernel-0.2.0}/PKG-INFO +36 -11
  3. {mjolnir_kernel-0.1.0 → mjolnir_kernel-0.2.0}/README.md +34 -7
  4. {mjolnir_kernel-0.1.0 → mjolnir_kernel-0.2.0}/mjolnir/__init__.py +3 -3
  5. mjolnir_kernel-0.2.0/mjolnir/arch/__init__.py +7 -0
  6. mjolnir_kernel-0.2.0/mjolnir/arch/thor.py +42 -0
  7. {mjolnir_kernel-0.1.0 → mjolnir_kernel-0.2.0}/mjolnir/backend.py +23 -12
  8. {mjolnir_kernel-0.1.0 → mjolnir_kernel-0.2.0}/mjolnir/banner.py +2 -2
  9. {mjolnir_kernel-0.1.0 → mjolnir_kernel-0.2.0}/mjolnir/calibrate.py +4 -3
  10. {mjolnir_kernel-0.1.0 → mjolnir_kernel-0.2.0}/mjolnir/draftvocab.py +2 -3
  11. {mjolnir_kernel-0.1.0 → mjolnir_kernel-0.2.0}/mjolnir/kvfp6.py +6 -6
  12. mjolnir_kernel-0.2.0/mjolnir/lmhead.py +689 -0
  13. mjolnir_kernel-0.2.0/mjolnir/platforms.py +103 -0
  14. mjolnir_kernel-0.2.0/mjolnir/py.typed +0 -0
  15. mjolnir_kernel-0.2.0/mjolnir/quant.py +120 -0
  16. {mjolnir_kernel-0.1.0 → mjolnir_kernel-0.2.0}/mjolnir/runner.py +8 -5
  17. mjolnir_kernel-0.2.0/mjolnir/t5_call.py +560 -0
  18. mjolnir_kernel-0.2.0/mjolnir/t5_const.py +58 -0
  19. {mjolnir_kernel-0.1.0 → mjolnir_kernel-0.2.0}/mjolnir/t5_decode.py +94 -1537
  20. mjolnir_kernel-0.2.0/mjolnir/t5_harness.py +711 -0
  21. mjolnir_kernel-0.2.0/mjolnir/t5_plan.py +197 -0
  22. {mjolnir_kernel-0.1.0 → mjolnir_kernel-0.2.0}/pyproject.toml +1 -3
  23. mjolnir_kernel-0.1.0/mjolnir/lmhead.py +0 -379
  24. {mjolnir_kernel-0.1.0 → mjolnir_kernel-0.2.0}/LICENSE +0 -0
  25. {mjolnir_kernel-0.1.0 → mjolnir_kernel-0.2.0}/NOTICE +0 -0
  26. {mjolnir_kernel-0.1.0 → mjolnir_kernel-0.2.0}/mjolnir/kvscales.py +0 -0
  27. {mjolnir_kernel-0.1.0 → mjolnir_kernel-0.2.0}/mjolnir/qpack.py +0 -0
@@ -3,6 +3,12 @@ __pycache__/
3
3
  .venv/
4
4
  .pytest_cache/
5
5
  .ruff_cache/
6
+ .env
7
+ # coverage artifacts (the CI test stage writes these; they regenerate)
8
+ .coverage
9
+ .coverage.*
10
+ coverage.xml
11
+ .coverage_html/
6
12
  dist/
7
13
  build/
8
14
  *.egg-info/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mjolnir-kernel
3
- Version: 0.1.0
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
@@ -18,12 +18,10 @@ Classifier: Intended Audience :: Developers
18
18
  Classifier: Intended Audience :: Science/Research
19
19
  Classifier: Operating System :: POSIX :: Linux
20
20
  Classifier: Programming Language :: Python :: 3
21
- Classifier: Programming Language :: Python :: 3.10
22
- Classifier: Programming Language :: Python :: 3.11
23
21
  Classifier: Programming Language :: Python :: 3.12
24
22
  Classifier: Programming Language :: Python :: 3.13
25
23
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
26
- Requires-Python: >=3.10
24
+ Requires-Python: >=3.12
27
25
  Provides-Extra: dsl
28
26
  Requires-Dist: nvidia-cutlass-dsl>=4.2; extra == 'dsl'
29
27
  Description-Content-Type: text/markdown
@@ -36,7 +34,7 @@ packaged as a vLLM attention backend.
36
34
 
37
35
  ```bash
38
36
  vllm serve <model> --attention-backend MJOLNIR # the mjolnir-vllm image
39
- vllm serve <model> --attention-backend CUSTOM # stock vLLM 0.31.0 + this package
37
+ vllm serve <model> --attention-backend CUSTOM # stock vLLM 0.32.0 + this package
40
38
  ```
41
39
 
42
40
  This package is the kernel only. Serving, calibration and benchmarking are the
@@ -89,10 +87,10 @@ fused QKV buffer.
89
87
 
90
88
  ## Install
91
89
 
92
- **On the image** (`narandill/mjolnir-vllm:v0.31.0`) it is pre-installed and the
90
+ **On the image** (`narandill/mjolnir-vllm:v0.32.0`) it is pre-installed and the
93
91
  `MJOLNIR` backend name is patched into vLLM — nothing to do.
94
92
 
95
- **On a stock vLLM 0.31.0** (inside a container that has CUDA 13 and `sm_110`):
93
+ **On a stock vLLM 0.32.0** (inside a container that has CUDA 13 and `sm_110`):
96
94
 
97
95
  ```bash
98
96
  pip install "mjolnir-kernel[dsl]" # [dsl] pulls nvidia-cutlass-dsl
@@ -143,8 +141,8 @@ runs on the fallback backend.
143
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 |
144
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 |
145
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 |
146
- | `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) |
147
- | `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) |
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
+ | `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 |
148
146
  | `MJOLNIR_ATTN_VERBOSE` | – | `1`: print the kernel's compile-time notes (ring stages, TMEM columns) — the standalone harness always does |
149
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) |
150
148
  | `MJOLNIR_ATTN_KV_LAYOUT` | auto | force the KV view: `BHN2D BNH2D B2NHD B2HND 2BNHD 2BHND` |
@@ -168,8 +166,8 @@ expands the pages a prefill / mixed step reads to e4m3 for the FA4 fallback
168
166
  (channel scales applied, one descale per head). The scales come from a calibration run on fp8 KV:
169
167
 
170
168
  ```bash
171
- python -m mjolnir.calibrate --config <serve.yaml> --out kv_scales.json # in the image
172
- 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
173
171
  ```
174
172
 
175
173
  (`mjolnir calibrate` / `mjolnir serve` do both for you —
@@ -187,6 +185,31 @@ How they compare in speed and accuracy:
187
185
  [`quant`](https://github.com/thor-arena/mjolnir/blob/main/docs/kernel/quant.md).
188
186
  Wiring a vLLM cache-write path for fp6 is an open contribution.
189
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
+
190
213
  ## Tests
191
214
 
192
215
  From a checkout, on a Thor:
@@ -215,6 +238,8 @@ is printed as information.
215
238
  | `mjolnir/t5_decode.py` | the CuTe-DSL kernel, the split-KV merge, the harness and the emulator |
216
239
  | `mjolnir/runner.py` | plan buffers, compile cache, launch, KV views (no vLLM import) |
217
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 |
218
243
  | `mjolnir/__init__.py` | `register()` — the vLLM plugin entry point |
219
244
 
220
245
  Apache-2.0, except `mjolnir/t5_decode.py`, which is derived from an NVIDIA
@@ -6,7 +6,7 @@ packaged as a vLLM attention backend.
6
6
 
7
7
  ```bash
8
8
  vllm serve <model> --attention-backend MJOLNIR # the mjolnir-vllm image
9
- vllm serve <model> --attention-backend CUSTOM # stock vLLM 0.31.0 + this package
9
+ vllm serve <model> --attention-backend CUSTOM # stock vLLM 0.32.0 + this package
10
10
  ```
11
11
 
12
12
  This package is the kernel only. Serving, calibration and benchmarking are the
@@ -59,10 +59,10 @@ fused QKV buffer.
59
59
 
60
60
  ## Install
61
61
 
62
- **On the image** (`narandill/mjolnir-vllm:v0.31.0`) it is pre-installed and the
62
+ **On the image** (`narandill/mjolnir-vllm:v0.32.0`) it is pre-installed and the
63
63
  `MJOLNIR` backend name is patched into vLLM — nothing to do.
64
64
 
65
- **On a stock vLLM 0.31.0** (inside a container that has CUDA 13 and `sm_110`):
65
+ **On a stock vLLM 0.32.0** (inside a container that has CUDA 13 and `sm_110`):
66
66
 
67
67
  ```bash
68
68
  pip install "mjolnir-kernel[dsl]" # [dsl] pulls nvidia-cutlass-dsl
@@ -113,8 +113,8 @@ 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) |
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) |
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
+ | `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) |
120
120
  | `MJOLNIR_ATTN_KV_LAYOUT` | auto | force the KV view: `BHN2D BNH2D B2NHD B2HND 2BNHD 2BHND` |
@@ -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
@@ -23,8 +23,8 @@ from __future__ import annotations
23
23
 
24
24
  import os
25
25
 
26
- __all__ = ["register", "BACKEND_PATH", "BACKEND_NAME", "__version__"]
27
- __version__ = "0.1.0"
26
+ __all__ = ["BACKEND_NAME", "BACKEND_PATH", "__version__", "register"]
27
+ __version__ = "0.2.0"
28
28
 
29
29
  BACKEND_NAME = "MJOLNIR"
30
30
  BACKEND_PATH = "mjolnir.backend.MjolnirAttentionBackend"
@@ -41,7 +41,7 @@ def _registry():
41
41
  try:
42
42
  from vllm.v1.attention.backends.registry import AttentionBackendEnum, register_backend
43
43
  except ImportError: # older vLLM layout
44
- from vllm.attention.backends.registry import ( # type: ignore
44
+ from vllm.attention.backends.registry import (
45
45
  AttentionBackendEnum,
46
46
  register_backend,
47
47
  )
@@ -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
  # --------------------------------------------------------------------------
@@ -57,12 +64,12 @@ logger = logging.getLogger("vllm.mjolnir") # under vLLM's handler
57
64
  try:
58
65
  from vllm.v1.attention.backends.registry import AttentionBackendEnum
59
66
  except ImportError: # older layout
60
- from vllm.attention.backends.registry import AttentionBackendEnum # type: ignore
67
+ from vllm.attention.backends.registry import AttentionBackendEnum
61
68
 
62
69
  try:
63
70
  from vllm.v1.attention.backend import AttentionCGSupport
64
71
  except ImportError:
65
- from vllm.v1.attention.backends.utils import AttentionCGSupport # type: ignore
72
+ from vllm.v1.attention.backends.utils import AttentionCGSupport
66
73
 
67
74
  FP8_CACHE_DTYPES = ("fp8", "fp8_e4m3")
68
75
  FP6_CACHE_DTYPES = ("fp6_e2m3",)
@@ -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,
@@ -718,13 +736,6 @@ class MjolnirImpl(_BaseImpl): # type: ignore[misc, valid-type]
718
736
  row = runner.kv_row_bytes
719
737
  if self._kv_layout is None:
720
738
  self._kv_layout = detect_kv_layout(kv_cache.shape, runner.h_k, runner.page_size, row)
721
- # logger.info(
722
- # "Mjolnir: KV cache %s %s -> layout %s (%s)",
723
- # tuple(kv_cache.shape),
724
- # tuple(kv_cache.stride()),
725
- # self._kv_layout,
726
- # runner.kv_format,
727
- # )
728
739
  return split_kv_cache(kv_cache, self._kv_layout, row)
729
740
 
730
741
  def _write_kv(self, layer, key, value, kv_cache, k_cache, v_cache, md):
@@ -122,6 +122,6 @@ if __name__ == "__main__": # `python -m mjolnir.banner [--plain]`: preview with
122
122
  color=sys.stdout.isatty() and not plain,
123
123
  model=os.environ.get("MJOLNIR_CONFIG", "").strip() or None,
124
124
  )
125
- print("")
125
+ print()
126
126
  print(msg)
127
- print("")
127
+ print()
@@ -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
@@ -86,7 +86,8 @@ def engine_kwargs_from_config(path: str) -> dict:
86
86
  import yaml
87
87
 
88
88
  parser, engine_args = _engine_parser()
89
- data = yaml.safe_load(open(path)) or {}
89
+ with open(path) as f:
90
+ data = yaml.safe_load(f) or {}
90
91
  known = {k: v for k, v in data.items() if "--" + str(k).replace("_", "-") in parser._option_string_actions}
91
92
  with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) as f:
92
93
  yaml.safe_dump(known, f)
@@ -22,6 +22,7 @@ Special tokens (EOS, the chat / think markers) are always included.
22
22
  from __future__ import annotations
23
23
 
24
24
  import argparse
25
+ import contextlib
25
26
  import json
26
27
  import os
27
28
  import sys
@@ -116,10 +117,8 @@ def main(argv=None) -> int:
116
117
  return 1
117
118
  tok = llm.get_tokenizer()
118
119
  special = set(getattr(tok, "all_special_ids", []) or [])
119
- try:
120
+ with contextlib.suppress(Exception):
120
121
  special |= set(tok.get_added_vocab().values())
121
- except Exception: # noqa: BLE001
122
- pass
123
122
  vocab = len(tok)
124
123
  ranked = rank(counts, special, vocab)
125
124
  coverage = held_out_coverage(halves[0], halves[1], special, vocab)
@@ -47,7 +47,7 @@ def e2m3_values(device) -> torch.Tensor:
47
47
  """(64,) fp32: the value of every e2m3 code."""
48
48
  key = str(device)
49
49
  if key not in _LUTF:
50
- from .t5_decode import minifloat_table
50
+ from .quant import minifloat_table
51
51
 
52
52
  _LUTF[key] = minifloat_table("e2m3").to(device)
53
53
  return _LUTF[key]
@@ -57,7 +57,7 @@ def e2m3_to_e4m3_lut(device) -> torch.Tensor:
57
57
  """(64,) uint8: e4m3fn byte of every e2m3 code (exact)."""
58
58
  key = str(device)
59
59
  if key not in _LUT:
60
- from .t5_decode import minifloat_table
60
+ from .quant import minifloat_table
61
61
 
62
62
  vals = minifloat_table("e2m3") # 64 float values, code order
63
63
  e4 = vals.to(torch.float8_e4m3fn)
@@ -229,7 +229,7 @@ def write_kv(
229
229
  return
230
230
  _, h_k, d = key.shape
231
231
  _, H, N, C = kv_cache.shape
232
- assert H == h_k and C == 2 * row_bytes(d) and kv_cache.stride(3) == 1 and kv_cache.dtype == torch.uint8
232
+ assert h_k == H and 2 * row_bytes(d) == C and kv_cache.stride(3) == 1 and kv_cache.dtype == torch.uint8
233
233
  assert key.stride(2) == 1 and value.stride(2) == 1
234
234
  _write_kv_kernel[(T, h_k)](
235
235
  key,
@@ -271,7 +271,7 @@ def expand_pages_to_fp8(
271
271
  P = src_blocks.shape[0]
272
272
  _, H, N, C = kv_cache.shape
273
273
  d = head_size
274
- assert C == 2 * row_bytes(d) and out.shape[1:] == (H, N, 2 * d) and out.stride(3) == 1
274
+ assert 2 * row_bytes(d) == C and out.shape[1:] == (H, N, 2 * d) and out.stride(3) == 1
275
275
  if P == 0:
276
276
  return out[:0]
277
277
  _expand_kernel[(P, H, N)](
@@ -303,7 +303,7 @@ def expand_pages_to_fp8(
303
303
  def quantize_rows_reference(x: torch.Tensor, scale: torch.Tensor) -> torch.Tensor:
304
304
  """(..., h_k, d) float -> packed bytes (..., h_k, row_bytes(d)) uint8;
305
305
  scale per tensor, per KV head or (h_k, d) per channel."""
306
- from .t5_decode import minifloat_quantize, pack_subbyte
306
+ from .quant import minifloat_quantize, pack_subbyte
307
307
 
308
308
  h_k, d = x.shape[-2], x.shape[-1]
309
309
  s = scale.float().cpu()
@@ -318,7 +318,7 @@ def quantize_rows_reference(x: torch.Tensor, scale: torch.Tensor) -> torch.Tenso
318
318
 
319
319
  def decode_rows_reference(packed: torch.Tensor, d: int) -> torch.Tensor:
320
320
  """packed (..., row_bytes) uint8 -> e2m3 code values (..., d) float."""
321
- from .t5_decode import minifloat_table
321
+ from .quant import minifloat_table
322
322
 
323
323
  b = packed.to(torch.int64).cpu().reshape(*packed.shape[:-1], d // 4, 3)
324
324
  w = b[..., 0] | (b[..., 1] << 8) | (b[..., 2] << 16)