Code example (MPS sampling MPO)#

The following code example illustrates how to define a tensor network state by a circuit with Matrix Product Operators (MPO), then compute its Matrix Product State (MPS) factorization, and, finally, sample the MPS-factorized state. The full code can be found in the NVIDIA/cuQuantum repository (here).

Headers and error handling#

 7#include <cstdlib>
 8#include <cstdio>
 9#include <cmath>
10#include <cassert>
11#include <complex>
12#include <random>
13#include <functional>
14#include <vector>
15#include <iostream>
16
17#include <cuda_runtime.h>
18#include <cutensornet.h>
19
20#define HANDLE_CUDA_ERROR(x) \
21{ const auto err = x; \
22  if( err != cudaSuccess ) \
23  { printf("CUDA error %s in line %d\n", cudaGetErrorString(err), __LINE__); fflush(stdout); std::abort(); } \
24};
25
26#define HANDLE_CUTN_ERROR(x) \
27{ const auto err = x; \
28  if( err != CUTENSORNET_STATUS_SUCCESS ) \
29  { printf("cuTensorNet error %s in line %d\n", cutensornetGetErrorString(err), __LINE__); fflush(stdout); std::abort(); } \
30};
31
32
33int main()
34{
35  static_assert(sizeof(size_t) == sizeof(int64_t), "Please build this sample on a 64-bit architecture!");
36
37  constexpr std::size_t fp64size = sizeof(double);
38  constexpr cudaDataType_t dataType = CUDA_C_64F;

Define the tensor network state and the desired number of output samples to generate#

Let’s define all parameters of the tensor network state for a quantum circuit with the given number of qubits and request to produce a given number of output samples for the full qubit register. We also configure the maximal bond dimension for the MPS and MPO tensor network representations used in this example. For the MPO, we also define the number of sites (qubits) it acts on as well as the number of layers of the MPO.

42  // Quantum state configuration
43  const int64_t numSamples = 128; // number of samples to generate
44  const int32_t numQudits = 6;    // number of qudits
45  const int64_t quditDim = 2;     // dimension of all qudits
46  const std::vector<int64_t> quditDims(numQudits, quditDim); // qudit dimensions
47  const int64_t mpsBondDim = 8;   // maximum MPS bond dimension
48  const int64_t mpoBondDim = 2;   // maximum MPO bond dimension
49  const int32_t mpoNumLayers = 5; // number of MPO layers
50  constexpr int32_t mpoNumSites = 4;  // number of MPO sites
51  std::cout << "Quantum circuit: " << numQudits << " qudits; " << numSamples << " samples\n";
52
53  /* Action of five alternating four-site MPO gates (operators)
54     on the 6-quqit quantum register (illustration):
55
56  Q----X---------X---------X----
57       |         |         |
58  Q----X---------X---------X----
59       |         |         |
60  Q----X----X----X----X----X----
61       |    |    |    |    |
62  Q----X----X----X----X----X----
63            |         |
64  Q---------X---------X---------
65            |         |
66  Q---------X---------X---------
67
68    |layer|
69  */
70  static_assert(mpoNumSites > 1, "Number of MPO sites must be larger than one!");
71
72  // Random number generator
73  std::default_random_engine generator;
74  std::uniform_real_distribution<double> distribution(-1.0, 1.0);
75  auto rnd = std::bind(distribution, generator);

Initialize the cuTensorNet library handle#

79  // Initialize the cuTensorNet library
80  HANDLE_CUDA_ERROR(cudaSetDevice(0));
81  cutensornetHandle_t cutnHandle;
82  HANDLE_CUTN_ERROR(cutensornetCreate(&cutnHandle));
83  std::cout << "Initialized cuTensorNet library on GPU 0\n";

Define and allocate MPO tensors#

Here we define the shapes of the MPO tensors, fill them in with random data on Host, and, finally, transfer them into Device memory.

 87  /* MPO tensor mode numeration (open boundary condition):
 88
 89     2            3                   2
 90     |            |                   |
 91     X--1------0--X--2---- ... ----0--X
 92     |            |                   |
 93     0            1                   1
 94
 95  */
 96  // Define shapes of the MPO tensors
 97  using GateData = std::vector<std::complex<double>>;
 98  using ModeExtents = std::vector<int64_t>;
 99  std::vector<GateData> mpoTensors(mpoNumSites);        // MPO tensors
100  std::vector<ModeExtents> mpoModeExtents(mpoNumSites); // mode extents for MPO tensors
101  int64_t upperBondDim = 1;
102  for(int tensId = 0; tensId < (mpoNumSites / 2); ++tensId) {
103    const auto leftBondDim = std::min(mpoBondDim, upperBondDim);
104    const auto rightBondDim = std::min(mpoBondDim, leftBondDim * (quditDim * quditDim));
105    if(tensId == 0) {
106      mpoModeExtents[tensId] = {quditDim, rightBondDim, quditDim};
107    }else{
108      mpoModeExtents[tensId] = {leftBondDim, quditDim, rightBondDim, quditDim};
109    }
110    upperBondDim = rightBondDim;
111  }
112  auto centralBondDim = upperBondDim;
113  if(mpoNumSites % 2 != 0) {
114    const int tensId = mpoNumSites / 2;
115    mpoModeExtents[tensId] = {centralBondDim, quditDim, centralBondDim, quditDim};
116  }
117  upperBondDim = 1;
118  for(int tensId = (mpoNumSites-1); tensId >= (mpoNumSites / 2) + (mpoNumSites % 2); --tensId) {
119    const auto rightBondDim = std::min(mpoBondDim, upperBondDim);
120    const auto leftBondDim = std::min(mpoBondDim, rightBondDim * (quditDim * quditDim));
121    if(tensId == (mpoNumSites-1)) {
122      mpoModeExtents[tensId] = {leftBondDim, quditDim, quditDim};
123    }else{
124      mpoModeExtents[tensId] = {leftBondDim, quditDim, rightBondDim, quditDim};
125    }
126    upperBondDim = leftBondDim;
127  }
128  // Fill in the MPO tensors with random data on Host
129  std::vector<const int64_t*> mpoModeExtentsPtr(mpoNumSites, nullptr);
130  for(int tensId = 0; tensId < mpoNumSites; ++tensId) {
131    mpoModeExtentsPtr[tensId] = mpoModeExtents[tensId].data();
132    const auto tensRank = mpoModeExtents[tensId].size();
133    int64_t tensVol = 1;
134    for(int i = 0; i < tensRank; ++i) {
135      tensVol *= mpoModeExtents[tensId][i];
136    }
137    mpoTensors[tensId].resize(tensVol);
138    for(int64_t i = 0; i < tensVol; ++i) {
139      mpoTensors[tensId][i] = std::complex<double>(rnd(), rnd());
140    }
141  }
142  // Allocate and copy MPO tensors into Device memory
143  std::vector<void*> d_mpoTensors(mpoNumSites, nullptr);
144  for(int tensId = 0; tensId < mpoNumSites; ++tensId) {
145    const auto tensRank = mpoModeExtents[tensId].size();
146    int64_t tensVol = 1;
147    for(int i = 0; i < tensRank; ++i) {
148      tensVol *= mpoModeExtents[tensId][i];
149    }
150    HANDLE_CUDA_ERROR(cudaMalloc(&(d_mpoTensors[tensId]), 
151                                 std::size_t(tensVol) * (2 * fp64size)));
152    HANDLE_CUDA_ERROR(cudaMemcpy(d_mpoTensors[tensId], mpoTensors[tensId].data(),
153                                 std::size_t(tensVol) * (2 * fp64size), cudaMemcpyHostToDevice));
154  }
155  std::cout << "Allocated and defined MPO tensors in GPU memory\n";

Define and allocate MPS tensors#

Here we define the shapes of the MPS tensors and allocate Device memory for their storage.

159  // Define the MPS representation and allocate memory buffers for the MPS tensors
160  std::vector<ModeExtents> mpsModeExtents(numQudits);
161  std::vector<int64_t*> mpsModeExtentsPtr(numQudits, nullptr);
162  std::vector<void*> d_mpsTensors(numQudits, nullptr);
163  upperBondDim = 1;
164  for (int tensId = 0; tensId < (numQudits / 2); ++tensId) {
165    const auto leftBondDim = std::min(mpsBondDim, upperBondDim);
166    const auto rightBondDim = std::min(mpsBondDim, leftBondDim * quditDim);
167    if (tensId == 0) { // left boundary MPS tensor  
168      mpsModeExtents[tensId] = {quditDim, rightBondDim};
169      HANDLE_CUDA_ERROR(cudaMalloc(&(d_mpsTensors[tensId]),
170                                   std::size_t(quditDim * rightBondDim) * (2 * fp64size)));
171    } else { // middle MPS tensors
172      mpsModeExtents[tensId] = {leftBondDim, quditDim, rightBondDim};
173      HANDLE_CUDA_ERROR(cudaMalloc(&(d_mpsTensors[tensId]),
174                                   std::size_t(leftBondDim * quditDim * rightBondDim) * (2 * fp64size)));
175    }
176    upperBondDim = rightBondDim;
177    mpsModeExtentsPtr[tensId] = mpsModeExtents[tensId].data();
178  }
179  centralBondDim = upperBondDim;
180  if(numQudits % 2 != 0) {
181    const int tensId = numQudits / 2;
182    mpsModeExtents[tensId] = {centralBondDim, quditDim, centralBondDim};
183    mpsModeExtentsPtr[tensId] = mpsModeExtents[tensId].data();
184  }
185  upperBondDim = 1;
186  for (int tensId = (numQudits-1); tensId >= (numQudits / 2) + (numQudits % 2); --tensId) {
187    const auto rightBondDim = std::min(mpsBondDim, upperBondDim);
188    const auto leftBondDim = std::min(mpsBondDim, rightBondDim * quditDim);
189    if (tensId == (numQudits-1)) { // right boundary MPS tensor  
190      mpsModeExtents[tensId] = {leftBondDim, quditDim};
191      HANDLE_CUDA_ERROR(cudaMalloc(&(d_mpsTensors[tensId]),
192                                   std::size_t(leftBondDim * quditDim) * (2 * fp64size)));
193    } else { // middle MPS tensors
194      mpsModeExtents[tensId] = {leftBondDim, quditDim, rightBondDim};
195      HANDLE_CUDA_ERROR(cudaMalloc(&(d_mpsTensors[tensId]),
196                                   std::size_t(leftBondDim * quditDim * rightBondDim) * (2 * fp64size)));
197    }
198    upperBondDim = leftBondDim;
199    mpsModeExtentsPtr[tensId] = mpsModeExtents[tensId].data();
200  }
201  std::cout << "Allocated MPS tensors in GPU memory\n";

Allocate the scratch buffer on GPU#

205  // Query free memory on Device and allocate a scratch buffer
206  std::size_t freeSize {0}, totalSize {0};
207  HANDLE_CUDA_ERROR(cudaMemGetInfo(&freeSize, &totalSize));
208  const std::size_t scratchSize = (freeSize - (freeSize % 4096)) / 2; // use half of available memory with alignment
209  void *d_scratch {nullptr};
210  void *h_scratch{nullptr};  // host scratch workspace buffer (attached if the prepare call reports a need)
211  int64_t hostWorksize{0};
212  HANDLE_CUDA_ERROR(cudaMalloc(&d_scratch, scratchSize));
213  std::cout << "Allocated " << scratchSize << " bytes of scratch memory on GPU: "
214            << "[" << d_scratch << ":" << (void*)(((char*)(d_scratch))  + scratchSize) << ")\n";

Create a pure tensor network state#

Now let’s create a pure tensor network state for a quantum circuit with the given number of qubits (vacuum state).

218  // Create the initial quantum state
219  cutensornetState_t quantumState;
220  HANDLE_CUTN_ERROR(cutensornetCreateState(cutnHandle, CUTENSORNET_STATE_PURITY_PURE, numQudits, quditDims.data(),
221                    dataType, &quantumState));
222  std::cout << "Created the initial (vacuum) quantum state\n";

Construct necessary MPO tensor network operators#

Let’s construct two MPO tensor network operators acting on different subsets of qubits.

226  // Construct the MPO tensor network operators
227  int64_t componentId;
228  cutensornetNetworkOperator_t tnOperator1;
229  HANDLE_CUTN_ERROR(cutensornetCreateNetworkOperator(cutnHandle, numQudits, quditDims.data(),
230                    dataType, &tnOperator1));
231  HANDLE_CUTN_ERROR(cutensornetNetworkOperatorAppendMPO(cutnHandle, tnOperator1, make_cuDoubleComplex(1.0, 0.0),
232                    mpoNumSites, std::vector<int32_t>({0,1,2,3}).data(),
233                    mpoModeExtentsPtr.data(), /*strides=*/nullptr,
234                    std::vector<const void*>(d_mpoTensors.cbegin(), d_mpoTensors.cend()).data(),
235                    CUTENSORNET_BOUNDARY_CONDITION_OPEN, &componentId));
236  cutensornetNetworkOperator_t tnOperator2;
237  HANDLE_CUTN_ERROR(cutensornetCreateNetworkOperator(cutnHandle, numQudits, quditDims.data(),
238                    dataType, &tnOperator2));
239  HANDLE_CUTN_ERROR(cutensornetNetworkOperatorAppendMPO(cutnHandle, tnOperator2, make_cuDoubleComplex(1.0, 0.0),
240                    mpoNumSites, std::vector<int32_t>({2,3,4,5}).data(),
241                    mpoModeExtentsPtr.data(), /*strides=*/nullptr,
242                    std::vector<const void*>(d_mpoTensors.cbegin(), d_mpoTensors.cend()).data(),
243                    CUTENSORNET_BOUNDARY_CONDITION_OPEN, &componentId));
244  std::cout << "Constructed two MPO tensor network operators\n";

Apply MPO tensor network operators to the quantum circuit state#

Let’s construct the target quantum circuit by applying MPO tensor network operators in an alternating fashion.

248  // Apply the MPO tensor network operators to the quantum state
249  for(int layer = 0; layer < mpoNumLayers; ++layer) {
250    int64_t operatorId;
251    if(layer % 2 == 0) {
252      HANDLE_CUTN_ERROR(cutensornetStateApplyNetworkOperator(cutnHandle, quantumState, tnOperator1,
253                        1, 0, 0, &operatorId));
254    }else{
255      HANDLE_CUTN_ERROR(cutensornetStateApplyNetworkOperator(cutnHandle, quantumState, tnOperator2,
256        1, 0, 0, &operatorId));
257    }
258  }
259  std::cout << "Applied " << mpoNumLayers << " MPO gates to the quantum state\n";

Request MPS factorization for the final quantum circuit state#

Here we express our intent to factorize the final state of the quantum circuit using MPS factorization. The provided shapes (mode extents) of the MPS tensors represent maximum size limits during renormalization. Computed mode extents for the final MPS tensors are less than or equal to these limits. Note: that no computation is done here yet.

263  // Specify the target MPS representation (use default strides)
264  HANDLE_CUTN_ERROR(cutensornetStateFinalizeMPS(cutnHandle, quantumState, 
265                    CUTENSORNET_BOUNDARY_CONDITION_OPEN, mpsModeExtentsPtr.data(), /*strides=*/nullptr ));
266  std::cout << "Finalized the form of the desired MPS representation\n";

Configure MPS factorization procedure#

After expressing our intent to perform MPS factorization of the final quantum circuit state, we can also configure the MPS factorization +procedure by setting different options, for example, the SVD algorithm.

270  // Set up the SVD method for bonds truncation (optional)
271  cutensornetTensorSVDAlgo_t algo = CUTENSORNET_TENSOR_SVD_ALGO_GESVDJ; 
272  HANDLE_CUTN_ERROR(cutensornetStateConfigure(cutnHandle, quantumState, 
273                    CUTENSORNET_STATE_MPS_SVD_CONFIG_ALGO, &algo, sizeof(algo)));
274  cutensornetStateMPOApplication_t mpsApplication = CUTENSORNET_STATE_MPO_APPLICATION_EXACT; 
275  // Use exact MPS-MPO application as extent of 8 can faithfully represent a 6 qubit state (optional)
276  HANDLE_CUTN_ERROR(cutensornetStateConfigure(cutnHandle, quantumState, 
277                    CUTENSORNET_STATE_CONFIG_MPS_MPO_APPLICATION, &mpsApplication, sizeof(mpsApplication)));
278  std::cout << "Configured the MPS computation\n";

Prepare the computation of MPS factorization#

Let’s create a workspace descriptor and prepare the computation of the MPS factorization of the final quantum circuit state.

282  // Prepare the MPS computation and attach a workspace buffer
283  cutensornetWorkspaceDescriptor_t workDesc;
284  HANDLE_CUTN_ERROR(cutensornetCreateWorkspaceDescriptor(cutnHandle, &workDesc));
285  std::cout << "Created the workspace descriptor\n";
286  HANDLE_CUTN_ERROR(cutensornetStatePrepare(cutnHandle, quantumState, scratchSize, workDesc, 0x0));
287  int64_t worksize {0};
288  HANDLE_CUTN_ERROR(cutensornetWorkspaceGetMemorySize(cutnHandle,
289                                                      workDesc,
290                                                      CUTENSORNET_WORKSIZE_PREF_RECOMMENDED,
291                                                      CUTENSORNET_MEMSPACE_DEVICE,
292                                                      CUTENSORNET_WORKSPACE_SCRATCH,
293                                                      &worksize));
294  std::cout << "Scratch GPU workspace size (bytes) for the MPS computation = " << worksize << std::endl;
295  if(worksize <= scratchSize) {
296    HANDLE_CUTN_ERROR(cutensornetWorkspaceSetMemory(cutnHandle, workDesc, CUTENSORNET_MEMSPACE_DEVICE,
297                      CUTENSORNET_WORKSPACE_SCRATCH, d_scratch, worksize));
298  }else{
299    std::cout << "ERROR: Insufficient workspace size on Device!\n";
300    std::abort();
301  }
302
303  // Query the host scratch workspace size and attach a host scratch buffer if one is required
304  HANDLE_CUTN_ERROR(cutensornetWorkspaceGetMemorySize(cutnHandle, workDesc, CUTENSORNET_WORKSIZE_PREF_RECOMMENDED,
305                                                      CUTENSORNET_MEMSPACE_HOST, CUTENSORNET_WORKSPACE_SCRATCH, &hostWorksize));
306  std::cout << "Scratch Host workspace size (bytes) = " << hostWorksize << std::endl;
307  if(hostWorksize > 0) {
308    h_scratch = std::realloc(h_scratch, hostWorksize);
309    if(h_scratch == nullptr) { std::cout << "ERROR: Host scratch allocation failed!\n"; std::abort(); }
310    HANDLE_CUTN_ERROR(cutensornetWorkspaceSetMemory(cutnHandle, workDesc, CUTENSORNET_MEMSPACE_HOST,
311                      CUTENSORNET_WORKSPACE_SCRATCH, h_scratch, hostWorksize));
312  }
313  std::cout << "Set the workspace buffer and prepared the MPS computation\n";

Compute MPS factorization#

Once the MPS factorization procedure has been configured and prepared, let’s compute the MPS factorization of the final quantum circuit state.

317  // Compute the MPS factorization of the quantum circuit state
318  HANDLE_CUTN_ERROR(cutensornetStateCompute(cutnHandle, quantumState, 
319                    workDesc, mpsModeExtentsPtr.data(), /*strides=*/nullptr, d_mpsTensors.data(), 0));
320  std::cout << "Computed the MPS factorization of the quantum circuit state\n";
321  

Create the tensor network state sampler#

Once the quantum circuit state has been constructed and factorized using the MPS representation, let’s create the tensor network state sampler for all qubits.

324  // Create the quantum circuit sampler
325  cutensornetStateSampler_t sampler;
326  HANDLE_CUTN_ERROR(cutensornetCreateSampler(cutnHandle, quantumState, numQudits, nullptr, &sampler));
327  std::cout << "Created the quantum circuit sampler\n";

Configure the tensor network state sampler#

Optionally, we can configure the tensor network state sampler by setting the number of hyper-samples to be used by the tensor network contraction path finder.

331  // Configure the quantum circuit sampler
332  const int32_t numHyperSamples = 8; // desired number of hyper samples used in the tensor network contraction path finder
333  HANDLE_CUTN_ERROR(cutensornetSamplerConfigure(cutnHandle, sampler,
334                    CUTENSORNET_SAMPLER_OPT_NUM_HYPER_SAMPLES, &numHyperSamples, sizeof(numHyperSamples)));

Prepare the tensor network state sampler#

Now let’s prepare the tensor network state sampler.

338  // Prepare the quantum circuit sampler
339  HANDLE_CUTN_ERROR(cutensornetSamplerPrepare(cutnHandle, sampler, scratchSize, workDesc, 0x0));
340  std::cout << "Prepared the quantum circuit state sampler\n";

Set up the workspace#

Now we can set up the required workspace buffer.

344  // Attach the workspace buffer
345  HANDLE_CUTN_ERROR(cutensornetWorkspaceGetMemorySize(cutnHandle,
346                                                      workDesc,
347                                                      CUTENSORNET_WORKSIZE_PREF_RECOMMENDED,
348                                                      CUTENSORNET_MEMSPACE_DEVICE,
349                                                      CUTENSORNET_WORKSPACE_SCRATCH,
350                                                      &worksize));
351  std::cout << "Scratch GPU workspace size (bytes) for MPS Sampling = " << worksize << std::endl;
352  assert(worksize > 0);
353  if(worksize <= scratchSize) {
354    HANDLE_CUTN_ERROR(cutensornetWorkspaceSetMemory(cutnHandle, workDesc, CUTENSORNET_MEMSPACE_DEVICE,
355                      CUTENSORNET_WORKSPACE_SCRATCH, d_scratch, worksize));
356  }else{
357    std::cout << "ERROR: Insufficient workspace size on Device!\n";
358    std::abort();
359  }
360
361  // Query the host scratch workspace size and attach a host scratch buffer if one is required
362  HANDLE_CUTN_ERROR(cutensornetWorkspaceGetMemorySize(cutnHandle, workDesc, CUTENSORNET_WORKSIZE_PREF_RECOMMENDED,
363                                                      CUTENSORNET_MEMSPACE_HOST, CUTENSORNET_WORKSPACE_SCRATCH, &hostWorksize));
364  std::cout << "Scratch Host workspace size (bytes) = " << hostWorksize << std::endl;
365  if(hostWorksize > 0) {
366    h_scratch = std::realloc(h_scratch, hostWorksize);
367    if(h_scratch == nullptr) { std::cout << "ERROR: Host scratch allocation failed!\n"; std::abort(); }
368    HANDLE_CUTN_ERROR(cutensornetWorkspaceSetMemory(cutnHandle, workDesc, CUTENSORNET_MEMSPACE_HOST,
369                      CUTENSORNET_WORKSPACE_SCRATCH, h_scratch, hostWorksize));
370  }
371  std::cout << "Set the workspace buffer\n";

Perform sampling of the final quantum circuit state#

Once everything had been set up, we perform sampling of the final MPS-factorized quantum circuit state and print the output samples.

375  // Sample the quantum circuit state
376  std::vector<int64_t> samples(numQudits * numSamples); // samples[SampleId][QuditId] reside in Host memory
377  HANDLE_CUTN_ERROR(cutensornetSamplerSample(cutnHandle, sampler, numSamples, workDesc, samples.data(), 0));
378  std::cout << "Performed quantum circuit state sampling\n";
379  std::cout << "Generated samples:\n";
380  for(int64_t i = 0; i < numSamples; ++i) {
381    for(int64_t j = 0; j < numQudits; ++j) std::cout << " " << samples[i * numQudits + j];
382    std::cout << std::endl;
383  }

Free resources#

387  // Destroy the quantum circuit sampler
388  HANDLE_CUTN_ERROR(cutensornetDestroySampler(sampler));
389  std::cout << "Destroyed the quantum circuit state sampler\n";
390
391  // Destroy the workspace descriptor
392  HANDLE_CUTN_ERROR(cutensornetDestroyWorkspaceDescriptor(workDesc));
393  std::cout << "Destroyed the workspace descriptor\n";
394
395  // Destroy the MPO tensor network operators
396  HANDLE_CUTN_ERROR(cutensornetDestroyNetworkOperator(tnOperator2));
397  HANDLE_CUTN_ERROR(cutensornetDestroyNetworkOperator(tnOperator1));
398  std::cout << "Destroyed the MPO tensor network operators\n";
399
400  // Destroy the quantum circuit state
401  HANDLE_CUTN_ERROR(cutensornetDestroyState(quantumState));
402  std::cout << "Destroyed the quantum circuit state\n";
403
404  // Free GPU buffers
405  std::free(h_scratch);
406  HANDLE_CUDA_ERROR(cudaFree(d_scratch));
407  for (int32_t i = 0; i < numQudits; ++i) {
408    HANDLE_CUDA_ERROR(cudaFree(d_mpsTensors[i]));
409  }
410  for (int32_t i = 0; i < mpoNumSites; ++i) {
411    HANDLE_CUDA_ERROR(cudaFree(d_mpoTensors[i]));
412  }
413  std::cout << "Freed memory on GPU\n";
414  
415  // Finalize the cuTensorNet library
416  HANDLE_CUTN_ERROR(cutensornetDestroy(cutnHandle));
417  std::cout << "Finalized the cuTensorNet library\n";
418
419  return 0;
420}