Skip to content

Conversation

ritu-thombre99
Copy link
Contributor

@ritu-thombre99 ritu-thombre99 commented Jul 22, 2025

Context:

Description of the Change:

When running a quantum circuit on a hardware with certain connectivity constraints, two-qubit gates like CNOTs can only be executed using physical qubits that are connected by an edge on the hardware.

Hence, necessary SWAPs need to be inserted in order to route the logical qubits so that they are mapped to an edge on the device, while respecting other compiling constraint i.e. order the gate dependencies from the input quantum circuit, and ensuring compiled quantum circuit is equivalent to the input quantum circuit.

Following image shows a simple example:

image

Benefits:

  1. This will support qubit mapping and routing in the runtime
  2. Analogue to qml.transpile transform
  3. Circuits with control structure will also be supported since they will already be optimized by Catalyst passes and unrolled in the runtime.

Possible Drawbacks:

  1. Modification of RuntimeCAPI
  2. Currently, not checking if device supports SWAP
  3. There are multi-qubit gates like Toffoli, MultiRZ, QubitUnitary in RuntimeCAPI.cpp. Routing is not applied to these qubits yet.

Related GitHub Issues:

Example usage:

Currently, seeing what SWAP gates are added can be viewed by using null.qubit done by printing NamedOperation in runtime/lib/backend/null_qubit/NullQubit.hpp

import pennylane as qml
import numpy as np
dev = qml.device("null.qubit", wires = [(0,1),(1,2),(2,3)])
@qml.qjit(autograph=True)
@qml.qnode(dev)
def circuit(c: bool, x: int, y: int):
    if c:
        qml.Hadamard(x)
    qml.Hadamard(y)
    qml.CRZ(0.4,[x,y])
    qml.CNOT([x,2])
    qml.CRX(0.6,[2,y])
    qml.CRY(0.2,[x,y])
    qml.IsingXX(0.24,[0,y])
    qml.CNOT([2,4])
    qml.CNOT([0,5])
    return qml.state()
print(circuit(True,0,3))

Output:

Name: Hadamard
Wires : 0,
Name: Hadamard
Wires : 3,
Name: SWAP
Wires : 0,1,
Name: SWAP
Wires : 1,2,
Name: CRZ
Wires : 2,3,
Name: CNOT
Wires : 2,1,
Name: SWAP
Wires : 1,2,
Name: CRX
Wires : 2,3,
Name: SWAP
Wires : 1,2,
Name: CRY
Wires : 2,3,
Name: IsingXX
Wires : 2,3,
Name: SWAP
Wires : 1,2,
Name: SWAP
Wires : 2,3,
Name: CNOT
Wires : 3,4,
Name: SWAP
Wires : 1,2,
Name: SWAP
Wires : 2,3,
Name: SWAP
Wires : 3,4,
Name: CNOT
Wires : 4,5,
Name: SWAP
Wires : 1,3,
Name: SWAP
Wires : 0,1,
Name: SWAP
Wires : 4,0,
[1.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j
 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j
 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j
 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j
 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j
 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j
 0.+0.j 0.+0.j 0.+0.j 0.+0.j]

Copy link
Contributor

github-actions bot commented Aug 5, 2025

Hello. You may have forgotten to update the changelog!
Please edit doc/releases/changelog-dev.md on your branch with:

  • A one-to-two sentence description of the change. You may include a small working example for new features.
  • A link back to this PR.
  • Your name (or GitHub username) in the contributors section.

@dime10
Copy link
Contributor

dime10 commented Aug 8, 2025

@ritu-thombre99 can you fix the checks?

Copy link
Contributor

@dime10 dime10 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, I'm excited to add this feature to Catalyst! To merge this in we'll have to meet the PennyLane development standards, like adding tests (both unit and integration), code docs, and make sure there is a good way to control this feature :)

RT_FAIL_IF(control == target,
"Invalid input for CNOT gate. Control and target qubit operands must be distinct.");

std::pair<int,int> routedQubits = getRoutedQubits(control, target, modifiers);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have a flag here that controls whether routing is performed or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getQuantumDevicePtr()->StartTapeRecording();
}

// Extract coupling map from the kwargs passed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be split out into its own function.

In fact, we want to take all these functions and put them in a new file (Routing.cpp/hpp) that you can then call into.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modularized in Routing.hpp 4ae4a46

const std::vector<QubitIdType> &controlled_wires = {},
const std::vector<bool> &controlled_values = {})
{
// Print to see what naive router does on Null qubits
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add some runtime tests for the routing functionality

Comment on lines 785 to 786
{/* control = */ routedQubits.first,
/* target = */ routedQubits.second},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep the type that is passed to the function as a QubitIdType (even if you don't use it in your algorithm internally).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed types in 4227d89

if (distanceMatrix[std::make_pair(*j_itr,*i_itr)] + distanceMatrix[std::make_pair(*i_itr,*k_itr)] < distanceMatrix[std::make_pair(*j_itr,*k_itr)] )
{
distanceMatrix[std::make_pair(*j_itr,*k_itr)] = distanceMatrix[std::make_pair(*j_itr,*i_itr)] + distanceMatrix[std::make_pair(*i_itr,*k_itr)];
predecessorMatrix[std::make_pair(*j_itr,*k_itr)] = predecessorMatrix[std::make_pair(*i_itr,*k_itr)];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The next question is where we want to store this kind of information. If we don't want to touch device backends, but still want it to specific to each device (unlike the global you have right now), I think the natural place for it would be in the RTDevice class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defined them as class variables in Routing.hpp 4ae4a46 which are initialized only if wires passed to PennyLane device are in a format of list of tuples representing coupling map of the hardware

// Extract coupling map from the kwargs passed
// If coupling map is provided then it takes in the form {...,'couplingMap' ((a,b),(b,c))}
// else {...,'couplingMap' (a,b,c)}
size_t start = args[2].find("coupling_map': ") + 15; // Find key and opening parenthesis
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finally we'll want to device how to pass down the coupling map from the frontend to the runtime.

I think the shortcut via the device_kwargs is okay for now, although normally those are meant to go the Device class constructor. We may want to associate them closer to the runtime then than the device, like we do with shots or qubit management.

Copy link
Member

@mlxd mlxd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few fly-by comments from me.
Feel free to ignore them as needed

@@ -0,0 +1,180 @@
// Copyright 2025 Xanadu Quantum Technologies Inc.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this file meant to be the header, or the implementation?
If the header, can we add an #pragma once at the top? If the implementation, we should probably split the header interface and implementation details separately.


#include "RuntimeCAPI.h"

const int MAXIMUM = 1e9;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than having scope visibility across this header, can this be isolated? Otherwise, this will be exported and visible in the generated translation unit.

Either programmatically passing this as an argument to a function/method where needed with a given default value, or (if it is required we set this magic variable), it can be isolated in an anonymous namespace, which should ensure it isn't visible in the compiled code outside of where it is needed.

/**
* @brief Global routing pass pointer.
*/
static std::unique_ptr<RoutingPass> RUNTIME_ROUTER = nullptr;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this to be global? Is there a way we can isolate this so it isn't exposed? Any concerns with thread locality?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, the router should be able to be scoped to each device instance (RTDevice)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants