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