diff --git a/libs/qec/include/cudaq/qec/plugin_loader.h b/libs/qec/include/cudaq/qec/plugin_loader.h new file mode 100644 index 0000000..280b1f1 --- /dev/null +++ b/libs/qec/include/cudaq/qec/plugin_loader.h @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2024 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +#ifndef PLUGIN_LOADER_H +#define PLUGIN_LOADER_H + +#include +#include +#include +#include + +/// @brief Enum to define different types of plugins +enum class PluginType { + DECODER, // Decoder plugins + CODE // QEC codes plugins + // Add other plugin types here as needed +}; + +/// @brief A struct to store plugin handle with its type +struct PluginHandle { + std::shared_ptr handle; // Pointer to the shared library handle. This is + // the result of dlopen() function. + PluginType type; // Type of the plugin (e.g., decoder, code, etc) +}; + +/// @brief Function to load plugins from a directory based on type +/// @param plugin_dir The directory where the plugins are located +/// @param type The type of plugins to load. Only plugins of this type will be +/// loaded. +void load_plugins(const std::string &plugin_dir, PluginType type); + +/// @brief Function to clean up loaded plugins of a specific type +/// @param type The type of plugins to clean up. Only plugins of this type will +/// be cleaned up. +void cleanup_plugins(PluginType type); + +#endif // PLUGIN_LOADER_H diff --git a/libs/qec/lib/CMakeLists.txt b/libs/qec/lib/CMakeLists.txt index 420efe2..55b5433 100644 --- a/libs/qec/lib/CMakeLists.txt +++ b/libs/qec/lib/CMakeLists.txt @@ -9,6 +9,7 @@ set(LIBRARY_NAME cudaq-qec) add_compile_options(-Wno-attributes) +add_compile_definitions(DECODER_PLUGIN_DIR="${CMAKE_INSTALL_PREFIX}/lib/decoder-plugins") # FIXME?: This must be a shared library. Trying to build a static one will fail. add_library(${LIBRARY_NAME} SHARED @@ -17,8 +18,10 @@ add_library(${LIBRARY_NAME} SHARED decoder.cpp experiments.cpp decoders/single_error_lut.cpp + plugin_loader.cpp ) +add_subdirectory(decoders/plugins/example) add_subdirectory(codes) add_subdirectory(device) diff --git a/libs/qec/lib/decoder.cpp b/libs/qec/lib/decoder.cpp index 9da1a07..9f4cc56 100644 --- a/libs/qec/lib/decoder.cpp +++ b/libs/qec/lib/decoder.cpp @@ -7,7 +7,10 @@ ******************************************************************************/ #include "cudaq/qec/decoder.h" +#include "cudaq/qec/plugin_loader.h" #include +#include +#include #include INSTANTIATE_REGISTRY(cudaq::qec::decoder, const cudaqx::tensor &) @@ -71,3 +74,15 @@ std::unique_ptr get_decoder(const std::string &name, return decoder::get(name, H, options); } } // namespace cudaq::qec + +// Constructor function for auto-loading plugins +__attribute__((constructor)) void load_decoder_plugins() { + // Load plugins from the decoder-specific plugin directory + load_plugins(DECODER_PLUGIN_DIR, PluginType::DECODER); +} + +// Destructor function to clean up only decoder plugins +__attribute__((destructor)) void cleanup_decoder_plugins() { + // Clean up decoder-specific plugins + cleanup_plugins(PluginType::DECODER); +} \ No newline at end of file diff --git a/libs/qec/lib/decoders/plugins/example/CMakeLists.txt b/libs/qec/lib/decoders/plugins/example/CMakeLists.txt new file mode 100644 index 0000000..2cb1ea3 --- /dev/null +++ b/libs/qec/lib/decoders/plugins/example/CMakeLists.txt @@ -0,0 +1,72 @@ +# ============================================================================ # +# Copyright (c) 2024 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # + +cmake_minimum_required(VERSION 3.28 FATAL_ERROR) + +set(MODULE_NAME "cudaq-qec-example") + +project(${MODULE_NAME}) + +# Specify the source file for the plugin +set(PLUGIN_SRC + single_error_lut_example.cpp + # single_error_lut_example2.cpp // add other decoder source files here +) + +# Create the shared library +add_library(${MODULE_NAME} SHARED ${PLUGIN_SRC}) + +# Set the include directories for dependencies +target_include_directories(${MODULE_NAME} + PUBLIC + ${CMAKE_SOURCE_DIR}/libs/qec/include + ${CMAKE_SOURCE_DIR}/libs/core/include +) + +# Link with required libraries +target_link_libraries(${MODULE_NAME} + PUBLIC + cudaqx-core + cudaq::cudaq + cudaq::cudaq-spin + PRIVATE + cudaq::cudaq-common + cudaq-qec +) + +set_target_properties(${MODULE_NAME} PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/decoder-plugins +) + +# RPATH configuration +# ============================================================================== + +if (NOT SKBUILD) + set_target_properties(${LIBRARY_NAME} PROPERTIES + BUILD_RPATH "$ORIGIN" + INSTALL_RPATH "$ORIGIN:$ORIGIN/.." + ) + + # Let CMake automatically add paths of linked libraries to the RPATH: + set_target_properties(${LIBRARY_NAME} PROPERTIES + INSTALL_RPATH_USE_LINK_PATH TRUE) +else() + # CUDA-Q install its libraries in site-packages/lib (or dist-packages/lib) + # Thus, we need the $ORIGIN/../lib + set_target_properties(${LIBRARY_NAME} PROPERTIES + INSTALL_RPATH "$ORIGIN/../../lib" + ) +endif() + +# Install +# ============================================================================== + +install(TARGETS ${MODULE_NAME} + COMPONENT qec-lib-plugins + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/decoder-plugins +) diff --git a/libs/qec/lib/decoders/plugins/example/decoder_plugins_demo.cpp b/libs/qec/lib/decoders/plugins/example/decoder_plugins_demo.cpp new file mode 100644 index 0000000..f6149ac --- /dev/null +++ b/libs/qec/lib/decoders/plugins/example/decoder_plugins_demo.cpp @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) 2024 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +// This example shows how to use decoders from decoder plugins +// +// Compile and run with +// nvq++ --enable-mlir -lcudaq-qec decoder_plugins_demo.cpp -o +// decoder_plugins_demo +// ./decoder_plugins_demo + +#include +#include +#include +#include +#include +#include + +#include "cudaq.h" +#include "cudaq/qec/decoder.h" +#include "cudaq/qec/experiments.h" + +int main() { + auto steane = cudaq::qec::get_code("steane"); + auto Hz = steane->get_parity_z(); + std::vector t_shape = Hz.shape(); + + std::cout << "Hz.shape():\n"; + for (size_t elem : t_shape) + std::cout << elem << " "; + std::cout << "\n"; + + std::cout << "Hz:\n"; + Hz.dump(); + + auto Lz = steane->get_observables_x(); + std::cout << "Lz:\n"; + Lz.dump(); + + double p = 0.2; + size_t nShots = 5; + + // Check for available decoders + for (auto &name : cudaq::qec::decoder::get_registered()) + printf("Decoder: %s\n", name.c_str()); + // create a decoder from the plugins + auto lut_decoder = cudaq::qec::get_decoder("single_error_lut_example", Hz); + + std::cout << "nShots: " << nShots << "\n"; + + // May want a order-2 tensor of syndromes + // access tensor by stride to write in an entire syndrome + cudaqx::tensor syndrome({Hz.shape()[0]}); + + int nErrors = 0; + for (size_t shot = 0; shot < nShots; ++shot) { + std::cout << "shot: " << shot << "\n"; + auto shot_data = cudaq::qec::generate_random_bit_flips(Hz.shape()[1], p); + std::cout << "shot data\n"; + shot_data.dump(); + + auto observable_z_data = Lz.dot(shot_data); + observable_z_data = observable_z_data % 2; + std::cout << "Data Lz state:\n"; + observable_z_data.dump(); + + auto syndrome = Hz.dot(shot_data); + syndrome = syndrome % 2; + std::cout << "syndrome:\n"; + syndrome.dump(); + + auto [converged, v_result] = lut_decoder->decode(syndrome); + cudaqx::tensor result_tensor; + // v_result is a std::vector, of soft information. We'll convert + // this to hard information and store as a tensor. + cudaq::qec::convert_vec_soft_to_tensor_hard(v_result, result_tensor); + std::cout << "decode result:\n"; + result_tensor.dump(); + + // check observable result + auto decoded_observable_z = Lz.dot(result_tensor); + std::cout << "decoded observable:\n"; + decoded_observable_z.dump(); + + // check how many observable operators were decoded correctly + // observable_z_data == decoded_observable_z This maps onto element wise + // addition (mod 2) + auto observable_flips = decoded_observable_z + observable_z_data; + observable_flips = observable_flips % 2; + std::cout << "Logical errors:\n"; + observable_flips.dump(); + std::cout << "\n"; + + // shot counts as a observable error unless all observables are correct + if (observable_flips.any()) { + nErrors++; + } + } + std::cout << "Total logical errors: " << nErrors << "\n"; + + // Full data gen in function call + auto [syn, data] = cudaq::qec::sample_code_capacity(Hz, nShots, p); + std::cout << "Numerical experiment:\n"; + std::cout << "Data:\n"; + data.dump(); + std::cout << "Syn:\n"; + syn.dump(); +} diff --git a/libs/qec/lib/decoders/plugins/example/single_error_lut_example.cpp b/libs/qec/lib/decoders/plugins/example/single_error_lut_example.cpp new file mode 100644 index 0000000..1df26ef --- /dev/null +++ b/libs/qec/lib/decoders/plugins/example/single_error_lut_example.cpp @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2024 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +#include "cudaq/qec/decoder.h" +#include +#include +#include + +namespace cudaq::qec { + +/// @brief This is a simple LUT (LookUp Table) decoder that demonstrates how to +/// build a simple decoder that can correctly decode errors during a single bit +/// flip in the block. +class single_error_lut_example : public decoder { +private: + std::map single_qubit_err_signatures; + +public: + single_error_lut_example(const cudaqx::tensor &H, + const cudaqx::heterogeneous_map ¶ms) + : decoder(H) { + // Decoder-specific constructor arguments can be placed in `params`. + + // Build a lookup table for an error on each possible qubit + + // For each qubit with a possible error, calculate an error signature. + for (std::size_t qErr = 0; qErr < block_size; qErr++) { + std::string err_sig(syndrome_size, '0'); + for (std::size_t r = 0; r < syndrome_size; r++) { + bool syndrome = 0; + // Toggle syndrome on every "1" entry in the row. + // Except if there is an error on this qubit (c == qErr). + for (std::size_t c = 0; c < block_size; c++) + syndrome ^= (c != qErr) && H.at({r, c}); + err_sig[r] = syndrome ? '1' : '0'; + } + // printf("Adding err_sig=%s for qErr=%lu\n", err_sig.c_str(), qErr); + single_qubit_err_signatures.insert({err_sig, qErr}); + } + } + + virtual decoder_result decode(const std::vector &syndrome) { + // This is a simple decoder that simply results + decoder_result result{false, std::vector(block_size, 0.0)}; + + // Convert syndrome to a string + std::string syndrome_str(syndrome.size(), '0'); + assert(syndrome_str.length() == syndrome_size); + bool anyErrors = false; + for (std::size_t i = 0; i < syndrome_size; i++) { + if (syndrome[i] >= 0.5) { + syndrome_str[i] = '1'; + anyErrors = true; + } + } + + if (!anyErrors) { + result.converged = true; + return result; + } + + auto it = single_qubit_err_signatures.find(syndrome_str); + if (it != single_qubit_err_signatures.end()) { + assert(it->second < block_size); + result.converged = true; + result.result[it->second] = 1.0; + } else { + // Leave result.converged set to false. + } + + return result; + } + + virtual ~single_error_lut_example() {} + + CUDAQ_EXTENSION_CUSTOM_CREATOR_FUNCTION( + single_error_lut_example, static std::unique_ptr create( + const cudaqx::tensor &H, + const cudaqx::heterogeneous_map ¶ms) { + return std::make_unique(H, params); + }) +}; + +CUDAQ_REGISTER_TYPE(single_error_lut_example) + +} // namespace cudaq::qec diff --git a/libs/qec/lib/plugin_loader.cpp b/libs/qec/lib/plugin_loader.cpp new file mode 100644 index 0000000..35a69f5 --- /dev/null +++ b/libs/qec/lib/plugin_loader.cpp @@ -0,0 +1,59 @@ +/****************************************************************-*- C++ -*-**** + * Copyright (c) 2024 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +#include "cudaq/qec/plugin_loader.h" +#include +#include + +namespace fs = std::filesystem; + +static std::map &get_plugin_handles() { + static std::map plugin_handles; + return plugin_handles; +} + +// Function to load plugins from a directory based on their type +void load_plugins(const std::string &plugin_dir, PluginType type) { + if (!fs::exists(plugin_dir)) { + std::cerr << "WARNING: Plugin directory does not exist: " << plugin_dir + << std::endl; + return; + } + for (const auto &entry : fs::directory_iterator(plugin_dir)) { + if (entry.path().extension() == ".so") { + void *raw_handle = dlopen(entry.path().c_str(), RTLD_NOW); + if (raw_handle) { + // Custom deleter ensures dlclose is called + auto deleter = [](void *h) { + if (h) + dlclose(h); + }; + + get_plugin_handles().emplace( + entry.path().filename().string(), + PluginHandle{std::shared_ptr(raw_handle, deleter), type}); + } else { + std::cerr << "ERROR: Failed to load plugin: " << entry.path() + << " Error: " << dlerror() << std::endl; + } + } + } +} + +// Function to clean up the plugin handles +void cleanup_plugins(PluginType type) { + auto &handles = get_plugin_handles(); + auto it = handles.begin(); + while (it != handles.end()) { + if (it->second.type == type) { + it = handles.erase(it); // dlclose is handled by the custom deleter + } else { + ++it; + } + } +} diff --git a/libs/qec/python/bindings/py_decoder.cpp b/libs/qec/python/bindings/py_decoder.cpp index 8b6d591..11b6789 100644 --- a/libs/qec/python/bindings/py_decoder.cpp +++ b/libs/qec/python/bindings/py_decoder.cpp @@ -14,6 +14,7 @@ #include "common/Logger.h" #include "cudaq/qec/decoder.h" +#include "cudaq/qec/plugin_loader.h" #include "type_casters.h" #include "utils.h" @@ -70,6 +71,13 @@ std::unordered_map() diff --git a/libs/qec/python/tests/test_decoder.py b/libs/qec/python/tests/test_decoder.py index 59e1cde..e889042 100644 --- a/libs/qec/python/tests/test_decoder.py +++ b/libs/qec/python/tests/test_decoder.py @@ -40,6 +40,22 @@ def test_decoder_result_structure(): assert len(result.result) == 10 +def test_decoder_plugin_initialization(): + decoder = qec.get_decoder('single_error_lut_example', H) + assert decoder is not None + assert hasattr(decoder, 'decode') + + +def test_decoder_plugin_result_structure(): + decoder = qec.get_decoder('single_error_lut_example', H) + result = decoder.decode(create_test_syndrome()) + + assert hasattr(result, 'converged') + assert hasattr(result, 'result') + assert isinstance(result.converged, bool) + assert isinstance(result.result, list) + + def test_decoder_result_values(): decoder = qec.get_decoder('example_byod', H) result = decoder.decode(create_test_syndrome())