Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
72fee8e
initial commit
multiphaseCFD Apr 21, 2026
ff9b619
Add steps needed for the decoder implementation
multiphaseCFD Apr 21, 2026
3539b73
recover dense matrix from the csc representation
multiphaseCFD Apr 21, 2026
8ef5f3f
make format
multiphaseCFD Apr 22, 2026
7ecc331
create a decoder module in runtime lib
multiphaseCFD Apr 22, 2026
d4370ef
move decoder implementation to a separate dir
multiphaseCFD Apr 22, 2026
10962f8
make format + add limitations
multiphaseCFD Apr 23, 2026
b19ddd7
update the Util funcs
multiphaseCFD Apr 23, 2026
ebf3b12
update implementation
multiphaseCFD Apr 23, 2026
99dc602
rename path
multiphaseCFD Apr 23, 2026
d8de11f
change the return type to limit the scope of decoder for Steane code …
multiphaseCFD Apr 23, 2026
b0f35b9
rename some source/header file
multiphaseCFD Apr 23, 2026
d954cbb
add skeleton for unit tests
multiphaseCFD Apr 23, 2026
25115db
Add unit tests for convert_syndrome_res_to_bitstr
multiphaseCFD Apr 23, 2026
cea66f5
Add unit tests for get_parity_check_matrix
multiphaseCFD Apr 23, 2026
92aed5f
add a test for get_syndrome_from_errors and make format
multiphaseCFD Apr 23, 2026
66c26e4
Add more tests for get_syndrome_from_errors
multiphaseCFD Apr 23, 2026
53973dd
fix permutation in generate_lookup_table and add more tests
multiphaseCFD Apr 23, 2026
3fc147b
update unit tests for the __catalyst__qecp__lut_decoder
multiphaseCFD Apr 24, 2026
b747a17
update __catalyst__qecp__lut_decoder implementation and add unit tests
multiphaseCFD Apr 24, 2026
76abeff
tidy up the unit tests
multiphaseCFD Apr 24, 2026
a53aaef
make format
multiphaseCFD Apr 24, 2026
8750e10
update the runtime signature
multiphaseCFD Apr 24, 2026
ee02e7a
refactor the code
multiphaseCFD Apr 24, 2026
ff66dab
ensure lut built once using static
multiphaseCFD Apr 24, 2026
0cb82de
replace vector with dataview
multiphaseCFD Apr 24, 2026
42415d6
replace some vector with dataview
multiphaseCFD Apr 24, 2026
cced6cc
further tidy up the code
multiphaseCFD Apr 24, 2026
0af182a
tidy cmake file
multiphaseCFD Apr 24, 2026
e17f6c8
fix cf
multiphaseCFD Apr 24, 2026
6f4490a
refactor LUTs to singleton pattern
multiphaseCFD Apr 25, 2026
2a85422
update docstr
multiphaseCFD Apr 25, 2026
05e3dfa
make format
multiphaseCFD Apr 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions runtime/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ ifeq ($(ENABLE_ASAN), ON)
ASAN_COMMAND = $(ASAN_FLAGS)
endif

BUILD_TARGETS := rt_capi rtd_null_qubit rt_rsdecomp decompsolver
TEST_TARGETS := runner_tests_qir_runtime runner_tests_mbqc_runtime runner_tests_rsdecomp_runtime runner_tests_decompsolver
BUILD_TARGETS := rt_capi rtd_null_qubit rt_rsdecomp decompsolver rt_decoder
TEST_TARGETS := runner_tests_qir_runtime runner_tests_mbqc_runtime runner_tests_rsdecomp_runtime runner_tests_decompsolver runner_tests_decoder_runtime

ifeq ($(ENABLE_OPENQASM), ON)
BUILD_TARGETS += rtd_openqasm
Expand Down
1 change: 1 addition & 0 deletions runtime/lib/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ add_subdirectory(backend)
add_subdirectory(registry)
add_subdirectory(DecompGraphSolver)
add_subdirectory(RSDecompRuntime)
add_subdirectory(Decoder)

if(ENABLE_OQD)
add_subdirectory(OQDcapi)
Expand Down
12 changes: 12 additions & 0 deletions runtime/lib/Decoder/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_library(rt_decoder SHARED LUTDecoder.cpp)

target_include_directories(rt_decoder
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/../../include
)

set_property(TARGET rt_decoder PROPERTY POSITION_INDEPENDENT_CODE ON)
75 changes: 75 additions & 0 deletions runtime/lib/Decoder/LUTDecoder.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2026 Xanadu Quantum Technologies Inc.

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

// http://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "LUTDecoder.hpp"

#include <algorithm>
#include <numeric>
#include <ranges>
#include <vector>

#include "DataView.hpp"
#include "LUTDecoderUtils.hpp"

namespace Catalyst::Runtime::QEC {
/**
* @brief A runtime lookup table based decoder.
*
* NOTE: As CAPI does not support setting default values for args, as discussed, we hardcode the
* required args in the beginning of the function body. Those values are specifically for the [[7,
* 1, 3]] Steane code. We expect those values are from args inputs later.
* @param row_idx_tanner Pointer to the row_idx data of a Tanner graph.
* @param col_ptr_tanner Pointer to the col_ptr data of a Tanner graph.
* @param syndrome_results Pointer to the syndrome measurement data.
* @param err_idx Pointer to the error qubit indices data.
*/
void __catalyst__qecp__lut_decoder(MemRefT_int64_1d *row_idx_tanner,
MemRefT_int64_1d *col_ptr_tanner,
MemRefT_int8_1d *current_syndromes, MemRefT_int64_1d *err_idx)
{
// TODOs: We should expect the following const value from args.
// The default values here only work for the [[7, 1, 3]] Steane code.
const size_t code_size = 7;
const size_t code_distance = 3;
// The following parameter depends on the design choice of tanner graph would
// change.
const size_t aux_col_offset = 7;

DataView<int64_t, 1> row_idx(row_idx_tanner->data_aligned, row_idx_tanner->offset,
row_idx_tanner->sizes, row_idx_tanner->strides);
DataView<int64_t, 1> col_ptr(col_ptr_tanner->data_aligned, col_ptr_tanner->offset,
col_ptr_tanner->sizes, col_ptr_tanner->strides);

auto current_lut =
LUTs::getInstance().get_lut(aux_col_offset, code_size, code_distance, row_idx, col_ptr);

DataView<int8_t, 1> syndromes_res(current_syndromes->data_aligned, current_syndromes->offset,
current_syndromes->sizes, current_syndromes->strides);

auto syndrome_str = convert_syndrome_res_to_bitstr<int8_t>(syndromes_res);

std::vector<int64_t> error_indices = current_lut[syndrome_str];

// We use `-1` to full fill the err_idx array if the number of
// errors is less than (code_distance - 1)/2
for (size_t i = 0; i < (code_distance - 1) / 2; i++) {
if (i < error_indices.size()) {
err_idx->data_allocated[i] = error_indices[i];
}
else {
err_idx->data_allocated[i] = -1;
}
}
}
} // namespace Catalyst::Runtime::QEC
29 changes: 29 additions & 0 deletions runtime/lib/Decoder/LUTDecoder.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2026 Xanadu Quantum Technologies Inc.

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

// http://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once

#include "Types.h"

namespace Catalyst::Runtime::QEC {

extern "C" {

void __catalyst__qecp__lut_decoder(/*row_idx*/ MemRefT_int64_1d *row_idx_tanner,
/*col_ptr*/ MemRefT_int64_1d *col_ptr_tanner,
/*syndrome*/ MemRefT_int8_1d *syndrome_res,
/*err_idx*/ MemRefT_int64_1d *err_idx);

} // extern "C"
} // namespace Catalyst::Runtime::QEC
255 changes: 255 additions & 0 deletions runtime/lib/Decoder/LUTDecoderUtils.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
// Copyright 2026 Xanadu Quantum Technologies Inc.

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

// http://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once
#include <algorithm>
#include <mutex>
#include <numeric>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

#include "DataView.hpp"
#include "Exception.hpp"
#include "Types.h"

namespace Catalyst::Runtime::QEC {
/**
* @brief Convert a vector of syndrome results to a bit string representation.
*
* @tparam IntegerType
* @param syndrome_res A dataview of syndrome results.
* @return std::string A bit string representation of the given syndrome results.
*/
template <typename T = int8_t>
std::string convert_syndrome_res_to_bitstr(DataView<T, 1> &syndrome_res)
{
std::string syndrom_str;
for (const auto &bit : syndrome_res) {
RT_ASSERT(bit == 0 || bit == 1)
syndrom_str += (bit ? '1' : '0');
}

// Return results
return syndrom_str;
}

/**
* @brief Get a parity check matrix from Tanner graph data.
*
* NOTE: With the current design of Tanner graph operation in MLIR, the first $n$ or $code_size$
* columns represent the physical data qubits, while the last $n-1$ columns represent auxillary
* qubits (more details here
* https://github.com/PennyLaneAI/catalyst/blob/ab97f982539b31ab802a63020292595476f22d15/mlir/include/QecPhysical/IR/QecPhysicalTypes.td).
*
* @param tanner_row_idx The dataview of row indices of non-zero elements with a length of $nnz$.
* @param tanner_col_ptr The column offsets dataview of a length number of $num_col + 1$ that
* represents the starting position of each column.
* @param aux_cols A vector of column indices for the corresponding type of auxillary qubits.
*
* @return std::pair<std::vector<int64_t>, std::vector<int64_t>> The corresponding parity check
* matrix in the CSS format. Each column represents an auxillary qubit.
*/

std::pair<std::vector<int64_t>, std::vector<int64_t>>
get_parity_check_matrix(DataView<int64_t, 1> &tanner_row_idx, DataView<int64_t, 1> &tanner_col_ptr,
const std::vector<size_t> &aux_cols)
{
std::vector<int64_t> row_idx_parity;
std::vector<int64_t> col_ptr_parity{0};

for (const auto &col : aux_cols) {
auto offset_start = tanner_col_ptr(col);
auto offset_end = tanner_col_ptr(col + 1);

for (int i = offset_start; i < offset_end; i++) {
row_idx_parity.push_back(tanner_row_idx(i));
}
size_t new_offset = col_ptr_parity.back() + offset_end - offset_start;
col_ptr_parity.push_back(new_offset);
}
return {row_idx_parity, col_ptr_parity};
}

/**
* @brief Get the bit representation of a syndrome from errors object
* The syndrome $s$ is calculated using a CSC parity check matrix $H$ and the
* error vector $e$ according to the linear relation:
*
* $$s = He \pmod 2$$
*
* @param row_idx The row_idx vector of $H$.
* @param col_ptr The col_ptr vector of $H$.
* @param num_rows Number of rows of $H$.
* @param num_cols Number of columns of $H$.

Check notice on line 96 in runtime/lib/Decoder/LUTDecoderUtils.hpp

View check run for this annotation

codefactor.io / CodeFactor

runtime/lib/Decoder/LUTDecoderUtils.hpp#L96

Redundant blank line at the start of a code block should be deleted. (whitespace/blank_line)
* @param err_vec A vector of qubit errors.
* @return std::string The syndrome string corresponds to the err_vec.
*/
std::string get_syndrome_from_errors(const std::vector<int64_t> &row_idx,
const std::vector<int64_t> &col_ptr, const size_t num_rows,
const size_t num_cols, std::vector<uint8_t> &err_vec)
{

std::vector<size_t> syndrome_res(num_cols, 0);

for (size_t col = 0; col < num_cols; col++) {
for (size_t idx = col_ptr[col]; idx < col_ptr[col + 1]; idx++) {
size_t row = row_idx[idx];
syndrome_res[col] += err_vec[row];
}
syndrome_res[col] = syndrome_res[col] % 2;
}
DataView<size_t, 1> syndrome_res_data_view(syndrome_res);

return convert_syndrome_res_to_bitstr<size_t>(syndrome_res_data_view);
}

/**
* @brief Get the error indices of a vector of qubit errors.
*
* @param err_vec A vector of qubit errors.
* @return std::vector<int64_t> Indices of qubit errors.
*/
std::vector<int64_t> get_error_indices(std::vector<uint8_t> &err_vec)
{
std::vector<int64_t> error_indices;

error_indices.reserve(err_vec.size());

for (size_t i = 0; i < err_vec.size(); ++i) {
if (err_vec[i] != 0) {
error_indices.push_back(i);
}
}

return error_indices;
}

/**
* @brief Generates a look up table with a CSC parity check matrix $H$ and QEC code information.
*
* NOTE: Note that this function has a combinatorial time complexity of $O(n^k)$, where $n$
* represents the number of data qubits and $k$ represents the maximum error weight. Consequently,
* it is computationally intractable for large-scale codes.
*
* @param parity_mat_row_idx The row vector of length nnz that contains row indices of the
* corresponding elements. Each column corresponds to an auxillary qubit.
* @param parity_mat_col_ptr The column offsets vector of length number of num_col + 1 that
* represents the starting position of each row.
* @param code_size The number of data qubits in the QEC code. This param is for safe guard only
* purpose.
* @param code_distance The code distance, which represents the number of quantum errors can be
* corrected.
* @return std::unordered_map<std::string, std::vector<size_t>>& The result lookup table.
*/
std::unordered_map<std::string, std::vector<int64_t>>
generate_lookup_table(const std::vector<int64_t> &parity_mat_row_idx,
const std::vector<int64_t> &parity_mat_col_ptr, const size_t code_size,
const size_t code_distance)
{
// The key here is the bitstr representation of the syndrome results, e.g., "0101"
// The value is the corresponding indices of qubits to correct, e.g., {0, 2}.
std::unordered_map<std::string, std::vector<int64_t>> lut;

const size_t nnz = parity_mat_row_idx.size();
const size_t num_aux_qubits =
parity_mat_col_ptr.size() - 1; // number of cols or number of auxillary qubits
const size_t num_data_qubits =
*std::max_element(parity_mat_row_idx.begin(), parity_mat_row_idx.end()) +
1; // number of rows or number of data qubits

RT_ASSERT(num_aux_qubits == (code_size - 1) >> 1);
RT_ASSERT(nnz > 0);
RT_ASSERT(num_data_qubits == code_size);

// Get number of errors can be detected from code distance
const size_t num_errors = (code_distance - 1) / 2;

// Traverse all possible quantum error combinations
for (int i = 0; i <= num_errors; i++) {
// create a base error vector
std::vector<uint8_t> err_vector(num_data_qubits, 0);
std::fill(err_vector.end() - i, err_vector.end(), 1);

do {
std::string syndrome_str =
get_syndrome_from_errors(parity_mat_row_idx, parity_mat_col_ptr, num_data_qubits,
num_aux_qubits, err_vector);
std::vector<int64_t> error_indices = get_error_indices(err_vector);
// We assume that 1:1 mapping for the syndrome and err_vector
lut[syndrome_str] = error_indices;
} while (std::next_permutation(err_vector.begin(), err_vector.end()));
}

return lut;
}

class LUTs final {
private:
std::unordered_map<size_t, std::unordered_map<std::string, std::vector<int64_t>>> luts_;

mutable std::mutex mutex_;

explicit LUTs() = default;

public:
LUTs(const LUTs &) = delete;
LUTs &operator=(const LUTs &) = delete;
LUTs(LUTs &&) = delete;
LUTs &operator=(LUTs &&) = delete;

static auto getInstance() -> LUTs &
{
static LUTs instance;
return instance;
}

/**
* @brief Get a lookup table.
*
* @param aux_col_offset The offset of the first X-check or Z-check column in a Tanner graph.
* @param code_size Number of data qubits in a QEC code.
* @param code_distance Code distance of a QEC code.
* @param row_idx Dataview of the row_idx of a Tanner graph.
* @param col_ptr Dataview of the col_ptr of a Tanner graph.
* @return const std::unordered_map<std::string, std::vector<int64_t>>& The corresponding lookup
* table.
*/
auto get_lut(size_t aux_col_offset, size_t code_size, size_t code_distance,
DataView<int64_t, 1> &row_idx, DataView<int64_t, 1> &col_ptr)
-> const std::unordered_map<std::string, std::vector<int64_t>> &
{
std::lock_guard<std::mutex> lock(mutex_);

auto it = luts_.find(aux_col_offset);

if (it == luts_.end()) {
std::vector<size_t> aux_cols((code_size - 1) / 2);
std::iota(aux_cols.begin(), aux_cols.end(), aux_col_offset);

auto csc_parity_matrix = get_parity_check_matrix(row_idx, col_ptr, aux_cols);

auto lut = generate_lookup_table(csc_parity_matrix.first, csc_parity_matrix.second,
code_size, code_distance);

luts_[aux_col_offset] = std::move(lut);
return luts_[aux_col_offset];
}

return it->second;
}
};

} // namespace Catalyst::Runtime::QEC
Loading
Loading