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/lmhead.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""Opt-in lm_head experiments for decode (vLLM GPU model runner).
|
|
2
|
+
|
|
3
|
+
On Qwen3.8-27B NVFP4 the bf16 lm_head (248,320 x 5,120, 2.54 GB) is read by
|
|
4
|
+
the target's verify and by every MTP draft step — ~40% of a decode step's
|
|
5
|
+
bytes. Two experiments, both off unless their env var is set (a config opts
|
|
6
|
+
in through the ``.env`` sidecar inside its config dir, see
|
|
7
|
+
``mjolnir_cli.dockerctl``):
|
|
8
|
+
|
|
9
|
+
* ``MJOLNIR_LM_HEAD=fp8`` — the lm_head weight quantized to e4m3 per row at
|
|
10
|
+
load (scale = row amax / 448) and multiplied by a Triton fp8-weight GEMM
|
|
11
|
+
(bf16 activations, fp32 accumulate): half the bytes for the verify and the
|
|
12
|
+
drafts. The fp8 head makes the bf16 copy dead weight, so the bf16 weight is
|
|
13
|
+
freed automatically (an untied head only), returning 2.5 GB to the KV cache.
|
|
14
|
+
* ``MJOLNIR_DRAFT_VOCAB=<json>`` (+ ``MJOLNIR_DRAFT_VOCAB_SIZE``) — VocabTrim
|
|
15
|
+
for the drafter: its logits are computed only over the most frequent
|
|
16
|
+
target tokens (``mjolnir calibrate --draft-vocab``), the rest are -inf.
|
|
17
|
+
Verification still uses the full vocabulary, so the output distribution is
|
|
18
|
+
unchanged (rejection sampling stays exact); only acceptance can drop.
|
|
19
|
+
|
|
20
|
+
The hooks wrap ``GPUModelRunner.load_model`` (installed by
|
|
21
|
+
``mjolnir.register`` when either variable is set) and run after the target
|
|
22
|
+
and the drafter are loaded, before CUDA-graph capture.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import logging
|
|
29
|
+
import os
|
|
30
|
+
|
|
31
|
+
import torch
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
import triton
|
|
35
|
+
import triton.language as tl
|
|
36
|
+
except ImportError: # pragma: no cover
|
|
37
|
+
triton = None
|
|
38
|
+
tl = None
|
|
39
|
+
|
|
40
|
+
_log = logging.getLogger("vllm.mjolnir")
|
|
41
|
+
|
|
42
|
+
ENV_LM_HEAD = "MJOLNIR_LM_HEAD"
|
|
43
|
+
ENV_DRAFT_VOCAB = "MJOLNIR_DRAFT_VOCAB"
|
|
44
|
+
ENV_DRAFT_VOCAB_SIZE = "MJOLNIR_DRAFT_VOCAB_SIZE"
|
|
45
|
+
FP8_MAX = 448.0
|
|
46
|
+
SMALL_M = 64 # beyond this many rows (prompt logprobs) the dequantized chunks win
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def wanted() -> bool:
|
|
50
|
+
return bool(os.environ.get(ENV_LM_HEAD, "").strip() or os.environ.get(ENV_DRAFT_VOCAB, "").strip())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ── fp8-weight GEMM ─────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
if triton is not None:
|
|
56
|
+
|
|
57
|
+
@triton.jit
|
|
58
|
+
def _fp8_head_kernel(
|
|
59
|
+
x_ptr,
|
|
60
|
+
w_ptr,
|
|
61
|
+
s_ptr,
|
|
62
|
+
o_ptr,
|
|
63
|
+
M,
|
|
64
|
+
N,
|
|
65
|
+
K,
|
|
66
|
+
stride_xm,
|
|
67
|
+
stride_om,
|
|
68
|
+
BLOCK_M: tl.constexpr,
|
|
69
|
+
BLOCK_N: tl.constexpr,
|
|
70
|
+
BLOCK_K: tl.constexpr,
|
|
71
|
+
):
|
|
72
|
+
pid_n = tl.program_id(0)
|
|
73
|
+
pid_m = tl.program_id(1)
|
|
74
|
+
rn = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
|
|
75
|
+
rm = pid_m * BLOCK_M + tl.arange(0, BLOCK_M)
|
|
76
|
+
rk = tl.arange(0, BLOCK_K)
|
|
77
|
+
acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32)
|
|
78
|
+
for k0 in range(0, K, BLOCK_K):
|
|
79
|
+
kk = k0 + rk
|
|
80
|
+
x = tl.load(
|
|
81
|
+
x_ptr + rm[:, None] * stride_xm + kk[None, :],
|
|
82
|
+
mask=(rm[:, None] < M) & (kk[None, :] < K),
|
|
83
|
+
other=0.0,
|
|
84
|
+
)
|
|
85
|
+
w = tl.load(
|
|
86
|
+
w_ptr + rn[:, None] * K + kk[None, :],
|
|
87
|
+
mask=(rn[:, None] < N) & (kk[None, :] < K),
|
|
88
|
+
other=0.0,
|
|
89
|
+
)
|
|
90
|
+
acc += tl.dot(x.to(tl.bfloat16), tl.trans(w.to(tl.bfloat16)))
|
|
91
|
+
s = tl.load(s_ptr + rn, mask=rn < N, other=0.0)
|
|
92
|
+
acc = acc * s[None, :]
|
|
93
|
+
tl.store(
|
|
94
|
+
o_ptr + rm[:, None] * stride_om + rn[None, :],
|
|
95
|
+
acc.to(o_ptr.dtype.element_ty),
|
|
96
|
+
mask=(rm[:, None] < M) & (rn[None, :] < N),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
ROW_CHUNK = 4096 # rows per step when quantizing / testing a head (bounds the temporaries)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def quantize_rows(w: torch.Tensor, chunk: int = ROW_CHUNK) -> tuple[torch.Tensor, torch.Tensor]:
|
|
104
|
+
"""(N, K) float -> (N, K) e4m3 and (N,) fp32 row scales (amax / 448).
|
|
105
|
+
|
|
106
|
+
Row chunks of ``chunk``: a whole-head fp32 copy of the 248K x 5120 lm_head
|
|
107
|
+
is 5 GiB, and the abs / divide / clamp temporaries of one expression are
|
|
108
|
+
several more. Freed or not, the caching allocator keeps them reserved,
|
|
109
|
+
and vLLM's startup memory profile (on Thor: one unified pool) counts them
|
|
110
|
+
as used — they came straight out of the KV cache. Chunked, the peak is a
|
|
111
|
+
few hundred MiB."""
|
|
112
|
+
N = w.shape[0]
|
|
113
|
+
q = torch.empty(w.shape, dtype=torch.float8_e4m3fn, device=w.device)
|
|
114
|
+
scale = torch.empty(N, dtype=torch.float32, device=w.device)
|
|
115
|
+
for r0 in range(0, N, chunk):
|
|
116
|
+
w32 = w[r0 : r0 + chunk].float()
|
|
117
|
+
sc = (w32.abs().amax(dim=1) / FP8_MAX).clamp_min(1e-12)
|
|
118
|
+
q[r0 : r0 + chunk] = (w32.div_(sc[:, None])).clamp_(-FP8_MAX, FP8_MAX).to(torch.float8_e4m3fn)
|
|
119
|
+
scale[r0 : r0 + chunk] = sc
|
|
120
|
+
del w32
|
|
121
|
+
return q, scale
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def fp8_linear_reference(x: torch.Tensor, w8: torch.Tensor, scale: torch.Tensor) -> torch.Tensor:
|
|
125
|
+
return (x.float() @ (w8.float() * scale[:, None]).t()).to(x.dtype)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def fp8_linear(x: torch.Tensor, w8: torch.Tensor, scale: torch.Tensor, chunk: int = 16384) -> torch.Tensor:
|
|
129
|
+
"""x (..., K) bf16/fp16 @ (w8 * scale)^T -> (..., N), x's dtype."""
|
|
130
|
+
shape = x.shape
|
|
131
|
+
x2 = x.reshape(-1, shape[-1])
|
|
132
|
+
if x2.stride(-1) != 1:
|
|
133
|
+
x2 = x2.contiguous()
|
|
134
|
+
M, K = x2.shape
|
|
135
|
+
N = w8.shape[0]
|
|
136
|
+
out = torch.empty(M, N, dtype=x.dtype, device=x.device)
|
|
137
|
+
if M <= SMALL_M and triton is not None:
|
|
138
|
+
BM, BN, BK = 16, 64, 128
|
|
139
|
+
grid = (triton.cdiv(N, BN), triton.cdiv(M, BM))
|
|
140
|
+
_fp8_head_kernel[grid](
|
|
141
|
+
x2, w8, scale, out, M, N, K, x2.stride(0), out.stride(0), BLOCK_M=BM, BLOCK_N=BN, BLOCK_K=BK,
|
|
142
|
+
num_warps=4, num_stages=4,
|
|
143
|
+
) # fmt: skip
|
|
144
|
+
else: # many rows: dequantize a slab of the head at a time
|
|
145
|
+
for n0 in range(0, N, chunk):
|
|
146
|
+
w = w8[n0 : n0 + chunk].to(x.dtype) * scale[n0 : n0 + chunk, None].to(x.dtype)
|
|
147
|
+
out[:, n0 : n0 + chunk] = x2 @ w.t()
|
|
148
|
+
return out.reshape(*shape[:-1], N)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class Fp8HeadMethod:
|
|
152
|
+
"""Stands in for the lm_head's quant method: ``apply`` runs the fp8
|
|
153
|
+
GEMM; everything else (``embedding`` for a tied head, attributes) goes
|
|
154
|
+
to the original method."""
|
|
155
|
+
|
|
156
|
+
def __init__(self, base, w8: torch.Tensor, scale: torch.Tensor):
|
|
157
|
+
self._base = base
|
|
158
|
+
self.w8 = w8
|
|
159
|
+
self.scale = scale
|
|
160
|
+
|
|
161
|
+
def apply(self, layer, x, bias=None):
|
|
162
|
+
out = fp8_linear(x, self.w8, self.scale)
|
|
163
|
+
return out + bias if bias is not None else out
|
|
164
|
+
|
|
165
|
+
def __getattr__(self, name):
|
|
166
|
+
return getattr(self._base, name)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _self_test(w: torch.Tensor, w8: torch.Tensor, scale: torch.Tensor) -> float:
|
|
170
|
+
"""Relative error of the fp8 head vs bf16 on random activations."""
|
|
171
|
+
g = torch.Generator(device=w.device).manual_seed(0)
|
|
172
|
+
x = torch.randn(4, w.shape[1], device=w.device, dtype=w.dtype, generator=g)
|
|
173
|
+
rows = torch.arange(0, w.shape[0], max(1, w.shape[0] // ROW_CHUNK), device=w.device)[:ROW_CHUNK]
|
|
174
|
+
ref = x.float() @ w.index_select(0, rows).float().t() # a strided row sample: no full fp32 copy
|
|
175
|
+
w8s = w8.view(torch.uint8).index_select(0, rows).view(torch.float8_e4m3fn) # fp8 has no index_select kernel
|
|
176
|
+
got = fp8_linear(x, w8s, scale.index_select(0, rows)).float()
|
|
177
|
+
return float((got - ref).norm() / ref.norm().clamp_min(1e-12))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _is_tied(lm_head, model) -> bool:
|
|
181
|
+
ptr = lm_head.weight.data_ptr()
|
|
182
|
+
for name, mod in model.named_modules():
|
|
183
|
+
if mod is not lm_head and name.endswith("embed_tokens") and hasattr(mod, "weight"):
|
|
184
|
+
if mod.weight.data_ptr() == ptr:
|
|
185
|
+
return True
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def install_fp8_head(lm_head, model=None) -> bool:
|
|
190
|
+
if getattr(lm_head, "_mjolnir_fp8", False):
|
|
191
|
+
return True
|
|
192
|
+
qm = getattr(lm_head, "quant_method", None)
|
|
193
|
+
if qm is None or "Unquantized" not in type(qm).__name__:
|
|
194
|
+
_log.warning("Mjolnir lm_head fp8: %s is not an unquantized head; left as is", type(qm).__name__)
|
|
195
|
+
return False
|
|
196
|
+
w = lm_head.weight.data
|
|
197
|
+
w8, scale = quantize_rows(w)
|
|
198
|
+
err = _self_test(w, w8, scale)
|
|
199
|
+
if not err < 0.05:
|
|
200
|
+
_log.warning("Mjolnir lm_head fp8: self-test relative error %.4f; left in bf16", err)
|
|
201
|
+
return False
|
|
202
|
+
lm_head.quant_method = Fp8HeadMethod(qm, w8, scale)
|
|
203
|
+
lm_head._mjolnir_fp8 = True
|
|
204
|
+
freed = ""
|
|
205
|
+
if model is not None: # the fp8 head is now the only consumer: the bf16 copy is dead weight
|
|
206
|
+
if _is_tied(lm_head, model):
|
|
207
|
+
freed = " (tied to the embedding: bf16 weight kept)"
|
|
208
|
+
else:
|
|
209
|
+
lm_head.weight.data = torch.empty(0, dtype=w.dtype, device=w.device)
|
|
210
|
+
torch.cuda.empty_cache()
|
|
211
|
+
freed = f", bf16 weight freed ({w.numel() * w.element_size() / 2**30:.2f} GiB)"
|
|
212
|
+
_log.info(
|
|
213
|
+
"Mjolnir lm_head fp8: %s x %s e4m3 per-row, self-test rel. error %.4f%s",
|
|
214
|
+
w.shape[0],
|
|
215
|
+
w.shape[1],
|
|
216
|
+
err,
|
|
217
|
+
freed,
|
|
218
|
+
)
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ── VocabTrim for the drafter ───────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def load_vocab(path: str, size: int | None = None) -> list[int]:
|
|
226
|
+
"""Token ids, most frequent first, from a ``mjolnir.draft_vocab/1`` file
|
|
227
|
+
(``{"format": ..., "tokens": [ids], ...}``) or a plain JSON list."""
|
|
228
|
+
with open(path) as f:
|
|
229
|
+
doc = json.load(f)
|
|
230
|
+
ids = doc["tokens"] if isinstance(doc, dict) else doc
|
|
231
|
+
ids = [int(t) for t in ids]
|
|
232
|
+
return ids[:size] if size else ids
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def pad_vocab(ids: list[int], size: int | None, vocab: int) -> list[int]:
|
|
236
|
+
"""Fill a short id list up to ``size`` with the lowest unused ids.
|
|
237
|
+
|
|
238
|
+
A calibration that saw fewer distinct tokens than the requested size
|
|
239
|
+
(6K distinct from ~50K generated tokens is typical) would otherwise trim
|
|
240
|
+
the drafter far below the size the config names. Low ids come first
|
|
241
|
+
because BPE vocabularies are ordered by merge rank, a fair frequency
|
|
242
|
+
prior for tokens the calibration never generated."""
|
|
243
|
+
seen = {i for i in ids if 0 <= i < vocab}
|
|
244
|
+
out = [i for i in dict.fromkeys(ids) if 0 <= i < vocab]
|
|
245
|
+
if size is None or len(out) >= size:
|
|
246
|
+
return out[:size] if size else out
|
|
247
|
+
need = min(size, vocab) - len(out)
|
|
248
|
+
for i in range(vocab):
|
|
249
|
+
if need <= 0:
|
|
250
|
+
break
|
|
251
|
+
if i not in seen:
|
|
252
|
+
out.append(i)
|
|
253
|
+
need -= 1
|
|
254
|
+
return out
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def install_draft_vocab(draft_model, ids: list[int], size: int | None = None) -> bool:
|
|
258
|
+
lm_head = getattr(draft_model, "lm_head", None)
|
|
259
|
+
lp = getattr(draft_model, "logits_processor", None)
|
|
260
|
+
if lm_head is None or lp is None or lm_head.weight.numel() == 0:
|
|
261
|
+
_log.warning("Mjolnir draft vocab: the drafter has no usable lm_head; left as is")
|
|
262
|
+
return False
|
|
263
|
+
if getattr(lp, "scale", 1.0) != 1.0 or getattr(lp, "soft_cap", None) is not None:
|
|
264
|
+
_log.warning("Mjolnir draft vocab: logit scale / soft cap not supported; left as is")
|
|
265
|
+
return False
|
|
266
|
+
w = lm_head.weight.data
|
|
267
|
+
vocab = int(getattr(lp, "org_vocab_size", w.shape[0]))
|
|
268
|
+
calibrated = len({i for i in ids if 0 <= i < vocab})
|
|
269
|
+
ids = pad_vocab(ids, size, vocab)
|
|
270
|
+
if size and calibrated < size:
|
|
271
|
+
_log.warning(
|
|
272
|
+
"Mjolnir draft vocab: the file has %d tokens, padded to %d with the lowest unused ids "
|
|
273
|
+
"(re-run `mjolnir calibrate --draft-vocab` with more prompts for a measured ranking)",
|
|
274
|
+
calibrated,
|
|
275
|
+
len(ids),
|
|
276
|
+
)
|
|
277
|
+
idx = torch.tensor(sorted(set(ids)), dtype=torch.long, device=w.device)
|
|
278
|
+
fp8 = getattr(lm_head, "_mjolnir_fp8", False) or os.environ.get(ENV_LM_HEAD, "").strip() == "fp8"
|
|
279
|
+
sub = w.index_select(0, idx).contiguous()
|
|
280
|
+
if fp8:
|
|
281
|
+
sub8, sub_scale = quantize_rows(sub)
|
|
282
|
+
|
|
283
|
+
def head(h):
|
|
284
|
+
return fp8_linear(h, sub8, sub_scale)
|
|
285
|
+
else:
|
|
286
|
+
|
|
287
|
+
def head(h):
|
|
288
|
+
return torch.nn.functional.linear(h, sub)
|
|
289
|
+
|
|
290
|
+
# row chunks bound the temporaries when a caller (vLLM's startup memory
|
|
291
|
+
# profile) passes thousands of rows at once
|
|
292
|
+
def compute_logits(hidden_states, *args, **kwargs):
|
|
293
|
+
h = hidden_states.reshape(-1, hidden_states.shape[-1])
|
|
294
|
+
out = torch.full((h.shape[0], vocab), float("-inf"), dtype=h.dtype, device=h.device)
|
|
295
|
+
for r0 in range(0, h.shape[0], ROW_CHUNK):
|
|
296
|
+
out[r0 : r0 + ROW_CHUNK].index_copy_(1, idx, head(h[r0 : r0 + ROW_CHUNK]).to(out.dtype))
|
|
297
|
+
return out.reshape(*hidden_states.shape[:-1], vocab)
|
|
298
|
+
|
|
299
|
+
def get_top_tokens(hidden_states, *args, **kwargs):
|
|
300
|
+
h = hidden_states.reshape(-1, hidden_states.shape[-1])
|
|
301
|
+
top = torch.cat([head(h[r0 : r0 + ROW_CHUNK]).argmax(dim=-1) for r0 in range(0, max(1, h.shape[0]), ROW_CHUNK)])
|
|
302
|
+
return idx[top].reshape(hidden_states.shape[:-1])
|
|
303
|
+
|
|
304
|
+
draft_model.compute_logits = compute_logits
|
|
305
|
+
if hasattr(draft_model, "get_top_tokens"):
|
|
306
|
+
draft_model.get_top_tokens = get_top_tokens
|
|
307
|
+
draft_model._mjolnir_draft_vocab = int(idx.numel())
|
|
308
|
+
_log.info(
|
|
309
|
+
"Mjolnir draft vocab: drafter logits over %d of %d tokens (%s head, %.0f MB)",
|
|
310
|
+
idx.numel(),
|
|
311
|
+
vocab,
|
|
312
|
+
"fp8" if fp8 else "bf16",
|
|
313
|
+
sub.numel() * (1 if fp8 else sub.element_size()) / 2**20,
|
|
314
|
+
)
|
|
315
|
+
return True
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# ── the hook ────────────────────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _lm_heads(model) -> list:
|
|
322
|
+
seen, out = set(), []
|
|
323
|
+
for name, mod in model.named_modules():
|
|
324
|
+
if name.split(".")[-1] == "lm_head" and hasattr(mod, "quant_method") and id(mod) not in seen:
|
|
325
|
+
seen.add(id(mod))
|
|
326
|
+
out.append(mod)
|
|
327
|
+
return out
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def after_load(runner) -> None:
|
|
331
|
+
"""Apply the opted-in experiments to a loaded GPU model runner."""
|
|
332
|
+
target = getattr(runner, "model", None)
|
|
333
|
+
spec = getattr(runner, "speculator", None)
|
|
334
|
+
draft = getattr(spec, "model", None) if spec is not None else None
|
|
335
|
+
vocab_path = os.environ.get(ENV_DRAFT_VOCAB, "").strip()
|
|
336
|
+
if vocab_path and draft is not None: # before fp8 may free the bf16 weight
|
|
337
|
+
try:
|
|
338
|
+
size = int(os.environ.get(ENV_DRAFT_VOCAB_SIZE, "0") or 0) or None
|
|
339
|
+
install_draft_vocab(draft, load_vocab(vocab_path), size)
|
|
340
|
+
except Exception as e: # noqa: BLE001 - an experiment never breaks serving
|
|
341
|
+
_log.warning("Mjolnir draft vocab: not installed (%r)", e)
|
|
342
|
+
elif vocab_path:
|
|
343
|
+
_log.warning("Mjolnir draft vocab: %s set but this config has no drafter", ENV_DRAFT_VOCAB)
|
|
344
|
+
if os.environ.get(ENV_LM_HEAD, "").strip().lower() == "fp8":
|
|
345
|
+
for model in (target, draft):
|
|
346
|
+
if model is None:
|
|
347
|
+
continue
|
|
348
|
+
for head in _lm_heads(model):
|
|
349
|
+
try:
|
|
350
|
+
install_fp8_head(head, target)
|
|
351
|
+
except Exception as e: # noqa: BLE001
|
|
352
|
+
_log.warning("Mjolnir lm_head fp8: not installed (%r)", e)
|
|
353
|
+
if torch.cuda.is_available():
|
|
354
|
+
# hand the quantization temporaries back before vLLM profiles memory
|
|
355
|
+
# for the KV cache (a cached-but-free block still counts as used)
|
|
356
|
+
torch.cuda.empty_cache()
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def install_hooks() -> None:
|
|
360
|
+
"""Wrap GPUModelRunner.load_model (idempotent; no-op unless opted in)."""
|
|
361
|
+
if not wanted():
|
|
362
|
+
return
|
|
363
|
+
try:
|
|
364
|
+
from vllm.v1.worker.gpu import model_runner as mr
|
|
365
|
+
except Exception as e: # noqa: BLE001
|
|
366
|
+
_log.warning("Mjolnir lm_head hooks: vLLM's GPU model runner not importable (%r)", e)
|
|
367
|
+
return
|
|
368
|
+
cls = mr.GPUModelRunner
|
|
369
|
+
if getattr(cls, "_mjolnir_lmhead_hooked", False):
|
|
370
|
+
return
|
|
371
|
+
orig = cls.load_model
|
|
372
|
+
|
|
373
|
+
def load_model(self, *args, **kwargs):
|
|
374
|
+
out = orig(self, *args, **kwargs)
|
|
375
|
+
after_load(self)
|
|
376
|
+
return out
|
|
377
|
+
|
|
378
|
+
cls.load_model = load_model
|
|
379
|
+
cls._mjolnir_lmhead_hooked = True
|
mjolnir/qpack.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Fused dynamic fp8 Q quantization + packing for the Mjolnir decode kernel.
|
|
2
|
+
|
|
3
|
+
The kernel reads Q as ``(b, h_k, n_q, d)`` e4m3 with one descale per
|
|
4
|
+
``(sequence, KV head)``, where row ``n = qi * h_r + r`` holds query token
|
|
5
|
+
``qi`` of KV-head group member ``r``. vLLM hands Q over as ``(tokens, h_q, d)``
|
|
6
|
+
bf16/fp16, any token and head stride (often a view of the fused QKV buffer).
|
|
7
|
+
|
|
8
|
+
Doing that with PyTorch ops costs ~10 launches per layer (cast, abs, amax,
|
|
9
|
+
clamp, divide, copy, divide, cast, permute-copy, copy) — at short contexts
|
|
10
|
+
more GPU time than the attention kernel saves. ``pack_q_fp8`` is one Triton
|
|
11
|
+
launch: one program per (sequence, KV head) loads its ``q_len * h_r * d``
|
|
12
|
+
values once, takes the amax, writes the descale and the packed e4m3 rows.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import torch
|
|
18
|
+
|
|
19
|
+
try: # Triton ships with vLLM / torch on CUDA; the harness may run without it
|
|
20
|
+
import triton
|
|
21
|
+
import triton.language as tl
|
|
22
|
+
except ImportError: # pragma: no cover - no Triton: the torch path is used
|
|
23
|
+
triton = None
|
|
24
|
+
tl = None
|
|
25
|
+
|
|
26
|
+
FP8_MAX = 448.0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def available() -> bool:
|
|
30
|
+
return triton is not None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if triton is not None:
|
|
34
|
+
|
|
35
|
+
@triton.jit
|
|
36
|
+
def _pack_q_fp8_kernel(
|
|
37
|
+
q_ptr,
|
|
38
|
+
out_ptr,
|
|
39
|
+
qd_ptr,
|
|
40
|
+
mul_ptr,
|
|
41
|
+
stride_t,
|
|
42
|
+
stride_h,
|
|
43
|
+
q_len: tl.constexpr,
|
|
44
|
+
h_k: tl.constexpr,
|
|
45
|
+
h_r: tl.constexpr,
|
|
46
|
+
d: tl.constexpr,
|
|
47
|
+
BLOCK_N: tl.constexpr,
|
|
48
|
+
BLOCK_D: tl.constexpr,
|
|
49
|
+
FP8_MAX_C: tl.constexpr,
|
|
50
|
+
HAS_MUL: tl.constexpr,
|
|
51
|
+
):
|
|
52
|
+
b = tl.program_id(0)
|
|
53
|
+
k = tl.program_id(1)
|
|
54
|
+
n_q = q_len * h_r
|
|
55
|
+
rows = tl.arange(0, BLOCK_N) # n = qi * h_r + r
|
|
56
|
+
cols = tl.arange(0, BLOCK_D)
|
|
57
|
+
qi = rows // h_r
|
|
58
|
+
r = rows % h_r
|
|
59
|
+
token = b * q_len + qi
|
|
60
|
+
head = k * h_r + r
|
|
61
|
+
mask = (rows[:, None] < n_q) & (cols[None, :] < d)
|
|
62
|
+
src = q_ptr + token[:, None] * stride_t + head[:, None] * stride_h + cols[None, :]
|
|
63
|
+
x = tl.load(src, mask=mask, other=0.0).to(tl.float32)
|
|
64
|
+
if HAS_MUL: # per-(q head, channel) factor: fp6 K channel scales folded into Q
|
|
65
|
+
x = x * tl.load(mul_ptr + head[:, None] * d + cols[None, :], mask=mask, other=0.0)
|
|
66
|
+
amax = tl.maximum(tl.max(tl.max(tl.abs(x), axis=1), axis=0), 1e-6)
|
|
67
|
+
scale = amax / FP8_MAX_C
|
|
68
|
+
tl.store(qd_ptr + b * h_k + k, scale)
|
|
69
|
+
y = tl.minimum(tl.maximum(x / scale, -FP8_MAX_C), FP8_MAX_C)
|
|
70
|
+
dst = out_ptr + ((b * h_k + k) * n_q + rows[:, None]) * d + cols[None, :]
|
|
71
|
+
tl.store(dst, y.to(out_ptr.dtype.element_ty), mask=mask)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def pack_q_fp8(
|
|
75
|
+
q3: torch.Tensor,
|
|
76
|
+
b: int,
|
|
77
|
+
q_len: int,
|
|
78
|
+
h_k: int,
|
|
79
|
+
h_r: int,
|
|
80
|
+
out: torch.Tensor,
|
|
81
|
+
qd: torch.Tensor,
|
|
82
|
+
mul: torch.Tensor | None = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Quantize ``q3`` (``(b * q_len, h_k * h_r, d)``, unit stride on ``d``)
|
|
85
|
+
into ``out`` (contiguous ``(b, h_k, q_len * h_r, d)`` e4m3) and ``qd``
|
|
86
|
+
(contiguous ``(b, h_k)`` fp32 descales: amax / 448). ``mul`` (contiguous
|
|
87
|
+
``(h_k * h_r, d)`` fp32) multiplies Q before quantization. One launch."""
|
|
88
|
+
d = q3.shape[-1]
|
|
89
|
+
assert q3.stride(-1) == 1, "Q needs a unit stride on head_dim"
|
|
90
|
+
assert out.is_contiguous() and qd.is_contiguous()
|
|
91
|
+
if mul is not None:
|
|
92
|
+
assert mul.is_contiguous() and mul.dtype == torch.float32 and mul.numel() == h_k * h_r * d
|
|
93
|
+
n_q = q_len * h_r
|
|
94
|
+
block_n = triton.next_power_of_2(n_q)
|
|
95
|
+
block_d = triton.next_power_of_2(d)
|
|
96
|
+
warps = 8 if block_n * block_d >= 4096 else 4
|
|
97
|
+
_pack_q_fp8_kernel[(b, h_k)](
|
|
98
|
+
q3,
|
|
99
|
+
out,
|
|
100
|
+
qd,
|
|
101
|
+
mul if mul is not None else qd,
|
|
102
|
+
q3.stride(0),
|
|
103
|
+
q3.stride(1),
|
|
104
|
+
q_len=q_len,
|
|
105
|
+
h_k=h_k,
|
|
106
|
+
h_r=h_r,
|
|
107
|
+
d=d,
|
|
108
|
+
BLOCK_N=block_n,
|
|
109
|
+
BLOCK_D=block_d,
|
|
110
|
+
FP8_MAX_C=FP8_MAX,
|
|
111
|
+
HAS_MUL=mul is not None,
|
|
112
|
+
num_warps=warps,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def pack_q_fp8_reference(q3: torch.Tensor, b: int, q_len: int, h_k: int, h_r: int, mul: torch.Tensor | None = None):
|
|
117
|
+
"""The PyTorch formulation (the runner's fallback, and the test oracle)."""
|
|
118
|
+
d = q3.shape[-1]
|
|
119
|
+
qg = q3[: b * q_len].float()
|
|
120
|
+
if mul is not None:
|
|
121
|
+
qg = qg * mul.view(h_k * h_r, d)
|
|
122
|
+
qg = qg.reshape(b, q_len, h_k, h_r, d)
|
|
123
|
+
amax = qg.abs().amax(dim=(1, 3, 4)).clamp_min(1e-6) # (b, h_k)
|
|
124
|
+
scale = amax / FP8_MAX
|
|
125
|
+
packed = (
|
|
126
|
+
(qg / scale[:, None, :, None, None])
|
|
127
|
+
.clamp(-FP8_MAX, FP8_MAX)
|
|
128
|
+
.to(torch.float8_e4m3fn)
|
|
129
|
+
.permute(0, 2, 1, 3, 4)
|
|
130
|
+
.reshape(b, h_k, q_len * h_r, d)
|
|
131
|
+
)
|
|
132
|
+
return packed, scale
|