diff --git a/docs/changelog.rst b/docs/changelog.rst index 5ac06223..e00497b0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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) diff --git a/pytket/extensions/cutensornet/backends/cutensornet_backend.py b/pytket/extensions/cutensornet/backends/cutensornet_backend.py index 08b24a91..b09d33c0 100644 --- a/pytket/extensions/cutensornet/backends/cutensornet_backend.py +++ b/pytket/extensions/cutensornet/backends/cutensornet_backend.py @@ -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,15 +202,27 @@ def process_circuits( The results will be stored in the backend's result cache to be retrieved by the corresponding get_ 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) @@ -211,7 +230,7 @@ def process_circuits( 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_ 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 diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 1cee58e1..bedfc0f2 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -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. diff --git a/tests/test_cutensornet_backend.py b/tests/test_cutensornet_backend.py index 2ffbbfb9..25c9d31c 100644 --- a/tests/test_cutensornet_backend.py +++ b/tests/test_cutensornet_backend.py @@ -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)