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/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
+