Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] Backend options #146

Merged
merged 6 commits into from
Jul 31, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ Changelog
Unreleased
----------

* Backend methods can now be given a ``scratch_fraction`` argument to configure the amount of GPU memory allocated to cuTensorNet contraction. Users can also configure the values of the ``StateAttribute`` and ``SamplerAttribute`` from cuTensornet via the backend interface.
* Fixed a bug causing the logger to fail displaying device properties.

0.7.0 (July 2024)
37 changes: 35 additions & 2 deletions pytket/extensions/cutensornet/backends/cutensornet_backend.py
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@
"""Methods to allow tket circuits to be run on the cuTensorNet simulator."""

from abc import abstractmethod
import warnings

from typing import List, Union, Optional, Sequence
from uuid import uuid4
@@ -44,6 +45,12 @@
FullPeepholeOptimise,
CustomPass,
)

try:
from cuquantum.cutensornet import StateAttribute, SamplerAttribute # type: ignore
except ImportError:
warnings.warn("local settings failed to import cuquantum", ImportWarning)

from .._metadata import __extension_version__, __extension_name__


@@ -195,23 +202,35 @@ def process_circuits(
The results will be stored in the backend's result cache to be retrieved by the
corresponding get_<data> method.

Note:
Any element from the ``StateAttribute`` enum (see NVIDIA's CuTensorNet
API) can be provided as arguments to this method. For instance:
``process_circuits(..., CONFIG_NUM_HYPER_SAMPLES=100)``.

Args:
circuits: List of circuits to be submitted.
n_shots: Number of shots in case of shot-based calculation.
This should be ``None``, since this backend does not support shots.
valid_check: Whether to check for circuit correctness.
scratch_fraction: Optional. Fraction of free memory on GPU to allocate as
scratch space. Defaults to `0.75`.

Returns:
Results handle objects.
"""
scratch_fraction = float(kwargs.get("scratch_fraction", 0.75)) # type: ignore
attributes = {
k: v for k, v in kwargs.items() if k in StateAttribute._member_names_
}

circuit_list = list(circuits)
if valid_check:
self._check_all_circuits(circuit_list)
handle_list = []
with CuTensorNetHandle() as libhandle:
for circuit in circuit_list:
tn = GeneralState(circuit, libhandle)
sv = tn.get_statevector()
sv = tn.get_statevector(attributes, scratch_fraction)
res_qubits = [qb for qb in sorted(circuit.qubits)]
handle = ResultHandle(str(uuid4()))
self._cache[handle] = {
@@ -260,16 +279,28 @@ def process_circuits(
The results will be stored in the backend's result cache to be retrieved by the
corresponding get_<data> method.

Note:
Any element from the ``SamplerAttribute`` enum (see NVIDIA's CuTensorNet
API) can be provided as arguments to this method. For instance:
``process_circuits(..., CONFIG_NUM_HYPER_SAMPLES=100)``.

Args:
circuits: List of circuits to be submitted.
n_shots: Number of shots in case of shot-based calculation.
Optionally, this can be a list of shots specifying the number of shots
for each circuit separately.
valid_check: Whether to check for circuit correctness.
scratch_fraction: Optional. Fraction of free memory on GPU to allocate as
scratch space. Defaults to `0.75`.

Returns:
Results handle objects.
"""
scratch_fraction = float(kwargs.get("scratch_fraction", 0.75)) # type: ignore
attributes = {
k: v for k, v in kwargs.items() if k in SamplerAttribute._member_names_
}

if "seed" in kwargs and kwargs["seed"] is not None:
# Current CuTensorNet does not support seeds for Sampler. I created
# a feature request in their repository.
@@ -294,7 +325,9 @@ def process_circuits(
for circuit, circ_shots in zip(circuit_list, all_shots):
tn = GeneralState(circuit, libhandle)
handle = ResultHandle(str(uuid4()))
self._cache[handle] = {"result": tn.sample(circ_shots)}
self._cache[handle] = {
"result": tn.sample(circ_shots, attributes, scratch_fraction)
}
handle_list.append(handle)
return handle_list

Original file line number Diff line number Diff line change
@@ -44,7 +44,7 @@ def __init__(
self,
circuit: Circuit,
libhandle: CuTensorNetHandle,
loglevel: int = logging.INFO,
loglevel: int = logging.WARNING,
) -> None:
"""Constructs a tensor network for the output state of a pytket circuit.

@@ -114,7 +114,7 @@ def __init__(
def get_statevector(
self,
attributes: Optional[dict] = None,
scratch_fraction: float = 0.5,
scratch_fraction: float = 0.75,
on_host: bool = True,
) -> Union[cp.ndarray, np.ndarray]:
"""Contracts the circuit and returns the final statevector.
@@ -229,7 +229,7 @@ def expectation_value(
self,
operator: QubitPauliOperator,
attributes: Optional[dict] = None,
scratch_fraction: float = 0.5,
scratch_fraction: float = 0.75,
) -> complex:
"""Calculates the expectation value of the given operator.

@@ -407,7 +407,7 @@ def sample(
self,
n_shots: int,
attributes: Optional[dict] = None,
scratch_fraction: float = 0.5,
scratch_fraction: float = 0.75,
) -> BackendResult:
"""Obtains samples from the measurements at the end of the circuit.

@@ -418,12 +418,17 @@ def sample(
scratch_fraction: Optional. Fraction of free memory on GPU to allocate as
scratch space.
Raises:
ValueError: If the circuit contains no measurements.
MemoryError: If there is insufficient workspace on GPU.
Returns:
A pytket ``BackendResult`` with the data from the shots.
"""

num_measurements = len(self._measurements)
if num_measurements == 0:
raise ValueError(
"Cannot sample from the circuit, it contains no measurements."
)
# We will need both a list of the qubits and a list of the classical bits
# and it is essential that the elements in the same index of either list
# match according to the self._measurements map. We guarantee this here.
33 changes: 33 additions & 0 deletions tests/test_cutensornet_backend.py
Original file line number Diff line number Diff line change
@@ -34,6 +34,39 @@ def test_sampler_bell() -> None:
assert np.isclose(res.get_counts()[(1, 1)] / n_shots, 0.5, atol=0.01)


def test_config_options() -> None:
c = Circuit(2, 2)
c.H(0)
c.CX(0, 1)
c.measure_all()

# State based
b1 = CuTensorNetStateBackend()
c = b1.get_compiled_circuit(c)
h = b1.process_circuit(
c,
scratch_fraction=0.3,
CONFIG_NUM_HYPER_SAMPLES=100,
)
assert np.allclose(
b1.get_result(h).get_state(), np.asarray([1, 0, 0, 1]) * 1 / np.sqrt(2)
)

# Shot based
n_shots = 100000
b2 = CuTensorNetShotsBackend()
c = b2.get_compiled_circuit(c)
res = b2.run_circuit(
c,
n_shots=n_shots,
scratch_fraction=0.3,
CONFIG_NUM_HYPER_SAMPLES=100,
)
assert res.get_shots().shape == (n_shots, 2)
assert np.isclose(res.get_counts()[(0, 0)] / n_shots, 0.5, atol=0.01)
assert np.isclose(res.get_counts()[(1, 1)] / n_shots, 0.5, atol=0.01)


def test_basisorder() -> None:
c = Circuit(2)
c.X(1)
Loading