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,140 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ """Single-token (T == 1) SDM step: write then read, one kernel launch for all streams."""
3
+
4
+ import torch
5
+ import triton
6
+ import triton.language as tl
7
+
8
+ from .memory_ops import MAX_WIDTH
9
+
10
+
11
+ @triton.jit
12
+ def _sdm_decode_kernel(
13
+ mem_ptr, k_idx_ptr, k_val_ptr, v_ptr, beta_ptr, g_ptr, q_idx_ptr, q_val_ptr, out_ptr,
14
+ hd,
15
+ W: tl.constexpr, R: tl.constexpr, G: tl.constexpr,
16
+ BLOCK_W: tl.constexpr, BLOCK_R: tl.constexpr, BLOCK_D: tl.constexpr,
17
+ ):
18
+ s = tl.program_id(0).to(tl.int64)
19
+ cols = tl.program_id(1) * BLOCK_D + tl.arange(0, BLOCK_D)
20
+ col_mask = cols < hd
21
+ bank = mem_ptr.dtype.element_ty
22
+
23
+ w = tl.arange(0, BLOCK_W)
24
+ w_mask = w < W
25
+ k_slot = tl.load(k_idx_ptr + s * W + w, mask=w_mask, other=0).to(tl.int64)
26
+ k_val = tl.load(k_val_ptr + s * W + w, mask=w_mask, other=0.0).to(bank).to(tl.float32)
27
+ # G == 1: one gate per token; G == W: one gate per write.
28
+ decay = tl.exp(tl.load(g_ptr + s * G + w % G, mask=w_mask, other=0.0).to(tl.float32))
29
+ # Read slots and weights do not depend on the bank: load them before the barrier.
30
+ r = tl.arange(0, BLOCK_R)
31
+ r_mask = r < R
32
+ q_slot = tl.load(q_idx_ptr + s * R + r, mask=r_mask, other=0).to(tl.int64)
33
+ q_val = tl.load(q_val_ptr + s * R + r, mask=r_mask, other=0.0).to(bank).to(tl.float32)
34
+ k_ptrs = mem_ptr + k_slot[:, None] * hd + cols[None, :]
35
+ k_mask = w_mask[:, None] & col_mask[None, :]
36
+ rows = tl.load(k_ptrs, mask=k_mask, other=0.0).to(tl.float32)
37
+
38
+ retrieved = tl.sum((k_val * decay)[:, None] * rows, axis=0)
39
+ v = tl.load(v_ptr + s * hd + cols, mask=col_mask, other=0.0).to(bank).to(tl.float32)
40
+ delta_v = tl.load(beta_ptr + s).to(tl.float32) * (v - retrieved)
41
+ # Write slots are distinct within a stream, so plain stores suffice.
42
+ rows = rows * decay[:, None] + k_val[:, None] * delta_v[None, :]
43
+ tl.store(k_ptrs, rows.to(bank), mask=k_mask)
44
+
45
+ # The reads must see this step's writes. Only this program touches these columns of
46
+ # this stream's slots, so a CTA barrier orders the stores before the loads.
47
+ tl.debug_barrier()
48
+
49
+ q_rows = tl.load(mem_ptr + q_slot[:, None] * hd + cols[None, :],
50
+ mask=r_mask[:, None] & col_mask[None, :], other=0.0).to(tl.float32)
51
+ readings = tl.sum(q_val[:, None] * q_rows, axis=0)
52
+ tl.store(out_ptr + s * hd + cols, readings.to(bank), mask=col_mask)
53
+
54
+
55
+ def sdm_decode_step(
56
+ memory: torch.Tensor,
57
+ k_idx: torch.Tensor,
58
+ k_val: torch.Tensor,
59
+ v: torch.Tensor,
60
+ beta: torch.Tensor,
61
+ g: torch.Tensor,
62
+ q_idx: torch.Tensor,
63
+ q_val: torch.Tensor,
64
+ ) -> torch.Tensor:
65
+ """Advance ``memory`` by one token per stream and read it back.
66
+
67
+ Per stream, with d = exp(g):
68
+ retrieved = sum_w k_val[w] * d[w] * memory[k_idx[w]]
69
+ delta_v = beta * (v - retrieved)
70
+ memory[k_w] = d[w] * memory[k_w] + k_val[w] * delta_v
71
+ readings = sum_r q_val[r] * memory[q_idx[r]] (after the write)
72
+
73
+ Args:
74
+ memory: [num_slots, head_dim] contiguous bank, bfloat16 or float32; updated in
75
+ place. Each stream's slots must be private to it (the layer adds per-stream
76
+ offsets) and its W write slots distinct; neither is checked.
77
+ k_idx, k_val: [..., W] write slots (int32 or int64) and weights, W <= 256.
78
+ v: [..., head_dim] values. beta: [..., 1] write strengths.
79
+ g: [..., 1] log-decay per token, or [..., W] per write.
80
+ q_idx, q_val: [..., R] read slots and weights, R <= 256.
81
+ All inputs share the leading dims, one entry per stream. As in the chunked op, k_val,
82
+ v and q_val are rounded to the bank dtype, and beta and g are used in float32.
83
+
84
+ Returns:
85
+ readings shaped like ``v``, in the bank dtype.
86
+ """
87
+ if memory.dtype not in (torch.bfloat16, torch.float32):
88
+ raise TypeError(f"memory must be bfloat16 or float32, got {memory.dtype}")
89
+ if memory.ndim != 2 or not memory.is_cuda or not memory.is_contiguous():
90
+ raise ValueError("memory must be a contiguous CUDA tensor [num_slots, head_dim]")
91
+ if memory.shape[0] < 1 or memory.shape[1] < 1:
92
+ raise ValueError(f"memory needs at least one slot and one column, got shape "
93
+ f"{tuple(memory.shape)}")
94
+ if k_idx.dtype not in (torch.int32, torch.int64) or q_idx.dtype not in (torch.int32,
95
+ torch.int64):
96
+ raise TypeError(f"k_idx and q_idx must be int32 or int64, got {k_idx.dtype} and "
97
+ f"{q_idx.dtype}")
98
+ dev = memory.get_device()
99
+ for name, t in (("k_idx", k_idx), ("k_val", k_val), ("v", v), ("beta", beta), ("g", g),
100
+ ("q_idx", q_idx), ("q_val", q_val)):
101
+ if not t.is_cuda or t.get_device() != dev:
102
+ raise ValueError(f"{name} is on {t.device}, memory on {memory.device}")
103
+ hd = memory.shape[1]
104
+ W, R, G = k_idx.shape[-1], q_idx.shape[-1], g.shape[-1]
105
+ if not (1 <= W <= MAX_WIDTH and 1 <= R <= MAX_WIDTH):
106
+ raise ValueError(f"W and R must be in 1..{MAX_WIDTH}, got W={W}, R={R}")
107
+ if G not in (1, W):
108
+ raise ValueError(f"g must have last dim 1 or W={W}, got {G}")
109
+ lead = k_idx.shape[:-1]
110
+ for name, t, n in (("k_val", k_val, W), ("v", v, hd), ("beta", beta, 1), ("g", g, G),
111
+ ("q_idx", q_idx, R), ("q_val", q_val, R)):
112
+ if t.shape != (*lead, n):
113
+ raise ValueError(f"{name} has shape {tuple(t.shape)}, expected {(*lead, n)}")
114
+ return _decode_launch(memory, k_idx, k_val, v, beta, g, q_idx, q_val)
115
+
116
+
117
+ def _decode_launch(memory, k_idx, k_val, v, beta, g, q_idx, q_val):
118
+ """sdm_decode_step without its argument checks: the layer's decode branch, whose inputs the layer builds and whose
119
+ bank was validated at the prefill (or made by create_kv_cache)."""
120
+ hd = memory.shape[1]
121
+ W, R, G = k_idx.shape[-1], q_idx.shape[-1], g.shape[-1]
122
+ streams = k_idx.shape[:-1].numel()
123
+
124
+ def flat(t):
125
+ return t.reshape(streams, t.shape[-1]).contiguous()
126
+
127
+ readings = torch.empty(v.shape, device=memory.device, dtype=memory.dtype)
128
+ block_w, block_r = triton.next_power_of_2(W), triton.next_power_of_2(R)
129
+ # Narrow column blocks: many programs keep many row loads in flight, and the step is
130
+ # latency bound. About 128 bytes of bank rows per thread.
131
+ block_d = min(16, triton.next_power_of_2(hd))
132
+ tile_bytes = max(block_w, block_r) * block_d * memory.element_size()
133
+ num_warps = min(4, max(1, tile_bytes // (128 * 32)))
134
+ _sdm_decode_kernel[(streams, triton.cdiv(hd, block_d))](
135
+ memory, flat(k_idx), flat(k_val), flat(v), flat(beta), flat(g), flat(q_idx),
136
+ flat(q_val), readings, hd,
137
+ W=W, R=R, G=G, BLOCK_W=block_w, BLOCK_R=block_r, BLOCK_D=block_d,
138
+ num_warps=num_warps,
139
+ )
140
+ return readings
@@ -0,0 +1,464 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ """The forward of the chunked write/read over a range of steps (``forward_steps``).
3
+
4
+ Phase 1 with the per-call field set (``phase1_fields``) and the solve, then per step the gather, the dual matmul and the
5
+ decay-and-write (``movers.make_mover_dw``), with a compact-snapshot handshake between the gather and the DW.
6
+ memory_ops' entry points (phase1_forward, token_gather, make_dual_matmul, make_mover_dw, ...) are looked up there at
7
+ call time."""
8
+
9
+ import threading
10
+
11
+ import torch
12
+
13
+ from . import backward, memory_ops as mo, movers
14
+ from .cuda import _devmemo, gather_ns_cuda
15
+ from .cuda import phase1_cuda as ph
16
+
17
+ STATS = {}
18
+
19
+ def _dual_trig_wanted(fx_geom, num_parallel, head_dim, chunk_size, pdl, device_index):
20
+ """Whether to launch the dual matmul with its early trigger (TRIG): only when the DW follows it with programmatic
21
+ dependent launch (pdl), the chunk has at most 64 tokens and the DW's grid fits in one wave of resident CTAs
22
+ (num_parallel * column slices * K CTAs <= 2 CTAs of 8 warps per SM of the call's device, half that for 16-warp
23
+ CTAs). The DW's setup then overlaps the dual; with longer chunks or a grid of several waves, starting the DW early
24
+ costs more than it saves. Speed only: the kernel body, hence the bits, are the same either way."""
25
+ if not pdl or fx_geom is None:
26
+ return False
27
+ if chunk_size > 64:
28
+ return False
29
+ nw, K = int(fx_geom.get("nw", 8)), int(fx_geom.get("K", 0))
30
+ nsl = -(-head_dim // 128)
31
+ ctas = num_parallel * (nsl // 2 if fx_geom.get("w2") else nsl) * K # the two-slice walk: one CTA per slice pair
32
+ wave = 2 * mo._num_sms(device_index) # resident 8-warp DW CTAs per wave (speed only)
33
+ return 0 < ctas <= (wave if nw <= 8 else wave // 2)
34
+
35
+
36
+ # Phase-1 fields (phase1_forward(det_fields=...)) per call. Lean sets only when every consumer that can run takes the
37
+ # CUDA kernels: the DW (movers.dw_admits); for a saving forward also the backward (backward.admits_at_forward: its own
38
+ # admission with the worst grad dtype, and the CUDA GDS for every grad dtype it may use). No-grad: SPAY_K | KREC | TILES
39
+ # (296); saving adds SPAY_Q | QREC | DC_GIDX (952), with the 8-byte read records instead of the 16-byte ones (QREC8)
40
+ # when gx_gds_cuda's GDS mover, which reads them, will take the backward's GDS (_qrec8_ok). Otherwise the full set (the
41
+ # shipped fields, the mover records and DC_GIDX: 1023): since 2.0.0 only a saving call whose backward admission at
42
+ # forward time (every grad dtype it may use) declines while memory_ops.refuse_non_native admitted it with its actual grad
43
+ # dtype; every other call this function would give the full set is refused up front.
44
+ _NOGRAD_FIELDS = ph.DET_SPAY_K | ph.DET_KREC | ph.DET_TILES
45
+ _SAVING_FIELDS = _NOGRAD_FIELDS | ph.DET_SPAY_Q | ph.DET_QREC | ph.DET_DC_GIDX
46
+ _SAVING_FIELDS_QREC8 = (_SAVING_FIELDS & ~ph.DET_QREC) | ph.DET_QREC8
47
+ _FULL_FIELDS = (ph.DET_SHIPPED | ph.DET_KREC | ph.DET_KSPA | ph.DET_QREC | ph.DET_TILES | ph.DET_DC_GIDX)
48
+
49
+
50
+ def _qrec8_ok(memory, C, R, grad_bank_fp32):
51
+ """gx_gds_cuda's GDS mover will take this saving call's GDS (the CUDA GDS admission already passed):
52
+ gx_gds_cuda.admits for the call's bank, grad dtype (float32 with grad_bank_fp32 on a bfloat16 bank), C and R.
53
+ grad_bank_fp32 None (grad dtype unknown) -> False."""
54
+ if grad_bank_fp32 is None:
55
+ return False
56
+ from .cuda import gx_gds_cuda
57
+ return bool(gx_gds_cuda.admits(memory, torch.float32 if grad_bank_fp32 else memory.dtype, C, R))
58
+
59
+
60
+ def _fxq_rows(memory, C, quant):
61
+ """fp8 / int4: mover_fx_cuda's quantized DW writes this call's compact rows (eager and captured calls); bf16 (float32
62
+ banks): its bank-direct WR walk writes them as bf16 rows (mover_fx_cuda.s16_supported)."""
63
+ from .cuda import mover_fx_cuda
64
+ if quant == "bf16":
65
+ return mover_fx_cuda.s16_supported(memory, C)
66
+ return mover_fx_cuda.quant_supported(memory, C, {"fp8": 1, "int4": 2}[quant])
67
+
68
+
69
+ # snapshot_quant "bf16" on a float32 bank runs on every CUDA backend (an RTX GPU gets the image
70
+ # guard's one-time untested-hardware warning); a bfloat16 bank runs "bf16" as "none" (memory_ops.effective_snapshot_quant).
71
+ def bf16_backend_ok(device) -> bool:
72
+ """snapshot_quant "bf16" on a float32 bank may run on this device's class: every CUDA backend (read through _devmemo)."""
73
+ try:
74
+ return bool(_devmemo.backend(device))
75
+ except RuntimeError: # a device outside the CUDA backends
76
+ return False
77
+
78
+
79
+ def bf16_route(memory, C, rows) -> bool:
80
+ """snapshot_quant bf16 (float32 banks) has its native CUDA route for this call: the unquantized DW on the CUDA movers
81
+ (dw_admits as "none"), mover_fx_cuda's bank-direct WR walk writing the bf16 compact rows (s16_supported) and the CUDA
82
+ backward reading them (backward.supported "bf16": gx_pa2's bf16-snapshot rows, the GDS / GWR of "none"). Shapes,
83
+ dtypes and the device; rows = backward.compact_rows(lay). Anything else is refused (no fallback route)."""
84
+ return bool(memory.is_cuda and memory.dim() == 2 and memory.dtype == torch.float32
85
+ and bf16_backend_ok(memory.device)
86
+ and movers.dw_admits(memory, C, "none")
87
+ and backward.supported(memory, "bf16", C=C, grad_f32_possible=True, rows=rows)
88
+ and _fxq_rows(memory, C, "bf16"))
89
+
90
+
91
+ # A bf16 saving forward's admission, memoized per signature (phase1_fields' saving checks and
92
+ # forward_steps' compact-snapshot layout evaluated the bf16 rows' producer / reader predicates, mover_fx_cuda.s16_supported
93
+ # and gx_pa2_cuda.s16_admits, on every call). Admitted signatures only: a declined call evaluates them as before (and
94
+ # memory_ops.refuse_non_native refuses such a call up front).
95
+ _BF16_SAVING = {}
96
+ _BF16_SAVING_MAX = 4096
97
+ _BF16_SAVING_LOCK = threading.Lock()
98
+
99
+
100
+ def bf16_saving_admitted(memory, C, rows) -> bool:
101
+ """A bf16 saving forward (snapshot_quant "bf16", float32 bank; rows = backward.compact_rows(lay)) is admitted: the
102
+ unquantized CUDA DW (movers.dw_admits "none"), the CUDA backward reading the bf16 compact rows with its GDS at every
103
+ grad dtype (backward.admits_at_forward "bf16"), mover_fx_cuda's WR walk writing them (_fxq_rows "bf16") and the
104
+ compact layout's condition (backward.supported "bf16", worst grad dtype): phase1_fields' saving checks and
105
+ forward_steps' layout 2. Memoized per signature, keyed by the device ordinal, dtype, shape, C, rows,
106
+ memory_ops.route_token() (the forced test row, the routing predicates as bound) and
107
+ backward.admits_at_forward as bound. ONLY admitted signatures are stored (at most _BF16_SAVING_MAX, under
108
+ _BF16_SAVING_LOCK): a warm call evaluates no predicate; a declined one evaluates them every time."""
109
+ if not (memory.is_cuda and memory.dim() == 2 and memory.dtype == torch.float32 and rows is not None):
110
+ return False
111
+ key = (memory.device.index, memory.dtype, tuple(memory.shape), int(C), int(rows), mo.route_token(),
112
+ backward.admits_at_forward)
113
+ if key in _BF16_SAVING:
114
+ return True
115
+ ok = bool(movers.dw_admits(memory, C, "none") and backward.admits_at_forward(memory, C, "bf16", rows)
116
+ and _fxq_rows(memory, C, "bf16")
117
+ and backward.supported(memory, "bf16", C=C, grad_f32_possible=True, rows=rows))
118
+ if ok and len(_BF16_SAVING) < _BF16_SAVING_MAX:
119
+ with _BF16_SAVING_LOCK:
120
+ if len(_BF16_SAVING) < _BF16_SAVING_MAX:
121
+ _BF16_SAVING[key] = True
122
+ return ok
123
+
124
+
125
+ def phase1_fields(memory, C, snapshot_quant, save, R, grad_bank_fp32=None, rows=None):
126
+ """The det_fields of this call's Phase 1 (see above). rows: the compact snapshot's row count
127
+ (backward.compact_rows(lay)), which fp8 / int4 need for the backward's admission. A bf16 saving call takes its
128
+ admission from bf16_saving_admitted (memoized per signature); a declined one, and every other call, as below."""
129
+ dq = "none" if snapshot_quant == "bf16" else snapshot_quant # bf16 rows: the unquantized DW (bf16 stores)
130
+ if not (save and snapshot_quant == "bf16" and bf16_saving_admitted(memory, C, rows)):
131
+ if not (memory.is_cuda and memory.dim() == 2) or not movers.dw_admits(memory, C, dq):
132
+ return _FULL_FIELDS
133
+ if not save:
134
+ return _NOGRAD_FIELDS
135
+ if not backward.admits_at_forward(memory, C, snapshot_quant, rows):
136
+ return _FULL_FIELDS
137
+ if snapshot_quant != "none" and not _fxq_rows(memory, C, snapshot_quant):
138
+ return _FULL_FIELDS
139
+ if _qrec8_ok(memory, C, R, grad_bank_fp32):
140
+ STATS["saving_qrec8"] = STATS.get("saving_qrec8", 0) + 1
141
+ return _SAVING_FIELDS_QREC8
142
+ STATS["saving_qrec16"] = STATS.get("saving_qrec16", 0) + 1
143
+ return _SAVING_FIELDS
144
+
145
+
146
+ def _det_dual(x, qx, v, beta, retrieved, inter, delta_v, readings, residual, num_parallel, valid_tokens, trig=False):
147
+ """The dual matmul's step launcher. memory_ops.make_dual_matmul runs first (its checks, and its launcher kept as the
148
+ fallback); then the same Triton kernel for the same arguments, grid and launch options is compiled or fetched once
149
+ (JITFunction.warmup with exactly the launcher's arguments: ti and valid_tokens are do_not_specialize, so one
150
+ compiled kernel serves every step), and each step calls that kernel's own launcher (CompiledKernel.run) with the
151
+ arguments JITFunction.run would pass, on the current stream, skipping JITFunction.run's per-launch binding,
152
+ specialization key and cache lookup. Same kernel, grid, arguments and stream: same launches, same bits.
153
+ Registered launch hooks are honoured as JITFunction.run does (launch metadata and hook objects passed to the
154
+ launcher); pre-run hooks, a stages-inspection hook, a Triton without these entry points or any error while binding
155
+ keep the fallback launcher. The fused C++ step loop (no Python hooks) runs only with Triton's own, empty hook chains
156
+ (hooks_idle). trig: the kernel's early-trigger constexpr (TRIG; same arithmetic)."""
157
+ launch = mo.make_dual_matmul(x, qx, v, beta, retrieved, inter, delta_v, readings, residual,
158
+ num_parallel=num_parallel, valid_tokens=valid_tokens)
159
+ try:
160
+ import triton
161
+ from triton import knobs
162
+ from triton.runtime.driver import driver as _tdriver
163
+ from . import phase2_kernels as _p2
164
+ kern = _p2._sdm_dual_matmul
165
+ # Bind only phase2_kernels' own kernel (a substituted memory_ops.make_dual_matmul keeps its own launcher).
166
+ if mo.make_dual_matmul is not _p2.make_dual_matmul:
167
+ return launch
168
+ trig = bool(trig)
169
+
170
+ def state():
171
+ # Everything JITFunction.run reads besides the arguments that could change what or how it launches:
172
+ # hooks, the debug / instrumentation options and the stages-inspection hook (they enter the cache key),
173
+ # the current device (its stream). Checked again before EVERY bound launch.
174
+ return (knobs.runtime.launch_enter_hook, knobs.runtime.launch_exit_hook,
175
+ tuple(getattr(kern, "pre_run_hooks", None) or ()), bool(kern.debug or knobs.runtime.debug),
176
+ knobs.compilation.instrumentation_mode, knobs.runtime.add_stages_inspection_hook,
177
+ _tdriver.active.get_current_device())
178
+ st0 = state()
179
+ # Pre-run hooks (JITFunction.run calls them before every launch) or a stages-inspection hook (it enters the
180
+ # cache key): the fallback launcher. Launch hooks need no refusal: the bound launch passes JITFunction.run's
181
+ # launch_metadata and hook objects (HookChain in Triton 3.7) to the launcher exactly as run does.
182
+ if st0[2] or st0[5] is not None:
183
+ return launch
184
+ n, c, _ = x.shape
185
+ hd = v.shape[-1]
186
+ geom = _p2.dual_matmul_geom(c, v.dtype) # = make_dual_matmul's
187
+ dv_rows = _p2._step_rows(delta_v, "delta_v", num_parallel * c, n // num_parallel, hd)
188
+ grid0 = triton.cdiv(c, geom.block_m) * triton.cdiv(hd, geom.block_n) * num_parallel
189
+ head = (x, qx, v, beta, retrieved, inter, delta_v, dv_rows, readings, residual)
190
+ meta = dict(C=c, HD=hd, BLOCK_M=geom.block_m, BLOCK_N=geom.block_n, BLOCK_K=geom.block_k, DOT=geom.dot,
191
+ TRIG=1 if trig else 0)
192
+ ck = kern.warmup(*head, 0, num_parallel, valid_tokens, grid=(grid0,), **meta, num_warps=4)
193
+ if ck is None:
194
+ return launch
195
+ run = ck.run # loads the binary (CompiledKernel._init_handles)
196
+ fn, pm = ck.function, ck.packed_metadata
197
+ tail = (num_parallel, valid_tokens, c, hd, geom.block_m, geom.block_n, geom.block_k, geom.dot, meta["TRIG"])
198
+ dev = _tdriver.active.get_current_device()
199
+ get_stream = _tdriver.active.get_current_stream
200
+ gvals = kern.used_global_vals
201
+ missing = object()
202
+ except Exception: # noqa: BLE001 - any binding problem keeps the fallback launcher
203
+ return launch
204
+
205
+ def valid() -> bool:
206
+ """The bound launch equals JITFunction.run's: same runtime state as at binding and the kernel's used globals
207
+ unchanged (JITFunction.run raises on a changed global: the fallback launcher is then called and raises)."""
208
+ if state() != st0:
209
+ return False
210
+ for (name, _), (val, gdict) in gvals.items():
211
+ if gdict.get(name, missing) != val:
212
+ return False
213
+ return True
214
+
215
+ grid = (grid0,) # the grid JITFunction.run receives from make_dual_matmul's kernel[grid]
216
+
217
+ def bound(ti: int) -> None:
218
+ if not valid():
219
+ STATS["fx_dual_fallback"] = STATS.get("fx_dual_fallback", 0) + 1
220
+ launch(ti) # the fallback launcher (JITFunction.run with its own checks and hooks)
221
+ return
222
+ # = JITFunction.run's launch statements: launch_metadata = kernel.launch_metadata(grid, stream, *bound_args);
223
+ # kernel.run(grid_0, grid_1, grid_2, stream, kernel.function, kernel.packed_metadata, launch_metadata,
224
+ # knobs.runtime.launch_enter_hook, knobs.runtime.launch_exit_hook, *bound_args)
225
+ stream = get_stream(dev)
226
+ args = (*head, ti, *tail)
227
+ run(grid0, 1, 1, stream, fn, pm, ck.launch_metadata(grid, stream, *args),
228
+ knobs.runtime.launch_enter_hook, knobs.runtime.launch_exit_hook, *args)
229
+
230
+ hook_chain = getattr(knobs, "HookChain", None)
231
+
232
+ def hooks_idle() -> bool:
233
+ """No launch hook registered: each hook is None or EXACTLY Triton's own HookChain with no callbacks (a custom hook
234
+ object may act in __call__ whatever its attributes). Only then does the C++ step loop, which calls no
235
+ Python hook, launch exactly what JITFunction.run would. Checked at every fused call (hooks may be added later)."""
236
+ for h in (knobs.runtime.launch_enter_hook, knobs.runtime.launch_exit_hook):
237
+ if h is None:
238
+ continue
239
+ if hook_chain is None or type(h) is not hook_chain or h.calls:
240
+ return False
241
+ return True
242
+ bound.shipped = launch
243
+ bound.valid = valid
244
+ bound.hooks_idle = hooks_idle
245
+ # The same launch as a descriptor for the fused C++ step loop (mover_fx_cuda.fx_fwd_steps), when the kernel's launch
246
+ # is one fx_fwd_steps replicates and the kernel was loaded on the bank's device; else None (Python loop).
247
+ bound.tri_desc = None
248
+ # Only without registered launch hooks.
249
+ if dev == x.device.index and hooks_idle():
250
+ try:
251
+ from .cuda import mover_fx_cuda as _fxm
252
+ bound.tri_desc = _fxm.tri_desc(ck, list(kern.arg_names), (*head, 0, *tail), grid0)
253
+ except Exception: # noqa: BLE001 - no descriptor: the Python step loop
254
+ bound.tri_desc = None
255
+ STATS["fx_dual_bound"] = STATS.get("fx_dual_bound", 0) + 1 # engagement counters (per forward call)
256
+ if trig:
257
+ STATS["fx_dual_trig"] = STATS.get("fx_dual_trig", 0) + 1 # bound with the early trigger
258
+ if bound.tri_desc is not None:
259
+ STATS["fx_dual_desc"] = STATS.get("fx_dual_desc", 0) + 1
260
+ return bound
261
+
262
+
263
+ def forward_steps(memory, k_idx, k_val, v, beta, g, q_idx, q_val, lay, snapshot_quant, save, steps,
264
+ out=None, geom_tokens=0, grad_bank_fp32=None, info=None):
265
+ """The forward over the steps ``range(*steps)``: stages their chunks, runs Phase 1 and the solve on them, then the
266
+ steps, and writes their readings into ``out`` (or a new [P * T, hd] tensor). Returns the readings and the saved
267
+ tensors (or None). The token gather's geometry is the pinned shape-only one (memory_ops.pinned_gather_geometry),
268
+ the same as the backward's, so forward and backward agree and the bits do not depend on the SM count; it comes from
269
+ the call's full token count (geom_tokens), so the groups and slabs of a no-grad call reduce in the same order as a
270
+ saving call. grad_bank_fp32: the op's setting (saving calls; None: unknown). Only the engagement counters
271
+ (STATS) and the dispatch record are module state; the computation keeps none (thread-safe, reentrant). info (a
272
+ dict, saving calls): receives quant_layout, the compact-snapshot layout written (2: compact rows, row chunk * C * W +
273
+ group ordinal, in the payload / scale slots for fp8 / int4; 0: per-entry rows)."""
274
+ fields = phase1_fields(memory, lay.C, snapshot_quant, bool(save), lay.R, grad_bank_fp32,
275
+ rows=backward.compact_rows(lay))
276
+ P, T, C, W, R, hd = lay.P, lay.T, lay.C, lay.W, lay.R, lay.hd
277
+ Nt, SC = steps[1] - steps[0], P * C
278
+ N = Nt * P
279
+ NC = N * C
280
+ tokens = T - steps[0] * C
281
+ dtype, dev, f32 = memory.dtype, memory.device, torch.float32
282
+
283
+ def stage(t, dt):
284
+ return mo.stage_time_major(t, P, T, C, dt, steps)
285
+
286
+ idx_dtype = torch.int32 if k_idx.dtype == q_idx.dtype == torch.int32 else torch.int64
287
+ k_idx = stage(k_idx, idx_dtype).view(N, C, W)
288
+ q_idx = stage(q_idx, idx_dtype).view(N, C, R)
289
+ k_val = stage(k_val, dtype).view(N, C, W)
290
+ q_val = stage(q_val, dtype).view(N, C, R)
291
+ g = stage(g, f32).view(N, C, lay.G)
292
+ beta = stage(beta, f32).view(NC)
293
+ v = stage(v, dtype)
294
+
295
+ p1 = mo.phase1_forward(k_idx, q_idx, k_val, q_val, g, mo._repeat_counter(dev),
296
+ num_parallel=P, stream_offset=lay.stream_offset,
297
+ num_keys=lay.num_keys, valid_last=min(C, tokens - (Nt - 1) * C),
298
+ need_read_groups=save, beta=beta.view(N, C), det_fields=fields)
299
+ # Phase 1's write records go to this call's DW only: the saved Phase-1 tuple keeps an empty krec (the backward
300
+ # never reads it; 16 B per write entry).
301
+ krec = mo._nonempty(p1.krec)
302
+ if krec is not None:
303
+ p1 = p1._replace(krec=p1.krec.new_empty(0)) # fresh tensor: no view keeping the storage alive
304
+ x, qx = mo.wy_solve(p1.a, beta, p1.qk)
305
+
306
+ quant = snapshot_quant if save else "none"
307
+ crow = backward.compact_rows(lay) # the backward's admission sees the same row count (any count: 64-bit rows)
308
+ # snapshot_quant bf16 (float32 banks): the unquantized forward ('none' DW: the bank-direct WR walk) with a bf16
309
+ # every-step compact snapshot, which that walk writes as bf16 rows (RN) in eager and captured calls; the backward reads
310
+ # them with gx_pa2's bf16-snapshot rows. No other route: memory_ops.refuse_non_native refused a call without it up
311
+ # front (memoized: no per-call predicate here); the DW and the backward raise if their kernels decline these rows.
312
+ bf16_rows = quant == "bf16"
313
+ dw_quant = "none" if bf16_rows else quant # the DW's (and the saved tensors') snapshot mode
314
+ full_snapshot = save and dw_quant == "none"
315
+ snapshot = torch.empty(NC if full_snapshot else SC, W, hd, device=dev,
316
+ dtype=torch.bfloat16 if bf16_rows else dtype)
317
+ payload = scale = None
318
+ if dw_quant != "none":
319
+ width, qdtype = mo.quant_row(quant, hd)
320
+ payload = torch.empty(NC * W, width, device=dev, dtype=qdtype)
321
+ scale = torch.empty(NC * W, device=dev, dtype=f32)
322
+ retrieved = torch.empty(SC, hd, device=dev, dtype=f32)
323
+ inter = torch.empty_like(retrieved)
324
+ delta_v = torch.empty(NC if save else SC, hd, device=dev, dtype=f32)
325
+ residual = torch.empty(NC, hd, device=dev, dtype=f32) if save else None
326
+ readings = torch.empty(NC, hd, device=dev, dtype=dtype)
327
+
328
+ geo = mo.pinned_gather_geometry(hd, dtype, max(W, R), geom_tokens or SC)
329
+ # Compact snapshot, writer-authoritative handshake: the forward declares the layout it will write on the snapshot
330
+ # tensor (attribute _dc_snapc_layout: 2 = one row per valid write group, in group order; 4 = no compact rows, the DW
331
+ # reads base rows from the bank) before the DW launcher is built; a compact-reading DW (the CUDA DW) reads compact
332
+ # rows only for that declared layout and announces it (launcher attribute reads_snapc). The forward then writes
333
+ # compact rows iff the DW announces exactly the declared layout; announced 0 -> the reference gather writes the full
334
+ # per-entry stash. Any other combination raises. The written layout is returned in info["quant_layout"].
335
+ # (bf16 rows: the memoized admission, which includes this backward.supported; a declined bf16 call evaluates it)
336
+ want = 2 if full_snapshot and ((bf16_rows and bf16_saving_admitted(memory, C, crow))
337
+ or backward.supported(memory, quant, C=C, grad_f32_possible=True, rows=crow)) else 0
338
+ if (save and dw_quant in ("fp8", "int4") and backward.supported(memory, quant, C=C, grad_f32_possible=True, rows=crow)
339
+ and _fxq_rows(memory, C, quant)):
340
+ want = 2 # fp8 / int4 compact rows, quantized by the DW
341
+ if want:
342
+ snapshot._dc_snapc_layout = want
343
+ # Ask the DW to write the compact rows itself; it announces it (writes_snapc) and the gather then writes none
344
+ # (the same raw rows at the same places, see below). Same bits either way. Only in eager calls, not while a CUDA
345
+ # graph is being captured: in graph replays it does not pay; in eager calls it lets the saving forward take the
346
+ # gather plan and the fused step loop.
347
+ # the capture state of the bank's device's current stream (the launches go there)
348
+ dev_idx = memory.device.index
349
+ if dev_idx is None or dev_idx == torch.cuda.current_device():
350
+ capturing = torch.cuda.is_current_stream_capturing()
351
+ else:
352
+ with torch.cuda.device(dev_idx):
353
+ capturing = torch.cuda.is_current_stream_capturing()
354
+ if not capturing or quant != "none": # quantized rows: written by the DW in captured calls too
355
+ snapshot._dc_snapc_by_dw = True
356
+ # The no-grad forward declares layout 4 (no compact rows): a DW that supports it reads each group's base row from
357
+ # the bank itself, and the gather writes no stash.
358
+ want_step = 0
359
+ if not save:
360
+ want_step = 4
361
+ snapshot._dc_snapc_layout = want_step
362
+ dw = mo.make_mover_dw(memory, snapshot, delta_v, k_idx, p1.cpe, k_val, p1.rex, payload, scale, chunk_size=C,
363
+ num_parallel=P, valid_tokens=tokens, snapshot_quant=dw_quant, sorted_k=p1.spay_k, krec=krec,
364
+ tiles_k=mo._nonempty(p1.tiles_k), segb_k=mo._nonempty(p1.segb_k))
365
+ k_rows, k_eff = k_idx.view(NC, W), p1.k_eff.view(NC, W)
366
+ q_rows, q_w = q_idx.view(NC, R), p1.q_w.view(NC, R)
367
+ if (getattr(getattr(dw, "fx", None), "geom", None) or {}).get("w2"):
368
+ STATS["fx_dw2"] = STATS.get("fx_dw2", 0) + 1 # the DW runs the two-slice walk (engagement counter)
369
+ # The dual's step launcher is built after the DW's (no step is launched by either factory), so the early-trigger
370
+ # rule can read the DW's grid.
371
+ dual = _det_dual(x, qx, v, beta, retrieved, inter, delta_v, readings, residual, P, tokens,
372
+ trig=_dual_trig_wanted(getattr(getattr(dw, "fx", None), "geom", None), P, hd, C,
373
+ gather_ns_cuda.pdl_ok(memory.device), memory.device.index))
374
+ # Programmatic dependent launch: every step's DW launches right after that step's dual matmul (launched after
375
+ # the step's gather), and every step's gather for ti >= 1 right after the previous step's DW, so what either reads
376
+ # before its wait (records, tiles, compact rows; slots, weights, kpos: written before this loop or by the step's
377
+ # gather) was written at least two launches earlier.
378
+ pdl = gather_ns_cuda.pdl_ok(memory.device)
379
+ if pdl and hasattr(dw, "set_pdl"):
380
+ dw.set_pdl(True)
381
+ mo._record(save=save, chunk=C, gather=geo._asdict(),
382
+ dual_mm=mo.dual_matmul_geom(C, dtype)._asdict(), mover=mo.mover_geom(dtype, hd)._asdict())
383
+ announced = int(getattr(dw, "reads_snapc", 0) or 0)
384
+ if announced not in (0, want if save else want_step):
385
+ raise RuntimeError(f"SDM forward: the write mover reads compact snapshot layout {announced} but the forward declared "
386
+ f"{want if save else want_step} (full snapshot={full_snapshot})")
387
+ layout = announced
388
+ # A DW that writes the compact rows itself (writes_snapc == 2, every step) replaces the gather's compact-row stores;
389
+ # the rows, their places and the saved layout are unchanged.
390
+ dw_writes = int(getattr(dw, "writes_snapc", 0) or 0)
391
+ if bf16_rows and (dw_writes != 2 or layout != 2): # bf16 rows: written by the DW only (the gather writes bank rows)
392
+ raise RuntimeError(f"SDM forward: bf16 compact snapshot rows need the write mover to write them (layout "
393
+ f"{layout}, writes {dw_writes})")
394
+ if dw_writes and (dw_writes != 2 or layout != 2 or not save):
395
+ raise RuntimeError(f"SDM forward: the write mover writes compact snapshot layout {dw_writes} but the forward uses layout {layout} "
396
+ f"(save={save})")
397
+ snapc_rows = layout == 2 # compact rows exist (layout 4: none, no kpos)
398
+ gather_snapc = snapc_rows and not dw_writes # ... and the gather writes them
399
+ kpos = None
400
+ if gather_snapc:
401
+ kpos = p1.dc_kpos.view(NC, W)
402
+ snap2 = snapshot.view(-1, hd) # one view per call, not one per step
403
+ # L2 hints in the gather (gather_ns_cuda.l2_hint: shapes only), decided once per call, only when the call's gather
404
+ # runs gather_ns_cuda's module: layout 4 or DW-written compact rows (the no-stash gather), or compact rows written
405
+ # by the gather (layout 0 runs the reference token_gather). Step tokens = geom_tokens or SC: the caller's tokens per
406
+ # step (geom_tokens = the caller's P * C in the no-grad group and slab loop, whose sub-layouts cover fewer streams;
407
+ # 0 elsewhere), so every sub-call of one op call takes the same decision.
408
+ gl2 = bool((layout == 4 or dw_writes or gather_snapc)
409
+ and gather_ns_cuda.l2_hint(R, geom_tokens or SC, memory.shape[0], hd * memory.element_size(),
410
+ device=memory.device))
411
+ if gl2:
412
+ STATS["fx_gather_l2"] = STATS.get("fx_gather_l2", 0) + 1 # engagement (per call using the hinted gather)
413
+ gplan = gstep = None # the gather's plan (the no-stash gather only)
414
+ if layout == 4 or dw_writes:
415
+ # The gather's checks and arguments once per call (a plan); each step passes row0 and valid only
416
+ # (gather_ns_cuda.token_gather_dc's kernel and arguments: the same launches).
417
+ snap0 = torch.empty(0, device=memory.device, dtype=memory.dtype)
418
+ g4 = (geo.vec_bytes, geo.lanes, geo.groups, geo.tpc)
419
+ if Nt > 0:
420
+ gmod = gather_ns_cuda._module(gl2)
421
+ gplan = gmod.token_gather_dc_plan(memory, k_rows, k_eff, snap0, retrieved, q_rows, q_w, inter,
422
+ (Nt - 1) * SC, 0, SC, C, *g4)
423
+ gstep = gmod.token_gather_dc_step
424
+ # The whole step loop as one C++ call (the gather plan's step, the Triton dual through its compiled function, the DW
425
+ # plan's step: the same kernels, arguments, order and stream as the loop below) when all three exist: the no-stash
426
+ # gather plan, the dual's launch descriptor and the DW's plan launcher.
427
+ fx_dw = getattr(dw, "fx", None)
428
+ tdesc = getattr(dual, "tri_desc", None)
429
+ if (Nt > 0 and gplan is not None and tdesc is not None and fx_dw is not None
430
+ and getattr(fx_dw, "fx_plan", None) is not None and (layout == 4 or dw_writes) and dual.valid()
431
+ and dual.hooks_idle()):
432
+ from .cuda import mover_fx_cuda as _fxm
433
+ STATS["fx_fused"] = STATS.get("fx_fused", 0) + 1 # fused step-loop calls (engagement counter)
434
+ _fxm._module().fx_fwd_steps(gplan, gather_ns_cuda._module(gl2).token_gather_dc_launch_addr(), tdesc,
435
+ fx_dw.fx_plan, 0, Nt, SC, C, tokens, 1 if pdl else 0, fx_dw.fx_pdl[0])
436
+ Nt_loop = 0
437
+ else:
438
+ Nt_loop = Nt
439
+ for ti in range(Nt_loop):
440
+ if layout == 4 or dw_writes:
441
+ # = gather_ns_cuda.token_gather_ns(memory, k_rows, k_eff, retrieved, geo, q_idx=q_rows, q_weights=q_w,
442
+ # q_out=inter, tokens=SC, chunk=C, valid=min(C, tokens - ti * C), row0=ti * SC) (snapc None, kpos None)
443
+ gstep(gplan, ti * SC, min(C, tokens - ti * C), 1 if (pdl and ti > 0) else 0)
444
+ elif gather_snapc:
445
+ gather_ns_cuda.token_gather_ns(memory, k_rows, k_eff, retrieved, geo, q_idx=q_rows,
446
+ q_weights=q_w, q_out=inter, tokens=SC, chunk=C,
447
+ valid=min(C, tokens - ti * C), row0=ti * SC,
448
+ snapc=snap2, kpos=kpos, crow=C * W, pdl=pdl and ti > 0, l2=gl2)
449
+ else:
450
+ mo.token_gather(memory, k_rows, k_eff, snapshot, retrieved, geo, q_idx=q_rows,
451
+ q_weights=q_w, q_out=inter, tokens=SC, chunk=C,
452
+ valid=min(C, tokens - ti * C), row0=ti * SC,
453
+ stash_row0=ti * SC if full_snapshot else 0)
454
+ dual(ti)
455
+ dw(ti)
456
+ readings = mo.unstage_time_major(readings, P, T, C, dtype, steps, out)
457
+ if not save:
458
+ return readings, None
459
+ if info is not None:
460
+ info["quant_layout"] = layout
461
+ kept = (payload, scale) if dw_quant != "none" else (snapshot, None)
462
+ # dc_kpos is forward-only (the gather's map)
463
+ return readings, (memory, k_idx, q_idx, k_val, q_val, g, beta, x, qx, delta_v, residual,
464
+ *kept, *p1._replace(k_eff=None, q_w=None, dc_kpos=None))