Skip to content

Commit 97cdc1c

Browse files
authored
Add nv-qldpc-decoder example that uses pre-generated test data (#130)
Generating stressing test data is non-trivial. This PR adds an example Python script showing how to download pre-existing test data that is large enough to suitably stress the decoder. --------- Signed-off-by: Ben Howe <[email protected]>
1 parent 8d2645b commit 97cdc1c

File tree

6 files changed

+264
-4
lines changed

6 files changed

+264
-4
lines changed

docs/sphinx/api/qec/cpp_api.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ Decoder Interfaces
3535
Built-in Decoders
3636
=================
3737

38+
.. _nv_qldpc_decoder_api_cpp:
39+
40+
NVIDIA QLDPC Decoder
41+
--------------------
42+
3843
.. include:: nv_qldpc_decoder_api.rst
3944

4045
Common

docs/sphinx/api/qec/nv_qldpc_decoder_api.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
[0, 1, 0, 1, 1, 0, 1],
3434
[0, 0, 1, 0, 1, 1, 1]], dtype=np.uint8) # sample 3x7 PCM
3535
opts = dict() # see below for options
36-
nvdec = qec.get_decoder('nv-qldpc-decoder', H, opts)
36+
nvdec = qec.get_decoder('nv-qldpc-decoder', H, **opts)
3737
3838
.. tab:: C++
3939

@@ -56,8 +56,8 @@
5656
5757
.. note::
5858
The `"nv-qldpc-decoder"` implements the :class:`cudaq_qec.Decoder`
59-
interface for Python and the :class:`cudaq::qec::decoder` interface for
60-
C++, so it supports all the methods in those respective classes.
59+
interface for Python and the :cpp:class:`cudaq::qec::decoder` interface
60+
for C++, so it supports all the methods in those respective classes.
6161

6262
:param H: Parity check matrix (tensor format)
6363
:param params: Heterogeneous map of parameters:

docs/sphinx/api/qec/python_api.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ Decoder Interfaces
2222
Built-in Decoders
2323
=================
2424

25+
.. _nv_qldpc_decoder_api_python:
26+
27+
NVIDIA QLDPC Decoder
28+
--------------------
29+
2530
.. include:: nv_qldpc_decoder_api.rst
2631

2732
Common
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
# ============================================================================ #
2+
# Copyright (c) 2025 NVIDIA Corporation & Affiliates. #
3+
# All rights reserved. #
4+
# #
5+
# This source code and the accompanying materials are made available under #
6+
# the terms of the Apache License 2.0 which accompanies this distribution. #
7+
# ============================================================================ #
8+
# [Begin Documentation]
9+
10+
import numpy as np
11+
from scipy.sparse import csr_matrix
12+
import cudaq_qec as qec
13+
import json
14+
import time
15+
16+
# For fetching data
17+
import requests
18+
import bz2
19+
import os
20+
21+
# Note: running this script will automatically download data if necessary.
22+
23+
### Helper functions ###
24+
25+
26+
def parse_csr_mat(j, dims, mat_name):
27+
"""
28+
Parse a CSR-style matrix from a JSON file using SciPy's sparse matrix utilities.
29+
"""
30+
assert len(dims) == 2, "dims must be a tuple of two integers"
31+
32+
# Extract indptr and indices from the JSON.
33+
indptr = np.array(j[f"{mat_name}_indptr"], dtype=int)
34+
indices = np.array(j[f"{mat_name}_indices"], dtype=int)
35+
36+
# Check that the CSR structure is consistent.
37+
assert len(indptr) == dims[0] + 1, "indptr length must equal dims[0] + 1"
38+
assert np.all(
39+
indices < dims[1]), "All column indices must be less than dims[1]"
40+
41+
# Create a data array of ones.
42+
data = np.ones(indptr[-1], dtype=np.uint8)
43+
44+
# Build the CSR matrix and return it as a dense numpy array.
45+
csr = csr_matrix((data, indices, indptr), shape=dims, dtype=np.uint8)
46+
return csr.toarray()
47+
48+
49+
def parse_H_csr(j, dims):
50+
"""
51+
Parse a CSR-style parity check matrix from an input file in JSON format"
52+
"""
53+
return parse_csr_mat(j, dims, "H")
54+
55+
56+
def parse_obs_csr(j, dims):
57+
"""
58+
Parse a CSR-style observable matrix from an input file in JSON format"
59+
"""
60+
return parse_csr_mat(j, dims, "obs_mat")
61+
62+
63+
### Main decoder loop ###
64+
65+
66+
def run_decoder(filename, num_shots, run_as_batched):
67+
"""
68+
Load a JSON file and decode "num_shots" syndromes.
69+
"""
70+
t_load_begin = time.time()
71+
with open(filename, "r") as f:
72+
j = json.load(f)
73+
74+
dims = j["shape"]
75+
assert len(dims) == 2
76+
77+
# Read the Parity Check Matrix
78+
H = parse_H_csr(j, dims)
79+
syndrome_length, block_length = dims
80+
t_load_end = time.time()
81+
82+
print(f"{filename} parsed in {1e3 * (t_load_end-t_load_begin)} ms")
83+
84+
error_rate_vec = np.array(j["error_rate_vec"])
85+
assert len(error_rate_vec) == block_length
86+
obs_mat_dims = j["obs_mat_shape"]
87+
obs_mat = parse_obs_csr(j, obs_mat_dims)
88+
assert dims[1] == obs_mat_dims[0]
89+
file_num_trials = j["num_trials"]
90+
num_shots = min(num_shots, file_num_trials)
91+
print(
92+
f'Your JSON file has {file_num_trials} shots. Running {num_shots} now.')
93+
94+
# osd_method: 0=Off, 1=OSD-0, 2=Exhaustive, 3=Combination Sweep
95+
osd_method = 1
96+
97+
# When osd_method is:
98+
# 2) there are 2^osd_order additional error mechanisms checked.
99+
# 3) there are an additional k + osd_order*(osd_order-1)/2 error
100+
# mechanisms checked.
101+
# Ref: https://arxiv.org/pdf/2005.07016
102+
osd_order = 0
103+
104+
# Maximum number of BP iterations before attempting OSD (if necessary)
105+
max_iter = 50
106+
107+
nv_dec_args = {
108+
"max_iterations": max_iter,
109+
"error_rate_vec": error_rate_vec,
110+
"use_sparsity": True,
111+
"use_osd": osd_method > 0,
112+
"osd_order": osd_order,
113+
"osd_method": osd_method
114+
}
115+
116+
if run_as_batched:
117+
# Perform BP processing for up to 1000 syndromes per batch. If there
118+
# are more than 1000 syndromes, the decoder will chunk them up and
119+
# process each batch sequentially under the hood.
120+
nv_dec_args['bp_batch_size'] = min(1000, num_shots)
121+
122+
try:
123+
nv_dec_gpu_and_cpu = qec.get_decoder("nv-qldpc-decoder", H,
124+
**nv_dec_args)
125+
except Exception as e:
126+
print(
127+
'The nv-qldpc-decoder is not available with your current CUDA-Q ' +
128+
'QEC installation.')
129+
exit(0)
130+
decoding_time = 0
131+
bp_converged_flags = []
132+
num_logical_errors = 0
133+
134+
# Batched API
135+
if run_as_batched:
136+
syndrome_list = []
137+
obs_truth_list = []
138+
for i in range(num_shots):
139+
syndrome = j["trials"][i]["syndrome_truth"]
140+
obs_truth = j["trials"][i]["obs_truth"]
141+
syndrome_list.append(syndrome)
142+
obs_truth_list.append(obs_truth)
143+
t0 = time.time()
144+
results = nv_dec_gpu_and_cpu.decode_batch(syndrome_list)
145+
t1 = time.time()
146+
decoding_time += t1 - t0
147+
for r, obs_truth in zip(results, obs_truth_list):
148+
bp_converged_flags.append(r.converged)
149+
dec_result = np.array(r.result, dtype=np.uint8)
150+
151+
# See if this prediction flipped the observable
152+
predicted_observable = obs_mat.T @ dec_result % 2
153+
print(f"predicted_observable: {predicted_observable}")
154+
155+
# See if the observable was actually flipped according to the truth
156+
# data
157+
actual_observable = np.array(obs_truth, dtype=np.uint8)
158+
print(f"actual_observable: {actual_observable}")
159+
160+
if np.sum(predicted_observable != actual_observable) > 0:
161+
num_logical_errors += 1
162+
163+
# Non-batched API
164+
else:
165+
for i in range(num_shots):
166+
syndrome = j["trials"][i]["syndrome_truth"]
167+
obs_truth = j["trials"][i]["obs_truth"]
168+
169+
t0 = time.time()
170+
bp_converged, dec_result = nv_dec_gpu_and_cpu.decode(syndrome)
171+
t1 = time.time()
172+
trial_diff = t1 - t0
173+
decoding_time += trial_diff
174+
175+
dec_result = np.array(dec_result, dtype=np.uint8)
176+
bp_converged_flags.append(bp_converged)
177+
178+
# See if this prediction flipped the observable
179+
predicted_observable = obs_mat.T @ dec_result % 2
180+
print(f"predicted_observable: {predicted_observable}")
181+
182+
# See if the observable was actually flipped according to the truth
183+
# data
184+
actual_observable = np.array(obs_truth, dtype=np.uint8)
185+
print(f"actual_observable: {actual_observable}")
186+
187+
if np.sum(predicted_observable != actual_observable) > 0:
188+
num_logical_errors += 1
189+
190+
# Count how many shots the decoder failed to correct the errors
191+
print(f"{num_logical_errors} logical errors in {num_shots} shots")
192+
print(
193+
f"Number of shots that converged with BP processing: {np.sum(np.array(bp_converged_flags))}"
194+
)
195+
print(
196+
f"Average decoding time for {num_shots} shots was {1e3 * decoding_time / num_shots} ms per shot"
197+
)
198+
199+
200+
if __name__ == "__main__":
201+
# See other test data options in https://github.com/NVIDIA/cudaqx/releases/tag/0.2.0
202+
filename = 'osd_1008_8785_0.001.json'
203+
bz2filename = filename + '.bz2'
204+
if not os.path.exists(filename):
205+
url = f"https://github.com/NVIDIA/cudaqx/releases/download/0.2.0/{bz2filename}"
206+
207+
print(f'Downloading data from {url}')
208+
209+
# Download the file
210+
response = requests.get(url, stream=True)
211+
response.raise_for_status() # Raise an error if download fails
212+
with open(bz2filename, "wb") as f:
213+
for chunk in response.iter_content(chunk_size=8192):
214+
f.write(chunk)
215+
216+
print(f'Decompressing {bz2filename} into {filename}')
217+
218+
# Decompress the file
219+
with bz2.BZ2File(bz2filename, "rb") as f_in, open(filename,
220+
"wb") as f_out:
221+
f_out.write(f_in.read())
222+
223+
print(f"Decompressed file saved as {filename}")
224+
225+
num_shots = 100
226+
run_as_batched = True
227+
run_decoder(filename, num_shots, run_as_batched)

docs/sphinx/examples_rst/qec/circuit_level_noise.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,26 @@ additional ways to introduce errors into a quantum kernel:
123123
124124
For a full list of supported noise models and their parameters, refer to the `CUDA-Q documentation <https://nvidia.github.io/cuda-quantum/latest/index.html>`_.
125125

126+
Getting Started with the NVIDIA QLDPC Decoder
127+
+++++++++++++++++++++++++++++++++++++++++++++
128+
129+
Starting with CUDA-Q QEC v0.2, a GPU-accelerated decoder is included with the
130+
CUDA-Q QEC library. The library follows the CUDA-Q decoder Python and C++ interfaces
131+
(namely :class:`cudaq_qec.Decoder` for Python and
132+
:cpp:class:`cudaq::qec::decoder` for C++), but as documented in the API sections
133+
(:ref:`nv_qldpc_decoder_api_python` for Python and
134+
:ref:`nv_qldpc_decoder_api_cpp` for C++), there are many configuration options
135+
that can be passed to the constructor. The following example shows how to
136+
exercise the decoder using non-trivial pre-generated test data. The test data
137+
was generated using scripts originating from the GitHub repo for
138+
`BivariateBicycleCodes
139+
<https://github.com/sbravyi/BivariateBicycleCodes>`_ [#f1]_; it includes parity
140+
check matrices (PCMs) and test syndromes to exercise a decoder.
141+
142+
.. literalinclude:: ../../examples/qec/python/nv-qldpc-decoder.py
143+
:language: python
144+
:start-after: [Begin Documentation]
145+
146+
.. rubric:: Footnotes
147+
148+
.. [#f1] [BCGMRY] Sergey Bravyi, Andrew Cross, Jay Gambetta, Dmitri Maslov, Patrick Rall, Theodore Yoder, High-threshold and low-overhead fault-tolerant quantum memory https://arxiv.org/abs/2308.07915

docs/sphinx/examples_rst/qec/code_capacity_noise.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ Code Explanation
7474
- Since we are using the `Z` parity check matrix `H_Z`, we want to simulate random `X` errors on our 7 data qubits.
7575

7676
4. Logical Errors:
77-
- Once we have noisy data, we see what the resuling syndromes are by multiplying our noisy data vector with our parity check matrix (mod 2).
77+
- Once we have noisy data, we see what the resulting syndromes are by multiplying our noisy data vector with our parity check matrix (mod 2).
7878
- From this syndrome, we see what the decoder predicts what errors occurred in the data.
7979
- To classify as a logical error, the decoder does not need to exactly guess what happened to the data, but if there was a flip in the logical observable or not.
8080
- If the decoder guesses this successfully, we have corrected the quantum error. If not, we have incurred a logical error.

0 commit comments

Comments
 (0)