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/kernels.py
ADDED
|
@@ -0,0 +1,2208 @@
|
|
|
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
|
+
"""Triton kernels for Relay-BP-S belief propagation decoding.
|
|
12
|
+
|
|
13
|
+
This module contains GPU kernels implementing the core belief propagation operations
|
|
14
|
+
for quantum error correction. The kernels closely follow the Rust implementation
|
|
15
|
+
in crates/relay_bp/src/bp/min_sum.rs, providing GPU acceleration for:
|
|
16
|
+
|
|
17
|
+
- Check-to-variable (C2V) message passing with min-sum algorithm
|
|
18
|
+
- Variable-to-check (V2C) message passing with gamma mixing
|
|
19
|
+
- Parity checking for syndrome validation
|
|
20
|
+
- Memory management and tensor operations
|
|
21
|
+
|
|
22
|
+
Key kernels:
|
|
23
|
+
- c2v_min_sum_kernel: Implements min-sum check node updates
|
|
24
|
+
- v2c_and_marginals_fused_gamma_kernel: Variable node updates with memory mixing
|
|
25
|
+
- parity_*_kernel: Syndrome validation kernels
|
|
26
|
+
- Various utility kernels for memory management and batch processing
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import triton
|
|
30
|
+
import triton.language as tl
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@triton.jit
|
|
34
|
+
def c2v_min_sum_kernel(
|
|
35
|
+
mu,
|
|
36
|
+
nu, # [B,E]
|
|
37
|
+
chk_ptr,
|
|
38
|
+
chk_edges, # CSR over checks -> edge IDs
|
|
39
|
+
syndrome, # [B,C]
|
|
40
|
+
active, # [B] uint8 mask (1=active, 0=frozen)
|
|
41
|
+
B,
|
|
42
|
+
C,
|
|
43
|
+
E,
|
|
44
|
+
alpha,
|
|
45
|
+
beta,
|
|
46
|
+
use_alpha: tl.constexpr,
|
|
47
|
+
use_beta: tl.constexpr,
|
|
48
|
+
dtype_enum: tl.constexpr, # 0=fp16, 1=fp32, 2=fp64 (compile-time constant)
|
|
49
|
+
BLOCK_SIZE: tl.constexpr,
|
|
50
|
+
ROWS_PER_CHK: tl.constexpr, # Number of check rows to process per program
|
|
51
|
+
):
|
|
52
|
+
"""Check-to-variable min-sum kernel implementing belief propagation check node updates.
|
|
53
|
+
|
|
54
|
+
This kernel implements the min-sum algorithm for check nodes in belief propagation,
|
|
55
|
+
corresponding to the compute_check_to_variable function in Rust min_sum.rs.
|
|
56
|
+
|
|
57
|
+
For each check node, it computes outgoing messages to variable nodes using:
|
|
58
|
+
1. Two-pass algorithm to find min1, min2, and parity
|
|
59
|
+
2. Min-sum rule with optional alpha/beta scaling
|
|
60
|
+
3. Syndrome integration for quantum error correction
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
mu, nu: Message tensors [B,E] (variable-to-check, check-to-variable)
|
|
64
|
+
chk_ptr, chk_edges: CSR representation of check constraints
|
|
65
|
+
syndrome: [B,C] syndrome bits from quantum measurements
|
|
66
|
+
active: [B] mask for active decoding lanes
|
|
67
|
+
B, C, E: batch size, number of checks, number of edges
|
|
68
|
+
alpha, beta: min-sum scaling parameters
|
|
69
|
+
use_alpha, use_beta: flags for parameter usage
|
|
70
|
+
dtype_enum: compile-time dtype selector (0=fp16, 1=fp32, 2=fp64)
|
|
71
|
+
BLOCK_SIZE: vectorization block size
|
|
72
|
+
ROWS_PER_CHK: number of check rows processed per program
|
|
73
|
+
"""
|
|
74
|
+
pid = tl.program_id(axis=0)
|
|
75
|
+
base = pid * ROWS_PER_CHK
|
|
76
|
+
|
|
77
|
+
# Process up to ROWS_PER_CHK {b,i} pairs
|
|
78
|
+
for r in tl.static_range(0, ROWS_PER_CHK):
|
|
79
|
+
idx = base + r
|
|
80
|
+
# Only process if within bounds
|
|
81
|
+
if idx < B * C:
|
|
82
|
+
b = idx // C
|
|
83
|
+
i = idx % C
|
|
84
|
+
|
|
85
|
+
# Early-out for inactive lanes - check active status first
|
|
86
|
+
act_b = tl.load(active + b)
|
|
87
|
+
if act_b != 0:
|
|
88
|
+
# Only read CSR data for active lanes
|
|
89
|
+
row_start = tl.load(chk_ptr + i)
|
|
90
|
+
row_end = tl.load(chk_ptr + i + 1)
|
|
91
|
+
deg = row_end - row_start
|
|
92
|
+
if deg > 0:
|
|
93
|
+
deg_is_one = deg == 1
|
|
94
|
+
# PASS 1: Compute parity and find min1/min2 values (vectorized)
|
|
95
|
+
neg_parity = tl.zeros((), dtype=tl.int32)
|
|
96
|
+
# Use native dtype for min1/min2 (no casting)
|
|
97
|
+
if dtype_enum == 0: # fp16
|
|
98
|
+
min1 = tl.full((), 1e30, dtype=tl.float16)
|
|
99
|
+
min2 = tl.full((), 1e30, dtype=tl.float16)
|
|
100
|
+
elif dtype_enum == 2: # fp64
|
|
101
|
+
min1 = tl.full((), 1e30, dtype=tl.float64)
|
|
102
|
+
min2 = tl.full((), 1e30, dtype=tl.float64)
|
|
103
|
+
else: # fp32
|
|
104
|
+
min1 = tl.full((), 1e30, dtype=tl.float32)
|
|
105
|
+
min2 = tl.full((), 1e30, dtype=tl.float32)
|
|
106
|
+
cnt1 = tl.zeros((), dtype=tl.int32)
|
|
107
|
+
argmin_e = tl.full((), -1, dtype=tl.int32)
|
|
108
|
+
|
|
109
|
+
tile = row_start
|
|
110
|
+
while tile < row_end:
|
|
111
|
+
offs = tile + tl.arange(0, BLOCK_SIZE)
|
|
112
|
+
m = offs < row_end
|
|
113
|
+
e = tl.load(chk_edges + offs, mask=m, other=0)
|
|
114
|
+
mu_e = tl.load(mu + b * E + e, mask=m, other=0.0)
|
|
115
|
+
|
|
116
|
+
# parity via xor of tile parity bits
|
|
117
|
+
tile_par = tl.sum((mu_e < 0.0) & m, axis=0).to(tl.int32) & 1
|
|
118
|
+
neg_parity = neg_parity ^ tile_par
|
|
119
|
+
|
|
120
|
+
a = tl.abs(mu_e)
|
|
121
|
+
a_mask = tl.where(m, a, 1e30)
|
|
122
|
+
|
|
123
|
+
# tile min1/argmin and tie count
|
|
124
|
+
pos1 = tl.argmax(-a_mask, axis=0)
|
|
125
|
+
min1_t = tl.max(-a_mask, axis=0) * (-1.0)
|
|
126
|
+
e1_t = tl.load(chk_edges + tile + pos1)
|
|
127
|
+
cnt1_t = tl.sum(a_mask == min1_t, axis=0).to(tl.int32)
|
|
128
|
+
# tile min2 (mask just one occurrence of min1)
|
|
129
|
+
a_mask2 = tl.where(tl.arange(0, BLOCK_SIZE) == pos1, 1e30, a_mask)
|
|
130
|
+
min2_t = tl.min(a_mask2, axis=0)
|
|
131
|
+
|
|
132
|
+
# merge into global running mins and counts
|
|
133
|
+
better1 = min1_t < min1
|
|
134
|
+
argmin_e = tl.where(better1, e1_t, argmin_e)
|
|
135
|
+
cnt1 = tl.where(
|
|
136
|
+
better1, cnt1_t, tl.where(min1_t == min1, cnt1 + cnt1_t, cnt1)
|
|
137
|
+
)
|
|
138
|
+
min2 = tl.where(
|
|
139
|
+
better1,
|
|
140
|
+
tl.minimum(min1, min2_t),
|
|
141
|
+
tl.where(
|
|
142
|
+
min1_t == min1, tl.minimum(min2, min2_t), tl.minimum(min2, min1_t)
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
min1 = tl.where(better1, min1_t, min1)
|
|
146
|
+
|
|
147
|
+
tile += BLOCK_SIZE
|
|
148
|
+
|
|
149
|
+
# include syndrome bit in sign product
|
|
150
|
+
syn = tl.load(syndrome + b * C + i).to(tl.int32) & 1
|
|
151
|
+
# parity bit = (neg_parity & 1) XOR syn
|
|
152
|
+
par_bit = (neg_parity & 1) ^ syn
|
|
153
|
+
sign_prod = tl.where(par_bit == 0, 1.0, -1.0)
|
|
154
|
+
|
|
155
|
+
# PASS 2: Write outgoing messages (vectorized)
|
|
156
|
+
tile = row_start
|
|
157
|
+
while tile < row_end:
|
|
158
|
+
offs = tile + tl.arange(0, BLOCK_SIZE)
|
|
159
|
+
m = offs < row_end
|
|
160
|
+
e = tl.load(chk_edges + offs, mask=m, other=0)
|
|
161
|
+
mu_e = tl.load(mu + b * E + e, mask=m, other=0.0)
|
|
162
|
+
sgn_mu = tl.where(mu_e >= 0.0, 1.0, -1.0)
|
|
163
|
+
|
|
164
|
+
is_argmin = e == argmin_e
|
|
165
|
+
use_min2 = (cnt1 == 1) & is_argmin
|
|
166
|
+
out_mag = tl.where(use_min2, min2, min1)
|
|
167
|
+
# Degree-1 checks must send zero.
|
|
168
|
+
out_mag = tl.where(deg_is_one, 0.0, out_mag)
|
|
169
|
+
|
|
170
|
+
if use_alpha:
|
|
171
|
+
out_mag = alpha * out_mag
|
|
172
|
+
if use_beta:
|
|
173
|
+
out_mag = tl.maximum(out_mag - beta, 0.0)
|
|
174
|
+
|
|
175
|
+
nu_e = (sign_prod * sgn_mu) * out_mag
|
|
176
|
+
# Store with appropriate dtype based on dtype_enum (compile-time optimized)
|
|
177
|
+
if dtype_enum == 0: # fp16
|
|
178
|
+
tl.store(nu + b * E + e, nu_e.to(tl.float16), mask=m)
|
|
179
|
+
elif dtype_enum == 2: # fp64
|
|
180
|
+
tl.store(nu + b * E + e, nu_e.to(tl.float64), mask=m)
|
|
181
|
+
else: # fp32 (dtype_enum == 1)
|
|
182
|
+
tl.store(nu + b * E + e, nu_e, mask=m)
|
|
183
|
+
|
|
184
|
+
tile += BLOCK_SIZE
|
|
185
|
+
# else: inactive lane — do nothing for this (b,i)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@triton.jit
|
|
189
|
+
def v2c_and_marginals_fused_gamma_kernel(
|
|
190
|
+
nu,
|
|
191
|
+
mu, # [B,E]
|
|
192
|
+
var_ptr,
|
|
193
|
+
var_edges, # CSR over vars -> edge IDs
|
|
194
|
+
lam,
|
|
195
|
+
posterior_ratios,
|
|
196
|
+
hard_dec, # [B,V], [B,V], [B,V]
|
|
197
|
+
# lam: Current mixed beliefs [B,V]
|
|
198
|
+
# posterior_ratios: Posterior ratios/marginals [B,V] (matches Rust: posterior_ratios)
|
|
199
|
+
active, # [B] uint8 mask (1=active)
|
|
200
|
+
log_prior_ratios,
|
|
201
|
+
gamma, # [1,V] or [B,V], [B,V] - for gamma mixing
|
|
202
|
+
# log_prior_ratios: Prior log-likelihood ratios, [1,V] shared or [B,V] per-shot
|
|
203
|
+
# gamma: Memory strengths [B,V] (matches Rust: memory_strengths)
|
|
204
|
+
B,
|
|
205
|
+
V,
|
|
206
|
+
E,
|
|
207
|
+
dtype_enum: tl.constexpr, # 0=fp16, 1=fp32, 2=fp64 (compile-time constant)
|
|
208
|
+
BLOCK_SIZE: tl.constexpr,
|
|
209
|
+
ROWS_PER_VAR: tl.constexpr, # Number of variable rows to process per program
|
|
210
|
+
PRIOR_BATCHED: tl.constexpr = False, # if True, log_prior_ratios is [B,V], else [1,V]
|
|
211
|
+
):
|
|
212
|
+
"""Variable-to-check kernel with fused gamma mixing for memory-based belief propagation.
|
|
213
|
+
|
|
214
|
+
This kernel implements variable node updates in belief propagation with memory mixing,
|
|
215
|
+
corresponding to the compute_variable_to_check function in Rust min_sum.rs.
|
|
216
|
+
|
|
217
|
+
For each variable node, it:
|
|
218
|
+
1. Loads previous iteration's marginal (posterior_prev) from posterior_ratios tensor
|
|
219
|
+
2. Applies gamma mixing: λ_mixed = (1-γ)*log_prior + γ*posterior_prev (Rust-compatible: uses previous posterior)
|
|
220
|
+
3. Computes incoming message sum from check nodes
|
|
221
|
+
4. Updates current marginal: posterior_curr = λ_mixed + sum(nu)
|
|
222
|
+
5. Updates hard decisions
|
|
223
|
+
6. Computes outgoing messages to check nodes
|
|
224
|
+
|
|
225
|
+
The gamma mixing implements the "memory" aspect of Relay-BP, matching Rust's behavior:
|
|
226
|
+
Rust uses: λ_mixed = (1-γ)*log_prior + γ*posterior_prev, then posterior = λ_mixed + sum(nu)
|
|
227
|
+
This ensures identical convergence behavior with the Rust reference implementation.
|
|
228
|
+
|
|
229
|
+
Note: posterior_ratios is always stored (required for next iteration's gamma mixing).
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
nu, mu: Message tensors [B,E] (check-to-variable, variable-to-check)
|
|
233
|
+
var_ptr, var_edges: CSR representation of variable constraints
|
|
234
|
+
lam, posterior_ratios, hard_dec: [B,V] beliefs, marginals, hard decisions
|
|
235
|
+
active: [B] mask for active decoding lanes
|
|
236
|
+
log_prior_ratios, gamma: [1,V], [B,V] prior beliefs and memory strengths
|
|
237
|
+
B, V, E: batch size, number of variables, number of edges
|
|
238
|
+
dtype_enum: compile-time dtype selector (0=fp16, 1=fp32, 2=fp64)
|
|
239
|
+
BLOCK_SIZE: vectorization block size
|
|
240
|
+
ROWS_PER_VAR: number of variable rows processed per program
|
|
241
|
+
"""
|
|
242
|
+
pid = tl.program_id(axis=0)
|
|
243
|
+
base = pid * ROWS_PER_VAR
|
|
244
|
+
|
|
245
|
+
# Process up to ROWS_PER_VAR {b,j} pairs
|
|
246
|
+
for r in tl.static_range(0, ROWS_PER_VAR):
|
|
247
|
+
idx = base + r
|
|
248
|
+
# Only process if within bounds
|
|
249
|
+
if idx < B * V:
|
|
250
|
+
b = idx // V
|
|
251
|
+
j = idx % V
|
|
252
|
+
|
|
253
|
+
# Early-out for inactive lanes - check active status first
|
|
254
|
+
act_b = tl.load(active + b)
|
|
255
|
+
if act_b != 0:
|
|
256
|
+
# Only read CSR data for active lanes
|
|
257
|
+
row_start = tl.load(var_ptr + j)
|
|
258
|
+
row_end = tl.load(var_ptr + j + 1)
|
|
259
|
+
|
|
260
|
+
# Load previous iteration's marginal (posterior_prev) for Rust-compatible gamma mixing
|
|
261
|
+
# Rust uses: λ_mixed = (1-γ)*log_prior + γ*posterior_prev, then posterior = λ_mixed + sum(nu)
|
|
262
|
+
posterior_bj_prev = tl.load(
|
|
263
|
+
posterior_ratios + b * V + j
|
|
264
|
+
) # Previous iteration's marginal/posterior
|
|
265
|
+
|
|
266
|
+
# FUSED GAMMA MIXING (Rust-compatible): λ = (1-γ)*log_prior + γ*posterior_prev
|
|
267
|
+
# Use storage dtype directly for all computations (fp16/fp32/fp64)
|
|
268
|
+
g_bj = tl.load(gamma + b * V + j) # [B,V]
|
|
269
|
+
if PRIOR_BATCHED:
|
|
270
|
+
log_prior_j = tl.load(log_prior_ratios + b * V + j) # [B,V]
|
|
271
|
+
else:
|
|
272
|
+
log_prior_j = tl.load(log_prior_ratios + j) # [1,V]
|
|
273
|
+
lam_bj_mixed = (1.0 - g_bj) * log_prior_j + g_bj * posterior_bj_prev
|
|
274
|
+
|
|
275
|
+
# PASS 1: Sum incoming messages from check nodes (vectorized)
|
|
276
|
+
# Use fp32 accumulators for fp16/fp32 storage, fp64 for fp64
|
|
277
|
+
if dtype_enum == 2: # fp64
|
|
278
|
+
sum_nu = tl.zeros((), dtype=tl.float64)
|
|
279
|
+
else: # fp16 or fp32
|
|
280
|
+
sum_nu = tl.zeros((), dtype=tl.float32)
|
|
281
|
+
tile = row_start
|
|
282
|
+
while tile < row_end:
|
|
283
|
+
offs = tile + tl.arange(0, BLOCK_SIZE)
|
|
284
|
+
m = offs < row_end
|
|
285
|
+
e = tl.load(var_edges + offs, mask=m, other=0)
|
|
286
|
+
nu_e = tl.load(nu + b * E + e, mask=m, other=0.0)
|
|
287
|
+
# nu_e is already in correct dtype, no conversion needed
|
|
288
|
+
sum_nu += tl.sum(tl.where(m, nu_e, 0.0), axis=0)
|
|
289
|
+
tile += BLOCK_SIZE
|
|
290
|
+
|
|
291
|
+
# Compute current marginal: posterior = λ_mixed + sum(nu) (matches Rust posterior_ratios update)
|
|
292
|
+
posterior_bj_curr = lam_bj_mixed + sum_nu
|
|
293
|
+
# Always store posterior_ratios for next iteration's gamma mixing (required for Rust compatibility)
|
|
294
|
+
tl.store(posterior_ratios + b * V + j, posterior_bj_curr)
|
|
295
|
+
# Match Rust convergence rule Bit::from(posterior <= 0)
|
|
296
|
+
tl.store(hard_dec + b * V + j, (posterior_bj_curr <= 0.0).to(tl.uint8))
|
|
297
|
+
|
|
298
|
+
# Store mixed lambda for next iteration (used in message computation)
|
|
299
|
+
tl.store(lam + b * V + j, lam_bj_mixed)
|
|
300
|
+
|
|
301
|
+
# PASS 2: Write outgoing messages using mixed lambda (matches Rust schedule)
|
|
302
|
+
tile = row_start
|
|
303
|
+
while tile < row_end:
|
|
304
|
+
offs = tile + tl.arange(0, BLOCK_SIZE)
|
|
305
|
+
m = offs < row_end
|
|
306
|
+
e = tl.load(var_edges + offs, mask=m, other=0)
|
|
307
|
+
nu_e = tl.load(nu + b * E + e, mask=m, other=0.0)
|
|
308
|
+
# Use lam_bj_mixed (current iteration's mixed belief) to compute mu, matching Rust
|
|
309
|
+
# nu_e is already in correct dtype, no conversion needed
|
|
310
|
+
mu_e = lam_bj_mixed + (sum_nu - nu_e)
|
|
311
|
+
# mu_e is already in storage dtype (computed from storage dtype values), no conversion needed
|
|
312
|
+
tl.store(mu + b * E + e, mu_e, mask=m)
|
|
313
|
+
tile += BLOCK_SIZE
|
|
314
|
+
# else: inactive — do nothing (no loads, no stores)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
PARITY_CONFIGS = [
|
|
318
|
+
triton.Config({"BLOCK_SIZE": 32}, num_warps=2, num_stages=2),
|
|
319
|
+
triton.Config({"BLOCK_SIZE": 64}, num_warps=2, num_stages=2),
|
|
320
|
+
triton.Config({"BLOCK_SIZE": 128}, num_warps=4, num_stages=2),
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@triton.jit
|
|
325
|
+
def parity_per_check_kernel(
|
|
326
|
+
posterior_ratios, # [B,V] fp32 (read-only)
|
|
327
|
+
chk_ptr,
|
|
328
|
+
chk_edges, # CSR over checks -> edge IDs
|
|
329
|
+
edge_var, # [E] var index per edge
|
|
330
|
+
syndrome, # [B,C] uint8
|
|
331
|
+
check_ok, # [B,C] uint8 (out)
|
|
332
|
+
B,
|
|
333
|
+
C,
|
|
334
|
+
V,
|
|
335
|
+
E,
|
|
336
|
+
BLOCK_SIZE: tl.constexpr,
|
|
337
|
+
ROWS_PER_CHK: tl.constexpr, # Number of check rows to process per program
|
|
338
|
+
):
|
|
339
|
+
"""Parity check kernel for syndrome validation using belief marginals.
|
|
340
|
+
|
|
341
|
+
This kernel validates whether the current hard decisions satisfy all check constraints,
|
|
342
|
+
corresponding to the check_convergence function in Rust min_sum.rs.
|
|
343
|
+
|
|
344
|
+
For each check constraint, it computes the parity of connected variable bits
|
|
345
|
+
and compares with the syndrome to determine if the constraint is satisfied.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
posterior_ratios: [B,V] belief marginals (read-only)
|
|
349
|
+
chk_ptr, chk_edges: CSR representation of check constraints
|
|
350
|
+
edge_var: [E] variable index for each edge
|
|
351
|
+
syndrome: [B,C] syndrome bits from quantum measurements
|
|
352
|
+
check_ok: [B,C] output mask for satisfied constraints
|
|
353
|
+
B, C, V, E: batch size, checks, variables, edges
|
|
354
|
+
BLOCK_SIZE: vectorization block size
|
|
355
|
+
ROWS_PER_CHK: number of check rows processed per program
|
|
356
|
+
"""
|
|
357
|
+
pid = tl.program_id(axis=0)
|
|
358
|
+
base = pid * ROWS_PER_CHK
|
|
359
|
+
|
|
360
|
+
# Process up to ROWS_PER_CHK {b,i} pairs
|
|
361
|
+
for r in tl.static_range(0, ROWS_PER_CHK):
|
|
362
|
+
idx = base + r
|
|
363
|
+
# Only process if within bounds
|
|
364
|
+
if idx < B * C:
|
|
365
|
+
b = idx // C
|
|
366
|
+
i = idx % C
|
|
367
|
+
|
|
368
|
+
row_start = tl.load(chk_ptr + i)
|
|
369
|
+
row_end = tl.load(chk_ptr + i + 1)
|
|
370
|
+
par = tl.zeros((), dtype=tl.int32)
|
|
371
|
+
|
|
372
|
+
tile = row_start
|
|
373
|
+
while tile < row_end:
|
|
374
|
+
offs = tile + tl.arange(0, BLOCK_SIZE)
|
|
375
|
+
m = offs < row_end
|
|
376
|
+
e = tl.load(chk_edges + offs, mask=m, other=0)
|
|
377
|
+
v = tl.load(edge_var + e, mask=m, other=0)
|
|
378
|
+
posterior_v = tl.load(posterior_ratios + b * V + v, mask=m, other=0.0)
|
|
379
|
+
# Match Rust: Bit::from(posterior <= 0)
|
|
380
|
+
bits = (posterior_v <= 0.0).to(tl.int32) & 1
|
|
381
|
+
par = par ^ (tl.sum(bits & m.to(tl.int32), axis=0) & 1)
|
|
382
|
+
tile += BLOCK_SIZE
|
|
383
|
+
|
|
384
|
+
syn = tl.load(syndrome + b * C + i).to(tl.int32) & 1
|
|
385
|
+
ok = (par == syn).to(tl.uint8)
|
|
386
|
+
tl.store(check_ok + b * C + i, ok)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@triton.jit
|
|
390
|
+
def parity_from_hard_kernel(
|
|
391
|
+
hard_dec, # [B,V] uint8
|
|
392
|
+
chk_ptr,
|
|
393
|
+
chk_edges, # CSR over checks -> edge IDs
|
|
394
|
+
edge_var, # [E] var index per edge
|
|
395
|
+
syndrome, # [B,C] uint8
|
|
396
|
+
check_ok, # [B,C] uint8 (out)
|
|
397
|
+
B,
|
|
398
|
+
C,
|
|
399
|
+
V,
|
|
400
|
+
E,
|
|
401
|
+
BLOCK_SIZE: tl.constexpr,
|
|
402
|
+
ROWS_PER_CHK: tl.constexpr, # Number of check rows to process per program
|
|
403
|
+
):
|
|
404
|
+
"""Parity check kernel for syndrome validation using hard decisions.
|
|
405
|
+
|
|
406
|
+
This kernel validates check constraints using pre-computed hard decisions,
|
|
407
|
+
optimized for throughput mode to avoid reading posterior_ratios when hard_dec is already available.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
hard_dec: [B,V] hard decisions (0/1 bits)
|
|
411
|
+
chk_ptr, chk_edges: CSR representation of check constraints
|
|
412
|
+
edge_var: [E] variable index for each edge
|
|
413
|
+
syndrome: [B,C] syndrome bits from quantum measurements
|
|
414
|
+
check_ok: [B,C] output mask for satisfied constraints
|
|
415
|
+
B, C, V, E: batch size, checks, variables, edges
|
|
416
|
+
BLOCK_SIZE: vectorization block size
|
|
417
|
+
ROWS_PER_CHK: number of check rows processed per program
|
|
418
|
+
"""
|
|
419
|
+
pid = tl.program_id(axis=0)
|
|
420
|
+
base = pid * ROWS_PER_CHK
|
|
421
|
+
for r in tl.static_range(0, ROWS_PER_CHK):
|
|
422
|
+
idx = base + r
|
|
423
|
+
if idx < B * C:
|
|
424
|
+
b = idx // C
|
|
425
|
+
i = idx % C
|
|
426
|
+
|
|
427
|
+
row_start = tl.load(chk_ptr + i)
|
|
428
|
+
row_end = tl.load(chk_ptr + i + 1)
|
|
429
|
+
par = tl.zeros((), dtype=tl.int32)
|
|
430
|
+
|
|
431
|
+
tile = row_start
|
|
432
|
+
while tile < row_end:
|
|
433
|
+
offs = tile + tl.arange(0, BLOCK_SIZE)
|
|
434
|
+
m = offs < row_end
|
|
435
|
+
e = tl.load(chk_edges + offs, mask=m, other=0)
|
|
436
|
+
v = tl.load(edge_var + e, mask=m, other=0)
|
|
437
|
+
bits = tl.load(hard_dec + b * V + v, mask=m, other=0).to(tl.int32) & 1
|
|
438
|
+
par = par ^ (tl.sum(bits & m.to(tl.int32), axis=0) & 1)
|
|
439
|
+
tile += BLOCK_SIZE
|
|
440
|
+
|
|
441
|
+
syn = tl.load(syndrome + b * C + i).to(tl.int32) & 1
|
|
442
|
+
ok = (par == syn).to(tl.uint8)
|
|
443
|
+
tl.store(check_ok + b * C + i, ok)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
@triton.jit
|
|
447
|
+
def parity_from_hard_compact_kernel(
|
|
448
|
+
hard_dec, # [B,V] uint8
|
|
449
|
+
chk_ptr,
|
|
450
|
+
chk_edges, # CSR over checks -> edge IDs
|
|
451
|
+
edge_var, # [E] var index per edge
|
|
452
|
+
syndrome, # [B,C] uint8
|
|
453
|
+
check_ok, # [B,C] uint8 (out)
|
|
454
|
+
active_idx, # [B_active] int32 - compact list of active batch indices
|
|
455
|
+
B,
|
|
456
|
+
B_active,
|
|
457
|
+
C,
|
|
458
|
+
V,
|
|
459
|
+
E,
|
|
460
|
+
BLOCK_SIZE: tl.constexpr,
|
|
461
|
+
ROWS_PER_CHK: tl.constexpr, # Number of check rows to process per program
|
|
462
|
+
):
|
|
463
|
+
"""Parity check kernel for active lanes only (throughput mode optimization).
|
|
464
|
+
|
|
465
|
+
This kernel validates check constraints for only the active decoding lanes,
|
|
466
|
+
reducing computation when many lanes have already converged.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
hard_dec: [B,V] hard decisions (0/1 bits)
|
|
470
|
+
chk_ptr, chk_edges: CSR representation of check constraints
|
|
471
|
+
edge_var: [E] variable index for each edge
|
|
472
|
+
syndrome: [B,C] syndrome bits from quantum measurements
|
|
473
|
+
check_ok: [B,C] output mask for satisfied constraints
|
|
474
|
+
active_idx: [B_active] indices of active batch lanes
|
|
475
|
+
B, B_active, C, V, E: batch size, active batch size, checks, variables, edges
|
|
476
|
+
BLOCK_SIZE: vectorization block size
|
|
477
|
+
ROWS_PER_CHK: number of check rows processed per program
|
|
478
|
+
"""
|
|
479
|
+
pid = tl.program_id(axis=0)
|
|
480
|
+
base = pid * ROWS_PER_CHK
|
|
481
|
+
for r in tl.static_range(0, ROWS_PER_CHK):
|
|
482
|
+
idx = base + r
|
|
483
|
+
if idx < B_active * C:
|
|
484
|
+
b_active = idx // C
|
|
485
|
+
i = idx % C
|
|
486
|
+
|
|
487
|
+
# Map active batch index to actual batch index
|
|
488
|
+
b = tl.load(active_idx + b_active)
|
|
489
|
+
|
|
490
|
+
row_start = tl.load(chk_ptr + i)
|
|
491
|
+
row_end = tl.load(chk_ptr + i + 1)
|
|
492
|
+
par = tl.zeros((), dtype=tl.int32)
|
|
493
|
+
|
|
494
|
+
tile = row_start
|
|
495
|
+
while tile < row_end:
|
|
496
|
+
offs = tile + tl.arange(0, BLOCK_SIZE)
|
|
497
|
+
m = offs < row_end
|
|
498
|
+
e = tl.load(chk_edges + offs, mask=m, other=0)
|
|
499
|
+
v = tl.load(edge_var + e, mask=m, other=0)
|
|
500
|
+
bits = tl.load(hard_dec + b * V + v, mask=m, other=0).to(tl.int32) & 1
|
|
501
|
+
par = par ^ (tl.sum(bits & m.to(tl.int32), axis=0) & 1)
|
|
502
|
+
tile += BLOCK_SIZE
|
|
503
|
+
|
|
504
|
+
syn = tl.load(syndrome + b * C + i).to(tl.int32) & 1
|
|
505
|
+
ok = (par == syn).to(tl.uint8)
|
|
506
|
+
tl.store(check_ok + b * C + i, ok)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
@triton.jit
|
|
510
|
+
def parity_from_hard_compact_with_zero_kernel(
|
|
511
|
+
hard_dec, # [B,V] uint8
|
|
512
|
+
chk_ptr,
|
|
513
|
+
chk_edges, # CSR over checks -> edge IDs
|
|
514
|
+
edge_var, # [E] var index per edge
|
|
515
|
+
syndrome, # [B,C] uint8
|
|
516
|
+
check_ok, # [B,C] uint8 (out)
|
|
517
|
+
active, # [B] uint8 - active lane mask
|
|
518
|
+
B,
|
|
519
|
+
C,
|
|
520
|
+
V,
|
|
521
|
+
E,
|
|
522
|
+
BLOCK_SIZE: tl.constexpr,
|
|
523
|
+
ROWS_PER_CHK: tl.constexpr, # Number of check rows to process per program
|
|
524
|
+
):
|
|
525
|
+
"""Parity check over all lanes that writes check_ok for active lanes only.
|
|
526
|
+
|
|
527
|
+
Inactive lanes keep the check_ok values from their last active iteration.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
hard_dec: [B,V] hard decisions (0/1 bits)
|
|
531
|
+
chk_ptr, chk_edges: CSR representation of check constraints
|
|
532
|
+
edge_var: [E] variable index for each edge
|
|
533
|
+
syndrome: [B,C] syndrome bits from quantum measurements
|
|
534
|
+
check_ok: [B,C] output mask for satisfied constraints
|
|
535
|
+
active: [B] active lane mask (1=active, 0=frozen)
|
|
536
|
+
B, C, V, E: batch size, checks, variables, edges
|
|
537
|
+
BLOCK_SIZE: vectorization block size
|
|
538
|
+
ROWS_PER_CHK: number of check rows processed per program
|
|
539
|
+
"""
|
|
540
|
+
pid = tl.program_id(axis=0)
|
|
541
|
+
base = pid * ROWS_PER_CHK
|
|
542
|
+
for r in tl.static_range(0, ROWS_PER_CHK):
|
|
543
|
+
idx = base + r
|
|
544
|
+
if idx < B * C:
|
|
545
|
+
b = idx // C
|
|
546
|
+
i = idx % C
|
|
547
|
+
|
|
548
|
+
# Check if lane is active
|
|
549
|
+
is_active = tl.load(active + b) != 0
|
|
550
|
+
|
|
551
|
+
if is_active:
|
|
552
|
+
# Active lane: compute parity check
|
|
553
|
+
row_start = tl.load(chk_ptr + i)
|
|
554
|
+
row_end = tl.load(chk_ptr + i + 1)
|
|
555
|
+
par = tl.zeros((), dtype=tl.int32)
|
|
556
|
+
|
|
557
|
+
tile = row_start
|
|
558
|
+
while tile < row_end:
|
|
559
|
+
offs = tile + tl.arange(0, BLOCK_SIZE)
|
|
560
|
+
m = offs < row_end
|
|
561
|
+
e = tl.load(chk_edges + offs, mask=m, other=0)
|
|
562
|
+
v = tl.load(edge_var + e, mask=m, other=0)
|
|
563
|
+
bits = tl.load(hard_dec + b * V + v, mask=m, other=0).to(tl.int32) & 1
|
|
564
|
+
par = par ^ (tl.sum(bits & m.to(tl.int32), axis=0) & 1)
|
|
565
|
+
tile += BLOCK_SIZE
|
|
566
|
+
|
|
567
|
+
syn = tl.load(syndrome + b * C + i).to(tl.int32) & 1
|
|
568
|
+
ok = (par == syn).to(tl.uint8)
|
|
569
|
+
tl.store(check_ok + b * C + i, ok)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
@triton.jit
|
|
573
|
+
def zero_check_ok_inactive_kernel(
|
|
574
|
+
check_ok, # [B,C] uint8
|
|
575
|
+
active, # [B] uint8
|
|
576
|
+
B,
|
|
577
|
+
C,
|
|
578
|
+
BLOCK_B: tl.constexpr,
|
|
579
|
+
BLOCK_C: tl.constexpr,
|
|
580
|
+
):
|
|
581
|
+
"""Zero out check_ok for inactive lanes to ensure clean parity statistics.
|
|
582
|
+
|
|
583
|
+
This utility kernel ensures that inactive decoding lanes have zero check_ok values,
|
|
584
|
+
preventing them from contributing to convergence statistics.
|
|
585
|
+
"""
|
|
586
|
+
pid_b = tl.program_id(0)
|
|
587
|
+
pid_c = tl.program_id(1)
|
|
588
|
+
|
|
589
|
+
b_idx = pid_b * BLOCK_B + tl.arange(0, BLOCK_B)
|
|
590
|
+
c_idx = pid_c * BLOCK_C + tl.arange(0, BLOCK_C)
|
|
591
|
+
|
|
592
|
+
mb = b_idx < B
|
|
593
|
+
mc = c_idx < C
|
|
594
|
+
|
|
595
|
+
# load active flags for this tile's rows
|
|
596
|
+
act = tl.load(active + b_idx, mask=mb, other=0)
|
|
597
|
+
inactive_rows = (act == 0) & mb
|
|
598
|
+
|
|
599
|
+
# broadcast to 2D tile
|
|
600
|
+
bb = b_idx[:, None]
|
|
601
|
+
cc = c_idx[None, :]
|
|
602
|
+
mask2d = inactive_rows[:, None] & mc[None, :]
|
|
603
|
+
|
|
604
|
+
tl.store(check_ok + bb * C + cc, 0, mask=mask2d)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
# fmt: off
|
|
608
|
+
@triton.jit
|
|
609
|
+
def check_and_select_kernel(
|
|
610
|
+
check_ok, # [B,C] uint8
|
|
611
|
+
posterior_ratios, # [B,V] dtype - always used for weight computation (matches Rust)
|
|
612
|
+
hard_dec, # [B,V] uint8
|
|
613
|
+
wj, # [V] shared or [B,V] per-shot dtype
|
|
614
|
+
best_weights, # [B] dtype (in/out)
|
|
615
|
+
best_errors, # [B,V] uint8 (out when improved)
|
|
616
|
+
valid_solutions, # [B] uint8 (out flag)
|
|
617
|
+
found_count, # [B] int32 (in/out)
|
|
618
|
+
active, # [B] uint8 - active lane mask (skip frozen lanes)
|
|
619
|
+
B,
|
|
620
|
+
C,
|
|
621
|
+
V,
|
|
622
|
+
dtype_enum: tl.constexpr, # 0=fp16, 1=fp32, 2=fp64 (compile-time constant)
|
|
623
|
+
BLOCK_C: tl.constexpr,
|
|
624
|
+
BLOCK_V: tl.constexpr,
|
|
625
|
+
NUM_SOLS: tl.constexpr, # stop_nconv
|
|
626
|
+
PRIOR_BATCHED: tl.constexpr = False, # if True, wj is [B,V], else [V]
|
|
627
|
+
):
|
|
628
|
+
# fmt: on
|
|
629
|
+
"""Device-side solution selection and validation kernel.
|
|
630
|
+
|
|
631
|
+
This kernel performs solution selection and validation entirely on device,
|
|
632
|
+
avoiding host-device transfers. For each lane, it:
|
|
633
|
+
1. Checks if all parity constraints are satisfied
|
|
634
|
+
2. Computes solution weight using log-prior ratios (always uses posterior_ratios, matching Rust)
|
|
635
|
+
3. Updates best solution if current solution is better
|
|
636
|
+
|
|
637
|
+
This corresponds to the solution selection logic in Rust relay.rs.
|
|
638
|
+
|
|
639
|
+
Weight computation matches Rust get_decoding_quality():
|
|
640
|
+
- weight = sum(wj[i] for i where error_bits[i] == 1 and is_finite(wj[i]))
|
|
641
|
+
- wj contains raw log_prior_ratios (can be negative when error prob > 0.5)
|
|
642
|
+
- Only finite values contribute (non-finite values are set to 0 in wj)
|
|
643
|
+
|
|
644
|
+
Args:
|
|
645
|
+
check_ok: [B,C] mask of satisfied constraints
|
|
646
|
+
posterior_ratios: [B,V] belief marginals (always used for weight computation)
|
|
647
|
+
hard_dec: [B,V] hard decisions
|
|
648
|
+
wj: [V] raw log-prior ratios for weight computation (not abs, can be negative)
|
|
649
|
+
best_weights: [B] current best weights (in/out)
|
|
650
|
+
best_errors: [B,V] current best error patterns (out)
|
|
651
|
+
valid_solutions: [B] valid solution flags (out)
|
|
652
|
+
found_count: [B] convergence counters (in/out)
|
|
653
|
+
active: [B] active lane mask (1=active, 0=frozen; frozen lanes are skipped)
|
|
654
|
+
B, C, V: batch size, checks, variables
|
|
655
|
+
BLOCK_C, BLOCK_V: vectorization block sizes
|
|
656
|
+
"""
|
|
657
|
+
b = tl.program_id(axis=0)
|
|
658
|
+
if b >= B:
|
|
659
|
+
return
|
|
660
|
+
# Skip frozen lanes (avoid redundant parity reads)
|
|
661
|
+
if tl.load(active + b) == 0:
|
|
662
|
+
return
|
|
663
|
+
|
|
664
|
+
# 1) all_ok = all(check_ok[b, :])
|
|
665
|
+
all_ok = tl.full((), 1, dtype=tl.int32)
|
|
666
|
+
c0 = 0
|
|
667
|
+
while c0 < C:
|
|
668
|
+
cc = c0 + tl.arange(0, BLOCK_C)
|
|
669
|
+
mc = cc < C
|
|
670
|
+
vals = tl.load(check_ok + b * C + cc, mask=mc, other=0).to(tl.int32)
|
|
671
|
+
# if any zero present in this tile, min will be 0
|
|
672
|
+
tile_all = tl.min(tl.where(mc, vals, 1), axis=0)
|
|
673
|
+
all_ok = tl.minimum(all_ok, tile_all)
|
|
674
|
+
c0 += BLOCK_C
|
|
675
|
+
|
|
676
|
+
if all_ok == 0:
|
|
677
|
+
return
|
|
678
|
+
|
|
679
|
+
# 2) compute current hard decisions and weight (always use posterior_ratios, matching Rust)
|
|
680
|
+
# Use storage dtype directly for all computations
|
|
681
|
+
if dtype_enum == 0: # fp16
|
|
682
|
+
weight = tl.zeros((), dtype=tl.float16)
|
|
683
|
+
elif dtype_enum == 2: # fp64
|
|
684
|
+
weight = tl.zeros((), dtype=tl.float64)
|
|
685
|
+
else: # fp32
|
|
686
|
+
weight = tl.zeros((), dtype=tl.float32)
|
|
687
|
+
v0 = 0
|
|
688
|
+
while v0 < V:
|
|
689
|
+
vv = v0 + tl.arange(0, BLOCK_V)
|
|
690
|
+
mv = vv < V
|
|
691
|
+
posterior_v = tl.load(posterior_ratios + b * V + vv, mask=mv, other=0.0)
|
|
692
|
+
bits = (posterior_v <= 0.0).to(tl.uint8)
|
|
693
|
+
if PRIOR_BATCHED:
|
|
694
|
+
wtile = tl.load(wj + b * V + vv, mask=mv, other=0.0)
|
|
695
|
+
else:
|
|
696
|
+
wtile = tl.load(wj + vv, mask=mv, other=0.0)
|
|
697
|
+
# Convert bits to match weight dtype (optimize fp32 path)
|
|
698
|
+
if dtype_enum == 0: # fp16
|
|
699
|
+
weight += tl.sum(wtile * bits.to(tl.float16), axis=0)
|
|
700
|
+
elif dtype_enum == 2: # fp64
|
|
701
|
+
weight += tl.sum(wtile * bits.to(tl.float64), axis=0)
|
|
702
|
+
else: # fp32 - bits is uint8, need to convert to float for multiplication
|
|
703
|
+
weight += tl.sum(wtile * bits.to(tl.float32), axis=0)
|
|
704
|
+
v0 += BLOCK_V
|
|
705
|
+
|
|
706
|
+
# 3) compare-and-update best
|
|
707
|
+
# Match Rust: First valid solution always becomes the initial best (line 309: min_pm = result.decoding_quality)
|
|
708
|
+
# Subsequent solutions only update if strictly better (line 362: if pm < min_pm)
|
|
709
|
+
prev_best = tl.load(best_weights + b)
|
|
710
|
+
prev_valid = tl.load(valid_solutions + b) # Load before updating
|
|
711
|
+
# Check if this is the first valid solution ever (prev_best == inf)
|
|
712
|
+
# Use dtype-appropriate threshold for infinity detection
|
|
713
|
+
if dtype_enum == 0: # fp16: max finite ~65504
|
|
714
|
+
inf_threshold = 60000.0
|
|
715
|
+
elif dtype_enum == 2: # fp64
|
|
716
|
+
inf_threshold = 1e20
|
|
717
|
+
else: # fp32
|
|
718
|
+
inf_threshold = 1e20
|
|
719
|
+
is_inf = (prev_best >= inf_threshold).to(tl.int32)
|
|
720
|
+
# First valid solution always updates (matches Rust line 309)
|
|
721
|
+
# Subsequent solutions only update if strictly better (matches Rust line 362)
|
|
722
|
+
better = (weight < prev_best) | (is_inf == 1)
|
|
723
|
+
if better:
|
|
724
|
+
# weight is already in correct dtype, no conversion needed
|
|
725
|
+
tl.store(best_weights + b, weight)
|
|
726
|
+
# write best_errors row (always use posterior_ratios, matching Rust)
|
|
727
|
+
off = 0
|
|
728
|
+
while off < V:
|
|
729
|
+
vv = off + tl.arange(0, BLOCK_V)
|
|
730
|
+
mv = vv < V
|
|
731
|
+
posterior_v2 = tl.load(posterior_ratios + b * V + vv, mask=mv, other=0.0)
|
|
732
|
+
# Match Rust: Bit::from(posterior <= 0) for best_errors storage
|
|
733
|
+
bits2 = (posterior_v2 <= 0.0).to(tl.uint8)
|
|
734
|
+
tl.store(best_errors + b * V + vv, bits2, mask=mv)
|
|
735
|
+
off += BLOCK_V
|
|
736
|
+
# mark lane valid for this cadence
|
|
737
|
+
tl.store(valid_solutions + b, 1)
|
|
738
|
+
# Increment found_count only on first success since last reset (per leg)
|
|
739
|
+
if prev_valid == 0:
|
|
740
|
+
old = tl.load(found_count + b)
|
|
741
|
+
tl.store(found_count + b, old + 1)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
# fmt: off
|
|
745
|
+
@triton.jit
|
|
746
|
+
def freeze_finished_lanes_kernel(
|
|
747
|
+
best_errors, # [B,V] uint8
|
|
748
|
+
hard_dec,
|
|
749
|
+
gamma,
|
|
750
|
+
active, # [B,V] uint8, [B,V] fp32, [B] uint8
|
|
751
|
+
found_count,
|
|
752
|
+
first_iter,
|
|
753
|
+
iter_counter, # [B] int32, [B] int32, [B] int32
|
|
754
|
+
sought_sols,
|
|
755
|
+
stop_nconv,
|
|
756
|
+
in_leg: tl.constexpr,
|
|
757
|
+
count_iteration: tl.constexpr,
|
|
758
|
+
V: tl.constexpr,
|
|
759
|
+
all_done_flag, # [1] uint8 (out) - set to 1 if all lanes converged
|
|
760
|
+
arrival_counter, # [1] int32 (atomic) - arrival counter for last-program reduction
|
|
761
|
+
B, # scalar int32 - number of batch lanes
|
|
762
|
+
):
|
|
763
|
+
# fmt: on
|
|
764
|
+
"""Freeze converged lanes, increment iter_counter, and check all-done (fused).
|
|
765
|
+
|
|
766
|
+
This kernel implements lane freezing for converged solutions, corresponding
|
|
767
|
+
to the convergence handling in Rust relay.rs:
|
|
768
|
+
1. With count_iteration, increments iter_counter for every lane that is active on
|
|
769
|
+
entry, including a lane that converges in this call
|
|
770
|
+
2. For lanes that have found enough valid solutions: records the first convergence
|
|
771
|
+
iteration (outside relay legs), freezes the lane (sets active=0) and resets its
|
|
772
|
+
gamma values
|
|
773
|
+
3. Last-arriving program checks if all lanes converged (fused reduce_all_ge)
|
|
774
|
+
|
|
775
|
+
Args:
|
|
776
|
+
best_errors: [B,V] best error patterns found so far
|
|
777
|
+
hard_dec: [B,V] current hard decisions (updated for converged lanes)
|
|
778
|
+
gamma: [B,V] memory strengths (reset to 0 for converged lanes)
|
|
779
|
+
active: [B] active lane mask (set to 0 for converged lanes)
|
|
780
|
+
found_count: [B] convergence counters
|
|
781
|
+
first_iter: [B] first convergence iteration (updated for converged lanes)
|
|
782
|
+
iter_counter: [B] per-lane count of BP iterations run
|
|
783
|
+
stop_nconv: target number of solutions for convergence
|
|
784
|
+
in_leg: use min(sought_sols, stop_nconv) as the stop criterion and do not record first_iter
|
|
785
|
+
count_iteration: count the BP iteration that precedes this call
|
|
786
|
+
V: number of variables
|
|
787
|
+
all_done_flag: [1] output flag (1 if all lanes converged, 0 otherwise)
|
|
788
|
+
arrival_counter: [1] atomic counter for coordinating last-program reduction
|
|
789
|
+
B: number of batch lanes
|
|
790
|
+
"""
|
|
791
|
+
b = tl.program_id(axis=0)
|
|
792
|
+
# Process lane only if active (skip frozen lanes)
|
|
793
|
+
if tl.load(active + b) != 0:
|
|
794
|
+
# Increment current_iteration after every iteration, including the one that converges.
|
|
795
|
+
itc = tl.load(iter_counter + b)
|
|
796
|
+
if count_iteration:
|
|
797
|
+
itc = itc + 1
|
|
798
|
+
tl.store(iter_counter + b, itc)
|
|
799
|
+
fc = tl.load(found_count + b)
|
|
800
|
+
if in_leg:
|
|
801
|
+
ss = tl.load(sought_sols + b)
|
|
802
|
+
stop_criterion = tl.minimum(ss, stop_nconv)
|
|
803
|
+
else:
|
|
804
|
+
stop_criterion = stop_nconv
|
|
805
|
+
done_now = fc >= stop_criterion
|
|
806
|
+
if done_now:
|
|
807
|
+
fi = tl.load(first_iter + b)
|
|
808
|
+
if fi < 0:
|
|
809
|
+
if not in_leg:
|
|
810
|
+
tl.store(first_iter + b, itc)
|
|
811
|
+
col = 0
|
|
812
|
+
while col < V:
|
|
813
|
+
offs = col + tl.arange(0, 128)
|
|
814
|
+
m = offs < V
|
|
815
|
+
be = tl.load(best_errors + b * V + offs, mask=m, other=0)
|
|
816
|
+
tl.store(hard_dec + b * V + offs, be, mask=m)
|
|
817
|
+
col += 128
|
|
818
|
+
tl.store(active + b, 0)
|
|
819
|
+
col2 = 0
|
|
820
|
+
while col2 < V:
|
|
821
|
+
offs2 = col2 + tl.arange(0, 128)
|
|
822
|
+
m2 = offs2 < V
|
|
823
|
+
tl.store(gamma + b * V + offs2, 0.0, mask=m2)
|
|
824
|
+
col2 += 128
|
|
825
|
+
|
|
826
|
+
# All programs (frozen and active) participate in the all-done reduction.
|
|
827
|
+
# The last program to arrive performs the reduction over found_count.
|
|
828
|
+
old_arrival = tl.atomic_add(arrival_counter, 1)
|
|
829
|
+
if old_arrival == B - 1:
|
|
830
|
+
# Last program: check if all lanes have converged
|
|
831
|
+
all_ok = 1
|
|
832
|
+
tile = 0
|
|
833
|
+
while tile < B:
|
|
834
|
+
offs_r = tile + tl.arange(0, 256)
|
|
835
|
+
m_r = offs_r < B
|
|
836
|
+
fc_r = tl.load(found_count + offs_r, mask=m_r, other=stop_nconv)
|
|
837
|
+
if in_leg:
|
|
838
|
+
ss_r = tl.load(sought_sols + offs_r, mask=m_r, other=stop_nconv)
|
|
839
|
+
sc_r = tl.minimum(ss_r, stop_nconv)
|
|
840
|
+
else:
|
|
841
|
+
sc_r = tl.zeros_like(fc_r) + stop_nconv
|
|
842
|
+
ok_r = (fc_r >= sc_r).to(tl.int32)
|
|
843
|
+
all_ok = tl.minimum(all_ok, tl.min(tl.where(m_r, ok_r, 1), axis=0))
|
|
844
|
+
tile += 256
|
|
845
|
+
tl.store(all_done_flag, all_ok)
|
|
846
|
+
# Reset arrival counter for next call
|
|
847
|
+
tl.store(arrival_counter, 0)
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
# fmt: off
|
|
851
|
+
@triton.jit
|
|
852
|
+
def init_messages_kernel(
|
|
853
|
+
mu,
|
|
854
|
+
nu, # [B,E] messages - default mode layout
|
|
855
|
+
edge_var, # [E] variable index for each edge
|
|
856
|
+
log_prior_ratios, # [V] shared or [B,V] per-shot prior log-likelihood ratios
|
|
857
|
+
B,
|
|
858
|
+
E,
|
|
859
|
+
V, # batch size, number of edges, number of variables
|
|
860
|
+
dtype_enum: tl.constexpr, # 0=fp16, 1=fp32, 2=fp64
|
|
861
|
+
BLOCK: tl.constexpr,
|
|
862
|
+
PRIOR_BATCHED: tl.constexpr = False, # if True, log_prior_ratios is [B,V], else [V]
|
|
863
|
+
):
|
|
864
|
+
# fmt: on
|
|
865
|
+
"""Initialize message tensors: nu to zero, mu to prior values (for [B,E] layout).
|
|
866
|
+
|
|
867
|
+
Note: This is used for default mode. For throughput mode with [E,B] layout,
|
|
868
|
+
use init_messages_with_priors_EB_kernel.
|
|
869
|
+
"""
|
|
870
|
+
pid = tl.program_id(axis=0)
|
|
871
|
+
N = B * E
|
|
872
|
+
offs = pid * BLOCK + tl.arange(0, BLOCK)
|
|
873
|
+
m = offs < N
|
|
874
|
+
|
|
875
|
+
# Compute batch and edge indices for [B,E] layout
|
|
876
|
+
b_idx = offs // E
|
|
877
|
+
e_idx = offs % E
|
|
878
|
+
|
|
879
|
+
# Additional safety: ensure indices are within bounds
|
|
880
|
+
m = m & (b_idx < B) & (e_idx < E)
|
|
881
|
+
|
|
882
|
+
# Zero nu (check-to-variable messages)
|
|
883
|
+
z = tl.zeros((BLOCK,), dtype=tl.float32)
|
|
884
|
+
tl.store(nu + offs, z, mask=m)
|
|
885
|
+
|
|
886
|
+
# Initialize mu (variable-to-check messages) to prior values
|
|
887
|
+
j = tl.load(edge_var + e_idx, mask=m, other=0)
|
|
888
|
+
if PRIOR_BATCHED:
|
|
889
|
+
prior = tl.load(log_prior_ratios + b_idx * V + j, mask=m, other=0.0)
|
|
890
|
+
else:
|
|
891
|
+
prior = tl.load(log_prior_ratios + j, mask=m, other=0.0)
|
|
892
|
+
|
|
893
|
+
# Store with appropriate dtype based on dtype_enum (compile-time optimized)
|
|
894
|
+
if dtype_enum == 0: # fp16
|
|
895
|
+
tl.store(mu + offs, prior.to(tl.float16), mask=m)
|
|
896
|
+
elif dtype_enum == 2: # fp64
|
|
897
|
+
tl.store(mu + offs, prior.to(tl.float64), mask=m)
|
|
898
|
+
else: # fp32 (dtype_enum == 1)
|
|
899
|
+
tl.store(mu + offs, prior, mask=m)
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
# fmt: off
|
|
903
|
+
@triton.jit
|
|
904
|
+
def init_messages_with_priors_EB_kernel(
|
|
905
|
+
muT,
|
|
906
|
+
nuT, # [E,B] messages - throughput layout
|
|
907
|
+
edge_var, # [E] variable index for each edge
|
|
908
|
+
log_prior_ratios, # [V] shared or [B,V] per-shot prior log-likelihood ratios
|
|
909
|
+
E,
|
|
910
|
+
V,
|
|
911
|
+
B,
|
|
912
|
+
active_idx, # [B_active] int32 - compact list of active batch indices (ignored if use_active=False)
|
|
913
|
+
B_active, # int32 - number of active lanes
|
|
914
|
+
use_active: tl.constexpr, # bool - if True, only initialize active lanes
|
|
915
|
+
dtype_enum: tl.constexpr, # 0=fp16, 1=fp32, 2=fp64 (compile-time constant)
|
|
916
|
+
BLOCK: tl.constexpr,
|
|
917
|
+
PRIOR_BATCHED: tl.constexpr = False, # if True, log_prior_ratios is [B,V], else [V]
|
|
918
|
+
):
|
|
919
|
+
# fmt: on
|
|
920
|
+
"""Initialize messages: nu to zero, mu to prior values (throughput [E,B] layout).
|
|
921
|
+
|
|
922
|
+
This matches Rust's initialization:
|
|
923
|
+
- check_to_variable (nu) -> zero
|
|
924
|
+
- variable_to_check (mu) -> priors (log_prior_ratios)
|
|
925
|
+
|
|
926
|
+
If use_active=True and B_active < B, only initializes messages for active lanes,
|
|
927
|
+
dramatically reducing memory traffic when B_active << B.
|
|
928
|
+
|
|
929
|
+
For each edge e:
|
|
930
|
+
- If use_active=False: sets muT[e*B + b] = log_prior_ratios[j] for all batches b
|
|
931
|
+
- If use_active=True: sets muT[e*B + bs[b_active]] = log_prior_ratios[j] for active batches only
|
|
932
|
+
"""
|
|
933
|
+
pid = tl.program_id(axis=0)
|
|
934
|
+
|
|
935
|
+
if use_active and (B_active < B):
|
|
936
|
+
# Active lanes only: process E * B_active elements
|
|
937
|
+
offs = pid * BLOCK + tl.arange(0, BLOCK)
|
|
938
|
+
m = offs < (E * B_active)
|
|
939
|
+
|
|
940
|
+
# Compute edge and active batch indices
|
|
941
|
+
edge_idx = offs // B_active
|
|
942
|
+
b_active_idx = offs % B_active
|
|
943
|
+
|
|
944
|
+
# Additional safety: ensure edge_idx is within bounds
|
|
945
|
+
m = m & (edge_idx < E) & (b_active_idx < B_active)
|
|
946
|
+
|
|
947
|
+
# Load active batch index
|
|
948
|
+
# Safety: b_active_idx is already validated to be < B_active, so active_idx[b_active_idx] is valid
|
|
949
|
+
# If active_idx contains invalid indices (>= B), this will cause out-of-bounds access
|
|
950
|
+
# Validation in decoder.py should catch this before kernel launch
|
|
951
|
+
bs = tl.load(active_idx + b_active_idx, mask=m, other=0)
|
|
952
|
+
|
|
953
|
+
# Compute linear offset in [E,B] layout: e*B + b
|
|
954
|
+
# Note: edge_idx is already validated to be < E by the mask
|
|
955
|
+
# Validation in decoder.py ensures E * B <= INT32_MAX, so int32 arithmetic is safe
|
|
956
|
+
linear_offs = edge_idx * B + bs
|
|
957
|
+
|
|
958
|
+
# Zero nu (check-to-variable messages) for active lanes only
|
|
959
|
+
z = tl.zeros((BLOCK,), dtype=tl.float32)
|
|
960
|
+
tl.store(nuT + linear_offs, z, mask=m)
|
|
961
|
+
|
|
962
|
+
# Initialize mu (variable-to-check messages) to prior values for active lanes only
|
|
963
|
+
j = tl.load(edge_var + edge_idx, mask=m, other=0)
|
|
964
|
+
if PRIOR_BATCHED:
|
|
965
|
+
prior = tl.load(log_prior_ratios + bs * V + j, mask=m, other=0.0)
|
|
966
|
+
else:
|
|
967
|
+
prior = tl.load(log_prior_ratios + j, mask=m, other=0.0)
|
|
968
|
+
|
|
969
|
+
# Store with appropriate dtype based on dtype_enum (compile-time optimized)
|
|
970
|
+
if dtype_enum == 0: # fp16
|
|
971
|
+
tl.store(muT + linear_offs, prior.to(tl.float16), mask=m)
|
|
972
|
+
elif dtype_enum == 2: # fp64
|
|
973
|
+
tl.store(muT + linear_offs, prior.to(tl.float64), mask=m)
|
|
974
|
+
else: # fp32 (dtype_enum == 1)
|
|
975
|
+
tl.store(muT + linear_offs, prior, mask=m)
|
|
976
|
+
else:
|
|
977
|
+
# Full initialization: process E * B elements
|
|
978
|
+
offs = pid * BLOCK + tl.arange(0, BLOCK)
|
|
979
|
+
m = offs < (E * B)
|
|
980
|
+
|
|
981
|
+
# Zero nu (check-to-variable messages)
|
|
982
|
+
z = tl.zeros((BLOCK,), dtype=tl.float32)
|
|
983
|
+
tl.store(nuT + offs, z, mask=m)
|
|
984
|
+
|
|
985
|
+
# Initialize mu (variable-to-check messages) to prior values
|
|
986
|
+
edge_idx = offs // B
|
|
987
|
+
j = tl.load(edge_var + edge_idx, mask=m, other=0)
|
|
988
|
+
if PRIOR_BATCHED:
|
|
989
|
+
b_idx = offs % B # [E,B] layout: batch index is the inner stride
|
|
990
|
+
prior = tl.load(log_prior_ratios + b_idx * V + j, mask=m, other=0.0)
|
|
991
|
+
else:
|
|
992
|
+
prior = tl.load(log_prior_ratios + j, mask=m, other=0.0)
|
|
993
|
+
|
|
994
|
+
# Store with appropriate dtype based on dtype_enum (compile-time optimized)
|
|
995
|
+
if dtype_enum == 0: # fp16
|
|
996
|
+
tl.store(muT + offs, prior.to(tl.float16), mask=m)
|
|
997
|
+
elif dtype_enum == 2: # fp64
|
|
998
|
+
tl.store(muT + offs, prior.to(tl.float64), mask=m)
|
|
999
|
+
else: # fp32 (dtype_enum == 1)
|
|
1000
|
+
tl.store(muT + offs, prior, mask=m)
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
# fmt: off
|
|
1004
|
+
@triton.jit
|
|
1005
|
+
def reduce_all_ge_kernel(
|
|
1006
|
+
found_count, # [B] int32
|
|
1007
|
+
stop_nconv, # scalar int32
|
|
1008
|
+
out_flag, # [1] uint8 (out)
|
|
1009
|
+
B, # scalar int32
|
|
1010
|
+
):
|
|
1011
|
+
# fmt: on
|
|
1012
|
+
"""Set out_flag to 1 if every lane has found_count >= stop_nconv, else 0.
|
|
1013
|
+
|
|
1014
|
+
Single-program device-side reduction over the B lanes.
|
|
1015
|
+
"""
|
|
1016
|
+
pid = tl.program_id(axis=0)
|
|
1017
|
+
if pid != 0:
|
|
1018
|
+
return
|
|
1019
|
+
acc = tl.full((), 1, dtype=tl.int32)
|
|
1020
|
+
tile = 0
|
|
1021
|
+
while tile < B:
|
|
1022
|
+
offs = tile + tl.arange(0, 256)
|
|
1023
|
+
m = offs < B
|
|
1024
|
+
fc = tl.load(found_count + offs, mask=m, other=stop_nconv)
|
|
1025
|
+
ok = (fc >= stop_nconv).to(tl.int32)
|
|
1026
|
+
acc = tl.minimum(acc, tl.min(tl.where(m, ok, 1), axis=0))
|
|
1027
|
+
tile += 256
|
|
1028
|
+
tl.store(out_flag, acc.to(tl.uint8))
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
@triton.jit
|
|
1032
|
+
def unfreeze_finished_lanes_kernel(active, found_count, stop_nconv): # [B] uint8 # [B] int32
|
|
1033
|
+
"""Unfreeze lanes with less than stop_nconv solutions after
|
|
1034
|
+
they were frozen for findig previous solution.
|
|
1035
|
+
|
|
1036
|
+
Args:
|
|
1037
|
+
active: [B] active lane mask (set to 0 for converged lanes)
|
|
1038
|
+
found_count: [B] convergence counters
|
|
1039
|
+
stop_nconv: target number of solutions for convergence
|
|
1040
|
+
"""
|
|
1041
|
+
b = tl.program_id(axis=0)
|
|
1042
|
+
fc = tl.load(found_count + b)
|
|
1043
|
+
done_now = fc >= stop_nconv
|
|
1044
|
+
if not done_now:
|
|
1045
|
+
tl.store(active + b, 1)
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
# fmt: off
|
|
1051
|
+
@triton.jit
|
|
1052
|
+
def c2v_min_sum_btile_kernel(
|
|
1053
|
+
muT,
|
|
1054
|
+
nuT, # [E,B]
|
|
1055
|
+
chk_ptr,
|
|
1056
|
+
chk_edges, # CSR over checks -> edge IDs
|
|
1057
|
+
syndrome, # [B,C] (uint8)
|
|
1058
|
+
active_idx, # [B_active] int32 - compact list of active batch indices
|
|
1059
|
+
active, # [B] uint8 - active lane mask (skip frozen lanes)
|
|
1060
|
+
B,
|
|
1061
|
+
B_active,
|
|
1062
|
+
C,
|
|
1063
|
+
E,
|
|
1064
|
+
alpha,
|
|
1065
|
+
beta,
|
|
1066
|
+
use_alpha: tl.constexpr,
|
|
1067
|
+
use_beta: tl.constexpr,
|
|
1068
|
+
dtype_enum: tl.constexpr, # 0=fp16, 1=fp32, 2=fp64 (compile-time constant)
|
|
1069
|
+
BLOCK_SIZE: tl.constexpr,
|
|
1070
|
+
BTILE: tl.constexpr,
|
|
1071
|
+
chk_start: int, # Start check index for block-layered (0 = all checks)
|
|
1072
|
+
chk_end: int, # End check index for block-layered (0 = all checks, means C)
|
|
1073
|
+
edge_var, # [E] int32 - variable index per edge (only used when use_delta=True)
|
|
1074
|
+
L, # [B,V] - per-lane marginals (only used when use_delta=True)
|
|
1075
|
+
V: int, # number of variables
|
|
1076
|
+
use_delta: tl.constexpr, # if True, run delta-trick variant
|
|
1077
|
+
):
|
|
1078
|
+
# fmt: on
|
|
1079
|
+
"""Check-to-variable min-sum over [E,B] layout, vectorized across BTILE batch lanes.
|
|
1080
|
+
Each program processes one check row i and updates all its edges' nuT[:, b:b+BTILE].
|
|
1081
|
+
|
|
1082
|
+
For block-layered scheduling:
|
|
1083
|
+
- chk_start, chk_end: Process only checks in range [chk_start, chk_end)
|
|
1084
|
+
- If chk_end == 0, processes all checks (flood-fill mode)
|
|
1085
|
+
"""
|
|
1086
|
+
i = tl.program_id(axis=0)
|
|
1087
|
+
btile_id = tl.program_id(axis=1)
|
|
1088
|
+
|
|
1089
|
+
# Block-layered: adjust check index if range is specified
|
|
1090
|
+
if chk_end > 0:
|
|
1091
|
+
# Block-layered mode: i is relative to block, add chk_start offset
|
|
1092
|
+
chk_idx = chk_start + i
|
|
1093
|
+
if chk_idx >= chk_end:
|
|
1094
|
+
return # Out of block range
|
|
1095
|
+
else:
|
|
1096
|
+
# Flood-fill mode: i is absolute check index
|
|
1097
|
+
chk_idx = i
|
|
1098
|
+
if chk_idx >= C:
|
|
1099
|
+
return
|
|
1100
|
+
|
|
1101
|
+
# Use chk_idx instead of i for all check operations
|
|
1102
|
+
i = chk_idx
|
|
1103
|
+
b0 = btile_id * BTILE
|
|
1104
|
+
pos = b0 + tl.arange(0, BTILE)
|
|
1105
|
+
mpos = pos < B_active
|
|
1106
|
+
bs = tl.load(active_idx + pos, mask=mpos, other=0)
|
|
1107
|
+
# Skip frozen lanes via active mask (avoids compaction overhead)
|
|
1108
|
+
active_vals = tl.load(active + bs, mask=mpos, other=0)
|
|
1109
|
+
mb = mpos & (active_vals != 0)
|
|
1110
|
+
|
|
1111
|
+
row_start = tl.load(chk_ptr + i)
|
|
1112
|
+
row_end = tl.load(chk_ptr + i + 1)
|
|
1113
|
+
if row_end <= row_start:
|
|
1114
|
+
return
|
|
1115
|
+
deg_is_one = row_end - row_start == 1
|
|
1116
|
+
|
|
1117
|
+
# Accumulators per batch lane
|
|
1118
|
+
neg_parity = tl.zeros((BTILE,), dtype=tl.int32)
|
|
1119
|
+
# Use fp32 accumulators for fp16/fp32 storage, fp64 for fp64
|
|
1120
|
+
if dtype_enum == 2: # fp64
|
|
1121
|
+
min1 = tl.full((BTILE,), 1e30, dtype=tl.float64)
|
|
1122
|
+
min2 = tl.full((BTILE,), 1e30, dtype=tl.float64)
|
|
1123
|
+
else: # fp16 or fp32
|
|
1124
|
+
min1 = tl.full((BTILE,), 1e30, dtype=tl.float32)
|
|
1125
|
+
min2 = tl.full((BTILE,), 1e30, dtype=tl.float32)
|
|
1126
|
+
cnt1 = tl.zeros((BTILE,), dtype=tl.int32)
|
|
1127
|
+
|
|
1128
|
+
# PASS 1: compute min1/min2 and parity per batch lane
|
|
1129
|
+
tile = row_start
|
|
1130
|
+
while tile < row_end:
|
|
1131
|
+
offs = tile + tl.arange(0, BLOCK_SIZE)
|
|
1132
|
+
mrow = offs < row_end
|
|
1133
|
+
evec = tl.load(chk_edges + offs, mask=mrow, other=0) # [BLOCK_SIZE]
|
|
1134
|
+
# Build 2D mask and gather inputs over edges×BTILE
|
|
1135
|
+
mask2d = (mrow[:, None]) & (mb[None, :])
|
|
1136
|
+
if use_delta:
|
|
1137
|
+
v_idx = tl.load(edge_var + evec, mask=mrow, other=0)
|
|
1138
|
+
L_tile = tl.load(L + (bs[None, :] * V) + v_idx[:, None], mask=mask2d, other=0.0)
|
|
1139
|
+
nu_old = tl.load(nuT + (evec[:, None]) * B + bs[None, :], mask=mask2d, other=0.0)
|
|
1140
|
+
mu_tile = L_tile - nu_old
|
|
1141
|
+
else:
|
|
1142
|
+
mu_tile = tl.load(
|
|
1143
|
+
muT + (evec[:, None]) * B + bs[None, :], mask=mask2d, other=0.0
|
|
1144
|
+
) # [BLOCK_SIZE, BTILE]
|
|
1145
|
+
# tile parity via xor of tile parity bits
|
|
1146
|
+
tile_par = tl.sum((mu_tile < 0.0) & mask2d, axis=0).to(tl.int32) & 1
|
|
1147
|
+
neg_parity = neg_parity ^ tile_par
|
|
1148
|
+
# For fp16 storage, do reductions in fp32; otherwise stay in storage dtype
|
|
1149
|
+
a = tl.abs(mu_tile.to(tl.float32)) if dtype_enum == 0 else tl.abs(mu_tile)
|
|
1150
|
+
a = tl.where(mask2d, a, 1e30)
|
|
1151
|
+
# min1 over edges and tie count
|
|
1152
|
+
min1_t = tl.min(a, axis=0)
|
|
1153
|
+
cnt1_t = tl.sum(a == min1_t[None, :], axis=0).to(tl.int32)
|
|
1154
|
+
# min2: mask a single occurrence along edge axis
|
|
1155
|
+
pos1 = tl.argmax(-a, axis=0)
|
|
1156
|
+
a2 = tl.where(tl.arange(0, BLOCK_SIZE)[:, None] == pos1[None, :], 1e30, a)
|
|
1157
|
+
min2_t = tl.min(a2, axis=0)
|
|
1158
|
+
# merge with running min1/min2 and counts
|
|
1159
|
+
better1 = min1_t < min1
|
|
1160
|
+
cnt1 = tl.where(better1, cnt1_t, tl.where(min1_t == min1, cnt1 + cnt1_t, cnt1))
|
|
1161
|
+
min2 = tl.where(
|
|
1162
|
+
better1,
|
|
1163
|
+
tl.minimum(min1, min2_t),
|
|
1164
|
+
tl.where(min1_t == min1, tl.minimum(min2, min2_t), tl.minimum(min2, min1_t)),
|
|
1165
|
+
)
|
|
1166
|
+
min1 = tl.where(better1, min1_t, min1)
|
|
1167
|
+
tile += BLOCK_SIZE
|
|
1168
|
+
|
|
1169
|
+
syn = tl.load(syndrome + bs * C + i, mask=mb, other=0).to(tl.int32) & 1
|
|
1170
|
+
par_bit = (neg_parity & 1) ^ syn
|
|
1171
|
+
sign_prod = tl.where(par_bit == 0, 1.0, -1.0)
|
|
1172
|
+
|
|
1173
|
+
# PASS 2: compute and store nu over edges×BTILE
|
|
1174
|
+
tile = row_start
|
|
1175
|
+
while tile < row_end:
|
|
1176
|
+
offs = tile + tl.arange(0, BLOCK_SIZE)
|
|
1177
|
+
mrow = offs < row_end
|
|
1178
|
+
evec = tl.load(chk_edges + offs, mask=mrow, other=0)
|
|
1179
|
+
mask2d = (mrow[:, None]) & (mb[None, :])
|
|
1180
|
+
if use_delta:
|
|
1181
|
+
v_idx = tl.load(edge_var + evec, mask=mrow, other=0)
|
|
1182
|
+
L_tile = tl.load(L + (bs[None, :] * V) + v_idx[:, None], mask=mask2d, other=0.0)
|
|
1183
|
+
nu_old = tl.load(nuT + (evec[:, None]) * B + bs[None, :], mask=mask2d, other=0.0)
|
|
1184
|
+
mu_tile = L_tile - nu_old
|
|
1185
|
+
else:
|
|
1186
|
+
mu_tile = tl.load(muT + (evec[:, None]) * B + bs[None, :], mask=mask2d, other=0.0)
|
|
1187
|
+
nu_old = tl.load(nuT + (evec[:, None]) * B + bs[None, :], mask=mask2d, other=0.0)
|
|
1188
|
+
sgn = tl.where(mu_tile >= 0.0, 1.0, -1.0)
|
|
1189
|
+
# Identify min1 lanes per edge×batch and apply correct tie logic
|
|
1190
|
+
a = tl.abs(mu_tile)
|
|
1191
|
+
a_mask = tl.where(mask2d, a, 1e30)
|
|
1192
|
+
is_min1 = a_mask == min1[None, :]
|
|
1193
|
+
use_min2 = (cnt1[None, :] == 1) & is_min1
|
|
1194
|
+
out_mag = tl.where(use_min2, min2[None, :], min1[None, :])
|
|
1195
|
+
# Degree-1 checks must send zero.
|
|
1196
|
+
out_mag = tl.where(deg_is_one, 0.0, out_mag)
|
|
1197
|
+
if use_alpha:
|
|
1198
|
+
out_mag = alpha * out_mag
|
|
1199
|
+
if use_beta:
|
|
1200
|
+
out_mag = tl.maximum(out_mag - beta, 0.0)
|
|
1201
|
+
nu_tile = (sign_prod[None, :] * sgn) * out_mag
|
|
1202
|
+
# Write updated S (nu) back to nuT
|
|
1203
|
+
if dtype_enum == 0: # fp16
|
|
1204
|
+
nu_new = nu_tile.to(tl.float16)
|
|
1205
|
+
elif dtype_enum == 2: # fp64
|
|
1206
|
+
nu_new = nu_tile.to(tl.float64)
|
|
1207
|
+
else: # fp32 (dtype_enum == 1)
|
|
1208
|
+
nu_new = nu_tile
|
|
1209
|
+
tl.store(nuT + (evec[:, None]) * B + bs[None, :], nu_new, mask=mask2d)
|
|
1210
|
+
# Delta-trick: update L in place for this block (conflict-free layers guarantee no races)
|
|
1211
|
+
if use_delta:
|
|
1212
|
+
delta = nu_new - nu_old
|
|
1213
|
+
new_L = L_tile + delta
|
|
1214
|
+
tl.store(L + (bs[None, :] * V) + v_idx[:, None], new_L, mask=mask2d)
|
|
1215
|
+
tile += BLOCK_SIZE
|
|
1216
|
+
|
|
1217
|
+
|
|
1218
|
+
# fmt: off
|
|
1219
|
+
@triton.jit
|
|
1220
|
+
def v2c_and_gamma_btile_kernel(
|
|
1221
|
+
nuT,
|
|
1222
|
+
muT, # [E,B]
|
|
1223
|
+
var_ptr,
|
|
1224
|
+
var_edges, # CSR over vars -> edge IDs
|
|
1225
|
+
lam,
|
|
1226
|
+
log_prior_ratios,
|
|
1227
|
+
gamma, # [B,V], [V], [B,V]
|
|
1228
|
+
# lam: Current mixed beliefs [B,V]
|
|
1229
|
+
# log_prior_ratios: Prior log-likelihood ratios [V] (matches Rust: log_prior_ratios)
|
|
1230
|
+
# gamma: Memory strengths [B,V] (matches Rust: memory_strengths)
|
|
1231
|
+
posterior_ratios,
|
|
1232
|
+
hard_dec, # [B,V], [B,V]
|
|
1233
|
+
# posterior_ratios: Posterior ratios/marginals [B,V] (matches Rust: posterior_ratios)
|
|
1234
|
+
active_idx, # [B_active] int32 - compact list of active batch indices
|
|
1235
|
+
active, # [B] uint8 - active lane mask (skip frozen lanes)
|
|
1236
|
+
B,
|
|
1237
|
+
B_active,
|
|
1238
|
+
V,
|
|
1239
|
+
E,
|
|
1240
|
+
dtype_enum: tl.constexpr, # 0=fp16, 1=fp32, 2=fp64 (compile-time constant)
|
|
1241
|
+
BLOCK_SIZE: tl.constexpr,
|
|
1242
|
+
BTILE: tl.constexpr,
|
|
1243
|
+
WRITE_HARD: tl.constexpr,
|
|
1244
|
+
block_vars, # [num_block_vars] int32 - variable indices for block (can be None pointer for flood-fill)
|
|
1245
|
+
block_var_ptr, # [num_blocks+1] int32 - prefix sum for block_vars (can be None pointer for flood-fill)
|
|
1246
|
+
block_idx: int, # Block index for block-layered (-1 = flood-fill, use all vars)
|
|
1247
|
+
PRIOR_BATCHED: tl.constexpr = False, # if True, log_prior_ratios is [B,V], else [V]
|
|
1248
|
+
):
|
|
1249
|
+
# fmt: on
|
|
1250
|
+
"""Variable-to-check + gamma mixing on [E,B] layout, vectorized across BTILE.
|
|
1251
|
+
|
|
1252
|
+
Rust-compatible gamma mixing: uses previous iteration's marginal (M_prev) in mixing,
|
|
1253
|
+
then computes current marginal M_curr = λ_mixed + sum(nu). This matches Rust's
|
|
1254
|
+
compute_variable_prior which uses previous posterior_ratios.
|
|
1255
|
+
|
|
1256
|
+
For each variable j and batch tile bs:
|
|
1257
|
+
1. Load M_prev (previous iteration's marginal)
|
|
1258
|
+
2. Compute λ_mixed = (1-γ)*λ₀ + γ*M_prev
|
|
1259
|
+
3. Compute sum_nu (incoming messages)
|
|
1260
|
+
4. Compute M_curr = λ_mixed + sum(nu)
|
|
1261
|
+
5. Write muT (outgoing messages)
|
|
1262
|
+
|
|
1263
|
+
For block-layered scheduling:
|
|
1264
|
+
- block_vars, block_var_ptr, block_idx: Process only variables in the specified block
|
|
1265
|
+
- If block_idx == -1, processes all variables (flood-fill mode)
|
|
1266
|
+
"""
|
|
1267
|
+
var_list_idx = tl.program_id(
|
|
1268
|
+
axis=0
|
|
1269
|
+
) # Index into block_vars list (or variable index in flood-fill)
|
|
1270
|
+
btile_id = tl.program_id(axis=1)
|
|
1271
|
+
|
|
1272
|
+
# Block-layered: get variable index from block_vars list
|
|
1273
|
+
if block_idx >= 0:
|
|
1274
|
+
# Block-layered mode: var_list_idx is index into block_vars for this block
|
|
1275
|
+
var_start = tl.load(block_var_ptr + block_idx)
|
|
1276
|
+
var_end = tl.load(block_var_ptr + block_idx + 1)
|
|
1277
|
+
if var_list_idx >= (var_end - var_start):
|
|
1278
|
+
return # Out of block variable range
|
|
1279
|
+
j = tl.load(block_vars + var_start + var_list_idx)
|
|
1280
|
+
else:
|
|
1281
|
+
# Flood-fill mode: var_list_idx is absolute variable index
|
|
1282
|
+
j = var_list_idx
|
|
1283
|
+
if j >= V:
|
|
1284
|
+
return
|
|
1285
|
+
b0 = btile_id * BTILE
|
|
1286
|
+
pos = b0 + tl.arange(0, BTILE)
|
|
1287
|
+
mpos = pos < B_active
|
|
1288
|
+
# Safety: Only load from active_idx if pos < B_active to prevent out-of-bounds access
|
|
1289
|
+
bs = tl.load(active_idx + pos, mask=mpos, other=0)
|
|
1290
|
+
# Skip frozen lanes via active mask (avoids compaction overhead)
|
|
1291
|
+
active_vals = tl.load(active + bs, mask=mpos, other=0)
|
|
1292
|
+
mb = mpos & (active_vals != 0)
|
|
1293
|
+
|
|
1294
|
+
row_start = tl.load(var_ptr + j)
|
|
1295
|
+
row_end = tl.load(var_ptr + j + 1)
|
|
1296
|
+
|
|
1297
|
+
# Load previous iteration's marginal (posterior_prev) for Rust-compatible gamma mixing
|
|
1298
|
+
# Rust uses: λ_mixed = (1-γ)*log_prior + γ*posterior_prev, then posterior = λ_mixed + sum(nu)
|
|
1299
|
+
posterior_bj_prev = tl.load(
|
|
1300
|
+
posterior_ratios + bs * V + j, mask=mb, other=0.0
|
|
1301
|
+
) # Previous iteration's marginal/posterior
|
|
1302
|
+
|
|
1303
|
+
# FUSED GAMMA MIXING (Rust-compatible): λ = (1-γ)*log_prior + γ*posterior_prev
|
|
1304
|
+
g_bj = tl.load(gamma + bs * V + j, mask=mb, other=0.0)
|
|
1305
|
+
if PRIOR_BATCHED:
|
|
1306
|
+
log_prior_j = tl.load(log_prior_ratios + bs * V + j, mask=mb, other=0.0)
|
|
1307
|
+
else:
|
|
1308
|
+
log_prior_j = tl.load(log_prior_ratios + j)
|
|
1309
|
+
lam_mixed = (1.0 - g_bj) * log_prior_j + g_bj * posterior_bj_prev
|
|
1310
|
+
|
|
1311
|
+
# sum incoming nu over edges for batch lanes (vectorized)
|
|
1312
|
+
# Use fp32 accumulators for fp16/fp32 storage, fp64 for fp64
|
|
1313
|
+
if dtype_enum == 2: # fp64
|
|
1314
|
+
sum_nu = tl.zeros((BTILE,), dtype=tl.float64)
|
|
1315
|
+
else: # fp16 or fp32
|
|
1316
|
+
sum_nu = tl.zeros((BTILE,), dtype=tl.float32)
|
|
1317
|
+
tile = row_start
|
|
1318
|
+
while tile < row_end:
|
|
1319
|
+
offs = tile + tl.arange(0, BLOCK_SIZE)
|
|
1320
|
+
mrow = offs < row_end
|
|
1321
|
+
evec = tl.load(var_edges + offs, mask=mrow, other=0) # [BLOCK_SIZE]
|
|
1322
|
+
mask2d = (mrow[:, None]) & (mb[None, :])
|
|
1323
|
+
nu_tile = tl.load(nuT + (evec[:, None]) * B + bs[None, :], mask=mask2d, other=0.0)
|
|
1324
|
+
sum_nu += tl.sum(tl.where(mask2d, nu_tile, 0.0), axis=0)
|
|
1325
|
+
tile += BLOCK_SIZE
|
|
1326
|
+
|
|
1327
|
+
# Compute current marginal: posterior = λ_mixed + sum(nu) (matches Rust posterior_ratios update)
|
|
1328
|
+
posterior_bj = lam_mixed + sum_nu
|
|
1329
|
+
# Always store posterior_ratios for next iteration's gamma mixing (required for Rust compatibility)
|
|
1330
|
+
tl.store(posterior_ratios + bs * V + j, posterior_bj, mask=mb)
|
|
1331
|
+
# Optionally update hard_dec if requested (to avoid bandwidth in hot path)
|
|
1332
|
+
if WRITE_HARD:
|
|
1333
|
+
# Match Rust convergence rule Bit::from(posterior <= 0)
|
|
1334
|
+
tl.store(hard_dec + bs * V + j, (posterior_bj <= 0.0).to(tl.uint8), mask=mb)
|
|
1335
|
+
|
|
1336
|
+
# Store mixed lambda for next iteration (used in message computation)
|
|
1337
|
+
tl.store(lam + bs * V + j, lam_mixed, mask=mb)
|
|
1338
|
+
|
|
1339
|
+
# write muT for each edge lane (vectorized) using previous iteration beliefs
|
|
1340
|
+
tile = row_start
|
|
1341
|
+
while tile < row_end:
|
|
1342
|
+
offs = tile + tl.arange(0, BLOCK_SIZE)
|
|
1343
|
+
mrow = offs < row_end
|
|
1344
|
+
evec = tl.load(var_edges + offs, mask=mrow, other=0)
|
|
1345
|
+
mask2d = (mrow[:, None]) & (mb[None, :])
|
|
1346
|
+
nu_tile = tl.load(nuT + (evec[:, None]) * B + bs[None, :], mask=mask2d, other=0.0)
|
|
1347
|
+
# Use lam_mixed (current iteration's mixed belief) to compute mu, matching Rust schedule
|
|
1348
|
+
mu_tile = lam_mixed[None, :] + (sum_nu[None, :] - nu_tile)
|
|
1349
|
+
tl.store(muT + (evec[:, None]) * B + bs[None, :], mu_tile, mask=mask2d)
|
|
1350
|
+
tile += BLOCK_SIZE
|
|
1351
|
+
|
|
1352
|
+
|
|
1353
|
+
# fmt: off
|
|
1354
|
+
@triton.jit
|
|
1355
|
+
def post_from_L_kernel(
|
|
1356
|
+
L,
|
|
1357
|
+
posterior_ratios,
|
|
1358
|
+
hard_dec, # [B,V], [B,V], [B,V]
|
|
1359
|
+
active_idx, # [B_active]
|
|
1360
|
+
B,
|
|
1361
|
+
B_active,
|
|
1362
|
+
V,
|
|
1363
|
+
dtype_enum: tl.constexpr,
|
|
1364
|
+
BLOCK_V: tl.constexpr,
|
|
1365
|
+
):
|
|
1366
|
+
# fmt: on
|
|
1367
|
+
"""Update posterior_ratios and hard_dec from L for active lanes only.
|
|
1368
|
+
|
|
1369
|
+
This is used by layered delta-trick mode where L already contains the
|
|
1370
|
+
current per-variable marginals.
|
|
1371
|
+
"""
|
|
1372
|
+
j = tl.program_id(axis=0) # variable index
|
|
1373
|
+
btile_id = tl.program_id(axis=1)
|
|
1374
|
+
if j >= V:
|
|
1375
|
+
return
|
|
1376
|
+
b0 = btile_id * BLOCK_V
|
|
1377
|
+
pos = b0 + tl.arange(0, BLOCK_V)
|
|
1378
|
+
mpos = pos < B_active
|
|
1379
|
+
bs = tl.load(active_idx + pos, mask=mpos, other=0)
|
|
1380
|
+
|
|
1381
|
+
# Load L and write posterior + hard_dec
|
|
1382
|
+
if dtype_enum == 2: # fp64
|
|
1383
|
+
L_tile = tl.load(L + bs * V + j, mask=mpos, other=0.0).to(tl.float64)
|
|
1384
|
+
else:
|
|
1385
|
+
# For fp16/fp32 storage, do the compare in fp32
|
|
1386
|
+
L_tile = tl.load(L + bs * V + j, mask=mpos, other=0.0).to(tl.float32)
|
|
1387
|
+
tl.store(posterior_ratios + bs * V + j, L_tile, mask=mpos)
|
|
1388
|
+
tl.store(hard_dec + bs * V + j, (L_tile <= 0.0).to(tl.uint8), mask=mpos)
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
@triton.jit
|
|
1392
|
+
def permute_rows_kernel(
|
|
1393
|
+
srcEB,
|
|
1394
|
+
dstEB, # [E, B]
|
|
1395
|
+
perm, # [E] int32; maps src row e -> dst row p
|
|
1396
|
+
E: tl.constexpr,
|
|
1397
|
+
B: tl.constexpr,
|
|
1398
|
+
BTILE: tl.constexpr,
|
|
1399
|
+
):
|
|
1400
|
+
"""Permute edge rows from source layout to destination layout.
|
|
1401
|
+
|
|
1402
|
+
This kernel copies edge rows (all B batch lanes) using a permutation vector,
|
|
1403
|
+
enabling efficient layout switching between check-major and variable-major
|
|
1404
|
+
orderings for optimized memory access patterns.
|
|
1405
|
+
|
|
1406
|
+
Args:
|
|
1407
|
+
srcEB: [E, B] source message tensor
|
|
1408
|
+
dstEB: [E, B] destination message tensor
|
|
1409
|
+
perm: [E] permutation vector mapping source row e -> destination row p
|
|
1410
|
+
E: number of edges
|
|
1411
|
+
B: batch size
|
|
1412
|
+
BTILE: vectorization tile size for batch dimension
|
|
1413
|
+
"""
|
|
1414
|
+
e = tl.program_id(0)
|
|
1415
|
+
if e >= E:
|
|
1416
|
+
return
|
|
1417
|
+
|
|
1418
|
+
# destination row for this source row
|
|
1419
|
+
p = tl.load(perm + e)
|
|
1420
|
+
|
|
1421
|
+
# copy B entries in BTILE chunks for coalesced IO
|
|
1422
|
+
b0 = 0
|
|
1423
|
+
while b0 < B:
|
|
1424
|
+
bs = b0 + tl.arange(0, BTILE)
|
|
1425
|
+
m = bs < B
|
|
1426
|
+
vals = tl.load(srcEB + e * B + bs, mask=m, other=0)
|
|
1427
|
+
tl.store(dstEB + p * B + bs, vals, mask=m)
|
|
1428
|
+
b0 += BTILE
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
@triton.jit
|
|
1432
|
+
def count_active_kernel(
|
|
1433
|
+
active, # [B] uint8 mask (1=active, 0=frozen)
|
|
1434
|
+
out_count, # [1] int32 (out) - total count of active lanes
|
|
1435
|
+
B: tl.constexpr,
|
|
1436
|
+
BLOCK_SIZE: tl.constexpr,
|
|
1437
|
+
):
|
|
1438
|
+
"""Count active lanes on device (replaces torch.count_nonzero().item()).
|
|
1439
|
+
|
|
1440
|
+
This kernel performs a device-side reduction to count the number of active
|
|
1441
|
+
decoding lanes, avoiding host-device synchronization.
|
|
1442
|
+
|
|
1443
|
+
Args:
|
|
1444
|
+
active: [B] active lane mask
|
|
1445
|
+
out_count: [1] output count (device tensor)
|
|
1446
|
+
B: batch size
|
|
1447
|
+
BLOCK_SIZE: vectorization block size
|
|
1448
|
+
"""
|
|
1449
|
+
pid = tl.program_id(axis=0)
|
|
1450
|
+
if pid != 0:
|
|
1451
|
+
return
|
|
1452
|
+
|
|
1453
|
+
count = tl.zeros((), dtype=tl.int32)
|
|
1454
|
+
tile = 0
|
|
1455
|
+
while tile < B:
|
|
1456
|
+
offs = tile + tl.arange(0, BLOCK_SIZE)
|
|
1457
|
+
m = offs < B
|
|
1458
|
+
act = tl.load(active + offs, mask=m, other=0).to(tl.int32)
|
|
1459
|
+
count += tl.sum(tl.where(m, act, 0), axis=0)
|
|
1460
|
+
tile += BLOCK_SIZE
|
|
1461
|
+
|
|
1462
|
+
tl.store(out_count, count)
|
|
1463
|
+
|
|
1464
|
+
|
|
1465
|
+
@triton.jit
|
|
1466
|
+
def count_active_blocks_kernel(
|
|
1467
|
+
active, # [B] uint8 mask (1=active, 0=frozen)
|
|
1468
|
+
block_counts, # [num_blocks] int32 (out) - count of active per block
|
|
1469
|
+
B: tl.constexpr,
|
|
1470
|
+
BLOCK_SIZE: tl.constexpr,
|
|
1471
|
+
num_blocks: tl.constexpr,
|
|
1472
|
+
):
|
|
1473
|
+
"""Count active lanes per block (for prefix sum compaction).
|
|
1474
|
+
|
|
1475
|
+
Each program counts active elements in its block and writes to block_counts.
|
|
1476
|
+
This is used as the first phase of compaction.
|
|
1477
|
+
|
|
1478
|
+
Args:
|
|
1479
|
+
active: [B] active lane mask
|
|
1480
|
+
block_counts: [num_blocks] output counts per block
|
|
1481
|
+
B: batch size
|
|
1482
|
+
BLOCK_SIZE: vectorization block size
|
|
1483
|
+
num_blocks: number of blocks (should match grid size)
|
|
1484
|
+
"""
|
|
1485
|
+
pid = tl.program_id(axis=0)
|
|
1486
|
+
# Note: pid should always be < num_blocks if grid size is set correctly
|
|
1487
|
+
base = pid * BLOCK_SIZE
|
|
1488
|
+
|
|
1489
|
+
offs = base + tl.arange(0, BLOCK_SIZE)
|
|
1490
|
+
m = offs < B
|
|
1491
|
+
act = tl.load(active + offs, mask=m, other=0)
|
|
1492
|
+
active_mask = (act != 0) & m
|
|
1493
|
+
|
|
1494
|
+
block_count = tl.sum(active_mask.to(tl.int32), axis=0)
|
|
1495
|
+
tl.store(block_counts + pid, block_count)
|
|
1496
|
+
|
|
1497
|
+
|
|
1498
|
+
@triton.jit
|
|
1499
|
+
def compact_active_indices_kernel(
|
|
1500
|
+
active, # [B] uint8 mask (1=active, 0=frozen)
|
|
1501
|
+
active_idx, # [B] int32 (out) - compact list of active batch indices
|
|
1502
|
+
prefix_sum, # [num_blocks+1] int32 (in) - prefix sum of block counts (from torch.cumsum)
|
|
1503
|
+
B: tl.constexpr,
|
|
1504
|
+
BLOCK_SIZE: tl.constexpr,
|
|
1505
|
+
num_blocks: tl.constexpr,
|
|
1506
|
+
):
|
|
1507
|
+
"""Compact active indices using prefix sum offsets.
|
|
1508
|
+
|
|
1509
|
+
This kernel compacts active batch indices into a contiguous array.
|
|
1510
|
+
Requires prefix_sum array computed via torch.cumsum(block_counts) on device.
|
|
1511
|
+
|
|
1512
|
+
Args:
|
|
1513
|
+
active: [B] active lane mask
|
|
1514
|
+
active_idx: [B] output array for compacted indices (pre-allocated)
|
|
1515
|
+
prefix_sum: [num_blocks+1] prefix sum (prefix_sum[i] = sum of counts for blocks 0..i-1)
|
|
1516
|
+
B: batch size
|
|
1517
|
+
BLOCK_SIZE: vectorization block size
|
|
1518
|
+
num_blocks: number of blocks (should match grid size)
|
|
1519
|
+
"""
|
|
1520
|
+
pid = tl.program_id(axis=0)
|
|
1521
|
+
# Note: pid should always be < num_blocks if grid size is set correctly
|
|
1522
|
+
base = pid * BLOCK_SIZE
|
|
1523
|
+
|
|
1524
|
+
# Get write offset for this block from prefix sum
|
|
1525
|
+
# prefix_sum has size num_blocks+1, so prefix_sum[pid] is valid for pid in [0, num_blocks-1]
|
|
1526
|
+
write_offset = tl.load(prefix_sum + pid)
|
|
1527
|
+
|
|
1528
|
+
# Process this block
|
|
1529
|
+
offs = base + tl.arange(0, BLOCK_SIZE)
|
|
1530
|
+
m = offs < B
|
|
1531
|
+
act = tl.load(active + offs, mask=m, other=0)
|
|
1532
|
+
|
|
1533
|
+
# Write active indices sequentially starting at write_offset
|
|
1534
|
+
local_pos = 0
|
|
1535
|
+
for i in tl.static_range(0, BLOCK_SIZE):
|
|
1536
|
+
idx = base + i
|
|
1537
|
+
if idx < B:
|
|
1538
|
+
# Check if this index is active by loading the value directly
|
|
1539
|
+
is_active = tl.load(active + idx) != 0
|
|
1540
|
+
if is_active:
|
|
1541
|
+
write_pos = write_offset + local_pos
|
|
1542
|
+
if write_pos < B: # Safety check
|
|
1543
|
+
tl.store(active_idx + write_pos, idx)
|
|
1544
|
+
local_pos += 1
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
@triton.jit
|
|
1548
|
+
def compare_count_changed_kernel(
|
|
1549
|
+
new_count, # [1] int32 - new active count
|
|
1550
|
+
old_count, # [1] int32 - old active count
|
|
1551
|
+
changed_flag, # [1] uint8 (out) - 1 if changed, 0 otherwise
|
|
1552
|
+
):
|
|
1553
|
+
"""Device-side comparison to check if active count changed (avoids host sync).
|
|
1554
|
+
|
|
1555
|
+
This kernel compares two device tensors and writes a flag, allowing
|
|
1556
|
+
device-side control flow without host-device synchronization.
|
|
1557
|
+
"""
|
|
1558
|
+
new = tl.load(new_count)
|
|
1559
|
+
old = tl.load(old_count)
|
|
1560
|
+
changed = (new != old).to(tl.uint8)
|
|
1561
|
+
tl.store(changed_flag, changed)
|
|
1562
|
+
|
|
1563
|
+
|
|
1564
|
+
# ============================================================================
|
|
1565
|
+
# Data type Enum Helper
|
|
1566
|
+
# ============================================================================
|
|
1567
|
+
# All kernels now use dtype_enum (0=fp16, 1=fp32, 2=fp64) instead of msg_is_fp16.
|
|
1568
|
+
# This is a compile-time constant that enables Triton to optimize dtype-specific
|
|
1569
|
+
# code paths while maintaining a single kernel implementation.
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
def dtype_str_to_enum(data_type_str: str) -> int:
|
|
1573
|
+
"""Convert data_type string to enum value for kernel calls.
|
|
1574
|
+
|
|
1575
|
+
Args:
|
|
1576
|
+
data_type_str: "float16", "float32", or "float64"
|
|
1577
|
+
|
|
1578
|
+
Returns:
|
|
1579
|
+
Integer enum: 0 for float16, 1 for float32, 2 for float64
|
|
1580
|
+
|
|
1581
|
+
Raises:
|
|
1582
|
+
ValueError: If data_type_str is not one of the supported values
|
|
1583
|
+
"""
|
|
1584
|
+
return {"float16": 0, "float32": 1, "float64": 2}[data_type_str]
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
# ============================================================================
|
|
1588
|
+
# C2V Kernel with Batch-Organized Grid-Stride Pattern
|
|
1589
|
+
# ============================================================================
|
|
1590
|
+
|
|
1591
|
+
|
|
1592
|
+
# fmt: off
|
|
1593
|
+
@triton.jit
|
|
1594
|
+
def c2v_min_sum_grid_stride_kernel(
|
|
1595
|
+
# Message tensors
|
|
1596
|
+
mu,
|
|
1597
|
+
nu, # [B,E]
|
|
1598
|
+
# CSR graph data
|
|
1599
|
+
chk_ptr,
|
|
1600
|
+
chk_edges, # CSR over checks -> edge IDs
|
|
1601
|
+
var_ptr,
|
|
1602
|
+
var_edges, # CSR over vars -> edge IDs
|
|
1603
|
+
edge_var, # [E] var index per edge
|
|
1604
|
+
# Beliefs and decisions
|
|
1605
|
+
lam,
|
|
1606
|
+
posterior_ratios,
|
|
1607
|
+
hard_dec, # [B,V], [B,V], [B,V]
|
|
1608
|
+
# Inputs
|
|
1609
|
+
syndrome, # [B,C]
|
|
1610
|
+
log_prior_ratios,
|
|
1611
|
+
gamma, # [1,V], [B,V]
|
|
1612
|
+
active, # [B] uint8 mask
|
|
1613
|
+
# Solution tracking
|
|
1614
|
+
check_ok, # [B,C] uint8 (temporary)
|
|
1615
|
+
best_weights,
|
|
1616
|
+
best_errors, # [B] dtype, [B,V] uint8
|
|
1617
|
+
valid_solutions,
|
|
1618
|
+
found_count, # [B] uint8, [B] int32
|
|
1619
|
+
wj, # [V] dtype (log prior ratios for weight)
|
|
1620
|
+
# Parameters
|
|
1621
|
+
B,
|
|
1622
|
+
C,
|
|
1623
|
+
V,
|
|
1624
|
+
E,
|
|
1625
|
+
iteration: int, # Current iteration number (for alpha schedule)
|
|
1626
|
+
alpha_base, # Base alpha value (or 0 for schedule)
|
|
1627
|
+
alpha_scaling: float, # Alpha scaling factor (for schedule)
|
|
1628
|
+
use_alpha: tl.constexpr,
|
|
1629
|
+
use_beta: tl.constexpr,
|
|
1630
|
+
beta: float,
|
|
1631
|
+
in_leg: tl.constexpr,
|
|
1632
|
+
dtype_enum: tl.constexpr,
|
|
1633
|
+
BLOCK_SIZE: tl.constexpr,
|
|
1634
|
+
ROWS_PER_CHK: tl.constexpr,
|
|
1635
|
+
ROWS_PER_VAR: tl.constexpr,
|
|
1636
|
+
):
|
|
1637
|
+
# fmt: on
|
|
1638
|
+
"""C2V (check-to-variable) kernel with batch-organized grid-stride parallelism.
|
|
1639
|
+
|
|
1640
|
+
This kernel only performs the C2V phase. V2C, parity, and selection phases run
|
|
1641
|
+
in separate kernel launches after this kernel completes, providing implicit barriers
|
|
1642
|
+
(kernel launch completion) between phases:
|
|
1643
|
+
- C2V completes -> V2C starts (V2C reads nu written by C2V)
|
|
1644
|
+
- V2C completes -> parity starts (parity reads hard_dec written by V2C)
|
|
1645
|
+
- parity completes -> selection starts (selection reads check_ok written by parity)
|
|
1646
|
+
|
|
1647
|
+
Key differences from c2v_min_sum_kernel:
|
|
1648
|
+
1. Grid-stride pattern: Batch-organized (processes consecutive checks per batch lane)
|
|
1649
|
+
vs flat pattern (processes all (batch,check) pairs in flat order)
|
|
1650
|
+
2. Alpha schedule: Computes alpha per iteration internally vs pre-computed alpha passed in
|
|
1651
|
+
3. Better cache locality: When ROWS_PER_CHK > 1, consecutive checks per lane improve cache hits
|
|
1652
|
+
|
|
1653
|
+
|
|
1654
|
+
|
|
1655
|
+
"""
|
|
1656
|
+
pid = tl.program_id(axis=0)
|
|
1657
|
+
grid_size = tl.num_programs(axis=0)
|
|
1658
|
+
|
|
1659
|
+
# Compute work dimensions (grid-stride pattern)
|
|
1660
|
+
# Each program processes ROWS_PER_CHK consecutive checks/vars
|
|
1661
|
+
work_dim = max(C, V)
|
|
1662
|
+
# Calculate which batch lane and starting check/var this program handles
|
|
1663
|
+
# Programs are organized as: program handles (batch_lane, starting_check/var)
|
|
1664
|
+
# Total work items per batch = (work_dim + ROWS_PER_CHK - 1) // ROWS_PER_CHK
|
|
1665
|
+
work_items_per_batch = (work_dim + ROWS_PER_CHK - 1) // ROWS_PER_CHK
|
|
1666
|
+
work_item_idx = pid % work_items_per_batch
|
|
1667
|
+
b = pid // work_items_per_batch
|
|
1668
|
+
|
|
1669
|
+
if b >= B:
|
|
1670
|
+
return
|
|
1671
|
+
|
|
1672
|
+
# Check if lane is active
|
|
1673
|
+
active_b = tl.load(active + b)
|
|
1674
|
+
if active_b == 0:
|
|
1675
|
+
return
|
|
1676
|
+
|
|
1677
|
+
# Starting check/var index for this program
|
|
1678
|
+
start_check_var = work_item_idx * ROWS_PER_CHK
|
|
1679
|
+
|
|
1680
|
+
# Compute alpha for this iteration (once per program, not per check/var)
|
|
1681
|
+
if use_alpha:
|
|
1682
|
+
if alpha_base == 0.0:
|
|
1683
|
+
exp_arg = -((iteration + 1) / alpha_scaling) * 0.6931471805599453
|
|
1684
|
+
alpha_iter = 1.0 - tl.exp(exp_arg)
|
|
1685
|
+
elif alpha_base < 0.0:
|
|
1686
|
+
alpha_iter = 1.0
|
|
1687
|
+
else:
|
|
1688
|
+
alpha_iter = alpha_base
|
|
1689
|
+
else:
|
|
1690
|
+
alpha_iter = 1.0
|
|
1691
|
+
|
|
1692
|
+
# PHASE 1: C2V (Check-to-Variable) - Process ROWS_PER_CHK consecutive checks
|
|
1693
|
+
r = 0
|
|
1694
|
+
while r < ROWS_PER_CHK:
|
|
1695
|
+
i = start_check_var + r # Process consecutive checks
|
|
1696
|
+
if i < C:
|
|
1697
|
+
row_start = tl.load(chk_ptr + i)
|
|
1698
|
+
row_end = tl.load(chk_ptr + i + 1)
|
|
1699
|
+
deg = row_end - row_start
|
|
1700
|
+
|
|
1701
|
+
if deg > 0:
|
|
1702
|
+
deg_is_one = deg == 1
|
|
1703
|
+
neg_parity = tl.zeros((), dtype=tl.int32)
|
|
1704
|
+
|
|
1705
|
+
if dtype_enum == 0: # fp16
|
|
1706
|
+
min1 = tl.full((), 1e30, dtype=tl.float16)
|
|
1707
|
+
min2 = tl.full((), 1e30, dtype=tl.float16)
|
|
1708
|
+
elif dtype_enum == 2: # fp64
|
|
1709
|
+
min1 = tl.full((), 1e30, dtype=tl.float64)
|
|
1710
|
+
min2 = tl.full((), 1e30, dtype=tl.float64)
|
|
1711
|
+
else: # fp32
|
|
1712
|
+
min1 = tl.full((), 1e30, dtype=tl.float32)
|
|
1713
|
+
min2 = tl.full((), 1e30, dtype=tl.float32)
|
|
1714
|
+
cnt1 = tl.zeros((), dtype=tl.int32)
|
|
1715
|
+
argmin_e = tl.full((), -1, dtype=tl.int32)
|
|
1716
|
+
|
|
1717
|
+
tile = row_start
|
|
1718
|
+
while tile < row_end:
|
|
1719
|
+
offs = tile + tl.arange(0, BLOCK_SIZE)
|
|
1720
|
+
m = offs < row_end
|
|
1721
|
+
e = tl.load(chk_edges + offs, mask=m, other=0)
|
|
1722
|
+
mu_e = tl.load(mu + b * E + e, mask=m, other=0.0)
|
|
1723
|
+
|
|
1724
|
+
tile_par = tl.sum((mu_e < 0.0) & m, axis=0).to(tl.int32) & 1
|
|
1725
|
+
neg_parity = neg_parity ^ tile_par
|
|
1726
|
+
|
|
1727
|
+
a = tl.abs(mu_e)
|
|
1728
|
+
a_mask = tl.where(m, a, 1e30)
|
|
1729
|
+
|
|
1730
|
+
pos1 = tl.argmax(-a_mask, axis=0)
|
|
1731
|
+
min1_t = tl.max(-a_mask, axis=0) * (-1.0)
|
|
1732
|
+
e1_t = tl.load(chk_edges + tile + pos1)
|
|
1733
|
+
cnt1_t = tl.sum(a_mask == min1_t, axis=0).to(tl.int32)
|
|
1734
|
+
a_mask2 = tl.where(tl.arange(0, BLOCK_SIZE) == pos1, 1e30, a_mask)
|
|
1735
|
+
min2_t = tl.min(a_mask2, axis=0)
|
|
1736
|
+
|
|
1737
|
+
better1 = min1_t < min1
|
|
1738
|
+
argmin_e = tl.where(better1, e1_t, argmin_e)
|
|
1739
|
+
cnt1 = tl.where(better1, cnt1_t, tl.where(min1_t == min1, cnt1 + cnt1_t, cnt1))
|
|
1740
|
+
min2 = tl.where(
|
|
1741
|
+
better1,
|
|
1742
|
+
tl.minimum(min1, min2_t),
|
|
1743
|
+
tl.where(
|
|
1744
|
+
min1_t == min1, tl.minimum(min2, min2_t), tl.minimum(min2, min1_t)
|
|
1745
|
+
),
|
|
1746
|
+
)
|
|
1747
|
+
min1 = tl.where(better1, min1_t, min1)
|
|
1748
|
+
|
|
1749
|
+
tile += BLOCK_SIZE
|
|
1750
|
+
|
|
1751
|
+
syn = tl.load(syndrome + b * C + i).to(tl.int32) & 1
|
|
1752
|
+
par_bit = (neg_parity & 1) ^ syn
|
|
1753
|
+
sign_prod = tl.where(par_bit == 0, 1.0, -1.0)
|
|
1754
|
+
|
|
1755
|
+
# PASS 2: Write outgoing messages
|
|
1756
|
+
tile = row_start
|
|
1757
|
+
while tile < row_end:
|
|
1758
|
+
offs = tile + tl.arange(0, BLOCK_SIZE)
|
|
1759
|
+
m = offs < row_end
|
|
1760
|
+
e = tl.load(chk_edges + offs, mask=m, other=0)
|
|
1761
|
+
mu_e = tl.load(mu + b * E + e, mask=m, other=0.0)
|
|
1762
|
+
sgn_mu = tl.where(mu_e >= 0.0, 1.0, -1.0)
|
|
1763
|
+
|
|
1764
|
+
is_argmin = e == argmin_e
|
|
1765
|
+
use_min2 = (cnt1 == 1) & is_argmin
|
|
1766
|
+
out_mag = tl.where(use_min2, min2, min1)
|
|
1767
|
+
out_mag = tl.where(deg_is_one, 0.0, out_mag)
|
|
1768
|
+
|
|
1769
|
+
if use_alpha:
|
|
1770
|
+
out_mag = alpha_iter * out_mag
|
|
1771
|
+
if use_beta:
|
|
1772
|
+
out_mag = tl.maximum(out_mag - beta, 0.0)
|
|
1773
|
+
|
|
1774
|
+
nu_e = (sign_prod * sgn_mu) * out_mag
|
|
1775
|
+
if dtype_enum == 0: # fp16
|
|
1776
|
+
tl.store(nu + b * E + e, nu_e.to(tl.float16), mask=m)
|
|
1777
|
+
elif dtype_enum == 2: # fp64
|
|
1778
|
+
tl.store(nu + b * E + e, nu_e.to(tl.float64), mask=m)
|
|
1779
|
+
else: # fp32
|
|
1780
|
+
tl.store(nu + b * E + e, nu_e, mask=m)
|
|
1781
|
+
|
|
1782
|
+
tile += BLOCK_SIZE
|
|
1783
|
+
|
|
1784
|
+
r += 1
|
|
1785
|
+
|
|
1786
|
+
# NOTE: V2C, parity, and selection phases removed from iteration-fused kernel to ensure
|
|
1787
|
+
# memory consistency. Different programs can execute in parallel on different SMs, so they
|
|
1788
|
+
# can be at different phases simultaneously, causing race conditions. Separate kernel launches
|
|
1789
|
+
# provide implicit barriers (kernel launch completion) between phases, ensuring correctness:
|
|
1790
|
+
# - C2V completes -> V2C starts (V2C reads nu written by C2V)
|
|
1791
|
+
# - V2C completes -> parity starts (parity reads hard_dec written by V2C)
|
|
1792
|
+
# - parity completes -> selection starts (selection reads check_ok written by parity)
|
|
1793
|
+
|
|
1794
|
+
|
|
1795
|
+
# ============================================================================
|
|
1796
|
+
# Persistent BP Kernel (Fully Fused)
|
|
1797
|
+
# ============================================================================
|
|
1798
|
+
|
|
1799
|
+
|
|
1800
|
+
# fmt: off
|
|
1801
|
+
@triton.jit
|
|
1802
|
+
def persistent_bp_iterations_kernel(
|
|
1803
|
+
# Message tensors
|
|
1804
|
+
mu,
|
|
1805
|
+
nu, # [B,E]
|
|
1806
|
+
# CSR graph data
|
|
1807
|
+
chk_ptr,
|
|
1808
|
+
chk_edges, # CSR over checks -> edge IDs
|
|
1809
|
+
var_ptr,
|
|
1810
|
+
var_edges, # CSR over vars -> edge IDs
|
|
1811
|
+
edge_var, # [E] var index per edge
|
|
1812
|
+
# Beliefs and decisions
|
|
1813
|
+
lam,
|
|
1814
|
+
posterior_ratios,
|
|
1815
|
+
hard_dec, # [B,V], [B,V], [B,V]
|
|
1816
|
+
# Inputs
|
|
1817
|
+
syndrome, # [B,C]
|
|
1818
|
+
log_prior_ratios,
|
|
1819
|
+
gamma, # [1,V], [B,V]
|
|
1820
|
+
active, # [B] uint8 mask
|
|
1821
|
+
# Solution tracking
|
|
1822
|
+
check_ok, # [B,C] uint8 (temporary, can reuse)
|
|
1823
|
+
best_weights,
|
|
1824
|
+
best_errors, # [B] dtype, [B,V] uint8
|
|
1825
|
+
valid_solutions,
|
|
1826
|
+
found_count, # [B] uint8, [B] int32
|
|
1827
|
+
first_iter,
|
|
1828
|
+
iter_counter, # [B] int32, [B] int32
|
|
1829
|
+
sought_sols, # [B] int32
|
|
1830
|
+
wj, # [V] dtype (log prior ratios for weight)
|
|
1831
|
+
# Parameters
|
|
1832
|
+
B,
|
|
1833
|
+
C,
|
|
1834
|
+
V,
|
|
1835
|
+
E,
|
|
1836
|
+
max_iter: int,
|
|
1837
|
+
start_iter: int,
|
|
1838
|
+
stop_nconv: int,
|
|
1839
|
+
alpha_base, # Base alpha value (or None/0 for schedule)
|
|
1840
|
+
alpha_scaling: float, # Alpha scaling factor (for schedule)
|
|
1841
|
+
use_alpha: tl.constexpr,
|
|
1842
|
+
use_beta: tl.constexpr,
|
|
1843
|
+
beta: float,
|
|
1844
|
+
in_leg: tl.constexpr,
|
|
1845
|
+
force_hard: tl.constexpr,
|
|
1846
|
+
dtype_enum: tl.constexpr,
|
|
1847
|
+
BLOCK_SIZE: tl.constexpr,
|
|
1848
|
+
):
|
|
1849
|
+
# fmt: on
|
|
1850
|
+
"""Fully fused persistent BP kernel that runs multiple iterations for one batch lane.
|
|
1851
|
+
|
|
1852
|
+
This kernel processes one batch lane (b) and runs all iterations for that lane
|
|
1853
|
+
inside a single kernel launch. It fuses C2V, V2C, parity, select, and freeze
|
|
1854
|
+
operations to eliminate kernel launch overhead and host-device syncs.
|
|
1855
|
+
|
|
1856
|
+
Each program_id handles one batch lane:
|
|
1857
|
+
- program_id(0) = batch lane 0
|
|
1858
|
+
- program_id(1) = batch lane 1
|
|
1859
|
+
- etc.
|
|
1860
|
+
|
|
1861
|
+
Args:
|
|
1862
|
+
mu, nu: [B,E] message tensors
|
|
1863
|
+
chk_ptr, chk_edges: CSR check constraints
|
|
1864
|
+
var_ptr, var_edges: CSR variable constraints
|
|
1865
|
+
edge_var: [E] variable index per edge
|
|
1866
|
+
lam, posterior_ratios, hard_dec: [B,V] beliefs, marginals, decisions
|
|
1867
|
+
syndrome: [B,C] syndrome bits
|
|
1868
|
+
log_prior_ratios, gamma: [1,V], [B,V] priors and memory strengths
|
|
1869
|
+
active: [B] active lane mask
|
|
1870
|
+
check_ok: [B,C] temporary parity check results
|
|
1871
|
+
best_weights, best_errors: [B] dtype, [B,V] uint8 best solutions
|
|
1872
|
+
valid_solutions, found_count: [B] uint8, [B] int32 convergence tracking
|
|
1873
|
+
first_iter, iter_counter: [B] int32 iteration tracking
|
|
1874
|
+
sought_sols: [B] int32 sought solutions per lane
|
|
1875
|
+
wj: [V] dtype log prior ratios for weight computation
|
|
1876
|
+
B, C, V, E: batch size, checks, variables, edges
|
|
1877
|
+
max_iter: Maximum iterations to run
|
|
1878
|
+
start_iter: Starting iteration index (for alpha schedule)
|
|
1879
|
+
stop_nconv: Stop after N converged solutions
|
|
1880
|
+
alpha_base: Base alpha value (None/0 for schedule)
|
|
1881
|
+
alpha_scaling: Alpha scaling factor
|
|
1882
|
+
use_alpha, use_beta: Flags for min-sum parameters
|
|
1883
|
+
beta: Offset min-sum beta
|
|
1884
|
+
in_leg: Whether in relay leg
|
|
1885
|
+
force_hard: Whether to force hard_dec parity check
|
|
1886
|
+
dtype_enum: Compile-time dtype selector
|
|
1887
|
+
BLOCK_SIZE: Vectorization block size
|
|
1888
|
+
"""
|
|
1889
|
+
b = tl.program_id(axis=0)
|
|
1890
|
+
if b >= B:
|
|
1891
|
+
return
|
|
1892
|
+
|
|
1893
|
+
# Check if lane is active
|
|
1894
|
+
active_b = tl.load(active + b)
|
|
1895
|
+
if active_b == 0:
|
|
1896
|
+
return # Lane is frozen, skip all iterations
|
|
1897
|
+
|
|
1898
|
+
# Convergence tracking
|
|
1899
|
+
converged = False
|
|
1900
|
+
converged_iter = -1
|
|
1901
|
+
|
|
1902
|
+
# Run iterations in a loop
|
|
1903
|
+
t_offset = 0
|
|
1904
|
+
while t_offset < max_iter:
|
|
1905
|
+
t = start_iter + t_offset
|
|
1906
|
+
|
|
1907
|
+
# Skip work if already converged (but still need to update iter_counter)
|
|
1908
|
+
if not converged:
|
|
1909
|
+
# Compute alpha for this iteration (matches decoder logic)
|
|
1910
|
+
if use_alpha:
|
|
1911
|
+
if alpha_base == 0.0:
|
|
1912
|
+
# Alpha schedule: 1.0 - 2^(-(t+1)/s)
|
|
1913
|
+
# Use exp(-x * ln(2)) instead of 2^(-x) since Triton doesn't support **
|
|
1914
|
+
exp_arg = -((t + 1) / alpha_scaling) * 0.6931471805599453 # ln(2) ≈ 0.693
|
|
1915
|
+
alpha_iter = 1.0 - tl.exp(exp_arg)
|
|
1916
|
+
elif alpha_base < 0.0:
|
|
1917
|
+
alpha_iter = 1.0
|
|
1918
|
+
else:
|
|
1919
|
+
alpha_iter = alpha_base
|
|
1920
|
+
else:
|
|
1921
|
+
alpha_iter = 1.0
|
|
1922
|
+
|
|
1923
|
+
# ====================================================================
|
|
1924
|
+
# PHASE 1: C2V (Check-to-Variable) - Process all checks for lane b
|
|
1925
|
+
# ====================================================================
|
|
1926
|
+
i = 0
|
|
1927
|
+
while i < C:
|
|
1928
|
+
row_start = tl.load(chk_ptr + i)
|
|
1929
|
+
row_end = tl.load(chk_ptr + i + 1)
|
|
1930
|
+
deg = row_end - row_start
|
|
1931
|
+
|
|
1932
|
+
if deg > 0:
|
|
1933
|
+
deg_is_one = deg == 1
|
|
1934
|
+
|
|
1935
|
+
# PASS 1: Compute parity and find min1/min2
|
|
1936
|
+
neg_parity = tl.zeros((), dtype=tl.int32)
|
|
1937
|
+
if dtype_enum == 0: # fp16
|
|
1938
|
+
min1 = tl.full((), 1e30, dtype=tl.float16)
|
|
1939
|
+
min2 = tl.full((), 1e30, dtype=tl.float16)
|
|
1940
|
+
elif dtype_enum == 2: # fp64
|
|
1941
|
+
min1 = tl.full((), 1e30, dtype=tl.float64)
|
|
1942
|
+
min2 = tl.full((), 1e30, dtype=tl.float64)
|
|
1943
|
+
else: # fp32
|
|
1944
|
+
min1 = tl.full((), 1e30, dtype=tl.float32)
|
|
1945
|
+
min2 = tl.full((), 1e30, dtype=tl.float32)
|
|
1946
|
+
cnt1 = tl.zeros((), dtype=tl.int32)
|
|
1947
|
+
argmin_e = tl.full((), -1, dtype=tl.int32)
|
|
1948
|
+
|
|
1949
|
+
tile = row_start
|
|
1950
|
+
while tile < row_end:
|
|
1951
|
+
offs = tile + tl.arange(0, BLOCK_SIZE)
|
|
1952
|
+
m = offs < row_end
|
|
1953
|
+
e = tl.load(chk_edges + offs, mask=m, other=0)
|
|
1954
|
+
mu_e = tl.load(mu + b * E + e, mask=m, other=0.0)
|
|
1955
|
+
|
|
1956
|
+
# Parity
|
|
1957
|
+
tile_par = tl.sum((mu_e < 0.0) & m, axis=0).to(tl.int32) & 1
|
|
1958
|
+
neg_parity = neg_parity ^ tile_par
|
|
1959
|
+
|
|
1960
|
+
# Min1/min2
|
|
1961
|
+
a = tl.abs(mu_e)
|
|
1962
|
+
a_mask = tl.where(m, a, 1e30)
|
|
1963
|
+
pos1 = tl.argmax(-a_mask, axis=0)
|
|
1964
|
+
min1_t = tl.max(-a_mask, axis=0) * (-1.0)
|
|
1965
|
+
e1_t = tl.load(chk_edges + tile + pos1)
|
|
1966
|
+
cnt1_t = tl.sum(a_mask == min1_t, axis=0).to(tl.int32)
|
|
1967
|
+
a_mask2 = tl.where(tl.arange(0, BLOCK_SIZE) == pos1, 1e30, a_mask)
|
|
1968
|
+
min2_t = tl.min(a_mask2, axis=0)
|
|
1969
|
+
|
|
1970
|
+
better1 = min1_t < min1
|
|
1971
|
+
argmin_e = tl.where(better1, e1_t, argmin_e)
|
|
1972
|
+
cnt1 = tl.where(
|
|
1973
|
+
better1, cnt1_t, tl.where(min1_t == min1, cnt1 + cnt1_t, cnt1)
|
|
1974
|
+
)
|
|
1975
|
+
min2 = tl.where(
|
|
1976
|
+
better1,
|
|
1977
|
+
tl.minimum(min1, min2_t),
|
|
1978
|
+
tl.where(
|
|
1979
|
+
min1_t == min1, tl.minimum(min2, min2_t), tl.minimum(min2, min1_t)
|
|
1980
|
+
),
|
|
1981
|
+
)
|
|
1982
|
+
min1 = tl.where(better1, min1_t, min1)
|
|
1983
|
+
|
|
1984
|
+
tile += BLOCK_SIZE
|
|
1985
|
+
|
|
1986
|
+
# Syndrome integration
|
|
1987
|
+
syn = tl.load(syndrome + b * C + i).to(tl.int32) & 1
|
|
1988
|
+
par_bit = (neg_parity & 1) ^ syn
|
|
1989
|
+
sign_prod = tl.where(par_bit == 0, 1.0, -1.0)
|
|
1990
|
+
|
|
1991
|
+
# PASS 2: Write outgoing messages
|
|
1992
|
+
tile = row_start
|
|
1993
|
+
while tile < row_end:
|
|
1994
|
+
offs = tile + tl.arange(0, BLOCK_SIZE)
|
|
1995
|
+
m = offs < row_end
|
|
1996
|
+
e = tl.load(chk_edges + offs, mask=m, other=0)
|
|
1997
|
+
mu_e = tl.load(mu + b * E + e, mask=m, other=0.0)
|
|
1998
|
+
sgn_mu = tl.where(mu_e >= 0.0, 1.0, -1.0)
|
|
1999
|
+
|
|
2000
|
+
is_argmin = e == argmin_e
|
|
2001
|
+
use_min2 = (cnt1 == 1) & is_argmin
|
|
2002
|
+
out_mag = tl.where(use_min2, min2, min1)
|
|
2003
|
+
out_mag = tl.where(deg_is_one, 0.0, out_mag)
|
|
2004
|
+
|
|
2005
|
+
if use_alpha:
|
|
2006
|
+
out_mag = alpha_iter * out_mag
|
|
2007
|
+
if use_beta:
|
|
2008
|
+
out_mag = tl.maximum(out_mag - beta, 0.0)
|
|
2009
|
+
|
|
2010
|
+
nu_e = (sign_prod * sgn_mu) * out_mag
|
|
2011
|
+
if dtype_enum == 0: # fp16
|
|
2012
|
+
tl.store(nu + b * E + e, nu_e.to(tl.float16), mask=m)
|
|
2013
|
+
elif dtype_enum == 2: # fp64
|
|
2014
|
+
tl.store(nu + b * E + e, nu_e.to(tl.float64), mask=m)
|
|
2015
|
+
else: # fp32
|
|
2016
|
+
tl.store(nu + b * E + e, nu_e, mask=m)
|
|
2017
|
+
|
|
2018
|
+
tile += BLOCK_SIZE
|
|
2019
|
+
|
|
2020
|
+
i += 1
|
|
2021
|
+
|
|
2022
|
+
# ====================================================================
|
|
2023
|
+
# PHASE 2: V2C (Variable-to-Check) with Gamma Mixing - Process all vars for lane b
|
|
2024
|
+
# ====================================================================
|
|
2025
|
+
j = 0
|
|
2026
|
+
while j < V:
|
|
2027
|
+
row_start = tl.load(var_ptr + j)
|
|
2028
|
+
row_end = tl.load(var_ptr + j + 1)
|
|
2029
|
+
|
|
2030
|
+
# Load previous posterior and compute gamma mixing
|
|
2031
|
+
posterior_prev = tl.load(posterior_ratios + b * V + j)
|
|
2032
|
+
g_bj = tl.load(gamma + b * V + j)
|
|
2033
|
+
log_prior_j = tl.load(log_prior_ratios + j)
|
|
2034
|
+
lam_mixed = (1.0 - g_bj) * log_prior_j + g_bj * posterior_prev
|
|
2035
|
+
|
|
2036
|
+
# PASS 1: Sum incoming messages
|
|
2037
|
+
if dtype_enum == 2: # fp64
|
|
2038
|
+
sum_nu = tl.zeros((), dtype=tl.float64)
|
|
2039
|
+
else:
|
|
2040
|
+
sum_nu = tl.zeros((), dtype=tl.float32)
|
|
2041
|
+
|
|
2042
|
+
tile = row_start
|
|
2043
|
+
while tile < row_end:
|
|
2044
|
+
offs = tile + tl.arange(0, BLOCK_SIZE)
|
|
2045
|
+
m = offs < row_end
|
|
2046
|
+
e = tl.load(var_edges + offs, mask=m, other=0)
|
|
2047
|
+
nu_e = tl.load(nu + b * E + e, mask=m, other=0.0)
|
|
2048
|
+
sum_nu += tl.sum(tl.where(m, nu_e, 0.0), axis=0)
|
|
2049
|
+
tile += BLOCK_SIZE
|
|
2050
|
+
|
|
2051
|
+
# Update posterior and hard_dec
|
|
2052
|
+
posterior_curr = lam_mixed + sum_nu
|
|
2053
|
+
tl.store(posterior_ratios + b * V + j, posterior_curr)
|
|
2054
|
+
tl.store(hard_dec + b * V + j, (posterior_curr <= 0.0).to(tl.uint8))
|
|
2055
|
+
tl.store(lam + b * V + j, lam_mixed)
|
|
2056
|
+
|
|
2057
|
+
# PASS 2: Write outgoing messages
|
|
2058
|
+
tile = row_start
|
|
2059
|
+
while tile < row_end:
|
|
2060
|
+
offs = tile + tl.arange(0, BLOCK_SIZE)
|
|
2061
|
+
m = offs < row_end
|
|
2062
|
+
e = tl.load(var_edges + offs, mask=m, other=0)
|
|
2063
|
+
nu_e = tl.load(nu + b * E + e, mask=m, other=0.0)
|
|
2064
|
+
mu_e = lam_mixed + (sum_nu - nu_e)
|
|
2065
|
+
tl.store(mu + b * E + e, mu_e, mask=m)
|
|
2066
|
+
tile += BLOCK_SIZE
|
|
2067
|
+
|
|
2068
|
+
j += 1
|
|
2069
|
+
|
|
2070
|
+
# ====================================================================
|
|
2071
|
+
# PHASE 3: Parity Check - Check all checks for lane b
|
|
2072
|
+
# ====================================================================
|
|
2073
|
+
i = 0
|
|
2074
|
+
while i < C:
|
|
2075
|
+
row_start = tl.load(chk_ptr + i)
|
|
2076
|
+
row_end = tl.load(chk_ptr + i + 1)
|
|
2077
|
+
par = tl.zeros((), dtype=tl.int32)
|
|
2078
|
+
|
|
2079
|
+
tile = row_start
|
|
2080
|
+
while tile < row_end:
|
|
2081
|
+
offs = tile + tl.arange(0, BLOCK_SIZE)
|
|
2082
|
+
m = offs < row_end
|
|
2083
|
+
e = tl.load(chk_edges + offs, mask=m, other=0)
|
|
2084
|
+
v = tl.load(edge_var + e, mask=m, other=0)
|
|
2085
|
+
if force_hard:
|
|
2086
|
+
bits = tl.load(hard_dec + b * V + v, mask=m, other=0).to(tl.int32) & 1
|
|
2087
|
+
else:
|
|
2088
|
+
posterior_v = tl.load(posterior_ratios + b * V + v, mask=m, other=0.0)
|
|
2089
|
+
bits = (posterior_v <= 0.0).to(tl.int32) & 1
|
|
2090
|
+
par = par ^ (tl.sum(bits & m.to(tl.int32), axis=0) & 1)
|
|
2091
|
+
tile += BLOCK_SIZE
|
|
2092
|
+
|
|
2093
|
+
syn = tl.load(syndrome + b * C + i).to(tl.int32) & 1
|
|
2094
|
+
ok = (par == syn).to(tl.uint8)
|
|
2095
|
+
tl.store(check_ok + b * C + i, ok)
|
|
2096
|
+
i += 1
|
|
2097
|
+
|
|
2098
|
+
# ====================================================================
|
|
2099
|
+
# PHASE 4: Check and Select Solutions
|
|
2100
|
+
# ====================================================================
|
|
2101
|
+
# Check if all checks are satisfied
|
|
2102
|
+
all_ok = tl.full((), 1, dtype=tl.int32)
|
|
2103
|
+
i = 0
|
|
2104
|
+
while i < C:
|
|
2105
|
+
ok_val = tl.load(check_ok + b * C + i).to(tl.int32)
|
|
2106
|
+
all_ok = tl.minimum(all_ok, ok_val)
|
|
2107
|
+
i += 1
|
|
2108
|
+
|
|
2109
|
+
if all_ok != 0:
|
|
2110
|
+
# Compute weight
|
|
2111
|
+
if dtype_enum == 0: # fp16
|
|
2112
|
+
weight = tl.zeros((), dtype=tl.float16)
|
|
2113
|
+
elif dtype_enum == 2: # fp64
|
|
2114
|
+
weight = tl.zeros((), dtype=tl.float64)
|
|
2115
|
+
else: # fp32
|
|
2116
|
+
weight = tl.zeros((), dtype=tl.float32)
|
|
2117
|
+
|
|
2118
|
+
j = 0
|
|
2119
|
+
while j < V:
|
|
2120
|
+
posterior_v = tl.load(posterior_ratios + b * V + j)
|
|
2121
|
+
bits = (posterior_v <= 0.0).to(tl.uint8)
|
|
2122
|
+
wj_val = tl.load(wj + j)
|
|
2123
|
+
if dtype_enum == 0: # fp16
|
|
2124
|
+
weight += wj_val * bits.to(tl.float16)
|
|
2125
|
+
elif dtype_enum == 2: # fp64
|
|
2126
|
+
weight += wj_val * bits.to(tl.float64)
|
|
2127
|
+
else: # fp32
|
|
2128
|
+
weight += wj_val * bits.to(tl.float32)
|
|
2129
|
+
j += 1
|
|
2130
|
+
|
|
2131
|
+
# Update best solution
|
|
2132
|
+
prev_best = tl.load(best_weights + b)
|
|
2133
|
+
prev_valid = tl.load(valid_solutions + b)
|
|
2134
|
+
|
|
2135
|
+
if dtype_enum == 0: # fp16
|
|
2136
|
+
inf_threshold = 60000.0
|
|
2137
|
+
elif dtype_enum == 2: # fp64
|
|
2138
|
+
inf_threshold = 1e20
|
|
2139
|
+
else: # fp32
|
|
2140
|
+
inf_threshold = 1e20
|
|
2141
|
+
is_inf = (prev_best >= inf_threshold).to(tl.int32)
|
|
2142
|
+
better = (weight < prev_best) | (is_inf == 1)
|
|
2143
|
+
|
|
2144
|
+
if better:
|
|
2145
|
+
tl.store(best_weights + b, weight)
|
|
2146
|
+
j = 0
|
|
2147
|
+
while j < V:
|
|
2148
|
+
posterior_v = tl.load(posterior_ratios + b * V + j)
|
|
2149
|
+
bits = (posterior_v <= 0.0).to(tl.uint8)
|
|
2150
|
+
tl.store(best_errors + b * V + j, bits)
|
|
2151
|
+
j += 1
|
|
2152
|
+
|
|
2153
|
+
tl.store(valid_solutions + b, 1)
|
|
2154
|
+
if prev_valid == 0:
|
|
2155
|
+
old = tl.load(found_count + b)
|
|
2156
|
+
tl.store(found_count + b, old + 1)
|
|
2157
|
+
|
|
2158
|
+
# ====================================================================
|
|
2159
|
+
# PHASE 5: Freeze and Check Convergence
|
|
2160
|
+
# ====================================================================
|
|
2161
|
+
fc = tl.load(found_count + b)
|
|
2162
|
+
if in_leg:
|
|
2163
|
+
ss = tl.load(sought_sols + b)
|
|
2164
|
+
stop_criterion = tl.minimum(ss, stop_nconv)
|
|
2165
|
+
else:
|
|
2166
|
+
stop_criterion = stop_nconv
|
|
2167
|
+
|
|
2168
|
+
done_now = fc >= stop_criterion
|
|
2169
|
+
if done_now:
|
|
2170
|
+
fi = tl.load(first_iter + b)
|
|
2171
|
+
if fi < 0:
|
|
2172
|
+
if not in_leg:
|
|
2173
|
+
tl.store(first_iter + b, t + 1)
|
|
2174
|
+
# Copy best_errors to hard_dec
|
|
2175
|
+
j = 0
|
|
2176
|
+
while j < V:
|
|
2177
|
+
be = tl.load(best_errors + b * V + j)
|
|
2178
|
+
tl.store(hard_dec + b * V + j, be)
|
|
2179
|
+
j += 1
|
|
2180
|
+
tl.store(active + b, 0)
|
|
2181
|
+
# Reset gamma
|
|
2182
|
+
j = 0
|
|
2183
|
+
while j < V:
|
|
2184
|
+
tl.store(gamma + b * V + j, 0.0)
|
|
2185
|
+
j += 1
|
|
2186
|
+
|
|
2187
|
+
converged = True
|
|
2188
|
+
converged_iter = t_offset
|
|
2189
|
+
# Set iter_counter to convergence iteration (1-indexed) when convergence happens
|
|
2190
|
+
# This prevents iter_counter from being updated to max_iter in subsequent loop iterations
|
|
2191
|
+
tl.store(iter_counter + b, t + 1)
|
|
2192
|
+
|
|
2193
|
+
# Update iteration counter only if not converged (to avoid overwriting convergence iteration)
|
|
2194
|
+
if not converged:
|
|
2195
|
+
tl.store(iter_counter + b, t + 1)
|
|
2196
|
+
|
|
2197
|
+
t_offset += 1
|
|
2198
|
+
|
|
2199
|
+
# Store convergence iteration if converged (backup in case first_iter wasn't set in loop)
|
|
2200
|
+
# Note: converged_iter is 0-indexed (t_offset), but first_iter should be 1-indexed to match Rust
|
|
2201
|
+
# Rust's current_iteration is incremented after each iteration, so it's 1-indexed
|
|
2202
|
+
if converged_iter >= 0 and not in_leg:
|
|
2203
|
+
fi = tl.load(first_iter + b)
|
|
2204
|
+
if fi < 0:
|
|
2205
|
+
# Convert to 1-indexed: start_iter + converged_iter + 1
|
|
2206
|
+
# This matches the t + 1 assignment in the loop above
|
|
2207
|
+
tl.store(first_iter + b, start_iter + converged_iter + 1)
|
|
2208
|
+
|