External compiler pass plugins

cudaq-opt can load a custom MLIR operation pass from a shared library. This keeps the pass outside CUDA-Q’s built-in pass catalog and production pipelines while allowing it to transform CUDA-Q IR with the same MLIR APIs used by built-in passes.

CUDA-Q currently builds and tests pass plugins within a CUDA-Q development build. A plugin must use CUDA-Q, LLVM, and MLIR headers and libraries compatible with the cudaq-opt binary that loads it. Rebuild the plugin when those dependencies change.

Implement and register the pass

A plugin pass is an MLIR operation pass with a textual argument. Include cudaq/Support/Plugin.h and place CUDAQ_REGISTER_MLIR_PASS at file scope after the pass definition. The macro exports the CUDA-Q plugin entry point and registers one default-constructible pass.

The complete example below is included from the source compiled by the plugin test target. It replaces each quake.h operation with quake.s so that the regression can observe the transformation. This example demonstrates plugin registration and does not preserve circuit semantics.

/*******************************************************************************
 * Copyright (c) 2022 - 2026 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/Optimizer/Dialect/Quake/QuakeOps.h"
#include "cudaq/Optimizer/Transforms/CommutationAwareRewrite.h"
#include "cudaq/Support/Plugin.h"

// Here is an example MLIR Pass that one can write externally and
// use via the cudaq-opt tool, with the --load-cudaq-plugin flag.
// The pass here is simple, replace Hadamard operations with S operations.

using namespace mlir;

namespace {

struct ReplaceH : public OpRewritePattern<cudaq::quake::HOp> {
  using OpRewritePattern::OpRewritePattern;
  LogicalResult matchAndRewrite(cudaq::quake::HOp hOp,
                                PatternRewriter &rewriter) const override {
    rewriter.replaceOpWithNewOp<cudaq::quake::SOp>(
        hOp, hOp.isAdj(), hOp.getParameters(), hOp.getControls(),
        hOp.getTargets());
    return success();
  }
};

class CustomPassPlugin
    : public PassWrapper<CustomPassPlugin, OperationPass<func::FuncOp>> {
public:
  MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(CustomPassPlugin)

  llvm::StringRef getArgument() const override { return "cudaq-custom-pass"; }

  void runOnOperation() override {
    auto circuit = getOperation();
    auto ctx = circuit.getContext();

    cudaq::opt::CommutationAwareRewriteDriver driver(*ctx);
    driver.get_patterns().add<ReplaceH>(ctx);
    if (failed(driver.run(circuit.getBody()))) {
      circuit.emitOpError("simple pass failed");
      signalPassFailure();
    }
  }
};

} // namespace

CUDAQ_REGISTER_MLIR_PASS(CustomPassPlugin)

Build the plugin

The tested CMake target uses LLVM’s pass-plugin helper and depends on the generated Quake dialect headers:

# ============================================================================ #
# Copyright (c) 2022 - 2026 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(HandleLLVMOptions)
add_llvm_pass_plugin(CustomPassPlugin CustomPassPlugin.cpp)
# Depends on QuakeDialect TableGen to use the generated `.h.inc` files.
add_dependencies(CustomPassPlugin QuakeDialect)
# Link no CUDA-Q or MLIR library: cudaq-opt links MLIR/LLVM statically and
# re-exports it for plugins, so every symbol resolves from the host. Linking
# libcudaqMLIR would pull a second copy of LLVM's globals into the process and
# abort on duplicate cl::opt registration.

Build that target from a configured CUDA-Q build tree:

cmake --build build --target CustomPassPlugin

Load and test the plugin

Load the shared library before naming its registered pass. This Linux command uses the paths produced by the in-tree build:

build/bin/cudaq-opt input.qke \
  --load-cudaq-plugin build/lib/CustomPassPlugin.so \
  --cudaq-custom-pass

A corresponding regression test loads the plugin into cudaq-opt and checks the transformed IR. The pass is registered only for that invocation and is not added to a CUDA-Q compilation pipeline.