From 66dfb169fb04e03b8868c346aca1ef7a581943fe Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Fri, 17 Jul 2026 11:27:08 -0400 Subject: [PATCH 1/4] add support for ids --- .../DecompGraphSolver/DGTypes.hpp | 37 +++++++++++++++++-- .../DecompGraphSolver/DGUtils.hpp | 5 +++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGTypes.hpp b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGTypes.hpp index 9734285415..db092e74c8 100644 --- a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGTypes.hpp +++ b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGTypes.hpp @@ -20,7 +20,9 @@ */ #pragma once +#include #include +#include #include #include #include @@ -58,9 +60,16 @@ struct OperatorNode { // Optional static arguments for operators that require additional data. std::unordered_map staticNamedArgs{}; + std::string id{""}; bool operator==(const OperatorNode &other) const { + // id match + if (!id.empty() && !other.id.empty() && id == other.id) { + return true; + } + // legacy fallback if either op is missing ID + // For equality, we consider numWires and numParams conditionally equal // if they are not set to -1 (which indicates a wildcard that can match any value). const bool default_wires = @@ -98,6 +107,10 @@ struct OperatorNode { struct OperatorNodeHash { std::size_t operator()(const OperatorNode &node) const { + // prefer id if available + if (!node.id.empty()) { + return std::hash{}(node.id); + } return std::hash{}(node.name); } }; @@ -108,7 +121,23 @@ struct OperatorNodeHash { struct WeightedGateset { std::unordered_map ops; - [[nodiscard]] bool contains(const OperatorNode &op) const { return ops.find(op) != ops.end(); } + [[nodiscard]] bool contains(const OperatorNode &op) const + { + // hash match + if (ops.find(op) != ops.end()) { + return true; + } + + // use op-matching if hashes failed (could be id vs name) + for (auto [gatesetOp, cost] : ops) { + if (gatesetOp == op) { + return true; + } + } + + return false; + } + [[nodiscard]] double getCost(const OperatorNode &op) const { auto it = ops.find(op); @@ -137,7 +166,8 @@ struct RuleTerm { * @brief This represents the origin of a decomposition rule. * * This enum is used to categorize decomposition rules based on their source or type: - * - Default: The default rule for decomposing an operator as defined in the decomposition graph. + * - Default: The default rule for decomposing an operator as defined in the decomposition + * graph. * - Fixed: A fixed rule that cannot be changed or overridden by the solver. * - Alternative: An alternative rule that can be used in place of the default rule. */ @@ -154,7 +184,8 @@ enum class RuleOrigin : uint8_t { Default = 0, Fixed = 1, Alternative = 2 }; * * TODO: * - We can add a field for work_wires_required if we want to consider the number of ancillary - * wires needed for the decomposition, which can be an important factor in resource optimization. + * wires needed for the decomposition, which can be an important factor in resource + * optimization. * - We can also consider adding a field for the decomposition function or a pointer to it, * which can be used to actually perform the decomposition after the graph solver selects * the rules. diff --git a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGUtils.hpp b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGUtils.hpp index ebab329883..e962638a33 100644 --- a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGUtils.hpp +++ b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGUtils.hpp @@ -33,6 +33,11 @@ namespace DecompGraph::Core { static inline auto print_op(const OperatorNode &op) -> std::string { + // id override + if (!op.id.empty()) { + return "id: " + op.id; + } + std::ostringstream oss; oss << op.name; oss << "[w:" << op.numWires << "]"; From 1f539f30299584d8e04c7178f520840d583fdf1f Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Fri, 17 Jul 2026 11:27:17 -0400 Subject: [PATCH 2/4] add tests --- .../Test_DecompGraphCore.cpp | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/mlir/unittests/DecompGraphSolver/Test_DecompGraphCore.cpp b/mlir/unittests/DecompGraphSolver/Test_DecompGraphCore.cpp index 2963378b30..6d5894694d 100644 --- a/mlir/unittests/DecompGraphSolver/Test_DecompGraphCore.cpp +++ b/mlir/unittests/DecompGraphSolver/Test_DecompGraphCore.cpp @@ -146,3 +146,45 @@ TEST_CASE("Test ChosenDecompRule construction", "[DecompGraph::Core]") REQUIRE(chosenRule.basisCounts[rz] == 2); REQUIRE(chosenRule.basisCounts[rx] == 1); } + +TEST_CASE("Test graphOpId Support", "[DecompGraph::Core]") +{ + // comparing an op without an ID should fallback to legacy match + const OperatorNode h{"H"}; + const OperatorNode hId{"H", -1, -1, false, {}, "H[][1]{}"}; + + REQUIRE(h == hId); + + // id + legacy match with non-wildcard params + const OperatorNode x{"X", 1, 0}; + const OperatorNode xId{"X", -1, -1, false, {}, "X[][1]{}"}; + + REQUIRE(x == xId); + + // id nodes should fail legacy match if params differ + const OperatorNode op1{"op", 1, 1}; + const OperatorNode op2{"op", 2, 2, false, {}, "op[f64,f64][2]{}"}; + + REQUIRE_FALSE(op1 == op2); + + // nodes with same ids should match + const OperatorNode pr1{"PauliRot", -1, -1, false, {}, "PauliRot[f64][2]{pauli_word:XX}"}; + const OperatorNode pr2{"PauliRot", -1, -1, false, {}, "PauliRot[f64][2]{pauli_word:XX}"}; + + REQUIRE(pr1 == pr2); + + // nodes with matching ids should ignore other parameters (id is source of truth) + const OperatorNode id1{"name1", 1, 1, false, {}, "sameID"}; + const OperatorNode id2{"name2", 2, 2, true, {}, "sameID"}; + + REQUIRE(id1 == id2); + + // check ID hashes match for same IDs, doesn't match name: + const OperatorNode hash1{"name", -1, -1, false, {}, "id"}; + const OperatorNode hash2{"name2", 1, 1, true, {}, "id"}; + const OperatorNode hash3{"name2", 1, 1, true, {}}; + + const OperatorNodeHash hashFunc; + REQUIRE(hashFunc(hash1) == hashFunc(hash2)); + REQUIRE(hashFunc(hash2) != hashFunc(hash3)); +} From cafd58ce644097968183dcff68d23a752c8df714 Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Fri, 17 Jul 2026 11:46:03 -0400 Subject: [PATCH 3/4] changelog --- doc/releases/changelog-dev.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index a0c22c992e..ad052f13ce 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -251,8 +251,9 @@ of the default else branch. [(#3018)](https://github.com/PennyLaneAI/catalyst/pull/3018) -* Add the `DecomposableGate` op interface to allow generic handling of operations in the `graph-decomposition` pass. This allows arbitrary operations implementing the interface to be registered to and decomposed by the graph. This also allows the use of python-decompositions for any operator pre-registered in the frontend graph. +* Add the `DecomposableGate` op interface to allow generic handling of operations in the `graph-decomposition` pass. This allows arbitrary operations implementing the interface to be registered to and decomposed by the graph. This also allows the use of python-decompositions for any operator pre-registered in the frontend graph. The graph solver now supports the new `graphOpId`s provided by the interface, as well as the legacy pathway with `name`, `numWires` etc. [(#2983)](https://github.com/PennyLaneAI/catalyst/pull/2983) + [(#3039)](https://github.com/PennyLaneAI/catalyst/pull/3039) * The `graph-decomposition` pass eliminates three redundant IR manipulations: the cloning, removal, and re-insertion of user rules. This optimization is particularly From 4b8912c5f46f9ea90666482b54f2add3fde038ed Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Fri, 17 Jul 2026 14:26:27 -0400 Subject: [PATCH 4/4] Update mlir/unittests/DecompGraphSolver/Test_DecompGraphCore.cpp Co-authored-by: Paul <79805239+paul0403@users.noreply.github.com> --- mlir/unittests/DecompGraphSolver/Test_DecompGraphCore.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlir/unittests/DecompGraphSolver/Test_DecompGraphCore.cpp b/mlir/unittests/DecompGraphSolver/Test_DecompGraphCore.cpp index 6d5894694d..5d8db73d22 100644 --- a/mlir/unittests/DecompGraphSolver/Test_DecompGraphCore.cpp +++ b/mlir/unittests/DecompGraphSolver/Test_DecompGraphCore.cpp @@ -179,7 +179,7 @@ TEST_CASE("Test graphOpId Support", "[DecompGraph::Core]") REQUIRE(id1 == id2); - // check ID hashes match for same IDs, doesn't match name: + // Unit test for `OperatorNodeHash`. Check that the hash function prefers ID over name const OperatorNode hash1{"name", -1, -1, false, {}, "id"}; const OperatorNode hash2{"name2", 1, 1, true, {}, "id"}; const OperatorNode hash3{"name2", 1, 1, true, {}};