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/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