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
Show file tree
Hide file tree
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
Expand Up @@ -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)
Expand Down
37 changes: 35 additions & 2 deletions pytket/extensions/cutensornet/backends/cutensornet_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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__


Expand Down Expand Up @@ -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] = {
Expand Down Expand Up @@ -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.
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand All @@ -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.
Expand Down
33 changes: 33 additions & 0 deletions tests/test_cutensornet_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading