pelagos-decoder 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pelagos/__init__.py +34 -0
- pelagos/_process_worker.py +56 -0
- pelagos/adapter.py +858 -0
- pelagos/autotune.py +793 -0
- pelagos/decoder.py +3891 -0
- pelagos/graph.py +479 -0
- pelagos/kernels.py +2208 -0
- pelagos/process.py +287 -0
- pelagos/stim/__init__.py +11 -0
- pelagos/stim/sinter/__init__.py +11 -0
- pelagos/stim/sinter/check_matrices.py +112 -0
- pelagos/utils.py +995 -0
- pelagos_decoder-0.1.0.dist-info/METADATA +317 -0
- pelagos_decoder-0.1.0.dist-info/RECORD +16 -0
- pelagos_decoder-0.1.0.dist-info/WHEEL +4 -0
- pelagos_decoder-0.1.0.dist-info/licenses/LICENSE +201 -0
pelagos/decoder.py
ADDED
|
@@ -0,0 +1,3891 @@
|
|
|
1
|
+
# (C) Copyright IBM 2025, 2026
|
|
2
|
+
#
|
|
3
|
+
# This code is licensed under the Apache License, Version 2.0. You may
|
|
4
|
+
# obtain a copy of this license in the LICENSE file in the root directory
|
|
5
|
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
|
6
|
+
#
|
|
7
|
+
# Any modifications or derivative works of this code must retain this
|
|
8
|
+
# copyright notice, and modified files need to carry a notice indicating
|
|
9
|
+
# that they have been altered from the originals.
|
|
10
|
+
|
|
11
|
+
"""Relay-BP-S decoder with Triton GPU kernels.
|
|
12
|
+
|
|
13
|
+
This module implements the Relay-BP (Relay Belief Propagation) algorithm for quantum error correction
|
|
14
|
+
using GPU-accelerated Triton kernels. The algorithm performs ensemble decoding with disordered memory
|
|
15
|
+
strengths to improve decoding performance on quantum error correction codes.
|
|
16
|
+
|
|
17
|
+
The implementation closely follows the Rust reference implementation in crates/relay_bp/src/bp/,
|
|
18
|
+
providing GPU acceleration while maintaining algorithmic fidelity.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import math
|
|
22
|
+
|
|
23
|
+
import torch
|
|
24
|
+
import numpy as np
|
|
25
|
+
from scipy.sparse import csr_matrix
|
|
26
|
+
from typing import Dict, Optional, Tuple, Union, List, Any
|
|
27
|
+
import os
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
|
|
30
|
+
from .graph import CSRGraph
|
|
31
|
+
from . import autotune as ac
|
|
32
|
+
from .kernels import (
|
|
33
|
+
c2v_min_sum_kernel,
|
|
34
|
+
v2c_and_marginals_fused_gamma_kernel,
|
|
35
|
+
parity_per_check_kernel,
|
|
36
|
+
parity_from_hard_kernel,
|
|
37
|
+
parity_from_hard_compact_kernel,
|
|
38
|
+
parity_from_hard_compact_with_zero_kernel,
|
|
39
|
+
init_messages_kernel,
|
|
40
|
+
init_messages_with_priors_EB_kernel,
|
|
41
|
+
c2v_min_sum_btile_kernel,
|
|
42
|
+
v2c_and_gamma_btile_kernel,
|
|
43
|
+
post_from_L_kernel,
|
|
44
|
+
reduce_all_ge_kernel,
|
|
45
|
+
freeze_finished_lanes_kernel,
|
|
46
|
+
unfreeze_finished_lanes_kernel,
|
|
47
|
+
count_active_blocks_kernel,
|
|
48
|
+
compact_active_indices_kernel,
|
|
49
|
+
c2v_min_sum_grid_stride_kernel,
|
|
50
|
+
check_and_select_kernel,
|
|
51
|
+
dtype_str_to_enum,
|
|
52
|
+
)
|
|
53
|
+
from .utils import (
|
|
54
|
+
compute_log_prior_ratios,
|
|
55
|
+
sample_gamma_uniform,
|
|
56
|
+
sample_gamma_scalar,
|
|
57
|
+
sample_gamma_per_variable,
|
|
58
|
+
GAMMA_DRAW_MODES,
|
|
59
|
+
validate_error_priors,
|
|
60
|
+
validate_csr_matrix,
|
|
61
|
+
bitpack_errors,
|
|
62
|
+
)
|
|
63
|
+
from .autotune import (
|
|
64
|
+
V2C_BTILE_BLOCK_SIZE,
|
|
65
|
+
build_c2v_configs,
|
|
66
|
+
build_v2c_configs,
|
|
67
|
+
build_parity_configs,
|
|
68
|
+
build_init_messages_configs,
|
|
69
|
+
build_c2v_btile_configs,
|
|
70
|
+
build_v2c_btile_configs,
|
|
71
|
+
build_check_select_configs,
|
|
72
|
+
build_c2v_grid_stride_configs,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _build_check_conflict_adjacency(H_csr: csr_matrix) -> List[np.ndarray]:
|
|
77
|
+
"""Build adjacency of the check–check conflict graph from CSR H.
|
|
78
|
+
|
|
79
|
+
Nodes are checks (rows). Two checks are adjacent if they share at least one variable.
|
|
80
|
+
"""
|
|
81
|
+
# Work in CSC so we can iterate variable → incident checks efficiently
|
|
82
|
+
H_csc = H_csr.tocsc()
|
|
83
|
+
C = H_csr.shape[0]
|
|
84
|
+
|
|
85
|
+
adj_sets: List[set[int]] = [set() for _ in range(C)]
|
|
86
|
+
|
|
87
|
+
indptr = H_csc.indptr
|
|
88
|
+
indices = H_csc.indices # row indices (checks) for each column
|
|
89
|
+
|
|
90
|
+
for v in range(H_csc.shape[1]):
|
|
91
|
+
a, b = indptr[v], indptr[v + 1]
|
|
92
|
+
checks = indices[a:b]
|
|
93
|
+
dv = checks.size
|
|
94
|
+
if dv <= 1:
|
|
95
|
+
continue
|
|
96
|
+
# Add clique among incident checks
|
|
97
|
+
for i in range(dv):
|
|
98
|
+
ci = int(checks[i])
|
|
99
|
+
for j in range(i + 1, dv):
|
|
100
|
+
cj = int(checks[j])
|
|
101
|
+
if ci == cj:
|
|
102
|
+
continue
|
|
103
|
+
adj_sets[ci].add(cj)
|
|
104
|
+
adj_sets[cj].add(ci)
|
|
105
|
+
|
|
106
|
+
adj: List[np.ndarray] = []
|
|
107
|
+
for s in adj_sets:
|
|
108
|
+
if s:
|
|
109
|
+
arr = np.fromiter(s, dtype=np.int32)
|
|
110
|
+
adj.append(np.sort(arr))
|
|
111
|
+
else:
|
|
112
|
+
adj.append(np.empty(0, dtype=np.int32))
|
|
113
|
+
return adj
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _dsatur_coloring(adj: List[np.ndarray]) -> Tuple[np.ndarray, int]:
|
|
117
|
+
"""DSATUR heuristic coloring of the check–check conflict graph.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
color: np.int32 array of size C with color IDs [0..L-1]
|
|
121
|
+
L: number of colors (layers)
|
|
122
|
+
"""
|
|
123
|
+
C = len(adj)
|
|
124
|
+
color = -np.ones(C, dtype=np.int32)
|
|
125
|
+
|
|
126
|
+
# degrees for tie-break
|
|
127
|
+
deg = np.array([len(a) for a in adj], dtype=np.int32)
|
|
128
|
+
|
|
129
|
+
# saturation sets: which colors are present among colored neighbors
|
|
130
|
+
sat: List[set[int]] = [set() for _ in range(C)]
|
|
131
|
+
|
|
132
|
+
uncolored: set[int] = set(range(C))
|
|
133
|
+
|
|
134
|
+
def pick_next() -> int:
|
|
135
|
+
# pick vertex with max saturation, tie-break by degree
|
|
136
|
+
best = None
|
|
137
|
+
best_key = None
|
|
138
|
+
for c in uncolored:
|
|
139
|
+
key = (len(sat[c]), deg[c])
|
|
140
|
+
if best is None or key > best_key:
|
|
141
|
+
best = c
|
|
142
|
+
best_key = key
|
|
143
|
+
return int(best) # type: ignore[arg-type]
|
|
144
|
+
|
|
145
|
+
while uncolored:
|
|
146
|
+
c = pick_next()
|
|
147
|
+
|
|
148
|
+
# forbidden colors from colored neighbors
|
|
149
|
+
forbidden: set[int] = set()
|
|
150
|
+
for nb in adj[c]:
|
|
151
|
+
nb_i = int(nb)
|
|
152
|
+
if color[nb_i] >= 0:
|
|
153
|
+
forbidden.add(int(color[nb_i]))
|
|
154
|
+
|
|
155
|
+
# smallest available color
|
|
156
|
+
k = 0
|
|
157
|
+
while k in forbidden:
|
|
158
|
+
k += 1
|
|
159
|
+
color[c] = k
|
|
160
|
+
|
|
161
|
+
# update saturation of its uncolored neighbors
|
|
162
|
+
for nb in adj[c]:
|
|
163
|
+
nb_i = int(nb)
|
|
164
|
+
if color[nb_i] < 0:
|
|
165
|
+
sat[nb_i].add(k)
|
|
166
|
+
|
|
167
|
+
uncolored.remove(c)
|
|
168
|
+
|
|
169
|
+
L = int(color.max() + 1) if C > 0 else 0
|
|
170
|
+
return color, L
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _color_layers_from_H(H_csr: csr_matrix) -> Tuple[np.ndarray, np.ndarray]:
|
|
174
|
+
"""Compute conflict-free layers (graph coloring) from H_csr.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
check_permutation: np.int32 array of length C giving new row order
|
|
178
|
+
grouped by layer (layer 0, then layer 1, ...)
|
|
179
|
+
layer_sizes: np.int32 array of length L with number of checks per layer
|
|
180
|
+
"""
|
|
181
|
+
adj = _build_check_conflict_adjacency(H_csr)
|
|
182
|
+
color, L = _dsatur_coloring(adj)
|
|
183
|
+
if L == 0:
|
|
184
|
+
C = H_csr.shape[0]
|
|
185
|
+
return np.arange(C, dtype=np.int32), np.zeros(0, dtype=np.int32)
|
|
186
|
+
|
|
187
|
+
layers: List[List[int]] = [[] for _ in range(L)]
|
|
188
|
+
for chk, k in enumerate(color):
|
|
189
|
+
layers[int(k)].append(chk)
|
|
190
|
+
|
|
191
|
+
layer_sizes = np.array([len(layer) for layer in layers], dtype=np.int32)
|
|
192
|
+
perm_list: List[int] = []
|
|
193
|
+
for layer in layers:
|
|
194
|
+
perm_list.extend(layer)
|
|
195
|
+
check_permutation = np.asarray(perm_list, dtype=np.int32)
|
|
196
|
+
return check_permutation, layer_sizes
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _clone_bench_state(args: Tuple, meta_base: Dict[str, Any]) -> Tuple[Tuple, Dict[str, Any]]:
|
|
200
|
+
"""Return (args, meta_base) with every tensor replaced by a scratch clone.
|
|
201
|
+
|
|
202
|
+
Autotuning benchmarks a kernel by launching it repeatedly on the argument
|
|
203
|
+
tensors it is handed.
|
|
204
|
+
"""
|
|
205
|
+
seen: Dict[int, torch.Tensor] = {}
|
|
206
|
+
|
|
207
|
+
def _clone(x):
|
|
208
|
+
if not isinstance(x, torch.Tensor):
|
|
209
|
+
return x
|
|
210
|
+
key = id(x)
|
|
211
|
+
hit = seen.get(key)
|
|
212
|
+
if hit is None:
|
|
213
|
+
hit = x.clone()
|
|
214
|
+
seen[key] = hit
|
|
215
|
+
return hit
|
|
216
|
+
|
|
217
|
+
cloned_args = tuple(_clone(a) for a in args)
|
|
218
|
+
cloned_meta = {k: _clone(v) for k, v in meta_base.items()}
|
|
219
|
+
return cloned_args, cloned_meta
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# Values accepted by ``unconverged_correction``.
|
|
223
|
+
UNCONVERGED_CORRECTIONS = ("first_leg", "last_leg")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass
|
|
227
|
+
class PerfCfg:
|
|
228
|
+
"""Performance configuration for kernel tuning.
|
|
229
|
+
|
|
230
|
+
Stores autotuned parameters for different performance modes.
|
|
231
|
+
Corresponds to kernel configuration in the Rust implementation.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
perf: str
|
|
235
|
+
msg_dtype: str
|
|
236
|
+
btile: int
|
|
237
|
+
rows_per_chk: int
|
|
238
|
+
rows_per_var: int
|
|
239
|
+
c2v_block: int
|
|
240
|
+
c2v_warps: int
|
|
241
|
+
c2v_stages: int
|
|
242
|
+
v2c_block: int
|
|
243
|
+
v2c_warps: int
|
|
244
|
+
v2c_stages: int
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class RelayBPDecoder:
|
|
248
|
+
"""Relay-BP-S decoder with Triton GPU kernels.
|
|
249
|
+
|
|
250
|
+
Implements the Relay-BP (Relay Belief Propagation) algorithm for quantum error correction
|
|
251
|
+
using GPU-accelerated message passing. The algorithm uses ensemble decoding with disordered
|
|
252
|
+
memory strengths (gamma mixing) to improve decoding performance on quantum error correction codes.
|
|
253
|
+
|
|
254
|
+
This implementation closely follows the Rust reference in crates/relay_bp/src/bp/relay.rs
|
|
255
|
+
and crates/relay_bp/src/bp/min_sum.rs, providing GPU acceleration while maintaining
|
|
256
|
+
algorithmic fidelity to the original min-sum belief propagation with memory.
|
|
257
|
+
|
|
258
|
+
Key features:
|
|
259
|
+
- Batch processing for multiple syndrome samples
|
|
260
|
+
- Two performance modes: default, throughput
|
|
261
|
+
- Support for both plain BP and Relay-BP algorithms
|
|
262
|
+
- Configurable min-sum parameters (alpha, beta)
|
|
263
|
+
- Memory strength (gamma) mixing for ensemble decoding
|
|
264
|
+
- Early stopping after finding stop_nconv valid solutions
|
|
265
|
+
- Optional explicit per-leg gamma matrices (K×V) for deterministic relay legs
|
|
266
|
+
- unconverged_correction selects the correction for a lane with no valid solution:
|
|
267
|
+
the hard decision at the end of the first leg ("first_leg", default) or of the last
|
|
268
|
+
leg that ran ("last_leg")
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
def __init__(
|
|
272
|
+
self,
|
|
273
|
+
H_csr: csr_matrix,
|
|
274
|
+
error_priors: np.ndarray,
|
|
275
|
+
pre_iter: int = 80,
|
|
276
|
+
num_sets: int = 100,
|
|
277
|
+
set_max_iter: int = 60,
|
|
278
|
+
gamma0: float = 0.65,
|
|
279
|
+
gamma_dist_interval: Tuple[float, float] = (-0.24, 0.66),
|
|
280
|
+
stop_nconv: int = 5,
|
|
281
|
+
normalized_min_sum_alpha: Optional[float] = None,
|
|
282
|
+
offset_min_sum_beta: Optional[float] = None,
|
|
283
|
+
data_type: str = "float32",
|
|
284
|
+
device: Union[str, List[int], int] = [0],
|
|
285
|
+
seed: int = 1234,
|
|
286
|
+
gamma_draw: str = "broadcast",
|
|
287
|
+
bitpack_output: bool = False,
|
|
288
|
+
algo: Optional[str] = None, # "relay" or "plain"
|
|
289
|
+
perf: Optional[str] = None, # "default" | "throughput"
|
|
290
|
+
alpha_iteration_scaling_factor: Optional[float] = 1.0,
|
|
291
|
+
explicit_gammas: Optional[np.ndarray] = None,
|
|
292
|
+
gamma_dist_interval_by_error: Optional[np.ndarray] = None,
|
|
293
|
+
fast_max_unconverged_pct: Optional[
|
|
294
|
+
float
|
|
295
|
+
] = None, # Optional early exit threshold: stop when unconverged percentage <= this value (0.0-1.0)
|
|
296
|
+
scheduling: str = "flood-fill", # "flood-fill" (default) or "layered" (block-layered for first K iterations)
|
|
297
|
+
layered_k_iters: int = 0, # Number of layered iterations per leg (0 = disabled, only used when scheduling="layered")
|
|
298
|
+
layered_block_policy: str = "contiguous", # "contiguous" (default), "permute", "strided_interleaving", "color", or "color_use_delta_trick"
|
|
299
|
+
layered_num_blocks: Optional[
|
|
300
|
+
int
|
|
301
|
+
] = None, # Explicit number of blocks (if None, computed from target_edges_per_block)
|
|
302
|
+
layered_target_edges_per_block: Optional[
|
|
303
|
+
int
|
|
304
|
+
] = None, # Target edges per block (default: 200000)
|
|
305
|
+
layered_block_seed: int = 42, # Random seed for "permute" policy
|
|
306
|
+
unconverged_correction: str = "first_leg",
|
|
307
|
+
):
|
|
308
|
+
"""Initialize Relay-BP decoder.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
H_csr: Check matrix (C x V) in CSR format representing the quantum code's
|
|
312
|
+
stabilizer generators. Each row is a check constraint.
|
|
313
|
+
error_priors: [V] error probabilities in (0, 0.5) for each qubit
|
|
314
|
+
pre_iter: T0 - iterations for first set (ordered memory phase)
|
|
315
|
+
num_sets: R - number of relay legs/ensembles (disordered memory phase)
|
|
316
|
+
set_max_iter: Tr - iterations per relay set
|
|
317
|
+
gamma0: γ for first set (ordered memory strength, corresponds to Rust gamma0)
|
|
318
|
+
gamma_dist_interval: (min, max) for disordered γ sampling in relay legs
|
|
319
|
+
stop_nconv: S - number of valid solutions to collect before stopping
|
|
320
|
+
normalized_min_sum_alpha: α for normalized min-sum (0 < α ≤ 1, corresponds to Rust alpha)
|
|
321
|
+
offset_min_sum_beta: β for offset min-sum (mutually exclusive with α)
|
|
322
|
+
data_type: "float16", "float32", or "float64" for uniform precision (storage and computation). Default: "float32".
|
|
323
|
+
device: GPU device selection. Can be:
|
|
324
|
+
- List[int]: Explicit device IDs, e.g. [0, 1, 2, 3] for 4 GPUs
|
|
325
|
+
- int: Number of GPUs to auto-detect, e.g. 4 -> [0, 1, 2, 3]
|
|
326
|
+
- str: Legacy support - "auto" or "cuda" -> [0] (single GPU)
|
|
327
|
+
On ROCm builds, PyTorch uses device type "cuda" for ROCm.
|
|
328
|
+
seed: RNG seed for γ sampling (corresponds to Rust seed)
|
|
329
|
+
gamma_draw: how disordered-leg γ is drawn from gamma_dist_interval or
|
|
330
|
+
gamma_dist_interval_by_error. "broadcast": one [1, V] row per leg from
|
|
331
|
+
seed + leg, shared by every shot. "per_row": one [B, V] draw per leg
|
|
332
|
+
from seed + leg, so a shot's γ depends on its row in the batch.
|
|
333
|
+
Ignored when explicit_gammas is set.
|
|
334
|
+
bitpack_output: whether to return packed error bits for memory efficiency
|
|
335
|
+
algo: "relay" or "plain" algorithm mode
|
|
336
|
+
perf: "default" or "throughput" performance mode
|
|
337
|
+
alpha_iteration_scaling_factor: scaling factor for alpha ramp schedule
|
|
338
|
+
explicit_gammas: optional np.ndarray of shape (K, V) providing per-leg, per-variable
|
|
339
|
+
memory strengths γ. Relay leg l (0-based) uses row ((l + 1) % K), the row Rust
|
|
340
|
+
uses for set l + 1. When provided, uniform γ sampling is disabled.
|
|
341
|
+
fast_max_unconverged_pct: Optional early exit threshold (0.0-1.0) for difficult case collection.
|
|
342
|
+
If set, decoder stops when the percentage of unconverged syndromes drops to this threshold
|
|
343
|
+
or below (e.g., 0.1 = stop when <=10% unconverged) and returns unconverged indices.
|
|
344
|
+
If None, runs full num_sets relay legs. Default: None.
|
|
345
|
+
When using for difficult case collection, collect unconverged_indices across batches
|
|
346
|
+
and process them separately at the end with full num_sets.
|
|
347
|
+
scheduling: Iteration scheduling mode. "flood-fill" (default) processes all checks then all vars.
|
|
348
|
+
"layered" uses block-layered scheduling for first K iterations (Gauss-Seidel style), then
|
|
349
|
+
switches to flood-fill. Only supported in throughput mode.
|
|
350
|
+
layered_k_iters: Number of layered iterations per leg (default: 0, disabled). When scheduling="layered"
|
|
351
|
+
and layered_k_iters > 0, first K iterations use block-layered, remaining use flood-fill.
|
|
352
|
+
Only used when scheduling="layered" and perf="throughput".
|
|
353
|
+
unconverged_correction: Correction returned for a lane with no valid solution in
|
|
354
|
+
any leg. "first_leg" (default, as in the Rust reference) returns the hard
|
|
355
|
+
decision at the end of the pre-iter leg, or of relay leg 1 when pre_iter is 0.
|
|
356
|
+
"last_leg" returns the hard decision at the end of the last leg that ran,
|
|
357
|
+
including a leg after which fast_max_unconverged_pct stops the decode.
|
|
358
|
+
|
|
359
|
+
Raises:
|
|
360
|
+
ValueError: If unconverged_correction is not one of UNCONVERGED_CORRECTIONS.
|
|
361
|
+
"""
|
|
362
|
+
if unconverged_correction not in UNCONVERGED_CORRECTIONS:
|
|
363
|
+
raise ValueError(
|
|
364
|
+
f"unconverged_correction must be one of {UNCONVERGED_CORRECTIONS} "
|
|
365
|
+
f"(got {unconverged_correction!r})"
|
|
366
|
+
)
|
|
367
|
+
self.unconverged_correction = unconverged_correction
|
|
368
|
+
# Validate inputs
|
|
369
|
+
validate_csr_matrix(H_csr)
|
|
370
|
+
error_priors = validate_error_priors(error_priors)
|
|
371
|
+
|
|
372
|
+
if normalized_min_sum_alpha is not None and offset_min_sum_beta is not None:
|
|
373
|
+
raise ValueError("Cannot specify both normalized_min_sum_alpha and offset_min_sum_beta")
|
|
374
|
+
|
|
375
|
+
# Allow alpha==0.0 to enable the ramp schedule (Rust-like semantics)
|
|
376
|
+
if normalized_min_sum_alpha is not None and not (0 <= normalized_min_sum_alpha <= 1):
|
|
377
|
+
raise ValueError("normalized_min_sum_alpha must be in [0, 1]")
|
|
378
|
+
|
|
379
|
+
if offset_min_sum_beta is not None and offset_min_sum_beta < 0:
|
|
380
|
+
raise ValueError("offset_min_sum_beta must be >= 0")
|
|
381
|
+
# Store parameters
|
|
382
|
+
self.H_csr = H_csr
|
|
383
|
+
# Store check_permutation if used (needed for syndrome permutation in adapter)
|
|
384
|
+
self.check_permutation = None
|
|
385
|
+
self.error_priors = error_priors
|
|
386
|
+
self.pre_iter = pre_iter
|
|
387
|
+
self.num_sets = num_sets
|
|
388
|
+
self.set_max_iter = set_max_iter
|
|
389
|
+
self.gamma0 = gamma0
|
|
390
|
+
self.gamma_dist_interval = gamma_dist_interval
|
|
391
|
+
self.stop_nconv = stop_nconv
|
|
392
|
+
self.fast_max_unconverged_pct = fast_max_unconverged_pct
|
|
393
|
+
if fast_max_unconverged_pct is not None:
|
|
394
|
+
if not (0.0 <= fast_max_unconverged_pct <= 1.0):
|
|
395
|
+
raise ValueError(
|
|
396
|
+
f"fast_max_unconverged_pct must be between 0.0 and 1.0, got {fast_max_unconverged_pct}"
|
|
397
|
+
)
|
|
398
|
+
self.normalized_min_sum_alpha = normalized_min_sum_alpha
|
|
399
|
+
self.offset_min_sum_beta = offset_min_sum_beta
|
|
400
|
+
|
|
401
|
+
# Unified dtype system: support float16, float32, float64
|
|
402
|
+
_dtype_aliases = {
|
|
403
|
+
"float": "float32",
|
|
404
|
+
"half": "float16",
|
|
405
|
+
"double": "float64",
|
|
406
|
+
"fp16": "float16",
|
|
407
|
+
"fp32": "float32",
|
|
408
|
+
"fp64": "float64",
|
|
409
|
+
}
|
|
410
|
+
data_type = _dtype_aliases.get(data_type, data_type)
|
|
411
|
+
if data_type not in ("float16", "float32", "float64"):
|
|
412
|
+
raise ValueError(
|
|
413
|
+
f"data_type must be 'float16', 'float32', or 'float64', got {data_type}"
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
self.dtype_str = data_type
|
|
417
|
+
self.dtype = {
|
|
418
|
+
"float16": torch.float16,
|
|
419
|
+
"float32": torch.float32,
|
|
420
|
+
"float64": torch.float64,
|
|
421
|
+
}[data_type]
|
|
422
|
+
# Cache enum once to avoid recomputing it on every kernel launch
|
|
423
|
+
self._dtype_enum = dtype_str_to_enum(self.dtype_str)
|
|
424
|
+
# Parse device specification to list of device IDs
|
|
425
|
+
self.device_ids = self._parse_device_spec(device)
|
|
426
|
+
self.num_gpus = len(self.device_ids)
|
|
427
|
+
|
|
428
|
+
# Detect backend (CUDA or ROCm) - PyTorch uses "cuda" device type for both.
|
|
429
|
+
has_gpu = bool(torch.cuda.is_available())
|
|
430
|
+
is_rocm = getattr(torch.version, "hip", None) is not None
|
|
431
|
+
is_cuda_drv = torch.version.cuda is not None
|
|
432
|
+
if not has_gpu:
|
|
433
|
+
raise RuntimeError(
|
|
434
|
+
"GPU not available (install a GPU-enabled PyTorch build: CUDA or ROCm)"
|
|
435
|
+
)
|
|
436
|
+
# Track backend for logging/metrics only (device type is always "cuda" for both CUDA and ROCm)
|
|
437
|
+
self._backend = "rocm" if (is_rocm and not is_cuda_drv) else "cuda"
|
|
438
|
+
# Device type string for PyTorch (works for both CUDA and ROCm)
|
|
439
|
+
self.device_type = "cuda"
|
|
440
|
+
# Cache per-GPU device strings to avoid rebuilding on every use
|
|
441
|
+
self._device_strs: Dict[int, str] = {
|
|
442
|
+
gpu_id: f"{self.device_type}:{gpu_id}" for gpu_id in self.device_ids
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
self.seed = seed
|
|
446
|
+
if gamma_draw not in GAMMA_DRAW_MODES:
|
|
447
|
+
raise ValueError(f"gamma_draw must be one of {GAMMA_DRAW_MODES}; got {gamma_draw!r}")
|
|
448
|
+
self.gamma_draw = gamma_draw
|
|
449
|
+
self.bitpack_output = bitpack_output
|
|
450
|
+
# Normalize separate axes
|
|
451
|
+
algo_norm = algo
|
|
452
|
+
if algo_norm not in ("relay", "plain"):
|
|
453
|
+
raise ValueError(f"Unknown algo '{algo_norm}'. Use 'relay' or 'plain'.")
|
|
454
|
+
self.algo = algo_norm
|
|
455
|
+
|
|
456
|
+
perf_norm = (perf or "throughput").strip().lower()
|
|
457
|
+
perf_aliases = {
|
|
458
|
+
"default": "default",
|
|
459
|
+
"row": "default",
|
|
460
|
+
"rowwise": "default",
|
|
461
|
+
"throughput": "throughput",
|
|
462
|
+
"btile": "throughput",
|
|
463
|
+
}
|
|
464
|
+
if perf_norm not in perf_aliases:
|
|
465
|
+
raise ValueError(f"Unknown perf '{perf_norm}'. Use 'default'/'throughput'.")
|
|
466
|
+
self.perf = perf_aliases[perf_norm]
|
|
467
|
+
self.alpha_iteration_scaling_factor = float(alpha_iteration_scaling_factor or 1.0)
|
|
468
|
+
|
|
469
|
+
# Scheduling validation
|
|
470
|
+
scheduling_norm = (scheduling or "flood-fill").strip().lower()
|
|
471
|
+
if scheduling_norm not in ("flood-fill", "layered"):
|
|
472
|
+
raise ValueError(
|
|
473
|
+
f"Unknown scheduling '{scheduling_norm}'. Use 'flood-fill' or 'layered'."
|
|
474
|
+
)
|
|
475
|
+
self.scheduling = scheduling_norm
|
|
476
|
+
self.layered_k_iters = int(layered_k_iters) if layered_k_iters is not None else 0
|
|
477
|
+
# Layered scheduling only works in throughput mode
|
|
478
|
+
if self.scheduling == "layered" and self.perf != "throughput":
|
|
479
|
+
raise ValueError("scheduling='layered' requires perf='throughput'")
|
|
480
|
+
# Explicit gammas (for relay legs only). Replicate on each GPU as [K,V] fp32.
|
|
481
|
+
self.explicit_gammas_per_gpu = None
|
|
482
|
+
if explicit_gammas is not None:
|
|
483
|
+
eg = np.asarray(explicit_gammas, dtype=np.float32)
|
|
484
|
+
if eg.ndim != 2 or eg.shape[1] != int(H_csr.shape[1]):
|
|
485
|
+
raise ValueError(f"explicit_gammas must have shape (K, V={int(H_csr.shape[1])})")
|
|
486
|
+
# Replicate on each GPU
|
|
487
|
+
self.explicit_gammas_per_gpu = []
|
|
488
|
+
for gpu_id in self.device_ids:
|
|
489
|
+
device_str = self._device_strs[gpu_id]
|
|
490
|
+
eg_gpu = torch.from_numpy(eg).to(device_str, dtype=self.dtype)
|
|
491
|
+
self.explicit_gammas_per_gpu.append(eg_gpu)
|
|
492
|
+
|
|
493
|
+
# Per-variable gamma intervals (local gammas feature).
|
|
494
|
+
# Mirrors Rust `gamma_dist_interval_by_error` behavior in crates/relay_bp/src/bp/relay.rs.
|
|
495
|
+
# Stored as [V, 2] tensors (low, high) replicated per GPU.
|
|
496
|
+
self.gamma_dist_interval_by_error_per_gpu = None
|
|
497
|
+
if gamma_dist_interval_by_error is not None:
|
|
498
|
+
gd = np.asarray(gamma_dist_interval_by_error, dtype=np.float32)
|
|
499
|
+
V = int(H_csr.shape[1])
|
|
500
|
+
if gd.ndim != 2 or gd.shape[0] != V or gd.shape[1] != 2:
|
|
501
|
+
raise ValueError(
|
|
502
|
+
f"gamma_dist_interval_by_error must have shape (V={V}, 2), got {tuple(gd.shape)}"
|
|
503
|
+
)
|
|
504
|
+
if np.any(gd[:, 0] > gd[:, 1]):
|
|
505
|
+
raise ValueError(
|
|
506
|
+
"gamma_dist_interval_by_error: lower bounds must be <= upper bounds"
|
|
507
|
+
)
|
|
508
|
+
self.gamma_dist_interval_by_error_per_gpu = []
|
|
509
|
+
for gpu_id in self.device_ids:
|
|
510
|
+
device_str = self._device_strs[gpu_id]
|
|
511
|
+
gd_gpu = torch.from_numpy(gd).to(device_str, dtype=self.dtype)
|
|
512
|
+
self.gamma_dist_interval_by_error_per_gpu.append(gd_gpu)
|
|
513
|
+
|
|
514
|
+
# Set up device (availability already validated above)
|
|
515
|
+
|
|
516
|
+
# Build block metadata for layered scheduling
|
|
517
|
+
# For "permute", "strided_interleaving", and "color" policies, we need to apply
|
|
518
|
+
# a permutation before building graphs
|
|
519
|
+
self.block_metadata = None
|
|
520
|
+
self.block_overlap_stats = None
|
|
521
|
+
self._color_layer_sizes: Optional[np.ndarray] = None
|
|
522
|
+
self.layered_block_policy = layered_block_policy
|
|
523
|
+
self._use_delta_trick: bool = (
|
|
524
|
+
self.scheduling == "layered"
|
|
525
|
+
and self.perf == "throughput"
|
|
526
|
+
and layered_block_policy == "color_use_delta_trick"
|
|
527
|
+
)
|
|
528
|
+
check_permutation: Optional[np.ndarray] = None
|
|
529
|
+
if self.scheduling == "layered" and self.perf == "throughput":
|
|
530
|
+
# Validate layered_block_policy
|
|
531
|
+
if layered_block_policy not in (
|
|
532
|
+
"contiguous",
|
|
533
|
+
"permute",
|
|
534
|
+
"strided_interleaving",
|
|
535
|
+
"color",
|
|
536
|
+
"color_use_delta_trick",
|
|
537
|
+
):
|
|
538
|
+
raise ValueError(
|
|
539
|
+
f"layered_block_policy must be 'contiguous', 'permute', 'strided_interleaving', 'color', "
|
|
540
|
+
f"or 'color_use_delta_trick', got {layered_block_policy!r}"
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# Apply check permutation if needed
|
|
544
|
+
if layered_block_policy == "permute":
|
|
545
|
+
# Random permutation of checks (deterministic with seed)
|
|
546
|
+
# Use default_rng to avoid global RNG state mutation
|
|
547
|
+
rng = np.random.default_rng(layered_block_seed)
|
|
548
|
+
check_permutation = rng.permutation(self.H_csr.shape[0]).astype(np.int32)
|
|
549
|
+
# Store permutation for syndrome reordering in adapter
|
|
550
|
+
self.check_permutation = check_permutation
|
|
551
|
+
elif layered_block_policy == "strided_interleaving":
|
|
552
|
+
# Cyclic interleaving: check k goes to group (k % num_groups)
|
|
553
|
+
# Exploits quasi-cyclic structure of bicycle codes to spread checks across blocks.
|
|
554
|
+
if layered_num_blocks is None:
|
|
555
|
+
raise ValueError(
|
|
556
|
+
"layered_block_policy='strided_interleaving' requires layered_num_blocks to be set"
|
|
557
|
+
)
|
|
558
|
+
C = self.H_csr.shape[0]
|
|
559
|
+
num_groups = int(layered_num_blocks)
|
|
560
|
+
check_permutation = np.array(
|
|
561
|
+
[i for g in range(num_groups) for i in range(g, C, num_groups)],
|
|
562
|
+
dtype=np.int32,
|
|
563
|
+
)
|
|
564
|
+
self.check_permutation = check_permutation
|
|
565
|
+
elif layered_block_policy in ("color", "color_use_delta_trick"):
|
|
566
|
+
# Conflict-free layers via DSATUR coloring. Each layer contains checks that do not
|
|
567
|
+
# share variables (overlap=0 within a layer). We build a permutation that groups
|
|
568
|
+
# layers contiguously.
|
|
569
|
+
check_permutation, layer_sizes = _color_layers_from_H(self.H_csr)
|
|
570
|
+
self.check_permutation = check_permutation
|
|
571
|
+
self._color_layer_sizes = layer_sizes
|
|
572
|
+
|
|
573
|
+
# Create graph on each GPU (read-only, can be replicated).
|
|
574
|
+
# Apply check_permutation if it was computed above.
|
|
575
|
+
self.graphs = []
|
|
576
|
+
for gpu_id in self.device_ids:
|
|
577
|
+
device_str = self._device_strs[gpu_id]
|
|
578
|
+
graph = CSRGraph(H_csr, device_str, check_permutation=check_permutation)
|
|
579
|
+
self.graphs.append(graph)
|
|
580
|
+
|
|
581
|
+
# Set graph shape info
|
|
582
|
+
self.C, self.V = self.graphs[0].C, self.graphs[0].V
|
|
583
|
+
self.E = self.graphs[0].E
|
|
584
|
+
|
|
585
|
+
# Build block metadata for layered scheduling
|
|
586
|
+
if self.scheduling == "layered" and self.perf == "throughput":
|
|
587
|
+
graph0 = self.graphs[0]
|
|
588
|
+
|
|
589
|
+
# First, construct block check ranges and variable lists depending on policy
|
|
590
|
+
if (
|
|
591
|
+
layered_block_policy in ("color", "color_use_delta_trick")
|
|
592
|
+
and self._color_layer_sizes is not None
|
|
593
|
+
):
|
|
594
|
+
# One block per color layer; after applying check_permutation, each layer
|
|
595
|
+
# occupies a contiguous range of checks.
|
|
596
|
+
layer_sizes = self._color_layer_sizes
|
|
597
|
+
num_blocks = int(layer_sizes.shape[0])
|
|
598
|
+
block_chk_start_cpu = np.zeros(num_blocks, dtype=np.int32)
|
|
599
|
+
block_chk_end_cpu = np.zeros(num_blocks, dtype=np.int32)
|
|
600
|
+
offset = 0
|
|
601
|
+
for b in range(num_blocks):
|
|
602
|
+
block_chk_start_cpu[b] = offset
|
|
603
|
+
offset += int(layer_sizes[b])
|
|
604
|
+
block_chk_end_cpu[b] = offset
|
|
605
|
+
|
|
606
|
+
# Build per-block variable lists using the same CSR-based logic as build_block_metadata
|
|
607
|
+
chk_ptr_host = graph0.chk_ptr.cpu().numpy()
|
|
608
|
+
chk_edges_host = graph0.chk_edges.cpu().numpy()
|
|
609
|
+
edge_var_host = graph0.edge_var.cpu().numpy()
|
|
610
|
+
|
|
611
|
+
block_vars_list: List[List[int]] = []
|
|
612
|
+
block_var_ptr_list = [0]
|
|
613
|
+
|
|
614
|
+
for b in range(num_blocks):
|
|
615
|
+
chk_start = int(block_chk_start_cpu[b])
|
|
616
|
+
chk_end = int(block_chk_end_cpu[b])
|
|
617
|
+
|
|
618
|
+
e0 = chk_ptr_host[chk_start]
|
|
619
|
+
e1 = chk_ptr_host[chk_end]
|
|
620
|
+
|
|
621
|
+
if e1 > e0:
|
|
622
|
+
all_edge_ids = chk_edges_host[e0:e1]
|
|
623
|
+
all_var_ids = edge_var_host[all_edge_ids]
|
|
624
|
+
block_vars_sorted = np.unique(all_var_ids).astype(np.int32)
|
|
625
|
+
else:
|
|
626
|
+
block_vars_sorted = np.array([], dtype=np.int32)
|
|
627
|
+
|
|
628
|
+
block_vars_list.append(block_vars_sorted.tolist())
|
|
629
|
+
block_var_ptr_list.append(block_var_ptr_list[-1] + len(block_vars_sorted))
|
|
630
|
+
|
|
631
|
+
total_vars = block_var_ptr_list[-1]
|
|
632
|
+
block_vars_flat = np.zeros(total_vars, dtype=np.int32)
|
|
633
|
+
for b, var_list in enumerate(block_vars_list):
|
|
634
|
+
start = block_var_ptr_list[b]
|
|
635
|
+
end = block_var_ptr_list[b + 1]
|
|
636
|
+
block_vars_flat[start:end] = var_list
|
|
637
|
+
|
|
638
|
+
dev = graph0.chk_ptr.device
|
|
639
|
+
block_chk_start = torch.from_numpy(block_chk_start_cpu).to(dev)
|
|
640
|
+
block_chk_end = torch.from_numpy(block_chk_end_cpu).to(dev)
|
|
641
|
+
block_var_ptr = torch.from_numpy(np.array(block_var_ptr_list, dtype=np.int32)).to(
|
|
642
|
+
dev
|
|
643
|
+
)
|
|
644
|
+
block_vars = torch.from_numpy(block_vars_flat).to(dev)
|
|
645
|
+
|
|
646
|
+
block_var_ptr_cpu = block_var_ptr.cpu().numpy()
|
|
647
|
+
else:
|
|
648
|
+
# Default edge-balanced block construction (contiguous / permute / strided_interleaving)
|
|
649
|
+
block_chk_start, block_chk_end, block_var_ptr, block_vars, num_blocks = (
|
|
650
|
+
graph0.build_block_metadata(
|
|
651
|
+
num_blocks=layered_num_blocks,
|
|
652
|
+
target_edges_per_block=layered_target_edges_per_block,
|
|
653
|
+
)
|
|
654
|
+
)
|
|
655
|
+
block_chk_start_cpu = block_chk_start.cpu().numpy()
|
|
656
|
+
block_chk_end_cpu = block_chk_end.cpu().numpy()
|
|
657
|
+
block_var_ptr_cpu = block_var_ptr.cpu().numpy()
|
|
658
|
+
|
|
659
|
+
# Compute block overlap diagnostics (for debugging/validation)
|
|
660
|
+
block_overlap_stats = self._compute_block_overlap_stats(
|
|
661
|
+
block_chk_start_cpu,
|
|
662
|
+
block_chk_end_cpu,
|
|
663
|
+
block_var_ptr_cpu,
|
|
664
|
+
block_vars.cpu().numpy(),
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
# Log block statistics for validation/optimization
|
|
668
|
+
import logging
|
|
669
|
+
|
|
670
|
+
logger = logging.getLogger(__name__)
|
|
671
|
+
if logger.isEnabledFor(logging.INFO):
|
|
672
|
+
logger.info(
|
|
673
|
+
f"Layered scheduling: {num_blocks} blocks, policy='{layered_block_policy}'"
|
|
674
|
+
)
|
|
675
|
+
logger.info(
|
|
676
|
+
f" Inter-block overlap (adj blocks share): "
|
|
677
|
+
f"avg={block_overlap_stats['avg_overlap']:.3f}, "
|
|
678
|
+
f"min={block_overlap_stats['min_overlap']:.3f}, "
|
|
679
|
+
f"max={block_overlap_stats['max_overlap']:.3f}"
|
|
680
|
+
)
|
|
681
|
+
if "max_intra_block_overlap" in block_overlap_stats:
|
|
682
|
+
logger.info(
|
|
683
|
+
f" Max intra-block overlap (max checks/var within block): "
|
|
684
|
+
f"{block_overlap_stats['max_intra_block_overlap']}"
|
|
685
|
+
)
|
|
686
|
+
# Per-block details at DEBUG level
|
|
687
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
688
|
+
chk_ptr_host = graph0.chk_ptr.cpu().numpy()
|
|
689
|
+
for b in range(num_blocks):
|
|
690
|
+
chk_start = int(block_chk_start_cpu[b])
|
|
691
|
+
chk_end = int(block_chk_end_cpu[b])
|
|
692
|
+
var_start = int(block_var_ptr_cpu[b])
|
|
693
|
+
var_end = int(block_var_ptr_cpu[b + 1])
|
|
694
|
+
e0 = chk_ptr_host[chk_start]
|
|
695
|
+
e1 = chk_ptr_host[chk_end]
|
|
696
|
+
edges_in_block = e1 - e0
|
|
697
|
+
vars_in_block = var_end - var_start
|
|
698
|
+
overlap_ratio = (
|
|
699
|
+
block_overlap_stats["overlap_ratios"][b - 1] if b > 0 else 0.0
|
|
700
|
+
)
|
|
701
|
+
logger.debug(
|
|
702
|
+
f" Block {b}: checks [{chk_start}, {chk_end}), "
|
|
703
|
+
f"edges={edges_in_block}, vars={vars_in_block}, "
|
|
704
|
+
f"overlap_with_prev={overlap_ratio:.3f}"
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
# Store metadata and CPU copies
|
|
708
|
+
self.block_metadata = {
|
|
709
|
+
"block_chk_start": block_chk_start,
|
|
710
|
+
"block_chk_end": block_chk_end,
|
|
711
|
+
"block_var_ptr": block_var_ptr,
|
|
712
|
+
"block_vars": block_vars,
|
|
713
|
+
"num_blocks": num_blocks,
|
|
714
|
+
"block_chk_start_cpu": block_chk_start_cpu,
|
|
715
|
+
"block_chk_end_cpu": block_chk_end_cpu,
|
|
716
|
+
"block_var_ptr_cpu": block_var_ptr_cpu,
|
|
717
|
+
"overlap_stats": block_overlap_stats,
|
|
718
|
+
}
|
|
719
|
+
self._block_chk_start_cpu = block_chk_start_cpu
|
|
720
|
+
self._block_chk_end_cpu = block_chk_end_cpu
|
|
721
|
+
self._block_var_ptr_cpu = block_var_ptr_cpu
|
|
722
|
+
self.block_overlap_stats = block_overlap_stats
|
|
723
|
+
|
|
724
|
+
# Compute log prior ratios (Rust: log_prior_ratios)
|
|
725
|
+
log_ratios = compute_log_prior_ratios(error_priors)
|
|
726
|
+
# Replicate log_prior_ratios on each GPU
|
|
727
|
+
self.log_prior_ratios_per_gpu = []
|
|
728
|
+
for gpu_id in self.device_ids:
|
|
729
|
+
device_str = self._device_strs[gpu_id]
|
|
730
|
+
# Use unified dtype for log_prior_ratios
|
|
731
|
+
log_prior = (
|
|
732
|
+
torch.from_numpy(log_ratios).to(device_str, dtype=self.dtype).unsqueeze(0)
|
|
733
|
+
) # [1, V]
|
|
734
|
+
self.log_prior_ratios_per_gpu.append(log_prior)
|
|
735
|
+
|
|
736
|
+
# Precompute decoding weights for solution selection (matches Rust decoding_quality)
|
|
737
|
+
# Weight = sum of log-priors for error bits, used to select best solutions
|
|
738
|
+
# Rust: decoding_quality += log_prior_ratios[i] when errors[i] == 1 and is_finite(log_prior_ratios[i])
|
|
739
|
+
# log_prior_ratios can be negative when error probability > 0.5
|
|
740
|
+
# Use first GPU's log_prior_ratios (all are identical)
|
|
741
|
+
wj = (
|
|
742
|
+
self.log_prior_ratios_per_gpu[0]
|
|
743
|
+
.squeeze(0)
|
|
744
|
+
.to(self._device_strs[self.device_ids[0]], dtype=self.dtype)
|
|
745
|
+
) # [V]
|
|
746
|
+
# Filter finite values: set non-finite to 0 (matches Rust's is_finite check)
|
|
747
|
+
wj = torch.where(torch.isfinite(wj), wj, torch.zeros_like(wj))
|
|
748
|
+
self.wj = wj.contiguous()
|
|
749
|
+
# Cache wj on each GPU to avoid .to(...) on every selection call
|
|
750
|
+
self.wj_per_gpu: Dict[int, torch.Tensor] = {}
|
|
751
|
+
for gpu_id in self.device_ids:
|
|
752
|
+
device_str = self._device_strs[gpu_id]
|
|
753
|
+
self.wj_per_gpu[gpu_id] = self.wj.to(device_str)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
# Performance configuration (fallback values, overridden by autotuning)
|
|
759
|
+
self.cfg = PerfCfg(
|
|
760
|
+
perf=self.perf,
|
|
761
|
+
msg_dtype=self.dtype_str,
|
|
762
|
+
btile=64, # fallback only; tuned meta overrides
|
|
763
|
+
rows_per_chk=16, # fallback only
|
|
764
|
+
rows_per_var=16, # fallback only
|
|
765
|
+
c2v_block=0,
|
|
766
|
+
c2v_warps=0,
|
|
767
|
+
c2v_stages=0,
|
|
768
|
+
v2c_block=0,
|
|
769
|
+
v2c_warps=0,
|
|
770
|
+
v2c_stages=0,
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
# Set up RNG
|
|
774
|
+
torch.manual_seed(seed)
|
|
775
|
+
|
|
776
|
+
# Pre-allocate device tensors (will be allocated in decode)
|
|
777
|
+
# Per-GPU tensor caches for multi-GPU support
|
|
778
|
+
self._device_tensors_per_gpu = {gpu_id: {} for gpu_id in self.device_ids}
|
|
779
|
+
# Cache dummy L tensor per GPU (used when L is not allocated but kernel requires valid pointer)
|
|
780
|
+
self._dummy_L_per_gpu = {}
|
|
781
|
+
|
|
782
|
+
# Optional logging for cached tuning usage
|
|
783
|
+
self._tune_log = os.getenv("RELAY_TUNE_VERBOSE", "0") == "1"
|
|
784
|
+
self._tune_logged: set[str] = set()
|
|
785
|
+
|
|
786
|
+
def override_llr_columns(self, cols, value):
|
|
787
|
+
"""Override the prior log-likelihood-ratio (LLR) on specific variable cols.
|
|
788
|
+
|
|
789
|
+
The per-variable prior LLR is ln((1-p)/p). Building from an
|
|
790
|
+
error_priors probability cannot express a belief that a fault did
|
|
791
|
+
fire, because priors are clipped to <= 0.5 (LLR >= 0). This method
|
|
792
|
+
writes such values directly into the kernel's init-LLR tensor
|
|
793
|
+
(log_prior_ratios_per_gpu) and the solution-selection weights
|
|
794
|
+
(wj / wj_per_gpu), bypassing the clip. Useful for staged decoding
|
|
795
|
+
schemes that set per-column priors between rounds.
|
|
796
|
+
|
|
797
|
+
Args:
|
|
798
|
+
cols: 1-D int array of variable (column) indices to override.
|
|
799
|
+
value: LLR to write. Negative => biased toward firing. Either a
|
|
800
|
+
scalar (same LLR on all cols) or a length-len(cols) array
|
|
801
|
+
(per-column graded LLRs, e.g. confidence-graded conditioning).
|
|
802
|
+
|
|
803
|
+
Returns:
|
|
804
|
+
A restore-callable that reverts exactly these entries to their
|
|
805
|
+
previous values when invoked (so callers can patch-per-shot then
|
|
806
|
+
restore cheaply, no decoder rebuild).
|
|
807
|
+
"""
|
|
808
|
+
cols = np.asarray(cols, dtype=np.int64)
|
|
809
|
+
# Save all originals before writing anything: self.wj and
|
|
810
|
+
# wj_per_gpu[gpu] may alias the same tensor (single-GPU case), so
|
|
811
|
+
# interleaving save/write would capture already-overwritten values.
|
|
812
|
+
saved_lpr = [
|
|
813
|
+
(gpu_idx, lpr[0, cols].clone())
|
|
814
|
+
for gpu_idx, lpr in enumerate(self.log_prior_ratios_per_gpu)
|
|
815
|
+
]
|
|
816
|
+
saved_wj = {gpu_id: wj[cols].clone() for gpu_id, wj in self.wj_per_gpu.items()}
|
|
817
|
+
saved_self_wj = self.wj[cols].clone()
|
|
818
|
+
|
|
819
|
+
# value may be a scalar (broadcasts) or a length-len(cols) array
|
|
820
|
+
# (per-column LLRs supplied by a caller).
|
|
821
|
+
# Cast per-tensor to match device/dtype before advanced-index assign.
|
|
822
|
+
for gpu_idx, lpr in enumerate(self.log_prior_ratios_per_gpu):
|
|
823
|
+
lpr[0, cols] = torch.as_tensor(value, dtype=lpr.dtype, device=lpr.device)
|
|
824
|
+
for gpu_id, wj in self.wj_per_gpu.items():
|
|
825
|
+
wj[cols] = torch.as_tensor(value, dtype=wj.dtype, device=wj.device)
|
|
826
|
+
self.wj[cols] = torch.as_tensor(value, dtype=self.wj.dtype, device=self.wj.device)
|
|
827
|
+
|
|
828
|
+
def _restore():
|
|
829
|
+
for gpu_idx, old in saved_lpr:
|
|
830
|
+
self.log_prior_ratios_per_gpu[gpu_idx][0, cols] = old
|
|
831
|
+
for gpu_id, old in saved_wj.items():
|
|
832
|
+
self.wj_per_gpu[gpu_id][cols] = old
|
|
833
|
+
self.wj[cols] = saved_self_wj
|
|
834
|
+
|
|
835
|
+
return _restore
|
|
836
|
+
|
|
837
|
+
def _compute_block_overlap_stats(
|
|
838
|
+
self, block_chk_start, block_chk_end, block_var_ptr, block_vars
|
|
839
|
+
):
|
|
840
|
+
"""Compute block overlap statistics for diagnostics.
|
|
841
|
+
|
|
842
|
+
Returns:
|
|
843
|
+
dict with overlap metrics:
|
|
844
|
+
- var_counts: list of variable counts per block
|
|
845
|
+
- overlap_ratios: list of overlap ratios between adjacent blocks
|
|
846
|
+
- avg_overlap: average overlap ratio
|
|
847
|
+
- max_intra_block_overlap: maximum checks-per-variable within any single block
|
|
848
|
+
"""
|
|
849
|
+
num_blocks = len(block_chk_start)
|
|
850
|
+
var_counts = []
|
|
851
|
+
overlap_ratios = []
|
|
852
|
+
|
|
853
|
+
# Get variable sets for each block
|
|
854
|
+
block_var_sets = []
|
|
855
|
+
for b in range(num_blocks):
|
|
856
|
+
var_start = int(block_var_ptr[b])
|
|
857
|
+
var_end = int(block_var_ptr[b + 1])
|
|
858
|
+
var_set = set(block_vars[var_start:var_end])
|
|
859
|
+
block_var_sets.append(var_set)
|
|
860
|
+
var_counts.append(len(var_set))
|
|
861
|
+
|
|
862
|
+
# Compute max intra-block overlap (max checks per variable within a block)
|
|
863
|
+
max_intra = 0
|
|
864
|
+
if hasattr(self, "graphs") and self.graphs:
|
|
865
|
+
chk_ptr_host = self.graphs[0].chk_ptr.cpu().numpy().astype(np.int32)
|
|
866
|
+
edge_var_host = self.graphs[0].edge_var.cpu().numpy().astype(np.int32)
|
|
867
|
+
for b in range(num_blocks):
|
|
868
|
+
chk_start = int(block_chk_start[b])
|
|
869
|
+
chk_end = int(block_chk_end[b])
|
|
870
|
+
var_counts_in_block: Dict[int, int] = {}
|
|
871
|
+
for c in range(chk_start, chk_end):
|
|
872
|
+
e0 = chk_ptr_host[c]
|
|
873
|
+
e1 = chk_ptr_host[c + 1]
|
|
874
|
+
for e in range(e0, e1):
|
|
875
|
+
v = int(edge_var_host[e])
|
|
876
|
+
var_counts_in_block[v] = var_counts_in_block.get(v, 0) + 1
|
|
877
|
+
if var_counts_in_block:
|
|
878
|
+
local_max = max(var_counts_in_block.values())
|
|
879
|
+
if local_max > max_intra:
|
|
880
|
+
max_intra = local_max
|
|
881
|
+
|
|
882
|
+
# Compute overlap between adjacent blocks
|
|
883
|
+
for b in range(num_blocks - 1):
|
|
884
|
+
var_set_i = block_var_sets[b]
|
|
885
|
+
var_set_j = block_var_sets[b + 1]
|
|
886
|
+
intersection = var_set_i & var_set_j
|
|
887
|
+
min_size = min(len(var_set_i), len(var_set_j))
|
|
888
|
+
if min_size > 0:
|
|
889
|
+
overlap_ratio = len(intersection) / min_size
|
|
890
|
+
else:
|
|
891
|
+
overlap_ratio = 0.0
|
|
892
|
+
overlap_ratios.append(overlap_ratio)
|
|
893
|
+
|
|
894
|
+
avg_overlap = np.mean(overlap_ratios) if overlap_ratios else 0.0
|
|
895
|
+
|
|
896
|
+
return {
|
|
897
|
+
"var_counts": var_counts,
|
|
898
|
+
"overlap_ratios": overlap_ratios,
|
|
899
|
+
"avg_overlap": float(avg_overlap),
|
|
900
|
+
"min_overlap": float(min(overlap_ratios)) if overlap_ratios else 0.0,
|
|
901
|
+
"max_overlap": float(max(overlap_ratios)) if overlap_ratios else 0.0,
|
|
902
|
+
"max_intra_block_overlap": int(max_intra),
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
def _get_tuned_meta(
|
|
906
|
+
self,
|
|
907
|
+
*,
|
|
908
|
+
kernel_name: str,
|
|
909
|
+
problem_key: Dict[str, Any],
|
|
910
|
+
build_cfgs_fn,
|
|
911
|
+
kernel,
|
|
912
|
+
grid,
|
|
913
|
+
args: Tuple,
|
|
914
|
+
meta_base: Dict[str, Any],
|
|
915
|
+
number: int = 20,
|
|
916
|
+
grid_fn=None,
|
|
917
|
+
log_name: Optional[str] = None,
|
|
918
|
+
) -> Dict[str, Any]:
|
|
919
|
+
"""Shared helper for cache-based autotuning.
|
|
920
|
+
|
|
921
|
+
Wraps the common pattern:
|
|
922
|
+
- try_get_saved
|
|
923
|
+
- bench_and_select if missing
|
|
924
|
+
- set_saved
|
|
925
|
+
- optional one-time logging via self._tune_log / self._tune_logged
|
|
926
|
+
"""
|
|
927
|
+
saved = ac.try_get_saved(kernel_name, problem_key)
|
|
928
|
+
if saved is None:
|
|
929
|
+
cfgs = [
|
|
930
|
+
dict(c.kwargs, num_warps=c.num_warps, num_stages=c.num_stages)
|
|
931
|
+
for c in build_cfgs_fn()
|
|
932
|
+
]
|
|
933
|
+
# Tune on scratch clones so benchmarking is side-effect free.
|
|
934
|
+
bench_args, bench_meta = _clone_bench_state(args, meta_base)
|
|
935
|
+
best = ac.bench_and_select(
|
|
936
|
+
kernel=kernel,
|
|
937
|
+
grid=grid,
|
|
938
|
+
args=bench_args,
|
|
939
|
+
meta_base=bench_meta,
|
|
940
|
+
configs=cfgs,
|
|
941
|
+
number=number,
|
|
942
|
+
grid_fn=grid_fn,
|
|
943
|
+
)
|
|
944
|
+
del bench_args, bench_meta
|
|
945
|
+
ac.set_saved(kernel_name, problem_key, best)
|
|
946
|
+
saved = best
|
|
947
|
+
|
|
948
|
+
name = log_name or kernel_name
|
|
949
|
+
if self._tune_log and name not in self._tune_logged:
|
|
950
|
+
config_summary = {}
|
|
951
|
+
for key in [
|
|
952
|
+
"BLOCK_SIZE",
|
|
953
|
+
"ROWS_PER_CHK",
|
|
954
|
+
"ROWS_PER_VAR",
|
|
955
|
+
"BTILE",
|
|
956
|
+
"BLOCK_C",
|
|
957
|
+
"BLOCK_V",
|
|
958
|
+
"num_warps",
|
|
959
|
+
"num_stages",
|
|
960
|
+
]:
|
|
961
|
+
if key in saved:
|
|
962
|
+
config_summary[key] = saved[key]
|
|
963
|
+
config_str = f" config={config_summary}" if config_summary else ""
|
|
964
|
+
print(
|
|
965
|
+
f"[relay-bp-triton][{name}] Using cached meta from {ac.DEFAULT_CACHE} key={problem_key}{config_str}"
|
|
966
|
+
)
|
|
967
|
+
self._tune_logged.add(name)
|
|
968
|
+
|
|
969
|
+
return saved
|
|
970
|
+
|
|
971
|
+
def _resolve_configs(self, B, tensors, graph, gpu_idx):
|
|
972
|
+
"""Resolve all autotuning configs once per decode call."""
|
|
973
|
+
device_str = self._device_strs[self.device_ids[gpu_idx]]
|
|
974
|
+
log_prior_ratios = self.log_prior_ratios_per_gpu[gpu_idx]
|
|
975
|
+
alpha = self.normalized_min_sum_alpha or 0.0
|
|
976
|
+
beta = self.offset_min_sum_beta or 0.0
|
|
977
|
+
use_alpha = self.normalized_min_sum_alpha is not None
|
|
978
|
+
use_beta = self.offset_min_sum_beta is not None
|
|
979
|
+
|
|
980
|
+
# Full-batch active index for benchmarking
|
|
981
|
+
active_idx = torch.arange(B, device=device_str, dtype=torch.int32)
|
|
982
|
+
B_active = B
|
|
983
|
+
|
|
984
|
+
if self.cfg.perf == "throughput":
|
|
985
|
+
self._resolve_throughput_configs(
|
|
986
|
+
B,
|
|
987
|
+
B_active,
|
|
988
|
+
active_idx,
|
|
989
|
+
tensors,
|
|
990
|
+
graph,
|
|
991
|
+
gpu_idx,
|
|
992
|
+
device_str,
|
|
993
|
+
log_prior_ratios,
|
|
994
|
+
alpha,
|
|
995
|
+
beta,
|
|
996
|
+
use_alpha,
|
|
997
|
+
use_beta,
|
|
998
|
+
)
|
|
999
|
+
else:
|
|
1000
|
+
self._resolve_default_configs(
|
|
1001
|
+
B,
|
|
1002
|
+
tensors,
|
|
1003
|
+
graph,
|
|
1004
|
+
gpu_idx,
|
|
1005
|
+
device_str,
|
|
1006
|
+
log_prior_ratios,
|
|
1007
|
+
alpha,
|
|
1008
|
+
beta,
|
|
1009
|
+
use_alpha,
|
|
1010
|
+
use_beta,
|
|
1011
|
+
)
|
|
1012
|
+
|
|
1013
|
+
def _resolve_throughput_configs(
|
|
1014
|
+
self,
|
|
1015
|
+
B,
|
|
1016
|
+
B_active,
|
|
1017
|
+
active_idx,
|
|
1018
|
+
tensors,
|
|
1019
|
+
graph,
|
|
1020
|
+
gpu_idx,
|
|
1021
|
+
device_str,
|
|
1022
|
+
log_prior_ratios,
|
|
1023
|
+
alpha,
|
|
1024
|
+
beta,
|
|
1025
|
+
use_alpha,
|
|
1026
|
+
use_beta,
|
|
1027
|
+
):
|
|
1028
|
+
"""Resolve autotuning configs for throughput (btile) mode."""
|
|
1029
|
+
# --- C2V btile ---
|
|
1030
|
+
c2v_key = {
|
|
1031
|
+
"B": B,
|
|
1032
|
+
"C": self.C,
|
|
1033
|
+
"E": self.E,
|
|
1034
|
+
"dtype": self.dtype_str,
|
|
1035
|
+
"sparsity_hash": graph.sparsity_hash,
|
|
1036
|
+
}
|
|
1037
|
+
dummy_L = self._dummy_L_per_gpu.get(gpu_idx)
|
|
1038
|
+
if dummy_L is None:
|
|
1039
|
+
dummy_L = torch.zeros(1, 1, dtype=self.dtype, device=device_str)
|
|
1040
|
+
self._dummy_L_per_gpu[gpu_idx] = dummy_L
|
|
1041
|
+
|
|
1042
|
+
def _c2v_grid_fn(cfg):
|
|
1043
|
+
bt = int(cfg.get("BTILE", self.cfg.btile))
|
|
1044
|
+
return (self.C, (B_active + bt - 1) // bt)
|
|
1045
|
+
|
|
1046
|
+
saved = self._get_tuned_meta(
|
|
1047
|
+
kernel_name="c2v_min_sum_btile_kernel",
|
|
1048
|
+
problem_key=c2v_key,
|
|
1049
|
+
build_cfgs_fn=build_c2v_btile_configs,
|
|
1050
|
+
kernel=c2v_min_sum_btile_kernel,
|
|
1051
|
+
grid=(self.C, 1),
|
|
1052
|
+
args=(
|
|
1053
|
+
tensors["muT"],
|
|
1054
|
+
tensors["nuT"],
|
|
1055
|
+
graph.chk_ptr,
|
|
1056
|
+
graph.chk_edges,
|
|
1057
|
+
tensors["syndrome"],
|
|
1058
|
+
active_idx,
|
|
1059
|
+
tensors["active"],
|
|
1060
|
+
),
|
|
1061
|
+
meta_base=dict(
|
|
1062
|
+
B=B,
|
|
1063
|
+
B_active=B_active,
|
|
1064
|
+
C=self.C,
|
|
1065
|
+
E=self.E,
|
|
1066
|
+
alpha=alpha,
|
|
1067
|
+
beta=beta,
|
|
1068
|
+
use_alpha=use_alpha,
|
|
1069
|
+
use_beta=use_beta,
|
|
1070
|
+
dtype_enum=self._dtype_enum,
|
|
1071
|
+
chk_start=0,
|
|
1072
|
+
chk_end=0,
|
|
1073
|
+
edge_var=graph.edge_var,
|
|
1074
|
+
L=dummy_L,
|
|
1075
|
+
V=self.V,
|
|
1076
|
+
use_delta=False,
|
|
1077
|
+
),
|
|
1078
|
+
number=20,
|
|
1079
|
+
grid_fn=_c2v_grid_fn,
|
|
1080
|
+
log_name=f"c2v_min_sum_btile_kernel[gpu={gpu_idx}]",
|
|
1081
|
+
)
|
|
1082
|
+
self._rc_c2v_btile = int(saved.get("BTILE", self.cfg.btile))
|
|
1083
|
+
self._rc_c2v_block = int(saved.get("BLOCK_SIZE", 64))
|
|
1084
|
+
self._rc_c2v_warps = int(saved.get("num_warps", 0))
|
|
1085
|
+
self._rc_c2v_stages = int(saved.get("num_stages", 0))
|
|
1086
|
+
|
|
1087
|
+
# --- V2C btile ---
|
|
1088
|
+
v2c_key = {
|
|
1089
|
+
"B": B,
|
|
1090
|
+
"V": self.V,
|
|
1091
|
+
"E": self.E,
|
|
1092
|
+
"dtype": self.dtype_str,
|
|
1093
|
+
"WRITE_HARD": True,
|
|
1094
|
+
"sparsity_hash": graph.sparsity_hash,
|
|
1095
|
+
"BLOCK_SIZE": V2C_BTILE_BLOCK_SIZE,
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
def _v2c_grid_fn(cfg):
|
|
1099
|
+
bt = int(cfg.get("BTILE", self.cfg.btile))
|
|
1100
|
+
return (self.V, (B_active + bt - 1) // bt)
|
|
1101
|
+
|
|
1102
|
+
saved = self._get_tuned_meta(
|
|
1103
|
+
kernel_name="v2c_and_gamma_btile_kernel",
|
|
1104
|
+
problem_key=v2c_key,
|
|
1105
|
+
build_cfgs_fn=build_v2c_btile_configs,
|
|
1106
|
+
kernel=v2c_and_gamma_btile_kernel,
|
|
1107
|
+
grid=(self.V, 1),
|
|
1108
|
+
args=(
|
|
1109
|
+
tensors["nuT"],
|
|
1110
|
+
tensors["muT"],
|
|
1111
|
+
graph.var_ptr,
|
|
1112
|
+
graph.var_edges,
|
|
1113
|
+
tensors["lambda_"],
|
|
1114
|
+
log_prior_ratios,
|
|
1115
|
+
tensors["gamma"],
|
|
1116
|
+
tensors["posterior_ratios"],
|
|
1117
|
+
tensors["hard_dec"],
|
|
1118
|
+
active_idx,
|
|
1119
|
+
tensors["active"],
|
|
1120
|
+
),
|
|
1121
|
+
meta_base=dict(
|
|
1122
|
+
B=B,
|
|
1123
|
+
B_active=B_active,
|
|
1124
|
+
V=self.V,
|
|
1125
|
+
E=self.E,
|
|
1126
|
+
WRITE_HARD=True,
|
|
1127
|
+
dtype_enum=self._dtype_enum,
|
|
1128
|
+
block_vars=(
|
|
1129
|
+
graph.var_edges[:1] if graph.var_edges.shape[0] > 0 else graph.var_ptr[:1]
|
|
1130
|
+
),
|
|
1131
|
+
block_var_ptr=graph.var_ptr[:2],
|
|
1132
|
+
block_idx=-1,
|
|
1133
|
+
),
|
|
1134
|
+
number=20,
|
|
1135
|
+
grid_fn=_v2c_grid_fn,
|
|
1136
|
+
log_name=f"v2c_and_gamma_btile_kernel[gpu={gpu_idx}]",
|
|
1137
|
+
)
|
|
1138
|
+
self._rc_v2c_btile = int(saved.get("BTILE", self.cfg.btile))
|
|
1139
|
+
# Fixed, not tuned: see autotune.V2C_BTILE_BLOCK_SIZE.
|
|
1140
|
+
self._rc_v2c_block = V2C_BTILE_BLOCK_SIZE
|
|
1141
|
+
self._rc_v2c_warps = int(saved.get("num_warps", 0))
|
|
1142
|
+
self._rc_v2c_stages = int(saved.get("num_stages", 0))
|
|
1143
|
+
|
|
1144
|
+
# --- Parity (shared by compact_with_zero and compact kernels) ---
|
|
1145
|
+
parity_key = {
|
|
1146
|
+
"B": B,
|
|
1147
|
+
"C": self.C,
|
|
1148
|
+
"V": self.V,
|
|
1149
|
+
"E": self.E,
|
|
1150
|
+
"ROWS_PER_CHK": self.cfg.rows_per_chk,
|
|
1151
|
+
"dtype": self.dtype_str,
|
|
1152
|
+
"sparsity_hash": graph.sparsity_hash,
|
|
1153
|
+
}
|
|
1154
|
+
parity_meta = dict(B=B, C=self.C, V=self.V, E=self.E, ROWS_PER_CHK=self.cfg.rows_per_chk)
|
|
1155
|
+
total = B * self.C
|
|
1156
|
+
parity_grid = ((total + self.cfg.rows_per_chk - 1) // self.cfg.rows_per_chk,)
|
|
1157
|
+
|
|
1158
|
+
saved = self._get_tuned_meta(
|
|
1159
|
+
kernel_name="parity_from_hard_compact_with_zero_kernel",
|
|
1160
|
+
problem_key=parity_key,
|
|
1161
|
+
build_cfgs_fn=build_parity_configs,
|
|
1162
|
+
kernel=parity_from_hard_compact_with_zero_kernel,
|
|
1163
|
+
grid=parity_grid,
|
|
1164
|
+
args=(
|
|
1165
|
+
tensors["hard_dec"],
|
|
1166
|
+
graph.chk_ptr,
|
|
1167
|
+
graph.chk_edges,
|
|
1168
|
+
graph.edge_var,
|
|
1169
|
+
tensors["syndrome"],
|
|
1170
|
+
tensors["check_ok"],
|
|
1171
|
+
tensors["active"],
|
|
1172
|
+
),
|
|
1173
|
+
meta_base=parity_meta,
|
|
1174
|
+
number=20,
|
|
1175
|
+
log_name=f"parity_from_hard_compact_with_zero_kernel[gpu={gpu_idx}]",
|
|
1176
|
+
)
|
|
1177
|
+
self._rc_parity_block = int(saved["BLOCK_SIZE"])
|
|
1178
|
+
self._rc_parity_warps = int(saved.get("num_warps", 0))
|
|
1179
|
+
self._rc_parity_stages = int(saved.get("num_stages", 0))
|
|
1180
|
+
|
|
1181
|
+
# --- Select ---
|
|
1182
|
+
gpu_id = self.device_ids[gpu_idx]
|
|
1183
|
+
wj_gpu = self.wj_per_gpu[gpu_id]
|
|
1184
|
+
select_key = {
|
|
1185
|
+
"B": B,
|
|
1186
|
+
"C": self.C,
|
|
1187
|
+
"V": self.V,
|
|
1188
|
+
"dtype": self.dtype_str,
|
|
1189
|
+
"sparsity_hash": graph.sparsity_hash,
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
saved = self._get_tuned_meta(
|
|
1193
|
+
kernel_name="check_and_select_kernel",
|
|
1194
|
+
problem_key=select_key,
|
|
1195
|
+
build_cfgs_fn=build_check_select_configs,
|
|
1196
|
+
kernel=check_and_select_kernel,
|
|
1197
|
+
grid=(B,),
|
|
1198
|
+
args=(
|
|
1199
|
+
tensors["check_ok"],
|
|
1200
|
+
tensors["posterior_ratios"],
|
|
1201
|
+
tensors["hard_dec"],
|
|
1202
|
+
wj_gpu,
|
|
1203
|
+
tensors["best_weights"],
|
|
1204
|
+
tensors["best_errors"],
|
|
1205
|
+
tensors["valid_solutions"],
|
|
1206
|
+
tensors["found_count"],
|
|
1207
|
+
tensors["active"],
|
|
1208
|
+
),
|
|
1209
|
+
meta_base=dict(
|
|
1210
|
+
B=B,
|
|
1211
|
+
C=self.C,
|
|
1212
|
+
V=self.V,
|
|
1213
|
+
dtype_enum=self._dtype_enum,
|
|
1214
|
+
NUM_SOLS=int(self.stop_nconv),
|
|
1215
|
+
),
|
|
1216
|
+
number=20,
|
|
1217
|
+
grid_fn=lambda _cfg: (B,),
|
|
1218
|
+
log_name=f"check_and_select_kernel[gpu={gpu_idx}]",
|
|
1219
|
+
)
|
|
1220
|
+
self._rc_select_block_c = int(saved.get("BLOCK_C", 256))
|
|
1221
|
+
self._rc_select_block_v = int(saved.get("BLOCK_V", 128))
|
|
1222
|
+
self._rc_select_warps = int(saved.get("num_warps", 1))
|
|
1223
|
+
|
|
1224
|
+
def _resolve_default_configs(
|
|
1225
|
+
self,
|
|
1226
|
+
B,
|
|
1227
|
+
tensors,
|
|
1228
|
+
graph,
|
|
1229
|
+
gpu_idx,
|
|
1230
|
+
device_str,
|
|
1231
|
+
log_prior_ratios,
|
|
1232
|
+
alpha,
|
|
1233
|
+
beta,
|
|
1234
|
+
use_alpha,
|
|
1235
|
+
use_beta,
|
|
1236
|
+
):
|
|
1237
|
+
"""Resolve autotuning configs for default (row-wise) mode."""
|
|
1238
|
+
# --- C2V ---
|
|
1239
|
+
c2v_key = {
|
|
1240
|
+
"B": B,
|
|
1241
|
+
"C": self.C,
|
|
1242
|
+
"E": self.E,
|
|
1243
|
+
"dtype": self.dtype_str,
|
|
1244
|
+
"sparsity_hash": graph.sparsity_hash,
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
def _c2v_grid_fn(cfg):
|
|
1248
|
+
rows = int(cfg.get("ROWS_PER_CHK", self.cfg.rows_per_chk))
|
|
1249
|
+
return ((B * self.C + rows - 1) // rows,)
|
|
1250
|
+
|
|
1251
|
+
saved = self._get_tuned_meta(
|
|
1252
|
+
kernel_name="c2v_min_sum_kernel",
|
|
1253
|
+
problem_key=c2v_key,
|
|
1254
|
+
build_cfgs_fn=build_c2v_configs,
|
|
1255
|
+
kernel=c2v_min_sum_kernel,
|
|
1256
|
+
grid=(1,),
|
|
1257
|
+
args=(
|
|
1258
|
+
tensors["mu"],
|
|
1259
|
+
tensors["nu"],
|
|
1260
|
+
graph.chk_ptr,
|
|
1261
|
+
graph.chk_edges,
|
|
1262
|
+
tensors["syndrome"],
|
|
1263
|
+
tensors["active"],
|
|
1264
|
+
),
|
|
1265
|
+
meta_base=dict(
|
|
1266
|
+
B=B,
|
|
1267
|
+
C=self.C,
|
|
1268
|
+
E=self.E,
|
|
1269
|
+
alpha=alpha,
|
|
1270
|
+
beta=beta,
|
|
1271
|
+
use_alpha=use_alpha,
|
|
1272
|
+
use_beta=use_beta,
|
|
1273
|
+
dtype_enum=self._dtype_enum,
|
|
1274
|
+
),
|
|
1275
|
+
number=20,
|
|
1276
|
+
grid_fn=_c2v_grid_fn,
|
|
1277
|
+
log_name=f"c2v_min_sum_kernel[gpu={gpu_idx}]",
|
|
1278
|
+
)
|
|
1279
|
+
self._rc_c2v_rows = int(saved.get("ROWS_PER_CHK", self.cfg.rows_per_chk))
|
|
1280
|
+
self._rc_c2v_block = int(saved.get("BLOCK_SIZE", 64))
|
|
1281
|
+
self._rc_c2v_warps = int(saved.get("num_warps", 0))
|
|
1282
|
+
self._rc_c2v_stages = int(saved.get("num_stages", 0))
|
|
1283
|
+
|
|
1284
|
+
# --- V2C ---
|
|
1285
|
+
v2c_key = {
|
|
1286
|
+
"B": B,
|
|
1287
|
+
"V": self.V,
|
|
1288
|
+
"E": self.E,
|
|
1289
|
+
"dtype": self.dtype_str,
|
|
1290
|
+
"sparsity_hash": graph.sparsity_hash,
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
def _v2c_grid_fn(cfg):
|
|
1294
|
+
rows = int(cfg.get("ROWS_PER_VAR", self.cfg.rows_per_var))
|
|
1295
|
+
return ((B * self.V + rows - 1) // rows,)
|
|
1296
|
+
|
|
1297
|
+
saved = self._get_tuned_meta(
|
|
1298
|
+
kernel_name="v2c_and_marginals_fused_gamma_kernel",
|
|
1299
|
+
problem_key=v2c_key,
|
|
1300
|
+
build_cfgs_fn=build_v2c_configs,
|
|
1301
|
+
kernel=v2c_and_marginals_fused_gamma_kernel,
|
|
1302
|
+
grid=(1,),
|
|
1303
|
+
args=(
|
|
1304
|
+
tensors["nu"],
|
|
1305
|
+
tensors["mu"],
|
|
1306
|
+
graph.var_ptr,
|
|
1307
|
+
graph.var_edges,
|
|
1308
|
+
tensors["lambda_"],
|
|
1309
|
+
tensors["posterior_ratios"],
|
|
1310
|
+
tensors["hard_dec"],
|
|
1311
|
+
tensors["active"],
|
|
1312
|
+
log_prior_ratios,
|
|
1313
|
+
tensors["gamma"],
|
|
1314
|
+
),
|
|
1315
|
+
meta_base=dict(B=B, V=self.V, E=self.E, dtype_enum=self._dtype_enum),
|
|
1316
|
+
number=20,
|
|
1317
|
+
grid_fn=_v2c_grid_fn,
|
|
1318
|
+
log_name=f"v2c_and_marginals_fused_gamma_kernel[gpu={gpu_idx}]",
|
|
1319
|
+
)
|
|
1320
|
+
self._rc_v2c_rows = int(saved.get("ROWS_PER_VAR", self.cfg.rows_per_var))
|
|
1321
|
+
self._rc_v2c_block = int(saved.get("BLOCK_SIZE", 64))
|
|
1322
|
+
self._rc_v2c_warps = int(saved.get("num_warps", 0))
|
|
1323
|
+
self._rc_v2c_stages = int(saved.get("num_stages", 0))
|
|
1324
|
+
|
|
1325
|
+
# --- Parity ---
|
|
1326
|
+
parity_key = {
|
|
1327
|
+
"B": B,
|
|
1328
|
+
"C": self.C,
|
|
1329
|
+
"V": self.V,
|
|
1330
|
+
"E": self.E,
|
|
1331
|
+
"ROWS_PER_CHK": self.cfg.rows_per_chk,
|
|
1332
|
+
"dtype": self.dtype_str,
|
|
1333
|
+
"sparsity_hash": graph.sparsity_hash,
|
|
1334
|
+
}
|
|
1335
|
+
parity_meta = dict(B=B, C=self.C, V=self.V, E=self.E, ROWS_PER_CHK=self.cfg.rows_per_chk)
|
|
1336
|
+
parity_grid = ((B * self.C + self.cfg.rows_per_chk - 1) // self.cfg.rows_per_chk,)
|
|
1337
|
+
|
|
1338
|
+
saved = self._get_tuned_meta(
|
|
1339
|
+
kernel_name="parity_per_check_kernel",
|
|
1340
|
+
problem_key=parity_key,
|
|
1341
|
+
build_cfgs_fn=build_parity_configs,
|
|
1342
|
+
kernel=parity_per_check_kernel,
|
|
1343
|
+
grid=parity_grid,
|
|
1344
|
+
args=(
|
|
1345
|
+
tensors["posterior_ratios"],
|
|
1346
|
+
graph.chk_ptr,
|
|
1347
|
+
graph.chk_edges,
|
|
1348
|
+
graph.edge_var,
|
|
1349
|
+
tensors["syndrome"],
|
|
1350
|
+
tensors["check_ok"],
|
|
1351
|
+
),
|
|
1352
|
+
meta_base=parity_meta,
|
|
1353
|
+
number=20,
|
|
1354
|
+
log_name=f"parity_per_check_kernel[gpu={gpu_idx}]",
|
|
1355
|
+
)
|
|
1356
|
+
self._rc_parity_block = int(saved["BLOCK_SIZE"])
|
|
1357
|
+
self._rc_parity_warps = int(saved.get("num_warps", 0))
|
|
1358
|
+
self._rc_parity_stages = int(saved.get("num_stages", 0))
|
|
1359
|
+
|
|
1360
|
+
# --- Select (shared config for both modes) ---
|
|
1361
|
+
gpu_id = self.device_ids[gpu_idx]
|
|
1362
|
+
wj_gpu = self.wj_per_gpu[gpu_id]
|
|
1363
|
+
select_key = {
|
|
1364
|
+
"B": B,
|
|
1365
|
+
"C": self.C,
|
|
1366
|
+
"V": self.V,
|
|
1367
|
+
"dtype": self.dtype_str,
|
|
1368
|
+
"sparsity_hash": graph.sparsity_hash,
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
saved = self._get_tuned_meta(
|
|
1372
|
+
kernel_name="check_and_select_kernel",
|
|
1373
|
+
problem_key=select_key,
|
|
1374
|
+
build_cfgs_fn=build_check_select_configs,
|
|
1375
|
+
kernel=check_and_select_kernel,
|
|
1376
|
+
grid=(B,),
|
|
1377
|
+
args=(
|
|
1378
|
+
tensors["check_ok"],
|
|
1379
|
+
tensors["posterior_ratios"],
|
|
1380
|
+
tensors["hard_dec"],
|
|
1381
|
+
wj_gpu,
|
|
1382
|
+
tensors["best_weights"],
|
|
1383
|
+
tensors["best_errors"],
|
|
1384
|
+
tensors["valid_solutions"],
|
|
1385
|
+
tensors["found_count"],
|
|
1386
|
+
tensors["active"],
|
|
1387
|
+
),
|
|
1388
|
+
meta_base=dict(
|
|
1389
|
+
B=B,
|
|
1390
|
+
C=self.C,
|
|
1391
|
+
V=self.V,
|
|
1392
|
+
dtype_enum=self._dtype_enum,
|
|
1393
|
+
NUM_SOLS=int(self.stop_nconv),
|
|
1394
|
+
),
|
|
1395
|
+
number=20,
|
|
1396
|
+
grid_fn=lambda _cfg: (B,),
|
|
1397
|
+
log_name=f"check_and_select_kernel[gpu={gpu_idx}]",
|
|
1398
|
+
)
|
|
1399
|
+
self._rc_select_block_c = int(saved.get("BLOCK_C", 256))
|
|
1400
|
+
self._rc_select_block_v = int(saved.get("BLOCK_V", 128))
|
|
1401
|
+
self._rc_select_warps = int(saved.get("num_warps", 1))
|
|
1402
|
+
|
|
1403
|
+
def _parse_device_spec(self, device: Union[str, List[int], int]) -> List[int]:
|
|
1404
|
+
"""Parse device specification into list of device IDs.
|
|
1405
|
+
|
|
1406
|
+
Supports:
|
|
1407
|
+
- List[int]: Explicit device IDs, e.g. [0, 1, 2, 3]
|
|
1408
|
+
- int: Number of GPUs to auto-detect, e.g. 4 → [0, 1, 2, 3]
|
|
1409
|
+
- str: Legacy support - "auto" or "cuda" → [0] (single GPU)
|
|
1410
|
+
|
|
1411
|
+
Args:
|
|
1412
|
+
device: Device specification
|
|
1413
|
+
|
|
1414
|
+
Returns:
|
|
1415
|
+
List of device IDs
|
|
1416
|
+
"""
|
|
1417
|
+
if isinstance(device, int):
|
|
1418
|
+
# Auto-detect N GPUs
|
|
1419
|
+
num_gpus = device
|
|
1420
|
+
if num_gpus < 1:
|
|
1421
|
+
raise ValueError(f"Number of GPUs must be >= 1, got {num_gpus}")
|
|
1422
|
+
available = torch.cuda.device_count()
|
|
1423
|
+
if num_gpus > available:
|
|
1424
|
+
raise ValueError(f"Requested {num_gpus} GPUs but only {available} available")
|
|
1425
|
+
return list(range(num_gpus))
|
|
1426
|
+
|
|
1427
|
+
elif isinstance(device, list):
|
|
1428
|
+
# Explicit list of device IDs
|
|
1429
|
+
if not device:
|
|
1430
|
+
raise ValueError("Device list cannot be empty")
|
|
1431
|
+
available = torch.cuda.device_count()
|
|
1432
|
+
for gpu_id in device:
|
|
1433
|
+
if not isinstance(gpu_id, int):
|
|
1434
|
+
raise ValueError(f"Device IDs must be integers, got {type(gpu_id)}")
|
|
1435
|
+
if gpu_id < 0 or gpu_id >= available:
|
|
1436
|
+
raise ValueError(f"Device ID {gpu_id} out of range [0, {available - 1}]")
|
|
1437
|
+
return device
|
|
1438
|
+
|
|
1439
|
+
elif isinstance(device, str):
|
|
1440
|
+
# Legacy string support: "auto" or "cuda" → single GPU [0]
|
|
1441
|
+
dev_arg = device.strip().lower()
|
|
1442
|
+
if dev_arg in ("auto", "cuda", "rocm"):
|
|
1443
|
+
return [0] # Single GPU
|
|
1444
|
+
else:
|
|
1445
|
+
raise ValueError(
|
|
1446
|
+
f"Unsupported device string '{device}'. Use List[int], int, or 'auto'/'cuda'."
|
|
1447
|
+
)
|
|
1448
|
+
|
|
1449
|
+
else:
|
|
1450
|
+
raise ValueError(
|
|
1451
|
+
f"Invalid device type: {type(device)}. Expected List[int], int, or str."
|
|
1452
|
+
)
|
|
1453
|
+
|
|
1454
|
+
def _allocate_device_tensors(self, B: int, gpu_id: int):
|
|
1455
|
+
"""Allocate device tensors for batch size B on specific GPU, resetting if reused.
|
|
1456
|
+
|
|
1457
|
+
Args:
|
|
1458
|
+
B: Batch size
|
|
1459
|
+
gpu_id: GPU device ID
|
|
1460
|
+
|
|
1461
|
+
Returns:
|
|
1462
|
+
Tensors dictionary on specified GPU
|
|
1463
|
+
"""
|
|
1464
|
+
device_str = self._device_strs[gpu_id]
|
|
1465
|
+
|
|
1466
|
+
# Safety: Check for integer overflow in E * B calculation
|
|
1467
|
+
# Triton uses int32 for pointer arithmetic, so E * B must not exceed int32 max
|
|
1468
|
+
# This prevents illegal memory access from overflow in kernels that compute offsets like evec * B + bs
|
|
1469
|
+
INT32_MAX = 2**31 - 1
|
|
1470
|
+
if self.E > 0 and B > 0:
|
|
1471
|
+
max_safe_B = INT32_MAX // self.E
|
|
1472
|
+
if B > max_safe_B:
|
|
1473
|
+
raise RuntimeError(
|
|
1474
|
+
f"Batch size {B} exceeds int32 overflow limit for E={self.E}: "
|
|
1475
|
+
f"E * B = {self.E * B:,} > int32 max ({INT32_MAX:,}). "
|
|
1476
|
+
f"Maximum safe batch size: {max_safe_B}."
|
|
1477
|
+
)
|
|
1478
|
+
|
|
1479
|
+
# Check if we're reusing existing tensors
|
|
1480
|
+
if B in self._device_tensors_per_gpu[gpu_id]:
|
|
1481
|
+
tensors = self._device_tensors_per_gpu[gpu_id][B]
|
|
1482
|
+
# Reset stateful tensors when reusing
|
|
1483
|
+
self._reset_reused_tensors(tensors)
|
|
1484
|
+
return tensors
|
|
1485
|
+
|
|
1486
|
+
# Allocate new tensors on specified GPU (already zero-initialized by torch.zeros/torch.full)
|
|
1487
|
+
tensors = {
|
|
1488
|
+
# Beliefs - use unified dtype
|
|
1489
|
+
# lambda_: Current mixed beliefs [B,V] (updated each iteration)
|
|
1490
|
+
"lambda_": torch.zeros(B, self.V, dtype=self.dtype, device=device_str),
|
|
1491
|
+
# posterior_ratios: Posterior ratios/marginals [B,V] (matches Rust: posterior_ratios)
|
|
1492
|
+
"posterior_ratios": torch.zeros(B, self.V, dtype=self.dtype, device=device_str),
|
|
1493
|
+
"hard_dec": torch.zeros(B, self.V, dtype=torch.uint8, device=device_str),
|
|
1494
|
+
# Gamma: Memory strengths [B,V] (Rust equivalent: memory_strengths)
|
|
1495
|
+
"gamma": torch.zeros(B, self.V, dtype=self.dtype, device=device_str),
|
|
1496
|
+
# Syndrome and validation
|
|
1497
|
+
"syndrome": torch.zeros(B, self.C, dtype=torch.uint8, device=device_str),
|
|
1498
|
+
"check_ok": torch.zeros(B, self.C, dtype=torch.uint8, device=device_str),
|
|
1499
|
+
# Solution tracking
|
|
1500
|
+
"best_weights": torch.full((B,), float("inf"), dtype=self.dtype, device=device_str),
|
|
1501
|
+
"best_errors": torch.zeros(B, self.V, dtype=torch.uint8, device=device_str),
|
|
1502
|
+
# Hard decision at the end of the first leg. Returned for lanes with no solution.
|
|
1503
|
+
"first_leg_hard_dec": torch.zeros(B, self.V, dtype=torch.uint8, device=device_str),
|
|
1504
|
+
"found_count": torch.zeros(B, dtype=torch.int32, device=device_str),
|
|
1505
|
+
"valid_solutions": torch.zeros(B, dtype=torch.uint8, device=device_str),
|
|
1506
|
+
"sought_solutions": torch.ones(B, dtype=torch.int32, device=device_str),
|
|
1507
|
+
# Device-side active lane management (eliminates host-device syncs)
|
|
1508
|
+
"active_idx_buffer": torch.zeros(
|
|
1509
|
+
B, dtype=torch.int32, device=device_str
|
|
1510
|
+
), # Pre-allocated buffer for compacted indices
|
|
1511
|
+
"active_count": torch.zeros(
|
|
1512
|
+
1, dtype=torch.int32, device=device_str
|
|
1513
|
+
), # Device tensor for count (no sync needed)
|
|
1514
|
+
"block_counts": torch.zeros(
|
|
1515
|
+
(B + 255) // 256, dtype=torch.int32, device=device_str
|
|
1516
|
+
), # Per-block counts (BLOCK_SIZE=256)
|
|
1517
|
+
"prefix_sum": torch.zeros(
|
|
1518
|
+
(B + 255) // 256 + 1, dtype=torch.int32, device=device_str
|
|
1519
|
+
), # Prefix sum for compaction
|
|
1520
|
+
"old_active_count": torch.zeros(1, dtype=torch.int32, device=device_str),
|
|
1521
|
+
"count_changed_flag": torch.zeros(1, dtype=torch.uint8, device=device_str),
|
|
1522
|
+
"all_done_flag": torch.zeros(1, dtype=torch.uint8, device=device_str),
|
|
1523
|
+
"arrival_counter": torch.zeros(1, dtype=torch.int32, device=device_str),
|
|
1524
|
+
"in_leg": torch.zeros(1, dtype=torch.uint8, device=device_str),
|
|
1525
|
+
}
|
|
1526
|
+
# Messages - use unified dtype
|
|
1527
|
+
if self.cfg.perf == "throughput":
|
|
1528
|
+
tensors["muT"] = torch.zeros(self.E, B, dtype=self.dtype, device=device_str)
|
|
1529
|
+
tensors["nuT"] = torch.zeros(self.E, B, dtype=self.dtype, device=device_str)
|
|
1530
|
+
# Optional L tensor for delta-trick layered scheduling (only used when self._use_delta_trick)
|
|
1531
|
+
if self._use_delta_trick:
|
|
1532
|
+
tensors["L"] = torch.zeros(B, self.V, dtype=self.dtype, device=device_str)
|
|
1533
|
+
else:
|
|
1534
|
+
tensors["mu"] = torch.zeros(B, self.E, dtype=self.dtype, device=device_str)
|
|
1535
|
+
tensors["nu"] = torch.zeros(B, self.E, dtype=self.dtype, device=device_str)
|
|
1536
|
+
|
|
1537
|
+
|
|
1538
|
+
self._device_tensors_per_gpu[gpu_id][B] = tensors
|
|
1539
|
+
|
|
1540
|
+
# Report max safe batch size
|
|
1541
|
+
import logging
|
|
1542
|
+
|
|
1543
|
+
_logger = logging.getLogger(__name__)
|
|
1544
|
+
if _logger.isEnabledFor(logging.INFO):
|
|
1545
|
+
max_safe = (2**31 - 1) // self.E if self.E > 0 else B
|
|
1546
|
+
_logger.info(
|
|
1547
|
+
"[GPU %d] Batch size %d allocated (max safe batch size: %d for E=%d)",
|
|
1548
|
+
gpu_id,
|
|
1549
|
+
B,
|
|
1550
|
+
max_safe,
|
|
1551
|
+
self.E,
|
|
1552
|
+
)
|
|
1553
|
+
|
|
1554
|
+
return tensors
|
|
1555
|
+
|
|
1556
|
+
|
|
1557
|
+
def _reset_reused_tensors(self, tensors: Dict):
|
|
1558
|
+
"""Reset stateful tensors when reusing from cache.
|
|
1559
|
+
|
|
1560
|
+
Performance: In-place zero operations are much faster than reallocating tensors.
|
|
1561
|
+
Only resets what's necessary for correctness.
|
|
1562
|
+
"""
|
|
1563
|
+
# Reset all message buffers to zero (will be properly initialized by init kernels)
|
|
1564
|
+
# These MUST be reset as they contain state from previous decode calls
|
|
1565
|
+
if self.cfg.perf == "throughput":
|
|
1566
|
+
tensors["muT"].zero_()
|
|
1567
|
+
tensors["nuT"].zero_()
|
|
1568
|
+
if "L" in tensors:
|
|
1569
|
+
tensors["L"].zero_()
|
|
1570
|
+
else:
|
|
1571
|
+
tensors["mu"].zero_()
|
|
1572
|
+
tensors["nu"].zero_()
|
|
1573
|
+
|
|
1574
|
+
# Reset beliefs that are used in first iteration BEFORE being overwritten
|
|
1575
|
+
# posterior_ratios is used in gamma mixing in the first iteration, so must be clean
|
|
1576
|
+
tensors["posterior_ratios"].zero_()
|
|
1577
|
+
# lambda_ is immediately overwritten, but reset for safety
|
|
1578
|
+
tensors["lambda_"].zero_()
|
|
1579
|
+
|
|
1580
|
+
# Reset solution tracking (these are NOT immediately overwritten)
|
|
1581
|
+
tensors["hard_dec"].zero_()
|
|
1582
|
+
tensors["gamma"].zero_()
|
|
1583
|
+
tensors["best_weights"].fill_(float("inf"))
|
|
1584
|
+
tensors["best_errors"].zero_()
|
|
1585
|
+
tensors["first_leg_hard_dec"].zero_()
|
|
1586
|
+
tensors["found_count"].zero_()
|
|
1587
|
+
tensors["valid_solutions"].zero_()
|
|
1588
|
+
tensors["sought_solutions"].fill_(1)
|
|
1589
|
+
tensors["check_ok"].zero_()
|
|
1590
|
+
|
|
1591
|
+
# Note: syndrome is NOT zeroed here because it's immediately overwritten
|
|
1592
|
+
# with new syndrome values, avoiding redundant work.
|
|
1593
|
+
|
|
1594
|
+
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
def _get_active_count_from_blocks(self, tensors: Dict, B: int, gpu_idx: int) -> torch.Tensor:
|
|
1598
|
+
"""Get active count by summing block_counts (avoids separate count kernel).
|
|
1599
|
+
|
|
1600
|
+
Args:
|
|
1601
|
+
tensors: Device tensors dictionary
|
|
1602
|
+
B: Batch size
|
|
1603
|
+
gpu_idx: GPU index for device string
|
|
1604
|
+
|
|
1605
|
+
Returns:
|
|
1606
|
+
Active count as device tensor (no sync) - use device-side comparison
|
|
1607
|
+
"""
|
|
1608
|
+
device_str = self._device_strs[self.device_ids[gpu_idx]]
|
|
1609
|
+
BLOCK_SIZE = 256
|
|
1610
|
+
num_blocks = (B + BLOCK_SIZE - 1) // BLOCK_SIZE
|
|
1611
|
+
|
|
1612
|
+
# Count active per block (reuses block_counts from compaction if available)
|
|
1613
|
+
count_active_blocks_kernel[(num_blocks,)](
|
|
1614
|
+
tensors["active"],
|
|
1615
|
+
tensors["block_counts"],
|
|
1616
|
+
B=B,
|
|
1617
|
+
BLOCK_SIZE=BLOCK_SIZE,
|
|
1618
|
+
num_blocks=num_blocks,
|
|
1619
|
+
)
|
|
1620
|
+
# Sum block counts on device (no sync) - return tensor for device-side comparison
|
|
1621
|
+
if "active_count_tensor" not in tensors:
|
|
1622
|
+
tensors["active_count_tensor"] = torch.zeros(1, dtype=torch.int32, device=device_str)
|
|
1623
|
+
# torch.sum() returns a 0D scalar tensor, use in-place copy to avoid sync
|
|
1624
|
+
total_sum = torch.sum(tensors["block_counts"]).to(torch.int32)
|
|
1625
|
+
# Copy scalar to 1D tensor element without sync (both on device)
|
|
1626
|
+
if total_sum.dim() == 0:
|
|
1627
|
+
# 0D scalar - expand to 1D and copy in-place
|
|
1628
|
+
tensors["active_count_tensor"].copy_(total_sum.unsqueeze(0))
|
|
1629
|
+
else:
|
|
1630
|
+
# Already 1D - direct copy
|
|
1631
|
+
tensors["active_count_tensor"].copy_(total_sum)
|
|
1632
|
+
return tensors["active_count_tensor"]
|
|
1633
|
+
|
|
1634
|
+
def _compact_active_indices_on_device(
|
|
1635
|
+
self, tensors: Dict, B: int, gpu_idx: int
|
|
1636
|
+
) -> Tuple[torch.Tensor, int]:
|
|
1637
|
+
"""Compact active indices on device (replaces torch.nonzero()).
|
|
1638
|
+
|
|
1639
|
+
Args:
|
|
1640
|
+
tensors: Device tensors dictionary
|
|
1641
|
+
B: Batch size
|
|
1642
|
+
gpu_idx: GPU index (for device string if needed)
|
|
1643
|
+
|
|
1644
|
+
Returns:
|
|
1645
|
+
Tuple of (active_idx_buffer slice, B_active count)
|
|
1646
|
+
"""
|
|
1647
|
+
BLOCK_SIZE = 256
|
|
1648
|
+
num_blocks = (B + BLOCK_SIZE - 1) // BLOCK_SIZE
|
|
1649
|
+
|
|
1650
|
+
# Phase 1: Count active per block (reuse if already computed)
|
|
1651
|
+
count_active_blocks_kernel[(num_blocks,)](
|
|
1652
|
+
tensors["active"],
|
|
1653
|
+
tensors["block_counts"],
|
|
1654
|
+
B=B,
|
|
1655
|
+
BLOCK_SIZE=BLOCK_SIZE,
|
|
1656
|
+
num_blocks=num_blocks,
|
|
1657
|
+
)
|
|
1658
|
+
# Phase 2: Compute prefix sum on device (no host sync)
|
|
1659
|
+
# Use torch.cumsum which is efficient on device
|
|
1660
|
+
torch.cumsum(tensors["block_counts"], dim=0, out=tensors["prefix_sum"][1:])
|
|
1661
|
+
tensors["prefix_sum"][0] = 0 # First element is 0
|
|
1662
|
+
|
|
1663
|
+
# Phase 3: Compact indices using prefix sum
|
|
1664
|
+
# Validate grid size matches buffer size to catch bugs early
|
|
1665
|
+
expected_prefix_sum_size = num_blocks + 1
|
|
1666
|
+
if expected_prefix_sum_size != tensors["prefix_sum"].shape[0]:
|
|
1667
|
+
raise RuntimeError(
|
|
1668
|
+
f"Grid size mismatch: num_blocks={num_blocks} (expects prefix_sum size={expected_prefix_sum_size}) "
|
|
1669
|
+
f"but prefix_sum has {tensors['prefix_sum'].shape[0]} elements. B={B}, BLOCK_SIZE={BLOCK_SIZE}"
|
|
1670
|
+
)
|
|
1671
|
+
compact_active_indices_kernel[(num_blocks,)](
|
|
1672
|
+
tensors["active"],
|
|
1673
|
+
tensors["active_idx_buffer"],
|
|
1674
|
+
tensors["prefix_sum"],
|
|
1675
|
+
B=B,
|
|
1676
|
+
BLOCK_SIZE=BLOCK_SIZE,
|
|
1677
|
+
num_blocks=num_blocks,
|
|
1678
|
+
)
|
|
1679
|
+
|
|
1680
|
+
# Get count from last element of prefix sum (sync only when needed)
|
|
1681
|
+
B_active = int(tensors["prefix_sum"][num_blocks].item())
|
|
1682
|
+
|
|
1683
|
+
return tensors["active_idx_buffer"][:B_active], B_active
|
|
1684
|
+
|
|
1685
|
+
def _launch_c2v_btile(
|
|
1686
|
+
self,
|
|
1687
|
+
tensors: Dict,
|
|
1688
|
+
B: int,
|
|
1689
|
+
active_idx: torch.Tensor = None,
|
|
1690
|
+
B_active: int = None,
|
|
1691
|
+
graph: "CSRGraph" = None,
|
|
1692
|
+
gpu_idx: int = 0,
|
|
1693
|
+
chk_start: int = 0,
|
|
1694
|
+
chk_end: int = 0,
|
|
1695
|
+
use_delta: bool = False,
|
|
1696
|
+
):
|
|
1697
|
+
"""Launch C2V BTILE kernel with optional block range for layered scheduling.
|
|
1698
|
+
|
|
1699
|
+
Args:
|
|
1700
|
+
chk_start: Start check index for block-layered (0 = all checks)
|
|
1701
|
+
chk_end: End check index for block-layered (0 = all checks, means C)
|
|
1702
|
+
use_delta: If True, use delta-trick mode (requires L tensor to be allocated)
|
|
1703
|
+
"""
|
|
1704
|
+
if graph is None:
|
|
1705
|
+
graph = self.graphs[gpu_idx]
|
|
1706
|
+
device_str = f"{self.device_type}:{self.device_ids[gpu_idx]}"
|
|
1707
|
+
if active_idx is None:
|
|
1708
|
+
active_idx = torch.arange(B, device=device_str, dtype=torch.int32)
|
|
1709
|
+
B_active = B
|
|
1710
|
+
alpha = getattr(self, "_alpha_current", self.normalized_min_sum_alpha or 0.0)
|
|
1711
|
+
beta = self.offset_min_sum_beta or 0.0
|
|
1712
|
+
use_alpha = self.normalized_min_sum_alpha is not None
|
|
1713
|
+
use_beta = self.offset_min_sum_beta is not None
|
|
1714
|
+
|
|
1715
|
+
# Use pre-resolved config (resolved once in _resolve_configs)
|
|
1716
|
+
btile = self._rc_c2v_btile
|
|
1717
|
+
grid_dim1 = (B_active + btile - 1) // btile
|
|
1718
|
+
grid_dim0 = (chk_end - chk_start) if chk_end > 0 else self.C
|
|
1719
|
+
grid = (grid_dim0, grid_dim1)
|
|
1720
|
+
|
|
1721
|
+
L_tensor = tensors.get("L")
|
|
1722
|
+
if L_tensor is None and use_delta:
|
|
1723
|
+
raise RuntimeError("Delta-trick mode requires L tensor to be allocated")
|
|
1724
|
+
if L_tensor is None:
|
|
1725
|
+
if gpu_idx not in self._dummy_L_per_gpu:
|
|
1726
|
+
self._dummy_L_per_gpu[gpu_idx] = torch.zeros(
|
|
1727
|
+
1, 1, dtype=self.dtype, device=device_str
|
|
1728
|
+
)
|
|
1729
|
+
L_tensor = self._dummy_L_per_gpu[gpu_idx]
|
|
1730
|
+
c2v_min_sum_btile_kernel[grid](
|
|
1731
|
+
tensors["muT"],
|
|
1732
|
+
tensors["nuT"],
|
|
1733
|
+
graph.chk_ptr,
|
|
1734
|
+
graph.chk_edges,
|
|
1735
|
+
tensors["syndrome"],
|
|
1736
|
+
active_idx,
|
|
1737
|
+
tensors["active"],
|
|
1738
|
+
B,
|
|
1739
|
+
B_active,
|
|
1740
|
+
self.C,
|
|
1741
|
+
self.E,
|
|
1742
|
+
alpha,
|
|
1743
|
+
beta,
|
|
1744
|
+
use_alpha,
|
|
1745
|
+
use_beta,
|
|
1746
|
+
self._dtype_enum,
|
|
1747
|
+
self._rc_c2v_block,
|
|
1748
|
+
btile,
|
|
1749
|
+
chk_start,
|
|
1750
|
+
chk_end,
|
|
1751
|
+
graph.edge_var,
|
|
1752
|
+
L_tensor,
|
|
1753
|
+
self.V,
|
|
1754
|
+
use_delta,
|
|
1755
|
+
num_warps=self._rc_c2v_warps,
|
|
1756
|
+
num_stages=self._rc_c2v_stages,
|
|
1757
|
+
)
|
|
1758
|
+
|
|
1759
|
+
def _launch_c2v_kernel(self, tensors: Dict, B: int, graph: "CSRGraph" = None, gpu_idx: int = 0):
|
|
1760
|
+
"""Launch check-to-variable kernel."""
|
|
1761
|
+
if graph is None:
|
|
1762
|
+
graph = self.graphs[gpu_idx]
|
|
1763
|
+
alpha = getattr(self, "_alpha_current", self.normalized_min_sum_alpha or 0.0)
|
|
1764
|
+
beta = self.offset_min_sum_beta or 0.0
|
|
1765
|
+
use_alpha = self.normalized_min_sum_alpha is not None
|
|
1766
|
+
use_beta = self.offset_min_sum_beta is not None
|
|
1767
|
+
|
|
1768
|
+
rows = self._rc_c2v_rows
|
|
1769
|
+
grid = (((B * self.C) + rows - 1) // rows,)
|
|
1770
|
+
|
|
1771
|
+
c2v_min_sum_kernel[grid](
|
|
1772
|
+
tensors["mu"],
|
|
1773
|
+
tensors["nu"],
|
|
1774
|
+
graph.chk_ptr,
|
|
1775
|
+
graph.chk_edges,
|
|
1776
|
+
tensors["syndrome"],
|
|
1777
|
+
tensors["active"],
|
|
1778
|
+
B=B,
|
|
1779
|
+
C=self.C,
|
|
1780
|
+
E=self.E,
|
|
1781
|
+
alpha=alpha,
|
|
1782
|
+
beta=beta,
|
|
1783
|
+
use_alpha=use_alpha,
|
|
1784
|
+
use_beta=use_beta,
|
|
1785
|
+
dtype_enum=self._dtype_enum,
|
|
1786
|
+
BLOCK_SIZE=self._rc_c2v_block,
|
|
1787
|
+
ROWS_PER_CHK=rows,
|
|
1788
|
+
num_warps=self._rc_c2v_warps,
|
|
1789
|
+
num_stages=self._rc_c2v_stages,
|
|
1790
|
+
)
|
|
1791
|
+
|
|
1792
|
+
def _launch_v2c_fused_gamma_kernel(
|
|
1793
|
+
self,
|
|
1794
|
+
tensors: Dict,
|
|
1795
|
+
B: int,
|
|
1796
|
+
graph: "CSRGraph" = None,
|
|
1797
|
+
log_prior_ratios: torch.Tensor = None,
|
|
1798
|
+
gpu_idx: int = 0,
|
|
1799
|
+
prior_batched: bool = False,
|
|
1800
|
+
):
|
|
1801
|
+
"""Launch variable-to-check and marginals kernel with fused gamma mixing."""
|
|
1802
|
+
if graph is None:
|
|
1803
|
+
graph = self.graphs[gpu_idx]
|
|
1804
|
+
if log_prior_ratios is None:
|
|
1805
|
+
log_prior_ratios = self.log_prior_ratios_per_gpu[gpu_idx]
|
|
1806
|
+
|
|
1807
|
+
rows = self._rc_v2c_rows
|
|
1808
|
+
grid = (((B * self.V) + rows - 1) // rows,)
|
|
1809
|
+
|
|
1810
|
+
v2c_and_marginals_fused_gamma_kernel[grid](
|
|
1811
|
+
tensors["nu"],
|
|
1812
|
+
tensors["mu"],
|
|
1813
|
+
graph.var_ptr,
|
|
1814
|
+
graph.var_edges,
|
|
1815
|
+
tensors["lambda_"],
|
|
1816
|
+
tensors["posterior_ratios"],
|
|
1817
|
+
tensors["hard_dec"],
|
|
1818
|
+
tensors["active"],
|
|
1819
|
+
log_prior_ratios,
|
|
1820
|
+
tensors["gamma"],
|
|
1821
|
+
B=B,
|
|
1822
|
+
V=self.V,
|
|
1823
|
+
E=self.E,
|
|
1824
|
+
dtype_enum=self._dtype_enum,
|
|
1825
|
+
BLOCK_SIZE=self._rc_v2c_block,
|
|
1826
|
+
ROWS_PER_VAR=rows,
|
|
1827
|
+
PRIOR_BATCHED=prior_batched,
|
|
1828
|
+
num_warps=self._rc_v2c_warps,
|
|
1829
|
+
num_stages=self._rc_v2c_stages,
|
|
1830
|
+
)
|
|
1831
|
+
|
|
1832
|
+
def _launch_parity_check_kernel(
|
|
1833
|
+
self, tensors: Dict, B: int, graph: "CSRGraph" = None, gpu_idx: int = 0
|
|
1834
|
+
):
|
|
1835
|
+
"""Launch parity check kernel with pre-resolved config."""
|
|
1836
|
+
if graph is None:
|
|
1837
|
+
graph = self.graphs[gpu_idx]
|
|
1838
|
+
assert not (self.cfg.perf == "throughput"), (
|
|
1839
|
+
"Throughput mode must use parity_from_hard_kernel"
|
|
1840
|
+
)
|
|
1841
|
+
total = B * self.C
|
|
1842
|
+
grid = ((total + self.cfg.rows_per_chk - 1) // self.cfg.rows_per_chk,)
|
|
1843
|
+
meta_base = dict(B=B, C=self.C, V=self.V, E=self.E, ROWS_PER_CHK=self.cfg.rows_per_chk)
|
|
1844
|
+
|
|
1845
|
+
parity_per_check_kernel[grid](
|
|
1846
|
+
tensors["posterior_ratios"],
|
|
1847
|
+
graph.chk_ptr,
|
|
1848
|
+
graph.chk_edges,
|
|
1849
|
+
graph.edge_var,
|
|
1850
|
+
tensors["syndrome"],
|
|
1851
|
+
tensors["check_ok"],
|
|
1852
|
+
**meta_base,
|
|
1853
|
+
BLOCK_SIZE=self._rc_parity_block,
|
|
1854
|
+
num_warps=self._rc_parity_warps,
|
|
1855
|
+
num_stages=self._rc_parity_stages,
|
|
1856
|
+
)
|
|
1857
|
+
|
|
1858
|
+
def _launch_init_kernel(
|
|
1859
|
+
self,
|
|
1860
|
+
tensors: Dict,
|
|
1861
|
+
B: int,
|
|
1862
|
+
graph: "CSRGraph" = None,
|
|
1863
|
+
log_prior_ratios: torch.Tensor = None,
|
|
1864
|
+
gpu_idx: int = 0,
|
|
1865
|
+
):
|
|
1866
|
+
"""Launch message initialization kernel for default mode.
|
|
1867
|
+
|
|
1868
|
+
Initializes messages to match Rust:
|
|
1869
|
+
- nu (check_to_variable) -> zero
|
|
1870
|
+
- mu (variable_to_check) -> log_prior_ratios
|
|
1871
|
+
"""
|
|
1872
|
+
if graph is None:
|
|
1873
|
+
graph = self.graphs[gpu_idx]
|
|
1874
|
+
if log_prior_ratios is None:
|
|
1875
|
+
log_prior_ratios = self.log_prior_ratios_per_gpu[gpu_idx]
|
|
1876
|
+
N = B * self.E
|
|
1877
|
+
BLOCK = 1024
|
|
1878
|
+
grid = ((N + BLOCK - 1) // BLOCK,)
|
|
1879
|
+
# Estimate: init kernel uses default 1 warp per program
|
|
1880
|
+
num_warps = 1
|
|
1881
|
+
dtype_enum = self._dtype_enum
|
|
1882
|
+
init_messages_kernel[grid](
|
|
1883
|
+
tensors["mu"],
|
|
1884
|
+
tensors["nu"],
|
|
1885
|
+
graph.edge_var,
|
|
1886
|
+
log_prior_ratios.squeeze(0), # [V] prior LLRs
|
|
1887
|
+
B,
|
|
1888
|
+
self.E,
|
|
1889
|
+
self.V,
|
|
1890
|
+
dtype_enum=dtype_enum,
|
|
1891
|
+
BLOCK=BLOCK,
|
|
1892
|
+
)
|
|
1893
|
+
|
|
1894
|
+
def _launch_init_messages_with_priors_EB(
|
|
1895
|
+
self,
|
|
1896
|
+
muT: torch.Tensor,
|
|
1897
|
+
nuT: torch.Tensor,
|
|
1898
|
+
B: int,
|
|
1899
|
+
active_idx: Optional[torch.Tensor] = None,
|
|
1900
|
+
B_active: Optional[int] = None,
|
|
1901
|
+
graph: "CSRGraph" = None,
|
|
1902
|
+
log_prior_ratios: Optional[torch.Tensor] = None,
|
|
1903
|
+
gpu_idx: int = 0,
|
|
1904
|
+
prior_batched: bool = False,
|
|
1905
|
+
):
|
|
1906
|
+
"""Launch initialization kernel for throughput mode: nu=zero, mu=priors (Rust-compatible).
|
|
1907
|
+
|
|
1908
|
+
This matches Rust's initialization:
|
|
1909
|
+
- initialize_check_to_variable() -> nu = zero
|
|
1910
|
+
- initialize_variable_to_check() -> mu = log_prior_ratios
|
|
1911
|
+
|
|
1912
|
+
If active_idx and B_active are provided and B_active < B, only initializes messages for active lanes,
|
|
1913
|
+
dramatically reducing memory traffic when B_active << B.
|
|
1914
|
+
|
|
1915
|
+
Args:
|
|
1916
|
+
muT, nuT: [E,B] message tensors
|
|
1917
|
+
B: total batch size
|
|
1918
|
+
active_idx: Optional [B_active] compact list of active batch indices
|
|
1919
|
+
B_active: Optional number of active lanes (if None, uses B)
|
|
1920
|
+
graph: CSRGraph instance (defaults to self.graphs[gpu_idx])
|
|
1921
|
+
log_prior_ratios: [1, V] log prior ratios tensor (defaults to self.log_prior_ratios_per_gpu[gpu_idx])
|
|
1922
|
+
gpu_idx: GPU index (for device string)
|
|
1923
|
+
"""
|
|
1924
|
+
if graph is None:
|
|
1925
|
+
graph = self.graphs[gpu_idx]
|
|
1926
|
+
if log_prior_ratios is None:
|
|
1927
|
+
log_prior_ratios = self.log_prior_ratios_per_gpu[gpu_idx]
|
|
1928
|
+
device_str = self._device_strs[self.device_ids[gpu_idx]]
|
|
1929
|
+
|
|
1930
|
+
prior_arg = log_prior_ratios if prior_batched else log_prior_ratios.squeeze(0)
|
|
1931
|
+
|
|
1932
|
+
# Determine if we should use active lanes only
|
|
1933
|
+
use_active = (
|
|
1934
|
+
(active_idx is not None)
|
|
1935
|
+
and (B_active is not None)
|
|
1936
|
+
and (B_active < B)
|
|
1937
|
+
and (B_active > 0)
|
|
1938
|
+
)
|
|
1939
|
+
|
|
1940
|
+
if use_active:
|
|
1941
|
+
# Active lanes path: use hardcoded config for now (will add autotuning later)
|
|
1942
|
+
# Note: Kernels use int64 for offset calculations, so overflow is prevented
|
|
1943
|
+
total = self.E * B_active
|
|
1944
|
+
dummy_active_idx = active_idx
|
|
1945
|
+
grid = ((total + 1024 - 1) // 1024,)
|
|
1946
|
+
num_warps = 1
|
|
1947
|
+
dtype_enum = self._dtype_enum
|
|
1948
|
+
init_messages_with_priors_EB_kernel[grid](
|
|
1949
|
+
muT,
|
|
1950
|
+
nuT,
|
|
1951
|
+
graph.edge_var,
|
|
1952
|
+
prior_arg, # [V] shared or [B,V] per-shot prior LLRs
|
|
1953
|
+
self.E,
|
|
1954
|
+
self.V,
|
|
1955
|
+
B,
|
|
1956
|
+
dummy_active_idx, # active_idx
|
|
1957
|
+
B_active, # Number of active lanes
|
|
1958
|
+
use_active=use_active, # Flag to enable active-lanes-only mode
|
|
1959
|
+
dtype_enum=dtype_enum,
|
|
1960
|
+
BLOCK=1024,
|
|
1961
|
+
PRIOR_BATCHED=prior_batched,
|
|
1962
|
+
num_warps=num_warps,
|
|
1963
|
+
num_stages=1,
|
|
1964
|
+
)
|
|
1965
|
+
else:
|
|
1966
|
+
# Full initialization path: use autotuning
|
|
1967
|
+
# Note: Kernels use int64 for offset calculations, so overflow is prevented
|
|
1968
|
+
total = self.E * B
|
|
1969
|
+
B_active = B # Use full batch
|
|
1970
|
+
# Create dummy tensor (won't be used, but Triton requires it)
|
|
1971
|
+
dummy_active_idx = torch.zeros(1, dtype=torch.int32, device=device_str)
|
|
1972
|
+
|
|
1973
|
+
problem_key = {
|
|
1974
|
+
"B": B,
|
|
1975
|
+
"E": self.E,
|
|
1976
|
+
"V": self.V,
|
|
1977
|
+
"dtype": self.dtype_str,
|
|
1978
|
+
"use_active": False, # Full path
|
|
1979
|
+
"sparsity_hash": graph.sparsity_hash,
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
def _grid_fn(cfg):
|
|
1983
|
+
block = int(cfg.get("BLOCK", 1024))
|
|
1984
|
+
return ((total + block - 1) // block,)
|
|
1985
|
+
|
|
1986
|
+
dtype_enum = self._dtype_enum
|
|
1987
|
+
|
|
1988
|
+
saved = self._get_tuned_meta(
|
|
1989
|
+
kernel_name="init_messages_with_priors_EB_kernel",
|
|
1990
|
+
problem_key=problem_key,
|
|
1991
|
+
build_cfgs_fn=build_init_messages_configs,
|
|
1992
|
+
kernel=init_messages_with_priors_EB_kernel,
|
|
1993
|
+
grid=(1,), # Dummy grid, overridden by grid_fn
|
|
1994
|
+
args=(
|
|
1995
|
+
muT,
|
|
1996
|
+
nuT,
|
|
1997
|
+
graph.edge_var,
|
|
1998
|
+
prior_arg, # [V] shared or [B,V] per-shot prior LLRs
|
|
1999
|
+
self.E,
|
|
2000
|
+
self.V,
|
|
2001
|
+
B,
|
|
2002
|
+
dummy_active_idx, # active_idx (dummy for full path)
|
|
2003
|
+
B_active, # Number of active lanes (B for full path)
|
|
2004
|
+
),
|
|
2005
|
+
meta_base=dict(
|
|
2006
|
+
use_active=False, # Full path
|
|
2007
|
+
dtype_enum=dtype_enum,
|
|
2008
|
+
PRIOR_BATCHED=prior_batched,
|
|
2009
|
+
),
|
|
2010
|
+
number=20,
|
|
2011
|
+
grid_fn=_grid_fn,
|
|
2012
|
+
log_name=f"init_messages_with_priors_EB_kernel[gpu={gpu_idx}]",
|
|
2013
|
+
)
|
|
2014
|
+
|
|
2015
|
+
block = int(saved.get("BLOCK", 1024))
|
|
2016
|
+
grid = ((total + block - 1) // block,)
|
|
2017
|
+
num_warps = int(saved.get("num_warps", 1))
|
|
2018
|
+
num_stages = int(saved.get("num_stages", 1))
|
|
2019
|
+
|
|
2020
|
+
dtype_enum = self._dtype_enum
|
|
2021
|
+
init_messages_with_priors_EB_kernel[grid](
|
|
2022
|
+
muT,
|
|
2023
|
+
nuT,
|
|
2024
|
+
graph.edge_var,
|
|
2025
|
+
prior_arg, # [V] shared or [B,V] per-shot prior LLRs
|
|
2026
|
+
self.E,
|
|
2027
|
+
self.V,
|
|
2028
|
+
B,
|
|
2029
|
+
dummy_active_idx, # active_idx (dummy for full path)
|
|
2030
|
+
B_active, # Number of active lanes (B for full path)
|
|
2031
|
+
use_active=False, # Full path
|
|
2032
|
+
dtype_enum=dtype_enum,
|
|
2033
|
+
BLOCK=block,
|
|
2034
|
+
PRIOR_BATCHED=prior_batched,
|
|
2035
|
+
num_warps=num_warps,
|
|
2036
|
+
num_stages=num_stages,
|
|
2037
|
+
)
|
|
2038
|
+
|
|
2039
|
+
def _launch_v2c_btile(
|
|
2040
|
+
self,
|
|
2041
|
+
tensors: Dict,
|
|
2042
|
+
B: int,
|
|
2043
|
+
write_hard: bool,
|
|
2044
|
+
active_idx: torch.Tensor = None,
|
|
2045
|
+
B_active: int = None,
|
|
2046
|
+
graph: "CSRGraph" = None,
|
|
2047
|
+
log_prior_ratios: torch.Tensor = None,
|
|
2048
|
+
gpu_idx: int = 0,
|
|
2049
|
+
block_vars: torch.Tensor = None,
|
|
2050
|
+
block_var_ptr: torch.Tensor = None,
|
|
2051
|
+
block_idx: int = -1,
|
|
2052
|
+
prior_batched: bool = False,
|
|
2053
|
+
):
|
|
2054
|
+
"""Launch variable-to-check BTILE kernel with fused gamma mixing.
|
|
2055
|
+
|
|
2056
|
+
Note: posterior_ratios is always stored (required for next iteration's gamma mixing).
|
|
2057
|
+
|
|
2058
|
+
Args:
|
|
2059
|
+
block_vars: Variable indices for block-layered (None = all vars)
|
|
2060
|
+
block_var_ptr: Prefix sum for block_vars (None = not used)
|
|
2061
|
+
block_idx: Block index for block-layered (-1 = flood-fill, use all vars)
|
|
2062
|
+
"""
|
|
2063
|
+
if graph is None:
|
|
2064
|
+
graph = self.graphs[gpu_idx]
|
|
2065
|
+
if log_prior_ratios is None:
|
|
2066
|
+
log_prior_ratios = self.log_prior_ratios_per_gpu[gpu_idx]
|
|
2067
|
+
device_str = self._device_strs[self.device_ids[gpu_idx]]
|
|
2068
|
+
if active_idx is None:
|
|
2069
|
+
active_idx = torch.arange(B, device=device_str, dtype=torch.int32)
|
|
2070
|
+
B_active = B
|
|
2071
|
+
|
|
2072
|
+
# Use pre-resolved config
|
|
2073
|
+
btile = self._rc_v2c_btile
|
|
2074
|
+
grid_dim1 = (B_active + btile - 1) // btile
|
|
2075
|
+
if block_idx >= 0 and block_var_ptr is not None:
|
|
2076
|
+
block_var_ptr_cpu = self._block_var_ptr_cpu
|
|
2077
|
+
var_start = int(block_var_ptr_cpu[block_idx])
|
|
2078
|
+
var_end = int(block_var_ptr_cpu[block_idx + 1])
|
|
2079
|
+
grid_dim0 = var_end - var_start
|
|
2080
|
+
else:
|
|
2081
|
+
grid_dim0 = self.V
|
|
2082
|
+
grid = (grid_dim0, grid_dim1)
|
|
2083
|
+
|
|
2084
|
+
# Block-layered vs flood-fill tensor selection
|
|
2085
|
+
if block_idx >= 0 and block_vars is not None and block_var_ptr is not None:
|
|
2086
|
+
block_vars_actual = block_vars
|
|
2087
|
+
block_var_ptr_actual = block_var_ptr
|
|
2088
|
+
block_idx_actual = block_idx
|
|
2089
|
+
else:
|
|
2090
|
+
block_vars_actual = (
|
|
2091
|
+
graph.var_edges[:1] if graph.var_edges.shape[0] > 0 else graph.var_ptr[:1]
|
|
2092
|
+
)
|
|
2093
|
+
block_var_ptr_actual = graph.var_ptr[:2]
|
|
2094
|
+
block_idx_actual = -1
|
|
2095
|
+
|
|
2096
|
+
v2c_and_gamma_btile_kernel[grid](
|
|
2097
|
+
tensors["nuT"],
|
|
2098
|
+
tensors["muT"],
|
|
2099
|
+
graph.var_ptr,
|
|
2100
|
+
graph.var_edges,
|
|
2101
|
+
tensors["lambda_"],
|
|
2102
|
+
log_prior_ratios,
|
|
2103
|
+
tensors["gamma"],
|
|
2104
|
+
tensors["posterior_ratios"],
|
|
2105
|
+
tensors["hard_dec"],
|
|
2106
|
+
active_idx,
|
|
2107
|
+
tensors["active"],
|
|
2108
|
+
B=B,
|
|
2109
|
+
B_active=B_active,
|
|
2110
|
+
V=self.V,
|
|
2111
|
+
E=self.E,
|
|
2112
|
+
dtype_enum=self._dtype_enum,
|
|
2113
|
+
BLOCK_SIZE=self._rc_v2c_block,
|
|
2114
|
+
BTILE=btile,
|
|
2115
|
+
WRITE_HARD=write_hard,
|
|
2116
|
+
block_vars=block_vars_actual,
|
|
2117
|
+
block_var_ptr=block_var_ptr_actual,
|
|
2118
|
+
block_idx=block_idx_actual,
|
|
2119
|
+
PRIOR_BATCHED=prior_batched,
|
|
2120
|
+
num_warps=self._rc_v2c_warps,
|
|
2121
|
+
num_stages=self._rc_v2c_stages,
|
|
2122
|
+
)
|
|
2123
|
+
|
|
2124
|
+
def _launch_parity_from_hard_kernel(
|
|
2125
|
+
self, tensors: Dict, B: int, graph: "CSRGraph" = None, gpu_idx: int = 0
|
|
2126
|
+
):
|
|
2127
|
+
"""Launch parity check using hard_dec (for throughput mode)."""
|
|
2128
|
+
if graph is None:
|
|
2129
|
+
graph = self.graphs[gpu_idx]
|
|
2130
|
+
total = B * self.C
|
|
2131
|
+
grid = ((total + self.cfg.rows_per_chk - 1) // self.cfg.rows_per_chk,)
|
|
2132
|
+
problem_key = {
|
|
2133
|
+
"B": B,
|
|
2134
|
+
"C": self.C,
|
|
2135
|
+
"V": self.V,
|
|
2136
|
+
"E": self.E,
|
|
2137
|
+
"ROWS_PER_CHK": self.cfg.rows_per_chk,
|
|
2138
|
+
"dtype": self.dtype_str,
|
|
2139
|
+
"sparsity_hash": graph.sparsity_hash,
|
|
2140
|
+
}
|
|
2141
|
+
meta_base = dict(B=B, C=self.C, V=self.V, E=self.E, ROWS_PER_CHK=self.cfg.rows_per_chk)
|
|
2142
|
+
|
|
2143
|
+
saved = self._get_tuned_meta(
|
|
2144
|
+
kernel_name="parity_from_hard_kernel",
|
|
2145
|
+
problem_key=problem_key,
|
|
2146
|
+
build_cfgs_fn=build_parity_configs,
|
|
2147
|
+
kernel=parity_from_hard_kernel,
|
|
2148
|
+
grid=grid,
|
|
2149
|
+
args=(
|
|
2150
|
+
tensors["hard_dec"],
|
|
2151
|
+
graph.chk_ptr,
|
|
2152
|
+
graph.chk_edges,
|
|
2153
|
+
graph.edge_var,
|
|
2154
|
+
tensors["syndrome"],
|
|
2155
|
+
tensors["check_ok"],
|
|
2156
|
+
),
|
|
2157
|
+
meta_base=meta_base,
|
|
2158
|
+
number=20,
|
|
2159
|
+
log_name=f"parity_from_hard_kernel[gpu={gpu_idx}]",
|
|
2160
|
+
)
|
|
2161
|
+
|
|
2162
|
+
block_size = int(saved["BLOCK_SIZE"])
|
|
2163
|
+
num_warps = int(saved.get("num_warps", 0))
|
|
2164
|
+
num_stages = int(saved.get("num_stages", 0))
|
|
2165
|
+
|
|
2166
|
+
parity_from_hard_kernel[grid](
|
|
2167
|
+
tensors["hard_dec"],
|
|
2168
|
+
graph.chk_ptr,
|
|
2169
|
+
graph.chk_edges,
|
|
2170
|
+
graph.edge_var,
|
|
2171
|
+
tensors["syndrome"],
|
|
2172
|
+
tensors["check_ok"],
|
|
2173
|
+
**meta_base,
|
|
2174
|
+
BLOCK_SIZE=block_size,
|
|
2175
|
+
num_warps=num_warps,
|
|
2176
|
+
num_stages=num_stages,
|
|
2177
|
+
)
|
|
2178
|
+
|
|
2179
|
+
def _launch_parity_from_hard_compact_with_zero_kernel(
|
|
2180
|
+
self, tensors: Dict, B: int, graph: "CSRGraph" = None, gpu_idx: int = 0
|
|
2181
|
+
):
|
|
2182
|
+
"""Launch fused parity check kernel: zeros inactive lanes and checks parity for active lanes."""
|
|
2183
|
+
if graph is None:
|
|
2184
|
+
graph = self.graphs[gpu_idx]
|
|
2185
|
+
total = B * self.C
|
|
2186
|
+
grid = ((total + self.cfg.rows_per_chk - 1) // self.cfg.rows_per_chk,)
|
|
2187
|
+
|
|
2188
|
+
parity_from_hard_compact_with_zero_kernel[grid](
|
|
2189
|
+
tensors["hard_dec"],
|
|
2190
|
+
graph.chk_ptr,
|
|
2191
|
+
graph.chk_edges,
|
|
2192
|
+
graph.edge_var,
|
|
2193
|
+
tensors["syndrome"],
|
|
2194
|
+
tensors["check_ok"],
|
|
2195
|
+
tensors["active"],
|
|
2196
|
+
B,
|
|
2197
|
+
self.C,
|
|
2198
|
+
self.V,
|
|
2199
|
+
self.E,
|
|
2200
|
+
BLOCK_SIZE=self._rc_parity_block,
|
|
2201
|
+
ROWS_PER_CHK=self.cfg.rows_per_chk,
|
|
2202
|
+
num_warps=self._rc_parity_warps,
|
|
2203
|
+
num_stages=self._rc_parity_stages,
|
|
2204
|
+
)
|
|
2205
|
+
|
|
2206
|
+
def _launch_parity_from_hard_compact_kernel(
|
|
2207
|
+
self,
|
|
2208
|
+
tensors: Dict,
|
|
2209
|
+
B: int,
|
|
2210
|
+
active_idx: torch.Tensor,
|
|
2211
|
+
B_active: int,
|
|
2212
|
+
graph: "CSRGraph" = None,
|
|
2213
|
+
gpu_idx: int = 0,
|
|
2214
|
+
):
|
|
2215
|
+
"""Launch parity check kernel for active lanes only (compact version).
|
|
2216
|
+
|
|
2217
|
+
Uses active_idx indirection to process only B_active lanes instead of all B.
|
|
2218
|
+
"""
|
|
2219
|
+
if graph is None:
|
|
2220
|
+
graph = self.graphs[gpu_idx]
|
|
2221
|
+
total = B_active * self.C
|
|
2222
|
+
grid = ((total + self.cfg.rows_per_chk - 1) // self.cfg.rows_per_chk,)
|
|
2223
|
+
|
|
2224
|
+
parity_from_hard_compact_kernel[grid](
|
|
2225
|
+
tensors["hard_dec"],
|
|
2226
|
+
graph.chk_ptr,
|
|
2227
|
+
graph.chk_edges,
|
|
2228
|
+
graph.edge_var,
|
|
2229
|
+
tensors["syndrome"],
|
|
2230
|
+
tensors["check_ok"],
|
|
2231
|
+
active_idx,
|
|
2232
|
+
B,
|
|
2233
|
+
B_active,
|
|
2234
|
+
self.C,
|
|
2235
|
+
self.V,
|
|
2236
|
+
self.E,
|
|
2237
|
+
BLOCK_SIZE=self._rc_parity_block,
|
|
2238
|
+
ROWS_PER_CHK=self.cfg.rows_per_chk,
|
|
2239
|
+
num_warps=self._rc_parity_warps,
|
|
2240
|
+
num_stages=self._rc_parity_stages,
|
|
2241
|
+
)
|
|
2242
|
+
|
|
2243
|
+
|
|
2244
|
+
|
|
2245
|
+
def _check_parity_and_select(
|
|
2246
|
+
self,
|
|
2247
|
+
tensors: Dict,
|
|
2248
|
+
B: int,
|
|
2249
|
+
active_idx: torch.Tensor = None,
|
|
2250
|
+
B_active: int = None,
|
|
2251
|
+
*,
|
|
2252
|
+
force_hard: bool = False,
|
|
2253
|
+
graph: "CSRGraph" = None,
|
|
2254
|
+
gpu_idx: int = 0,
|
|
2255
|
+
prior_batched: bool = False,
|
|
2256
|
+
wj_override: Optional[torch.Tensor] = None,
|
|
2257
|
+
):
|
|
2258
|
+
"""Check parity and select best solutions fully on device (no host reductions).
|
|
2259
|
+
|
|
2260
|
+
Args:
|
|
2261
|
+
tensors: device tensors dict
|
|
2262
|
+
B: batch size
|
|
2263
|
+
active_idx: optional compact active indices
|
|
2264
|
+
B_active: number of active lanes
|
|
2265
|
+
force_hard: if True, use hard_dec parity path regardless of perf mode
|
|
2266
|
+
graph: CSRGraph instance (defaults to self.graphs[gpu_idx])
|
|
2267
|
+
gpu_idx: GPU index (for device string if needed)
|
|
2268
|
+
"""
|
|
2269
|
+
if graph is None:
|
|
2270
|
+
graph = self.graphs[gpu_idx]
|
|
2271
|
+
if B_active is not None and B_active == 0:
|
|
2272
|
+
return
|
|
2273
|
+
# Launch the right parity kernel
|
|
2274
|
+
if self.cfg.perf == "throughput" or force_hard:
|
|
2275
|
+
if (
|
|
2276
|
+
self.cfg.perf == "throughput"
|
|
2277
|
+
and B_active is not None
|
|
2278
|
+
and active_idx is not None
|
|
2279
|
+
and B_active < B
|
|
2280
|
+
):
|
|
2281
|
+
self._launch_parity_from_hard_compact_kernel(
|
|
2282
|
+
tensors, B, active_idx, B_active, graph=graph, gpu_idx=gpu_idx
|
|
2283
|
+
)
|
|
2284
|
+
else:
|
|
2285
|
+
self._launch_parity_from_hard_compact_with_zero_kernel(
|
|
2286
|
+
tensors, B, graph=graph, gpu_idx=gpu_idx
|
|
2287
|
+
)
|
|
2288
|
+
else:
|
|
2289
|
+
self._launch_parity_check_kernel(tensors, B, graph=graph, gpu_idx=gpu_idx)
|
|
2290
|
+
|
|
2291
|
+
# Device-side select using pre-resolved config
|
|
2292
|
+
gpu_id = self.device_ids[gpu_idx]
|
|
2293
|
+
wj_gpu = wj_override if prior_batched else self.wj_per_gpu[gpu_id]
|
|
2294
|
+
|
|
2295
|
+
check_and_select_kernel[(B,)](
|
|
2296
|
+
tensors["check_ok"],
|
|
2297
|
+
tensors["posterior_ratios"],
|
|
2298
|
+
tensors["hard_dec"],
|
|
2299
|
+
wj_gpu,
|
|
2300
|
+
tensors["best_weights"],
|
|
2301
|
+
tensors["best_errors"],
|
|
2302
|
+
tensors["valid_solutions"],
|
|
2303
|
+
tensors["found_count"],
|
|
2304
|
+
tensors["active"],
|
|
2305
|
+
B,
|
|
2306
|
+
self.C,
|
|
2307
|
+
self.V,
|
|
2308
|
+
dtype_enum=self._dtype_enum,
|
|
2309
|
+
BLOCK_C=self._rc_select_block_c,
|
|
2310
|
+
BLOCK_V=self._rc_select_block_v,
|
|
2311
|
+
NUM_SOLS=int(self.stop_nconv),
|
|
2312
|
+
PRIOR_BATCHED=prior_batched,
|
|
2313
|
+
num_warps=self._rc_select_warps,
|
|
2314
|
+
num_stages=1,
|
|
2315
|
+
)
|
|
2316
|
+
|
|
2317
|
+
def decode(
|
|
2318
|
+
self,
|
|
2319
|
+
syndromes: torch.Tensor,
|
|
2320
|
+
*,
|
|
2321
|
+
init_posterior: Optional[torch.Tensor] = None,
|
|
2322
|
+
init_gamma: Optional[torch.Tensor] = None,
|
|
2323
|
+
skip_relay_legs: bool = False,
|
|
2324
|
+
pre_iter_override: Optional[int] = None,
|
|
2325
|
+
return_last_posterior: bool = False,
|
|
2326
|
+
prior_llr: Optional[torch.Tensor] = None,
|
|
2327
|
+
) -> Dict[str, torch.Tensor]:
|
|
2328
|
+
"""Decode syndromes using Relay-BP algorithm with multi-GPU support.
|
|
2329
|
+
|
|
2330
|
+
Args:
|
|
2331
|
+
syndromes: [B, C] syndrome bits (uint8)
|
|
2332
|
+
init_posterior: optional [B, V] tensor; when set, overrides the default
|
|
2333
|
+
log_prior_ratios initialization of posterior_ratios so the
|
|
2334
|
+
pre-iter phase resumes from a caller-supplied posterior, e.g. to
|
|
2335
|
+
continue decoding from a previous call.
|
|
2336
|
+
init_gamma: optional [B, V] tensor; when set, overrides the per-lane
|
|
2337
|
+
gamma vector used during the pre-iter phase (replaces the scalar
|
|
2338
|
+
gamma0 broadcast), so each shot can use its own memory strengths.
|
|
2339
|
+
skip_relay_legs: when True, return immediately after the pre-iter
|
|
2340
|
+
phase instead of running the relay legs (num_sets loop).
|
|
2341
|
+
pre_iter_override: optional override for the number of pre-iter
|
|
2342
|
+
iterations (replaces self.pre_iter for this call only).
|
|
2343
|
+
return_last_posterior: when True, the result dict additionally
|
|
2344
|
+
contains "last_posterior": [B, V]: a clone of
|
|
2345
|
+
posterior_ratios at the end of decode.
|
|
2346
|
+
prior_llr: optional [B, V] tensor of per-shot prior LLRs replacing the
|
|
2347
|
+
shared [1, V] log_prior_ratios for this call (each shot gets
|
|
2348
|
+
its own prior).
|
|
2349
|
+
|
|
2350
|
+
Returns:
|
|
2351
|
+
Dictionary with keys:
|
|
2352
|
+
- "errors": [B, V] decoded errors (uint8) or packed if bitpack_output=True
|
|
2353
|
+
- "weights": [B] solution weights (float32)
|
|
2354
|
+
- "valid_mask": [B] bool, True where found_count >= stop_nconv
|
|
2355
|
+
- "iterations": [B] iteration counts
|
|
2356
|
+
- "found_count": [B] int32 tensor of number of valid solutions found per lane
|
|
2357
|
+
- "unconverged_indices": [U] int32 tensor of batch indices that didn't converge
|
|
2358
|
+
(found_count < stop_nconv). Empty tensor if all converged or fast_max_unconverged_pct=None.
|
|
2359
|
+
Only non-empty when fast_max_unconverged_pct is set and early exit occurred.
|
|
2360
|
+
- "last_posterior" (optional): [B, V] cloned posterior at end of decode.
|
|
2361
|
+
"""
|
|
2362
|
+
B = syndromes.shape[0]
|
|
2363
|
+
|
|
2364
|
+
if syndromes.shape[1] != self.C:
|
|
2365
|
+
raise ValueError(f"Expected {self.C} syndrome bits, got {syndromes.shape[1]}")
|
|
2366
|
+
|
|
2367
|
+
# Validate the advanced kwargs.
|
|
2368
|
+
advanced_args_used = (
|
|
2369
|
+
init_posterior is not None
|
|
2370
|
+
or init_gamma is not None
|
|
2371
|
+
or skip_relay_legs
|
|
2372
|
+
or pre_iter_override is not None
|
|
2373
|
+
or return_last_posterior
|
|
2374
|
+
or prior_llr is not None
|
|
2375
|
+
)
|
|
2376
|
+
if advanced_args_used:
|
|
2377
|
+
if self.cfg.perf != "throughput":
|
|
2378
|
+
raise ValueError(
|
|
2379
|
+
"init_posterior / init_gamma / skip_relay_legs / pre_iter_override / "
|
|
2380
|
+
"return_last_posterior are only supported in throughput perf mode "
|
|
2381
|
+
f"(got perf={self.cfg.perf!r})."
|
|
2382
|
+
)
|
|
2383
|
+
if self.num_gpus > 1:
|
|
2384
|
+
raise ValueError(
|
|
2385
|
+
"init_posterior / init_gamma / skip_relay_legs / pre_iter_override / "
|
|
2386
|
+
"return_last_posterior / prior_llr require single-GPU decode "
|
|
2387
|
+
f"(self.num_gpus={self.num_gpus})."
|
|
2388
|
+
)
|
|
2389
|
+
if init_posterior is not None and tuple(init_posterior.shape) != (B, self.V):
|
|
2390
|
+
raise ValueError(
|
|
2391
|
+
f"init_posterior shape {tuple(init_posterior.shape)} != ({B}, {self.V})"
|
|
2392
|
+
)
|
|
2393
|
+
if init_gamma is not None and tuple(init_gamma.shape) != (B, self.V):
|
|
2394
|
+
raise ValueError(f"init_gamma shape {tuple(init_gamma.shape)} != ({B}, {self.V})")
|
|
2395
|
+
if prior_llr is not None and tuple(prior_llr.shape) != (B, self.V):
|
|
2396
|
+
raise ValueError(f"prior_llr shape {tuple(prior_llr.shape)} != ({B}, {self.V})")
|
|
2397
|
+
|
|
2398
|
+
# Path for single GPU: avoid thread/gather overhead
|
|
2399
|
+
if self.num_gpus == 1:
|
|
2400
|
+
gpu_id = self.device_ids[0]
|
|
2401
|
+
gpu_idx = 0 # Index in our device_ids list (always 0 for single GPU)
|
|
2402
|
+
device_str = f"{self.device_type}:{gpu_id}"
|
|
2403
|
+
|
|
2404
|
+
# Set device context for Triton kernel launches
|
|
2405
|
+
with torch.cuda.device(gpu_id):
|
|
2406
|
+
# Move syndromes to GPU (if not already there)
|
|
2407
|
+
if syndromes.device.type != self.device_type or syndromes.device.index != gpu_id:
|
|
2408
|
+
syndromes = syndromes.to(device_str)
|
|
2409
|
+
|
|
2410
|
+
# Allocate tensors on this GPU
|
|
2411
|
+
tensors = self._allocate_device_tensors(B, gpu_id)
|
|
2412
|
+
|
|
2413
|
+
# Use GPU-specific graph and log_prior_ratios
|
|
2414
|
+
graph = self.graphs[gpu_idx]
|
|
2415
|
+
log_prior_ratios = self.log_prior_ratios_per_gpu[gpu_idx]
|
|
2416
|
+
explicit_gammas = (
|
|
2417
|
+
self.explicit_gammas_per_gpu[gpu_idx] if self.explicit_gammas_per_gpu else None
|
|
2418
|
+
)
|
|
2419
|
+
|
|
2420
|
+
prior_batched = prior_llr is not None
|
|
2421
|
+
wj_override = None
|
|
2422
|
+
if prior_batched:
|
|
2423
|
+
plr = prior_llr.to(device=device_str, dtype=self.dtype).contiguous()
|
|
2424
|
+
log_prior_ratios = plr
|
|
2425
|
+
wj_override = torch.where(
|
|
2426
|
+
torch.isfinite(plr), plr, torch.zeros_like(plr)
|
|
2427
|
+
).contiguous()
|
|
2428
|
+
|
|
2429
|
+
# Run decode loop directly (no thread overhead)
|
|
2430
|
+
return self._decode_loop_single_gpu(
|
|
2431
|
+
syndromes,
|
|
2432
|
+
tensors,
|
|
2433
|
+
graph,
|
|
2434
|
+
log_prior_ratios,
|
|
2435
|
+
explicit_gammas,
|
|
2436
|
+
gpu_idx,
|
|
2437
|
+
init_posterior=init_posterior,
|
|
2438
|
+
init_gamma=init_gamma,
|
|
2439
|
+
skip_relay_legs=skip_relay_legs,
|
|
2440
|
+
pre_iter_override=pre_iter_override,
|
|
2441
|
+
return_last_posterior=return_last_posterior,
|
|
2442
|
+
prior_batched=prior_batched,
|
|
2443
|
+
wj_override=wj_override,
|
|
2444
|
+
)
|
|
2445
|
+
else:
|
|
2446
|
+
# Multi-GPU path: split batch and process in parallel
|
|
2447
|
+
batch_splits = self._split_batch(B, self.num_gpus)
|
|
2448
|
+
results = self._decode_parallel(batch_splits, syndromes)
|
|
2449
|
+
return self._gather_results(results, B, batch_splits)
|
|
2450
|
+
|
|
2451
|
+
def _split_batch(self, B: int, num_gpus: int) -> List[Tuple[int, int]]:
|
|
2452
|
+
"""Split batch into roughly equal chunks across GPUs.
|
|
2453
|
+
|
|
2454
|
+
Args:
|
|
2455
|
+
B: Total batch size
|
|
2456
|
+
num_gpus: Number of GPUs
|
|
2457
|
+
|
|
2458
|
+
Returns:
|
|
2459
|
+
List of (start, end) tuples for each GPU
|
|
2460
|
+
"""
|
|
2461
|
+
batch_per_gpu = B // num_gpus
|
|
2462
|
+
remainder = B % num_gpus
|
|
2463
|
+
|
|
2464
|
+
splits = []
|
|
2465
|
+
start = 0
|
|
2466
|
+
for i in range(num_gpus):
|
|
2467
|
+
# Distribute remainder across first few GPUs
|
|
2468
|
+
size = batch_per_gpu + (1 if i < remainder else 0)
|
|
2469
|
+
end = start + size
|
|
2470
|
+
splits.append((start, end))
|
|
2471
|
+
start = end
|
|
2472
|
+
|
|
2473
|
+
return splits
|
|
2474
|
+
|
|
2475
|
+
def _decode_parallel(
|
|
2476
|
+
self,
|
|
2477
|
+
batch_splits: List[Tuple[int, int]],
|
|
2478
|
+
syndromes: torch.Tensor,
|
|
2479
|
+
) -> List[Dict[str, torch.Tensor]]:
|
|
2480
|
+
"""Process batch splits on multiple GPUs in parallel.
|
|
2481
|
+
|
|
2482
|
+
Args:
|
|
2483
|
+
batch_splits: List of (start, end) tuples for each GPU
|
|
2484
|
+
syndromes: [B, C] syndrome tensor
|
|
2485
|
+
|
|
2486
|
+
Returns:
|
|
2487
|
+
List of result dictionaries
|
|
2488
|
+
"""
|
|
2489
|
+
import threading
|
|
2490
|
+
|
|
2491
|
+
results = [None] * self.num_gpus
|
|
2492
|
+
threads = []
|
|
2493
|
+
|
|
2494
|
+
def decode_on_gpu(gpu_idx: int, start: int, end: int):
|
|
2495
|
+
"""Decode batch slice on specific GPU."""
|
|
2496
|
+
# Set device context
|
|
2497
|
+
with torch.cuda.device(self.device_ids[gpu_idx]):
|
|
2498
|
+
# Extract batch slice
|
|
2499
|
+
batch_slice = syndromes[start:end].clone() # Clone to avoid issues
|
|
2500
|
+
B_slice = batch_slice.shape[0]
|
|
2501
|
+
|
|
2502
|
+
if B_slice == 0:
|
|
2503
|
+
# Empty batch slice
|
|
2504
|
+
results[gpu_idx] = None
|
|
2505
|
+
return
|
|
2506
|
+
|
|
2507
|
+
# Move to target GPU
|
|
2508
|
+
device_str = f"{self.device_type}:{self.device_ids[gpu_idx]}"
|
|
2509
|
+
batch_slice = batch_slice.to(device_str)
|
|
2510
|
+
|
|
2511
|
+
# Allocate tensors on this GPU
|
|
2512
|
+
tensors = self._allocate_device_tensors(B_slice, self.device_ids[gpu_idx])
|
|
2513
|
+
|
|
2514
|
+
# Use GPU-specific graph and log_prior_ratios
|
|
2515
|
+
graph = self.graphs[gpu_idx]
|
|
2516
|
+
log_prior_ratios = self.log_prior_ratios_per_gpu[gpu_idx]
|
|
2517
|
+
explicit_gammas = (
|
|
2518
|
+
self.explicit_gammas_per_gpu[gpu_idx] if self.explicit_gammas_per_gpu else None
|
|
2519
|
+
)
|
|
2520
|
+
|
|
2521
|
+
# Run decode loop on this GPU
|
|
2522
|
+
result = self._decode_loop_single_gpu(
|
|
2523
|
+
batch_slice,
|
|
2524
|
+
tensors,
|
|
2525
|
+
graph,
|
|
2526
|
+
log_prior_ratios,
|
|
2527
|
+
explicit_gammas,
|
|
2528
|
+
gpu_idx,
|
|
2529
|
+
)
|
|
2530
|
+
results[gpu_idx] = result
|
|
2531
|
+
|
|
2532
|
+
# Launch threads for parallel execution
|
|
2533
|
+
for gpu_idx, (start, end) in enumerate(batch_splits):
|
|
2534
|
+
if start < end: # Only if batch slice is non-empty
|
|
2535
|
+
thread = threading.Thread(target=decode_on_gpu, args=(gpu_idx, start, end))
|
|
2536
|
+
thread.start()
|
|
2537
|
+
threads.append(thread)
|
|
2538
|
+
|
|
2539
|
+
# Wait for all threads
|
|
2540
|
+
for thread in threads:
|
|
2541
|
+
thread.join()
|
|
2542
|
+
|
|
2543
|
+
return results
|
|
2544
|
+
|
|
2545
|
+
def _gather_results(
|
|
2546
|
+
self,
|
|
2547
|
+
results: List[Dict[str, torch.Tensor]],
|
|
2548
|
+
B: int,
|
|
2549
|
+
batch_splits: List[Tuple[int, int]],
|
|
2550
|
+
) -> Dict[str, torch.Tensor]:
|
|
2551
|
+
"""Gather results from all GPUs and concatenate.
|
|
2552
|
+
|
|
2553
|
+
Args:
|
|
2554
|
+
results: List of result dictionaries from each GPU
|
|
2555
|
+
B: Total batch size
|
|
2556
|
+
batch_splits: List of (start, end) tuples for each GPU (for unconverged index adjustment)
|
|
2557
|
+
|
|
2558
|
+
Returns:
|
|
2559
|
+
Concatenated result dictionary
|
|
2560
|
+
"""
|
|
2561
|
+
# Move all results to first GPU for concatenation
|
|
2562
|
+
target_device = f"{self.device_type}:{self.device_ids[0]}"
|
|
2563
|
+
|
|
2564
|
+
gathered = {
|
|
2565
|
+
"errors": [],
|
|
2566
|
+
"weights": [],
|
|
2567
|
+
"valid_mask": [],
|
|
2568
|
+
"iterations": [],
|
|
2569
|
+
"found_count": [],
|
|
2570
|
+
"unconverged_indices": [],
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
for gpu_idx, result in enumerate(results):
|
|
2574
|
+
if result is not None:
|
|
2575
|
+
gathered["errors"].append(result["errors"].to(target_device))
|
|
2576
|
+
gathered["weights"].append(result["weights"].to(target_device))
|
|
2577
|
+
gathered["valid_mask"].append(result["valid_mask"].to(target_device))
|
|
2578
|
+
gathered["iterations"].append(result["iterations"].to(target_device))
|
|
2579
|
+
gathered["found_count"].append(result["found_count"].to(target_device))
|
|
2580
|
+
# Adjust unconverged indices to global batch coordinates
|
|
2581
|
+
if "unconverged_indices" in result and len(result["unconverged_indices"]) > 0:
|
|
2582
|
+
start, _ = batch_splits[gpu_idx]
|
|
2583
|
+
local_indices = result["unconverged_indices"].to(target_device)
|
|
2584
|
+
global_indices = start + local_indices
|
|
2585
|
+
gathered["unconverged_indices"].append(global_indices)
|
|
2586
|
+
|
|
2587
|
+
|
|
2588
|
+
# Concatenate on target device
|
|
2589
|
+
# Handle case where all results might be None (shouldn't happen, but be safe)
|
|
2590
|
+
if not gathered["errors"]:
|
|
2591
|
+
raise RuntimeError("No valid results from any GPU. All decode threads may have failed.")
|
|
2592
|
+
|
|
2593
|
+
# Concatenate unconverged indices if any
|
|
2594
|
+
if gathered["unconverged_indices"]:
|
|
2595
|
+
unconverged = torch.cat(gathered["unconverged_indices"], dim=0)
|
|
2596
|
+
else:
|
|
2597
|
+
unconverged = torch.tensor([], dtype=torch.int32, device=target_device)
|
|
2598
|
+
|
|
2599
|
+
out = {
|
|
2600
|
+
"errors": torch.cat(gathered["errors"], dim=0),
|
|
2601
|
+
"weights": torch.cat(gathered["weights"], dim=0),
|
|
2602
|
+
"valid_mask": torch.cat(gathered["valid_mask"], dim=0),
|
|
2603
|
+
"iterations": torch.cat(gathered["iterations"], dim=0),
|
|
2604
|
+
"found_count": torch.cat(gathered["found_count"], dim=0),
|
|
2605
|
+
"unconverged_indices": unconverged,
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
return out
|
|
2609
|
+
|
|
2610
|
+
def _run_bp_iteration(
|
|
2611
|
+
self,
|
|
2612
|
+
t: int,
|
|
2613
|
+
tensors: Dict,
|
|
2614
|
+
B: int,
|
|
2615
|
+
active_idx: torch.Tensor,
|
|
2616
|
+
B_active: int,
|
|
2617
|
+
graph: "CSRGraph",
|
|
2618
|
+
log_prior_ratios: torch.Tensor,
|
|
2619
|
+
gpu_idx: int,
|
|
2620
|
+
in_leg_python: bool,
|
|
2621
|
+
force_hard: bool = False,
|
|
2622
|
+
skip_all_done_sync: bool = False,
|
|
2623
|
+
prior_batched: bool = False,
|
|
2624
|
+
wj_override: Optional[torch.Tensor] = None,
|
|
2625
|
+
) -> Tuple[torch.Tensor, int, bool]:
|
|
2626
|
+
"""Run a single BP iteration (shared between pre-iter and relay legs).
|
|
2627
|
+
|
|
2628
|
+
Args:
|
|
2629
|
+
t: Iteration index within the current leg
|
|
2630
|
+
tensors: Device tensors dictionary
|
|
2631
|
+
B: Batch size
|
|
2632
|
+
active_idx: Active lane indices tensor
|
|
2633
|
+
B_active: Number of active lanes
|
|
2634
|
+
graph: CSRGraph instance
|
|
2635
|
+
log_prior_ratios: [1, V] log prior ratios
|
|
2636
|
+
gpu_idx: GPU index
|
|
2637
|
+
in_leg_python: Whether we're in a relay leg (affects sought_solutions logic)
|
|
2638
|
+
force_hard: Whether to force hard_dec parity check (pre-iter only)
|
|
2639
|
+
|
|
2640
|
+
Returns:
|
|
2641
|
+
Tuple of (active_idx, B_active, all_done)
|
|
2642
|
+
"""
|
|
2643
|
+
# Make explicit local copies of active_idx / B_active so we can mutate
|
|
2644
|
+
# them safely without confusing Python's scoping rules. The function
|
|
2645
|
+
# returns the updated versions to the caller.
|
|
2646
|
+
local_active_idx = active_idx
|
|
2647
|
+
local_B_active = B_active
|
|
2648
|
+
|
|
2649
|
+
# Compute Rust-like alpha schedule
|
|
2650
|
+
if self.normalized_min_sum_alpha is None:
|
|
2651
|
+
self._alpha_current = 1.0
|
|
2652
|
+
else:
|
|
2653
|
+
a = float(self.normalized_min_sum_alpha)
|
|
2654
|
+
if a == 0.0:
|
|
2655
|
+
s = float(self.alpha_iteration_scaling_factor or 1.0)
|
|
2656
|
+
self._alpha_current = 1.0 - (2.0 ** (-((t + 1) / s)))
|
|
2657
|
+
elif a < 0.0:
|
|
2658
|
+
self._alpha_current = 1.0
|
|
2659
|
+
else:
|
|
2660
|
+
self._alpha_current = a
|
|
2661
|
+
|
|
2662
|
+
# Build active lane compaction for efficiency (only for non-throughput mode)
|
|
2663
|
+
# In throughput mode we keep the caller-provided active index set.
|
|
2664
|
+
# For batch=1, skip expensive compaction (always active, no sync needed)
|
|
2665
|
+
if self.cfg.perf == "default":
|
|
2666
|
+
if B == 1:
|
|
2667
|
+
# Skip compaction for batch=1 - always active, avoid sync overhead
|
|
2668
|
+
device_str = self._device_strs[self.device_ids[gpu_idx]]
|
|
2669
|
+
compact_active_idx = torch.tensor([0], dtype=torch.int32, device=device_str)
|
|
2670
|
+
compact_B_active = 1
|
|
2671
|
+
else:
|
|
2672
|
+
compact_active_idx, compact_B_active = self._compact_active_indices_on_device(
|
|
2673
|
+
tensors, B, gpu_idx
|
|
2674
|
+
)
|
|
2675
|
+
else:
|
|
2676
|
+
compact_active_idx, compact_B_active = local_active_idx, local_B_active
|
|
2677
|
+
|
|
2678
|
+
# Message passing: C2V and V2C
|
|
2679
|
+
if self.cfg.perf == "throughput":
|
|
2680
|
+
if local_B_active == 0:
|
|
2681
|
+
pass # nothing to do for BTILE kernels this iteration
|
|
2682
|
+
else:
|
|
2683
|
+
# Check if we should use layered scheduling for this iteration.
|
|
2684
|
+
# layered_k_iters semantics:
|
|
2685
|
+
# 0 -> flood-fill only (no layered iterations)
|
|
2686
|
+
# <0 -> always layered (all iterations in layered mode)
|
|
2687
|
+
# >0 -> use layered for first K iterations, then flood-fill
|
|
2688
|
+
use_layered = (
|
|
2689
|
+
self.scheduling == "layered"
|
|
2690
|
+
and self.block_metadata is not None
|
|
2691
|
+
and (self.layered_k_iters < 0 or t < self.layered_k_iters)
|
|
2692
|
+
)
|
|
2693
|
+
|
|
2694
|
+
if use_layered:
|
|
2695
|
+
# Block-layered scheduling: process blocks sequentially
|
|
2696
|
+
block_meta = self.block_metadata
|
|
2697
|
+
num_blocks = block_meta["num_blocks"]
|
|
2698
|
+
block_chk_start = block_meta["block_chk_start"]
|
|
2699
|
+
block_chk_end = block_meta["block_chk_end"]
|
|
2700
|
+
block_var_ptr = block_meta["block_var_ptr"]
|
|
2701
|
+
block_vars = block_meta["block_vars"]
|
|
2702
|
+
|
|
2703
|
+
# Rotate block order (simple rotation: shift by iteration)
|
|
2704
|
+
# This helps with early syndrome drop by varying block processing order
|
|
2705
|
+
block_perm = [(t + b) % num_blocks for b in range(num_blocks)]
|
|
2706
|
+
|
|
2707
|
+
# Use CPU-side precomputed ranges
|
|
2708
|
+
block_chk_start_cpu = block_meta["block_chk_start_cpu"]
|
|
2709
|
+
block_chk_end_cpu = block_meta["block_chk_end_cpu"]
|
|
2710
|
+
block_var_ptr_cpu = block_meta["block_var_ptr_cpu"]
|
|
2711
|
+
|
|
2712
|
+
if self.layered_block_policy == "color_use_delta_trick":
|
|
2713
|
+
# Delta-trick layered path: use modified C2V that updates L in-place
|
|
2714
|
+
for block_idx_perm in block_perm:
|
|
2715
|
+
chk_start = int(block_chk_start_cpu[block_idx_perm])
|
|
2716
|
+
chk_end = int(block_chk_end_cpu[block_idx_perm])
|
|
2717
|
+
if chk_end <= chk_start:
|
|
2718
|
+
continue
|
|
2719
|
+
var_start = int(block_var_ptr_cpu[block_idx_perm])
|
|
2720
|
+
var_end = int(block_var_ptr_cpu[block_idx_perm + 1])
|
|
2721
|
+
if var_end <= var_start:
|
|
2722
|
+
continue
|
|
2723
|
+
# C2V delta-trick for this block (updates nuT and L)
|
|
2724
|
+
self._launch_c2v_btile(
|
|
2725
|
+
tensors,
|
|
2726
|
+
B,
|
|
2727
|
+
local_active_idx,
|
|
2728
|
+
local_B_active,
|
|
2729
|
+
graph=graph,
|
|
2730
|
+
gpu_idx=gpu_idx,
|
|
2731
|
+
chk_start=chk_start,
|
|
2732
|
+
chk_end=chk_end,
|
|
2733
|
+
use_delta=True,
|
|
2734
|
+
)
|
|
2735
|
+
# After all blocks, update posterior_ratios and hard_dec from L
|
|
2736
|
+
BLOCK_V = 128
|
|
2737
|
+
bt = BLOCK_V
|
|
2738
|
+
grid_dim1 = (local_B_active + bt - 1) // bt
|
|
2739
|
+
grid = (self.V, grid_dim1)
|
|
2740
|
+
post_from_L_kernel[grid](
|
|
2741
|
+
tensors["L"],
|
|
2742
|
+
tensors["posterior_ratios"],
|
|
2743
|
+
tensors["hard_dec"],
|
|
2744
|
+
local_active_idx,
|
|
2745
|
+
B,
|
|
2746
|
+
local_B_active,
|
|
2747
|
+
self.V,
|
|
2748
|
+
dtype_enum=self._dtype_enum,
|
|
2749
|
+
BLOCK_V=BLOCK_V,
|
|
2750
|
+
)
|
|
2751
|
+
else:
|
|
2752
|
+
for block_idx_perm in block_perm:
|
|
2753
|
+
chk_start = int(block_chk_start_cpu[block_idx_perm])
|
|
2754
|
+
chk_end = int(block_chk_end_cpu[block_idx_perm])
|
|
2755
|
+
|
|
2756
|
+
# Skip empty blocks (can occur with edge-balanced splitting on irregular degree distributions)
|
|
2757
|
+
if chk_end <= chk_start:
|
|
2758
|
+
continue
|
|
2759
|
+
|
|
2760
|
+
# Get variable range for this block
|
|
2761
|
+
var_start = int(block_var_ptr_cpu[block_idx_perm])
|
|
2762
|
+
var_end = int(block_var_ptr_cpu[block_idx_perm + 1])
|
|
2763
|
+
|
|
2764
|
+
# Skip if no variables in this block
|
|
2765
|
+
if var_end <= var_start:
|
|
2766
|
+
continue
|
|
2767
|
+
|
|
2768
|
+
# C2V for block
|
|
2769
|
+
self._launch_c2v_btile(
|
|
2770
|
+
tensors,
|
|
2771
|
+
B,
|
|
2772
|
+
local_active_idx,
|
|
2773
|
+
local_B_active,
|
|
2774
|
+
graph=graph,
|
|
2775
|
+
gpu_idx=gpu_idx,
|
|
2776
|
+
chk_start=chk_start,
|
|
2777
|
+
chk_end=chk_end,
|
|
2778
|
+
)
|
|
2779
|
+
# V2C for block vars
|
|
2780
|
+
self._launch_v2c_btile(
|
|
2781
|
+
tensors,
|
|
2782
|
+
B,
|
|
2783
|
+
write_hard=True,
|
|
2784
|
+
active_idx=local_active_idx,
|
|
2785
|
+
B_active=local_B_active,
|
|
2786
|
+
graph=graph,
|
|
2787
|
+
log_prior_ratios=log_prior_ratios,
|
|
2788
|
+
gpu_idx=gpu_idx,
|
|
2789
|
+
block_vars=block_vars,
|
|
2790
|
+
block_var_ptr=block_var_ptr,
|
|
2791
|
+
block_idx=block_idx_perm,
|
|
2792
|
+
prior_batched=prior_batched,
|
|
2793
|
+
)
|
|
2794
|
+
else:
|
|
2795
|
+
# Flood-fill scheduling: process all checks then all vars
|
|
2796
|
+
self._launch_c2v_btile(
|
|
2797
|
+
tensors,
|
|
2798
|
+
B,
|
|
2799
|
+
local_active_idx,
|
|
2800
|
+
local_B_active,
|
|
2801
|
+
graph=graph,
|
|
2802
|
+
gpu_idx=gpu_idx,
|
|
2803
|
+
)
|
|
2804
|
+
self._launch_v2c_btile(
|
|
2805
|
+
tensors,
|
|
2806
|
+
B,
|
|
2807
|
+
write_hard=True,
|
|
2808
|
+
active_idx=local_active_idx,
|
|
2809
|
+
B_active=local_B_active,
|
|
2810
|
+
graph=graph,
|
|
2811
|
+
log_prior_ratios=log_prior_ratios,
|
|
2812
|
+
gpu_idx=gpu_idx,
|
|
2813
|
+
block_vars=None,
|
|
2814
|
+
block_var_ptr=None,
|
|
2815
|
+
block_idx=-1,
|
|
2816
|
+
prior_batched=prior_batched,
|
|
2817
|
+
)
|
|
2818
|
+
else:
|
|
2819
|
+
self._launch_c2v_kernel(tensors, B, graph=graph, gpu_idx=gpu_idx)
|
|
2820
|
+
# Fused V2C + gamma mixing updates lambda_ internally
|
|
2821
|
+
self._launch_v2c_fused_gamma_kernel(
|
|
2822
|
+
tensors,
|
|
2823
|
+
B,
|
|
2824
|
+
graph=graph,
|
|
2825
|
+
log_prior_ratios=log_prior_ratios,
|
|
2826
|
+
gpu_idx=gpu_idx,
|
|
2827
|
+
prior_batched=prior_batched,
|
|
2828
|
+
)
|
|
2829
|
+
|
|
2830
|
+
# Check parity and select solutions every iteration
|
|
2831
|
+
# Use compacted indices in non-throughput mode; in throughput mode we
|
|
2832
|
+
# pass through the caller-provided active index set.
|
|
2833
|
+
self._check_parity_and_select(
|
|
2834
|
+
tensors,
|
|
2835
|
+
B,
|
|
2836
|
+
compact_active_idx,
|
|
2837
|
+
compact_B_active,
|
|
2838
|
+
force_hard=force_hard,
|
|
2839
|
+
graph=graph,
|
|
2840
|
+
gpu_idx=gpu_idx,
|
|
2841
|
+
prior_batched=prior_batched,
|
|
2842
|
+
wj_override=wj_override,
|
|
2843
|
+
)
|
|
2844
|
+
# Device-side freeze + iter_counter increment + all-done reduction (fused)
|
|
2845
|
+
freeze_finished_lanes_kernel[(B,)](
|
|
2846
|
+
tensors["best_errors"],
|
|
2847
|
+
tensors["hard_dec"],
|
|
2848
|
+
tensors["gamma"],
|
|
2849
|
+
tensors["active"],
|
|
2850
|
+
tensors["found_count"],
|
|
2851
|
+
tensors["first_iter"],
|
|
2852
|
+
tensors["iter_counter"],
|
|
2853
|
+
tensors["sought_solutions"],
|
|
2854
|
+
self.stop_nconv,
|
|
2855
|
+
in_leg_python,
|
|
2856
|
+
True, # count_iteration
|
|
2857
|
+
self.V,
|
|
2858
|
+
tensors["all_done_flag"],
|
|
2859
|
+
tensors["arrival_counter"],
|
|
2860
|
+
B,
|
|
2861
|
+
)
|
|
2862
|
+
# Active compaction eliminated: btile kernels use active[] mask directly to skip
|
|
2863
|
+
# frozen lanes, avoiding count_active_blocks + compare + .item() sync overhead.
|
|
2864
|
+
|
|
2865
|
+
# Check flag every iteration to match Rust behavior exactly
|
|
2866
|
+
if skip_all_done_sync:
|
|
2867
|
+
# Skip sync - will be checked at chunk boundary
|
|
2868
|
+
all_done = False
|
|
2869
|
+
else:
|
|
2870
|
+
all_done = tensors["all_done_flag"].item()
|
|
2871
|
+
if all_done:
|
|
2872
|
+
tensors["all_done_flag"].fill_(0)
|
|
2873
|
+
tensors["in_leg"].fill_(0)
|
|
2874
|
+
|
|
2875
|
+
return local_active_idx, local_B_active, all_done
|
|
2876
|
+
|
|
2877
|
+
def _unconverged_hard_dec(self, tensors: Dict) -> torch.Tensor:
|
|
2878
|
+
"""Return the [B, V] hard decision used for lanes with no valid solution.
|
|
2879
|
+
|
|
2880
|
+
Args:
|
|
2881
|
+
tensors: Device tensors dictionary.
|
|
2882
|
+
|
|
2883
|
+
Returns:
|
|
2884
|
+
``first_leg_hard_dec`` for "first_leg", the live ``hard_dec`` for "last_leg".
|
|
2885
|
+
"""
|
|
2886
|
+
if self.unconverged_correction == "first_leg":
|
|
2887
|
+
return tensors["first_leg_hard_dec"]
|
|
2888
|
+
return tensors["hard_dec"]
|
|
2889
|
+
|
|
2890
|
+
def _check_fast_max_unconverged_pct(
|
|
2891
|
+
self, tensors: Dict, B: int, leg_count: int = -1
|
|
2892
|
+
) -> Optional[Dict[str, torch.Tensor]]:
|
|
2893
|
+
"""Check if early exit threshold is met and return results if so.
|
|
2894
|
+
|
|
2895
|
+
Args:
|
|
2896
|
+
tensors: Device tensors dictionary
|
|
2897
|
+
B: Batch size
|
|
2898
|
+
leg_count: Current leg count (for debugging/logging, -1 if unknown)
|
|
2899
|
+
|
|
2900
|
+
Returns:
|
|
2901
|
+
Result dictionary if early exit threshold met, None otherwise
|
|
2902
|
+
"""
|
|
2903
|
+
if self.fast_max_unconverged_pct is None:
|
|
2904
|
+
return None
|
|
2905
|
+
|
|
2906
|
+
unconverged_count = (tensors["found_count"] < self.stop_nconv).sum().item()
|
|
2907
|
+
unconverged_pct = unconverged_count / B if B > 0 else 0.0
|
|
2908
|
+
# Only exit early if the percentage of unconverged cases is < threshold
|
|
2909
|
+
# AND at least some cases have converged (unconverged_pct < 1.0)
|
|
2910
|
+
# This ensures we don't exit when all cases are still unconverged
|
|
2911
|
+
if unconverged_pct < self.fast_max_unconverged_pct and unconverged_pct < 1.0:
|
|
2912
|
+
# Early exit: compute unconverged indices and return
|
|
2913
|
+
unconverged_mask = tensors["found_count"] < self.stop_nconv
|
|
2914
|
+
unconverged_idx = torch.where(unconverged_mask)[0]
|
|
2915
|
+
has_solution = torch.isfinite(tensors["best_weights"])
|
|
2916
|
+
fallback_errors = torch.where(
|
|
2917
|
+
has_solution.view(-1, 1),
|
|
2918
|
+
tensors["best_errors"],
|
|
2919
|
+
self._unconverged_hard_dec(tensors),
|
|
2920
|
+
)
|
|
2921
|
+
errors = bitpack_errors(fallback_errors) if self.bitpack_output else fallback_errors
|
|
2922
|
+
# Compute iterations for all lanes (converged lanes use first_iter, unconverged use iter_counter)
|
|
2923
|
+
# Note: Unconverged lanes have partial iterations from early exit, but they will be filtered out
|
|
2924
|
+
# by the caller and reprocessed with full iterations in the difficult batch.
|
|
2925
|
+
iters = torch.where(
|
|
2926
|
+
tensors["first_iter"] >= 0, tensors["first_iter"], tensors["iter_counter"]
|
|
2927
|
+
)
|
|
2928
|
+
result = {
|
|
2929
|
+
"errors": errors,
|
|
2930
|
+
"weights": tensors["best_weights"],
|
|
2931
|
+
"valid_mask": tensors["found_count"] >= self.stop_nconv,
|
|
2932
|
+
"iterations": iters,
|
|
2933
|
+
"found_count": tensors["found_count"].to(torch.int32),
|
|
2934
|
+
"unconverged_indices": unconverged_idx.to(torch.int32),
|
|
2935
|
+
}
|
|
2936
|
+
return result
|
|
2937
|
+
return None
|
|
2938
|
+
|
|
2939
|
+
def _run_bp_iterations_grid_stride(
|
|
2940
|
+
self,
|
|
2941
|
+
start_iter: int,
|
|
2942
|
+
max_iter: int,
|
|
2943
|
+
tensors: Dict,
|
|
2944
|
+
B: int,
|
|
2945
|
+
active_idx: torch.Tensor,
|
|
2946
|
+
B_active: int,
|
|
2947
|
+
graph: "CSRGraph",
|
|
2948
|
+
log_prior_ratios: torch.Tensor,
|
|
2949
|
+
gpu_idx: int,
|
|
2950
|
+
in_leg_python: bool,
|
|
2951
|
+
force_hard: bool = False,
|
|
2952
|
+
prior_batched: bool = False,
|
|
2953
|
+
wj_override: Optional[torch.Tensor] = None,
|
|
2954
|
+
) -> Tuple[torch.Tensor, int, bool, int]:
|
|
2955
|
+
"""Run multiple BP iterations using C2V grid-stride kernel with separate V2C/parity/select phases.
|
|
2956
|
+
|
|
2957
|
+
This method uses c2v_min_sum_grid_stride_kernel (C2V only) with grid-stride parallelism
|
|
2958
|
+
to efficiently split work across GPU SMs. V2C, parity, and selection run in separate
|
|
2959
|
+
kernel launches after C2V completes.
|
|
2960
|
+
|
|
2961
|
+
Performance:
|
|
2962
|
+
- Uses grid-stride parallelism: grid = (B * work_items_per_batch,) splits work across SMs
|
|
2963
|
+
- 5 kernel launches per iteration (C2V, V2C, parity, select, freeze)
|
|
2964
|
+
- 1 sync per iteration for convergence checking
|
|
2965
|
+
|
|
2966
|
+
Returns:
|
|
2967
|
+
Tuple of (active_idx, B_active, all_done, iterations_run)
|
|
2968
|
+
"""
|
|
2969
|
+
if graph is None:
|
|
2970
|
+
graph = self.graphs[gpu_idx]
|
|
2971
|
+
if log_prior_ratios is None:
|
|
2972
|
+
log_prior_ratios = self.log_prior_ratios_per_gpu[gpu_idx]
|
|
2973
|
+
|
|
2974
|
+
gpu_id = self.device_ids[gpu_idx]
|
|
2975
|
+
wj_gpu = wj_override if prior_batched else self.wj_per_gpu[gpu_id]
|
|
2976
|
+
|
|
2977
|
+
alpha_base = self.normalized_min_sum_alpha or 0.0
|
|
2978
|
+
alpha_scaling = float(self.alpha_iteration_scaling_factor or 1.0)
|
|
2979
|
+
use_alpha = self.normalized_min_sum_alpha is not None
|
|
2980
|
+
use_beta = self.offset_min_sum_beta is not None
|
|
2981
|
+
beta = self.offset_min_sum_beta or 0.0
|
|
2982
|
+
|
|
2983
|
+
# Grid calculation depends on ROWS_PER_CHK/ROWS_PER_VAR (will be determined by autotuner)
|
|
2984
|
+
# We need to provide a grid for autotuning, so use a reasonable default
|
|
2985
|
+
# The actual grid will be recalculated after we get the tuned config
|
|
2986
|
+
work_dim = max(self.C, self.V)
|
|
2987
|
+
default_rows_per_chk = 1
|
|
2988
|
+
default_rows_per_var = 1
|
|
2989
|
+
default_grid = (B * work_dim,) # Grid for ROWS_PER_CHK=1 (will be adjusted)
|
|
2990
|
+
|
|
2991
|
+
# Autotune iteration-fused kernel
|
|
2992
|
+
problem_key = {
|
|
2993
|
+
"B": B,
|
|
2994
|
+
"C": self.C,
|
|
2995
|
+
"V": self.V,
|
|
2996
|
+
"E": self.E,
|
|
2997
|
+
"sparsity_hash": graph.sparsity_hash,
|
|
2998
|
+
"dtype_enum": self._dtype_enum,
|
|
2999
|
+
"use_alpha": use_alpha,
|
|
3000
|
+
"use_beta": use_beta,
|
|
3001
|
+
"in_leg": 1 if in_leg_python else 0,
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
# Grid function that adjusts grid based on ROWS_PER_CHK
|
|
3005
|
+
def _grid_fn(cfg):
|
|
3006
|
+
rows_chk = int(cfg.get("ROWS_PER_CHK", default_rows_per_chk))
|
|
3007
|
+
# Each program processes ROWS_PER_CHK consecutive checks/vars
|
|
3008
|
+
# work_items_per_batch = (work_dim + ROWS_PER_CHK - 1) // ROWS_PER_CHK
|
|
3009
|
+
# grid = B * work_items_per_batch
|
|
3010
|
+
work_dim = max(self.C, self.V)
|
|
3011
|
+
work_items_per_batch = (work_dim + rows_chk - 1) // rows_chk
|
|
3012
|
+
grid_size = B * work_items_per_batch
|
|
3013
|
+
return (grid_size,)
|
|
3014
|
+
|
|
3015
|
+
saved = self._get_tuned_meta(
|
|
3016
|
+
kernel_name="c2v_min_sum_grid_stride_kernel",
|
|
3017
|
+
problem_key=problem_key,
|
|
3018
|
+
build_cfgs_fn=build_c2v_grid_stride_configs,
|
|
3019
|
+
kernel=c2v_min_sum_grid_stride_kernel,
|
|
3020
|
+
grid=default_grid,
|
|
3021
|
+
args=(
|
|
3022
|
+
tensors["mu"],
|
|
3023
|
+
tensors["nu"],
|
|
3024
|
+
graph.chk_ptr,
|
|
3025
|
+
graph.chk_edges,
|
|
3026
|
+
graph.var_ptr,
|
|
3027
|
+
graph.var_edges,
|
|
3028
|
+
graph.edge_var,
|
|
3029
|
+
tensors["lambda_"],
|
|
3030
|
+
tensors["posterior_ratios"],
|
|
3031
|
+
tensors["hard_dec"],
|
|
3032
|
+
tensors["syndrome"],
|
|
3033
|
+
log_prior_ratios,
|
|
3034
|
+
tensors["gamma"],
|
|
3035
|
+
tensors["active"],
|
|
3036
|
+
tensors["check_ok"],
|
|
3037
|
+
tensors["best_weights"],
|
|
3038
|
+
tensors["best_errors"],
|
|
3039
|
+
tensors["valid_solutions"],
|
|
3040
|
+
tensors["found_count"],
|
|
3041
|
+
wj_gpu,
|
|
3042
|
+
B,
|
|
3043
|
+
self.C,
|
|
3044
|
+
self.V,
|
|
3045
|
+
self.E,
|
|
3046
|
+
0, # iteration (will vary)
|
|
3047
|
+
alpha_base,
|
|
3048
|
+
alpha_scaling,
|
|
3049
|
+
use_alpha,
|
|
3050
|
+
use_beta,
|
|
3051
|
+
beta,
|
|
3052
|
+
1 if in_leg_python else 0,
|
|
3053
|
+
self._dtype_enum,
|
|
3054
|
+
),
|
|
3055
|
+
meta_base={},
|
|
3056
|
+
number=20,
|
|
3057
|
+
grid_fn=_grid_fn,
|
|
3058
|
+
log_name=f"c2v_min_sum_grid_stride_kernel[gpu={gpu_idx}]",
|
|
3059
|
+
)
|
|
3060
|
+
|
|
3061
|
+
block_size = int(saved["BLOCK_SIZE"])
|
|
3062
|
+
num_warps = int(saved.get("num_warps", 4))
|
|
3063
|
+
num_stages = int(saved.get("num_stages", 1))
|
|
3064
|
+
rows_per_chk = int(saved.get("ROWS_PER_CHK", default_rows_per_chk))
|
|
3065
|
+
rows_per_var = int(saved.get("ROWS_PER_VAR", default_rows_per_var))
|
|
3066
|
+
|
|
3067
|
+
# Recalculate grid based on tuned ROWS_PER_CHK
|
|
3068
|
+
# Each program processes ROWS_PER_CHK consecutive checks/vars
|
|
3069
|
+
work_dim = max(self.C, self.V)
|
|
3070
|
+
work_items_per_batch = (work_dim + rows_per_chk - 1) // rows_per_chk
|
|
3071
|
+
grid = (B * work_items_per_batch,)
|
|
3072
|
+
|
|
3073
|
+
# Cache other kernel configs to avoid overhead
|
|
3074
|
+
from .kernels import (
|
|
3075
|
+
check_and_select_kernel,
|
|
3076
|
+
v2c_and_marginals_fused_gamma_kernel,
|
|
3077
|
+
parity_from_hard_kernel,
|
|
3078
|
+
freeze_finished_lanes_kernel,
|
|
3079
|
+
)
|
|
3080
|
+
from .autotune import build_check_select_configs, build_v2c_configs, build_parity_configs
|
|
3081
|
+
|
|
3082
|
+
# Cache V2C config
|
|
3083
|
+
v2c_problem_key = {
|
|
3084
|
+
"B": B,
|
|
3085
|
+
"V": self.V,
|
|
3086
|
+
"E": self.E,
|
|
3087
|
+
"dtype": self.dtype_str,
|
|
3088
|
+
"sparsity_hash": graph.sparsity_hash,
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
def _v2c_grid_fn(cfg):
|
|
3092
|
+
rows = int(cfg.get("ROWS_PER_VAR", self.cfg.rows_per_var))
|
|
3093
|
+
return (((B * self.V) + rows - 1) // rows,)
|
|
3094
|
+
|
|
3095
|
+
v2c_saved = self._get_tuned_meta(
|
|
3096
|
+
kernel_name="v2c_and_marginals_fused_gamma_kernel",
|
|
3097
|
+
problem_key=v2c_problem_key,
|
|
3098
|
+
build_cfgs_fn=build_v2c_configs,
|
|
3099
|
+
kernel=v2c_and_marginals_fused_gamma_kernel,
|
|
3100
|
+
grid=(1,),
|
|
3101
|
+
args=(
|
|
3102
|
+
tensors["nu"],
|
|
3103
|
+
tensors["mu"],
|
|
3104
|
+
graph.var_ptr,
|
|
3105
|
+
graph.var_edges,
|
|
3106
|
+
tensors["lambda_"],
|
|
3107
|
+
tensors["posterior_ratios"],
|
|
3108
|
+
tensors["hard_dec"],
|
|
3109
|
+
tensors["active"],
|
|
3110
|
+
log_prior_ratios,
|
|
3111
|
+
tensors["gamma"],
|
|
3112
|
+
),
|
|
3113
|
+
meta_base=dict(
|
|
3114
|
+
B=B, V=self.V, E=self.E, dtype_enum=self._dtype_enum, PRIOR_BATCHED=prior_batched
|
|
3115
|
+
),
|
|
3116
|
+
number=20,
|
|
3117
|
+
grid_fn=_v2c_grid_fn,
|
|
3118
|
+
log_name=f"v2c_and_marginals_fused_gamma_kernel[gpu={gpu_idx}]",
|
|
3119
|
+
)
|
|
3120
|
+
v2c_rows = int(v2c_saved.get("ROWS_PER_VAR", self.cfg.rows_per_var))
|
|
3121
|
+
v2c_grid = (((B * self.V) + v2c_rows - 1) // v2c_rows,)
|
|
3122
|
+
v2c_block_size = int(v2c_saved.get("BLOCK_SIZE", 64))
|
|
3123
|
+
v2c_num_warps = int(v2c_saved.get("num_warps", 0))
|
|
3124
|
+
v2c_num_stages = int(v2c_saved.get("num_stages", 0))
|
|
3125
|
+
|
|
3126
|
+
# Cache parity config
|
|
3127
|
+
parity_grid = ((B * self.C + self.cfg.rows_per_chk - 1) // self.cfg.rows_per_chk,)
|
|
3128
|
+
parity_problem_key = {
|
|
3129
|
+
"B": B,
|
|
3130
|
+
"C": self.C,
|
|
3131
|
+
"V": self.V,
|
|
3132
|
+
"E": self.E,
|
|
3133
|
+
"ROWS_PER_CHK": self.cfg.rows_per_chk,
|
|
3134
|
+
"dtype": self.dtype_str,
|
|
3135
|
+
"sparsity_hash": graph.sparsity_hash,
|
|
3136
|
+
}
|
|
3137
|
+
parity_meta_base = dict(
|
|
3138
|
+
B=B, C=self.C, V=self.V, E=self.E, ROWS_PER_CHK=self.cfg.rows_per_chk
|
|
3139
|
+
)
|
|
3140
|
+
|
|
3141
|
+
parity_saved = self._get_tuned_meta(
|
|
3142
|
+
kernel_name="parity_from_hard_kernel",
|
|
3143
|
+
problem_key=parity_problem_key,
|
|
3144
|
+
build_cfgs_fn=build_parity_configs,
|
|
3145
|
+
kernel=parity_from_hard_kernel,
|
|
3146
|
+
grid=parity_grid,
|
|
3147
|
+
args=(
|
|
3148
|
+
tensors["hard_dec"],
|
|
3149
|
+
graph.chk_ptr,
|
|
3150
|
+
graph.chk_edges,
|
|
3151
|
+
graph.edge_var,
|
|
3152
|
+
tensors["syndrome"],
|
|
3153
|
+
tensors["check_ok"],
|
|
3154
|
+
),
|
|
3155
|
+
meta_base=parity_meta_base,
|
|
3156
|
+
number=20,
|
|
3157
|
+
log_name=f"parity_from_hard_kernel[gpu={gpu_idx}]",
|
|
3158
|
+
)
|
|
3159
|
+
parity_block_size = int(parity_saved["BLOCK_SIZE"])
|
|
3160
|
+
parity_num_warps = int(parity_saved.get("num_warps", 0))
|
|
3161
|
+
parity_num_stages = int(parity_saved.get("num_stages", 0))
|
|
3162
|
+
|
|
3163
|
+
# Cache select config
|
|
3164
|
+
select_grid = (B,)
|
|
3165
|
+
select_problem_key = {
|
|
3166
|
+
"B": B,
|
|
3167
|
+
"C": self.C,
|
|
3168
|
+
"V": self.V,
|
|
3169
|
+
"dtype": self.dtype_str,
|
|
3170
|
+
"sparsity_hash": graph.sparsity_hash,
|
|
3171
|
+
}
|
|
3172
|
+
select_saved = self._get_tuned_meta(
|
|
3173
|
+
kernel_name="check_and_select_kernel",
|
|
3174
|
+
problem_key=select_problem_key,
|
|
3175
|
+
build_cfgs_fn=build_check_select_configs,
|
|
3176
|
+
kernel=check_and_select_kernel,
|
|
3177
|
+
grid=select_grid,
|
|
3178
|
+
args=(
|
|
3179
|
+
tensors["check_ok"],
|
|
3180
|
+
tensors["posterior_ratios"],
|
|
3181
|
+
tensors["hard_dec"],
|
|
3182
|
+
wj_gpu,
|
|
3183
|
+
tensors["best_weights"],
|
|
3184
|
+
tensors["best_errors"],
|
|
3185
|
+
tensors["valid_solutions"],
|
|
3186
|
+
tensors["found_count"],
|
|
3187
|
+
tensors["active"],
|
|
3188
|
+
),
|
|
3189
|
+
meta_base=dict(
|
|
3190
|
+
B=B,
|
|
3191
|
+
C=self.C,
|
|
3192
|
+
V=self.V,
|
|
3193
|
+
dtype_enum=self._dtype_enum,
|
|
3194
|
+
NUM_SOLS=int(self.stop_nconv),
|
|
3195
|
+
PRIOR_BATCHED=prior_batched,
|
|
3196
|
+
),
|
|
3197
|
+
number=20,
|
|
3198
|
+
log_name=f"check_and_select_kernel[gpu={gpu_idx}]",
|
|
3199
|
+
)
|
|
3200
|
+
select_block_c = int(select_saved.get("BLOCK_C", 256))
|
|
3201
|
+
select_block_v = int(select_saved.get("BLOCK_V", 256))
|
|
3202
|
+
select_num_warps = int(select_saved.get("num_warps", 1))
|
|
3203
|
+
select_num_stages = int(select_saved.get("num_stages", 1))
|
|
3204
|
+
|
|
3205
|
+
C_val = self.C
|
|
3206
|
+
V_val = self.V
|
|
3207
|
+
E_val = self.E
|
|
3208
|
+
dtype_enum_val = self._dtype_enum
|
|
3209
|
+
stop_nconv_val = self.stop_nconv
|
|
3210
|
+
in_leg_val = 1 if in_leg_python else 0
|
|
3211
|
+
|
|
3212
|
+
# Cache tensor references
|
|
3213
|
+
active_tensor = tensors["active"]
|
|
3214
|
+
iter_counter_tensor = tensors["iter_counter"]
|
|
3215
|
+
mu_tensor = tensors["mu"]
|
|
3216
|
+
nu_tensor = tensors["nu"]
|
|
3217
|
+
lam_tensor = tensors["lambda_"]
|
|
3218
|
+
posterior_tensor = tensors["posterior_ratios"]
|
|
3219
|
+
hard_dec_tensor = tensors["hard_dec"]
|
|
3220
|
+
syndrome_tensor = tensors["syndrome"]
|
|
3221
|
+
gamma_tensor = tensors["gamma"]
|
|
3222
|
+
check_ok_tensor = tensors["check_ok"]
|
|
3223
|
+
best_weights_tensor = tensors["best_weights"]
|
|
3224
|
+
best_errors_tensor = tensors["best_errors"]
|
|
3225
|
+
valid_solutions_tensor = tensors["valid_solutions"]
|
|
3226
|
+
found_count_tensor = tensors["found_count"]
|
|
3227
|
+
all_done_flag_tensor = tensors["all_done_flag"]
|
|
3228
|
+
sought_solutions_tensor = tensors["sought_solutions"]
|
|
3229
|
+
first_iter_tensor = tensors["first_iter"]
|
|
3230
|
+
|
|
3231
|
+
# Cache graph pointers
|
|
3232
|
+
chk_ptr = graph.chk_ptr
|
|
3233
|
+
chk_edges = graph.chk_edges
|
|
3234
|
+
var_ptr = graph.var_ptr
|
|
3235
|
+
var_edges = graph.var_edges
|
|
3236
|
+
edge_var = graph.edge_var
|
|
3237
|
+
|
|
3238
|
+
# For B=1, ensure active[0] is always 1 (lane is always active)
|
|
3239
|
+
if B == 1:
|
|
3240
|
+
active_tensor[0] = 1
|
|
3241
|
+
|
|
3242
|
+
t = start_iter
|
|
3243
|
+
all_done = False
|
|
3244
|
+
|
|
3245
|
+
while t < start_iter + max_iter and not all_done:
|
|
3246
|
+
# Phase 1: C2V (grid-stride kernel with batch-organized pattern for better cache locality)
|
|
3247
|
+
c2v_min_sum_grid_stride_kernel[grid](
|
|
3248
|
+
mu_tensor,
|
|
3249
|
+
nu_tensor,
|
|
3250
|
+
chk_ptr,
|
|
3251
|
+
chk_edges,
|
|
3252
|
+
var_ptr,
|
|
3253
|
+
var_edges,
|
|
3254
|
+
edge_var,
|
|
3255
|
+
lam_tensor,
|
|
3256
|
+
posterior_tensor,
|
|
3257
|
+
hard_dec_tensor,
|
|
3258
|
+
syndrome_tensor,
|
|
3259
|
+
log_prior_ratios,
|
|
3260
|
+
gamma_tensor,
|
|
3261
|
+
active_tensor,
|
|
3262
|
+
check_ok_tensor,
|
|
3263
|
+
best_weights_tensor,
|
|
3264
|
+
best_errors_tensor,
|
|
3265
|
+
valid_solutions_tensor,
|
|
3266
|
+
found_count_tensor,
|
|
3267
|
+
wj_gpu,
|
|
3268
|
+
B=B,
|
|
3269
|
+
C=C_val,
|
|
3270
|
+
V=V_val,
|
|
3271
|
+
E=E_val,
|
|
3272
|
+
iteration=t,
|
|
3273
|
+
alpha_base=alpha_base,
|
|
3274
|
+
alpha_scaling=alpha_scaling,
|
|
3275
|
+
use_alpha=use_alpha,
|
|
3276
|
+
use_beta=use_beta,
|
|
3277
|
+
beta=beta,
|
|
3278
|
+
in_leg=in_leg_val,
|
|
3279
|
+
dtype_enum=dtype_enum_val,
|
|
3280
|
+
BLOCK_SIZE=block_size,
|
|
3281
|
+
ROWS_PER_CHK=rows_per_chk,
|
|
3282
|
+
ROWS_PER_VAR=rows_per_var,
|
|
3283
|
+
num_warps=num_warps,
|
|
3284
|
+
num_stages=num_stages,
|
|
3285
|
+
)
|
|
3286
|
+
|
|
3287
|
+
# Phase 2: V2C
|
|
3288
|
+
v2c_and_marginals_fused_gamma_kernel[v2c_grid](
|
|
3289
|
+
nu_tensor,
|
|
3290
|
+
mu_tensor,
|
|
3291
|
+
var_ptr,
|
|
3292
|
+
var_edges,
|
|
3293
|
+
lam_tensor,
|
|
3294
|
+
posterior_tensor,
|
|
3295
|
+
hard_dec_tensor,
|
|
3296
|
+
active_tensor,
|
|
3297
|
+
log_prior_ratios,
|
|
3298
|
+
gamma_tensor,
|
|
3299
|
+
B=B,
|
|
3300
|
+
V=V_val,
|
|
3301
|
+
E=E_val,
|
|
3302
|
+
dtype_enum=dtype_enum_val,
|
|
3303
|
+
BLOCK_SIZE=v2c_block_size,
|
|
3304
|
+
ROWS_PER_VAR=v2c_rows,
|
|
3305
|
+
PRIOR_BATCHED=prior_batched,
|
|
3306
|
+
num_warps=v2c_num_warps,
|
|
3307
|
+
num_stages=v2c_num_stages,
|
|
3308
|
+
)
|
|
3309
|
+
|
|
3310
|
+
# Phase 3: Parity
|
|
3311
|
+
parity_from_hard_kernel[parity_grid](
|
|
3312
|
+
hard_dec_tensor,
|
|
3313
|
+
chk_ptr,
|
|
3314
|
+
chk_edges,
|
|
3315
|
+
edge_var,
|
|
3316
|
+
syndrome_tensor,
|
|
3317
|
+
check_ok_tensor,
|
|
3318
|
+
**parity_meta_base,
|
|
3319
|
+
BLOCK_SIZE=parity_block_size,
|
|
3320
|
+
num_warps=parity_num_warps,
|
|
3321
|
+
num_stages=parity_num_stages,
|
|
3322
|
+
)
|
|
3323
|
+
|
|
3324
|
+
# Phase 4: Selection
|
|
3325
|
+
check_and_select_kernel[select_grid](
|
|
3326
|
+
check_ok_tensor,
|
|
3327
|
+
posterior_tensor,
|
|
3328
|
+
hard_dec_tensor,
|
|
3329
|
+
wj_gpu,
|
|
3330
|
+
best_weights_tensor,
|
|
3331
|
+
best_errors_tensor,
|
|
3332
|
+
valid_solutions_tensor,
|
|
3333
|
+
found_count_tensor,
|
|
3334
|
+
active_tensor,
|
|
3335
|
+
B=B,
|
|
3336
|
+
C=C_val,
|
|
3337
|
+
V=V_val,
|
|
3338
|
+
dtype_enum=dtype_enum_val,
|
|
3339
|
+
BLOCK_C=select_block_c,
|
|
3340
|
+
BLOCK_V=select_block_v,
|
|
3341
|
+
NUM_SOLS=int(self.stop_nconv),
|
|
3342
|
+
PRIOR_BATCHED=prior_batched,
|
|
3343
|
+
num_warps=select_num_warps,
|
|
3344
|
+
num_stages=select_num_stages,
|
|
3345
|
+
)
|
|
3346
|
+
|
|
3347
|
+
# Phase 5: Freeze and increment iter_counter
|
|
3348
|
+
freeze_finished_lanes_kernel[(B,)](
|
|
3349
|
+
best_errors_tensor,
|
|
3350
|
+
hard_dec_tensor,
|
|
3351
|
+
gamma_tensor,
|
|
3352
|
+
active_tensor,
|
|
3353
|
+
found_count_tensor,
|
|
3354
|
+
first_iter_tensor,
|
|
3355
|
+
iter_counter_tensor,
|
|
3356
|
+
sought_solutions_tensor,
|
|
3357
|
+
stop_nconv_val,
|
|
3358
|
+
in_leg_val,
|
|
3359
|
+
True, # count_iteration
|
|
3360
|
+
V_val,
|
|
3361
|
+
all_done_flag_tensor,
|
|
3362
|
+
tensors["arrival_counter"],
|
|
3363
|
+
B,
|
|
3364
|
+
)
|
|
3365
|
+
|
|
3366
|
+
# all-done check is fused into freeze kernel (last-arriving program does reduction)
|
|
3367
|
+
all_done = all_done_flag_tensor.item() != 0
|
|
3368
|
+
if all_done:
|
|
3369
|
+
all_done_flag_tensor.fill_(0)
|
|
3370
|
+
break
|
|
3371
|
+
|
|
3372
|
+
t += 1
|
|
3373
|
+
|
|
3374
|
+
# Get iterations run
|
|
3375
|
+
if B == 1:
|
|
3376
|
+
iterations_run = int(iter_counter_tensor[0].item())
|
|
3377
|
+
else:
|
|
3378
|
+
iterations_run = int(iter_counter_tensor.max().item())
|
|
3379
|
+
|
|
3380
|
+
# Update active indices
|
|
3381
|
+
if B == 1:
|
|
3382
|
+
active_idx = torch.tensor([0], dtype=torch.int32, device=tensors["active"].device)
|
|
3383
|
+
B_active = 1
|
|
3384
|
+
else:
|
|
3385
|
+
active_mask = tensors["active"] != 0
|
|
3386
|
+
active_idx = torch.where(active_mask)[0].to(torch.int32)
|
|
3387
|
+
B_active = int(active_idx.shape[0])
|
|
3388
|
+
|
|
3389
|
+
if all_done:
|
|
3390
|
+
tensors["in_leg"].fill_(0)
|
|
3391
|
+
|
|
3392
|
+
return active_idx, B_active, all_done, iterations_run
|
|
3393
|
+
|
|
3394
|
+
def _decode_loop_single_gpu(
|
|
3395
|
+
self,
|
|
3396
|
+
syndromes: torch.Tensor,
|
|
3397
|
+
tensors: Dict,
|
|
3398
|
+
graph: "CSRGraph",
|
|
3399
|
+
log_prior_ratios: torch.Tensor,
|
|
3400
|
+
explicit_gammas: Optional[torch.Tensor],
|
|
3401
|
+
gpu_idx: int,
|
|
3402
|
+
*,
|
|
3403
|
+
init_posterior: Optional[torch.Tensor] = None,
|
|
3404
|
+
init_gamma: Optional[torch.Tensor] = None,
|
|
3405
|
+
skip_relay_legs: bool = False,
|
|
3406
|
+
pre_iter_override: Optional[int] = None,
|
|
3407
|
+
return_last_posterior: bool = False,
|
|
3408
|
+
prior_batched: bool = False,
|
|
3409
|
+
wj_override: Optional[torch.Tensor] = None,
|
|
3410
|
+
) -> Dict[str, torch.Tensor]:
|
|
3411
|
+
"""Execute the decode loop on a single GPU.
|
|
3412
|
+
|
|
3413
|
+
Args:
|
|
3414
|
+
syndromes: [B, C] syndrome bits on this GPU
|
|
3415
|
+
tensors: Device tensors dictionary for this GPU
|
|
3416
|
+
graph: CSRGraph instance for this GPU
|
|
3417
|
+
log_prior_ratios: [1, V] log prior ratios on this GPU
|
|
3418
|
+
explicit_gammas: [K, V] explicit gammas on this GPU (or None)
|
|
3419
|
+
gpu_idx: GPU index (for device string)
|
|
3420
|
+
init_posterior, init_gamma, skip_relay_legs, pre_iter_override,
|
|
3421
|
+
return_last_posterior: see ``decode``. Validated by caller.
|
|
3422
|
+
|
|
3423
|
+
Returns:
|
|
3424
|
+
Result dictionary with errors, weights, valid_mask, iterations, unconverged_indices
|
|
3425
|
+
"""
|
|
3426
|
+
B = syndromes.shape[0]
|
|
3427
|
+
device_str = f"{self.device_type}:{self.device_ids[gpu_idx]}"
|
|
3428
|
+
|
|
3429
|
+
# One-time allocations for per-lane freezing and iteration accounting
|
|
3430
|
+
if "first_iter" not in tensors:
|
|
3431
|
+
tensors["first_iter"] = torch.full((B,), -1, dtype=torch.int32, device=device_str)
|
|
3432
|
+
if "iter_counter" not in tensors:
|
|
3433
|
+
tensors["iter_counter"] = torch.zeros(B, dtype=torch.int32, device=device_str)
|
|
3434
|
+
if "active" not in tensors:
|
|
3435
|
+
tensors["active"] = torch.ones(B, dtype=torch.uint8, device=device_str)
|
|
3436
|
+
# Reset active lanes at the start of every decode
|
|
3437
|
+
tensors["active"].fill_(1)
|
|
3438
|
+
# Reset per-decode iteration accounting and frozen state
|
|
3439
|
+
tensors["first_iter"].fill_(-1)
|
|
3440
|
+
tensors["iter_counter"].fill_(0)
|
|
3441
|
+
tensors["all_done_flag"].fill_(0)
|
|
3442
|
+
tensors["in_leg"].fill_(0)
|
|
3443
|
+
tensors["old_active_count"].fill_(-1)
|
|
3444
|
+
|
|
3445
|
+
# Copy syndromes to device
|
|
3446
|
+
tensors["syndrome"][:] = syndromes
|
|
3447
|
+
|
|
3448
|
+
# Initialize beliefs from priors
|
|
3449
|
+
# lambda_: Start with prior log-likelihood ratios (Rust: log_prior_ratios)
|
|
3450
|
+
tensors["lambda_"].copy_(log_prior_ratios.expand(B, -1)) # Broadcast prior LLRs robustly
|
|
3451
|
+
# posterior_ratios: Initialize to log_prior_ratios for first iteration's gamma mixing
|
|
3452
|
+
# This matches Rust: posterior_ratios initialized to log_prior_ratios when gamma0 is set
|
|
3453
|
+
# (Rust line 157-161: posterior_ratios = log_prior_ratios if gamma0.is_some() else zeros)
|
|
3454
|
+
# For Relay-BP, gamma0 is always set, so we initialize posterior_ratios to log_prior_ratios.
|
|
3455
|
+
# Caller override: replace with the supplied posterior.
|
|
3456
|
+
if init_posterior is not None:
|
|
3457
|
+
tensors["posterior_ratios"].copy_(init_posterior.to(self.dtype))
|
|
3458
|
+
else:
|
|
3459
|
+
tensors["posterior_ratios"].copy_(log_prior_ratios.expand(B, -1))
|
|
3460
|
+
tensors["best_weights"].fill_(float("inf"))
|
|
3461
|
+
tensors["found_count"].fill_(0)
|
|
3462
|
+
tensors["valid_solutions"].fill_(0)
|
|
3463
|
+
# Initialize sought_solutions to 1 for pre_iter (will be updated for relay legs)
|
|
3464
|
+
tensors["sought_solutions"].fill_(1)
|
|
3465
|
+
|
|
3466
|
+
# Initialize messages (Rust-compatible: nu=zero, mu=priors)
|
|
3467
|
+
if self.cfg.perf == "throughput":
|
|
3468
|
+
# Rust: initialize_check_to_variable() -> nu = zero
|
|
3469
|
+
# Rust: initialize_variable_to_check() -> mu = log_prior_ratios
|
|
3470
|
+
self._launch_init_messages_with_priors_EB(
|
|
3471
|
+
tensors["muT"],
|
|
3472
|
+
tensors["nuT"],
|
|
3473
|
+
B,
|
|
3474
|
+
graph=graph,
|
|
3475
|
+
log_prior_ratios=log_prior_ratios,
|
|
3476
|
+
gpu_idx=gpu_idx,
|
|
3477
|
+
prior_batched=prior_batched,
|
|
3478
|
+
)
|
|
3479
|
+
# Delta-trick: initialize L to current marginals (lam_mixed + sum(nu)) for this phase
|
|
3480
|
+
# by running one standard V2C pass, then copying posterior_ratios -> L.
|
|
3481
|
+
if self._use_delta_trick:
|
|
3482
|
+
self._launch_v2c_btile(
|
|
3483
|
+
tensors,
|
|
3484
|
+
B,
|
|
3485
|
+
write_hard=True,
|
|
3486
|
+
active_idx=None,
|
|
3487
|
+
B_active=None,
|
|
3488
|
+
graph=graph,
|
|
3489
|
+
log_prior_ratios=log_prior_ratios,
|
|
3490
|
+
gpu_idx=gpu_idx,
|
|
3491
|
+
block_vars=None,
|
|
3492
|
+
block_var_ptr=None,
|
|
3493
|
+
block_idx=-1,
|
|
3494
|
+
prior_batched=prior_batched,
|
|
3495
|
+
)
|
|
3496
|
+
tensors["L"].copy_(tensors["posterior_ratios"])
|
|
3497
|
+
else:
|
|
3498
|
+
self._launch_init_kernel(
|
|
3499
|
+
tensors, B, graph=graph, log_prior_ratios=log_prior_ratios, gpu_idx=gpu_idx
|
|
3500
|
+
)
|
|
3501
|
+
|
|
3502
|
+
# Pre-iterations (T0) with uniform gamma0 (ordered memory phase)
|
|
3503
|
+
if self.algo == "plain":
|
|
3504
|
+
# Plain BP: no memory mixing; λ remains λ₀ each iteration
|
|
3505
|
+
tensors["gamma"].fill_(0.0)
|
|
3506
|
+
elif init_gamma is not None:
|
|
3507
|
+
# Caller override: per-lane gamma vector for the pre-iter phase.
|
|
3508
|
+
tensors["gamma"][:] = init_gamma.to(self.dtype)
|
|
3509
|
+
else:
|
|
3510
|
+
# Relay BP: use γ-mixing for ordered memory (even with num_sets=0 for MemBP)
|
|
3511
|
+
gamma0_tensor = sample_gamma_scalar(B, self.V, self.gamma0, device_str)
|
|
3512
|
+
tensors["gamma"][:] = gamma0_tensor
|
|
3513
|
+
|
|
3514
|
+
# Reset per-set valid flags so only one success can be counted in this set
|
|
3515
|
+
tensors["valid_solutions"].fill_(0)
|
|
3516
|
+
|
|
3517
|
+
# Initialize active_idx for pre-iter loop (all lanes start active)
|
|
3518
|
+
if self.cfg.perf == "throughput":
|
|
3519
|
+
if B == 1:
|
|
3520
|
+
# For B=1, skip compaction - lane is always active
|
|
3521
|
+
device_str = self._device_strs[self.device_ids[gpu_idx]]
|
|
3522
|
+
active_idx = torch.tensor([0], dtype=torch.int32, device=device_str)
|
|
3523
|
+
B_active = 1
|
|
3524
|
+
# Ensure active[0] is 1 (should already be, but be explicit)
|
|
3525
|
+
tensors["active"][0] = 1
|
|
3526
|
+
# Store old count for device-side comparison (though we won't use it for B=1)
|
|
3527
|
+
tensors["old_active_count"].fill_(1)
|
|
3528
|
+
else:
|
|
3529
|
+
active_idx, B_active = self._compact_active_indices_on_device(tensors, B, gpu_idx)
|
|
3530
|
+
# Store old count for device-side comparison
|
|
3531
|
+
tensors["old_active_count"].fill_(B_active)
|
|
3532
|
+
else:
|
|
3533
|
+
# Non-throughput mode: all lanes are active initially
|
|
3534
|
+
device_str = f"cuda:{gpu_idx}"
|
|
3535
|
+
if B == 1:
|
|
3536
|
+
# For B=1, use simple [0] tensor and ensure active[0] is 1
|
|
3537
|
+
active_idx = torch.tensor([0], dtype=torch.int32, device=device_str)
|
|
3538
|
+
B_active = 1
|
|
3539
|
+
tensors["active"][0] = 1
|
|
3540
|
+
else:
|
|
3541
|
+
active_idx = torch.arange(B, dtype=torch.int32, device=device_str)
|
|
3542
|
+
B_active = B
|
|
3543
|
+
|
|
3544
|
+
# Store in_leg as Python variable to avoid .item() calls in hot loop
|
|
3545
|
+
in_leg_python = True # We're in pre-iter phase
|
|
3546
|
+
|
|
3547
|
+
# Track leg count (pre_iter counts as leg 0, relay legs as 1..num_sets)
|
|
3548
|
+
leg_count = 0
|
|
3549
|
+
|
|
3550
|
+
# Resolve all autotuning configs once
|
|
3551
|
+
self._resolve_configs(B, tensors, graph, gpu_idx)
|
|
3552
|
+
|
|
3553
|
+
# Pre-iterations loop
|
|
3554
|
+
# Use C2V grid-stride kernel for default mode (batch-organized pattern, much faster)
|
|
3555
|
+
pre_iter_count = self.pre_iter if pre_iter_override is None else int(pre_iter_override)
|
|
3556
|
+
if self.cfg.perf == "default":
|
|
3557
|
+
tensors["in_leg"].fill_(1)
|
|
3558
|
+
active_idx, B_active, all_done, iterations_run = self._run_bp_iterations_grid_stride(
|
|
3559
|
+
0,
|
|
3560
|
+
pre_iter_count,
|
|
3561
|
+
tensors,
|
|
3562
|
+
B,
|
|
3563
|
+
active_idx,
|
|
3564
|
+
B_active,
|
|
3565
|
+
graph,
|
|
3566
|
+
log_prior_ratios,
|
|
3567
|
+
gpu_idx,
|
|
3568
|
+
in_leg_python,
|
|
3569
|
+
force_hard=True,
|
|
3570
|
+
prior_batched=prior_batched,
|
|
3571
|
+
wj_override=wj_override,
|
|
3572
|
+
)
|
|
3573
|
+
# Update iteration counters (iteration-fused method handles this internally)
|
|
3574
|
+
else:
|
|
3575
|
+
# Throughput mode: per-iteration loop
|
|
3576
|
+
for t in range(pre_iter_count):
|
|
3577
|
+
tensors["in_leg"].fill_(1)
|
|
3578
|
+
active_idx, B_active, all_done = self._run_bp_iteration(
|
|
3579
|
+
t,
|
|
3580
|
+
tensors,
|
|
3581
|
+
B,
|
|
3582
|
+
active_idx,
|
|
3583
|
+
B_active,
|
|
3584
|
+
graph,
|
|
3585
|
+
log_prior_ratios,
|
|
3586
|
+
gpu_idx,
|
|
3587
|
+
in_leg_python,
|
|
3588
|
+
force_hard=True,
|
|
3589
|
+
prior_batched=prior_batched,
|
|
3590
|
+
wj_override=wj_override,
|
|
3591
|
+
)
|
|
3592
|
+
if all_done:
|
|
3593
|
+
break
|
|
3594
|
+
|
|
3595
|
+
# Parity and selection already ran after the last pre-iteration, in both perf modes.
|
|
3596
|
+
# With pre_iter_count == 0 no iteration has run yet; relay leg 1 overwrites this.
|
|
3597
|
+
if self.unconverged_correction == "first_leg":
|
|
3598
|
+
tensors["first_leg_hard_dec"].copy_(tensors["hard_dec"])
|
|
3599
|
+
# Device-side freeze + all-done check (fused).
|
|
3600
|
+
freeze_finished_lanes_kernel[(B,)](
|
|
3601
|
+
tensors["best_errors"],
|
|
3602
|
+
tensors["hard_dec"],
|
|
3603
|
+
tensors["gamma"],
|
|
3604
|
+
tensors["active"],
|
|
3605
|
+
tensors["found_count"],
|
|
3606
|
+
tensors["first_iter"],
|
|
3607
|
+
tensors["iter_counter"],
|
|
3608
|
+
tensors["sought_solutions"],
|
|
3609
|
+
self.stop_nconv,
|
|
3610
|
+
in_leg_python,
|
|
3611
|
+
False, # count_iteration
|
|
3612
|
+
self.V,
|
|
3613
|
+
tensors["all_done_flag"],
|
|
3614
|
+
tensors["arrival_counter"],
|
|
3615
|
+
B,
|
|
3616
|
+
)
|
|
3617
|
+
# If every lane has stop_nconv solutions, skip legs entirely and return results now.
|
|
3618
|
+
# Also returned here when the caller requested skip_relay_legs.
|
|
3619
|
+
reduce_all_ge_kernel[(1,)](
|
|
3620
|
+
tensors["found_count"],
|
|
3621
|
+
self.stop_nconv,
|
|
3622
|
+
tensors["all_done_flag"],
|
|
3623
|
+
B,
|
|
3624
|
+
)
|
|
3625
|
+
if tensors["all_done_flag"].item() or skip_relay_legs:
|
|
3626
|
+
# Use lifetime-best solutions where they exist. Otherwise _unconverged_hard_dec.
|
|
3627
|
+
# A lane has a valid solution iff its best_weights entry is finite.
|
|
3628
|
+
has_solution = torch.isfinite(tensors["best_weights"])
|
|
3629
|
+
fallback_errors = torch.where(
|
|
3630
|
+
has_solution.view(-1, 1),
|
|
3631
|
+
tensors["best_errors"],
|
|
3632
|
+
self._unconverged_hard_dec(tensors),
|
|
3633
|
+
)
|
|
3634
|
+
errors = bitpack_errors(fallback_errors) if self.bitpack_output else fallback_errors
|
|
3635
|
+
iters = torch.where(
|
|
3636
|
+
tensors["first_iter"] >= 0,
|
|
3637
|
+
tensors["first_iter"],
|
|
3638
|
+
tensors["iter_counter"].expand_as(tensors["first_iter"]),
|
|
3639
|
+
)
|
|
3640
|
+
# Compute unconverged indices (should be empty if all done)
|
|
3641
|
+
unconverged_mask = tensors["found_count"] < self.stop_nconv
|
|
3642
|
+
unconverged_idx = torch.where(unconverged_mask)[0].to(torch.int32)
|
|
3643
|
+
result = {
|
|
3644
|
+
"errors": errors,
|
|
3645
|
+
"weights": tensors["best_weights"],
|
|
3646
|
+
"valid_mask": tensors["found_count"] >= self.stop_nconv,
|
|
3647
|
+
"iterations": iters,
|
|
3648
|
+
"found_count": tensors["found_count"].to(torch.int32),
|
|
3649
|
+
"unconverged_indices": unconverged_idx,
|
|
3650
|
+
}
|
|
3651
|
+
if return_last_posterior:
|
|
3652
|
+
result["last_posterior"] = tensors["posterior_ratios"].detach().clone()
|
|
3653
|
+
return result
|
|
3654
|
+
|
|
3655
|
+
# Track leg count (pre-iter is leg 0, relay legs are 1..num_sets)
|
|
3656
|
+
leg_count = 0 # Pre-iter is leg 0
|
|
3657
|
+
|
|
3658
|
+
# Check fast_max_unconverged_pct early exit after pre-iter (leg_count = 0)
|
|
3659
|
+
# Some syndromes may converge during pre-iter, so we should check here too
|
|
3660
|
+
early_exit_result = self._check_fast_max_unconverged_pct(tensors, B, leg_count=0)
|
|
3661
|
+
if early_exit_result is not None:
|
|
3662
|
+
return early_exit_result
|
|
3663
|
+
|
|
3664
|
+
unfreeze_finished_lanes_kernel[(B,)](
|
|
3665
|
+
tensors["active"],
|
|
3666
|
+
tensors["found_count"],
|
|
3667
|
+
self.stop_nconv,
|
|
3668
|
+
)
|
|
3669
|
+
|
|
3670
|
+
# Relay legs (R times)
|
|
3671
|
+
in_leg_python = True
|
|
3672
|
+
for leg in range(self.num_sets):
|
|
3673
|
+
# Increment leg count (relay legs are 1..num_sets)
|
|
3674
|
+
leg_count = leg + 1
|
|
3675
|
+
tensors["in_leg"].fill_(1)
|
|
3676
|
+
tensors["sought_solutions"] = tensors["found_count"] + 1
|
|
3677
|
+
|
|
3678
|
+
# Set leg gamma with the following priority:
|
|
3679
|
+
# 1) explicit_gammas (K×V) if provided
|
|
3680
|
+
# 2) gamma_dist_interval_by_error (V×2) if provided
|
|
3681
|
+
# 3) global gamma_dist_interval (scalar (min, max))
|
|
3682
|
+
# Cases 2 and 3 draw from seed + leg: one [1, V] row broadcast to all shots
|
|
3683
|
+
# (gamma_draw="broadcast") or one row per shot (gamma_draw="per_row").
|
|
3684
|
+
gamma_rows = 1 if self.gamma_draw == "broadcast" else B
|
|
3685
|
+
if explicit_gammas is not None:
|
|
3686
|
+
K = int(explicit_gammas.shape[0])
|
|
3687
|
+
row = explicit_gammas[((leg + 1) % K)]
|
|
3688
|
+
tensors["gamma"].copy_(row.unsqueeze(0).expand(B, -1))
|
|
3689
|
+
elif self.gamma_dist_interval_by_error_per_gpu is not None:
|
|
3690
|
+
gamma_intervals = self.gamma_dist_interval_by_error_per_gpu[gpu_idx]
|
|
3691
|
+
gamma_leg = sample_gamma_per_variable(
|
|
3692
|
+
gamma_rows, self.V, gamma_intervals, device_str, self.seed + leg
|
|
3693
|
+
)
|
|
3694
|
+
tensors["gamma"].copy_(gamma_leg.expand(B, -1))
|
|
3695
|
+
else:
|
|
3696
|
+
gamma_min, gamma_max = self.gamma_dist_interval
|
|
3697
|
+
gamma_leg = sample_gamma_uniform(
|
|
3698
|
+
gamma_rows, self.V, gamma_min, gamma_max, device_str, self.seed + leg
|
|
3699
|
+
)
|
|
3700
|
+
tensors["gamma"].copy_(gamma_leg.expand(B, -1))
|
|
3701
|
+
|
|
3702
|
+
# Rust-equivalent per-leg reinitialization of messages:
|
|
3703
|
+
# reset variable<->check messages at the start of each leg while
|
|
3704
|
+
# keeping current beliefs (lambda_) carried over
|
|
3705
|
+
# Rust: initialize_check_to_variable() -> nu = zero
|
|
3706
|
+
# Rust: initialize_variable_to_check() -> mu = log_prior_ratios
|
|
3707
|
+
if self.cfg.perf == "throughput":
|
|
3708
|
+
# Always reset messages between legs per Relay-BP spec
|
|
3709
|
+
# Compute active_idx once for the first iteration of the leg
|
|
3710
|
+
if B == 1:
|
|
3711
|
+
# For B=1, skip compaction - lane is always active
|
|
3712
|
+
device_str = self._device_strs[self.device_ids[gpu_idx]]
|
|
3713
|
+
active_idx = torch.tensor([0], dtype=torch.int32, device=device_str)
|
|
3714
|
+
B_active = 1
|
|
3715
|
+
# Ensure active[0] is 1 (should already be, but be explicit)
|
|
3716
|
+
tensors["active"][0] = 1
|
|
3717
|
+
# Store old count for device-side comparison (though we won't use it for B=1)
|
|
3718
|
+
tensors["old_active_count"].fill_(1)
|
|
3719
|
+
else:
|
|
3720
|
+
active_idx, B_active = self._compact_active_indices_on_device(
|
|
3721
|
+
tensors, B, gpu_idx
|
|
3722
|
+
)
|
|
3723
|
+
# Update old count for device-side comparison
|
|
3724
|
+
tensors["old_active_count"].fill_(B_active)
|
|
3725
|
+
|
|
3726
|
+
# OPTIMIZATION: Only reinitialize messages for ACTIVE lanes (lazy init)
|
|
3727
|
+
# This aims to reduce memory traffic when B_active << B
|
|
3728
|
+
# For example, if only 10% of lanes are active, we only write 10% of messages
|
|
3729
|
+
self._launch_init_messages_with_priors_EB(
|
|
3730
|
+
tensors["muT"],
|
|
3731
|
+
tensors["nuT"],
|
|
3732
|
+
B,
|
|
3733
|
+
active_idx=active_idx,
|
|
3734
|
+
B_active=B_active,
|
|
3735
|
+
graph=graph,
|
|
3736
|
+
log_prior_ratios=log_prior_ratios,
|
|
3737
|
+
gpu_idx=gpu_idx,
|
|
3738
|
+
prior_batched=prior_batched,
|
|
3739
|
+
)
|
|
3740
|
+
# Re-initialize L for delta-trick to current marginals (lam_mixed + sum(nu)) for this leg.
|
|
3741
|
+
if self._use_delta_trick:
|
|
3742
|
+
self._launch_v2c_btile(
|
|
3743
|
+
tensors,
|
|
3744
|
+
B,
|
|
3745
|
+
write_hard=True,
|
|
3746
|
+
active_idx=active_idx,
|
|
3747
|
+
B_active=B_active,
|
|
3748
|
+
graph=graph,
|
|
3749
|
+
log_prior_ratios=log_prior_ratios,
|
|
3750
|
+
gpu_idx=gpu_idx,
|
|
3751
|
+
block_vars=None,
|
|
3752
|
+
block_var_ptr=None,
|
|
3753
|
+
block_idx=-1,
|
|
3754
|
+
prior_batched=prior_batched,
|
|
3755
|
+
)
|
|
3756
|
+
tensors["L"].copy_(tensors["posterior_ratios"])
|
|
3757
|
+
else:
|
|
3758
|
+
self._launch_init_kernel(tensors, B, graph=graph, gpu_idx=gpu_idx)
|
|
3759
|
+
# Non-throughput mode: initialize active_idx for the relay leg
|
|
3760
|
+
# (all lanes start active at the beginning of each leg)
|
|
3761
|
+
device_str = f"cuda:{gpu_idx}"
|
|
3762
|
+
active_idx = torch.arange(B, dtype=torch.int32, device=device_str)
|
|
3763
|
+
B_active = B
|
|
3764
|
+
|
|
3765
|
+
# Reset per-set valid flags so only one success can be counted in this set
|
|
3766
|
+
tensors["valid_solutions"].fill_(0)
|
|
3767
|
+
|
|
3768
|
+
# Relay leg iterations loop
|
|
3769
|
+
if self.cfg.perf == "default":
|
|
3770
|
+
active_idx, B_active, all_done, iterations_run = (
|
|
3771
|
+
self._run_bp_iterations_grid_stride(
|
|
3772
|
+
0,
|
|
3773
|
+
self.set_max_iter,
|
|
3774
|
+
tensors,
|
|
3775
|
+
B,
|
|
3776
|
+
active_idx,
|
|
3777
|
+
B_active,
|
|
3778
|
+
graph,
|
|
3779
|
+
log_prior_ratios,
|
|
3780
|
+
gpu_idx,
|
|
3781
|
+
in_leg_python,
|
|
3782
|
+
force_hard=False,
|
|
3783
|
+
prior_batched=prior_batched,
|
|
3784
|
+
wj_override=wj_override,
|
|
3785
|
+
)
|
|
3786
|
+
)
|
|
3787
|
+
else:
|
|
3788
|
+
# Throughput mode: per-iteration loop (configs cached automatically on first call)
|
|
3789
|
+
for t in range(self.set_max_iter):
|
|
3790
|
+
active_idx, B_active, all_done = self._run_bp_iteration(
|
|
3791
|
+
t,
|
|
3792
|
+
tensors,
|
|
3793
|
+
B,
|
|
3794
|
+
active_idx,
|
|
3795
|
+
B_active,
|
|
3796
|
+
graph,
|
|
3797
|
+
log_prior_ratios,
|
|
3798
|
+
gpu_idx,
|
|
3799
|
+
in_leg_python,
|
|
3800
|
+
force_hard=False,
|
|
3801
|
+
prior_batched=prior_batched,
|
|
3802
|
+
wj_override=wj_override,
|
|
3803
|
+
)
|
|
3804
|
+
if all_done:
|
|
3805
|
+
break
|
|
3806
|
+
|
|
3807
|
+
# Parity and selection already ran after the last iteration of the leg.
|
|
3808
|
+
# The first leg that ran iterations is relay leg 1 when pre_iter_count == 0.
|
|
3809
|
+
if leg == 0 and pre_iter_count == 0 and self.unconverged_correction == "first_leg":
|
|
3810
|
+
tensors["first_leg_hard_dec"].copy_(tensors["hard_dec"])
|
|
3811
|
+
# Stop the legs only when every lane has stop_nconv solutions.
|
|
3812
|
+
reduce_all_ge_kernel[(1,)](
|
|
3813
|
+
tensors["found_count"],
|
|
3814
|
+
self.stop_nconv,
|
|
3815
|
+
tensors["all_done_flag"],
|
|
3816
|
+
B,
|
|
3817
|
+
)
|
|
3818
|
+
# Sync here is acceptable - only called once per leg, not in hot loop
|
|
3819
|
+
if tensors["all_done_flag"].item():
|
|
3820
|
+
break
|
|
3821
|
+
|
|
3822
|
+
# Check fast_max_unconverged_pct early exit after completing this relay leg (leg_count = leg + 1)
|
|
3823
|
+
# We check at leg boundaries (after final parity check) rather than every iteration because:
|
|
3824
|
+
# 1. A "leg" is a logical unit (pre_iter phase or a relay leg), matching the threshold semantics
|
|
3825
|
+
# 2. Less overhead: check once per leg vs once per iteration (set_max_iter can be 60+)
|
|
3826
|
+
# 3. Percentage-based threshold adapts to batch convergence behavior
|
|
3827
|
+
early_exit_result = self._check_fast_max_unconverged_pct(
|
|
3828
|
+
tensors, B, leg_count=leg_count
|
|
3829
|
+
)
|
|
3830
|
+
if early_exit_result is not None:
|
|
3831
|
+
return early_exit_result
|
|
3832
|
+
|
|
3833
|
+
unfreeze_finished_lanes_kernel[(B,)](
|
|
3834
|
+
tensors["active"],
|
|
3835
|
+
tensors["found_count"],
|
|
3836
|
+
self.stop_nconv,
|
|
3837
|
+
)
|
|
3838
|
+
|
|
3839
|
+
# Prepare output
|
|
3840
|
+
# A lane with no valid solution returns _unconverged_hard_dec (first or last leg)
|
|
3841
|
+
has_solution = torch.isfinite(tensors["best_weights"])
|
|
3842
|
+
fallback_errors = torch.where(
|
|
3843
|
+
has_solution.view(-1, 1),
|
|
3844
|
+
tensors["best_errors"],
|
|
3845
|
+
self._unconverged_hard_dec(tensors),
|
|
3846
|
+
)
|
|
3847
|
+
if self.bitpack_output:
|
|
3848
|
+
errors = bitpack_errors(fallback_errors)
|
|
3849
|
+
else:
|
|
3850
|
+
errors = fallback_errors
|
|
3851
|
+
|
|
3852
|
+
# Build per-lane iteration counts (Rust-like): first_iter if set, else per-lane iter_counter
|
|
3853
|
+
iters = torch.where(
|
|
3854
|
+
tensors["first_iter"] >= 0, tensors["first_iter"], tensors["iter_counter"]
|
|
3855
|
+
)
|
|
3856
|
+
|
|
3857
|
+
# Compute unconverged indices (found_count < stop_nconv)
|
|
3858
|
+
# If fast_max_unconverged_pct was set, this will be non-empty for early exits
|
|
3859
|
+
# If fast_max_unconverged_pct was None, this will be empty (all ran to completion)
|
|
3860
|
+
unconverged_mask = tensors["found_count"] < self.stop_nconv
|
|
3861
|
+
unconverged_idx = torch.where(unconverged_mask)[0].to(torch.int32)
|
|
3862
|
+
|
|
3863
|
+
result = {
|
|
3864
|
+
"errors": errors,
|
|
3865
|
+
"weights": tensors["best_weights"],
|
|
3866
|
+
"valid_mask": tensors["found_count"] >= self.stop_nconv,
|
|
3867
|
+
"iterations": iters,
|
|
3868
|
+
"found_count": tensors["found_count"].to(torch.int32),
|
|
3869
|
+
"unconverged_indices": unconverged_idx,
|
|
3870
|
+
}
|
|
3871
|
+
if return_last_posterior:
|
|
3872
|
+
result["last_posterior"] = tensors["posterior_ratios"].detach().clone()
|
|
3873
|
+
return result
|
|
3874
|
+
|
|
3875
|
+
def get_stats(self) -> Dict:
|
|
3876
|
+
"""Get decoder statistics."""
|
|
3877
|
+
return {
|
|
3878
|
+
"graph": self.graphs[0].get_stats(),
|
|
3879
|
+
"parameters": {
|
|
3880
|
+
"pre_iter": self.pre_iter,
|
|
3881
|
+
"num_sets": self.num_sets,
|
|
3882
|
+
"set_max_iter": self.set_max_iter,
|
|
3883
|
+
"gamma0": self.gamma0,
|
|
3884
|
+
"gamma_dist_interval": self.gamma_dist_interval,
|
|
3885
|
+
"stop_nconv": self.stop_nconv,
|
|
3886
|
+
"dtype": self.dtype_str,
|
|
3887
|
+
"device": self.device_ids,
|
|
3888
|
+
"algo": self.algo,
|
|
3889
|
+
"perf": self.perf,
|
|
3890
|
+
},
|
|
3891
|
+
}
|