Skip to content

Commit abbe96e

Browse files
authored
Merge pull request #298 from ghanem-nv/cuquantum-dev
cuTN integration
2 parents 611cc2a + a001907 commit abbe96e

16 files changed

+1014
-0
lines changed

examples/cuquantum/qaoa.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
import math
6+
import argparse
7+
8+
import torch
9+
from torch import nn
10+
from torchquantum.plugin.cuquantum import *
11+
from torchquantum.operator.standard_gates import *
12+
13+
14+
15+
16+
17+
class MAXCUT(nn.Module):
18+
def __init__(self, n_wires, input_graph, n_layers):
19+
super().__init__()
20+
self.n_wires = n_wires
21+
self.input_graph = input_graph
22+
self.n_layers = n_layers
23+
24+
self.circuit = ParameterizedQuantumCircuit(n_wires=n_wires, n_input_params=0, n_trainable_params=2 * n_layers)
25+
self.circuit.set_trainable_params(torch.randn(2 * n_layers))
26+
27+
for wire in range(self.n_wires):
28+
self.circuit.append_gate(Hadamard, wires=wire)
29+
30+
for l in range(self.n_layers):
31+
# mixer layer
32+
for i in range(self.n_wires):
33+
self.circuit.append_gate(RX, wires=i, trainable_idx=l)
34+
35+
# entangler layer
36+
for edge in self.input_graph:
37+
self.circuit.append_gate(CNOT, wires=[edge[0], edge[1]])
38+
self.circuit.append_gate(RZ, wires=edge[1], trainable_idx=n_layers + l)
39+
self.circuit.append_gate(CNOT, wires=[edge[0], edge[1]])
40+
41+
42+
hamiltonian = {}
43+
for edge in self.input_graph:
44+
pauli_string = ""
45+
for wire in range(self.n_wires):
46+
if wire in edge:
47+
pauli_string += "Z"
48+
else:
49+
pauli_string += "I"
50+
hamiltonian[pauli_string] = 0.5
51+
52+
backend = CuTensorNetworkBackend(TNConfig(num_hyper_samples=10))
53+
self.energy = QuantumExpectation(self.circuit, backend, [hamiltonian])
54+
self.sampling = QuantumSampling(self.circuit, backend, 100)
55+
56+
def forward(self):
57+
start_time = torch.cuda.Event(enable_timing=True)
58+
end_time = torch.cuda.Event(enable_timing=True)
59+
60+
start_time.record()
61+
output = self.energy() - len(self.input_graph) / 2
62+
end_time.record()
63+
64+
torch.cuda.synchronize()
65+
elapsed_time = start_time.elapsed_time(end_time)
66+
print(f"Forward pass took {elapsed_time:.2f} ms")
67+
68+
return output
69+
70+
71+
def optimize(model, n_steps=100, lr=0.1):
72+
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
73+
print(f"The initial parameters are:\n{next(model.parameters()).data.tolist()}")
74+
print("")
75+
for step in range(n_steps):
76+
optimizer.zero_grad()
77+
loss = model()
78+
start_time = torch.cuda.Event(enable_timing=True)
79+
end_time = torch.cuda.Event(enable_timing=True)
80+
81+
start_time.record()
82+
loss.backward()
83+
end_time.record()
84+
85+
torch.cuda.synchronize()
86+
elapsed_time = start_time.elapsed_time(end_time)
87+
print(f"Backward pass took {elapsed_time:.2f} ms")
88+
89+
optimizer.step()
90+
91+
print(f"Step: {step}, Cost Objective: {loss.item()}")
92+
93+
print("")
94+
print(f"The optimal parameters are:\n{next(model.parameters()).data.tolist()}")
95+
print("")
96+
97+
def main():
98+
parser = argparse.ArgumentParser()
99+
parser.add_argument("--n_wires", type=int, default=4, help="number of wires")
100+
parser.add_argument("--n_layers", type=int, default=4, help="number of layers")
101+
parser.add_argument("--steps", type=int, default=100, help="number of steps")
102+
parser.add_argument("--lr", type=float, default=0.01, help="learning rate")
103+
parser.add_argument("--seed", type=int, default=0, help="random seed")
104+
args = parser.parse_args()
105+
106+
torch.manual_seed(args.seed)
107+
108+
# create a fully connected graph
109+
input_graph = []
110+
for i in range(args.n_wires):
111+
for j in range(i):
112+
input_graph.append((i, j))
113+
114+
print(f"Cost Objective Minimum (Analytic Reference Result): {math.floor(args.n_wires**2 // 4)}")
115+
116+
model = MAXCUT(n_wires=args.n_wires, input_graph=input_graph, n_layers=args.n_layers)
117+
optimize(model, n_steps=args.steps, lr=args.lr)
118+
samples = model.sampling()
119+
120+
print(f"Sampling Results: {samples}")
121+
122+
123+
if __name__ == "__main__":
124+
main()

torchquantum/plugin/cuquantum/LICENSE

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
MIT License
2+
3+
Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6+
7+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8+
9+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
from .circuit import ParameterizedQuantumCircuit
6+
from .cutn import CuTensorNetworkBackend, TNConfig, MPSConfig
7+
from .expectation import QuantumExpectation
8+
from .sampling import QuantumSampling
9+
from .amplitude import QuantumAmplitude
10+
11+
__all__ = [
12+
"ParameterizedQuantumCircuit",
13+
"CuTensorNetworkBackend",
14+
"TNConfig",
15+
"MPSConfig",
16+
"QuantumExpectation",
17+
"QuantumSampling",
18+
"QuantumAmplitude",
19+
]
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
from typing import List
6+
7+
import torch.nn as nn
8+
9+
from .utils import check_input_params
10+
from .backend import QuantumBackend
11+
from .circuit import ParameterizedQuantumCircuit
12+
13+
14+
class QuantumAmplitude(nn.Module):
15+
"""A PyTorch module for computing quantum state amplitudes.
16+
17+
This module computes the amplitudes of specified bitstrings in the quantum state prepared by a given quantum circuit.
18+
19+
Args:
20+
circuit: The quantum circuit that prepares the state.
21+
backend: The quantum backend to use for computation.
22+
bitstrings: List of bitstrings whose amplitudes to compute.
23+
"""
24+
25+
def __init__(self, circuit: ParameterizedQuantumCircuit, backend: QuantumBackend, bitstrings: List[str]):
26+
super().__init__()
27+
self._circuit = circuit.copy()
28+
self._bitstrings = bitstrings.copy()
29+
self._backend = backend
30+
self._amplitude_module = self.backend._create_amplitude_module(circuit, bitstrings)
31+
32+
def forward(self, input_params=None):
33+
"""Compute the amplitudes for the bitstrings specified in the constructor.
34+
35+
Args:
36+
input_params: 2D Tensor of input parameters for the circuit. Shape should be (batch_size, n_input_params). If
37+
only one batch is being processed, the tensor can be instead a 1D tensor with shape (n_input_params,). If
38+
the circuit has no input parameters, this argument can be omitted (i.e. None).
39+
40+
Returns:
41+
2D Tensor of amplitudes for each bitstring in each batch. The shape is (batch_size, len(bitstrings)).
42+
"""
43+
input_params = check_input_params(input_params, self._circuit.n_input_params)
44+
return self._amplitude_module(input_params)
45+
46+
@property
47+
def bitstrings(self):
48+
"""Get the list of bitstrings whose amplitudes are being computed."""
49+
return self._bitstrings.copy()
50+
51+
@property
52+
def circuit(self):
53+
"""Get the quantum circuit used for state preparation."""
54+
return self._circuit.copy()
55+
56+
@property
57+
def backend(self):
58+
"""Get the quantum backend being used for computation."""
59+
return self._backend
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
from abc import ABC, abstractmethod
6+
from typing import List, Union, Dict, Optional
7+
8+
import torch.nn as nn
9+
10+
from .circuit import ParameterizedQuantumCircuit
11+
12+
13+
class QuantumBackend(ABC):
14+
"""Abstract base class for quantum backends.
15+
16+
This class defines the interface that all quantum backends must implement. Each backend must provide methods for
17+
creating PyTorch modules that compute:
18+
- Expectation values of Pauli operators.
19+
- State amplitudes for given bitstrings.
20+
- Sampling from the quantum state.
21+
"""
22+
23+
@abstractmethod
24+
def _create_expectation_module(
25+
self, circuit: ParameterizedQuantumCircuit, pauli_ops: Union[List[str], Dict[str, float]]
26+
) -> nn.Module:
27+
"""Create a module for computing expectation values of Pauli operators.
28+
29+
Args:
30+
circuit: The quantum circuit that prepares the state
31+
pauli_ops: List of Pauli operators to compute expectations for. Each Pauli operator can be either:
32+
- A single Pauli string specifying the pauli operator for each qubit ("I", "X", "Y", or "Z").
33+
- A linear combination of Pauli strings specified as a dictionary mapping each single Pauli string to its
34+
corresponding coefficient.
35+
36+
Returns:
37+
A PyTorch module that computes the expectation values.
38+
"""
39+
pass
40+
41+
@abstractmethod
42+
def _create_amplitude_module(self, circuit: ParameterizedQuantumCircuit, bitstrings: List[str]) -> nn.Module:
43+
"""Create a module for computing state amplitudes.
44+
45+
Args:
46+
circuit: The quantum circuit that prepares the state.
47+
bitstrings: List of bitstrings whose amplitudes to compute.
48+
49+
Returns:
50+
A PyTorch module that computes the amplitudes.
51+
"""
52+
pass
53+
54+
@abstractmethod
55+
def _create_sampling_module(
56+
self, circuit: ParameterizedQuantumCircuit, n_samples: int, wires: Optional[List[int]] = None
57+
) -> nn.Module:
58+
"""Create a module for sampling from the quantum state.
59+
60+
Args:
61+
circuit: The quantum circuit that prepares the state.
62+
n_samples: Number of samples to generate.
63+
wires: Optional list of wires/qubits to sample from. If not provided, all wires/qubits are sampled from.
64+
65+
Returns:
66+
A PyTorch module that generates samples from the quantum state.
67+
"""
68+
pass

0 commit comments

Comments
 (0)