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,221 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ """Opt-in statistics for SparseDeltaMemory (memory-access / activation / gradient stats).
3
+
4
+ Everything here is logging only, gated by ``SparseDeltaMemoryArgs.log_memory_access_stats``
5
+ and ``log_memory_norms``, and factored out of ``layer.py`` so that file holds the model
6
+ math. ``SparseDeltaMemory`` mixes in ``SDMStatsMixin`` and calls the ``_record_*`` hooks only
7
+ in eval mode outside CUDA-graph capture: they read tensor values on the host (``.item()``).
8
+ The buffers are registered on the layer (``self``) so they follow device/FSDP moves.
9
+ """
10
+ import torch
11
+
12
+
13
+ class SDMStatsMixin:
14
+ # ------------------------------------------------------------------ setup
15
+ def _init_stats(self, args, num_heads: int) -> None:
16
+ """Allocate the stats buffers/accumulators (called from the layer __init__)."""
17
+ if args.log_memory_access_stats:
18
+ stats_slots = num_heads * self.slots_per_head
19
+ # Unique-index access counts for read and write (across all heads).
20
+ for name in ("_read_idx_counts", "_write_idx_counts"):
21
+ self.register_buffer(name, torch.zeros(stats_slots, dtype=torch.int64), persistent=False)
22
+ # Activation statistics accumulators (for stability debugging).
23
+ self._activation_stats: dict[str, list[float]] = {
24
+ "read_weight_max": [],
25
+ "write_strength_max": [],
26
+ "write_strength_mean": [],
27
+ "output_norm": [],
28
+ "forget_gate_min": [],
29
+ "forget_gate_mean": [],
30
+ "forget_gate_max": [],
31
+ "input_gate_min": [],
32
+ "input_gate_mean": [],
33
+ "input_gate_max": [],
34
+ }
35
+ if args.log_memory_norms:
36
+ self._activation_stats.update({
37
+ "memory_norm": [],
38
+ "memory_max": [],
39
+ "updated_memory_norm": [],
40
+ "updated_memory_max": [],
41
+ "slot_norm_mean": [],
42
+ "slot_norm_max": [],
43
+ "updated_slot_norm_mean": [],
44
+ "updated_slot_norm_max": [],
45
+ })
46
+ # Full slot access histograms (accumulated, converted to entropy at log time).
47
+ for name in ("_read_slot_access_weights", "_write_slot_access_weights"):
48
+ self.register_buffer(name, torch.zeros(stats_slots, dtype=torch.float32), persistent=False)
49
+ # Gradient statistics, collected via collect_grad_stats() after backward.
50
+ self._grad_stats: dict[str, list[float]] = {
51
+ "memory_grad_norm": [],
52
+ "qk_proj_grad_norm": [],
53
+ "output_proj_grad_norm": [],
54
+ }
55
+ else:
56
+ self._read_idx_counts = None
57
+ self._write_idx_counts = None
58
+ self._read_slot_access_weights = None
59
+ self._write_slot_access_weights = None
60
+ self._activation_stats = None
61
+ self._grad_stats = None
62
+
63
+ # ------------------------------------------------------------ forward hooks
64
+ def _record_gate_stats(self, g: torch.Tensor, beta: torch.Tensor) -> None:
65
+ if self._activation_stats is None:
66
+ return
67
+ with torch.no_grad():
68
+ decay = torch.exp(g)
69
+ s = self._activation_stats
70
+ s["forget_gate_min"].append(decay.min().item())
71
+ s["forget_gate_mean"].append(decay.mean().item())
72
+ s["forget_gate_max"].append(decay.max().item())
73
+ s["input_gate_min"].append(beta.min().item())
74
+ s["input_gate_mean"].append(beta.mean().item())
75
+ s["input_gate_max"].append(beta.max().item())
76
+
77
+ def _record_read_stats(self, q_idx, q_val, BH, H, sph) -> None:
78
+ if self._read_idx_counts is None:
79
+ return
80
+ with torch.no_grad():
81
+ # Indices are [BH, T, num_reads] in [0, sph). Add head offset for [0, H*sph).
82
+ head_offset = (torch.arange(BH, device=q_idx.device) % H * sph).view(BH, 1, 1)
83
+ q_idx_flat = (q_idx + head_offset).view(-1)
84
+ self._read_idx_counts.index_add_(
85
+ 0, q_idx_flat.long(), torch.ones_like(q_idx_flat, dtype=torch.int64)
86
+ )
87
+ if self._activation_stats is not None:
88
+ self._activation_stats["read_weight_max"].append(q_val.max().item())
89
+ if self._read_slot_access_weights is not None:
90
+ # index_put_ with accumulate=True sums duplicates in sorted order on CUDA
91
+ # (deterministic); a float index_add_ uses atomics.
92
+ q_val_flat = q_val.reshape(-1)
93
+ self._read_slot_access_weights.index_put_(
94
+ (q_idx_flat.long(),), q_val_flat.to(self._read_slot_access_weights.dtype),
95
+ accumulate=True)
96
+
97
+ def _record_memory_norms_pre(self, memory: torch.Tensor) -> None:
98
+ if not (self.args.log_memory_norms and self._activation_stats is not None):
99
+ return
100
+ with torch.no_grad():
101
+ s = self._activation_stats
102
+ s["memory_norm"].append(memory.norm().item())
103
+ s["memory_max"].append(memory.abs().max().item())
104
+ slot_norms = memory.norm(dim=-1)
105
+ s["slot_norm_mean"].append(slot_norms.mean().item())
106
+ s["slot_norm_max"].append(slot_norms.max().item())
107
+
108
+ def _record_write_stats(self, k_idx, k_val, memory, output, BH, H, sph) -> None:
109
+ if self._write_idx_counts is None:
110
+ return
111
+ with torch.no_grad():
112
+ # k_idx has batch offsets; mod by sph then add head offset for [0, H*sph).
113
+ head_offset_w = (torch.arange(BH, device=k_idx.device) % H * sph).view(BH, 1, 1)
114
+ write_idx_flat = (k_idx % sph + head_offset_w).view(-1)
115
+ self._write_idx_counts.index_add_(
116
+ 0, write_idx_flat.long(), torch.ones_like(write_idx_flat, dtype=torch.int64)
117
+ )
118
+ if self._activation_stats is not None:
119
+ s = self._activation_stats
120
+ s["write_strength_max"].append(k_val.max().item())
121
+ s["write_strength_mean"].append(k_val.mean().item())
122
+ if self.args.log_memory_norms and memory is not None:
123
+ s["updated_memory_norm"].append(memory.norm().item())
124
+ s["updated_memory_max"].append(memory.abs().max().item())
125
+ updated_slot_norms = memory.norm(dim=-1)
126
+ s["updated_slot_norm_mean"].append(updated_slot_norms.mean().item())
127
+ s["updated_slot_norm_max"].append(updated_slot_norms.max().item())
128
+ s["output_norm"].append(output.norm(dim=-1).mean().item())
129
+ if self._write_slot_access_weights is not None:
130
+ write_strength_flat = k_val.reshape(-1)
131
+ self._write_slot_access_weights.index_put_(
132
+ (write_idx_flat.long(),),
133
+ write_strength_flat.to(self._write_slot_access_weights.dtype),
134
+ accumulate=True)
135
+
136
+ # ------------------------------------------------------------ public API
137
+ def collect_grad_stats(self) -> None:
138
+ """Collect gradient statistics after backward pass.
139
+
140
+ Call this after optimizer.step() to record gradient norms.
141
+ Hooks don't work reliably with torch.compile, so we check .grad directly.
142
+ """
143
+ if self._grad_stats is None:
144
+ return
145
+
146
+ # Memory parameter gradient
147
+ if self.memory is not None and self.memory.grad is not None:
148
+ self._grad_stats["memory_grad_norm"].append(self.memory.grad.norm().item())
149
+
150
+ # Q/K projection gradients
151
+ qk_grad_norms = []
152
+ for proj in [self.Wq_read, self.Wk_write]:
153
+ if proj is not None:
154
+ for param in proj.parameters():
155
+ if param.grad is not None:
156
+ qk_grad_norms.append(param.grad.norm().item())
157
+ break
158
+ if qk_grad_norms:
159
+ self._grad_stats["qk_proj_grad_norm"].append(sum(qk_grad_norms) / len(qk_grad_norms))
160
+
161
+ # Output projection gradient
162
+ if self.Wo is not None:
163
+ for param in self.Wo.parameters():
164
+ if param.grad is not None:
165
+ self._grad_stats["output_proj_grad_norm"].append(param.grad.norm().item())
166
+ break
167
+
168
+ def get_memory_access_stats(self, reset: bool = True) -> "dict[str, float] | None":
169
+ """Get memory access statistics and optionally reset counters.
170
+
171
+ Returns a dict of unique-access percentages, read/write slot-access entropy, and the
172
+ averaged activation + gradient statistics since the last reset. Returns None if
173
+ ``log_memory_access_stats`` is disabled.
174
+ """
175
+ if not self.args.log_memory_access_stats:
176
+ return None
177
+
178
+ if self._read_idx_counts is None or self._write_idx_counts is None:
179
+ return None
180
+
181
+ num_slots = self._read_idx_counts.numel()
182
+
183
+ # Compute stats - only count unique slots
184
+ read_unique = (self._read_idx_counts > 0).sum().item()
185
+ write_unique = (self._write_idx_counts > 0).sum().item()
186
+
187
+ stats = {
188
+ "read_unique_pct": 100.0 * read_unique / num_slots if num_slots > 0 else 0.0,
189
+ "write_unique_pct": 100.0 * write_unique / num_slots if num_slots > 0 else 0.0,
190
+ }
191
+
192
+ # Compute entropy from full slot access histograms
193
+ eps = 1e-10
194
+ for kind in ("read", "write"):
195
+ weights = getattr(self, f"_{kind}_slot_access_weights")
196
+ if weights is not None:
197
+ total_weight = weights.sum()
198
+ if total_weight > eps:
199
+ probs = weights / total_weight
200
+ max_entropy = torch.log(torch.tensor(num_slots, dtype=torch.float32))
201
+ entropy = -(probs * probs.clamp(min=eps).log()).sum()
202
+ stats[f"{kind}_slot_entropy_normalized"] = (entropy / max_entropy).item()
203
+ else:
204
+ stats[f"{kind}_slot_entropy_normalized"] = 0.0
205
+
206
+ # Add activation statistics (averaged over forward passes)
207
+ # and gradient statistics (averaged over backward passes)
208
+ for acc in (self._activation_stats, self._grad_stats):
209
+ for key, values in (acc or {}).items():
210
+ stats[key] = sum(values) / len(values) if values else 0.0
211
+
212
+ if reset:
213
+ for buf in (self._read_idx_counts, self._write_idx_counts, self._read_slot_access_weights,
214
+ self._write_slot_access_weights):
215
+ if buf is not None:
216
+ buf.zero_()
217
+ for acc in (self._activation_stats, self._grad_stats):
218
+ for key in acc or ():
219
+ acc[key] = []
220
+
221
+ return stats
@@ -0,0 +1,198 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ """The decay-and-write (DW) and read-side backward (GDS) step launchers of the op.
3
+
4
+ ``make_mover_dw`` and ``make_mover_gds`` run the CUDA movers (cuda/mover_fx_cuda.py and cuda/mover_da2_cuda.py for the
5
+ DW; cuda/gx_gds_cuda.py and cuda/mover_da2_cuda.py for the GDS) wherever ``dw_admits`` / ``gds_admits`` hold. There is
6
+ no other route (2.0.0): memory_ops.refuse_non_native refuses every call they do not take before any work, so a launcher
7
+ asked for one raises (NO_NATIVE_MOVER_MSG, an internal error).
8
+ Phase 1's records (krec, qrec / qrec8, tile bounds) are passed by the caller; without them the CUDA movers build their
9
+ own (the same values). Every mover gives each bank (or bank-gradient) row one writer per launch and sums a slot
10
+ group's entries in a fixed order, so its results are bitwise reproducible.
11
+ """
12
+
13
+ import torch
14
+
15
+ from .cuda import mover_da2_cuda as da2
16
+ from .cuda._device import backend
17
+
18
+ STATS = {"da2_dw": 0, "shipped_dw": 0, "da2_gds": 0, "shipped_gds": 0, "p1_krec": 0, "p1_qrec": 0}
19
+
20
+
21
+ # Tail classes (shapes outside the 16-byte-slice predicate): native only when EVERY kernel's hook admits them.
22
+ _TAIL_HOOKS = (("mover_fx_cuda", "tail_supported"), ("gather_cuda", "tail_supported"),
23
+ ("gather_ns_cuda", "tail_supported"), ("gx_pa2_cuda", "tail_supported"),
24
+ ("gx_gds_cuda", "tail_supported"), ("pa_dc_cuda", "gwr_tail_supported"))
25
+
26
+
27
+ NO_NATIVE_MOVER_MSG = ("sparse_delta_memory: internal error: no native {kind} mover for bank {dtype} {shape}, chunk_size {C}, "
28
+ "snapshot_quant {quant} (2.0.0 removed the Triton movers; memory_ops.refuse_non_native should have "
29
+ "refused this call); please report it")
30
+
31
+
32
+ def tail_admits(bank, chunk_size, quant="none") -> bool:
33
+ """The ONE tail predicate: every kernel of the call admits this tail class (the AND of _TAIL_HOOKS); forward and
34
+ backward admissions read it."""
35
+ import importlib
36
+ return all(getattr(importlib.import_module(f".cuda.{mod}", __package__), fn)(bank, chunk_size, quant)
37
+ for mod, fn in _TAIL_HOOKS)
38
+
39
+
40
+ def dw_admits(bank, chunk_size, quant="none") -> bool:
41
+ """The CUDA DW takes calls on ``bank`` with chunks of ``chunk_size`` tokens: mover_da2_cuda's admission on the
42
+ modern backend, mover_fx_cuda's on a100 / rtx; on every backend also tail_admits."""
43
+ if bank.is_cuda and backend(bank.device) != "modern":
44
+ from .cuda import mover_fx_cuda
45
+ # fp8 / int4 rows above the old 16384 head bound: not admitted (QT class T0)
46
+ fx_ok = mover_fx_cuda.admits(bank, chunk_size) and (
47
+ quant == "none" or bank.shape[-1] <= mover_fx_cuda._QUANT_MAX_HEAD_DIM)
48
+ return fx_ok or tail_admits(bank, chunk_size, quant)
49
+ return (da2.supported(bank, chunk_size) and da2.fits(bank, chunk_size, "dw")) or tail_admits(bank, chunk_size, quant)
50
+
51
+
52
+ def gds_admits(bank, grad_dtype, chunk_size, quant="none") -> bool:
53
+ """The CUDA GDS takes calls on ``bank`` with a ``grad_dtype`` gradient bank (the bank's, or float32 for a bf16 bank)
54
+ and chunks of ``chunk_size`` tokens: gx_gds_cuda's GDS (modern: mover_da2_cuda's for the shapes it declines); tail_admits."""
55
+ grad_ok = grad_dtype == bank.dtype or (bank.dtype == torch.bfloat16 and grad_dtype == torch.float32)
56
+ if not grad_ok or not bank.is_cuda:
57
+ return False
58
+ if backend(bank.device) != "modern":
59
+ from .cuda import gx_gds_cuda
60
+ return gx_gds_cuda.admits(bank, grad_dtype, chunk_size, 1, quant) or tail_admits(bank, chunk_size, quant)
61
+ return ((da2.supported(bank, chunk_size) and da2.fits(bank, chunk_size, "gds", grad_dtype))
62
+ or tail_admits(bank, chunk_size, quant))
63
+
64
+
65
+ def read_records(p1):
66
+ """Phase 1's read records for the GDS: the 16-byte qrec, or, when the forward asked Phase 1 for the compact form
67
+ only (DET_QREC8: int32 [N, C * R, 2] = qrec's words 1-2, {absolute slot, scale bits}), qrec8, which
68
+ gx_gds_cuda's GDS reads with spay_q (same bits); None when Phase 1 emitted neither. make_mover_gds builds 16-byte
69
+ records itself whenever gx_gds_cuda's GDS declines a call that got qrec8 (only speed lost)."""
70
+ q = p1.qrec
71
+ if q is not None and q.numel() > 0:
72
+ return q
73
+ q8 = p1.qrec8
74
+ return q8 if q8 is not None and q8.numel() > 0 else None
75
+
76
+
77
+ def make_mover_dw(bank, snapshot, delta_v, slot, cpe, k_val, rex, payload=None, scale=None, *, chunk_size,
78
+ num_parallel, valid_tokens, snapshot_quant="none", sorted_k=None, krec=None, tiles_k=None,
79
+ segb_k=None):
80
+ """The DW step launcher (the CUDA DW; a call dw_admits declines raises NO_NATIVE_MOVER_MSG: the op refuses it up
81
+ front). krec, tiles_k, segb_k: Phase 1's records and tile bounds for these entries (``phase1_forward(..., beta=...)``),
82
+ or None."""
83
+ # krec: Phase 1's write records, [N, C*W, 4] int32; when given they replace the mover's own record build.
84
+ # Quantized snapshots (fp8 / int4) stay on the CUDA DW, which also quantizes them; the bank update is unchanged.
85
+ # Tails run only on mover_fx_cuda's bank-direct DW (layouts 2 / 4); on layout 0 they raise below.
86
+ if snapshot_quant == "bf16": # bf16 rows: the forward passes "none" with its bf16 compact buffer (no other route)
87
+ raise RuntimeError("make_mover_dw: snapshot_quant bf16 runs as the unquantized DW with bf16 compact rows")
88
+ tail_off = (sorted_k is not None and not da2._shape_supported(bank, chunk_size)
89
+ and getattr(snapshot, "_dc_snapc_layout", 0) not in (2, 4))
90
+ if sorted_k is None or tail_off or not dw_admits(bank, chunk_size, snapshot_quant):
91
+ # 2.0.0 (A4): phase2_kernels' Triton DW is gone; memory_ops.refuse_non_native refuses such calls up front.
92
+ raise RuntimeError(NO_NATIVE_MOVER_MSG.format(kind="DW", shape=tuple(bank.shape), dtype=bank.dtype, C=chunk_size,
93
+ quant=snapshot_quant))
94
+ STATS["da2_dw"] += 1
95
+ n = sorted_k.shape[-1]
96
+ sorted_k = sorted_k.reshape(-1, n)
97
+ w = slot.shape[-1]
98
+ steps = slot.numel() // (num_parallel * chunk_size * w)
99
+ if krec is not None and krec.numel() == sorted_k.numel() * 4 and krec.is_contiguous() \
100
+ and krec.data_ptr() % 16 == 0 and krec.dtype == torch.int32: # read as uint4: 16-byte aligned
101
+ STATS["p1_krec"] += 1
102
+ krec = krec.reshape(-1, n, 4)
103
+ else:
104
+ krec = da2.build_krec(sorted_k, slot, k_val, rex, cpe)
105
+ # (a misaligned snapshot only disables the compact-snapshot read inside make_dw: it is never copied)
106
+ return _with_aligned(
107
+ lambda bk, dv: da2.make_dw(bk, dv, sorted_k, krec, chunk_size=chunk_size, num_parallel=num_parallel,
108
+ steps=steps, snapshot=snapshot, tiles_k=tiles_k, segb_k=segb_k,
109
+ snapshot_quant=snapshot_quant, payload=payload, scale=scale,
110
+ valid_tokens=valid_tokens),
111
+ (bank, delta_v), outs={0})
112
+
113
+
114
+ class _StepRecords:
115
+ """Record provider of gx_gds_cuda's GDS over px_qrec_cuda's output: the 8-byte read records of every chunk and
116
+ one CUDA event per step (or None)."""
117
+
118
+ def __init__(self, rec8, events):
119
+ self.rec8, self.events = rec8, events
120
+
121
+ def get(self, ti):
122
+ return self.rec8, (self.events[ti] if self.events is not None else None)
123
+
124
+
125
+ def _px_qrec_provider(sorted_q, slot, q_val, log_q, num_parallel, steps):
126
+ """px_qrec_cuda's per-step side-stream builder of the 8-byte read records: build_qrec8_steps(spay_q, q_idx, q_val,
127
+ log_q, *, P, Nt) -> (int32 [Nt * P, C * R, 2], list of Nt events or None); GDS(ti) waits on events[ti]. Same bits
128
+ as Phase 1's qrec words 1-2."""
129
+ from .cuda import px_qrec_cuda
130
+ rec8, events = px_qrec_cuda.build_qrec8_steps(sorted_q, slot, q_val, log_q, P=num_parallel, Nt=steps)
131
+ STATS["px_qrec8"] = STATS.get("px_qrec8", 0) + 1
132
+ return _StepRecords(rec8, events)
133
+
134
+
135
+ def make_mover_gds(bank, grad_bank, grad_readings, slot, q_val, log_q, d_q_w, *, chunk_size, num_parallel,
136
+ valid_tokens, sorted_q=None, qrec=None, tiles_q=None, snapshot_quant="none"):
137
+ """The GDS step launcher: gx_gds_cuda's mover (one sorted walk per tile), which delegates to mover_da2_cuda's (the
138
+ same bits) outside its scope; a call gds_admits declines raises NO_NATIVE_MOVER_MSG (the op refuses it up front). qrec, tiles_q: Phase 1's read
139
+ records (``read_records``) and tile bounds for these entries, or None. snapshot_quant: the op's mode (the shared
140
+ tail predicate admits tail classes per mode: fp8 / int4 tails are refused up front)."""
141
+ if sorted_q is None or not gds_admits(bank, grad_bank.dtype, chunk_size, snapshot_quant):
142
+ # 2.0.0 (A4): phase2_kernels' Triton GDS is gone; memory_ops.refuse_non_native refuses such calls up front.
143
+ raise RuntimeError(NO_NATIVE_MOVER_MSG.format(kind="GDS", shape=tuple(bank.shape), dtype=bank.dtype, C=chunk_size,
144
+ quant=snapshot_quant))
145
+ STATS["da2_gds"] += 1
146
+ n = sorted_q.shape[-1]
147
+ sorted_q = sorted_q.reshape(-1, n)
148
+ r = slot.shape[-1]
149
+ steps = slot.numel() // (num_parallel * chunk_size * r)
150
+ from .cuda import gx_gds_cuda as gxm
151
+ if gxm.supported(bank, grad_bank, chunk_size, r):
152
+ # gx_gds_cuda's mover reads Phase 1's qrec (16 B) or qrec8 (8 B and spay_q) as given; with neither,
153
+ # px_qrec_cuda's per-step side-stream builder (records of all steps and one event per step).
154
+ if gxm.records_ok(qrec, sorted_q):
155
+ STATS["gx_qrec_in"] = STATS.get("gx_qrec_in", 0) + 1
156
+ else:
157
+ qrec = _px_qrec_provider(sorted_q, slot, q_val, log_q, num_parallel, steps)
158
+ elif qrec is not None and torch.is_tensor(qrec) and qrec.numel() == sorted_q.numel() * 4 and \
159
+ qrec.is_contiguous() and qrec.data_ptr() % 16 == 0 and qrec.dtype == torch.int32: # Phase 1's records
160
+ STATS["p1_qrec"] += 1
161
+ qrec = qrec.reshape(-1, n, 4)
162
+ else:
163
+ qrec = da2.build_qrec(sorted_q, slot, q_val, log_q)
164
+ return _with_aligned(
165
+ lambda bk, gb, gr_: gxm.make_gds(bk, gb, gr_, sorted_q, qrec, d_q_w, chunk_size=chunk_size,
166
+ num_parallel=num_parallel, steps=steps, q_idx=slot,
167
+ valid_tokens=valid_tokens, tiles_q=tiles_q),
168
+ (bank, grad_bank, grad_readings), outs={1})
169
+
170
+
171
+ def _aligned16(*ts):
172
+ return all(t is None or t.numel() == 0 or t.data_ptr() % 16 == 0 for t in ts)
173
+
174
+
175
+ def _with_aligned(build, tensors, outs):
176
+ """Dispatch never depends on memory layout: when one of `tensors` is not 16-byte aligned (a
177
+ view), the CUDA launcher runs on aligned scratch copies instead. Before every launch the scratch is refreshed from
178
+ the caller's tensor (other kernels may change it between steps); after it, the tensors named in `outs` are copied
179
+ back. build(*aligned) returns the launcher. Same arithmetic, so the same bits as with aligned inputs."""
180
+ if _aligned16(*tensors):
181
+ return build(*tensors)
182
+ STATS["aligned_scratch"] = STATS.get("aligned_scratch", 0) + 1
183
+ scratch = [t if (t is None or t.numel() == 0 or t.data_ptr() % 16 == 0) else t.clone(memory_format=torch.contiguous_format)
184
+ for t in tensors]
185
+ launch = build(*scratch)
186
+ pairs = [(t, sc) for t, sc in zip(tensors, scratch) if sc is not t]
187
+ back = [(t, sc) for i, (t, sc) in enumerate(zip(tensors, scratch)) if sc is not t and i in outs]
188
+
189
+ def run(ti):
190
+ for t, sc in pairs:
191
+ sc.copy_(t)
192
+ launch(ti)
193
+ for t, sc in back:
194
+ t.copy_(sc)
195
+ for attr in ("reads_snapc", "writes_snapc", "geom"):
196
+ if hasattr(launch, attr):
197
+ setattr(run, attr, getattr(launch, attr))
198
+ return run