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/runner.py
ADDED
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
"""T5 paged fp8 decode-attention runner (framework-agnostic).
|
|
2
|
+
|
|
3
|
+
Owns everything the vLLM backend needs that is not vLLM-specific:
|
|
4
|
+
* the occupancy-pinned split plan, written into FIXED-ADDRESS device
|
|
5
|
+
buffers and padded to a FIXED grid size per q_len -> CUDA-graph safe
|
|
6
|
+
* a process-wide cache of compiled kernel variants (+ precompile)
|
|
7
|
+
* the launch: zero-copy Q/O views for q_len == 1, packing for q_len > 1,
|
|
8
|
+
per-tensor / per-head / per-(seq, head) descales via strides, optional
|
|
9
|
+
dynamic fp8 Q quantization
|
|
10
|
+
|
|
11
|
+
Nothing here imports vLLM, so tests/test_runner.py exercises it directly.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import math
|
|
18
|
+
import os
|
|
19
|
+
import threading
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
|
|
22
|
+
import torch
|
|
23
|
+
|
|
24
|
+
from . import qpack
|
|
25
|
+
from . import t5_decode as t5
|
|
26
|
+
|
|
27
|
+
FP8_MAX = 448.0
|
|
28
|
+
_log = logging.getLogger("vllm.mjolnir")
|
|
29
|
+
SUPPORTED_PAGE_SIZES = (16, 32, 64, 128)
|
|
30
|
+
|
|
31
|
+
_TORCH_TO_CUTLASS = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _cutlass_dtype(dt: torch.dtype):
|
|
35
|
+
global _TORCH_TO_CUTLASS
|
|
36
|
+
if _TORCH_TO_CUTLASS is None:
|
|
37
|
+
import cutlass
|
|
38
|
+
|
|
39
|
+
_TORCH_TO_CUTLASS = {
|
|
40
|
+
torch.bfloat16: cutlass.BFloat16,
|
|
41
|
+
torch.float16: cutlass.Float16,
|
|
42
|
+
torch.float32: cutlass.Float32,
|
|
43
|
+
torch.int32: cutlass.Int32,
|
|
44
|
+
torch.float8_e4m3fn: cutlass.Float8E4M3FN,
|
|
45
|
+
}
|
|
46
|
+
return _TORCH_TO_CUTLASS[dt]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _ptr(t: torch.Tensor):
|
|
50
|
+
"""Raw cute pointer for a tensor (cheap; no dlpack round trip)."""
|
|
51
|
+
import cutlass.cute as cute
|
|
52
|
+
|
|
53
|
+
assert t.data_ptr() % 16 == 0, "T5 kernel needs 16-byte aligned base pointers"
|
|
54
|
+
dtype = _cutlass_dtype(t.dtype)
|
|
55
|
+
try:
|
|
56
|
+
from cutlass.cute.runtime import make_ptr
|
|
57
|
+
|
|
58
|
+
return make_ptr(dtype, t.data_ptr(), cute.AddressSpace.gmem, assumed_align=16)
|
|
59
|
+
except ImportError: # older DSL: go through dlpack
|
|
60
|
+
from cutlass.cute.runtime import from_dlpack
|
|
61
|
+
|
|
62
|
+
src = t.view(torch.uint8) if t.dtype == torch.float8_e4m3fn else t
|
|
63
|
+
ct = from_dlpack(src, assumed_align=16)
|
|
64
|
+
ct.element_type = dtype
|
|
65
|
+
return ct.iterator
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _kv_cutlass_dtype(kv_format: str):
|
|
69
|
+
import cutlass
|
|
70
|
+
|
|
71
|
+
return {"fp8": cutlass.Float8E4M3FN, "e2m3": cutlass.Float6E2M3FN}[kv_format]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _stream():
|
|
75
|
+
import cuda.bindings.driver as cuda
|
|
76
|
+
|
|
77
|
+
return cuda.CUstream(torch.cuda.current_stream().cuda_stream)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def tile_width(n_q: int, fp8_q: bool) -> int:
|
|
81
|
+
"""Rows per CTA tile: next power of two >= n_q, min 8 (bf16) / 16 (fp8), max 32."""
|
|
82
|
+
base = 16 if fp8_q else 8
|
|
83
|
+
return max(base, min(32, 1 << max(0, (n_q - 1).bit_length())))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ----------------------------------------------------------------------------
|
|
87
|
+
# Compiled-kernel cache (process wide; shared by all layers / builders)
|
|
88
|
+
# ----------------------------------------------------------------------------
|
|
89
|
+
_COMPILED: dict = {}
|
|
90
|
+
_COMPILE_LOCK = threading.Lock()
|
|
91
|
+
|
|
92
|
+
# ----------------------------------------------------------------------------
|
|
93
|
+
# On-disk kernel cache: a compiled variant is exported as an object file
|
|
94
|
+
# (CuTe DSL export_to_c) and loaded back on the next server start
|
|
95
|
+
# (cute.runtime.load_module), skipping the ~2-3 s compile per variant.
|
|
96
|
+
# MJOLNIR_ATTN_KERNEL_CACHE: unset/1 = on (under $VLLM_CACHE_ROOT/mjolnir or
|
|
97
|
+
# ~/.cache/mjolnir), 0 = off, a path = that directory. The entry key covers
|
|
98
|
+
# the variant, the kernel source, the CuTe DSL stack and the GPU, so a new
|
|
99
|
+
# kernel, image or DSL never loads a stale object. Best effort: any failure
|
|
100
|
+
# compiles in-process as before and turns the cache off for the process.
|
|
101
|
+
# ----------------------------------------------------------------------------
|
|
102
|
+
_DISK_MODULES: dict = {} # keep loaded modules alive
|
|
103
|
+
CACHE_STATS = {"loaded": 0, "compiled": 0}
|
|
104
|
+
_disk_cache_ok = True
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def kernel_cache_dir():
|
|
108
|
+
from pathlib import Path
|
|
109
|
+
|
|
110
|
+
v = env_flag("MJOLNIR_ATTN_KERNEL_CACHE")
|
|
111
|
+
if v.lower() in ("0", "false", "no", "off"):
|
|
112
|
+
return None
|
|
113
|
+
if v and v.lower() not in ("1", "true", "yes", "on"):
|
|
114
|
+
return Path(v)
|
|
115
|
+
base = os.environ.get("VLLM_CACHE_ROOT") or os.path.join(os.path.expanduser("~"), ".cache")
|
|
116
|
+
return Path(base) / "mjolnir" / "kernels"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
_FINGERPRINT: str | None = None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _cache_fingerprint() -> str:
|
|
123
|
+
"""Everything a compiled object depends on besides the variant key."""
|
|
124
|
+
global _FINGERPRINT
|
|
125
|
+
if _FINGERPRINT is None:
|
|
126
|
+
import hashlib
|
|
127
|
+
import importlib.metadata
|
|
128
|
+
|
|
129
|
+
from . import __version__
|
|
130
|
+
|
|
131
|
+
h = hashlib.sha256()
|
|
132
|
+
with open(t5.__file__, "rb") as f:
|
|
133
|
+
h.update(f.read())
|
|
134
|
+
dsl = sorted(
|
|
135
|
+
f"{d.metadata['Name']}=={d.version}"
|
|
136
|
+
for d in importlib.metadata.distributions()
|
|
137
|
+
if (d.metadata["Name"] or "").lower().startswith("nvidia-cutlass-dsl")
|
|
138
|
+
)
|
|
139
|
+
cap = torch.cuda.get_device_capability() if torch.cuda.is_available() else (0, 0)
|
|
140
|
+
h.update(f"{__version__}|{';'.join(dsl)}|sm{cap[0]}{cap[1]}".encode())
|
|
141
|
+
_FINGERPRINT = h.hexdigest()[:20]
|
|
142
|
+
return _FINGERPRINT
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _cache_entry(key):
|
|
146
|
+
import hashlib
|
|
147
|
+
|
|
148
|
+
d = kernel_cache_dir()
|
|
149
|
+
if d is None:
|
|
150
|
+
return None
|
|
151
|
+
kh = hashlib.sha256(repr(key).encode()).hexdigest()[:20]
|
|
152
|
+
return d / _cache_fingerprint() / f"t5_{kh}.o", f"mjolnir_t5_{kh}"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _disk_load(key):
|
|
156
|
+
global _disk_cache_ok
|
|
157
|
+
if not _disk_cache_ok:
|
|
158
|
+
return None
|
|
159
|
+
try:
|
|
160
|
+
entry = _cache_entry(key)
|
|
161
|
+
if entry is None or not entry[0].exists():
|
|
162
|
+
return None
|
|
163
|
+
import cutlass.cute as cute
|
|
164
|
+
|
|
165
|
+
module = cute.runtime.load_module(str(entry[0]))
|
|
166
|
+
fn = getattr(module, entry[1])
|
|
167
|
+
_DISK_MODULES[key] = module
|
|
168
|
+
_log.debug("Mjolnir: loaded %s from %s", key, entry[0])
|
|
169
|
+
return fn
|
|
170
|
+
except Exception as e: # noqa: BLE001 - any failure: compile instead
|
|
171
|
+
_disk_cache_ok = False
|
|
172
|
+
_log.warning("Mjolnir: kernel cache load failed (%r); compiling, cache off for this process", e)
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _export(fn, path, symbol: str) -> None:
|
|
177
|
+
"""Write ``fn``'s object file to ``path`` (atomically), exported under
|
|
178
|
+
``symbol``. CuTe DSL 4.7 (vLLM 0.31's): ``export_to_c(dir, stem,
|
|
179
|
+
function_prefix=...)`` writes ``<dir>/<stem>.o`` + ``.h``; later DSLs:
|
|
180
|
+
``export_to_c(path, function_name=...)``."""
|
|
181
|
+
import inspect
|
|
182
|
+
|
|
183
|
+
params = inspect.signature(fn.export_to_c).parameters
|
|
184
|
+
stem = f"{path.stem}.tmp{os.getpid()}"
|
|
185
|
+
tmp_o, tmp_h = path.parent / f"{stem}.o", path.parent / f"{stem}.h"
|
|
186
|
+
try:
|
|
187
|
+
if "function_prefix" in params:
|
|
188
|
+
fn.export_to_c(str(path.parent), stem, function_prefix=symbol)
|
|
189
|
+
elif "function_name" in params:
|
|
190
|
+
fn.export_to_c(str(tmp_o), function_name=symbol)
|
|
191
|
+
else:
|
|
192
|
+
raise TypeError(f"unknown export_to_c signature {tuple(params)}")
|
|
193
|
+
os.replace(tmp_o, path)
|
|
194
|
+
finally:
|
|
195
|
+
for t in (tmp_o, tmp_h):
|
|
196
|
+
if t.exists():
|
|
197
|
+
t.unlink()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _disk_store(key, fn) -> None:
|
|
201
|
+
global _disk_cache_ok
|
|
202
|
+
if not _disk_cache_ok:
|
|
203
|
+
return
|
|
204
|
+
try:
|
|
205
|
+
entry = _cache_entry(key)
|
|
206
|
+
if entry is None:
|
|
207
|
+
return
|
|
208
|
+
path, symbol = entry
|
|
209
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
_export(fn, path, symbol)
|
|
211
|
+
except Exception as e: # noqa: BLE001 - persistence is best effort
|
|
212
|
+
_disk_cache_ok = False
|
|
213
|
+
_log.warning("Mjolnir: kernel cache store failed (%r); cache off for this process", e)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@dataclass(frozen=True)
|
|
217
|
+
class VariantKey:
|
|
218
|
+
q_dtype: torch.dtype # bfloat16 | float8_e4m3fn
|
|
219
|
+
out_dtype: torch.dtype
|
|
220
|
+
head_size: int
|
|
221
|
+
heads_q: int
|
|
222
|
+
heads_k: int
|
|
223
|
+
page_size: int
|
|
224
|
+
q_len: int
|
|
225
|
+
max_splits: int
|
|
226
|
+
with_reduction: bool
|
|
227
|
+
ctas_per_sm: int
|
|
228
|
+
kv_stages: int # 0 = kernel default
|
|
229
|
+
pdl: bool
|
|
230
|
+
l2_prefetch: int
|
|
231
|
+
kv_evict_first: bool
|
|
232
|
+
kv_format: str = "fp8" # "fp8" | "e2m3" (packed fp6, read by kind::f8f6f4)
|
|
233
|
+
causal: bool = True # False: parallel-drafter blocks (every query sees every key)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# KV formats the vLLM path serves: e4m3 bytes, or e2m3 codes packed 4 per 3
|
|
237
|
+
# bytes (little-endian bitstream, mjolnir.t5_decode.pack_subbyte).
|
|
238
|
+
RUNNER_KV_FORMATS = {"fp8": 8, "e2m3": 6}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@dataclass
|
|
242
|
+
class StepPlan:
|
|
243
|
+
"""Per-step plan shared by every layer of one KV-cache group."""
|
|
244
|
+
|
|
245
|
+
num_seqs: int
|
|
246
|
+
q_len: int
|
|
247
|
+
n_q: int
|
|
248
|
+
num_work: int # == grid.x, fixed per (q_len) capacity
|
|
249
|
+
with_reduction: bool
|
|
250
|
+
block_table: torch.Tensor # (>= num_seqs, max_blocks) int32, device
|
|
251
|
+
seq_lens: torch.Tensor # (>= num_seqs,) int32, device
|
|
252
|
+
work: torch.Tensor # (num_work, 4) int32 view of a persistent buffer
|
|
253
|
+
n_splits: torch.Tensor # (num_seqs,) int32 view of a persistent buffer
|
|
254
|
+
chunk_tiles: int
|
|
255
|
+
num_chunks: int
|
|
256
|
+
causal: bool = True
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class T5DecodeRunner:
|
|
260
|
+
def __init__(
|
|
261
|
+
self,
|
|
262
|
+
*,
|
|
263
|
+
num_heads: int,
|
|
264
|
+
num_kv_heads: int,
|
|
265
|
+
head_size: int,
|
|
266
|
+
page_size: int,
|
|
267
|
+
out_dtype: torch.dtype,
|
|
268
|
+
max_num_seqs: int,
|
|
269
|
+
max_q_len: int = 1,
|
|
270
|
+
q_mode: str = "bf16", # "bf16" (Q as is) | "fp8" (dynamic per (seq, kv head))
|
|
271
|
+
ctas_per_sm: int = 2,
|
|
272
|
+
max_splits: int = 16,
|
|
273
|
+
kv_stages: int = 0,
|
|
274
|
+
pdl: bool = False, # programmatic dependent launch (decode + reduction)
|
|
275
|
+
l2_prefetch: int = 0, # TMA L2 prefetch distance in 128-row KV tiles
|
|
276
|
+
kv_evict_first: bool = False, # EVICT_FIRST L2 policy on K/V TMA loads
|
|
277
|
+
kv_format: str = "fp8", # "fp8" | "e2m3" (fp8 Q only)
|
|
278
|
+
device: torch.device | str = "cuda",
|
|
279
|
+
sm_count: int | None = None,
|
|
280
|
+
):
|
|
281
|
+
assert num_heads % num_kv_heads == 0
|
|
282
|
+
assert head_size % 128 == 0, "T5 decode needs head_size % 128 == 0"
|
|
283
|
+
assert page_size in SUPPORTED_PAGE_SIZES, f"page size {page_size} unsupported"
|
|
284
|
+
assert q_mode in ("bf16", "fp8")
|
|
285
|
+
assert out_dtype in (torch.bfloat16, torch.float16)
|
|
286
|
+
self.h_q, self.h_k, self.d = num_heads, num_kv_heads, head_size
|
|
287
|
+
self.h_r = num_heads // num_kv_heads
|
|
288
|
+
self.page_size = page_size
|
|
289
|
+
self.out_dtype = out_dtype
|
|
290
|
+
self.max_num_seqs = max_num_seqs
|
|
291
|
+
self.max_q_len = max_q_len
|
|
292
|
+
self.fp8_q = q_mode == "fp8"
|
|
293
|
+
self.ctas_per_sm = ctas_per_sm
|
|
294
|
+
self.max_splits = max_splits
|
|
295
|
+
self.kv_stages = kv_stages
|
|
296
|
+
self.pdl = bool(pdl)
|
|
297
|
+
self.l2_prefetch = int(l2_prefetch)
|
|
298
|
+
self.kv_evict_first = bool(kv_evict_first)
|
|
299
|
+
assert kv_format in RUNNER_KV_FORMATS, f"kv_format must be one of {tuple(RUNNER_KV_FORMATS)}"
|
|
300
|
+
assert kv_format == "fp8" or q_mode == "fp8", f"{kv_format} KV runs on the fp8-Q path only"
|
|
301
|
+
self.kv_format = kv_format
|
|
302
|
+
self.kv_bits = RUNNER_KV_FORMATS[kv_format]
|
|
303
|
+
self.kv_row_bytes = head_size * self.kv_bits // 8 # bytes of one K (or V) row
|
|
304
|
+
self.device = torch.device(device)
|
|
305
|
+
if sm_count is None:
|
|
306
|
+
sm_count = torch.cuda.get_device_properties(self.device).multi_processor_count
|
|
307
|
+
self.sm_count = sm_count
|
|
308
|
+
|
|
309
|
+
# Capacity per q_len: work items that fit one wave (slots) or one per
|
|
310
|
+
# sequence, whichever is larger. Grid.x is always this -> fixed grid.
|
|
311
|
+
self._slots = {ql: self.slots(ql) for ql in range(1, max_q_len + 1)}
|
|
312
|
+
self._cap = {ql: max(self._slots[ql], max_num_seqs) for ql in self._slots}
|
|
313
|
+
max_cap = max(self._cap.values())
|
|
314
|
+
max_slots = max(self._slots.values())
|
|
315
|
+
nq_max = max_q_len * self.h_r
|
|
316
|
+
dev = self.device
|
|
317
|
+
|
|
318
|
+
# Persistent (fixed-address) buffers
|
|
319
|
+
self.work_buf = torch.zeros(max_cap, 4, dtype=torch.int32, device=dev)
|
|
320
|
+
self.nsplit_buf = torch.ones(max_num_seqs, dtype=torch.int32, device=dev)
|
|
321
|
+
self.lse_buf = torch.empty(max_num_seqs * self.h_k * nq_max, dtype=torch.float32, device=dev)
|
|
322
|
+
# Partials are only written for sequences the plan split, and the plan
|
|
323
|
+
# splits only when num_seqs < slots -> size them by slots, not batch.
|
|
324
|
+
part = max_splits * max_slots * self.h_k * nq_max
|
|
325
|
+
self.o_part = torch.empty(part * head_size, dtype=torch.float32, device=dev)
|
|
326
|
+
self.m_part = torch.empty(part, dtype=torch.float32, device=dev)
|
|
327
|
+
self.l_part = torch.empty(part, dtype=torch.float32, device=dev)
|
|
328
|
+
# Pack buffers: used for q_len > 1, fp8 Q, or a Q/O whose head stride
|
|
329
|
+
# is not d (always allocated so any vLLM view layout works).
|
|
330
|
+
rows = max_num_seqs * self.h_k * nq_max * head_size
|
|
331
|
+
q_pack_dtype = torch.float8_e4m3fn if self.fp8_q else torch.bfloat16
|
|
332
|
+
self.q_pack = torch.empty(rows, dtype=q_pack_dtype, device=dev)
|
|
333
|
+
self.o_pack = torch.empty(rows, dtype=out_dtype, device=dev)
|
|
334
|
+
# placeholder for the kernel's nvfp4 scale pointers (the vLLM path
|
|
335
|
+
# runs the fp8 KV format)
|
|
336
|
+
self.dummy_scales = torch.zeros(16, dtype=torch.float8_e4m3fn, device=dev)
|
|
337
|
+
self.q_descale = (
|
|
338
|
+
torch.ones(max_num_seqs, self.h_k, dtype=torch.float32, device=dev)
|
|
339
|
+
if self.fp8_q
|
|
340
|
+
else torch.ones(1, dtype=torch.float32, device=dev)
|
|
341
|
+
)
|
|
342
|
+
# One fused Triton launch for the fp8 Q quantization (MJOLNIR_ATTN_FUSED_QPACK=0:
|
|
343
|
+
# the PyTorch ops, ~10 launches per layer and step).
|
|
344
|
+
self._fused_qpack = (
|
|
345
|
+
self.fp8_q
|
|
346
|
+
and self.device.type == "cuda"
|
|
347
|
+
and qpack.available()
|
|
348
|
+
and env_flag("MJOLNIR_ATTN_FUSED_QPACK", "1").lower() not in ("0", "false", "no")
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# ------------------------------------------------------------------ plan
|
|
352
|
+
def n_tiles(self, q_len: int) -> int:
|
|
353
|
+
n_q = q_len * self.h_r
|
|
354
|
+
return math.ceil(n_q / tile_width(n_q, self.fp8_q))
|
|
355
|
+
|
|
356
|
+
def slots(self, q_len: int) -> int:
|
|
357
|
+
return max(1, (self.sm_count * self.ctas_per_sm) // (self.h_k * self.n_tiles(q_len)))
|
|
358
|
+
|
|
359
|
+
def plan(
|
|
360
|
+
self,
|
|
361
|
+
seq_lens_cpu, # sequence of ints (len >= num_seqs)
|
|
362
|
+
num_seqs: int,
|
|
363
|
+
q_len: int,
|
|
364
|
+
block_table: torch.Tensor,
|
|
365
|
+
seq_lens: torch.Tensor,
|
|
366
|
+
margin: int = 0,
|
|
367
|
+
causal: bool = True,
|
|
368
|
+
) -> StepPlan:
|
|
369
|
+
"""CPU plan + async copy into the persistent device buffers.
|
|
370
|
+
|
|
371
|
+
``margin`` plans every sequence that many tokens longer: the kernel
|
|
372
|
+
masks with the device ``seq_lens``, so the plan stays valid while
|
|
373
|
+
they grow by up to ``margin`` (fused multi-step draft decode)."""
|
|
374
|
+
assert 1 <= q_len <= self.max_q_len and num_seqs <= self.max_num_seqs
|
|
375
|
+
lens = [int(x) + margin for x in seq_lens_cpu[:num_seqs]]
|
|
376
|
+
cap = self._cap[q_len]
|
|
377
|
+
p = t5.plan_split_kv(
|
|
378
|
+
lens,
|
|
379
|
+
self.h_k,
|
|
380
|
+
self.n_tiles(q_len),
|
|
381
|
+
self.sm_count,
|
|
382
|
+
ctas_per_sm=self.ctas_per_sm,
|
|
383
|
+
max_splits=self.max_splits,
|
|
384
|
+
pad_to=cap,
|
|
385
|
+
)
|
|
386
|
+
# Pinned staging from the caching host allocator: safe to drop after
|
|
387
|
+
# a non_blocking copy (the allocator tracks the stream use).
|
|
388
|
+
work_h = torch.tensor(p.work, dtype=torch.int32).pin_memory()
|
|
389
|
+
ns_h = torch.tensor(p.n_splits, dtype=torch.int32).pin_memory()
|
|
390
|
+
self.work_buf[:cap].copy_(work_h, non_blocking=True)
|
|
391
|
+
self.nsplit_buf[:num_seqs].copy_(ns_h, non_blocking=True)
|
|
392
|
+
# Reduction launch is decided per batch SIZE (deterministic for a
|
|
393
|
+
# captured CUDA graph): a batch below one wave may split, a full
|
|
394
|
+
# wave never does.
|
|
395
|
+
with_reduction = self.max_splits > 1 and num_seqs < self._slots[q_len]
|
|
396
|
+
return StepPlan(
|
|
397
|
+
num_seqs=num_seqs,
|
|
398
|
+
q_len=q_len,
|
|
399
|
+
n_q=q_len * self.h_r,
|
|
400
|
+
num_work=cap,
|
|
401
|
+
with_reduction=with_reduction,
|
|
402
|
+
block_table=block_table,
|
|
403
|
+
seq_lens=seq_lens,
|
|
404
|
+
work=self.work_buf[:cap],
|
|
405
|
+
n_splits=self.nsplit_buf[:num_seqs],
|
|
406
|
+
chunk_tiles=p.chunk_tiles,
|
|
407
|
+
num_chunks=sum(1 for w in p.work if w[2] > w[1]),
|
|
408
|
+
causal=causal or q_len == 1, # one query token: the same kernel either way
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# --------------------------------------------------------------- compile
|
|
412
|
+
def _key(self, q_len: int, with_reduction: bool, causal: bool = True) -> VariantKey:
|
|
413
|
+
return VariantKey(
|
|
414
|
+
q_dtype=torch.float8_e4m3fn if self.fp8_q else torch.bfloat16,
|
|
415
|
+
out_dtype=self.out_dtype,
|
|
416
|
+
head_size=self.d,
|
|
417
|
+
heads_q=self.h_q,
|
|
418
|
+
heads_k=self.h_k,
|
|
419
|
+
page_size=self.page_size,
|
|
420
|
+
q_len=q_len,
|
|
421
|
+
max_splits=self.max_splits,
|
|
422
|
+
with_reduction=with_reduction,
|
|
423
|
+
ctas_per_sm=self.ctas_per_sm,
|
|
424
|
+
kv_stages=self.kv_stages,
|
|
425
|
+
pdl=self.pdl,
|
|
426
|
+
l2_prefetch=self.l2_prefetch,
|
|
427
|
+
kv_evict_first=self.kv_evict_first,
|
|
428
|
+
kv_format=self.kv_format,
|
|
429
|
+
causal=causal or q_len == 1,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
def _make_kernel(self, key: VariantKey):
|
|
433
|
+
n_q = key.q_len * self.h_r
|
|
434
|
+
fp8 = key.q_dtype == torch.float8_e4m3fn
|
|
435
|
+
if fp8:
|
|
436
|
+
cw = 0
|
|
437
|
+
elif self.d > 128 and key.ctas_per_sm == 1:
|
|
438
|
+
cw = 2
|
|
439
|
+
else:
|
|
440
|
+
cw = 1
|
|
441
|
+
k = t5.MixedInputFusedMultiHeadAttentionDecode(
|
|
442
|
+
headdim=self.d,
|
|
443
|
+
grouped_head_tile=tile_width(n_q, fp8),
|
|
444
|
+
convert_warpgroups=cw,
|
|
445
|
+
page_size=key.page_size,
|
|
446
|
+
q_len=key.q_len,
|
|
447
|
+
max_splits=key.max_splits,
|
|
448
|
+
ctas_per_sm=key.ctas_per_sm,
|
|
449
|
+
max_kv_stages=key.kv_stages,
|
|
450
|
+
with_reduction=key.with_reduction,
|
|
451
|
+
pdl=key.pdl,
|
|
452
|
+
l2_prefetch=key.l2_prefetch,
|
|
453
|
+
kv_evict_first=key.kv_evict_first,
|
|
454
|
+
kv_format=key.kv_format,
|
|
455
|
+
causal=key.causal,
|
|
456
|
+
)
|
|
457
|
+
import cutlass
|
|
458
|
+
|
|
459
|
+
k.can_implement(
|
|
460
|
+
1,
|
|
461
|
+
self.h_q,
|
|
462
|
+
self.h_k,
|
|
463
|
+
self.d,
|
|
464
|
+
cutlass.Float8E4M3FN if fp8 else cutlass.BFloat16,
|
|
465
|
+
_kv_cutlass_dtype(key.kv_format),
|
|
466
|
+
)
|
|
467
|
+
return k
|
|
468
|
+
|
|
469
|
+
def _get_compiled(self, key: VariantKey, args):
|
|
470
|
+
fn = _COMPILED.get(key)
|
|
471
|
+
if fn is not None:
|
|
472
|
+
return fn
|
|
473
|
+
with _COMPILE_LOCK:
|
|
474
|
+
fn = _COMPILED.get(key)
|
|
475
|
+
if fn is None:
|
|
476
|
+
if torch.cuda.is_current_stream_capturing():
|
|
477
|
+
raise RuntimeError(
|
|
478
|
+
f"T5 kernel variant {key} not compiled before CUDA-graph "
|
|
479
|
+
"capture; call precompile() (the vLLM backend does this)"
|
|
480
|
+
)
|
|
481
|
+
fn = _disk_load(key)
|
|
482
|
+
if fn is None:
|
|
483
|
+
import cutlass.cute as cute
|
|
484
|
+
|
|
485
|
+
kernel = self._make_kernel(key)
|
|
486
|
+
fn = cute.compile(kernel, *args, options="--opt-level 2")
|
|
487
|
+
CACHE_STATS["compiled"] += 1
|
|
488
|
+
_disk_store(key, fn)
|
|
489
|
+
else:
|
|
490
|
+
CACHE_STATS["loaded"] += 1
|
|
491
|
+
_COMPILED[key] = fn
|
|
492
|
+
return fn
|
|
493
|
+
|
|
494
|
+
def is_compiled(self, q_len: int, with_reduction: bool, causal: bool = True) -> bool:
|
|
495
|
+
return self._key(q_len, with_reduction, causal) in _COMPILED
|
|
496
|
+
|
|
497
|
+
def ensure_compiled(self, q_len: int, with_reduction: bool, causal: bool = True):
|
|
498
|
+
"""Compile one variant with tiny dummy tensors if it is missing.
|
|
499
|
+
Runtime shapes/strides/pointers are dynamic, so one compile per
|
|
500
|
+
(q_len, reduction) serves every batch and every layer. Must run
|
|
501
|
+
outside CUDA-graph capture (the vLLM builder calls it from build())."""
|
|
502
|
+
if self.is_compiled(q_len, with_reduction, causal):
|
|
503
|
+
return
|
|
504
|
+
dev = self.device
|
|
505
|
+
PS, d = self.page_size, self.d
|
|
506
|
+
n_q = q_len * self.h_r
|
|
507
|
+
if self.kv_format == "fp8":
|
|
508
|
+
kv = torch.zeros(1, PS, self.h_k, d, dtype=torch.float8_e4m3fn, device=dev)
|
|
509
|
+
else: # packed bytes, like vLLM's uint8 view
|
|
510
|
+
kv = torch.zeros(1, PS, self.h_k, self.kv_row_bytes, dtype=torch.uint8, device=dev)
|
|
511
|
+
one = torch.ones(1, dtype=torch.float32, device=dev)
|
|
512
|
+
q = torch.zeros(1, self.h_k, n_q, d, device=dev, dtype=torch.float8_e4m3fn if self.fp8_q else torch.bfloat16)
|
|
513
|
+
o = torch.empty(1, self.h_k, n_q, d, dtype=self.out_dtype, device=dev)
|
|
514
|
+
plan = StepPlan(
|
|
515
|
+
num_seqs=1,
|
|
516
|
+
q_len=q_len,
|
|
517
|
+
n_q=n_q,
|
|
518
|
+
num_work=1,
|
|
519
|
+
with_reduction=with_reduction,
|
|
520
|
+
block_table=torch.zeros(1, 1, dtype=torch.int32, device=dev),
|
|
521
|
+
seq_lens=torch.full((1,), PS, dtype=torch.int32, device=dev),
|
|
522
|
+
work=torch.tensor([[0, 0, 1, 0]], dtype=torch.int32, device=dev),
|
|
523
|
+
n_splits=torch.ones(1, dtype=torch.int32, device=dev),
|
|
524
|
+
chunk_tiles=1,
|
|
525
|
+
num_chunks=1,
|
|
526
|
+
causal=causal or q_len == 1,
|
|
527
|
+
)
|
|
528
|
+
self._launch(plan, q, o, kv, kv, one, one, one, 1.0, compile_only=True)
|
|
529
|
+
|
|
530
|
+
def precompile(self, q_lens=None):
|
|
531
|
+
"""Compile every variant this runner can launch."""
|
|
532
|
+
for ql in q_lens or range(1, self.max_q_len + 1):
|
|
533
|
+
for red in (False, True) if self.max_splits > 1 else (False,):
|
|
534
|
+
self.ensure_compiled(ql, red)
|
|
535
|
+
torch.cuda.synchronize(self.device)
|
|
536
|
+
|
|
537
|
+
# ---------------------------------------------------------------- launch
|
|
538
|
+
def _kv_ptr(self, t: torch.Tensor):
|
|
539
|
+
"""K/V pointer: e4m3 elements, or packed fp6 elements of a uint8 byte
|
|
540
|
+
view (the kernel converts byte strides to element strides)."""
|
|
541
|
+
if self.kv_format == "fp8":
|
|
542
|
+
return _ptr(t)
|
|
543
|
+
import cutlass.cute as cute
|
|
544
|
+
from cutlass.cute.runtime import make_ptr
|
|
545
|
+
|
|
546
|
+
assert t.dtype == torch.uint8 and t.data_ptr() % 32 == 0, "packed fp6 KV needs a 32-byte aligned uint8 view"
|
|
547
|
+
return make_ptr(_kv_cutlass_dtype(self.kv_format), t.data_ptr(), cute.AddressSpace.gmem, assumed_align=32)
|
|
548
|
+
|
|
549
|
+
def _launch(self, plan, q, o, k_cache, v_cache, qd, kd, vd, scale, compile_only=False):
|
|
550
|
+
b, n_q = plan.num_seqs, plan.n_q
|
|
551
|
+
strides = t5.kernel_strides(q, o, k_cache, v_cache, qd, kd, vd, kv_bits=self.kv_bits)
|
|
552
|
+
nqk = self.h_k * n_q
|
|
553
|
+
args = (
|
|
554
|
+
(b, self.h_k, n_q, self.d),
|
|
555
|
+
plan.block_table.stride(0), # block table row length (max_blocks)
|
|
556
|
+
k_cache.shape[0], # num_pages
|
|
557
|
+
plan.num_work,
|
|
558
|
+
strides,
|
|
559
|
+
_ptr(q),
|
|
560
|
+
self._kv_ptr(k_cache),
|
|
561
|
+
self._kv_ptr(v_cache),
|
|
562
|
+
_ptr(self.dummy_scales),
|
|
563
|
+
_ptr(self.dummy_scales), # nvfp4 scales (unused: fp8 KV)
|
|
564
|
+
_ptr(plan.block_table),
|
|
565
|
+
_ptr(plan.seq_lens),
|
|
566
|
+
_ptr(plan.work),
|
|
567
|
+
_ptr(plan.n_splits),
|
|
568
|
+
_ptr(qd),
|
|
569
|
+
_ptr(kd),
|
|
570
|
+
_ptr(vd),
|
|
571
|
+
_ptr(o),
|
|
572
|
+
_ptr(self.lse_buf[: max(1, b * nqk)]),
|
|
573
|
+
_ptr(self.o_part),
|
|
574
|
+
_ptr(self.m_part),
|
|
575
|
+
_ptr(self.l_part),
|
|
576
|
+
float(scale),
|
|
577
|
+
1.0,
|
|
578
|
+
_stream(),
|
|
579
|
+
)
|
|
580
|
+
fn = self._get_compiled(self._key(plan.q_len, plan.with_reduction, plan.causal), args)
|
|
581
|
+
if not compile_only:
|
|
582
|
+
fn(*args)
|
|
583
|
+
|
|
584
|
+
def _pack_q_fused(self, q3, b: int, ql: int, q_view, qd, q_mul=None) -> bool:
|
|
585
|
+
"""fp8 Q quantization + packing in one Triton launch (qpack). False
|
|
586
|
+
when unavailable; a failure disables it for good, outside capture."""
|
|
587
|
+
if not self._fused_qpack or q3.stride(-1) != 1:
|
|
588
|
+
return False
|
|
589
|
+
try:
|
|
590
|
+
qpack.pack_q_fp8(q3, b, ql, self.h_k, self.h_r, q_view, qd, q_mul)
|
|
591
|
+
return True
|
|
592
|
+
except Exception as e: # noqa: BLE001 - e.g. a Triton build that cannot target this GPU
|
|
593
|
+
if torch.cuda.is_current_stream_capturing():
|
|
594
|
+
raise
|
|
595
|
+
self._fused_qpack = False
|
|
596
|
+
_log.warning("Mjolnir: fused fp8 Q pack unavailable (%r); using the PyTorch ops", e)
|
|
597
|
+
return False
|
|
598
|
+
|
|
599
|
+
def run(
|
|
600
|
+
self,
|
|
601
|
+
plan: StepPlan,
|
|
602
|
+
query: torch.Tensor, # (>= b*q_len, h_q, d) bf16, any token stride
|
|
603
|
+
out: torch.Tensor, # (>= b*q_len, h_q, d) or (>= b*q_len, h_q*d)
|
|
604
|
+
k_cache: torch.Tensor, # (num_pages, page_size, h_k, d) logical view, fp8/uint8
|
|
605
|
+
v_cache: torch.Tensor,
|
|
606
|
+
k_scale: torch.Tensor, # fp32: 1 elem (per tensor), (h_k,), or (b, h_k)
|
|
607
|
+
v_scale: torch.Tensor,
|
|
608
|
+
softmax_scale: float,
|
|
609
|
+
q_mul: torch.Tensor | None = None, # (h_q, d) fp32: Q *= q_mul first (fp6 K channel scales)
|
|
610
|
+
o_mul: torch.Tensor | None = None, # (h_q, d) fp32: O *= o_mul last (fp6 V channel scales)
|
|
611
|
+
):
|
|
612
|
+
b, ql, n_q = plan.num_seqs, plan.q_len, plan.n_q
|
|
613
|
+
h_k, h_r, d = self.h_k, self.h_r, self.d
|
|
614
|
+
tokens = b * ql
|
|
615
|
+
if self.kv_format == "fp8":
|
|
616
|
+
if k_cache.dtype == torch.uint8:
|
|
617
|
+
k_cache = k_cache.view(torch.float8_e4m3fn)
|
|
618
|
+
if v_cache.dtype == torch.uint8:
|
|
619
|
+
v_cache = v_cache.view(torch.float8_e4m3fn)
|
|
620
|
+
q3 = query[:tokens]
|
|
621
|
+
if q3.dim() == 2:
|
|
622
|
+
q3 = q3.view(tokens, self.h_q, d)
|
|
623
|
+
o3 = out[:tokens]
|
|
624
|
+
if o3.dim() == 2:
|
|
625
|
+
o3 = o3.view(tokens, self.h_q, d)
|
|
626
|
+
|
|
627
|
+
# ---- Q -------------------------------------------------------------
|
|
628
|
+
qd = self.q_descale
|
|
629
|
+
if self.fp8_q:
|
|
630
|
+
# dynamic per-(seq, kv head) e4m3 quantization into the pack buffer
|
|
631
|
+
qd = self.q_descale[:b]
|
|
632
|
+
q_view = self.q_pack[: b * h_k * n_q * d].view(b, h_k, n_q, d)
|
|
633
|
+
if not self._pack_q_fused(q3, b, ql, q_view, qd, q_mul):
|
|
634
|
+
qg = q3.float()
|
|
635
|
+
if q_mul is not None:
|
|
636
|
+
qg = qg * q_mul.view(self.h_q, d)
|
|
637
|
+
qg = qg.reshape(b, ql, h_k, h_r, d)
|
|
638
|
+
amax = qg.abs().amax(dim=(1, 3, 4)).clamp_min(1e-6) # (b, h_k)
|
|
639
|
+
scale_q = amax / FP8_MAX
|
|
640
|
+
qd.copy_(scale_q)
|
|
641
|
+
q_view.copy_(
|
|
642
|
+
(qg / scale_q[:, None, :, None, None])
|
|
643
|
+
.to(torch.float8_e4m3fn)
|
|
644
|
+
.permute(0, 2, 1, 3, 4)
|
|
645
|
+
.reshape(b, h_k, n_q, d)
|
|
646
|
+
)
|
|
647
|
+
elif q_mul is not None:
|
|
648
|
+
q_view = self.q_pack[: b * h_k * n_q * d].view(b, h_k, n_q, d)
|
|
649
|
+
qm = (q3.float() * q_mul.view(self.h_q, d)).to(q_view.dtype)
|
|
650
|
+
q_view.copy_(qm.reshape(b, ql, h_k, h_r, d).permute(0, 2, 1, 3, 4).reshape(b, h_k, n_q, d))
|
|
651
|
+
elif ql == 1 and q3.stride(2) == 1 and q3.stride(1) == d and q3.data_ptr() % 16 == 0:
|
|
652
|
+
q_view = q3.unflatten(1, (h_k, h_r)) # zero-copy (b, h_k, h_r, d)
|
|
653
|
+
else:
|
|
654
|
+
q_view = self.q_pack[: b * h_k * n_q * d].view(b, h_k, n_q, d)
|
|
655
|
+
q_view.copy_(q3.reshape(b, ql, h_k, h_r, d).permute(0, 2, 1, 3, 4).reshape(b, h_k, n_q, d))
|
|
656
|
+
|
|
657
|
+
# ---- O -------------------------------------------------------------
|
|
658
|
+
direct_out = ql == 1 and o3.stride(2) == 1 and o3.stride(1) == d and o3.data_ptr() % 16 == 0
|
|
659
|
+
o_view = o3.unflatten(1, (h_k, h_r)) if direct_out else self.o_pack[: b * h_k * n_q * d].view(b, h_k, n_q, d)
|
|
660
|
+
|
|
661
|
+
self._launch(plan, q_view, o_view, k_cache, v_cache, qd, k_scale, v_scale, softmax_scale)
|
|
662
|
+
|
|
663
|
+
if not direct_out:
|
|
664
|
+
o3.copy_(o_view.reshape(b, h_k, ql, h_r, d).permute(0, 2, 1, 3, 4).reshape(tokens, self.h_q, d))
|
|
665
|
+
if o_mul is not None:
|
|
666
|
+
o3.mul_(o_mul.view(self.h_q, d))
|
|
667
|
+
return out
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
ENV_PREFIX = "MJOLNIR_ATTN_"
|
|
671
|
+
LEGACY_ENV_PREFIX = "THOR_T5_"
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def env_flag(name: str, default: str = "") -> str:
|
|
675
|
+
"""Read a kernel knob. ``name`` is the canonical ``MJOLNIR_ATTN_*`` name;
|
|
676
|
+
the pre-rename ``THOR_T5_*`` spelling is still honored as a fallback."""
|
|
677
|
+
if name in os.environ:
|
|
678
|
+
return os.environ[name].strip()
|
|
679
|
+
if name.startswith(ENV_PREFIX):
|
|
680
|
+
legacy = LEGACY_ENV_PREFIX + name[len(ENV_PREFIX) :]
|
|
681
|
+
if legacy in os.environ:
|
|
682
|
+
return os.environ[legacy].strip()
|
|
683
|
+
return default.strip()
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
# ----------------------------------------------------------------------------
|
|
687
|
+
# KV-cache views
|
|
688
|
+
# ----------------------------------------------------------------------------
|
|
689
|
+
KV_LAYOUTS = ("BHN2D", "BNH2D", "B2NHD", "B2HND", "2BNHD", "2BHND")
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _canon_singleton(t: torch.Tensor) -> torch.Tensor:
|
|
693
|
+
"""Size-1 dims may carry arbitrary strides (e.g. h_k == 1 under TP);
|
|
694
|
+
TMA wants 16-byte multiples -> zero them (never used for indexing)."""
|
|
695
|
+
if all(s != 1 for s in t.shape):
|
|
696
|
+
return t
|
|
697
|
+
st = [0 if n == 1 else s for n, s in zip(t.shape, t.stride())]
|
|
698
|
+
return t.as_strided(t.shape, st, t.storage_offset())
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def detect_kv_layout(shape, num_kv_heads: int, page_size: int, head_size: int) -> str:
|
|
702
|
+
"""Name the logical layout of a vLLM per-layer KV cache tensor.
|
|
703
|
+
|
|
704
|
+
vLLM main: (B, H, N, 2D) K|V interleaved in the last dim
|
|
705
|
+
older FlashInfer NHD: (B, 2, N, H, D); HND: (B, 2, H, N, D)
|
|
706
|
+
older FlashAttention: (2, B, N, H, D)
|
|
707
|
+
Ambiguous (H == N) shapes resolve to the vLLM-main / NHD reading;
|
|
708
|
+
MJOLNIR_ATTN_KV_LAYOUT overrides.
|
|
709
|
+
"""
|
|
710
|
+
forced = env_flag("MJOLNIR_ATTN_KV_LAYOUT").upper()
|
|
711
|
+
if forced:
|
|
712
|
+
assert forced in KV_LAYOUTS, f"MJOLNIR_ATTN_KV_LAYOUT must be one of {KV_LAYOUTS}"
|
|
713
|
+
return forced
|
|
714
|
+
H, N, D = num_kv_heads, page_size, head_size
|
|
715
|
+
s = tuple(shape)
|
|
716
|
+
if len(s) == 4 and s[3] == 2 * D:
|
|
717
|
+
if (s[1], s[2]) == (H, N):
|
|
718
|
+
return "BHN2D"
|
|
719
|
+
if (s[1], s[2]) == (N, H):
|
|
720
|
+
return "BNH2D"
|
|
721
|
+
if len(s) == 5 and s[4] == D:
|
|
722
|
+
if s[1] == 2 and (s[2], s[3]) == (N, H):
|
|
723
|
+
return "B2NHD"
|
|
724
|
+
if s[1] == 2 and (s[2], s[3]) == (H, N):
|
|
725
|
+
return "B2HND"
|
|
726
|
+
if s[0] == 2 and (s[2], s[3]) == (N, H):
|
|
727
|
+
return "2BNHD"
|
|
728
|
+
if s[0] == 2 and (s[2], s[3]) == (H, N):
|
|
729
|
+
return "2BHND"
|
|
730
|
+
raise ValueError(
|
|
731
|
+
f"unrecognized KV cache shape {s} for H={H} N={N} D={D}; set MJOLNIR_ATTN_KV_LAYOUT to one of {KV_LAYOUTS}"
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def split_kv_cache(kv_cache: torch.Tensor, layout: str, head_size: int):
|
|
736
|
+
"""Zero-copy logical (num_pages, page_size, h_k, d) K and V views of any
|
|
737
|
+
supported layout. Physical stride order (NHD/HND, layer-major, ...) is
|
|
738
|
+
carried in the strides, which the kernel takes as runtime arguments."""
|
|
739
|
+
D = head_size
|
|
740
|
+
if layout == "BHN2D":
|
|
741
|
+
k, v = kv_cache[..., :D].transpose(1, 2), kv_cache[..., D:].transpose(1, 2)
|
|
742
|
+
elif layout == "BNH2D":
|
|
743
|
+
k, v = kv_cache[..., :D], kv_cache[..., D:]
|
|
744
|
+
elif layout == "B2NHD":
|
|
745
|
+
k, v = kv_cache[:, 0], kv_cache[:, 1]
|
|
746
|
+
elif layout == "B2HND":
|
|
747
|
+
k, v = kv_cache[:, 0].transpose(1, 2), kv_cache[:, 1].transpose(1, 2)
|
|
748
|
+
elif layout == "2BNHD":
|
|
749
|
+
k, v = kv_cache[0], kv_cache[1]
|
|
750
|
+
elif layout == "2BHND":
|
|
751
|
+
k, v = kv_cache[0].transpose(1, 2), kv_cache[1].transpose(1, 2)
|
|
752
|
+
else:
|
|
753
|
+
raise ValueError(layout)
|
|
754
|
+
return _canon_singleton(k), _canon_singleton(v)
|