sparse-delta-memory 2.0.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.
Files changed (40) hide show
  1. sparse_delta_memory/__init__.py +13 -0
  2. sparse_delta_memory/backward.py +367 -0
  3. sparse_delta_memory/cache.py +70 -0
  4. sparse_delta_memory/cuda/__init__.py +3 -0
  5. sparse_delta_memory/cuda/_build.py +254 -0
  6. sparse_delta_memory/cuda/_device.py +158 -0
  7. sparse_delta_memory/cuda/_devmemo.py +64 -0
  8. sparse_delta_memory/cuda/_image.py +128 -0
  9. sparse_delta_memory/cuda/bwd_steps_cuda.py +111 -0
  10. sparse_delta_memory/cuda/gather_cuda.py +796 -0
  11. sparse_delta_memory/cuda/gather_ns_cuda.py +134 -0
  12. sparse_delta_memory/cuda/gx_gds_cuda.py +1297 -0
  13. sparse_delta_memory/cuda/gx_gemm_cuda.py +857 -0
  14. sparse_delta_memory/cuda/gx_pa2_cuda.py +1320 -0
  15. sparse_delta_memory/cuda/mover_da2_cuda.py +1802 -0
  16. sparse_delta_memory/cuda/mover_fx_cuda.py +2229 -0
  17. sparse_delta_memory/cuda/pa_dc_cuda.py +1500 -0
  18. sparse_delta_memory/cuda/phase1_bwd_cuda.py +852 -0
  19. sparse_delta_memory/cuda/phase1_common.cuh +181 -0
  20. sparse_delta_memory/cuda/phase1_cuda.py +3801 -0
  21. sparse_delta_memory/cuda/product_key_topk_anyf_cuda.py +668 -0
  22. sparse_delta_memory/cuda/product_key_topk_cuda.py +735 -0
  23. sparse_delta_memory/cuda/px_qrec_cuda.py +167 -0
  24. sparse_delta_memory/cuda/sdm_copy.cuh +90 -0
  25. sparse_delta_memory/cuda/sdm_plancap.cuh +150 -0
  26. sparse_delta_memory/cuda_graph.py +605 -0
  27. sparse_delta_memory/decode.py +140 -0
  28. sparse_delta_memory/forward.py +464 -0
  29. sparse_delta_memory/layer.py +663 -0
  30. sparse_delta_memory/memory_ops.py +1069 -0
  31. sparse_delta_memory/memory_stats.py +221 -0
  32. sparse_delta_memory/movers.py +198 -0
  33. sparse_delta_memory/phase2_kernels.py +267 -0
  34. sparse_delta_memory/product_key_topk.py +113 -0
  35. sparse_delta_memory/wy_solve.py +273 -0
  36. sparse_delta_memory-2.0.0.dist-info/METADATA +482 -0
  37. sparse_delta_memory-2.0.0.dist-info/RECORD +40 -0
  38. sparse_delta_memory-2.0.0.dist-info/WHEEL +5 -0
  39. sparse_delta_memory-2.0.0.dist-info/licenses/LICENSE +407 -0
  40. sparse_delta_memory-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,267 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ """Phase-2 kernels of the SDM recurrence: the dual matmul, and the geometry and quantization helpers the CUDA movers
4
+ share. (2.0.0 removed this module's Triton entry movers and quantizer: the CUDA movers, ``movers``, are the only route;
5
+ the description of the movers below is their shared contract.)
6
+
7
+ Phase 2 carries the bank across chunks. Step ``ti`` covers chunk ``ti`` of all P streams,
8
+ SC = P * C tokens:
9
+
10
+ forward gather -> dual matmul -> DW
11
+ backward GWR -> baddbmm -> Phase A -> GDS
12
+
13
+ DW (decay and write) updates the bank rows the step writes and quantizes the snapshot. GWR
14
+ (``token_gather`` of the bank gradient) gathers the gradient rows of the write slots. Phase A,
15
+ the backward of DW, writes the bank gradient, restores the bank rows and emits three dots per
16
+ write entry. GDS (read-side backward) emits one dot per read entry and scatters the reads'
17
+ bank gradient. Phase B, the backward through the solve, follows in ``memory_ops``.
18
+
19
+ Layout. Chunks are time-major (chunk ``ti * P + p``), N = Nt * P of them. Per-entry
20
+ tensors are contiguous [N, C, W] (reads: [N, C, R]) and token rows are [N * C, head_dim].
21
+ Tokens at or past ``valid_tokens`` in a stream are padding: they touch no bank row and
22
+ their outputs are zero.
23
+
24
+ Each ``make_*`` function checks its operands once and returns ``launch(ti)``. A launch
25
+ passes whole tensors plus the step index, so the step loop slices nothing on the host.
26
+ Buffers that may hold one step or every step (snapshot, delta_v) are told apart by their
27
+ row count.
28
+
29
+ Movers. A one-warp program owns ENT entries as an [ENT, BLOCK_D] tile and loops over
30
+ NUM_D column tiles (``mover_geom``). Steps without padding whose grid covers the entries
31
+ exactly run an unmasked variant.
32
+
33
+ Determinism: every bank (bank-gradient) element has one writer per launch, so every output
34
+ is bitwise reproducible. An entry whose slot occurs once in its chunk (is_only, q_only)
35
+ writes its row directly. Entries sharing a slot form a group, a contiguous run of Phase 1's
36
+ sorted kind row; the group's owner (its last entry, or for wide rows its last few entries,
37
+ each owning whole column slices) writes the group's sum (``_group_sums``): the members in
38
+ sorted order, in windows of consecutive entries of the kind row, each window reduced as a
39
+ tree of separately rounded f32 products (see ``_group_slices``), in f32, with one rounding
40
+ at the store. The other members write nothing. GDS adds into the bank gradient, each
41
+ element's single writer (a single-entry slot, or a group's owner) rounding once: a group's
42
+ owner, and a single-entry slot with a bf16 gradient, read the row, add the f32 sum or term
43
+ and store once; with an fp32 gradient a single-entry slot adds with one f32 atomic (its only
44
+ add). The lanes that hold copies of a sum agree bit for bit: no sum that crosses lanes
45
+ starts from a product fused into its first cross-lane add (``_group_slices``, ``_row_dot``).
46
+ The last entry of a slot restores its bank row in Phase A.
47
+
48
+ """
49
+
50
+ from typing import Callable, NamedTuple
51
+
52
+ import torch
53
+ import triton
54
+ import triton.language as tl
55
+
56
+ from .cuda._device import tbind
57
+
58
+ try: # Triton 3.7 (the tested version) has the programmatic-dependent-launch trigger
59
+ from triton.language.extra.cuda import gdc_launch_dependents
60
+ except ImportError: # pragma: no cover - without it the trigger is a no-op (same arithmetic)
61
+ @triton.jit
62
+ def gdc_launch_dependents():
63
+ pass
64
+
65
+ SNAPSHOT_QUANT_MODES = ("none", "fp8", "int4", "bf16")
66
+ # "bf16" (float32 banks; bfloat16 banks run it as "none"): bf16 compact snapshot rows on the CUDA kernels only (the
67
+ # unquantized forward with bf16 stores, gx_pa2's bf16-snapshot rows); never this module's movers (no fallback).
68
+
69
+
70
+ # ============================================================
71
+ # Geometry
72
+ # ============================================================
73
+
74
+ class MoverGeom(NamedTuple):
75
+ ent: int # entries per program
76
+ block_d: int # columns per tile
77
+ num_d: int # column tiles per row
78
+
79
+
80
+ def mover_geom(dtype: torch.dtype, head_dim: int, row_operands: int = 2) -> MoverGeom:
81
+ """Mover tile for bank rows of ``head_dim`` elements of ``dtype``; one warp per program.
82
+
83
+ A row is split into 16-byte vectors over up to 32 lanes, each lane holding at most 16
84
+ elements of it per tile; wider rows loop over column tiles. Each lane covers the same
85
+ columns of several rows, about four vectors of each of two row operands, so many bank
86
+ rows are in flight. Kernels with more ``row_operands`` take proportionally fewer rows,
87
+ but at least four when available. A warp spans whole rows, so the per-entry dots are
88
+ warp shuffles.
89
+ """
90
+ vec = 16 // dtype.itemsize
91
+ row_vecs = triton.cdiv(head_dim, vec)
92
+ lanes = min(32, triton.next_power_of_2(row_vecs))
93
+ per_lane = min(16 // vec, triton.next_power_of_2(triton.cdiv(row_vecs, lanes)))
94
+ block_d = lanes * per_lane * vec
95
+ ent = 32 // lanes * max(1, 4 // per_lane)
96
+ ent = min(ent, max(4, ent * 2 // row_operands))
97
+ return MoverGeom(ent, block_d, triton.cdiv(head_dim, block_d))
98
+
99
+
100
+ class DualMatmulGeom(NamedTuple):
101
+ block_m: int
102
+ block_n: int
103
+ block_k: int
104
+ dot: str # tl.dot input_precision
105
+
106
+
107
+ def dual_matmul_geom(chunk_size: int, dtype: torch.dtype) -> DualMatmulGeom:
108
+ """Tiles of the dual matmul: at least two row blocks per chunk up to 64 rows. bf16 banks
109
+ use TF32 dots; fp32 banks follow ``torch.backends.cuda.matmul.allow_tf32``. IEEE dots
110
+ run on the FMA units, where 32-row blocks keep the accumulators in registers."""
111
+ fp32 = dtype == torch.float32
112
+ dot = "ieee" if fp32 and not torch.backends.cuda.matmul.allow_tf32 else "tf32"
113
+ block_m = min(64 if dot == "tf32" else 32, max(16, triton.next_power_of_2(chunk_size) // 2))
114
+ block_k = 16 if chunk_size <= 32 else 32
115
+ return DualMatmulGeom(block_m, 64, block_k, dot)
116
+
117
+
118
+ # ============================================================
119
+ # Kernels
120
+ # ============================================================
121
+
122
+
123
+ @triton.jit(do_not_specialize=["ti", "valid_tokens"])
124
+ def _sdm_dual_matmul(
125
+ x_ptr, qx_ptr, # [N, C, C] f32: X = (I + diag(beta) A)^-1, QX = QK @ X
126
+ v_ptr, beta_ptr, # [N * C, HD] bank dtype, [N * C] f32
127
+ ret_ptr, inter_ptr, # [SC, HD] f32 step rows from the gather
128
+ dv_ptr, dv_step_rows, # [*, HD] f32 delta_v
129
+ out_ptr, # [N * C, HD] readings, bank dtype
130
+ res_ptr, # [N * C, HD] f32 residual, or None
131
+ ti, P, valid_tokens,
132
+ C: tl.constexpr, HD: tl.constexpr,
133
+ BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr, DOT: tl.constexpr,
134
+ TRIG: tl.constexpr,
135
+ ):
136
+ """delta_v = X @ u and readings = QX @ u + inter with u = beta * (v - retrieved), for
137
+ chunk p of step ti. The first row block also stores residual = v - retrieved.
138
+ The epilogue's ``inter`` rows are loaded before the k loop (they overlap its loads). TRIG: issue the
139
+ programmatic-dependent-launch trigger first, so the step's write mover, launched as a programmatic
140
+ dependent, can start its setup while this kernel runs (forward._dual_trig_wanted); no effect otherwise.
141
+
142
+ Grid: (cdiv(C, BLOCK_M) * cdiv(HD, BLOCK_N) * P,), row blocks first, then column
143
+ blocks, then chunks (one axis, which holds any number of chunks).
144
+ """
145
+ if TRIG:
146
+ gdc_launch_dependents()
147
+ NUM_M: tl.constexpr = (C + BLOCK_M - 1) // BLOCK_M
148
+ NUM_N: tl.constexpr = (HD + BLOCK_N - 1) // BLOCK_N
149
+ pid = tl.program_id(0)
150
+ pid_m = pid % NUM_M
151
+ pid_n = pid // NUM_M % NUM_N
152
+ p = (pid // (NUM_M * NUM_N)).to(tl.int64)
153
+ chunk = ti.to(tl.int64) * P + p
154
+ # 64-bit bases of this chunk's rows; the offsets within a chunk fit 32 bits.
155
+ x_ptr += chunk * C * C
156
+ qx_ptr += chunk * C * C
157
+ v_ptr += chunk * C * HD
158
+ beta_ptr += chunk * C
159
+ ret_ptr += p * C * HD
160
+ inter_ptr += p * C * HD
161
+ dv_ptr += (ti.to(tl.int64) * dv_step_rows + p * C) * HD
162
+ out_ptr += chunk * C * HD
163
+ if res_ptr is not None:
164
+ res_ptr += chunk * C * HD
165
+ rows = tl.minimum(valid_tokens - ti * C, C)
166
+ rm = pid_m * BLOCK_M + tl.arange(0, BLOCK_M)
167
+ rn = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
168
+ n_ok = rn[None, :] < HD
169
+ m_ok = (rm < rows)[:, None]
170
+ in_c = (rm < C)[:, None] & n_ok
171
+ m_rows = rm[:, None] * HD + rn[None, :]
172
+ inter = tl.load(inter_ptr + m_rows, mask=m_ok & n_ok, other=0.0)
173
+ acc_dv = tl.zeros((BLOCK_M, BLOCK_N), tl.float32)
174
+ acc_out = tl.zeros((BLOCK_M, BLOCK_N), tl.float32)
175
+ for k0 in range(0, C, BLOCK_K):
176
+ rk = k0 + tl.arange(0, BLOCK_K)
177
+ k_ok = rk < rows
178
+ tile = k_ok[:, None] & n_ok
179
+ k_rows = rk[:, None] * HD + rn[None, :]
180
+ v = tl.load(v_ptr + k_rows, mask=tile, other=0.0)
181
+ r = tl.load(ret_ptr + k_rows, mask=tile, other=0.0)
182
+ res = v.to(tl.float32) - r
183
+ if res_ptr is not None:
184
+ if pid_m == 0:
185
+ tl.store(res_ptr + k_rows, res, mask=(rk < C)[:, None] & n_ok)
186
+ u = tl.load(beta_ptr + rk, mask=k_ok, other=0.0)[:, None] * res
187
+ x_off = rm[:, None] * C + rk[None, :]
188
+ x_ok = (rm < C)[:, None] & (rk < C)[None, :]
189
+ x = tl.load(x_ptr + x_off, mask=x_ok, other=0.0)
190
+ acc_dv = tl.dot(x, u, acc_dv, input_precision=DOT)
191
+ qx = tl.load(qx_ptr + x_off, mask=x_ok, other=0.0)
192
+ acc_out = tl.dot(qx, u, acc_out, input_precision=DOT)
193
+ tl.store(dv_ptr + m_rows, tl.where(m_ok, acc_dv, 0.0), mask=in_c)
194
+ out = tl.where(m_ok, acc_out + inter, 0.0)
195
+ tl.store(out_ptr + m_rows, out.to(out_ptr.dtype.element_ty), mask=in_c)
196
+
197
+
198
+ # ============================================================
199
+ # Movers
200
+ # ============================================================
201
+
202
+
203
+ # ============================================================
204
+ # Launchers
205
+ # ============================================================
206
+
207
+ def _check(t: torch.Tensor, name: str, numel: int, dtype=None) -> torch.Tensor:
208
+ if not t.is_contiguous() or t.numel() != numel or (dtype is not None and t.dtype != dtype):
209
+ want = f"{numel} contiguous elements" + (f" of {dtype}" if dtype is not None else "")
210
+ raise ValueError(f"{name}: expected {want}, got shape {tuple(t.shape)} "
211
+ f"stride {t.stride()} {t.dtype}")
212
+ return t
213
+
214
+
215
+ def quant_row(mode: str, head_dim: int) -> tuple[int, torch.dtype]:
216
+ """(row width, dtype) of a quantized snapshot row."""
217
+ if mode == "fp8":
218
+ return head_dim, torch.float8_e4m3fn
219
+ if mode == "int4":
220
+ return (head_dim + 1) // 2, torch.uint8
221
+ raise ValueError(f"snapshot_quant must be one of {SNAPSHOT_QUANT_MODES}, got {mode!r}")
222
+
223
+
224
+ def _step_rows(t: torch.Tensor, name: str, rows: int, steps: int, width: int) -> int:
225
+ """Row stride between steps of ``t``: ``rows`` if it holds every step, 0 if one."""
226
+ if t.is_contiguous() and t.numel() == steps * rows * width:
227
+ return rows
228
+ if t.is_contiguous() and t.numel() == rows * width:
229
+ return 0
230
+ raise ValueError(f"{name}: expected {rows} or {steps * rows} contiguous rows of {width}, "
231
+ f"got shape {tuple(t.shape)} stride {t.stride()}")
232
+
233
+
234
+ def make_dual_matmul(x, qx, v, beta, retrieved, inter, delta_v, readings, residual=None, *,
235
+ num_parallel: int, valid_tokens: int) -> Callable[[int], None]:
236
+ """Step launcher of delta_v = X @ u and readings = QX @ u + inter, u = beta * (v - retrieved).
237
+
238
+ x, qx: [N, C, C] f32; v, readings: [N * C, hd] (bank dtype); beta: [N * C] f32;
239
+ retrieved, inter: [SC, hd] f32 (this step's gather); delta_v: f32 rows of one step or
240
+ of every step; residual (v - retrieved, f32 [N * C, hd]) is stored when given.
241
+ """
242
+ n, c, _ = x.shape
243
+ hd = v.shape[-1]
244
+ sc = num_parallel * c
245
+ steps = n // num_parallel
246
+ geom = dual_matmul_geom(c, v.dtype)
247
+ for t, name in ((x, "x"), (qx, "qx")):
248
+ _check(t, name, n * c * c, torch.float32)
249
+ for t, name in ((retrieved, "retrieved"), (inter, "inter")):
250
+ _check(t, name, sc * hd, torch.float32)
251
+ _check(v, "v", n * c * hd)
252
+ _check(readings, "readings", n * c * hd, v.dtype)
253
+ _check(beta, "beta", n * c, torch.float32)
254
+ if residual is not None:
255
+ _check(residual, "residual", n * c * hd, torch.float32)
256
+ dv_rows = _step_rows(delta_v, "delta_v", sc, steps, hd)
257
+ kernel = tbind(_sdm_dual_matmul, (triton.cdiv(c, geom.block_m) * triton.cdiv(hd, geom.block_n)
258
+ * num_parallel,), v)
259
+ head = (x, qx, v, beta, retrieved, inter, delta_v, dv_rows, readings, residual)
260
+ meta = dict(C=c, HD=hd, BLOCK_M=geom.block_m, BLOCK_N=geom.block_n, BLOCK_K=geom.block_k,
261
+ DOT=geom.dot, TRIG=0, num_warps=4)
262
+
263
+ def launch(ti: int) -> None:
264
+ kernel(*head, ti, num_parallel, valid_tokens, **meta)
265
+ return launch
266
+
267
+
@@ -0,0 +1,113 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ """Exact top-k over all sums of product-key factors, with a sparse backward."""
3
+
4
+ import math
5
+ import operator
6
+ from collections.abc import Sequence
7
+
8
+ import torch
9
+
10
+ from .cuda.product_key_topk_anyf_cuda import MAX_FACTORS, MAX_SLOTS
11
+ from .cuda.product_key_topk_anyf_cuda import _module as _anyf_module
12
+ from .cuda.product_key_topk_cuda import _module
13
+
14
+
15
+ def _topk_function(module):
16
+ """The autograd function of the exact top-k kernels of `module` (a cached extension loader), with a sparse backward."""
17
+
18
+ class _ProductKeyTopK(torch.autograd.Function):
19
+ @staticmethod
20
+ def forward(ctx, raw, factors, k):
21
+ rows = raw.reshape(-1, raw.shape[-1])
22
+ if rows.stride(-1) != 1:
23
+ rows = rows.contiguous()
24
+ val, lin = module().topk_fwd(rows, factors, k)
25
+ ctx.set_materialize_grads(False)
26
+ ctx.save_for_backward(lin)
27
+ ctx.raw_shape = raw.shape
28
+ ctx.factors = factors
29
+ ctx.mark_non_differentiable(lin)
30
+ out_shape = (*raw.shape[:-1], lin.shape[-1])
31
+ return val.view(out_shape), lin.view(out_shape)
32
+
33
+ @staticmethod
34
+ def backward(ctx, grad_val, _grad_lin):
35
+ (lin,) = ctx.saved_tensors
36
+ grad = module().topk_bwd(lin, grad_val.reshape(lin.shape), ctx.factors)
37
+ return grad.view(ctx.raw_shape), None, None
38
+ return _ProductKeyTopK
39
+
40
+
41
+ _ProductKeyTopK = _topk_function(_module) # cuda/product_key_topk_cuda.py
42
+ _ProductKeyTopKAnyF = _topk_function(_anyf_module) # cuda/product_key_topk_anyf_cuda.py
43
+
44
+
45
+ def _bit_packed(factors: tuple[int, ...]) -> bool:
46
+ """Whether the kernels of ``cuda/product_key_topk_cuda.py`` take ``factors``: two to four
47
+ factors of 1 to 65535 columns, with at most 32 bits of bit-packed slot index."""
48
+ return (2 <= len(factors) <= 4 and all(1 <= f <= 65535 for f in factors)
49
+ and sum((f - 1).bit_length() for f in factors) <= 32)
50
+
51
+
52
+ def check_product_key_factors(factors: Sequence[int]) -> tuple[int, ...]:
53
+ """The factor widths as a tuple of ints, or ``ValueError`` if ``product_key_topk`` cannot
54
+ select over them: it takes 1 to ``MAX_FACTORS`` factors of at least one column each,
55
+ addressing at most ``MAX_SLOTS`` slots with a row width of at most ``MAX_SLOTS`` (or
56
+ any factor list of the two- to four-factor kernels)."""
57
+ if isinstance(factors, (str, bytes)) or not isinstance(factors, Sequence):
58
+ raise ValueError(f"product-key factors must be a sequence of ints, got {factors!r}")
59
+ if any(isinstance(f, bool) for f in factors):
60
+ raise ValueError(f"product-key factors must be ints, got {list(factors)}")
61
+ try:
62
+ dims = tuple(operator.index(f) for f in factors)
63
+ except TypeError:
64
+ raise ValueError(f"product-key factors must be ints, got {list(factors)}") from None
65
+ if not 1 <= len(dims) <= MAX_FACTORS or min(dims) < 1:
66
+ raise ValueError(f"product-key factors must be 1 to {MAX_FACTORS} positive ints, got "
67
+ f"{list(dims)}")
68
+ if not _bit_packed(dims) and (math.prod(dims) > MAX_SLOTS or sum(dims) > MAX_SLOTS):
69
+ raise ValueError(f"product-key factors {list(dims)} address {math.prod(dims)} slots "
70
+ f"over {sum(dims)} columns; at most {MAX_SLOTS} of each.")
71
+ return dims
72
+
73
+
74
+ def product_key_topk(
75
+ raw: torch.Tensor, factors: Sequence[int], k: int
76
+ ) -> tuple[torch.Tensor, torch.Tensor]:
77
+ """Exact top-k over all sums of one value from each product-key factor, for any number
78
+ of factors.
79
+
80
+ raw [..., sum(factors)] (CUDA, bfloat16 or float32) holds the F >= 1 factors side by side,
81
+ each at least one column wide (see ``check_product_key_factors`` for the limits). Slot
82
+ (c_0, ..., c_{F-1}) scores the float32 sum of its factor values, added in factor order;
83
+ with bfloat16 input and more than two factors, each partial sum is rounded to bfloat16
84
+ before the next factor is added. With one factor, slot c_0 scores raw[..., c_0]. Returns
85
+ (val, lin), each [..., kk] with kk = min(k, prod(factors)) and 1 <= k <= 256:
86
+ lin = (..(c_0 * factors[1] + c_1) * ..) * factors[F-1] + c_{F-1} (int64), strictly
87
+ ascending, and val = the slot's score rounded to raw.dtype. val is differentiable with
88
+ respect to raw; the backward is deterministic.
89
+
90
+ Selection rule: scores compare as float32 numbers, with -0 below +0 and NaN counted as
91
+ -inf. Each factor is ranked by value, larger column first on ties; one factor selects its
92
+ first kk ranks. Stage f = 1..F-1 keeps the best c = min(k, n * m) of the pairs (i, j) of a
93
+ rank i < n = min(k, prod(factors[:f])) of the previous stage and a rank j < m =
94
+ min(k, factors[f]) of factor f with (i + 1) * (j + 1) <= c, exact ties going to the
95
+ larger i + j, then the larger i. Each pair left out has c pairs of lower or equal ranks
96
+ scoring at least as much, so the selected scores are an exact top-k. With two factors,
97
+ bfloat16 input and its float32 upcast select the same slots. The kernels never
98
+ synchronize and need no warmup before CUDA-graph capture.
99
+
100
+ Two to four factors of at most 65535 columns and 32 bits of bit-packed slot index run on
101
+ the kernels of ``cuda/product_key_topk_cuda.py``; every other factor list runs on those
102
+ of ``cuda/product_key_topk_anyf_cuda.py``, which apply the same rule.
103
+ """
104
+ factors = tuple(operator.index(f) for f in factors)
105
+ if _bit_packed(factors):
106
+ return _ProductKeyTopK.apply(raw, factors, operator.index(k))
107
+ return _ProductKeyTopKAnyF.apply(raw, factors, operator.index(k))
108
+
109
+
110
+ def _product_key_topk_anyf(raw, factors, k):
111
+ """``product_key_topk`` on the any-factor kernels whatever the factor list (tests)."""
112
+ factors = tuple(operator.index(f) for f in factors)
113
+ return _ProductKeyTopKAnyF.apply(raw, factors, operator.index(k))
@@ -0,0 +1,273 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # The 16 x 16 tile scheme of the level-1 kernel is adapted from the solve_tril kernel of
4
+ # flash-linear-attention (https://github.com/fla-org/flash-linear-attention), which is
5
+ # distributed under the MIT License:
6
+ #
7
+ # Copyright (c) 2023-2026 Songlin Yang, Yu Zhang, Zhiyuan Li
8
+ #
9
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ # of this software and associated documentation files (the "Software"), to deal
11
+ # in the Software without restriction, including without limitation the rights
12
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ # copies of the Software, and to permit persons to whom the Software is
14
+ # furnished to do so, subject to the following conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be included in all
17
+ # copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ # SOFTWARE.
26
+ """Per-chunk WY solve: X = (I + L)^-1 and QX = QK @ X.
27
+
28
+ For each chunk of C tokens, ``L = tril(beta[:, None] * A, -1)`` is the gated strictly lower
29
+ key-key matrix and ``QK`` the causal query-key matrix (diagonal included). ``wy_solve``
30
+ returns the unit lower-triangular ``X`` and ``QX``, both dense row-major [N, C, C] float32
31
+ with +0.0 above the diagonal. Only the strict lower triangle of ``A`` and the lower
32
+ triangle of ``QK`` are read, so the rest may hold anything.
33
+
34
+ Level 1: one program per 64 x 64 diagonal block (16 x 16 or 32 x 32 when C <= 32). Its
35
+ 16 x 16 diagonal tiles are inverted by forward substitution and the tiles below them by
36
+ matmuls. The same program emits the diagonal blocks of QX and the zeros of its block row.
37
+
38
+ Level 2 (C > 64): one program per 64-column panel J walks down the blocks below the
39
+ diagonal, X[i, J] = -X[i, i] @ sum_{k=J}^{i-1} L[i, k] X[k, J], and accumulates
40
+ QX[i, J] = sum_{k=J}^{i} QK[i, k] X[k, J] from the same loads of X. Its matmuls take
41
+ 16-wide slices of the reduction dimension from memory (a full-float32 dot keeps whole
42
+ operand rows in registers, so wider operands spill); X[i, J] is staged in its own output
43
+ block so that it can be read back that way.
44
+
45
+ Rows and columns >= C load as zero, i.e. identity rows of I + L. The leading block of a
46
+ lower-triangular inverse is the inverse of the leading block, so any C is exact.
47
+
48
+ Every dot is full float32 whatever torch.backends.cuda.matmul.allow_tf32 says: the inverse
49
+ amplifies input rounding. Launch configurations are pinned, not autotuned: the warp count
50
+ changes the rounding, so the table is part of the numerical contract, and a fixed launch
51
+ is capture-safe and gives the same bits for a chunk at any N.
52
+
53
+ The 16 x 16 tile scheme is adapted from the ``solve_tril`` kernel of flash-linear-attention
54
+ (MIT License, notice above).
55
+ """
56
+
57
+ import torch
58
+ import triton
59
+ import triton.language as tl
60
+
61
+ from .cuda._device import tbind
62
+
63
+ # Pinned (num_warps, num_stages) of the two kernels.
64
+ _DIAG_CONFIG = (1, 2)
65
+ _OFFDIAG_CONFIG = (2, 3)
66
+
67
+
68
+ @triton.jit
69
+ def _mm(a, b, acc=None):
70
+ return tl.dot(a, b, acc, input_precision="ieee")
71
+
72
+
73
+ @triton.jit
74
+ def _load(base, rows, cols, LIM, C: tl.constexpr):
75
+ """base[rows, cols] with row pitch C; zero outside the leading [LIM, LIM] block."""
76
+ return tl.load(base + rows[:, None] * C + cols[None, :],
77
+ mask=(rows < LIM)[:, None] & (cols < LIM)[None, :], other=0.0)
78
+
79
+
80
+ @triton.jit
81
+ def _store(base, v, rows, cols, LIM, C: tl.constexpr):
82
+ tl.store(base + rows[:, None] * C + cols[None, :], v,
83
+ mask=(rows < LIM)[:, None] & (cols < LIM)[None, :])
84
+
85
+
86
+ @triton.jit
87
+ def _tile(base, r0, c0, LIM, C: tl.constexpr):
88
+ o = tl.arange(0, 16)
89
+ return _load(base, r0 + o, c0 + o, LIM, C)
90
+
91
+
92
+ @triton.jit
93
+ def _put(base, v, r0, c0, LIM, C: tl.constexpr):
94
+ o = tl.arange(0, 16)
95
+ _store(base, v, r0 + o, c0 + o, LIM, C)
96
+
97
+
98
+ @triton.jit
99
+ def _band_beta(beta, r0, LIM):
100
+ o = tl.arange(0, 16)
101
+ return tl.load(beta + r0 + o, mask=r0 + o < LIM, other=0.0)[:, None]
102
+
103
+
104
+ @triton.jit
105
+ def _lower(a, beta, r0, c0, LIM, C: tl.constexpr):
106
+ """beta[:, None] * A on the 16 x 16 tile at (r0, c0)."""
107
+ return _tile(a, r0, c0, LIM, C) * _band_beta(beta, r0, LIM)
108
+
109
+
110
+ @triton.jit
111
+ def _subst_start(a, beta, r0, LIM, C: tl.constexpr):
112
+ """-L of the diagonal tile at r0: rows 0 and 1 of (I + L)^-1 - I are already final."""
113
+ o = tl.arange(0, 16)
114
+ return -tl.where(o[:, None] > o[None, :], _lower(a, beta, r0, r0, LIM, C), 0.0)
115
+
116
+
117
+ @triton.jit
118
+ def _subst_row(b, a, beta, r0, i, LIM, C: tl.constexpr):
119
+ """Forward substitution of row i of the diagonal tile at r0."""
120
+ o = tl.arange(0, 16)
121
+ r = r0 + i
122
+ row = tl.load(a + r * C + r0 + o, mask=(r < LIM) & (r0 + o < LIM), other=0.0)
123
+ row = -tl.where(r0 + o < r, row * tl.load(beta + r, mask=r < LIM, other=0.0), 0.0)
124
+ row += tl.sum(row[:, None] * b, 0)
125
+ return tl.where((o == i)[:, None], row, b)
126
+
127
+
128
+ @triton.jit
129
+ def _qk_diag(q, r0, LIM, C: tl.constexpr):
130
+ o = tl.arange(0, 16)
131
+ return tl.where(o[:, None] >= o[None, :], _tile(q, r0, r0, LIM, C), 0.0)
132
+
133
+
134
+ @triton.jit
135
+ def _wy_diag_kernel(A, Beta, QK, X, QX, C: tl.constexpr, BD: tl.constexpr):
136
+ n = tl.program_id(0).to(tl.int64)
137
+ off = tl.program_id(1) * BD
138
+ LIM = C - off
139
+ blk = n * C * C + off * C + off
140
+ a, q, x, qx = A + blk, QK + blk, X + blk, QX + blk
141
+ beta = Beta + n * C + off
142
+
143
+ # Diagonal tiles; the independent substitutions are interleaved for latency hiding.
144
+ x11 = _subst_start(a, beta, 0, LIM, C)
145
+ if BD >= 32:
146
+ x22 = _subst_start(a, beta, 16, LIM, C)
147
+ if BD == 64:
148
+ x33 = _subst_start(a, beta, 32, LIM, C)
149
+ x44 = _subst_start(a, beta, 48, LIM, C)
150
+ for i in range(2, 16):
151
+ x11 = _subst_row(x11, a, beta, 0, i, LIM, C)
152
+ if BD >= 32:
153
+ x22 = _subst_row(x22, a, beta, 16, i, LIM, C)
154
+ if BD == 64:
155
+ x33 = _subst_row(x33, a, beta, 32, i, LIM, C)
156
+ x44 = _subst_row(x44, a, beta, 48, i, LIM, C)
157
+ o = tl.arange(0, 16)
158
+ eye = (o[:, None] == o[None, :]).to(tl.float32)
159
+
160
+ x11 += eye
161
+ _put(x, x11, 0, 0, LIM, C)
162
+ _put(qx, _mm(_qk_diag(q, 0, LIM, C), x11), 0, 0, LIM, C)
163
+ if BD >= 32:
164
+ x22 += eye
165
+ x21 = -_mm(_mm(x22, _lower(a, beta, 16, 0, LIM, C)), x11)
166
+ _put(x, x21, 16, 0, LIM, C)
167
+ _put(x, x22, 16, 16, LIM, C)
168
+ q22 = _qk_diag(q, 16, LIM, C)
169
+ _put(qx, _mm(q22, x21, _mm(_tile(q, 16, 0, LIM, C), x11)), 16, 0, LIM, C)
170
+ _put(qx, _mm(q22, x22), 16, 16, LIM, C)
171
+ if BD == 64:
172
+ # The association of each product is fixed: it defines the output bits.
173
+ x33 += eye
174
+ x44 += eye
175
+ a32 = _lower(a, beta, 32, 16, LIM, C)
176
+ a42 = _lower(a, beta, 48, 16, LIM, C)
177
+ a43 = _lower(a, beta, 48, 32, LIM, C)
178
+ x32 = -_mm(_mm(x33, a32), x22)
179
+ x43 = -_mm(_mm(x44, a43), x33)
180
+ x31 = -_mm(x33, _mm(_lower(a, beta, 32, 0, LIM, C), x11) + _mm(a32, x21))
181
+ x42 = -_mm(x44, _mm(a42, x22) + _mm(a43, x32))
182
+ x41 = -_mm(x44, _mm(_lower(a, beta, 48, 0, LIM, C), x11) + _mm(a42, x21) + _mm(a43, x31))
183
+ _put(x, x31, 32, 0, LIM, C)
184
+ _put(x, x32, 32, 16, LIM, C)
185
+ _put(x, x33, 32, 32, LIM, C)
186
+ _put(x, x41, 48, 0, LIM, C)
187
+ _put(x, x42, 48, 16, LIM, C)
188
+ _put(x, x43, 48, 32, LIM, C)
189
+ _put(x, x44, 48, 48, LIM, C)
190
+ q32 = _tile(q, 32, 16, LIM, C)
191
+ q33 = _qk_diag(q, 32, LIM, C)
192
+ _put(qx, _mm(q33, x31, _mm(q32, x21, _mm(_tile(q, 32, 0, LIM, C), x11))), 32, 0, LIM, C)
193
+ _put(qx, _mm(q33, x32, _mm(q32, x22)), 32, 16, LIM, C)
194
+ _put(qx, _mm(q33, x33), 32, 32, LIM, C)
195
+ q42 = _tile(q, 48, 16, LIM, C)
196
+ q43 = _tile(q, 48, 32, LIM, C)
197
+ q44 = _qk_diag(q, 48, LIM, C)
198
+ _put(qx, _mm(q44, x41, _mm(q43, x31, _mm(q42, x21, _mm(_tile(q, 48, 0, LIM, C), x11)))),
199
+ 48, 0, LIM, C)
200
+ _put(qx, _mm(q44, x42, _mm(q43, x32, _mm(q42, x22))), 48, 16, LIM, C)
201
+ _put(qx, _mm(q44, x43, _mm(q43, x33)), 48, 32, LIM, C)
202
+ _put(qx, _mm(q44, x44), 48, 48, LIM, C)
203
+
204
+ # Zeros: the 16 x 16 tiles above the diagonal inside the block, then the rest of the row.
205
+ r = tl.arange(0, BD)
206
+ for c in range(0, LIM, BD):
207
+ cols = c + r
208
+ m = (cols[None, :] // 16 > r[:, None] // 16) & (r[:, None] < LIM) & (cols[None, :] < LIM)
209
+ offs = r[:, None] * C + cols[None, :]
210
+ tl.store(x + offs, 0.0, mask=m)
211
+ tl.store(qx + offs, 0.0, mask=m)
212
+
213
+
214
+ @triton.jit
215
+ def _wy_offdiag_kernel(A, Beta, QK, X, QX, C: tl.constexpr, NB: tl.constexpr):
216
+ n = tl.program_id(0).to(tl.int64)
217
+ j = tl.program_id(1)
218
+ base = n * C * C
219
+ a, q, x, qx = A + base, QK + base, X + base, QX + base
220
+ r = tl.arange(0, 64)
221
+ s = tl.arange(0, 16)
222
+ cj = j * 64 + r
223
+ for i in range(j + 1, NB):
224
+ ri = i * 64 + r
225
+ beta = tl.load(Beta + n * C + ri, mask=ri < C, other=0.0)[:, None]
226
+ lx = tl.zeros((64, 64), dtype=tl.float32)
227
+ qx_ij = tl.zeros((64, 64), dtype=tl.float32)
228
+ for k in range(j * 64, i * 64, 16):
229
+ x_kj = _load(x, k + s, cj, C, C)
230
+ lx = _mm(_load(a, ri, k + s, C, C) * beta, x_kj, lx)
231
+ qx_ij = _mm(_load(q, ri, k + s, C, C), x_kj, qx_ij)
232
+ # X[i, j] = -X[i, i] @ lx, with lx staged in the X[i, j] block.
233
+ _store(x, lx, ri, cj, C, C)
234
+ tl.debug_barrier()
235
+ x_ij = tl.zeros((64, 64), dtype=tl.float32)
236
+ for k in range(i * 64, i * 64 + 64, 16):
237
+ x_ij = _mm(_load(x, ri, k + s, C, C), _load(x, k + s, cj, C, C), x_ij)
238
+ tl.debug_barrier()
239
+ _store(x, -x_ij, ri, cj, C, C)
240
+ tl.debug_barrier()
241
+ for k in range(i * 64, i * 64 + 64, 16):
242
+ cols = k + s
243
+ q_ii = tl.where(ri[:, None] >= cols[None, :], _load(q, ri, cols, C, C), 0.0)
244
+ qx_ij = _mm(q_ii, _load(x, cols, cj, C, C), qx_ij)
245
+ _store(qx, qx_ij, ri, cj, C, C)
246
+
247
+
248
+ def wy_solve(A: torch.Tensor, beta: torch.Tensor, QK: torch.Tensor):
249
+ """X = (I + tril(beta[:, None] * A, -1))^-1 and QX = QK @ X for each chunk.
250
+
251
+ A, QK: [N, C, C] float32, any C >= 1. beta: N * C float32 values, e.g. [N, C].
252
+ Returns (X, QX), contiguous [N, C, C] float32.
253
+ """
254
+ if A.dim() != 3 or A.shape[1] != A.shape[2] or QK.shape != A.shape:
255
+ raise ValueError(
256
+ f"wy_solve: A and QK must be [N, C, C], got {tuple(A.shape)} and {tuple(QK.shape)}")
257
+ N, C, _ = A.shape
258
+ if beta.numel() != N * C:
259
+ raise ValueError(f"wy_solve: beta must have N * C = {N * C} elements, got {beta.numel()}")
260
+ if not A.dtype == QK.dtype == beta.dtype == torch.float32:
261
+ raise TypeError("wy_solve: A, beta and QK must be float32")
262
+ A, beta, QK = A.contiguous(), beta.reshape(N * C).contiguous(), QK.contiguous()
263
+ X = torch.empty_like(A)
264
+ QX = torch.empty_like(A)
265
+ bd = 16 if C <= 16 else 32 if C <= 32 else 64
266
+ nb = triton.cdiv(C, bd)
267
+ w, s = _DIAG_CONFIG
268
+ tbind(_wy_diag_kernel, (N, nb), A)(A, beta, QK, X, QX, C=C, BD=bd, num_warps=w, num_stages=s)
269
+ if nb > 1:
270
+ w, s = _OFFDIAG_CONFIG
271
+ tbind(_wy_offdiag_kernel, (N, nb - 1), A)(A, beta, QK, X, QX, C=C, NB=nb,
272
+ num_warps=w, num_stages=s)
273
+ return X, QX