From 8d7b56e3a253e86a8415b7b37c0803b2efe9cc72 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Tue, 30 Jul 2024 15:57:15 +0100 Subject: [PATCH 1/6] Allowing users of backends to provide cuTensorNet attributes to the configuration of the contraction path optimisation --- docs/changelog.rst | 1 + .../backends/cutensornet_backend.py | 24 +++++++++++- .../general_state/tensor_network_state.py | 39 ++++++++++--------- tests/test_cutensornet_backend.py | 33 ++++++++++++++++ 4 files changed, 76 insertions(+), 21 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5ac06223..20e4f46c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Unreleased ---------- +* Backend methods can now be given `cutn_attributes` and `scratch_fraction` arguments to configure the cuTensorNet contraction. * 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..610600d1 100644 --- a/pytket/extensions/cutensornet/backends/cutensornet_backend.py +++ b/pytket/extensions/cutensornet/backends/cutensornet_backend.py @@ -44,6 +44,7 @@ FullPeepholeOptimise, CustomPass, ) +from cuquantum.cutensornet import StateAttribute, SamplerAttribute # type: ignore from .._metadata import __extension_version__, __extension_name__ @@ -195,15 +196,25 @@ def process_circuits( The results will be stored in the backend's result cache to be retrieved by the corresponding get_ method. + Note: + TODO: Mention cutn attributes. + 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 + cutn_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 +222,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(cutn_attributes, scratch_fraction) res_qubits = [qb for qb in sorted(circuit.qubits)] handle = ResultHandle(str(uuid4())) self._cache[handle] = { @@ -266,10 +277,17 @@ def process_circuits( 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 + cutn_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 +312,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, cutn_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..9471bcbc 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. @@ -113,14 +113,14 @@ def __init__( def get_statevector( self, - attributes: Optional[dict] = None, - scratch_fraction: float = 0.5, + cutn_attributes: Optional[dict] = None, + scratch_fraction: float = 0.75, on_host: bool = True, ) -> Union[cp.ndarray, np.ndarray]: """Contracts the circuit and returns the final statevector. Args: - attributes: Optional. A dict of cuTensorNet `StateAttribute` keys and + cutn_attributes: Optional. A dict of cuTensorNet `StateAttribute` keys and their values. scratch_fraction: Optional. Fraction of free memory on GPU to allocate as scratch space. @@ -136,10 +136,10 @@ def get_statevector( #################################### # Configure the TN for contraction # #################################### - if attributes is None: - attributes = dict() + if cutn_attributes is None: + cutn_attributes = dict() attribute_pairs = [ - (getattr(cutn.StateAttribute, k), v) for k, v in attributes.items() + (getattr(cutn.StateAttribute, k), v) for k, v in cutn_attributes.items() ] for attr, val in attribute_pairs: @@ -228,14 +228,14 @@ def get_statevector( def expectation_value( self, operator: QubitPauliOperator, - attributes: Optional[dict] = None, - scratch_fraction: float = 0.5, + cutn_attributes: Optional[dict] = None, + scratch_fraction: float = 0.75, ) -> complex: """Calculates the expectation value of the given operator. Args: operator: The operator whose expectation value is to be measured. - attributes: Optional. A dict of cuTensorNet `ExpectationAttribute` keys + cutn_attributes: Optional. A dict of cuTensorNet `ExpectationAttribute` keys and their values. scratch_fraction: Optional. Fraction of free memory on GPU to allocate as scratch space. @@ -310,10 +310,11 @@ def expectation_value( self._lib.handle, self._state, tn_operator ) - if attributes is None: - attributes = dict() + if cutn_attributes is None: + cutn_attributes = dict() attribute_pairs = [ - (getattr(cutn.ExpectationAttribute, k), v) for k, v in attributes.items() + (getattr(cutn.ExpectationAttribute, k), v) + for k, v in cutn_attributes.items() ] for attr, val in attribute_pairs: @@ -406,14 +407,14 @@ def expectation_value( def sample( self, n_shots: int, - attributes: Optional[dict] = None, - scratch_fraction: float = 0.5, + cutn_attributes: Optional[dict] = None, + scratch_fraction: float = 0.75, ) -> BackendResult: """Obtains samples from the measurements at the end of the circuit. Args: n_shots: The number of samples to obtain. - attributes: Optional. A dict of cuTensorNet `SamplerAttribute` keys and + cutn_attributes: Optional. A dict of cuTensorNet `SamplerAttribute` keys and their values. scratch_fraction: Optional. Fraction of free memory on GPU to allocate as scratch space. @@ -441,10 +442,10 @@ def sample( modes_to_sample=measured_modes, ) - if attributes is None: - attributes = dict() + if cutn_attributes is None: + cutn_attributes = dict() attribute_pairs = [ - (getattr(cutn.SamplerAttribute, k), v) for k, v in attributes.items() + (getattr(cutn.SamplerAttribute, k), v) for k, v in cutn_attributes.items() ] for attr, val in attribute_pairs: 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) From d7a72b8c3e7e320428965c2c3b7340d391e58abd Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Tue, 30 Jul 2024 16:16:44 +0100 Subject: [PATCH 2/6] Updated docstrings --- .../cutensornet/backends/cutensornet_backend.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pytket/extensions/cutensornet/backends/cutensornet_backend.py b/pytket/extensions/cutensornet/backends/cutensornet_backend.py index 610600d1..e7a7e236 100644 --- a/pytket/extensions/cutensornet/backends/cutensornet_backend.py +++ b/pytket/extensions/cutensornet/backends/cutensornet_backend.py @@ -197,7 +197,9 @@ def process_circuits( corresponding get_ method. Note: - TODO: Mention cutn attributes. + 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. @@ -271,6 +273,11 @@ 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. From 206fd71c9d0c8d918ca63e5e366f884c3fe3683b Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Tue, 30 Jul 2024 16:23:45 +0100 Subject: [PATCH 3/6] Added a check to raise an error in the sampler if there are no measurements in the circuit. --- .../cutensornet/general_state/tensor_network_state.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pytket/extensions/cutensornet/general_state/tensor_network_state.py b/pytket/extensions/cutensornet/general_state/tensor_network_state.py index 9471bcbc..d2cffc0e 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -419,12 +419,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. From 061cbd5c396787cfb472e862e66cd57b0f2784b2 Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Tue, 30 Jul 2024 16:29:01 +0100 Subject: [PATCH 4/6] Updated changelog --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 20e4f46c..e00497b0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,7 @@ Changelog Unreleased ---------- -* Backend methods can now be given `cutn_attributes` and `scratch_fraction` arguments to configure the cuTensorNet contraction. +* 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) From 07de8c2e056ee9b01de944a63ee20ad2121ffd9a Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Tue, 30 Jul 2024 16:45:28 +0100 Subject: [PATCH 5/6] Import with try-except for CI to work without cuquantum installed --- .../cutensornet/backends/cutensornet_backend.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pytket/extensions/cutensornet/backends/cutensornet_backend.py b/pytket/extensions/cutensornet/backends/cutensornet_backend.py index e7a7e236..ecbea9a7 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,7 +45,12 @@ FullPeepholeOptimise, CustomPass, ) -from cuquantum.cutensornet import StateAttribute, SamplerAttribute # type: ignore + +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__ From 67e594f6056be62024c0a99bfd558e4edfddf44e Mon Sep 17 00:00:00 2001 From: PabloAndresCQ Date: Wed, 31 Jul 2024 12:33:05 +0100 Subject: [PATCH 6/6] Renamed cutn_attributes back to attributes --- .../backends/cutensornet_backend.py | 8 ++--- .../general_state/tensor_network_state.py | 31 +++++++++---------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/pytket/extensions/cutensornet/backends/cutensornet_backend.py b/pytket/extensions/cutensornet/backends/cutensornet_backend.py index ecbea9a7..b09d33c0 100644 --- a/pytket/extensions/cutensornet/backends/cutensornet_backend.py +++ b/pytket/extensions/cutensornet/backends/cutensornet_backend.py @@ -219,7 +219,7 @@ def process_circuits( Results handle objects. """ scratch_fraction = float(kwargs.get("scratch_fraction", 0.75)) # type: ignore - cutn_attributes = { + attributes = { k: v for k, v in kwargs.items() if k in StateAttribute._member_names_ } @@ -230,7 +230,7 @@ def process_circuits( with CuTensorNetHandle() as libhandle: for circuit in circuit_list: tn = GeneralState(circuit, libhandle) - sv = tn.get_statevector(cutn_attributes, scratch_fraction) + sv = tn.get_statevector(attributes, scratch_fraction) res_qubits = [qb for qb in sorted(circuit.qubits)] handle = ResultHandle(str(uuid4())) self._cache[handle] = { @@ -297,7 +297,7 @@ def process_circuits( Results handle objects. """ scratch_fraction = float(kwargs.get("scratch_fraction", 0.75)) # type: ignore - cutn_attributes = { + attributes = { k: v for k, v in kwargs.items() if k in SamplerAttribute._member_names_ } @@ -326,7 +326,7 @@ def process_circuits( tn = GeneralState(circuit, libhandle) handle = ResultHandle(str(uuid4())) self._cache[handle] = { - "result": tn.sample(circ_shots, cutn_attributes, scratch_fraction) + "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 d2cffc0e..bedfc0f2 100644 --- a/pytket/extensions/cutensornet/general_state/tensor_network_state.py +++ b/pytket/extensions/cutensornet/general_state/tensor_network_state.py @@ -113,14 +113,14 @@ def __init__( def get_statevector( self, - cutn_attributes: Optional[dict] = None, + attributes: Optional[dict] = None, scratch_fraction: float = 0.75, on_host: bool = True, ) -> Union[cp.ndarray, np.ndarray]: """Contracts the circuit and returns the final statevector. Args: - cutn_attributes: Optional. A dict of cuTensorNet `StateAttribute` keys and + attributes: Optional. A dict of cuTensorNet `StateAttribute` keys and their values. scratch_fraction: Optional. Fraction of free memory on GPU to allocate as scratch space. @@ -136,10 +136,10 @@ def get_statevector( #################################### # Configure the TN for contraction # #################################### - if cutn_attributes is None: - cutn_attributes = dict() + if attributes is None: + attributes = dict() attribute_pairs = [ - (getattr(cutn.StateAttribute, k), v) for k, v in cutn_attributes.items() + (getattr(cutn.StateAttribute, k), v) for k, v in attributes.items() ] for attr, val in attribute_pairs: @@ -228,14 +228,14 @@ def get_statevector( def expectation_value( self, operator: QubitPauliOperator, - cutn_attributes: Optional[dict] = None, + attributes: Optional[dict] = None, scratch_fraction: float = 0.75, ) -> complex: """Calculates the expectation value of the given operator. Args: operator: The operator whose expectation value is to be measured. - cutn_attributes: Optional. A dict of cuTensorNet `ExpectationAttribute` keys + attributes: Optional. A dict of cuTensorNet `ExpectationAttribute` keys and their values. scratch_fraction: Optional. Fraction of free memory on GPU to allocate as scratch space. @@ -310,11 +310,10 @@ def expectation_value( self._lib.handle, self._state, tn_operator ) - if cutn_attributes is None: - cutn_attributes = dict() + if attributes is None: + attributes = dict() attribute_pairs = [ - (getattr(cutn.ExpectationAttribute, k), v) - for k, v in cutn_attributes.items() + (getattr(cutn.ExpectationAttribute, k), v) for k, v in attributes.items() ] for attr, val in attribute_pairs: @@ -407,14 +406,14 @@ def expectation_value( def sample( self, n_shots: int, - cutn_attributes: Optional[dict] = None, + attributes: Optional[dict] = None, scratch_fraction: float = 0.75, ) -> BackendResult: """Obtains samples from the measurements at the end of the circuit. Args: n_shots: The number of samples to obtain. - cutn_attributes: Optional. A dict of cuTensorNet `SamplerAttribute` keys and + attributes: Optional. A dict of cuTensorNet `SamplerAttribute` keys and their values. scratch_fraction: Optional. Fraction of free memory on GPU to allocate as scratch space. @@ -447,10 +446,10 @@ def sample( modes_to_sample=measured_modes, ) - if cutn_attributes is None: - cutn_attributes = dict() + if attributes is None: + attributes = dict() attribute_pairs = [ - (getattr(cutn.SamplerAttribute, k), v) for k, v in cutn_attributes.items() + (getattr(cutn.SamplerAttribute, k), v) for k, v in attributes.items() ] for attr, val in attribute_pairs: