mjolnir-kernel 0.1.0__py3-none-any.whl
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.
- mjolnir/__init__.py +76 -0
- mjolnir/backend.py +934 -0
- mjolnir/banner.py +127 -0
- mjolnir/calibrate.py +182 -0
- mjolnir/draftvocab.py +150 -0
- mjolnir/kvfp6.py +326 -0
- mjolnir/kvscales.py +162 -0
- mjolnir/lmhead.py +379 -0
- mjolnir/qpack.py +132 -0
- mjolnir/runner.py +754 -0
- mjolnir/t5_decode.py +2990 -0
- mjolnir_kernel-0.1.0.dist-info/METADATA +222 -0
- mjolnir_kernel-0.1.0.dist-info/RECORD +17 -0
- mjolnir_kernel-0.1.0.dist-info/WHEEL +4 -0
- mjolnir_kernel-0.1.0.dist-info/entry_points.txt +2 -0
- mjolnir_kernel-0.1.0.dist-info/licenses/LICENSE +202 -0
- mjolnir_kernel-0.1.0.dist-info/licenses/NOTICE +64 -0
mjolnir/backend.py
ADDED
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
"""vLLM attention backend: the Mjolnir (T5) fp8 paged decode on Jetson Thor (sm_110).
|
|
2
|
+
|
|
3
|
+
Design
|
|
4
|
+
------
|
|
5
|
+
MjolnirAttentionBackend is a *subclass of a stock backend*, the "fallback":
|
|
6
|
+
FlashAttention 4 on the Mjolnir image (its prefill is ~40% faster than
|
|
7
|
+
FlashInfer's on Thor), FlashInfer on a stock vLLM (whose FA on sm_110 cannot
|
|
8
|
+
read an fp8 KV cache); MJOLNIR_ATTN_FALLBACK picks another. It inherits the
|
|
9
|
+
KV-cache layout, the KV-cache write, prefill, validation and everything else.
|
|
10
|
+
Only two things change:
|
|
11
|
+
|
|
12
|
+
* the metadata builder: a step whose batch is decode-only (uniform
|
|
13
|
+
q_len <= 1 + num_speculative_tokens, causal or a parallel drafter's
|
|
14
|
+
non-causal block, no cascade) gets a T5 plan
|
|
15
|
+
(occupancy-pinned split-KV, written into persistent buffers, padded to a
|
|
16
|
+
fixed grid -> FULL CUDA graphs replay it). Any other step is built by
|
|
17
|
+
the base builder and run by the base impl, unchanged.
|
|
18
|
+
* the impl: forward() runs the T5 kernel when it gets T5 metadata,
|
|
19
|
+
otherwise it calls the base forward.
|
|
20
|
+
|
|
21
|
+
KV formats: fp8 (e4m3, vLLM's own cache write), and ``fp6_e2m3`` on the
|
|
22
|
+
image (``thor-fp6-e2m3-kv-cache.patch`` adds the dtype): packed 6-bit codes
|
|
23
|
+
with static per-(layer, KV head, channel) scales from a calibration file
|
|
24
|
+
(``mjolnir.kvscales``). Mjolnir writes that cache itself (``mjolnir.kvfp6``),
|
|
25
|
+
T5 reads it directly (the channel scales folded into Q and the output), and
|
|
26
|
+
the steps the FlashAttention 4 fallback runs see their pages expanded to an
|
|
27
|
+
fp8 scratch first (channel scales applied, one descale per head).
|
|
28
|
+
|
|
29
|
+
Select it with `--attention-backend MJOLNIR` (the Mjolnir image's vLLM has
|
|
30
|
+
the enum member) or `--attention-backend CUSTOM` (stock vLLM); the
|
|
31
|
+
`vllm.general_plugins` entry point registers the class at vLLM start-up.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import copy
|
|
37
|
+
import logging
|
|
38
|
+
from typing import Any, ClassVar
|
|
39
|
+
|
|
40
|
+
import torch
|
|
41
|
+
|
|
42
|
+
from . import banner
|
|
43
|
+
from .runner import (
|
|
44
|
+
SUPPORTED_PAGE_SIZES,
|
|
45
|
+
StepPlan,
|
|
46
|
+
T5DecodeRunner,
|
|
47
|
+
detect_kv_layout,
|
|
48
|
+
env_flag,
|
|
49
|
+
split_kv_cache,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger("vllm.mjolnir") # under vLLM's handler
|
|
53
|
+
|
|
54
|
+
# --------------------------------------------------------------------------
|
|
55
|
+
# vLLM imports (paths moved between releases; try both)
|
|
56
|
+
# --------------------------------------------------------------------------
|
|
57
|
+
try:
|
|
58
|
+
from vllm.v1.attention.backends.registry import AttentionBackendEnum
|
|
59
|
+
except ImportError: # older layout
|
|
60
|
+
from vllm.attention.backends.registry import AttentionBackendEnum # type: ignore
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
from vllm.v1.attention.backend import AttentionCGSupport
|
|
64
|
+
except ImportError:
|
|
65
|
+
from vllm.v1.attention.backends.utils import AttentionCGSupport # type: ignore
|
|
66
|
+
|
|
67
|
+
FP8_CACHE_DTYPES = ("fp8", "fp8_e4m3")
|
|
68
|
+
FP6_CACHE_DTYPES = ("fp6_e2m3",)
|
|
69
|
+
MJOLNIR_CACHE_DTYPES = FP8_CACHE_DTYPES + FP6_CACHE_DTYPES
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _kv_format(cache_dtype: str) -> str:
|
|
73
|
+
return "e2m3" if str(cache_dtype) in FP6_CACHE_DTYPES else "fp8"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Builder-level step counters (per process): smoke tests read these to make
|
|
77
|
+
# sure decode steps really ran on T5.
|
|
78
|
+
STATS = {"t5_steps": 0, "fallback_steps": 0, "t5_noncausal_steps": 0}
|
|
79
|
+
# Parallel drafters' non-causal block attention on T5 (MJOLNIR_ATTN_NONCAUSAL=0:
|
|
80
|
+
# those steps go to the fallback, as before)
|
|
81
|
+
_NONCAUSAL = env_flag("MJOLNIR_ATTN_NONCAUSAL", "1").lower() not in ("0", "false", "no")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _default_fallback() -> tuple[str, bool]:
|
|
85
|
+
"""(backend name, explicitly chosen?). The Mjolnir image's vLLM carries
|
|
86
|
+
the default (``fa_utils.mjolnir_fallback_name``: FLASH_ATTN, which the same
|
|
87
|
+
patch resolves to FA4 on sm_110); a stock vLLM gets FLASHINFER."""
|
|
88
|
+
explicit = (env_flag("MJOLNIR_ATTN_FALLBACK", "") or "").strip().upper()
|
|
89
|
+
if explicit:
|
|
90
|
+
return explicit, True
|
|
91
|
+
try:
|
|
92
|
+
from vllm.v1.attention.backends.fa_utils import mjolnir_fallback_name
|
|
93
|
+
except ImportError: # stock vLLM: no Thor FA4 FP8-KV support
|
|
94
|
+
return "FLASHINFER", False
|
|
95
|
+
return mjolnir_fallback_name(), False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _fa4_serves_fp8_kv() -> bool | None:
|
|
99
|
+
"""Can FlashAttention serve this model's (fp8) KV cache here? None when
|
|
100
|
+
there is no config to ask (import outside vLLM's model construction)."""
|
|
101
|
+
try:
|
|
102
|
+
from vllm.config import get_current_vllm_config_or_none
|
|
103
|
+
from vllm.v1.attention.backends.fa_utils import flash_attn_supports_kv_cache_dtype
|
|
104
|
+
except ImportError:
|
|
105
|
+
return None
|
|
106
|
+
cfg = get_current_vllm_config_or_none()
|
|
107
|
+
if cfg is None or cfg.model_config is None:
|
|
108
|
+
return None
|
|
109
|
+
cache_dtype = str(getattr(cfg.cache_config, "cache_dtype", "auto"))
|
|
110
|
+
if cache_dtype not in FP8_CACHE_DTYPES:
|
|
111
|
+
return True # nothing to gate: FA reads bf16/fp16 KV on any version
|
|
112
|
+
head = cfg.model_config.get_head_size()
|
|
113
|
+
return bool(flash_attn_supports_kv_cache_dtype(cache_dtype, head_size=head, supports_fa4_hd256=True))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _resolve_fallback() -> str:
|
|
117
|
+
name, explicit = _default_fallback()
|
|
118
|
+
assert name not in ("CUSTOM", "MJOLNIR"), "MJOLNIR_ATTN_FALLBACK must name a stock backend"
|
|
119
|
+
if name == "FLASH_ATTN" and not explicit and _fa4_serves_fp8_kv() is False:
|
|
120
|
+
# The image default, but FA4's one-shot FP8-KV probe failed on this
|
|
121
|
+
# device (or the layer shape is not FA4's): keep serving on FlashInfer.
|
|
122
|
+
logger.warning(
|
|
123
|
+
"Mjolnir: FlashAttention 4 cannot serve this fp8 KV cache here "
|
|
124
|
+
"(see the 'THOR FA4 ... probe' log line); falling back to FLASHINFER"
|
|
125
|
+
)
|
|
126
|
+
return "FLASHINFER"
|
|
127
|
+
return name
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
_FALLBACK = _resolve_fallback()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _fallback_name() -> str:
|
|
134
|
+
return _FALLBACK
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _base_backend():
|
|
138
|
+
return AttentionBackendEnum[_FALLBACK].get_class()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
_Base = _base_backend()
|
|
142
|
+
_BaseBuilder = _Base.get_builder_cls()
|
|
143
|
+
_BaseImpl = _Base.get_impl_cls()
|
|
144
|
+
_BASE_WRITES_KV_IN_FORWARD = getattr(_Base, "forward_includes_kv_cache_update", True)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _fp6_known_to_vllm() -> bool:
|
|
148
|
+
"""Does this vLLM have the fp6_e2m3 cache dtype (the image's patch)?"""
|
|
149
|
+
try:
|
|
150
|
+
from vllm.v1.kv_cache_interface import KVQuantMode, get_kv_quant_mode
|
|
151
|
+
|
|
152
|
+
return get_kv_quant_mode("fp6_e2m3") == getattr(KVQuantMode, "FP6_E2M3", None)
|
|
153
|
+
except ImportError:
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _block_size_ok(sizes, size: int) -> bool:
|
|
158
|
+
"""Does a backend's supported-size list (ints and MultipleOf) take ``size``?"""
|
|
159
|
+
for s in sizes:
|
|
160
|
+
base = getattr(s, "base", None)
|
|
161
|
+
if (base is not None and size % base == 0) or s == size:
|
|
162
|
+
return True
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _max_q_len(vllm_config) -> int:
|
|
167
|
+
spec = getattr(vllm_config, "speculative_config", None)
|
|
168
|
+
n = getattr(spec, "num_speculative_tokens", None) if spec is not None else None
|
|
169
|
+
cap = int(env_flag("MJOLNIR_ATTN_MAX_QLEN", "0") or 0)
|
|
170
|
+
q = 1 + int(n or 0)
|
|
171
|
+
return min(q, cap) if cap > 0 else q
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _gpu_sync_allowed():
|
|
175
|
+
try:
|
|
176
|
+
from vllm.utils.gpu_sync_debug import gpu_sync_allowed
|
|
177
|
+
|
|
178
|
+
return gpu_sync_allowed()
|
|
179
|
+
except ImportError:
|
|
180
|
+
import contextlib
|
|
181
|
+
|
|
182
|
+
return contextlib.nullcontext()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# --------------------------------------------------------------------------
|
|
186
|
+
# Static eligibility (config level). Returns None if OK, else the reason.
|
|
187
|
+
# --------------------------------------------------------------------------
|
|
188
|
+
def _layers(vllm_config, layer_names):
|
|
189
|
+
try:
|
|
190
|
+
from vllm.config import get_layers_from_vllm_config
|
|
191
|
+
from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
|
|
192
|
+
|
|
193
|
+
return get_layers_from_vllm_config(vllm_config, AttentionLayerBase, layer_names)
|
|
194
|
+
except Exception: # noqa: BLE001 - layout differs across vLLM versions
|
|
195
|
+
return {}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _layer_reason(impl) -> str | None:
|
|
199
|
+
sw = getattr(impl, "sliding_window", None)
|
|
200
|
+
if sw not in (None, (-1, -1), -1):
|
|
201
|
+
return "sliding window"
|
|
202
|
+
if getattr(impl, "logits_soft_cap", None) not in (None, 0, 0.0):
|
|
203
|
+
return "logits soft-cap"
|
|
204
|
+
if getattr(impl, "sinks", None) is not None:
|
|
205
|
+
return "attention sinks"
|
|
206
|
+
if getattr(impl, "alibi_slopes", None) is not None:
|
|
207
|
+
return "ALiBi"
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def ineligible_reason(kv_cache_spec, layer_names, vllm_config) -> str | None:
|
|
212
|
+
if env_flag("MJOLNIR_ATTN_DISABLE") in ("1", "true", "yes"):
|
|
213
|
+
return "MJOLNIR_ATTN_DISABLE set"
|
|
214
|
+
cache_dtype = str(getattr(vllm_config.cache_config, "cache_dtype", "auto"))
|
|
215
|
+
if cache_dtype not in MJOLNIR_CACHE_DTYPES:
|
|
216
|
+
return f"kv cache dtype {cache_dtype!r} (T5 needs fp8_e4m3 or fp6_e2m3)"
|
|
217
|
+
head = getattr(kv_cache_spec, "head_size", None)
|
|
218
|
+
if head is None or head % 128:
|
|
219
|
+
return f"head_size {head} (needs a multiple of 128)"
|
|
220
|
+
if getattr(kv_cache_spec, "head_size_v", head) != head:
|
|
221
|
+
return "K/V head sizes differ"
|
|
222
|
+
if getattr(kv_cache_spec, "sliding_window", None):
|
|
223
|
+
return "sliding-window KV spec"
|
|
224
|
+
bs = getattr(kv_cache_spec, "block_size", None)
|
|
225
|
+
if bs not in SUPPORTED_PAGE_SIZES:
|
|
226
|
+
return f"block size {bs} (supported: {SUPPORTED_PAGE_SIZES})"
|
|
227
|
+
if vllm_config.model_config.dtype not in (torch.bfloat16, torch.float16):
|
|
228
|
+
return f"model dtype {vllm_config.model_config.dtype}"
|
|
229
|
+
pc = vllm_config.parallel_config
|
|
230
|
+
if getattr(pc, "decode_context_parallel_size", 1) > 1:
|
|
231
|
+
return "decode context parallel"
|
|
232
|
+
if getattr(pc, "prefill_context_parallel_size", 1) > 1:
|
|
233
|
+
return "prefill context parallel"
|
|
234
|
+
for name, layer in _layers(vllm_config, layer_names).items():
|
|
235
|
+
r = _layer_reason(getattr(layer, "impl", None))
|
|
236
|
+
if r:
|
|
237
|
+
return f"layer {name}: {r}"
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# --------------------------------------------------------------------------
|
|
242
|
+
# fp6 (e2m3) KV: preconditions and per-channel scales
|
|
243
|
+
# --------------------------------------------------------------------------
|
|
244
|
+
def _check_fp6(t5_reason, layer_names) -> None:
|
|
245
|
+
"""fp6 caches have exactly two readers: T5 and the fallback's fp8 scratch."""
|
|
246
|
+
if t5_reason is not None:
|
|
247
|
+
raise ValueError(
|
|
248
|
+
f"kv cache dtype fp6_e2m3 needs the Mjolnir decode kernel, but it is off for "
|
|
249
|
+
f"{layer_names[:1]}: {t5_reason}. Serve fp8 KV for this model."
|
|
250
|
+
)
|
|
251
|
+
if _FALLBACK != "FLASH_ATTN":
|
|
252
|
+
raise ValueError(
|
|
253
|
+
f"kv cache dtype fp6_e2m3 needs the FLASH_ATTN fallback (prefill reads an fp8 "
|
|
254
|
+
f"scratch through it); MJOLNIR_ATTN_FALLBACK is {_FALLBACK}"
|
|
255
|
+
)
|
|
256
|
+
from . import kvscales
|
|
257
|
+
|
|
258
|
+
if kvscales.calibrating():
|
|
259
|
+
raise ValueError("calibrate on fp8 KV (MJOLNIR_ATTN_KV_CALIBRATE), then serve fp6_e2m3 with the file")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
_FP6_SCRATCH: dict = {}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _fp6_scratch(pages: int, H: int, N: int, row: int, device) -> torch.Tensor:
|
|
266
|
+
"""A growing per-device (pages, H, N, 2d) uint8 buffer for the fallback's
|
|
267
|
+
e4m3 pages. Layers run one after another, so one buffer serves them all;
|
|
268
|
+
it lives outside vLLM's KV budget (a 200K-token context at H=4, d=256 is
|
|
269
|
+
~0.4 GB) and grows to the largest step seen."""
|
|
270
|
+
key = (str(device), H, N, row)
|
|
271
|
+
buf = _FP6_SCRATCH.get(key)
|
|
272
|
+
need = max(1, pages)
|
|
273
|
+
if buf is None or buf.shape[0] < need:
|
|
274
|
+
cap = max(need, 2 * buf.shape[0] if buf is not None else need)
|
|
275
|
+
buf = torch.empty(cap, H, N, row, dtype=torch.uint8, device=device)
|
|
276
|
+
_FP6_SCRATCH[key] = buf
|
|
277
|
+
return buf
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _set_scale(layer, name: str, value: torch.Tensor) -> None:
|
|
281
|
+
"""Replace a layer's k/v scale with ``value`` (any shape), keeping its
|
|
282
|
+
kind: quantized checkpoints (e.g. ModelOpt NVFP4) register the scales as
|
|
283
|
+
nn.Parameters, which a plain tensor cannot be assigned over."""
|
|
284
|
+
cur = getattr(layer, name, None)
|
|
285
|
+
if isinstance(cur, torch.nn.Parameter):
|
|
286
|
+
setattr(layer, name, torch.nn.Parameter(value, requires_grad=False))
|
|
287
|
+
elif name in getattr(layer, "_buffers", {}):
|
|
288
|
+
layer._buffers[name] = value
|
|
289
|
+
else:
|
|
290
|
+
setattr(layer, name, value)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _install_fp6_scales(vllm_config, layer_names, h_k: int, d: int) -> None:
|
|
294
|
+
"""Per-(layer, KV head, channel) e2m3 scales from the calibration file.
|
|
295
|
+
|
|
296
|
+
Channel scales s_c (h_k, d) quantize the cache write. Attention never
|
|
297
|
+
sees them as descales — they fold into the math around it:
|
|
298
|
+
|
|
299
|
+
* T5: K's into Q (q . k = sum_c (q_c s_c) code_c) and V's into the output
|
|
300
|
+
(O_c = s_c sum_t p_t code_tc); the kernel's K/V descales are 1.
|
|
301
|
+
* FA4 fallback: the scratch holds e4m3(code * s_c / S) with one
|
|
302
|
+
S = max_c(s_c) * 7.5 / 448 per head, and S is the descale — so
|
|
303
|
+
``layer._k_scale`` / ``_v_scale`` are the per-head S.
|
|
304
|
+
"""
|
|
305
|
+
from . import kvscales
|
|
306
|
+
|
|
307
|
+
path = kvscales.scales_path()
|
|
308
|
+
if not path:
|
|
309
|
+
raise ValueError(
|
|
310
|
+
"kv cache dtype fp6_e2m3 needs calibrated scales: set MJOLNIR_ATTN_KV_SCALES to a "
|
|
311
|
+
"calibration file (`mjolnir calibrate`, or python -m mjolnir.calibrate)"
|
|
312
|
+
)
|
|
313
|
+
doc = kvscales.load(path)
|
|
314
|
+
missing = kvscales.missing_layers(doc, layer_names)
|
|
315
|
+
if missing:
|
|
316
|
+
raise ValueError(f"{path} has no scales for {missing[:3]}{' ...' if len(missing) > 3 else ''}")
|
|
317
|
+
layers = _layers(vllm_config, layer_names)
|
|
318
|
+
for name, layer in layers.items():
|
|
319
|
+
ks, vs = kvscales.e2m3_scales(doc, name, h_k, d)
|
|
320
|
+
dev = layer._k_scale.device
|
|
321
|
+
h_r = max(1, int(getattr(layer, "num_heads", h_k)) // h_k)
|
|
322
|
+
state = {}
|
|
323
|
+
for side, rows in (("k", ks), ("v", vs)):
|
|
324
|
+
ch = torch.tensor(rows, dtype=torch.float32, device=dev).contiguous() # (h_k, d)
|
|
325
|
+
head = ch.amax(dim=1) * (kvscales.E2M3_MAX / 448.0) # (h_k,) fallback descale
|
|
326
|
+
state[f"{side}_ch"] = ch
|
|
327
|
+
state[f"{side}_head"] = head
|
|
328
|
+
state[f"{side}_ratio"] = (ch / head[:, None]).contiguous()
|
|
329
|
+
state[f"{side}_mul"] = ch.repeat_interleave(h_r, dim=0).contiguous() # (h_q, d)
|
|
330
|
+
state["one"] = torch.ones(1, dtype=torch.float32, device=dev)
|
|
331
|
+
layer._mjolnir_fp6 = state
|
|
332
|
+
_set_scale(layer, "_k_scale", state["k_head"])
|
|
333
|
+
_set_scale(layer, "_v_scale", state["v_head"])
|
|
334
|
+
logger.info(
|
|
335
|
+
"Mjolnir: fp6_e2m3 KV scales (%s) for %d layer(s) from %s",
|
|
336
|
+
"per channel" if any(kvscales.per_channel(doc, n) for n in layers) else "per head",
|
|
337
|
+
len(layers),
|
|
338
|
+
path,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# --------------------------------------------------------------------------
|
|
343
|
+
# Metadata
|
|
344
|
+
# --------------------------------------------------------------------------
|
|
345
|
+
class MjolnirMetadata:
|
|
346
|
+
"""Decode-only step metadata for T5 (one per KV-cache group).
|
|
347
|
+
|
|
348
|
+
Carries the attributes generic vLLM code reads from attention metadata
|
|
349
|
+
(num_actual_tokens, slot_mapping, ...); FlashInfer-specific fields are
|
|
350
|
+
absent because the base impl never sees this object."""
|
|
351
|
+
|
|
352
|
+
is_t5 = True
|
|
353
|
+
|
|
354
|
+
def __init__(self, *, plan: StepPlan, runner: T5DecodeRunner, cam):
|
|
355
|
+
self.plan = plan
|
|
356
|
+
self.runner = runner
|
|
357
|
+
self.num_actual_tokens = cam.num_actual_tokens
|
|
358
|
+
self.num_reqs = cam.num_reqs
|
|
359
|
+
self.max_query_len = cam.max_query_len
|
|
360
|
+
self.max_seq_len = cam.max_seq_len
|
|
361
|
+
self.query_start_loc = cam.query_start_loc
|
|
362
|
+
self.seq_lens = cam.seq_lens
|
|
363
|
+
self.block_table = cam.block_table_tensor
|
|
364
|
+
self.slot_mapping = cam.slot_mapping
|
|
365
|
+
self.num_decodes = cam.num_reqs
|
|
366
|
+
self.num_decode_tokens = cam.num_actual_tokens
|
|
367
|
+
self.num_prefills = 0
|
|
368
|
+
self.num_prefill_tokens = 0
|
|
369
|
+
self.use_cascade = False
|
|
370
|
+
self.causal = True
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# --------------------------------------------------------------------------
|
|
374
|
+
# Builder
|
|
375
|
+
# --------------------------------------------------------------------------
|
|
376
|
+
class MjolnirMetadataBuilder(_BaseBuilder): # type: ignore[misc, valid-type]
|
|
377
|
+
# Shared-metadata shortcut across groups would bypass our plan.
|
|
378
|
+
supports_update_block_table: bool = False
|
|
379
|
+
|
|
380
|
+
def __init__(self, kv_cache_spec, layer_names, vllm_config, device, *args, **kwargs):
|
|
381
|
+
super().__init__(kv_cache_spec, layer_names, vllm_config, device, *args, **kwargs)
|
|
382
|
+
if env_flag("MJOLNIR_ATTN_BANNER", "1").lower() not in ("0", "false", "no"):
|
|
383
|
+
banner.emit() # the hammer, once, into this log
|
|
384
|
+
# Fused multi-step draft decode (MTP / EAGLE): the speculator builds the
|
|
385
|
+
# draft metadata once and runs every draft step on it, advancing the
|
|
386
|
+
# device seq_lens in place. A T5 plan stays valid across those steps if
|
|
387
|
+
# it is made for the longest of them — the kernel masks with the live
|
|
388
|
+
# seq_lens, so tiles past a sequence's end are empty — hence every plan
|
|
389
|
+
# gets num_speculative_tokens of headroom, and update_draft_decode_metadata
|
|
390
|
+
# has nothing to do for T5 steps (fallback steps defer to the base
|
|
391
|
+
# builder). The capability is whatever the base builder grants.
|
|
392
|
+
spec = getattr(vllm_config, "speculative_config", None)
|
|
393
|
+
self._plan_margin = int(getattr(spec, "num_speculative_tokens", 0) or 0) if spec is not None else 0
|
|
394
|
+
self._kv_format = _kv_format(getattr(vllm_config.cache_config, "cache_dtype", "auto"))
|
|
395
|
+
self._fp6 = self._kv_format == "e2m3"
|
|
396
|
+
self._page = kv_cache_spec.block_size
|
|
397
|
+
self._t5_reason = ineligible_reason(kv_cache_spec, layer_names, vllm_config)
|
|
398
|
+
# Diagnosis knobs for the draft path (MTP / EAGLE drafter layers):
|
|
399
|
+
# MJOLNIR_ATTN_DRAFT_T5=0 runs every drafter step on the fallback;
|
|
400
|
+
# MJOLNIR_ATTN_FUSED_DRAFT=0 makes vLLM rebuild the draft metadata
|
|
401
|
+
# between draft steps instead of fusing them into one graph.
|
|
402
|
+
self._is_draft = any("mtp" in n or "draft" in n for n in layer_names)
|
|
403
|
+
self._force_fallback = self._is_draft and env_flag("MJOLNIR_ATTN_DRAFT_T5", "1") in ("0", "false", "no")
|
|
404
|
+
if env_flag("MJOLNIR_ATTN_FUSED_DRAFT", "1") in ("0", "false", "no"):
|
|
405
|
+
self.supports_draft_decode_metadata_update = False
|
|
406
|
+
if self._force_fallback and self._fp6:
|
|
407
|
+
# the drafter's steps replay as FULL CUDA graphs, and an fp6 fallback
|
|
408
|
+
# step (page expansion from a per-step page map) cannot be captured
|
|
409
|
+
raise ValueError(
|
|
410
|
+
"MJOLNIR_ATTN_DRAFT_T5=0 needs fp8 KV (e.g. NVFP4_MJOLNIR): on fp6 the drafter's "
|
|
411
|
+
"fallback steps would be captured into CUDA graphs with a stale page map"
|
|
412
|
+
)
|
|
413
|
+
if self._force_fallback:
|
|
414
|
+
logger.info("Mjolnir: drafter layers %s on the fallback (MJOLNIR_ATTN_DRAFT_T5=0)", layer_names[:1])
|
|
415
|
+
if self._fp6:
|
|
416
|
+
_check_fp6(self._t5_reason, layer_names)
|
|
417
|
+
_install_fp6_scales(vllm_config, layer_names, kv_cache_spec.num_kv_heads, kv_cache_spec.head_size)
|
|
418
|
+
self.t5: T5DecodeRunner | None = None
|
|
419
|
+
if self._t5_reason is not None:
|
|
420
|
+
logger.warning(
|
|
421
|
+
"Mjolnir decode disabled for %s: %s -> %s for all steps",
|
|
422
|
+
layer_names[:1],
|
|
423
|
+
self._t5_reason,
|
|
424
|
+
_fallback_name(),
|
|
425
|
+
)
|
|
426
|
+
return
|
|
427
|
+
mc, pc = vllm_config.model_config, vllm_config.parallel_config
|
|
428
|
+
num_heads = None
|
|
429
|
+
try:
|
|
430
|
+
from vllm.v1.attention.backends.utils import get_num_attention_heads_from_layers
|
|
431
|
+
|
|
432
|
+
num_heads = get_num_attention_heads_from_layers(vllm_config, layer_names)
|
|
433
|
+
except Exception: # noqa: BLE001
|
|
434
|
+
pass
|
|
435
|
+
if num_heads is None:
|
|
436
|
+
num_heads = mc.get_num_attention_heads(pc)
|
|
437
|
+
q_mode = env_flag("MJOLNIR_ATTN_Q_DTYPE", "fp8").lower()
|
|
438
|
+
q_mode = {"bf16": "bf16", "bfloat16": "bf16", "fp8": "fp8", "e4m3": "fp8"}[q_mode]
|
|
439
|
+
if self._fp6 and q_mode != "fp8":
|
|
440
|
+
raise ValueError("fp6_e2m3 KV runs on the fp8-Q path only: unset MJOLNIR_ATTN_Q_DTYPE")
|
|
441
|
+
self.t5 = T5DecodeRunner(
|
|
442
|
+
num_heads=num_heads,
|
|
443
|
+
num_kv_heads=kv_cache_spec.num_kv_heads,
|
|
444
|
+
head_size=kv_cache_spec.head_size,
|
|
445
|
+
page_size=kv_cache_spec.block_size,
|
|
446
|
+
out_dtype=mc.dtype,
|
|
447
|
+
max_num_seqs=vllm_config.scheduler_config.max_num_seqs,
|
|
448
|
+
max_q_len=_max_q_len(vllm_config),
|
|
449
|
+
q_mode=q_mode,
|
|
450
|
+
ctas_per_sm=int(env_flag("MJOLNIR_ATTN_CTAS", "2")),
|
|
451
|
+
max_splits=int(env_flag("MJOLNIR_ATTN_MAX_SPLITS", "16")),
|
|
452
|
+
kv_stages=int(env_flag("MJOLNIR_ATTN_KV_STAGES", "0")),
|
|
453
|
+
# EVICT_FIRST: default ON for the fully-fp8 path — the one change
|
|
454
|
+
# that won in every Thor A/B (b=1 wall clock -8..-29% at 4K-16K,
|
|
455
|
+
# ncu neutral at 64K-256K); bf16-Q long-L was mixed -> opt-in.
|
|
456
|
+
# PDL: opt-in — its A/Bs flipped sign between runs (-17% .. +5%)
|
|
457
|
+
# on a busy box; re-measure on an idle one before defaulting it.
|
|
458
|
+
pdl=env_flag("MJOLNIR_ATTN_PDL", "0").lower() in ("1", "true", "yes"),
|
|
459
|
+
l2_prefetch=int(env_flag("MJOLNIR_ATTN_L2_PREFETCH", "0")),
|
|
460
|
+
kv_evict_first=env_flag("MJOLNIR_ATTN_KV_EVICT_FIRST", "1" if q_mode == "fp8" else "0").lower()
|
|
461
|
+
in ("1", "true", "yes"),
|
|
462
|
+
kv_format=self._kv_format,
|
|
463
|
+
device=device,
|
|
464
|
+
)
|
|
465
|
+
pre = env_flag("MJOLNIR_ATTN_PRECOMPILE", "all").lower()
|
|
466
|
+
if pre in ("all", "1", "true", "yes"):
|
|
467
|
+
import time
|
|
468
|
+
|
|
469
|
+
from . import runner as _runner
|
|
470
|
+
|
|
471
|
+
before = dict(_runner.CACHE_STATS)
|
|
472
|
+
t0 = time.perf_counter()
|
|
473
|
+
self.t5.precompile()
|
|
474
|
+
loaded = _runner.CACHE_STATS["loaded"] - before["loaded"]
|
|
475
|
+
compiled = _runner.CACHE_STATS["compiled"] - before["compiled"]
|
|
476
|
+
if loaded or compiled:
|
|
477
|
+
logger.info(
|
|
478
|
+
"Mjolnir: decode kernels (q_len 1..%d, %s Q) ready in %.1f s: %d compiled, %d from %s",
|
|
479
|
+
self.t5.max_q_len,
|
|
480
|
+
q_mode,
|
|
481
|
+
time.perf_counter() - t0,
|
|
482
|
+
compiled,
|
|
483
|
+
loaded,
|
|
484
|
+
_runner.kernel_cache_dir() or "cache (off)",
|
|
485
|
+
)
|
|
486
|
+
logger.info(
|
|
487
|
+
"Mjolnir decode enabled: h_q=%d h_k=%d d=%d page=%d q_len<=%d Q=%s "
|
|
488
|
+
"ctas/SM=%d slots(q1)=%d pdl=%s l2_prefetch=%d "
|
|
489
|
+
"kv_evict_first=%s kv=%s fallback=%s",
|
|
490
|
+
num_heads,
|
|
491
|
+
kv_cache_spec.num_kv_heads,
|
|
492
|
+
kv_cache_spec.head_size,
|
|
493
|
+
kv_cache_spec.block_size,
|
|
494
|
+
self.t5.max_q_len,
|
|
495
|
+
q_mode,
|
|
496
|
+
self.t5.ctas_per_sm,
|
|
497
|
+
self.t5.slots(1),
|
|
498
|
+
self.t5.pdl,
|
|
499
|
+
self.t5.l2_prefetch,
|
|
500
|
+
self.t5.kv_evict_first,
|
|
501
|
+
self._kv_format,
|
|
502
|
+
_fallback_name(),
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
# -- CUDA graph support ------------------------------------------------
|
|
506
|
+
@classmethod
|
|
507
|
+
def get_cudagraph_support(cls, vllm_config, kv_cache_spec):
|
|
508
|
+
base = super().get_cudagraph_support(vllm_config, kv_cache_spec)
|
|
509
|
+
# Mixed batches always go to the base backend, so never promise more
|
|
510
|
+
# than it does for those; uniform decode is T5's and graph-safe.
|
|
511
|
+
if base == AttentionCGSupport.ALWAYS:
|
|
512
|
+
return base
|
|
513
|
+
if _max_q_len(vllm_config) > 1:
|
|
514
|
+
return max(base, AttentionCGSupport.UNIFORM_BATCH, key=lambda e: e.value)
|
|
515
|
+
return max(base, AttentionCGSupport.UNIFORM_SINGLE_TOKEN_DECODE, key=lambda e: e.value)
|
|
516
|
+
|
|
517
|
+
@classmethod
|
|
518
|
+
def get_varlen_cudagraph_max_query_len(cls, vllm_config, kv_cache_spec):
|
|
519
|
+
return None # T5 graphs replay uniform-q_len batches only
|
|
520
|
+
|
|
521
|
+
# -- build ---------------------------------------------------------------
|
|
522
|
+
def _t5_q_len(self, cam) -> int | None:
|
|
523
|
+
"""q_len if this step is a uniform decode batch T5 can take: causal
|
|
524
|
+
(decode, speculative verification), or non-causal (a parallel drafter's
|
|
525
|
+
block — DSpark / DFlash — where every query token sees every key; off
|
|
526
|
+
with MJOLNIR_ATTN_NONCAUSAL=0)."""
|
|
527
|
+
t5 = self.t5
|
|
528
|
+
if t5 is None:
|
|
529
|
+
return None
|
|
530
|
+
q_len = int(cam.max_query_len)
|
|
531
|
+
if not 1 <= q_len <= t5.max_q_len or cam.num_reqs > t5.max_num_seqs:
|
|
532
|
+
return None
|
|
533
|
+
causal = getattr(cam, "causal", True)
|
|
534
|
+
if causal is not True and not (causal is False and _NONCAUSAL):
|
|
535
|
+
return None
|
|
536
|
+
if getattr(cam, "dcp_local_seq_lens", None) is not None:
|
|
537
|
+
return None
|
|
538
|
+
if q_len == 1 and cam.num_actual_tokens <= cam.num_reqs:
|
|
539
|
+
return 1
|
|
540
|
+
# Uniform: every request has q_len tokens, except trailing CUDA-graph
|
|
541
|
+
# padding requests with 0.
|
|
542
|
+
qsl = cam.query_start_loc_cpu[: cam.num_reqs + 1]
|
|
543
|
+
lens = (qsl[1:] - qsl[:-1]).tolist()
|
|
544
|
+
n_real = 0
|
|
545
|
+
while n_real < len(lens) and lens[n_real] == q_len:
|
|
546
|
+
n_real += 1
|
|
547
|
+
if any(x != 0 for x in lens[n_real:]):
|
|
548
|
+
return None
|
|
549
|
+
return q_len
|
|
550
|
+
|
|
551
|
+
def _seq_lens_cpu(self, cam):
|
|
552
|
+
ub = getattr(cam, "seq_lens_cpu_upper_bound", None)
|
|
553
|
+
if ub is not None:
|
|
554
|
+
# An upper bound is fine for planning: the kernel masks with the
|
|
555
|
+
# exact device seq_lens, extra tiles are empty.
|
|
556
|
+
return ub
|
|
557
|
+
legacy = getattr(cam, "seq_lens_cpu", None)
|
|
558
|
+
if legacy is not None:
|
|
559
|
+
return legacy
|
|
560
|
+
with _gpu_sync_allowed():
|
|
561
|
+
return cam.seq_lens.cpu()
|
|
562
|
+
|
|
563
|
+
def build(self, common_prefix_len: int, common_attn_metadata, fast_build: bool = False):
|
|
564
|
+
cam = common_attn_metadata
|
|
565
|
+
q_len = self._t5_q_len(cam) if common_prefix_len == 0 and not self._force_fallback else None
|
|
566
|
+
if q_len is None:
|
|
567
|
+
STATS["fallback_steps"] += 1
|
|
568
|
+
md = super().build(common_prefix_len, cam, fast_build)
|
|
569
|
+
if self._fp6:
|
|
570
|
+
md.mjolnir_fp6_pages = self._fp6_page_map(cam)
|
|
571
|
+
return md
|
|
572
|
+
STATS["t5_steps"] += 1
|
|
573
|
+
t5 = self.t5
|
|
574
|
+
num_seqs = cam.num_reqs
|
|
575
|
+
causal = getattr(cam, "causal", True) is not False
|
|
576
|
+
if not causal:
|
|
577
|
+
STATS["t5_noncausal_steps"] += 1
|
|
578
|
+
plan = t5.plan(
|
|
579
|
+
self._seq_lens_cpu(cam),
|
|
580
|
+
num_seqs,
|
|
581
|
+
q_len,
|
|
582
|
+
cam.block_table_tensor,
|
|
583
|
+
cam.seq_lens,
|
|
584
|
+
margin=self._plan_margin,
|
|
585
|
+
causal=causal,
|
|
586
|
+
)
|
|
587
|
+
# Never compiled inside graph capture: build() runs outside it.
|
|
588
|
+
t5.ensure_compiled(q_len, plan.with_reduction, plan.causal)
|
|
589
|
+
return MjolnirMetadata(plan=plan, runner=t5, cam=cam)
|
|
590
|
+
|
|
591
|
+
def _fp6_page_map(self, cam):
|
|
592
|
+
"""For a step the fallback runs on an fp6 cache: which cache page
|
|
593
|
+
becomes which scratch page, and the block table over the scratch.
|
|
594
|
+
Scratch page offset_r + j holds request r's page j (no deduplication:
|
|
595
|
+
a shared prefix page is expanded once per request that uses it)."""
|
|
596
|
+
num_reqs = cam.num_reqs
|
|
597
|
+
lens = [int(x) for x in self._seq_lens_cpu(cam)[:num_reqs]]
|
|
598
|
+
n_pages = [max(1, -(-L // self._page)) if L > 0 else 0 for L in lens]
|
|
599
|
+
total = sum(n_pages)
|
|
600
|
+
width = cam.block_table_tensor.shape[1]
|
|
601
|
+
remap = torch.zeros(cam.block_table_tensor.shape[0], width, dtype=torch.int32)
|
|
602
|
+
req_idx, page_idx, off = [], [], 0
|
|
603
|
+
for r, n in enumerate(n_pages):
|
|
604
|
+
if n:
|
|
605
|
+
remap[r, :n] = torch.arange(off, off + n, dtype=torch.int32)
|
|
606
|
+
req_idx += [r] * n
|
|
607
|
+
page_idx += list(range(n))
|
|
608
|
+
off += n
|
|
609
|
+
dev = cam.block_table_tensor.device
|
|
610
|
+
rows = torch.tensor(req_idx, dtype=torch.long).pin_memory().to(dev, non_blocking=True)
|
|
611
|
+
cols = torch.tensor(page_idx, dtype=torch.long).pin_memory().to(dev, non_blocking=True)
|
|
612
|
+
src = cam.block_table_tensor[rows, cols].to(torch.int32).contiguous()
|
|
613
|
+
return {"src_pages": src, "block_table": remap.pin_memory().to(dev, non_blocking=True), "total": total}
|
|
614
|
+
|
|
615
|
+
def build_for_cudagraph_capture(self, common_attn_metadata):
|
|
616
|
+
return self.build(0, common_attn_metadata)
|
|
617
|
+
|
|
618
|
+
def update_draft_decode_metadata(self, metadata) -> None:
|
|
619
|
+
"""Fused draft decode: advance the metadata to the next draft step,
|
|
620
|
+
capture-safe. A T5 plan already covers it (see __init__); other steps
|
|
621
|
+
were built by the base builder, which updates its own metadata."""
|
|
622
|
+
if getattr(metadata, "is_t5", False):
|
|
623
|
+
return
|
|
624
|
+
super().update_draft_decode_metadata(metadata)
|
|
625
|
+
|
|
626
|
+
def use_cascade_attention(self, *args, **kwargs) -> bool:
|
|
627
|
+
# Cascade steps are built by the base builder (common_prefix_len > 0);
|
|
628
|
+
# keep the base decision — except on fp6, whose fallback path expands
|
|
629
|
+
# per-request pages and has no shared-prefix variant.
|
|
630
|
+
if self._fp6:
|
|
631
|
+
return False
|
|
632
|
+
return super().use_cascade_attention(*args, **kwargs)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
# --------------------------------------------------------------------------
|
|
636
|
+
# Impl
|
|
637
|
+
# --------------------------------------------------------------------------
|
|
638
|
+
class MjolnirImpl(_BaseImpl): # type: ignore[misc, valid-type]
|
|
639
|
+
def __init__(self, *args, **kwargs):
|
|
640
|
+
super().__init__(*args, **kwargs)
|
|
641
|
+
# T5 quantizes Q itself (dynamic, per (sequence, KV head)); the
|
|
642
|
+
# layer's static per-tensor quantization would change the input of
|
|
643
|
+
# every step, so the layer keeps handing us bf16 Q. A fallback that
|
|
644
|
+
# wants fp8 Q next to an fp8 KV cache (FlashAttention 4 on sm_110
|
|
645
|
+
# needs Q, K and V in one fp8 dtype) gets it quantized here, on the
|
|
646
|
+
# steps it runs — the same static quantization the layer would do.
|
|
647
|
+
cache_dtype = str(getattr(self, "kv_cache_dtype", "auto"))
|
|
648
|
+
# fp6: the fallback reads an fp8 scratch (see _forward_fp6_fallback)
|
|
649
|
+
self._fp6 = cache_dtype in FP6_CACHE_DTYPES
|
|
650
|
+
quantized_kv = cache_dtype in MJOLNIR_CACHE_DTYPES
|
|
651
|
+
self._fallback_fp8_q = (
|
|
652
|
+
bool(self.supports_quant_query_input) and quantized_kv and _FALLBACK == "FLASH_ATTN"
|
|
653
|
+
) or self._fp6
|
|
654
|
+
self.supports_quant_query_input = False
|
|
655
|
+
self._kv_layout: str | None = None
|
|
656
|
+
self._t5_reason = _layer_reason(self)
|
|
657
|
+
|
|
658
|
+
# -- KV cache write ----------------------------------------------------
|
|
659
|
+
def do_kv_cache_update(self, layer, key, value, kv_cache, slot_mapping):
|
|
660
|
+
"""fp6: Mjolnir's own packed write; fp8: the base backend's. In
|
|
661
|
+
calibration mode (MJOLNIR_ATTN_KV_CALIBRATE=1) also fold every
|
|
662
|
+
written K/V into the layer's per-channel amax."""
|
|
663
|
+
from . import kvscales
|
|
664
|
+
|
|
665
|
+
if kvscales.calibrating():
|
|
666
|
+
kvscales.observe(getattr(layer, "layer_name", str(id(layer))), key, value, slot_mapping)
|
|
667
|
+
if not self._fp6:
|
|
668
|
+
return super().do_kv_cache_update(layer, key, value, kv_cache, slot_mapping)
|
|
669
|
+
if getattr(self, "kv_sharing_target_layer_name", None) is not None:
|
|
670
|
+
return None
|
|
671
|
+
from . import kvfp6
|
|
672
|
+
|
|
673
|
+
st = layer._mjolnir_fp6
|
|
674
|
+
kvfp6.write_kv(key, value, kv_cache, slot_mapping, st["k_ch"], st["v_ch"])
|
|
675
|
+
return None
|
|
676
|
+
|
|
677
|
+
def _forward_fp6_fallback(self, layer, query, key, value, kv_cache, attn_metadata, *rest, **kwargs):
|
|
678
|
+
"""A fallback step on an fp6 cache: expand the pages this step reads
|
|
679
|
+
to e4m3 as code * s_c / S (s_c the channel scale, S the layer's
|
|
680
|
+
per-head k/v scale, the descale FA4 applies), point a copy of the
|
|
681
|
+
metadata at the scratch, and run the base forward on it."""
|
|
682
|
+
from . import kvfp6
|
|
683
|
+
|
|
684
|
+
if torch.cuda.is_current_stream_capturing():
|
|
685
|
+
raise RuntimeError(
|
|
686
|
+
"Mjolnir: an fp6 fallback step inside CUDA-graph capture — its page expansion "
|
|
687
|
+
"depends on per-step host data and would replay stale. Serve fp8 KV or report this shape."
|
|
688
|
+
)
|
|
689
|
+
pages = attn_metadata.mjolnir_fp6_pages
|
|
690
|
+
_, H, N, _ = kv_cache.shape
|
|
691
|
+
scratch = _fp6_scratch(pages["total"], H, N, 2 * self.head_size, kv_cache.device)
|
|
692
|
+
st = layer._mjolnir_fp6
|
|
693
|
+
kvfp6.expand_pages_to_fp8(kv_cache, pages["src_pages"], scratch, self.head_size, st["k_ratio"], st["v_ratio"])
|
|
694
|
+
md = copy.copy(attn_metadata)
|
|
695
|
+
md.block_table = pages["block_table"]
|
|
696
|
+
return super().forward(layer, query, key, value, scratch[: max(1, pages["total"])], md, *rest, **kwargs)
|
|
697
|
+
|
|
698
|
+
def _quantize_q(self, layer, query: torch.Tensor) -> torch.Tensor:
|
|
699
|
+
"""Static fp8 Q for the fallback, as the attention layer would
|
|
700
|
+
quantize it: Q / q_scale, saturated to e4m3, per tensor or per KV head."""
|
|
701
|
+
scale = layer._q_scale
|
|
702
|
+
n = query.shape[0]
|
|
703
|
+
if scale.numel() == 1:
|
|
704
|
+
from vllm import _custom_ops as ops
|
|
705
|
+
|
|
706
|
+
q8, _ = ops.scaled_fp8_quant(query.reshape(n, -1), scale.reshape(1).float())
|
|
707
|
+
return q8.view(query.shape)
|
|
708
|
+
fp8 = torch.float8_e4m3fn
|
|
709
|
+
lim = torch.finfo(fp8).max
|
|
710
|
+
q = query.reshape(n, scale.numel(), -1).float() / scale.float().reshape(1, -1, 1)
|
|
711
|
+
return q.clamp_(-lim, lim).to(fp8).view(query.shape)
|
|
712
|
+
|
|
713
|
+
def fused_output_quant_supported(self, quant_key) -> bool:
|
|
714
|
+
return False # T5 writes bf16/fp16 output
|
|
715
|
+
|
|
716
|
+
def _kv_views(self, kv_cache: torch.Tensor, runner: T5DecodeRunner):
|
|
717
|
+
# fp6 rows are packed bytes: K|V split at d * 6 / 8 bytes, not d
|
|
718
|
+
row = runner.kv_row_bytes
|
|
719
|
+
if self._kv_layout is None:
|
|
720
|
+
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
|
+
return split_kv_cache(kv_cache, self._kv_layout, row)
|
|
729
|
+
|
|
730
|
+
def _write_kv(self, layer, key, value, kv_cache, k_cache, v_cache, md):
|
|
731
|
+
"""Only for base backends whose forward() also writes the KV cache
|
|
732
|
+
(forward_includes_kv_cache_update = True, e.g. older vLLM)."""
|
|
733
|
+
if not _BASE_WRITES_KV_IN_FORWARD:
|
|
734
|
+
return
|
|
735
|
+
if key is None or value is None:
|
|
736
|
+
return
|
|
737
|
+
if getattr(self, "kv_sharing_target_layer_name", None) is not None:
|
|
738
|
+
return
|
|
739
|
+
n = md.slot_mapping.shape[0]
|
|
740
|
+
torch.ops._C_cache_ops.reshape_and_cache_flash(
|
|
741
|
+
key[:n],
|
|
742
|
+
value[:n],
|
|
743
|
+
k_cache,
|
|
744
|
+
v_cache,
|
|
745
|
+
md.slot_mapping,
|
|
746
|
+
self.kv_cache_dtype,
|
|
747
|
+
layer._k_scale,
|
|
748
|
+
layer._v_scale,
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
def forward(
|
|
752
|
+
self,
|
|
753
|
+
layer,
|
|
754
|
+
query,
|
|
755
|
+
key,
|
|
756
|
+
value,
|
|
757
|
+
kv_cache,
|
|
758
|
+
attn_metadata,
|
|
759
|
+
output=None,
|
|
760
|
+
output_scale=None,
|
|
761
|
+
output_block_scale=None,
|
|
762
|
+
*args: Any,
|
|
763
|
+
**kwargs: Any,
|
|
764
|
+
):
|
|
765
|
+
if not getattr(attn_metadata, "is_t5", False):
|
|
766
|
+
if self._fallback_fp8_q and attn_metadata is not None and query.dtype in (torch.bfloat16, torch.float16):
|
|
767
|
+
query = self._quantize_q(layer, query)
|
|
768
|
+
# The base forward passes the layer's q scale as q_descale
|
|
769
|
+
# only when it believes the layer quantized Q.
|
|
770
|
+
self.supports_quant_query_input = True
|
|
771
|
+
try:
|
|
772
|
+
if self._fp6:
|
|
773
|
+
return self._forward_fp6_fallback(
|
|
774
|
+
layer,
|
|
775
|
+
query,
|
|
776
|
+
key,
|
|
777
|
+
value,
|
|
778
|
+
kv_cache,
|
|
779
|
+
attn_metadata,
|
|
780
|
+
output,
|
|
781
|
+
output_scale,
|
|
782
|
+
output_block_scale,
|
|
783
|
+
*args,
|
|
784
|
+
**kwargs,
|
|
785
|
+
)
|
|
786
|
+
return super().forward(
|
|
787
|
+
layer,
|
|
788
|
+
query,
|
|
789
|
+
key,
|
|
790
|
+
value,
|
|
791
|
+
kv_cache,
|
|
792
|
+
attn_metadata,
|
|
793
|
+
output,
|
|
794
|
+
output_scale,
|
|
795
|
+
output_block_scale,
|
|
796
|
+
*args,
|
|
797
|
+
**kwargs,
|
|
798
|
+
)
|
|
799
|
+
finally:
|
|
800
|
+
self.supports_quant_query_input = False
|
|
801
|
+
return super().forward(
|
|
802
|
+
layer,
|
|
803
|
+
query,
|
|
804
|
+
key,
|
|
805
|
+
value,
|
|
806
|
+
kv_cache,
|
|
807
|
+
attn_metadata,
|
|
808
|
+
output,
|
|
809
|
+
output_scale,
|
|
810
|
+
output_block_scale,
|
|
811
|
+
*args,
|
|
812
|
+
**kwargs,
|
|
813
|
+
)
|
|
814
|
+
if self._t5_reason is not None:
|
|
815
|
+
raise RuntimeError(f"Mjolnir got a layer it cannot run ({self._t5_reason}); set MJOLNIR_ATTN_DISABLE=1")
|
|
816
|
+
assert output is not None and output_scale is None
|
|
817
|
+
md: MjolnirMetadata = attn_metadata
|
|
818
|
+
runner = md.runner
|
|
819
|
+
k_cache, v_cache = self._kv_views(kv_cache, runner)
|
|
820
|
+
if k_cache.shape[1] != runner.page_size:
|
|
821
|
+
raise RuntimeError(
|
|
822
|
+
f"Mjolnir: KV page size {k_cache.shape[1]} != planned {runner.page_size}; "
|
|
823
|
+
"set MJOLNIR_ATTN_KV_LAYOUT or MJOLNIR_ATTN_DISABLE=1"
|
|
824
|
+
)
|
|
825
|
+
self._write_kv(layer, key, value, kv_cache, k_cache, v_cache, md)
|
|
826
|
+
if query.dtype not in (torch.bfloat16, torch.float16):
|
|
827
|
+
# pre-quantized fp8 query from a layer fusion: dequantize
|
|
828
|
+
query = (query.float() * layer._q_scale).to(torch.bfloat16)
|
|
829
|
+
if self._fp6: # channel scales fold into Q and O; the cache codes have descale 1
|
|
830
|
+
st = layer._mjolnir_fp6
|
|
831
|
+
runner.run(
|
|
832
|
+
md.plan,
|
|
833
|
+
query,
|
|
834
|
+
output,
|
|
835
|
+
k_cache,
|
|
836
|
+
v_cache,
|
|
837
|
+
st["one"],
|
|
838
|
+
st["one"],
|
|
839
|
+
self.scale,
|
|
840
|
+
q_mul=st["k_mul"],
|
|
841
|
+
o_mul=st["v_mul"],
|
|
842
|
+
)
|
|
843
|
+
return output
|
|
844
|
+
runner.run(
|
|
845
|
+
md.plan,
|
|
846
|
+
query,
|
|
847
|
+
output,
|
|
848
|
+
k_cache,
|
|
849
|
+
v_cache,
|
|
850
|
+
layer._k_scale,
|
|
851
|
+
layer._v_scale,
|
|
852
|
+
self.scale,
|
|
853
|
+
)
|
|
854
|
+
return output
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
# --------------------------------------------------------------------------
|
|
858
|
+
# Backend
|
|
859
|
+
# --------------------------------------------------------------------------
|
|
860
|
+
class MjolnirAttentionBackend(_Base): # type: ignore[misc, valid-type]
|
|
861
|
+
accept_output_buffer: bool = True
|
|
862
|
+
base_backend: ClassVar[type] = _Base
|
|
863
|
+
|
|
864
|
+
@staticmethod
|
|
865
|
+
def get_name() -> str:
|
|
866
|
+
# vLLM maps this name back through AttentionBackendEnum[...]; the
|
|
867
|
+
# MJOLNIR member exists only in a patched vLLM (the Mjolnir image),
|
|
868
|
+
# a stock vLLM reaches this class through CUSTOM.
|
|
869
|
+
from mjolnir import BACKEND_NAME, has_native_member
|
|
870
|
+
|
|
871
|
+
return BACKEND_NAME if has_native_member() else "CUSTOM"
|
|
872
|
+
|
|
873
|
+
@classmethod
|
|
874
|
+
def get_supported_kernel_block_sizes(cls, kv_cache_spec=None) -> list:
|
|
875
|
+
"""The Mjolnir kernel runs 16/32/64/128-token pages. On FlashInfer
|
|
876
|
+
(which advertises 16/32/64 on sm_110) it adds 128 — the kernel's
|
|
877
|
+
benchmark default and the best fit for head-major pages — since FI's
|
|
878
|
+
prefill and KV-write paths take any page size. Any other fallback's
|
|
879
|
+
constraint is kept: FA4's hd256 kernel needs exactly 128."""
|
|
880
|
+
if _FALLBACK == "FLASHINFER":
|
|
881
|
+
return list(SUPPORTED_PAGE_SIZES)
|
|
882
|
+
try:
|
|
883
|
+
base = _Base.get_supported_kernel_block_sizes(kv_cache_spec)
|
|
884
|
+
except TypeError: # older vLLM: no kv_cache_spec argument
|
|
885
|
+
base = _Base.get_supported_kernel_block_sizes()
|
|
886
|
+
ours = [s for s in SUPPORTED_PAGE_SIZES if _block_size_ok(base, s)]
|
|
887
|
+
return ours or list(base)
|
|
888
|
+
|
|
889
|
+
# -- KV cache dtypes --------------------------------------------------
|
|
890
|
+
# The fallback's own list, plus fp6_e2m3 where vLLM knows the dtype (the
|
|
891
|
+
# image's thor-fp6-e2m3-kv-cache patch) and the FA4 fallback can read the
|
|
892
|
+
# fp8 scratch.
|
|
893
|
+
supported_kv_cache_dtypes: ClassVar[list] = list(getattr(_Base, "supported_kv_cache_dtypes", [])) + (
|
|
894
|
+
list(FP6_CACHE_DTYPES) if _FALLBACK == "FLASH_ATTN" else []
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
@classmethod
|
|
898
|
+
def supports_kv_cache_dtype(cls, kv_cache_dtype) -> bool:
|
|
899
|
+
if kv_cache_dtype in FP6_CACHE_DTYPES:
|
|
900
|
+
return _fp6_known_to_vllm() and _FALLBACK == "FLASH_ATTN" and _Base.supports_kv_cache_dtype("fp8")
|
|
901
|
+
return _Base.supports_kv_cache_dtype(kv_cache_dtype)
|
|
902
|
+
|
|
903
|
+
@classmethod
|
|
904
|
+
def supports_combination(cls, head_size, dtype, kv_cache_dtype, block_size, *args, **kwargs):
|
|
905
|
+
# fp6: the fallback runs on an e4m3 scratch, so it must support fp8
|
|
906
|
+
if kv_cache_dtype in FP6_CACHE_DTYPES:
|
|
907
|
+
kv_cache_dtype = "fp8"
|
|
908
|
+
return _Base.supports_combination(head_size, dtype, kv_cache_dtype, block_size, *args, **kwargs)
|
|
909
|
+
|
|
910
|
+
@classmethod
|
|
911
|
+
def customize_spec(cls, spec):
|
|
912
|
+
"""fp6_e2m3: one (head, token) cell is the packed K row then the
|
|
913
|
+
packed V row, d * 6 / 8 bytes each."""
|
|
914
|
+
mode = getattr(spec, "kv_quant_mode", None)
|
|
915
|
+
if getattr(mode, "is_fp6", False) and getattr(spec, "state_content_bytes", None) is None:
|
|
916
|
+
from dataclasses import replace
|
|
917
|
+
|
|
918
|
+
from .kvfp6 import row_bytes
|
|
919
|
+
|
|
920
|
+
return replace(spec, state_content_bytes=row_bytes(spec.head_size) + row_bytes(spec.head_size_v))
|
|
921
|
+
return super().customize_spec(spec)
|
|
922
|
+
|
|
923
|
+
@staticmethod
|
|
924
|
+
def get_impl_cls():
|
|
925
|
+
return MjolnirImpl
|
|
926
|
+
|
|
927
|
+
@staticmethod
|
|
928
|
+
def get_builder_cls():
|
|
929
|
+
return MjolnirMetadataBuilder
|
|
930
|
+
|
|
931
|
+
@classmethod
|
|
932
|
+
def supports_device_cpu_query_lens_mismatch(cls) -> bool:
|
|
933
|
+
# The uniform-decode check reads query_start_loc_cpu; it must be exact.
|
|
934
|
+
return False
|