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.
- sparse_delta_memory/__init__.py +13 -0
- sparse_delta_memory/backward.py +367 -0
- sparse_delta_memory/cache.py +70 -0
- sparse_delta_memory/cuda/__init__.py +3 -0
- sparse_delta_memory/cuda/_build.py +254 -0
- sparse_delta_memory/cuda/_device.py +158 -0
- sparse_delta_memory/cuda/_devmemo.py +64 -0
- sparse_delta_memory/cuda/_image.py +128 -0
- sparse_delta_memory/cuda/bwd_steps_cuda.py +111 -0
- sparse_delta_memory/cuda/gather_cuda.py +796 -0
- sparse_delta_memory/cuda/gather_ns_cuda.py +134 -0
- sparse_delta_memory/cuda/gx_gds_cuda.py +1297 -0
- sparse_delta_memory/cuda/gx_gemm_cuda.py +857 -0
- sparse_delta_memory/cuda/gx_pa2_cuda.py +1320 -0
- sparse_delta_memory/cuda/mover_da2_cuda.py +1802 -0
- sparse_delta_memory/cuda/mover_fx_cuda.py +2229 -0
- sparse_delta_memory/cuda/pa_dc_cuda.py +1500 -0
- sparse_delta_memory/cuda/phase1_bwd_cuda.py +852 -0
- sparse_delta_memory/cuda/phase1_common.cuh +181 -0
- sparse_delta_memory/cuda/phase1_cuda.py +3801 -0
- sparse_delta_memory/cuda/product_key_topk_anyf_cuda.py +668 -0
- sparse_delta_memory/cuda/product_key_topk_cuda.py +735 -0
- sparse_delta_memory/cuda/px_qrec_cuda.py +167 -0
- sparse_delta_memory/cuda/sdm_copy.cuh +90 -0
- sparse_delta_memory/cuda/sdm_plancap.cuh +150 -0
- sparse_delta_memory/cuda_graph.py +605 -0
- sparse_delta_memory/decode.py +140 -0
- sparse_delta_memory/forward.py +464 -0
- sparse_delta_memory/layer.py +663 -0
- sparse_delta_memory/memory_ops.py +1069 -0
- sparse_delta_memory/memory_stats.py +221 -0
- sparse_delta_memory/movers.py +198 -0
- sparse_delta_memory/phase2_kernels.py +267 -0
- sparse_delta_memory/product_key_topk.py +113 -0
- sparse_delta_memory/wy_solve.py +273 -0
- sparse_delta_memory-2.0.0.dist-info/METADATA +482 -0
- sparse_delta_memory-2.0.0.dist-info/RECORD +40 -0
- sparse_delta_memory-2.0.0.dist-info/WHEEL +5 -0
- sparse_delta_memory-2.0.0.dist-info/licenses/LICENSE +407 -0
- 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
|