Error adding loaded circuit into a Torch layer

It’s a bit hard to distill the code because of how much is going on here, but this is a (relatively) minimal example for the issue I’ve been having. The goal is to load in a QuantumCircuit object from Qiskit, then to use this loaded object as the circuit in a Torch layer.

import time

import torch
import torch.nn.functional as F

import pennylane as qml

from torchquantum.datasets import MNIST

import pennylane as qml

from qiskit.circuit.library import EfficientSU2

torch_device_name = "cpu"
torch_device = torch.device(torch_device_name)

def example_accuracy(preds, labels):
    _, indices = preds.topk(1, dim=1)
    masks = indices.eq(labels.view(-1, 1).expand_as(indices))
    corrects = masks.sum().item()

    size = labels.shape[0]
    accuracy = corrects / size

    return accuracy

def train(model, train_dl, epochs, loss_fn, optimizer, device=None):
    losses = []
    for epoch in range(epochs):
        running_loss = 0.0
        
        batches = 0
        total_batches = len(train_dl)
        
        start_time = time.time()
        
        for batch_dict in train_dl:
            x = batch_dict['image']
            y = batch_dict['digit']
            
            y = y.to(torch.long)   

            x = x.to(torch_device)
            y = y.to(torch_device)
            
            optimizer.zero_grad()
            
            if model.service == "TorchQuantum":
                preds = model(device, x)
            else:
                preds = model(x)

            loss = loss_fn(preds, y)
            loss.backward()

            optimizer.step()

            running_loss += loss.item()
            batches += 1
            
            cur_time = time.time()
            avg_batch_time = (cur_time-start_time)/batches

            print(f"Epoch {epoch + 1} | Loss: {running_loss/batches} | Est. Time Remaining: {avg_batch_time*(total_batches-batches)}", end="\r")
        
        print(f"Epoch {epoch + 1} | Loss: {running_loss/batches} | Time Elapsed: {cur_time-start_time}")
        losses.append(running_loss/batches)

    return losses

def qiskit_to_qnode_qnn(qc, device, n_wires, diff_method="best"):
    q_func = qml.load(qc, format='qiskit')
    qiskit_params = list(qc.parameters)

    @qml.qnode(device, diff_method=diff_method)
    def circuit(inputs, weights):
        qml.AngleEmbedding(inputs, wires=range(n_wires))
        q_func(params=dict(zip(qiskit_params, weights)))
        # qml.BasicEntanglerLayers(weights=weights, wires=range(n_wires))
        return [qml.expval(qml.PauliZ(wires=i)) for i in range(n_wires)]
    
    return circuit, qiskit_params

class PLNet(torch.nn.Module):
    def __init__(self, qcirc, weight_shapes, use_softmax=False):
        super().__init__()

        self.use_softmax = use_softmax

        self.circuit = qcirc
        self.qlayer = qml.qnn.TorchLayer(self.circuit, weight_shapes)

        self.service = "PennyLane"

    def forward(self, x):
        
        bsz = x.shape[0]
        x = F.avg_pool2d(x, 6)
        x = x.view(bsz, 16)

        out = self.qlayer(x)

        if self.use_softmax:
            out = F.log_softmax(out, dim=1)
        
        return out


if __name__ == "__main__":
    reps = 1
    n_qubits = 16

    device = qml.device('qiskit.aer', wires=n_qubits)

    qc = EfficientSU2(n_qubits, entanglement="linear", reps=reps)

    qnode, qiskit_params = qiskit_to_qnode_qnn(qc, device, n_qubits)

    weight_shapes = {"weights": (1, len(qiskit_params))}
    # weight_shapes = {"weights": (1, n_qubits)}
    net = PLNet(qnode, weight_shapes)

    loss_fn = F.nll_loss
    acc_fn = example_accuracy
    optimizer = torch.optim.SGD(net.parameters(), lr=0.05)

    dataset = MNIST(
        root='./mnist_data',
        train_valid_split_ratio=[.9, .1],
        digits_of_interest=[0, 1],
        n_test_samples=200,
    )

    train_dl = torch.utils.data.DataLoader(dataset['train'], batch_size=32, sampler=torch.utils.data.RandomSampler(dataset['train']))
    val_dl = torch.utils.data.DataLoader(dataset['valid'], batch_size=32, sampler=torch.utils.data.RandomSampler(dataset['valid']))
    test_dl = torch.utils.data.DataLoader(dataset['test'], batch_size=32, sampler=torch.utils.data.RandomSampler(dataset['test']))

    print("--Training--")
    train_losses = train(net, train_dl, 2, loss_fn, optimizer)

However, as I run the code, I get the following error, which suggests that there’s something going wrong with making the loaded circuit trainable. Notably, this error doesn’t happen if I replace

q_func(params=dict(zip(qiskit_params, weights)))

with

qml.BasicEntanglerLayers(weights=weights, wires=range(n_wires))

and accordingly change the weight_shapes to match.

As a seperate (but possibly related) issue, if I switch the device to use cuda instead of cpu, I also get an error relating to torch device incompatibility. It seems like running through the imported circuit switches the device of an input.

RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn

Hey @Vivekyy! Welcome to the forum :grin:

We will get to your question shortly. Apologies for the delay — we haven’t forgotten about your post!

Hey @Vivekyy, here’s a solution for you :slight_smile:

If all you want to do is load a quantum circuit in Qiskit into PennyLane and use it as a Torch layer, this works:

from pennylane import numpy as np
from qiskit.circuit import QuantumCircuit, Parameter

qc = QuantumCircuit(2)
theta = Parameter('θ')

qc.rx(theta, [0])
qc.cx(0, 1)

my_circuit = qml.load(qc, format='qiskit')

my_circuit can then be treated like a quantum function in PennyLane and can be put into a QNode like this:

dev = qml.device('default.qubit', wires=2)

@qml.qnode(dev)
def circuit(inputs, x):
    qml.AmplitudeEmbedding(features=inputs, wires=range(2))
    my_circuit(params={theta: x},wires=(1, 0))
    return qml.expval(qml.PauliZ(0))

Then you can make circuit into a torch layer:

weight_shapes = {"x": 1}
qlayer = qml.qnn.TorchLayer(circuit, weight_shapes)

Let me know if this helps!