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/kvfp6.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""Packed fp6 (e2m3) KV cache for the vLLM path: write, and expand for the fallback.
|
|
2
|
+
|
|
3
|
+
Format (the Mjolnir kernel's, see ``t5_decode.pack_subbyte``): one e2m3 code
|
|
4
|
+
per element (sign | 2-bit exponent | 3-bit mantissa, bias 1, no inf/nan,
|
|
5
|
+
values 0 .. ±7.5), packed little-endian, 4 codes in 3 bytes. A K (or V) row
|
|
6
|
+
of d elements is ``d * 3 / 4`` bytes; vLLM's page cell is ``K row | V row``
|
|
7
|
+
(``(B, H, N, 2 * d * 3 / 4)`` uint8, any physical stride order).
|
|
8
|
+
|
|
9
|
+
Values are ``code_value * scale`` with a static scale per (layer, KV head):
|
|
10
|
+
``scale = amax / 7.5`` from a calibration run (``mjolnir.calibrate``).
|
|
11
|
+
|
|
12
|
+
* :func:`write_kv` — the cache write (vLLM's ``do_kv_cache_update``): scale,
|
|
13
|
+
round to nearest (saturating), pack, scatter by slot.
|
|
14
|
+
* :func:`expand_pages_to_fp8` — prefill / mixed steps run on FlashAttention 4,
|
|
15
|
+
which reads e4m3 only: the pages a step touches are expanded to e4m3 bytes
|
|
16
|
+
in a scratch cache. Every e2m3 value is exactly representable in e4m3, so
|
|
17
|
+
with per-head scales the expansion is lossless and the same scales are the
|
|
18
|
+
descales; with per-channel scales s_c each value becomes e4m3(code * s_c / S)
|
|
19
|
+
for a per-head descale S = max_c(s_c) * 7.5 / 448 (one e4m3 rounding).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import torch
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import triton
|
|
28
|
+
import triton.language as tl
|
|
29
|
+
except ImportError: # pragma: no cover
|
|
30
|
+
triton = None
|
|
31
|
+
tl = None
|
|
32
|
+
|
|
33
|
+
E2M3_MAX = 7.5
|
|
34
|
+
BITS = 6
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def row_bytes(head_size: int) -> int:
|
|
38
|
+
assert head_size % 4 == 0
|
|
39
|
+
return head_size * BITS // 8
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_LUT: dict = {}
|
|
43
|
+
_LUTF: dict = {}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def e2m3_values(device) -> torch.Tensor:
|
|
47
|
+
"""(64,) fp32: the value of every e2m3 code."""
|
|
48
|
+
key = str(device)
|
|
49
|
+
if key not in _LUTF:
|
|
50
|
+
from .t5_decode import minifloat_table
|
|
51
|
+
|
|
52
|
+
_LUTF[key] = minifloat_table("e2m3").to(device)
|
|
53
|
+
return _LUTF[key]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def e2m3_to_e4m3_lut(device) -> torch.Tensor:
|
|
57
|
+
"""(64,) uint8: e4m3fn byte of every e2m3 code (exact)."""
|
|
58
|
+
key = str(device)
|
|
59
|
+
if key not in _LUT:
|
|
60
|
+
from .t5_decode import minifloat_table
|
|
61
|
+
|
|
62
|
+
vals = minifloat_table("e2m3") # 64 float values, code order
|
|
63
|
+
e4 = vals.to(torch.float8_e4m3fn)
|
|
64
|
+
assert torch.equal(e4.float(), vals), "e2m3 values must be exact in e4m3"
|
|
65
|
+
_LUT[key] = e4.view(torch.uint8).to(device)
|
|
66
|
+
return _LUT[key]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if triton is not None:
|
|
70
|
+
|
|
71
|
+
@triton.jit
|
|
72
|
+
def _e2m3_codes(x):
|
|
73
|
+
"""fp32 (already divided by the scale) -> e2m3 codes (int32 0..63):
|
|
74
|
+
round to nearest, saturating at 7.5, ties to the smaller magnitude
|
|
75
|
+
(as t5_decode.minifloat_quantize)."""
|
|
76
|
+
ax = tl.minimum(tl.abs(x), 7.5)
|
|
77
|
+
# quantum: 0.125 below 2 (subnormals and [1, 2)), 0.25 in [2, 4), 0.5 in [4, 7.5]
|
|
78
|
+
q = tl.where(ax < 2.0, 0.125, tl.where(ax < 4.0, 0.25, 0.5))
|
|
79
|
+
v = tl.minimum(tl.ceil(ax / q - 0.5) * q, 7.5)
|
|
80
|
+
# value -> code: [0, 2): 8 v; [2, 4): 16 + 4 (v - 2); [4, 7.5]: 24 + 2 (v - 4)
|
|
81
|
+
code = tl.where(v < 2.0, v * 8.0, tl.where(v < 4.0, 16.0 + (v - 2.0) * 4.0, 24.0 + (v - 4.0) * 2.0))
|
|
82
|
+
code = code.to(tl.int32)
|
|
83
|
+
sign = ((x < 0) & (code > 0)).to(tl.int32)
|
|
84
|
+
return code | (sign << 5)
|
|
85
|
+
|
|
86
|
+
@triton.jit
|
|
87
|
+
def _group_scales(sp, h, g, gm, D: tl.constexpr, MODE: tl.constexpr):
|
|
88
|
+
"""The scales of the 4 elements of each packed group: per channel
|
|
89
|
+
(MODE 2), per KV head (1) or per tensor (0)."""
|
|
90
|
+
if MODE == 2:
|
|
91
|
+
s0 = tl.load(sp + h * D + 4 * g + 0, mask=gm, other=1.0)
|
|
92
|
+
s1 = tl.load(sp + h * D + 4 * g + 1, mask=gm, other=1.0)
|
|
93
|
+
s2 = tl.load(sp + h * D + 4 * g + 2, mask=gm, other=1.0)
|
|
94
|
+
s3 = tl.load(sp + h * D + 4 * g + 3, mask=gm, other=1.0)
|
|
95
|
+
else:
|
|
96
|
+
if MODE == 1:
|
|
97
|
+
sc = tl.load(sp + h)
|
|
98
|
+
else:
|
|
99
|
+
sc = tl.load(sp)
|
|
100
|
+
s0 = tl.full(g.shape, 1.0, tl.float32) * sc
|
|
101
|
+
s1 = s0
|
|
102
|
+
s2 = s0
|
|
103
|
+
s3 = s0
|
|
104
|
+
return s0, s1, s2, s3
|
|
105
|
+
|
|
106
|
+
@triton.jit
|
|
107
|
+
def _write_kv_kernel(
|
|
108
|
+
key_ptr,
|
|
109
|
+
value_ptr,
|
|
110
|
+
cache_ptr,
|
|
111
|
+
slot_ptr,
|
|
112
|
+
k_scale_ptr,
|
|
113
|
+
v_scale_ptr,
|
|
114
|
+
stride_kt,
|
|
115
|
+
stride_kh,
|
|
116
|
+
stride_vt,
|
|
117
|
+
stride_vh,
|
|
118
|
+
stride_cb,
|
|
119
|
+
stride_ch,
|
|
120
|
+
stride_cn,
|
|
121
|
+
page_size,
|
|
122
|
+
K_SCALE_MODE: tl.constexpr, # 0 per tensor, 1 per KV head, 2 per (KV head, channel)
|
|
123
|
+
V_SCALE_MODE: tl.constexpr,
|
|
124
|
+
D: tl.constexpr,
|
|
125
|
+
G_BLOCK: tl.constexpr,
|
|
126
|
+
):
|
|
127
|
+
t = tl.program_id(0)
|
|
128
|
+
h = tl.program_id(1)
|
|
129
|
+
slot = tl.load(slot_ptr + t).to(tl.int64)
|
|
130
|
+
if slot < 0: # padding token
|
|
131
|
+
return
|
|
132
|
+
blk = slot // page_size
|
|
133
|
+
off = slot % page_size
|
|
134
|
+
G: tl.constexpr = D // 4
|
|
135
|
+
RB: tl.constexpr = D * 3 // 4
|
|
136
|
+
g = tl.arange(0, G_BLOCK)
|
|
137
|
+
gm = g < G
|
|
138
|
+
dst = cache_ptr + blk * stride_cb + h * stride_ch + off * stride_cn
|
|
139
|
+
for side in tl.static_range(2):
|
|
140
|
+
if side == 0:
|
|
141
|
+
src = key_ptr + t * stride_kt + h * stride_kh
|
|
142
|
+
s0, s1, s2, s3 = _group_scales(k_scale_ptr, h, g, gm, D, K_SCALE_MODE)
|
|
143
|
+
else:
|
|
144
|
+
src = value_ptr + t * stride_vt + h * stride_vh
|
|
145
|
+
s0, s1, s2, s3 = _group_scales(v_scale_ptr, h, g, gm, D, V_SCALE_MODE)
|
|
146
|
+
# x / scale, correctly rounded (div_rn): the same codes as the reference quantizer
|
|
147
|
+
c0 = _e2m3_codes(tl.math.div_rn(tl.load(src + 4 * g + 0, mask=gm, other=0.0).to(tl.float32), s0))
|
|
148
|
+
c1 = _e2m3_codes(tl.math.div_rn(tl.load(src + 4 * g + 1, mask=gm, other=0.0).to(tl.float32), s1))
|
|
149
|
+
c2 = _e2m3_codes(tl.math.div_rn(tl.load(src + 4 * g + 2, mask=gm, other=0.0).to(tl.float32), s2))
|
|
150
|
+
c3 = _e2m3_codes(tl.math.div_rn(tl.load(src + 4 * g + 3, mask=gm, other=0.0).to(tl.float32), s3))
|
|
151
|
+
w = c0 | (c1 << 6) | (c2 << 12) | (c3 << 18)
|
|
152
|
+
base = dst + side * RB + 3 * g
|
|
153
|
+
tl.store(base + 0, (w & 0xFF).to(tl.uint8), mask=gm)
|
|
154
|
+
tl.store(base + 1, ((w >> 8) & 0xFF).to(tl.uint8), mask=gm)
|
|
155
|
+
tl.store(base + 2, ((w >> 16) & 0xFF).to(tl.uint8), mask=gm)
|
|
156
|
+
|
|
157
|
+
@triton.jit
|
|
158
|
+
def _expand_kernel(
|
|
159
|
+
cache_ptr,
|
|
160
|
+
src_block_ptr,
|
|
161
|
+
out_ptr,
|
|
162
|
+
lut_ptr,
|
|
163
|
+
stride_cb,
|
|
164
|
+
stride_ch,
|
|
165
|
+
stride_cn,
|
|
166
|
+
stride_ob,
|
|
167
|
+
stride_oh,
|
|
168
|
+
stride_on,
|
|
169
|
+
k_ratio_ptr,
|
|
170
|
+
v_ratio_ptr,
|
|
171
|
+
lutf_ptr,
|
|
172
|
+
RATIO: tl.constexpr, # apply per-(head, channel) ratios: e4m3(value * ratio)
|
|
173
|
+
D: tl.constexpr,
|
|
174
|
+
G_BLOCK: tl.constexpr,
|
|
175
|
+
):
|
|
176
|
+
i = tl.program_id(0) # scratch page
|
|
177
|
+
h = tl.program_id(1)
|
|
178
|
+
n = tl.program_id(2) # row in the page
|
|
179
|
+
src_blk = tl.load(src_block_ptr + i).to(tl.int64)
|
|
180
|
+
G: tl.constexpr = D // 4
|
|
181
|
+
RB: tl.constexpr = D * 3 // 4
|
|
182
|
+
g = tl.arange(0, G_BLOCK)
|
|
183
|
+
gm = g < G
|
|
184
|
+
src = cache_ptr + src_blk * stride_cb + h * stride_ch + n * stride_cn
|
|
185
|
+
dst = out_ptr + i.to(tl.int64) * stride_ob + h * stride_oh + n * stride_on
|
|
186
|
+
for side in tl.static_range(2):
|
|
187
|
+
b = src + side * RB + 3 * g
|
|
188
|
+
w = (
|
|
189
|
+
tl.load(b + 0, mask=gm, other=0).to(tl.int32)
|
|
190
|
+
| (tl.load(b + 1, mask=gm, other=0).to(tl.int32) << 8)
|
|
191
|
+
| (tl.load(b + 2, mask=gm, other=0).to(tl.int32) << 16)
|
|
192
|
+
)
|
|
193
|
+
o = dst + side * D + 4 * g
|
|
194
|
+
if side == 0:
|
|
195
|
+
rp = k_ratio_ptr
|
|
196
|
+
else:
|
|
197
|
+
rp = v_ratio_ptr
|
|
198
|
+
for k in tl.static_range(4):
|
|
199
|
+
code = (w >> (6 * k)) & 63
|
|
200
|
+
if RATIO:
|
|
201
|
+
r = tl.load(rp + h * D + 4 * g + k, mask=gm, other=0.0)
|
|
202
|
+
val = tl.load(lutf_ptr + code) * r
|
|
203
|
+
tl.store(o + k, val.to(tl.float8e4nv).to(tl.uint8, bitcast=True), mask=gm)
|
|
204
|
+
else:
|
|
205
|
+
tl.store(o + k, tl.load(lut_ptr + code), mask=gm)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _scale_mode(scale: torch.Tensor, h_k: int, d: int) -> int:
|
|
209
|
+
"""0: per tensor, 1: per KV head, 2: per (KV head, channel) — contiguous."""
|
|
210
|
+
n = scale.numel()
|
|
211
|
+
if n == h_k * d and d > 1 and (h_k > 1 or n > 1):
|
|
212
|
+
assert scale.is_contiguous(), "per-channel KV scales must be contiguous (h_k, d)"
|
|
213
|
+
return 2
|
|
214
|
+
assert n in (1, h_k), f"KV scale must be per tensor, per KV head or (h_k, d), got {tuple(scale.shape)}"
|
|
215
|
+
return 1 if n == h_k and h_k > 1 else 0
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def write_kv(
|
|
219
|
+
key: torch.Tensor, # (>= T, h_k, d) bf16/fp16, d contiguous
|
|
220
|
+
value: torch.Tensor,
|
|
221
|
+
kv_cache: torch.Tensor, # (B, h_k, N, 2 * row_bytes) uint8 layer view, any strides, unit last stride
|
|
222
|
+
slot_mapping: torch.Tensor, # (T,) int, -1 = padding
|
|
223
|
+
k_scale: torch.Tensor, # fp32: 1 elem, (h_k,) or (h_k, d)
|
|
224
|
+
v_scale: torch.Tensor,
|
|
225
|
+
) -> None:
|
|
226
|
+
"""Quantize + pack + scatter K/V rows of T tokens into the e2m3 cache."""
|
|
227
|
+
T = slot_mapping.shape[0]
|
|
228
|
+
if T == 0:
|
|
229
|
+
return
|
|
230
|
+
_, h_k, d = key.shape
|
|
231
|
+
_, H, N, C = kv_cache.shape
|
|
232
|
+
assert H == h_k and C == 2 * row_bytes(d) and kv_cache.stride(3) == 1 and kv_cache.dtype == torch.uint8
|
|
233
|
+
assert key.stride(2) == 1 and value.stride(2) == 1
|
|
234
|
+
_write_kv_kernel[(T, h_k)](
|
|
235
|
+
key,
|
|
236
|
+
value,
|
|
237
|
+
kv_cache,
|
|
238
|
+
slot_mapping,
|
|
239
|
+
k_scale,
|
|
240
|
+
v_scale,
|
|
241
|
+
key.stride(0),
|
|
242
|
+
key.stride(1),
|
|
243
|
+
value.stride(0),
|
|
244
|
+
value.stride(1),
|
|
245
|
+
kv_cache.stride(0),
|
|
246
|
+
kv_cache.stride(1),
|
|
247
|
+
kv_cache.stride(2),
|
|
248
|
+
N,
|
|
249
|
+
K_SCALE_MODE=_scale_mode(k_scale, h_k, d),
|
|
250
|
+
V_SCALE_MODE=_scale_mode(v_scale, h_k, d),
|
|
251
|
+
D=d,
|
|
252
|
+
G_BLOCK=triton.next_power_of_2(d // 4),
|
|
253
|
+
num_warps=2,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def expand_pages_to_fp8(
|
|
258
|
+
kv_cache: torch.Tensor, # (B, h_k, N, 2 * row_bytes) uint8 e2m3 layer view
|
|
259
|
+
src_blocks: torch.Tensor, # (P,) int32: cache page of each scratch page
|
|
260
|
+
out: torch.Tensor, # (>= P, h_k, N, 2 * d) uint8 (e4m3 bytes), unit last stride
|
|
261
|
+
head_size: int,
|
|
262
|
+
k_ratio: torch.Tensor | None = None, # (h_k, d) fp32: per-channel scale / per-head descale
|
|
263
|
+
v_ratio: torch.Tensor | None = None,
|
|
264
|
+
) -> torch.Tensor:
|
|
265
|
+
"""Expand P pages (K and V) to e4m3 bytes in ``out[:P]``; returns it.
|
|
266
|
+
|
|
267
|
+
Without ratios the e4m3 bytes are the e2m3 values exactly (the cache's
|
|
268
|
+
scales are the descales). With per-channel scales s_c and a per-head
|
|
269
|
+
descale S, pass ratio = s_c / S: each value becomes e4m3(code * s_c / S),
|
|
270
|
+
so the expanded page carries the channel scales and S stays the descale."""
|
|
271
|
+
P = src_blocks.shape[0]
|
|
272
|
+
_, H, N, C = kv_cache.shape
|
|
273
|
+
d = head_size
|
|
274
|
+
assert C == 2 * row_bytes(d) and out.shape[1:] == (H, N, 2 * d) and out.stride(3) == 1
|
|
275
|
+
if P == 0:
|
|
276
|
+
return out[:0]
|
|
277
|
+
_expand_kernel[(P, H, N)](
|
|
278
|
+
kv_cache,
|
|
279
|
+
src_blocks,
|
|
280
|
+
out,
|
|
281
|
+
e2m3_to_e4m3_lut(kv_cache.device),
|
|
282
|
+
kv_cache.stride(0),
|
|
283
|
+
kv_cache.stride(1),
|
|
284
|
+
kv_cache.stride(2),
|
|
285
|
+
out.stride(0),
|
|
286
|
+
out.stride(1),
|
|
287
|
+
out.stride(2),
|
|
288
|
+
k_ratio if k_ratio is not None else out,
|
|
289
|
+
v_ratio if v_ratio is not None else out,
|
|
290
|
+
e2m3_values(kv_cache.device),
|
|
291
|
+
RATIO=k_ratio is not None,
|
|
292
|
+
D=d,
|
|
293
|
+
G_BLOCK=triton.next_power_of_2(d // 4),
|
|
294
|
+
num_warps=1,
|
|
295
|
+
)
|
|
296
|
+
return out[:P]
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
# References (tests): the same math in PyTorch, via the kernel package's
|
|
301
|
+
# own quantizer / packer, so the Triton kernels are checked against them.
|
|
302
|
+
# ---------------------------------------------------------------------------
|
|
303
|
+
def quantize_rows_reference(x: torch.Tensor, scale: torch.Tensor) -> torch.Tensor:
|
|
304
|
+
"""(..., h_k, d) float -> packed bytes (..., h_k, row_bytes(d)) uint8;
|
|
305
|
+
scale per tensor, per KV head or (h_k, d) per channel."""
|
|
306
|
+
from .t5_decode import minifloat_quantize, pack_subbyte
|
|
307
|
+
|
|
308
|
+
h_k, d = x.shape[-2], x.shape[-1]
|
|
309
|
+
s = scale.float().cpu()
|
|
310
|
+
if s.numel() == h_k * d and d > 1:
|
|
311
|
+
s = s.reshape(h_k, d)
|
|
312
|
+
else:
|
|
313
|
+
s = s.reshape(-1)
|
|
314
|
+
s = (s.expand(h_k) if s.numel() == 1 else s).reshape(h_k, 1)
|
|
315
|
+
codes, _ = minifloat_quantize(x.float().cpu() / s, "e2m3")
|
|
316
|
+
return pack_subbyte(codes, BITS)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def decode_rows_reference(packed: torch.Tensor, d: int) -> torch.Tensor:
|
|
320
|
+
"""packed (..., row_bytes) uint8 -> e2m3 code values (..., d) float."""
|
|
321
|
+
from .t5_decode import minifloat_table
|
|
322
|
+
|
|
323
|
+
b = packed.to(torch.int64).cpu().reshape(*packed.shape[:-1], d // 4, 3)
|
|
324
|
+
w = b[..., 0] | (b[..., 1] << 8) | (b[..., 2] << 16)
|
|
325
|
+
codes = torch.stack([(w >> (6 * k)) & 63 for k in range(4)], dim=-1).reshape(*packed.shape[:-1], d)
|
|
326
|
+
return minifloat_table("e2m3")[codes]
|
mjolnir/kvscales.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Static per-(layer, KV head, channel) scales for the fp6 (e2m3) KV cache.
|
|
2
|
+
|
|
3
|
+
e2m3 spans only 0 .. ±7.5 with 0.125 steps below 1 — about 60x of dynamic
|
|
4
|
+
range. One scale per KV head is not enough on real models: K has outlier
|
|
5
|
+
channels (Qwen3-0.6B layer 0: K amax 486 in a head whose other channels are
|
|
6
|
+
a few units), and a head-wide scale of 486 / 7.5 rounds every small channel
|
|
7
|
+
to zero. So every (layer, KV head, channel) gets its own scale,
|
|
8
|
+
``amax / 7.5``. They cost nothing at attention time — a K channel scale folds
|
|
9
|
+
into Q (q . k = sum_c (q_c s_c) code_c), a V channel scale into the output
|
|
10
|
+
(O_c = s_c sum_t p_t code_tc) — see ``mjolnir.backend``.
|
|
11
|
+
|
|
12
|
+
vLLM 0.31 has no runtime KV-scale calculation and checkpoints carry fp8
|
|
13
|
+
scales at best, so the amax values come from a calibration run
|
|
14
|
+
(``python -m mjolnir.calibrate``, or ``mjolnir calibrate``): serve the model
|
|
15
|
+
on fp8 KV with ``MJOLNIR_ATTN_KV_CALIBRATE=1``, run prompts, and dump the
|
|
16
|
+
running per-channel max of every K and V written to the cache.
|
|
17
|
+
|
|
18
|
+
File (``MJOLNIR_ATTN_KV_SCALES``)::
|
|
19
|
+
|
|
20
|
+
{"format": "mjolnir.kv_scales/2", "model": "...", "tokens": 123456,
|
|
21
|
+
"amax": {"<layer name>": {"k": [[d floats] x h_k], "v": [[d floats] x h_k]}, ...}}
|
|
22
|
+
|
|
23
|
+
(``mjolnir.kv_scales/1`` files — one amax per head — still load; each head's
|
|
24
|
+
amax then applies to all its channels.)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import threading
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
import torch
|
|
35
|
+
|
|
36
|
+
FORMAT = "mjolnir.kv_scales/2"
|
|
37
|
+
FORMATS = ("mjolnir.kv_scales/1", FORMAT)
|
|
38
|
+
E2M3_MAX = 7.5
|
|
39
|
+
|
|
40
|
+
# ── loading ───────────────────────────────────────────────────────────────
|
|
41
|
+
_FILES: dict[str, dict] = {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def scales_path() -> str | None:
|
|
45
|
+
for name in ("MJOLNIR_ATTN_KV_SCALES", "THOR_T5_KV_SCALES"):
|
|
46
|
+
v = os.environ.get(name, "").strip()
|
|
47
|
+
if v:
|
|
48
|
+
return v
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def load(path: str | os.PathLike) -> dict:
|
|
53
|
+
key = str(path)
|
|
54
|
+
if key not in _FILES:
|
|
55
|
+
doc = json.loads(Path(path).read_text())
|
|
56
|
+
fmt = doc.get("format")
|
|
57
|
+
if fmt not in FORMATS or not isinstance(doc.get("amax"), dict):
|
|
58
|
+
hint = (
|
|
59
|
+
" — the file is newer than this mjolnir package (rebuild the image: mjolnir image build)"
|
|
60
|
+
if isinstance(fmt, str) and fmt.startswith("mjolnir.kv_scales/")
|
|
61
|
+
else ""
|
|
62
|
+
)
|
|
63
|
+
raise ValueError(f"{path}: format {fmt!r}, this mjolnir reads {', '.join(FORMATS)}{hint}")
|
|
64
|
+
_FILES[key] = doc
|
|
65
|
+
return _FILES[key]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def headroom() -> float:
|
|
69
|
+
"""Scale multiplier (>= 1 leaves room above the calibrated amax; values
|
|
70
|
+
beyond it saturate at ±7.5 * scale). MJOLNIR_ATTN_KV_SCALE_HEADROOM."""
|
|
71
|
+
v = os.environ.get("MJOLNIR_ATTN_KV_SCALE_HEADROOM", "").strip()
|
|
72
|
+
return float(v) if v else 1.0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def e2m3_scales(doc: dict, layer_name: str, h_k: int, d: int) -> tuple[list[list[float]], list[list[float]]]:
|
|
76
|
+
"""(k scales, v scales), each (h_k, d) nested lists: amax / 7.5 * headroom.
|
|
77
|
+
Per-head entries (format /1, or one value) broadcast over channels."""
|
|
78
|
+
entry = doc["amax"].get(layer_name)
|
|
79
|
+
if entry is None:
|
|
80
|
+
raise KeyError(layer_name)
|
|
81
|
+
out = []
|
|
82
|
+
for side in ("k", "v"):
|
|
83
|
+
rows = entry[side]
|
|
84
|
+
if not isinstance(rows, list) or not rows:
|
|
85
|
+
raise ValueError(f"{layer_name}: bad {side} entry")
|
|
86
|
+
if not isinstance(rows[0], list): # per head (or one value)
|
|
87
|
+
rows = [[float(a)] * d for a in (rows * h_k if len(rows) == 1 else rows)]
|
|
88
|
+
if len(rows) != h_k or any(len(r) != d for r in rows):
|
|
89
|
+
raise ValueError(f"{layer_name}: {side} is {len(rows)}x{len(rows[0])}, the layer is {h_k}x{d}")
|
|
90
|
+
out.append([[max(float(a), 1e-6) / E2M3_MAX * headroom() for a in r] for r in rows])
|
|
91
|
+
return out[0], out[1]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def missing_layers(doc: dict, layer_names) -> list[str]:
|
|
95
|
+
return [n for n in layer_names if n not in doc["amax"]]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ── calibration (in-process accumulators) ─────────────────────────────────
|
|
99
|
+
_CALIB: dict[str, tuple[torch.Tensor, torch.Tensor]] = {}
|
|
100
|
+
_CALIB_TOKENS: dict[str, torch.Tensor] = {}
|
|
101
|
+
_LOCK = threading.Lock()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def calibrating() -> bool:
|
|
105
|
+
return os.environ.get("MJOLNIR_ATTN_KV_CALIBRATE", "").strip().lower() in ("1", "true", "yes")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def observe(layer_name: str, key: torch.Tensor, value: torch.Tensor, slot_mapping: torch.Tensor) -> None:
|
|
109
|
+
"""Fold the K/V of real tokens (slot >= 0) into the layer's running
|
|
110
|
+
per-(KV head, channel) amax. Graph-safe: in-place on persistent tensors."""
|
|
111
|
+
T = slot_mapping.shape[0]
|
|
112
|
+
if T == 0:
|
|
113
|
+
return
|
|
114
|
+
h_k, d = key.shape[1], key.shape[2]
|
|
115
|
+
with _LOCK:
|
|
116
|
+
if layer_name not in _CALIB:
|
|
117
|
+
dev = key.device
|
|
118
|
+
_CALIB[layer_name] = (
|
|
119
|
+
torch.zeros(h_k, d, dtype=torch.float32, device=dev),
|
|
120
|
+
torch.zeros(h_k, d, dtype=torch.float32, device=dev),
|
|
121
|
+
)
|
|
122
|
+
_CALIB_TOKENS[layer_name] = torch.zeros((), dtype=torch.int64, device=dev)
|
|
123
|
+
k_amax, v_amax = _CALIB[layer_name]
|
|
124
|
+
live = (slot_mapping[:T] >= 0).view(T, 1, 1)
|
|
125
|
+
ka = torch.where(live, key[:T].float().abs(), 0.0).amax(dim=0) # (h_k, d)
|
|
126
|
+
va = torch.where(live, value[:T].float().abs(), 0.0).amax(dim=0)
|
|
127
|
+
torch.maximum(k_amax, ka, out=k_amax)
|
|
128
|
+
torch.maximum(v_amax, va, out=v_amax)
|
|
129
|
+
_CALIB_TOKENS[layer_name].add_(live.sum())
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def per_channel(doc: dict, layer_name: str) -> bool:
|
|
133
|
+
"""Does the file carry per-channel amax for this layer?"""
|
|
134
|
+
k = doc["amax"].get(layer_name, {}).get("k")
|
|
135
|
+
return bool(k) and isinstance(k[0], list)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def reset() -> None:
|
|
139
|
+
"""Zero the accumulators (after engine warm-up and profiling runs).
|
|
140
|
+
|
|
141
|
+
They were created inside vLLM's forward (torch.inference_mode), so they
|
|
142
|
+
are inference tensors: in-place updates are only allowed in that mode."""
|
|
143
|
+
with _LOCK, torch.inference_mode():
|
|
144
|
+
for k, v in _CALIB.values():
|
|
145
|
+
k.zero_()
|
|
146
|
+
v.zero_()
|
|
147
|
+
for t in _CALIB_TOKENS.values():
|
|
148
|
+
t.zero_()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def snapshot(model: str = "") -> dict:
|
|
152
|
+
with _LOCK, torch.inference_mode():
|
|
153
|
+
amax = {name: {"k": k.tolist(), "v": v.tolist()} for name, (k, v) in sorted(_CALIB.items())}
|
|
154
|
+
tokens = max((int(t.item()) for t in _CALIB_TOKENS.values()), default=0)
|
|
155
|
+
return {"format": FORMAT, "model": model, "tokens": tokens, "amax": amax}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def dump(path: str | os.PathLike, model: str = "") -> dict:
|
|
159
|
+
doc = snapshot(model)
|
|
160
|
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
Path(path).write_text(json.dumps(doc, indent=1) + "\n")
|
|
162
|
+
return doc
|