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/graph.py
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
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
|
+
"""Graph utilities for converting SciPy CSR matrices to device-optimized edge-centric representation.
|
|
12
|
+
|
|
13
|
+
This module provides utilities for converting quantum error correction check matrices
|
|
14
|
+
from SciPy sparse format to GPU-optimized representations for belief propagation.
|
|
15
|
+
The CSRGraph class builds edge-centric data structures that enable efficient
|
|
16
|
+
message passing on GPU hardware.
|
|
17
|
+
|
|
18
|
+
The implementation follows the sparse graph representation used in the Rust
|
|
19
|
+
implementation in crates/relay_bp/src/bipartite_graph.rs.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import torch
|
|
23
|
+
import numpy as np
|
|
24
|
+
import math
|
|
25
|
+
from scipy.sparse import csr_matrix
|
|
26
|
+
from typing import Tuple, Optional
|
|
27
|
+
|
|
28
|
+
from scipy.sparse import issparse
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CSRGraph:
|
|
32
|
+
"""GPU-optimized representation of quantum error correction check matrix.
|
|
33
|
+
|
|
34
|
+
This class converts a SciPy CSR matrix representing quantum stabilizer generators
|
|
35
|
+
into GPU-optimized edge-centric data structures for belief propagation.
|
|
36
|
+
|
|
37
|
+
The representation includes:
|
|
38
|
+
- Check-centric view: chk_ptr, chk_edges for check-to-variable message passing
|
|
39
|
+
- Variable-centric view: var_ptr, var_edges for variable-to-check message passing
|
|
40
|
+
- Edge mappings: edge_var, edge_chk for edge-to-node lookups
|
|
41
|
+
|
|
42
|
+
This follows the sparse bipartite graph representation in Rust bipartite_graph.rs.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
H_csr: csr_matrix,
|
|
48
|
+
device: str = "cuda",
|
|
49
|
+
check_permutation: Optional[np.ndarray] = None,
|
|
50
|
+
):
|
|
51
|
+
"""Initialize CSRGraph from quantum check matrix.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
H_csr: Check matrix (C x V) in CSR format representing stabilizer generators
|
|
55
|
+
device: Target GPU device ("cuda" or "rocm")
|
|
56
|
+
check_permutation: Optional permutation of check rows [C] int32 array. If provided,
|
|
57
|
+
reorders CSR rows before building graph structures (used for graph-aware
|
|
58
|
+
block definitions in layered scheduling).
|
|
59
|
+
"""
|
|
60
|
+
if device == "cpu":
|
|
61
|
+
raise RuntimeError(
|
|
62
|
+
"CPU not supported - Triton kernels require GPU. Use device='cuda' or 'rocm'"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Accept any scipy sparse, ensure CSR matrix
|
|
66
|
+
if not issparse(H_csr):
|
|
67
|
+
raise ValueError("Input must be a scipy.sparse matrix")
|
|
68
|
+
if not isinstance(H_csr, csr_matrix):
|
|
69
|
+
H_csr = H_csr.tocsr()
|
|
70
|
+
|
|
71
|
+
self.device = device
|
|
72
|
+
# Store shape before permutation (for reference)
|
|
73
|
+
self.C, self.V = H_csr.shape
|
|
74
|
+
self.E = H_csr.nnz
|
|
75
|
+
|
|
76
|
+
# Validate input
|
|
77
|
+
if not isinstance(H_csr, csr_matrix):
|
|
78
|
+
raise ValueError("Input must be a scipy.sparse.csr_matrix")
|
|
79
|
+
|
|
80
|
+
if H_csr.dtype != np.uint8 and H_csr.dtype != bool:
|
|
81
|
+
# Convert to binary if needed
|
|
82
|
+
H_csr = (H_csr != 0).astype(np.uint8)
|
|
83
|
+
|
|
84
|
+
# Apply check permutation if provided (reorder CSR rows)
|
|
85
|
+
if check_permutation is not None:
|
|
86
|
+
check_permutation = np.asarray(check_permutation, dtype=np.int32)
|
|
87
|
+
if check_permutation.shape[0] != H_csr.shape[0]:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"check_permutation length ({check_permutation.shape[0]}) must match number of checks ({H_csr.shape[0]})"
|
|
90
|
+
)
|
|
91
|
+
if not (check_permutation.min() >= 0 and check_permutation.max() < H_csr.shape[0]):
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"check_permutation indices must be in range [0, {H_csr.shape[0]})"
|
|
94
|
+
)
|
|
95
|
+
# Reorder CSR matrix rows
|
|
96
|
+
H_csr = H_csr[check_permutation, :]
|
|
97
|
+
|
|
98
|
+
# Convert to device tensors
|
|
99
|
+
self._build_edge_representation(H_csr)
|
|
100
|
+
|
|
101
|
+
def _build_edge_representation(self, H_csr: csr_matrix):
|
|
102
|
+
"""Build edge-centric representation from CSR matrix.
|
|
103
|
+
|
|
104
|
+
This method constructs the GPU-optimized data structures needed for
|
|
105
|
+
belief propagation message passing, following the sparse graph format
|
|
106
|
+
used in the Rust implementation.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
H_csr: Check matrix in CSR format
|
|
110
|
+
"""
|
|
111
|
+
# Get CSR data
|
|
112
|
+
chk_ptr = H_csr.indptr.astype(np.int32)
|
|
113
|
+
chk_edges = H_csr.indices.astype(np.int32) # These are variable indices
|
|
114
|
+
|
|
115
|
+
# Build variable-centric representation
|
|
116
|
+
var_ptr = np.zeros(self.V + 1, dtype=np.int32)
|
|
117
|
+
var_edges = np.zeros(self.E, dtype=np.int32)
|
|
118
|
+
edge_var = np.zeros(self.E, dtype=np.int32)
|
|
119
|
+
edge_chk = np.zeros(self.E, dtype=np.int32)
|
|
120
|
+
|
|
121
|
+
# Build edge list and variable-centric pointers
|
|
122
|
+
edge_idx = 0
|
|
123
|
+
for chk_idx in range(self.C):
|
|
124
|
+
start = chk_ptr[chk_idx]
|
|
125
|
+
end = chk_ptr[chk_idx + 1]
|
|
126
|
+
|
|
127
|
+
for local_idx in range(start, end):
|
|
128
|
+
var_idx = chk_edges[local_idx]
|
|
129
|
+
|
|
130
|
+
# Add to variable's edge list
|
|
131
|
+
var_ptr[var_idx + 1] += 1
|
|
132
|
+
|
|
133
|
+
# Store edge information
|
|
134
|
+
edge_var[edge_idx] = var_idx
|
|
135
|
+
edge_chk[edge_idx] = chk_idx
|
|
136
|
+
var_edges[edge_idx] = edge_idx # Will be updated after sorting
|
|
137
|
+
|
|
138
|
+
edge_idx += 1
|
|
139
|
+
|
|
140
|
+
# Convert to cumulative pointers for variables
|
|
141
|
+
for i in range(1, self.V + 1):
|
|
142
|
+
var_ptr[i] += var_ptr[i - 1]
|
|
143
|
+
|
|
144
|
+
# Sort edges by variable index and update var_edges
|
|
145
|
+
edge_order = np.argsort(edge_var)
|
|
146
|
+
for i, edge_idx in enumerate(edge_order):
|
|
147
|
+
var_idx = edge_var[edge_idx]
|
|
148
|
+
var_edges[var_ptr[var_idx]] = edge_idx
|
|
149
|
+
var_ptr[var_idx] += 1
|
|
150
|
+
|
|
151
|
+
# Reset var_ptr to start positions
|
|
152
|
+
for i in range(self.V - 1, 0, -1):
|
|
153
|
+
var_ptr[i] = var_ptr[i - 1]
|
|
154
|
+
var_ptr[0] = 0
|
|
155
|
+
|
|
156
|
+
# Now build chk_edges as edge IDs (not variable indices)
|
|
157
|
+
chk_edges_edge_ids = np.zeros(self.E, dtype=np.int32)
|
|
158
|
+
edge_idx = 0
|
|
159
|
+
for chk_idx in range(self.C):
|
|
160
|
+
start = chk_ptr[chk_idx]
|
|
161
|
+
end = chk_ptr[chk_idx + 1]
|
|
162
|
+
|
|
163
|
+
for local_idx in range(start, end):
|
|
164
|
+
chk_edges_edge_ids[local_idx] = edge_idx
|
|
165
|
+
edge_idx += 1
|
|
166
|
+
|
|
167
|
+
# Move to device
|
|
168
|
+
self.chk_ptr = torch.from_numpy(chk_ptr).to(self.device)
|
|
169
|
+
self.chk_edges = torch.from_numpy(chk_edges_edge_ids).to(self.device) # Edge IDs
|
|
170
|
+
self.var_ptr = torch.from_numpy(var_ptr).to(self.device)
|
|
171
|
+
self.var_edges = torch.from_numpy(var_edges).to(self.device)
|
|
172
|
+
self.edge_var = torch.from_numpy(edge_var).to(self.device)
|
|
173
|
+
self.edge_chk = torch.from_numpy(edge_chk).to(self.device)
|
|
174
|
+
|
|
175
|
+
# Compute degree statistics for autotuning
|
|
176
|
+
self.max_deg_chk = int((chk_ptr[1:] - chk_ptr[:-1]).max())
|
|
177
|
+
self.max_deg_var = int((var_ptr[1:] - var_ptr[:-1]).max())
|
|
178
|
+
|
|
179
|
+
# Compute sparsity pattern hash for autotuning (captures connectivity, not just dimensions)
|
|
180
|
+
# This ensures different H matrices with same (C, V, E) get different autotuned configs
|
|
181
|
+
# The hash captures memory access patterns that affect kernel performance (degree distribution,
|
|
182
|
+
# irregularity, locality) which influence optimal BLOCK_SIZE, BTILE, and ROWS_PER_CHK values
|
|
183
|
+
import hashlib
|
|
184
|
+
|
|
185
|
+
# Hash the actual edge structure: degree distribution + sorted edge list per check
|
|
186
|
+
chk_degrees = (chk_ptr[1:] - chk_ptr[:-1]).astype(np.int32)
|
|
187
|
+
var_degrees = (var_ptr[1:] - var_ptr[:-1]).astype(np.int32)
|
|
188
|
+
# Build sorted edge list per check (for consistent hashing regardless of input CSR column order)
|
|
189
|
+
# Sort variable indices within each check to get canonical representation
|
|
190
|
+
sorted_edges_per_check = []
|
|
191
|
+
for chk_idx in range(self.C):
|
|
192
|
+
start = chk_ptr[chk_idx]
|
|
193
|
+
end = chk_ptr[chk_idx + 1]
|
|
194
|
+
if end > start:
|
|
195
|
+
# Get variable indices for this check from original CSR and sort them
|
|
196
|
+
var_indices = chk_edges[start:end] # These are variable indices from CSR
|
|
197
|
+
sorted_edges_per_check.append(np.sort(var_indices))
|
|
198
|
+
else:
|
|
199
|
+
sorted_edges_per_check.append(np.array([], dtype=np.int32))
|
|
200
|
+
# Combine degree distributions and sorted edge lists into hash
|
|
201
|
+
# Sort degree lists for consistency (order doesn't matter for degree distribution)
|
|
202
|
+
pattern_blob = np.concatenate(
|
|
203
|
+
[
|
|
204
|
+
np.sort(chk_degrees).astype(np.int32), # Check degree distribution (sorted)
|
|
205
|
+
np.sort(var_degrees).astype(np.int32), # Variable degree distribution (sorted)
|
|
206
|
+
np.concatenate([np.sort(e) for e in sorted_edges_per_check]).astype(
|
|
207
|
+
np.int32
|
|
208
|
+
), # All edges (sorted per check)
|
|
209
|
+
]
|
|
210
|
+
).tobytes()
|
|
211
|
+
self.sparsity_hash = hashlib.blake2b(pattern_blob, digest_size=16).hexdigest()
|
|
212
|
+
|
|
213
|
+
def get_device_tensors(self) -> Tuple[torch.Tensor, ...]:
|
|
214
|
+
"""Get all device tensors for kernel calls.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Tuple of (chk_ptr, chk_edges, var_ptr, var_edges, edge_var, edge_chk)
|
|
218
|
+
"""
|
|
219
|
+
return (
|
|
220
|
+
self.chk_ptr,
|
|
221
|
+
self.chk_edges,
|
|
222
|
+
self.var_ptr,
|
|
223
|
+
self.var_edges,
|
|
224
|
+
self.edge_var,
|
|
225
|
+
self.edge_chk,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def validate(self) -> bool:
|
|
229
|
+
"""Validate graph structure."""
|
|
230
|
+
# Check that all edge indices are valid
|
|
231
|
+
assert self.edge_var.max() < self.V, "Invalid variable index in edges"
|
|
232
|
+
assert self.edge_chk.max() < self.C, "Invalid check index in edges"
|
|
233
|
+
|
|
234
|
+
# Correctness checks: ensure CSR pointers have correct length
|
|
235
|
+
assert self.chk_ptr.shape[0] == self.C + 1, (
|
|
236
|
+
f"chk_ptr must have length C+1 ({self.C + 1}), got {self.chk_ptr.shape[0]}"
|
|
237
|
+
)
|
|
238
|
+
assert self.var_ptr.shape[0] == self.V + 1, (
|
|
239
|
+
f"var_ptr must have length V+1 ({self.V + 1}), got {self.var_ptr.shape[0]}"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Check that pointers are monotonic
|
|
243
|
+
assert torch.all(self.chk_ptr[1:] >= self.chk_ptr[:-1]), "Invalid check pointers"
|
|
244
|
+
assert torch.all(self.var_ptr[1:] >= self.var_ptr[:-1]), "Invalid variable pointers"
|
|
245
|
+
|
|
246
|
+
# Check that edge counts match
|
|
247
|
+
assert self.chk_ptr[-1] == self.E, "Check pointer mismatch"
|
|
248
|
+
assert self.var_ptr[-1] == self.E, "Variable pointer mismatch"
|
|
249
|
+
|
|
250
|
+
return True
|
|
251
|
+
|
|
252
|
+
def get_stats(self) -> dict:
|
|
253
|
+
"""Get graph statistics for debugging."""
|
|
254
|
+
return {
|
|
255
|
+
"checks": self.C,
|
|
256
|
+
"variables": self.V,
|
|
257
|
+
"edges": self.E,
|
|
258
|
+
"max_degree_check": self.max_deg_chk,
|
|
259
|
+
"max_degree_variable": self.max_deg_var,
|
|
260
|
+
"avg_degree_check": self.E / self.C,
|
|
261
|
+
"avg_degree_variable": self.E / self.V,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
def build_block_metadata(
|
|
265
|
+
self,
|
|
266
|
+
num_blocks: Optional[int] = None,
|
|
267
|
+
target_edges_per_block: Optional[int] = None,
|
|
268
|
+
):
|
|
269
|
+
"""Build block metadata for layered scheduling using edge-based heuristics.
|
|
270
|
+
|
|
271
|
+
This method computes:
|
|
272
|
+
- block_chk_start[nb], block_chk_end[nb]: Check ranges for each block
|
|
273
|
+
- block_var_ptr[nb+1], block_vars[total]: Variable lists touched by each block
|
|
274
|
+
|
|
275
|
+
Block count is determined by edge-based heuristics (more stable than check-based)
|
|
276
|
+
to balance kernel launch overhead vs block size.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
num_blocks: Explicit number of blocks (if None, computed from target_edges_per_block)
|
|
280
|
+
target_edges_per_block: Target edges per block for automatic sizing (default: 200000)
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Tuple of (block_chk_start, block_chk_end, block_var_ptr, block_vars, num_blocks)
|
|
284
|
+
All as torch tensors on device (except num_blocks which is int).
|
|
285
|
+
"""
|
|
286
|
+
# Determine num_blocks from edge-based heuristic if not provided
|
|
287
|
+
if num_blocks is None:
|
|
288
|
+
if target_edges_per_block is None:
|
|
289
|
+
target_edges_per_block = 1000 # Default: 200k edges per block
|
|
290
|
+
# Compute num_blocks based on total edges (use ceil, not floor)
|
|
291
|
+
# No max cap - if user wants many blocks, let them (they can set num_blocks explicitly)
|
|
292
|
+
num_blocks = max(1, math.ceil(self.E / target_edges_per_block))
|
|
293
|
+
|
|
294
|
+
num_blocks = int(num_blocks)
|
|
295
|
+
if num_blocks < 1:
|
|
296
|
+
raise ValueError(f"num_blocks must be >= 1, got {num_blocks}")
|
|
297
|
+
if num_blocks > self.C:
|
|
298
|
+
raise ValueError(f"num_blocks ({num_blocks}) cannot exceed number of checks ({self.C})")
|
|
299
|
+
|
|
300
|
+
# Edge-balanced block splitting: split by cumulative edge count, not check count
|
|
301
|
+
# This ensures each block processes roughly the same number of edges
|
|
302
|
+
chk_ptr_host = self.chk_ptr.cpu().numpy()
|
|
303
|
+
|
|
304
|
+
# Compute edge count per check (degree)
|
|
305
|
+
edges_per_check = chk_ptr_host[1:] - chk_ptr_host[:-1]
|
|
306
|
+
|
|
307
|
+
# Compute prefix sum of edge counts (cumulative edges up to each check)
|
|
308
|
+
cumulative_edges = np.zeros(self.C + 1, dtype=np.int64)
|
|
309
|
+
cumulative_edges[1:] = np.cumsum(edges_per_check)
|
|
310
|
+
# cumulative_edges[i] = total edges in checks [0, i)
|
|
311
|
+
|
|
312
|
+
# Target edges per block
|
|
313
|
+
target_edges = self.E / num_blocks
|
|
314
|
+
|
|
315
|
+
# Find block boundaries by splitting on cumulative edge count
|
|
316
|
+
block_chk_start = np.zeros(num_blocks, dtype=np.int32)
|
|
317
|
+
block_chk_end = np.zeros(num_blocks, dtype=np.int32)
|
|
318
|
+
|
|
319
|
+
block_idx = 0
|
|
320
|
+
target_cumulative = target_edges
|
|
321
|
+
block_chk_start[0] = 0
|
|
322
|
+
|
|
323
|
+
for chk_idx in range(1, self.C + 1):
|
|
324
|
+
# Check if we've accumulated enough edges for current block
|
|
325
|
+
# Or if we're on the last block, we must end it at C
|
|
326
|
+
if cumulative_edges[chk_idx] >= target_cumulative:
|
|
327
|
+
# End current block at this check
|
|
328
|
+
block_chk_end[block_idx] = chk_idx
|
|
329
|
+
block_idx += 1
|
|
330
|
+
if block_idx < num_blocks:
|
|
331
|
+
block_chk_start[block_idx] = chk_idx
|
|
332
|
+
target_cumulative = (block_idx + 1) * target_edges
|
|
333
|
+
else:
|
|
334
|
+
# We've filled all blocks, break early
|
|
335
|
+
break
|
|
336
|
+
|
|
337
|
+
# Ensure all blocks are properly terminated
|
|
338
|
+
# If we didn't fill all blocks, the last one ends at C
|
|
339
|
+
if block_idx < num_blocks:
|
|
340
|
+
# Some blocks weren't filled - end the current block at C
|
|
341
|
+
block_chk_end[block_idx] = self.C
|
|
342
|
+
block_idx += 1
|
|
343
|
+
# Fill any remaining empty blocks (shouldn't happen, but handle gracefully)
|
|
344
|
+
for b in range(block_idx, num_blocks):
|
|
345
|
+
block_chk_start[b] = self.C
|
|
346
|
+
block_chk_end[b] = self.C
|
|
347
|
+
else:
|
|
348
|
+
# All blocks filled - ensure last block ends at C
|
|
349
|
+
block_chk_end[num_blocks - 1] = self.C
|
|
350
|
+
|
|
351
|
+
# Post-process: ensure no empty blocks (merge empty blocks into previous)
|
|
352
|
+
# This can happen with highly irregular degree distributions
|
|
353
|
+
non_empty_blocks = []
|
|
354
|
+
for b in range(num_blocks):
|
|
355
|
+
if block_chk_end[b] > block_chk_start[b]:
|
|
356
|
+
non_empty_blocks.append(b)
|
|
357
|
+
|
|
358
|
+
if len(non_empty_blocks) < num_blocks:
|
|
359
|
+
# Some blocks are empty - compact them
|
|
360
|
+
num_non_empty = len(non_empty_blocks)
|
|
361
|
+
if num_non_empty == 0:
|
|
362
|
+
# All blocks empty (shouldn't happen, but handle gracefully)
|
|
363
|
+
num_non_empty = 1
|
|
364
|
+
non_empty_blocks = [0]
|
|
365
|
+
block_chk_start[0] = 0
|
|
366
|
+
block_chk_end[0] = self.C
|
|
367
|
+
|
|
368
|
+
# Rebuild block arrays with only non-empty blocks
|
|
369
|
+
block_chk_start_new = np.zeros(num_non_empty, dtype=np.int32)
|
|
370
|
+
block_chk_end_new = np.zeros(num_non_empty, dtype=np.int32)
|
|
371
|
+
for i, b in enumerate(non_empty_blocks):
|
|
372
|
+
block_chk_start_new[i] = block_chk_start[b]
|
|
373
|
+
block_chk_end_new[i] = block_chk_end[b]
|
|
374
|
+
|
|
375
|
+
block_chk_start = block_chk_start_new
|
|
376
|
+
block_chk_end = block_chk_end_new
|
|
377
|
+
num_blocks = num_non_empty
|
|
378
|
+
|
|
379
|
+
# Correctness assertions: ensure blocks form a contiguous, non-empty partition
|
|
380
|
+
# These catch any edge cases where compaction or splitting logic might produce invalid blocks
|
|
381
|
+
assert block_chk_start[0] == 0, f"First block must start at 0, got {block_chk_start[0]}"
|
|
382
|
+
assert block_chk_end[-1] == self.C, (
|
|
383
|
+
f"Last block must end at C={self.C}, got {block_chk_end[-1]}"
|
|
384
|
+
)
|
|
385
|
+
assert all(block_chk_end[i] == block_chk_start[i + 1] for i in range(num_blocks - 1)), (
|
|
386
|
+
"Blocks must be contiguous (end[i] == start[i+1] for all adjacent pairs)"
|
|
387
|
+
)
|
|
388
|
+
assert all(block_chk_end[i] > block_chk_start[i] for i in range(num_blocks)), (
|
|
389
|
+
"All blocks must be non-empty (end[i] > start[i] for all blocks)"
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# For each block, find unique variables touched by checks in that block
|
|
393
|
+
# Use CSR structure: checks in [chk_start, chk_end) have edges in [chk_ptr[chk_start], chk_ptr[chk_end])
|
|
394
|
+
# This is contiguous in check-major ordering, so we can use a single slice
|
|
395
|
+
chk_ptr_host = self.chk_ptr.cpu().numpy()
|
|
396
|
+
chk_edges_host = self.chk_edges.cpu().numpy()
|
|
397
|
+
edge_var_host = self.edge_var.cpu().numpy()
|
|
398
|
+
|
|
399
|
+
# Correctness check: ensure CSR pointers have correct length
|
|
400
|
+
assert chk_ptr_host.shape[0] == self.C + 1, (
|
|
401
|
+
f"chk_ptr must have length C+1 ({self.C + 1}), got {chk_ptr_host.shape[0]}"
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
block_vars_list = [] # List of variable lists per block
|
|
405
|
+
block_var_ptr = [0] # Prefix sum pointer
|
|
406
|
+
|
|
407
|
+
# Note: num_blocks may have been reduced if empty blocks were removed
|
|
408
|
+
for b in range(num_blocks):
|
|
409
|
+
chk_start = int(block_chk_start[b])
|
|
410
|
+
chk_end = int(block_chk_end[b])
|
|
411
|
+
|
|
412
|
+
# Fast numpy path: use contiguous edge range (much faster than list comprehension)
|
|
413
|
+
# Checks in [chk_start, chk_end) have edges in [chk_ptr[chk_start], chk_ptr[chk_end])
|
|
414
|
+
# This works because edges are stored in check-major order
|
|
415
|
+
e0 = chk_ptr_host[chk_start]
|
|
416
|
+
e1 = chk_ptr_host[chk_end]
|
|
417
|
+
|
|
418
|
+
if e1 > e0:
|
|
419
|
+
# Get all edge IDs for this block (contiguous slice)
|
|
420
|
+
all_edge_ids = chk_edges_host[e0:e1]
|
|
421
|
+
# Map edge IDs → var IDs (vectorized indexing)
|
|
422
|
+
all_var_ids = edge_var_host[all_edge_ids]
|
|
423
|
+
# Get unique var IDs (sorted automatically by np.unique)
|
|
424
|
+
block_vars_sorted = np.unique(all_var_ids).astype(np.int32)
|
|
425
|
+
else:
|
|
426
|
+
block_vars_sorted = np.array([], dtype=np.int32)
|
|
427
|
+
|
|
428
|
+
block_vars_list.append(block_vars_sorted.tolist())
|
|
429
|
+
block_var_ptr.append(block_var_ptr[-1] + len(block_vars_sorted))
|
|
430
|
+
|
|
431
|
+
# Flatten into single array
|
|
432
|
+
total_vars = block_var_ptr[-1]
|
|
433
|
+
block_vars_flat = np.zeros(total_vars, dtype=np.int32)
|
|
434
|
+
for b, var_list in enumerate(block_vars_list):
|
|
435
|
+
start = block_var_ptr[b]
|
|
436
|
+
end = block_var_ptr[b + 1]
|
|
437
|
+
block_vars_flat[start:end] = var_list
|
|
438
|
+
|
|
439
|
+
# Diagnostic output: block statistics for validation/optimization
|
|
440
|
+
import logging
|
|
441
|
+
|
|
442
|
+
logger = logging.getLogger(__name__)
|
|
443
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
444
|
+
logger.debug(f"Block metadata: {num_blocks} blocks, {total_vars} total variables")
|
|
445
|
+
for b in range(num_blocks):
|
|
446
|
+
chk_start = int(block_chk_start[b])
|
|
447
|
+
chk_end = int(block_chk_end[b])
|
|
448
|
+
var_start = int(block_var_ptr[b])
|
|
449
|
+
var_end = int(block_var_ptr[b + 1])
|
|
450
|
+
# Compute edges in block
|
|
451
|
+
e0 = chk_ptr_host[chk_start]
|
|
452
|
+
e1 = chk_ptr_host[chk_end]
|
|
453
|
+
edges_in_block = e1 - e0
|
|
454
|
+
vars_in_block = var_end - var_start
|
|
455
|
+
# Compute overlap with previous block
|
|
456
|
+
overlap_with_prev = 0
|
|
457
|
+
if b > 0:
|
|
458
|
+
prev_var_start = int(block_var_ptr[b - 1])
|
|
459
|
+
prev_var_end = int(block_var_ptr[b])
|
|
460
|
+
prev_vars = set(block_vars_flat[prev_var_start:prev_var_end])
|
|
461
|
+
curr_vars = set(block_vars_flat[var_start:var_end])
|
|
462
|
+
overlap_with_prev = len(prev_vars & curr_vars)
|
|
463
|
+
overlap_ratio = overlap_with_prev / vars_in_block if vars_in_block > 0 else 0.0
|
|
464
|
+
else:
|
|
465
|
+
overlap_ratio = 0.0
|
|
466
|
+
logger.debug(
|
|
467
|
+
f" Block {b}: checks [{chk_start}, {chk_end}), "
|
|
468
|
+
f"edges={edges_in_block}, vars={vars_in_block}, "
|
|
469
|
+
f"overlap_with_prev={overlap_with_prev} (ratio={overlap_ratio:.3f})"
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
# Convert to tensors on device (use existing tensor's device for safety)
|
|
473
|
+
dev = self.chk_ptr.device
|
|
474
|
+
block_chk_start_t = torch.from_numpy(block_chk_start).to(dev)
|
|
475
|
+
block_chk_end_t = torch.from_numpy(block_chk_end).to(dev)
|
|
476
|
+
block_var_ptr_t = torch.from_numpy(np.array(block_var_ptr, dtype=np.int32)).to(dev)
|
|
477
|
+
block_vars_t = torch.from_numpy(block_vars_flat).to(dev)
|
|
478
|
+
|
|
479
|
+
return block_chk_start_t, block_chk_end_t, block_var_ptr_t, block_vars_t, num_blocks
|