示例

在本节中,我们将展示如何使用 cuTensorNet 收缩张量网络。首先,我们描述如何编译示例代码。然后,我们展示一个示例代码,用于执行 cuTensorNet 中的常用步骤。在该示例中,我们执行以下张量收缩

\[R_{k,l} = A_{a,b,c,d,e,f} B_{b,g,h,e,i,j} C_{m,a,g,f,i,k} D_{l,c,h,d,j,m}\]

我们逐步构建代码,每个步骤都在末尾添加代码。这些步骤通过简洁的多行注释块分隔。

建议读者参考 概述cuTENSOR 文档,以熟悉术语和 cuTENSOR 操作。

编译代码

假设 cuQuantum 已在 CUQUANTUM_ROOT 中解压,cuTENSOR 已在 CUTENSOR_ROOT 中解压,我们按如下方式更新库路径

export LD_LIBRARY_PATH=${CUQUANTUM_ROOT}/lib:${CUTENSOR_ROOT}/lib/11:${LD_LIBRARY_PATH}

根据您的 CUDA 工具包,您可能需要选择不同的库版本(例如,${CUTENSOR_ROOT}/lib/11.0)。

下面讨论的串行示例代码 (tensornet_example.cu) 可以通过以下命令编译

nvcc tensornet_example.cu -I${CUQUANTUM_ROOT}/include -I${CUTENSOR_ROOT}/include -L${CUQUANTUM_ROOT}/lib -L${CUTENSOR_ROOT}/lib/11 -lcutensornet -lcutensor -o tensornet_example

对于与 cuTensorNet 库的静态链接,请使用以下命令(请注意,libmetis_static.a 需要显式链接,假设它通过 NVIDIA CUDA 工具包安装并且可以通过 $LIBRARY_PATH 访问)

nvcc tensornet_example.cu -I${CUQUANTUM_ROOT}/include -I${CUTENSOR_ROOT}/include ${CUQUANTUM_ROOT}/lib/libcutensornet_static.a -L${CUTENSOR_DIR}/lib/11 -lcutensor libmetis_static.a -o tensornet_example

为了构建示例的并行 (MPI) 版本(tensornet_example_mpi_auto.cutensornet_example_mpi.cu),您将需要安装 MPI 库(例如,最新的 Open MPI、MVAPICH 或 MPICH)。特别是,自动并行示例需要CUDA-aware MPI,请参阅下面的 代码示例(自动切片分布式并行化)。在这种情况下,您将需要将 -I${MPI_PATH}/include-L${MPI_PATH}/lib -lmpi 添加到构建命令

nvcc tensornet_example_mpi_auto.cu -I${CUQUANTUM_ROOT}/include -I${CUTENSOR_ROOT}/include -I${MPI_PATH}/include -L${CUQUANTUM_ROOT}/lib -L${CUTENSOR_ROOT}/lib/11 -lcutensornet -lcutensor -L${MPI_PATH}/lib -lmpi -o tensornet_example_mpi_auto
nvcc tensornet_example_mpi.cu -I${CUQUANTUM_ROOT}/include -I${CUTENSOR_ROOT}/include -I${MPI_PATH}/include -L${CUQUANTUM_ROOT}/lib -L${CUTENSOR_ROOT}/lib/11 -lcutensornet -lcutensor -L${MPI_PATH}/lib -lmpi -o tensornet_example_mpi

警告

当在没有 CUDA-aware MPI 的情况下运行 tensornet_example_mpi_auto.cu 时,程序将崩溃。

注意

根据 cuQuantum 包的来源,您可能需要将上面的 lib 替换为 lib64

代码示例(串行)

以下代码示例说明了使用 cuTensorNet 所需的常用步骤,并介绍了典型的张量网络操作。完整的示例代码可以在 NVIDIA/cuQuantum 存储库中找到(此处)。

头文件和数据类型

  8#include <stdlib.h>
  9#include <stdio.h>
 10
 11#include <unordered_map>
 12#include <vector>
 13#include <cassert>
 14
 15#include <cuda_runtime.h>
 16#include <cutensornet.h>
 17
 18
 19#define HANDLE_ERROR(x)                                           \
 20{ const auto err = x;                                             \
 21  if( err != CUTENSORNET_STATUS_SUCCESS )                         \
 22  { printf("Error: %s in line %d\n", cutensornetGetErrorString(err), __LINE__); \
 23    fflush(stdout);                                               \
 24  }                                                               \
 25};
 26
 27#define HANDLE_CUDA_ERROR(x)                                      \
 28{ const auto err = x;                                             \
 29  if( err != cudaSuccess )                                        \
 30  { printf("CUDA Error: %s in line %d\n", cudaGetErrorString(err), __LINE__); \
 31    fflush(stdout);                                               \
 32  }                                                               \
 33};
 34
 35
 36struct GPUTimer
 37{
 38    GPUTimer(cudaStream_t stream): stream_(stream)
 39    {
 40        HANDLE_CUDA_ERROR(cudaEventCreate(&start_));
 41        HANDLE_CUDA_ERROR(cudaEventCreate(&stop_));
 42    }
 43
 44    ~GPUTimer()
 45    {
 46        HANDLE_CUDA_ERROR(cudaEventDestroy(start_));
 47        HANDLE_CUDA_ERROR(cudaEventDestroy(stop_));
 48    }
 49
 50    void start()
 51    {
 52        HANDLE_CUDA_ERROR(cudaEventRecord(start_, stream_));
 53    }
 54
 55    float seconds()
 56    {
 57        HANDLE_CUDA_ERROR(cudaEventRecord(stop_, stream_));
 58        HANDLE_CUDA_ERROR(cudaEventSynchronize(stop_));
 59        float time;
 60        HANDLE_CUDA_ERROR(cudaEventElapsedTime(&time, start_, stop_));
 61        return time * 1e-3;
 62    }
 63
 64    private:
 65    cudaEvent_t start_, stop_;
 66    cudaStream_t stream_;
 67};
 68
 69
 70int main()
 71{
 72   static_assert(sizeof(size_t) == sizeof(int64_t), "Please build this sample on a 64-bit architecture!");
 73
 74   bool verbose = true;
 75
 76   // Check cuTensorNet version
 77   const size_t cuTensornetVersion = cutensornetGetVersion();
 78   if(verbose)
 79      printf("cuTensorNet version: %ld\n", cuTensornetVersion);
 80
 81   // Set GPU device
 82   int numDevices {0};
 83   HANDLE_CUDA_ERROR( cudaGetDeviceCount(&numDevices) );
 84   const int deviceId = 0;
 85   HANDLE_CUDA_ERROR( cudaSetDevice(deviceId) );
 86   cudaDeviceProp prop;
 87   HANDLE_CUDA_ERROR( cudaGetDeviceProperties(&prop, deviceId) );
 88
 89   if(verbose) {
 90      printf("===== device info ======\n");
 91      printf("GPU-local-id:%d\n", deviceId);
 92      printf("GPU-name:%s\n", prop.name);
 93      printf("GPU-clock:%d\n", prop.clockRate);
 94      printf("GPU-memoryClock:%d\n", prop.memoryClockRate);
 95      printf("GPU-nSM:%d\n", prop.multiProcessorCount);
 96      printf("GPU-major:%d\n", prop.major);
 97      printf("GPU-minor:%d\n", prop.minor);
 98      printf("========================\n");
 99   }
100
101   typedef float floatType;
102   cudaDataType_t typeData = CUDA_R_32F;
103   cutensornetComputeType_t typeCompute = CUTENSORNET_COMPUTE_32F;
104
105   if(verbose)
106      printf("Included headers and defined data types\n");

定义张量网络和张量大小

接下来,我们定义张量网络的拓扑结构(即,张量的模式、它们的范围以及它们的连通性)。

110   /**********************
111   * Computing: R_{k,l} = A_{a,b,c,d,e,f} B_{b,g,h,e,i,j} C_{m,a,g,f,i,k} D_{l,c,h,d,j,m}
112   **********************/
113
114   constexpr int32_t numInputs = 4;
115
116   // Create vectors of tensor modes
117   std::vector<int32_t> modesA{'a','b','c','d','e','f'};
118   std::vector<int32_t> modesB{'b','g','h','e','i','j'};
119   std::vector<int32_t> modesC{'m','a','g','f','i','k'};
120   std::vector<int32_t> modesD{'l','c','h','d','j','m'};
121   std::vector<int32_t> modesR{'k','l'};
122
123   // Set mode extents
124   std::unordered_map<int32_t, int64_t> extent;
125   extent['a'] = 16;
126   extent['b'] = 16;
127   extent['c'] = 16;
128   extent['d'] = 16;
129   extent['e'] = 16;
130   extent['f'] = 16;
131   extent['g'] = 16;
132   extent['h'] = 16;
133   extent['i'] = 16;
134   extent['j'] = 16;
135   extent['k'] = 16;
136   extent['l'] = 16;
137   extent['m'] = 16;
138
139   // Create a vector of extents for each tensor
140   std::vector<int64_t> extentA;
141   for (auto mode : modesA)
142      extentA.push_back(extent[mode]);
143   std::vector<int64_t> extentB;
144   for (auto mode : modesB)
145      extentB.push_back(extent[mode]);
146   std::vector<int64_t> extentC;
147   for (auto mode : modesC)
148      extentC.push_back(extent[mode]);
149   std::vector<int64_t> extentD;
150   for (auto mode : modesD)
151      extentD.push_back(extent[mode]);
152   std::vector<int64_t> extentR;
153   for (auto mode : modesR)
154      extentR.push_back(extent[mode]);
155
156   if(verbose)
157      printf("Defined tensor network, modes, and extents\n");

分配内存并初始化数据

接下来,我们为张量网络操作数分配内存并将它们初始化为随机值。

160   /**********************
161   * Allocating data
162   **********************/
163
164   size_t elementsA = 1;
165   for (auto mode : modesA)
166      elementsA *= extent[mode];
167   size_t elementsB = 1;
168   for (auto mode : modesB)
169      elementsB *= extent[mode];
170   size_t elementsC = 1;
171   for (auto mode : modesC)
172      elementsC *= extent[mode];
173   size_t elementsD = 1;
174   for (auto mode : modesD)
175      elementsD *= extent[mode];
176   size_t elementsR = 1;
177   for (auto mode : modesR)
178      elementsR *= extent[mode];
179
180   size_t sizeA = sizeof(floatType) * elementsA;
181   size_t sizeB = sizeof(floatType) * elementsB;
182   size_t sizeC = sizeof(floatType) * elementsC;
183   size_t sizeD = sizeof(floatType) * elementsD;
184   size_t sizeR = sizeof(floatType) * elementsR;
185   if(verbose)
186      printf("Total GPU memory used for tensor storage: %.2f GiB\n",
187             (sizeA + sizeB + sizeC + sizeD + sizeR) / 1024. /1024. / 1024);
188
189   void* rawDataIn_d[numInputs];
190   void* R_d;
191   HANDLE_CUDA_ERROR( cudaMalloc((void**) &rawDataIn_d[0], sizeA) );
192   HANDLE_CUDA_ERROR( cudaMalloc((void**) &rawDataIn_d[1], sizeB) );
193   HANDLE_CUDA_ERROR( cudaMalloc((void**) &rawDataIn_d[2], sizeC) );
194   HANDLE_CUDA_ERROR( cudaMalloc((void**) &rawDataIn_d[3], sizeD) );
195   HANDLE_CUDA_ERROR( cudaMalloc((void**) &R_d, sizeR));
196
197   floatType *A = (floatType*) malloc(sizeof(floatType) * elementsA);
198   floatType *B = (floatType*) malloc(sizeof(floatType) * elementsB);
199   floatType *C = (floatType*) malloc(sizeof(floatType) * elementsC);
200   floatType *D = (floatType*) malloc(sizeof(floatType) * elementsD);
201   floatType *R = (floatType*) malloc(sizeof(floatType) * elementsR);
202
203   if (A == NULL || B == NULL || C == NULL || D == NULL || R == NULL)
204   {
205      printf("Error: Host memory allocation failed!\n");
206      return -1;
207   }
208
209   /*******************
210   * Initialize data
211   *******************/
212
213   memset(R, 0, sizeof(floatType) * elementsR);
214   for (uint64_t i = 0; i < elementsA; i++)
215      A[i] = ((floatType) rand()) / RAND_MAX;
216   for (uint64_t i = 0; i < elementsB; i++)
217      B[i] = ((floatType) rand()) / RAND_MAX;
218   for (uint64_t i = 0; i < elementsC; i++)
219      C[i] = ((floatType) rand()) / RAND_MAX;
220   for (uint64_t i = 0; i < elementsD; i++)
221      D[i] = ((floatType) rand()) / RAND_MAX;
222
223   HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[0], A, sizeA, cudaMemcpyHostToDevice) );
224   HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[1], B, sizeB, cudaMemcpyHostToDevice) );
225   HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[2], C, sizeC, cudaMemcpyHostToDevice) );
226   HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[3], D, sizeD, cudaMemcpyHostToDevice) );
227
228   if(verbose)
229      printf("Allocated GPU memory for data, and initialize data\n");

cuTensorNet 句柄和网络描述符

接下来,我们通过 cutensornetCreate() 初始化 cuTensorNet 库,并使用所需的张量模式、范围和步幅以及数据和计算类型创建网络描述符。请注意,创建的库上下文将与当前活动的 GPU 关联。

232   /*************************
233   * cuTensorNet
234   *************************/
235
236   cudaStream_t stream;
237   HANDLE_CUDA_ERROR( cudaStreamCreate(&stream) );
238
239   cutensornetHandle_t handle;
240   HANDLE_ERROR( cutensornetCreate(&handle) );
241
242   const int32_t nmodeA = modesA.size();
243   const int32_t nmodeB = modesB.size();
244   const int32_t nmodeC = modesC.size();
245   const int32_t nmodeD = modesD.size();
246   const int32_t nmodeR = modesR.size();
247
248   /*******************************
249   * Create Network Descriptor
250   *******************************/
251
252   const int32_t* modesIn[] = {modesA.data(), modesB.data(), modesC.data(), modesD.data()};
253   int32_t const numModesIn[] = {nmodeA, nmodeB, nmodeC, nmodeD};
254   const int64_t* extentsIn[] = {extentA.data(), extentB.data(), extentC.data(), extentD.data()};
255   const int64_t* stridesIn[] = {NULL, NULL, NULL, NULL}; // strides are optional; if no stride is provided, cuTensorNet assumes a generalized column-major data layout
256
257   // Set up tensor network
258   cutensornetNetworkDescriptor_t descNet;
259   HANDLE_ERROR( cutensornetCreateNetworkDescriptor(handle,
260                     numInputs, numModesIn, extentsIn, stridesIn, modesIn, NULL,
261                     nmodeR, extentR.data(), /*stridesOut = */NULL, modesR.data(),
262                     typeData, typeCompute,
263                     &descNet) );
264
265   if(verbose)
266      printf("Initialized the cuTensorNet library and created a tensor network descriptor\n");

最佳收缩顺序和切片

在此阶段,我们可以部署 cuTensorNet 优化器来查找优化的收缩路径和切片组合。我们根据可用内存资源选择执行收缩所需的工作区限制,并将其作为约束提供给优化器。然后,我们创建 cutensornetContractionOptimizerConfig_t 类型的优化器配置对象,以指定各种优化器选项,并将其提供给通过 cutensornetContractionOptimize() 调用的优化器。优化器的结果将在 cutensornetContractionOptimizerInfo_t 类型的优化器信息对象中返回。

269   /*******************************
270   * Choose workspace limit based on available resources.
271   *******************************/
272
273   size_t freeMem, totalMem;
274   HANDLE_CUDA_ERROR( cudaMemGetInfo(&freeMem, &totalMem) );
275   uint64_t workspaceLimit = (uint64_t)((double)freeMem * 0.9);
276   if(verbose)
277      printf("Workspace limit = %lu\n", workspaceLimit);
278
279   /*******************************
280   * Find "optimal" contraction order and slicing
281   *******************************/
282
283   cutensornetContractionOptimizerConfig_t optimizerConfig;
284   HANDLE_ERROR( cutensornetCreateContractionOptimizerConfig(handle, &optimizerConfig) );
285
286   // Set the desired number of hyper-samples (defaults to 0)
287   int32_t num_hypersamples = 8;
288   HANDLE_ERROR( cutensornetContractionOptimizerConfigSetAttribute(handle,
289                     optimizerConfig,
290                     CUTENSORNET_CONTRACTION_OPTIMIZER_CONFIG_HYPER_NUM_SAMPLES,
291                     &num_hypersamples,
292                     sizeof(num_hypersamples)) );
293
294   // Create contraction optimizer info and find an optimized contraction path
295   cutensornetContractionOptimizerInfo_t optimizerInfo;
296   HANDLE_ERROR( cutensornetCreateContractionOptimizerInfo(handle, descNet, &optimizerInfo) );
297
298   HANDLE_ERROR( cutensornetContractionOptimize(handle,
299                                             descNet,
300                                             optimizerConfig,
301                                             workspaceLimit,
302                                             optimizerInfo) );
303
304   // Query the number of slices the tensor network execution will be split into
305   int64_t numSlices = 0;
306   HANDLE_ERROR( cutensornetContractionOptimizerInfoGetAttribute(
307                  handle,
308                  optimizerInfo,
309                  CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_NUM_SLICES,
310                  &numSlices,
311                  sizeof(numSlices)) );
312   assert(numSlices > 0);
313
314   if(verbose)
315      printf("Found an optimized contraction path using cuTensorNet optimizer\n");

也可以绕过 cuTensorNet 优化器,并通过 cutensornetContractionOptimizerInfoSetAttribute() 将预定的收缩路径以及切片信息直接导入到优化器信息对象。

创建工作区描述符并分配工作区内存

接下来,我们创建一个工作区描述符,计算工作区大小,并查询收缩网络所需的最小工作区大小。然后,我们为工作区分配设备内存,并在工作区描述符中设置它。工作区描述符将提供给收缩计划。

318   /*******************************
319   * Create workspace descriptor, allocate workspace, and set it.
320   *******************************/
321
322   cutensornetWorkspaceDescriptor_t workDesc;
323   HANDLE_ERROR( cutensornetCreateWorkspaceDescriptor(handle, &workDesc) );
324
325   int64_t requiredWorkspaceSize = 0;
326   HANDLE_ERROR( cutensornetWorkspaceComputeContractionSizes(handle,
327                                                         descNet,
328                                                         optimizerInfo,
329                                                         workDesc) );
330
331   HANDLE_ERROR( cutensornetWorkspaceGetMemorySize(handle,
332                                                   workDesc,
333                                                   CUTENSORNET_WORKSIZE_PREF_MIN,
334                                                   CUTENSORNET_MEMSPACE_DEVICE,
335                                                   CUTENSORNET_WORKSPACE_SCRATCH,
336                                                   &requiredWorkspaceSize) );
337
338   void* work = nullptr;
339   HANDLE_CUDA_ERROR( cudaMalloc(&work, requiredWorkspaceSize) );
340
341   HANDLE_ERROR( cutensornetWorkspaceSetMemory(handle,
342                                               workDesc,
343                                               CUTENSORNET_MEMSPACE_DEVICE,
344                                               CUTENSORNET_WORKSPACE_SCRATCH,
345                                               work,
346                                               requiredWorkspaceSize) );
347
348   if(verbose)
349      printf("Allocated and set up the GPU workspace\n");

收缩计划和自动调优

我们创建一个张量网络收缩计划,其中包含 cuTENSOR 的所有成对收缩计划。可选地,我们可以自动调整计划,以便 cuTENSOR 为每个成对收缩选择最佳内核。此收缩计划可以重用于许多(可能不同的)数据输入,从而避免冗余地初始化此计划的成本。

352   /*******************************
353   * Initialize the pairwise contraction plan (for cuTENSOR).
354   *******************************/
355
356   cutensornetContractionPlan_t plan;
357   HANDLE_ERROR( cutensornetCreateContractionPlan(handle,
358                                                descNet,
359                                                optimizerInfo,
360                                                workDesc,
361                                                &plan) );
362
363   /*******************************
364   * Optional: Auto-tune cuTENSOR's cutensorContractionPlan to pick the fastest kernel
365   *           for each pairwise tensor contraction.
366   *******************************/
367   cutensornetContractionAutotunePreference_t autotunePref;
368   HANDLE_ERROR( cutensornetCreateContractionAutotunePreference(handle,
369                                                      &autotunePref) );
370
371   const int numAutotuningIterations = 5; // may be 0
372   HANDLE_ERROR( cutensornetContractionAutotunePreferenceSetAttribute(
373                           handle,
374                           autotunePref,
375                           CUTENSORNET_CONTRACTION_AUTOTUNE_MAX_ITERATIONS,
376                           &numAutotuningIterations,
377                           sizeof(numAutotuningIterations)) );
378
379   // Modify the plan again to find the best pair-wise contractions
380   HANDLE_ERROR( cutensornetContractionAutotune(handle,
381                                                plan,
382                                                rawDataIn_d,
383                                                R_d,
384                                                workDesc,
385                                                autotunePref,
386                                                stream) );
387
388   HANDLE_ERROR( cutensornetDestroyContractionAutotunePreference(autotunePref) );
389
390   if(verbose)
391      printf("Created a contraction plan for cuTensorNet and optionally auto-tuned it\n");

张量网络收缩执行

最后,我们根据需要多次收缩张量网络,每次可能使用不同的输入。张量网络切片(捕获为 cutensornetSliceGroup_t 对象)使用相同的收缩计划计算。为方便起见,当目标是收缩网络中的所有切片时,可以将 NULL 提供给 cutensornetContractSlices() 函数,而不是切片组。我们还清理并释放分配的资源。

394   /**********************
395   * Execute the tensor network contraction
396   **********************/
397
398   // Create a cutensornetSliceGroup_t object from a range of slice IDs
399   cutensornetSliceGroup_t sliceGroup{};
400   HANDLE_ERROR( cutensornetCreateSliceGroupFromIDRange(handle, 0, numSlices, 1, &sliceGroup) );
401
402   GPUTimer timer {stream};
403   double minTimeCUTENSORNET = 1e100;
404   const int numRuns = 3; // number of repeats to get stable performance results
405   for (int i = 0; i < numRuns; ++i)
406   {
407      HANDLE_CUDA_ERROR( cudaMemcpy(R_d, R, sizeR, cudaMemcpyHostToDevice) ); // restore the output tensor on GPU
408      HANDLE_CUDA_ERROR( cudaDeviceSynchronize() );
409
410      /*
411      * Contract all slices of the tensor network
412      */
413      timer.start();
414
415      int32_t accumulateOutput = 0; // output tensor data will be overwritten
416      HANDLE_ERROR( cutensornetContractSlices(handle,
417                     plan,
418                     rawDataIn_d,
419                     R_d,
420                     accumulateOutput,
421                     workDesc,
422                     sliceGroup, // alternatively, NULL can also be used to contract over all slices instead of specifying a sliceGroup object
423                     stream) );
424
425      // Synchronize and measure best timing
426      auto time = timer.seconds();
427      minTimeCUTENSORNET = (time > minTimeCUTENSORNET) ? minTimeCUTENSORNET : time;
428   }
429
430   if(verbose)
431      printf("Contracted the tensor network, each slice used the same contraction plan\n");
432
433   // Print the 1-norm of the output tensor (verification)
434   HANDLE_CUDA_ERROR( cudaStreamSynchronize(stream) );
435   HANDLE_CUDA_ERROR( cudaMemcpy(R, R_d, sizeR, cudaMemcpyDeviceToHost) ); // restore the output tensor on Host
436   double norm1 = 0.0;
437   for (int64_t i = 0; i < elementsR; ++i) {
438      norm1 += std::abs(R[i]);
439   }
440   if(verbose)
441      printf("Computed the 1-norm of the output tensor: %e\n", norm1);
442
443   /*************************/
444
445   // Query the total Flop count for the tensor network contraction
446   double flops {0.0};
447   HANDLE_ERROR( cutensornetContractionOptimizerInfoGetAttribute(
448                     handle,
449                     optimizerInfo,
450                     CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_FLOP_COUNT,
451                     &flops,
452                     sizeof(flops)) );
453
454   if(verbose) {
455      printf("Number of tensor network slices = %ld\n", numSlices);
456      printf("Tensor network contraction time (ms) = %.3f\n", minTimeCUTENSORNET * 1000.f);
457   }
458
459   // Free cuTensorNet resources
460   HANDLE_ERROR( cutensornetDestroySliceGroup(sliceGroup) );
461   HANDLE_ERROR( cutensornetDestroyContractionPlan(plan) );
462   HANDLE_ERROR( cutensornetDestroyWorkspaceDescriptor(workDesc) );
463   HANDLE_ERROR( cutensornetDestroyContractionOptimizerInfo(optimizerInfo) );
464   HANDLE_ERROR( cutensornetDestroyContractionOptimizerConfig(optimizerConfig) );
465   HANDLE_ERROR( cutensornetDestroyNetworkDescriptor(descNet) );
466   HANDLE_ERROR( cutensornetDestroy(handle) );
467
468   // Free Host memory resources
469   if (R) free(R);
470   if (D) free(D);
471   if (C) free(C);
472   if (B) free(B);
473   if (A) free(A);
474
475   // Free GPU memory resources
476   if (work) cudaFree(work);
477   if (R_d) cudaFree(R_d);
478   if (rawDataIn_d[0]) cudaFree(rawDataIn_d[0]);
479   if (rawDataIn_d[1]) cudaFree(rawDataIn_d[1]);
480   if (rawDataIn_d[2]) cudaFree(rawDataIn_d[2]);
481   if (rawDataIn_d[3]) cudaFree(rawDataIn_d[3]);
482
483   if(verbose)
484      printf("Freed resources and exited\n");
485
486   return 0;
487}

回想一下,完整的示例代码可以在 NVIDIA/cuQuantum 存储库中找到(此处)。

代码示例(自动切片分布式并行化)

可以轻松地调整 代码示例(串行) 并启用跨多个/许多 GPU 设备(跨多个/许多节点)的自动并行执行。我们将通过使用消息传递接口 (MPI) 作为通信层的示例来说明这一点。下面我们展示了为了启用分布式并行执行而需要进行的少量添加,而无需对原始串行源代码进行任何更改。完整的 MPI 自动示例代码可以在 NVIDIA/cuQuantum 存储库中找到。为了启用自动并行性,cuTensorNet 要求

  • 环境变量 $CUTENSORNET_COMM_LIB 设置为包装器共享库 libcutensornet_distributed_interface_mpi.so 的路径,并且

  • 可执行文件链接到 CUDA-aware MPI 库

上面安装指南中给出了设置这些的详细说明。

首先,除了 头文件和数据类型 中提到的头文件和定义之外,我们还包括 MPI 头文件并定义一个宏来处理 MPI 错误。我们还需要初始化 MPI 服务,并为每个 MPI 进程分配一个唯一的 GPU 设备,该设备稍后将与 MPI 进程内部创建的 cuTensorNet 库句柄关联。

20#include <mpi.h>
44#define HANDLE_MPI_ERROR(x)                                       \
45{ const auto err = x;                                             \
46  if( err != MPI_SUCCESS )                                        \
47  { char error[MPI_MAX_ERROR_STRING]; int len;                    \
48    MPI_Error_string(err, error, &len);                           \
49    printf("MPI Error: %s in line %d\n", error, __LINE__);        \
50    fflush(stdout);                                               \
51    MPI_Abort(MPI_COMM_WORLD, err);                               \
52  }                                                               \
53};

MPI 服务初始化必须在第一个 cutensornetCreate() 调用之前进行,该调用创建 cuTensorNet 库句柄。尝试在初始化 MPI 服务之前调用 cutensornetCreate() 将导致错误。

 97   // Initialize MPI
 98   HANDLE_MPI_ERROR( MPI_Init(&argc, &argv) );
 99   int rank {-1};
100   HANDLE_MPI_ERROR( MPI_Comm_rank(MPI_COMM_WORLD, &rank) );
101   int numProcs {0};
102   HANDLE_MPI_ERROR( MPI_Comm_size(MPI_COMM_WORLD, &numProcs) );

如果位于同一节点上的多个 GPU 设备对 MPI 进程可见,我们需要为每个 MPI 进程选择一个独占 GPU 设备。如果您的 MPI 库实现提供的 mpirun(或 mpiexec)命令设置了一个环境变量,该变量显示 MPI 进程在其调用期间的排名,则可以使用该环境变量来设置 CUDA_VISIBLE_DEVICES 以指向专门分配给 MPI 进程的特定单个 GPU 设备(例如,Open MPI 为此目的提供了 ${OMPI_COMM_WORLD_LOCAL_RANK})。否则,可以手动设置 GPU 设备,如下所示。

122   // Set GPU device based on ranks and nodes
123   int numDevices {0};
124   HANDLE_CUDA_ERROR( cudaGetDeviceCount(&numDevices) );
125   const int deviceId = rank % numDevices; // we assume that the processes are mapped to nodes in contiguous chunks
126   HANDLE_CUDA_ERROR( cudaSetDevice(deviceId) );
127   cudaDeviceProp prop;
128   HANDLE_CUDA_ERROR( cudaGetDeviceProperties(&prop, deviceId) );

接下来,我们按照 定义张量网络和张量大小 中所述定义张量网络。在每个进程一个 GPU 设备模型中,张量网络(包括操作数和结果数据)在每个进程上复制。根进程初始化输入数据并将其广播到其他进程。

258   /*******************
259   * Initialize data
260   *******************/
261
262   memset(R, 0, sizeof(floatType) * elementsR);
263   if(rank == 0)
264   {
265      for (uint64_t i = 0; i < elementsA; i++)
266         A[i] = ((floatType) rand()) / RAND_MAX;
267      for (uint64_t i = 0; i < elementsB; i++)
268         B[i] = ((floatType) rand()) / RAND_MAX;
269      for (uint64_t i = 0; i < elementsC; i++)
270         C[i] = ((floatType) rand()) / RAND_MAX;
271      for (uint64_t i = 0; i < elementsD; i++)
272         D[i] = ((floatType) rand()) / RAND_MAX;
273   }
274
275   // Broadcast input data to all ranks
276   HANDLE_MPI_ERROR( MPI_Bcast(A, elementsA, floatTypeMPI, 0, MPI_COMM_WORLD) );
277   HANDLE_MPI_ERROR( MPI_Bcast(B, elementsB, floatTypeMPI, 0, MPI_COMM_WORLD) );
278   HANDLE_MPI_ERROR( MPI_Bcast(C, elementsC, floatTypeMPI, 0, MPI_COMM_WORLD) );
279   HANDLE_MPI_ERROR( MPI_Bcast(D, elementsD, floatTypeMPI, 0, MPI_COMM_WORLD) );
280
281   // Copy data to GPU
282   HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[0], A, sizeA, cudaMemcpyHostToDevice) );
283   HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[1], B, sizeB, cudaMemcpyHostToDevice) );
284   HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[2], C, sizeC, cudaMemcpyHostToDevice) );
285   HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[3], D, sizeD, cudaMemcpyHostToDevice) );
286
287   if(verbose)
288      printf("Allocated GPU memory for data, and initialize data\n");

一旦 MPI 服务被初始化并且 cuTensorNet 库句柄在之后被创建,就可以通过调用 cutensornetDistributedResetConfiguration() 来激活分布式并行执行。按照标准实践,用户的代码需要通过 MPI_Comm_dup 创建重复的 MPI 通信器。然后,通过将指向重复的 MPI 通信器的指针及其大小(以字节为单位)传递给 cutensornetDistributedResetConfiguration() 调用,将重复的 MPI 通信器与 cuTensorNet 库句柄关联。MPI 通信器将存储在 cuTensorNet 库句柄内部,以便对张量网络收缩路径查找器和张量网络收缩执行器的所有后续调用将在所有参与的 MPI 进程(每个 MPI 进程都与其自己的 GPU 关联)之间并行化。

342   /*******************************
343   * Activate distributed (parallel) execution prior to
344   * calling contraction path finder and contraction executor
345   *******************************/
346   // HANDLE_ERROR( cutensornetDistributedResetConfiguration(handle, NULL, 0) ); // resets back to serial execution
347   MPI_Comm cutnComm;
348   HANDLE_MPI_ERROR( MPI_Comm_dup(MPI_COMM_WORLD, &cutnComm) ); // duplicate MPI communicator
349   HANDLE_ERROR( cutensornetDistributedResetConfiguration(handle, &cutnComm, sizeof(cutnComm)) );
350   if(verbose)
351      printf("Reset distributed MPI configuration\n");

注意

cutensornetDistributedResetConfiguration() 是一个集体调用,必须由所有参与的 MPI 进程执行。

这种分布式并行化模型的 API 使得在多个 GPU/节点上运行为串行执行编写的源代码变得简单。本质上,所有 MPI 进程将执行完全相同的(串行)源代码,同时在张量网络收缩路径查找器和张量网络收缩执行器调用内部自动执行分布式并行化。张量网络收缩路径查找器的并行化仅在请求的超样本数大于零时才会发生。然而,无论如何,分布式并行化的激活必须先于张量网络收缩路径查找器的调用。也就是说,张量网络收缩路径查找器和张量网络收缩执行的调用必须严格在通过 cutensornetDistributedResetConfiguration() 激活分布式并行化之后完成。当分布式配置设置为并行模式时,通常期望用户通过调用 cutensornetContractSlices() 函数来调用张量网络收缩执行,该函数提供全范围的张量网络切片,这些切片将自动分布在所有 MPI 进程中。

由于张量网络的大小必须足够大,才能从分布式执行中获得加速益处,因此较小的张量网络(仅包含单个切片的张量网络)仍然可以在不进行分布式并行化的情况下进行处理。这可以通过调用 cutensornetDistributedResetConfiguration() 并使用 NULL 参数来代替 MPI 通信器指针来实现(与之前一样,这应该在调用张量网络收缩路径查找器之前完成)。也就是说,分布式并行化和冗余串行执行之间的切换可以基于每个张量网络进行。用户可以决定哪些(较大的)张量网络以并行方式处理,哪些(较小的)张量网络以冗余串行方式处理,只需适当地重置分布式配置即可。在这两种情况下,所有 MPI 进程都将在张量网络执行结束时生成相同的输出张量(结果)。

注意

在当前版本的 cuTensorNet 库中,由 cutensornetContractSlices() 调用触发的并行张量网络收缩执行将阻塞提供的 CUDA 流以及调用 CPU 线程,直到所有 MPI 进程上的执行完成。这是一个临时限制,将在未来版本的 cuTensorNet 库中解除。届时,对 cutensornetContractSlices() 的调用将是完全异步的,类似于串行执行的情况。此外,对于所有 MPI 进程的显式同步(屏障),可以集体调用 cutensornetDistributedSynchronize()

在终止之前,需要最终确定 MPI 服务。

561   // Shut down MPI service
562   HANDLE_MPI_ERROR( MPI_Finalize() );

完整的 MPI 自动示例 可以在 NVIDIA/cuQuantum 仓库中找到。

代码示例(手动切片分布式并行化)

对于高级用户,也可以(但更复杂)调整 代码示例(串行) 以在多个 GPU 设备上显式地并行化张量网络收缩操作的执行。这里我们也将使用 MPI 作为通信层。为了简洁起见,我们将仅展示需要在串行示例之上进行的更改。完整的 MPI 手动示例代码 可以在 NVIDIA/cuQuantum 仓库中找到。请注意,此示例 **不** 需要 CUDA-aware MPI。

首先,除了 头文件和数据类型 中提到的头文件和定义之外,我们还需要包含 MPI 头文件并定义一个宏来处理 MPI 错误。我们还需要初始化 MPI 服务,并将每个 MPI 进程与其自己的 GPU 设备关联起来,如前所述。

20#include <mpi.h>
44#define HANDLE_MPI_ERROR(x)                                       \
45{ const auto err = x;                                             \
46  if( err != MPI_SUCCESS )                                        \
47  { char error[MPI_MAX_ERROR_STRING]; int len;                    \
48    MPI_Error_string(err, error, &len);                           \
49    printf("MPI Error: %s in line %d\n", error, __LINE__);        \
50    fflush(stdout);                                               \
51    MPI_Abort(MPI_COMM_WORLD, err);                               \
52  }                                                               \
53};
 97   // Initialize MPI
 98   HANDLE_MPI_ERROR( MPI_Init(&argc, &argv) );
 99   int rank {-1};
100   HANDLE_MPI_ERROR( MPI_Comm_rank(MPI_COMM_WORLD, &rank) );
101   int numProcs {0};
102   HANDLE_MPI_ERROR( MPI_Comm_size(MPI_COMM_WORLD, &numProcs) );
122   // Set GPU device based on ranks and nodes
123   int numDevices {0};
124   HANDLE_CUDA_ERROR( cudaGetDeviceCount(&numDevices) );
125   const int deviceId = rank % numDevices; // we assume that the processes are mapped to nodes in contiguous chunks
126   HANDLE_CUDA_ERROR( cudaSetDevice(deviceId) );
127   cudaDeviceProp prop;
128   HANDLE_CUDA_ERROR( cudaGetDeviceProperties(&prop, deviceId) );

接下来,我们按照 定义张量网络和张量大小 中所述定义张量网络。在每个进程一个 GPU 设备的模型中,张量网络(包括操作数和结果数据)在每个进程上复制。根进程初始化输入数据并将其广播到其他进程。

258   /*******************
259   * Initialize data
260   *******************/
261
262   memset(R, 0, sizeof(floatType) * elementsR);
263   if(rank == 0)
264   {
265      for (uint64_t i = 0; i < elementsA; i++)
266         A[i] = ((floatType) rand()) / RAND_MAX;
267      for (uint64_t i = 0; i < elementsB; i++)
268         B[i] = ((floatType) rand()) / RAND_MAX;
269      for (uint64_t i = 0; i < elementsC; i++)
270         C[i] = ((floatType) rand()) / RAND_MAX;
271      for (uint64_t i = 0; i < elementsD; i++)
272         D[i] = ((floatType) rand()) / RAND_MAX;
273   }
274
275   // Broadcast input data to all ranks
276   HANDLE_MPI_ERROR( MPI_Bcast(A, elementsA, floatTypeMPI, 0, MPI_COMM_WORLD) );
277   HANDLE_MPI_ERROR( MPI_Bcast(B, elementsB, floatTypeMPI, 0, MPI_COMM_WORLD) );
278   HANDLE_MPI_ERROR( MPI_Bcast(C, elementsC, floatTypeMPI, 0, MPI_COMM_WORLD) );
279   HANDLE_MPI_ERROR( MPI_Bcast(D, elementsD, floatTypeMPI, 0, MPI_COMM_WORLD) );
280
281   // Copy data to GPU
282   HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[0], A, sizeA, cudaMemcpyHostToDevice) );
283   HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[1], B, sizeB, cudaMemcpyHostToDevice) );
284   HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[2], C, sizeC, cudaMemcpyHostToDevice) );
285   HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[3], D, sizeD, cudaMemcpyHostToDevice) );
286
287   if(verbose)
288      printf("Allocated GPU memory for data, and initialize data\n");

然后,我们在每个进程上创建库句柄和张量网络描述符,如 cuTensorNet 句柄和网络描述符 中所述。

接下来,我们为我们的张量网络找到最佳的收缩路径和切片组合。我们将在所有进程上运行 cuTensorNet 优化器,并确定哪个进程具有 FLOP 计数方面最佳的路径。然后,我们将在此进程上打包优化器信息对象,广播打包的缓冲区,并在所有其他进程上解包它。现在,每个进程都具有相同的优化器信息对象,我们使用它来计算每个进程的切片份额。

363   // Compute the path on all ranks so that we can choose the path with the lowest cost. Note that since this is a tiny
364   // example with 4 operands, all processes will compute the same globally optimal path. This is not the case for large
365   // tensor networks. For large networks, hyper-optimization does become beneficial.
366
367   // Enforce tensor network slicing (for parallelization)
368   const int32_t min_slices = numProcs;
369   HANDLE_ERROR( cutensornetContractionOptimizerConfigSetAttribute(handle,
370                  optimizerConfig,
371                  CUTENSORNET_CONTRACTION_OPTIMIZER_CONFIG_SLICER_MIN_SLICES,
372                  &min_slices,
373                  sizeof(min_slices)) );
374
375   // Find an optimized tensor network contraction path on each MPI process
376   HANDLE_ERROR( cutensornetContractionOptimize(handle,
377                                       descNet,
378                                       optimizerConfig,
379                                       workspaceLimit,
380                                       optimizerInfo) );
381
382   // Query the obtained Flop count
383   double flops{-1.};
384   HANDLE_ERROR( cutensornetContractionOptimizerInfoGetAttribute(handle,
385                     optimizerInfo,
386                     CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_FLOP_COUNT,
387                     &flops,
388                     sizeof(flops)) );
389
390   // Choose the contraction path with the lowest Flop cost
391   struct {
392      double value;
393      int rank;
394   } in{flops, rank}, out;
395   HANDLE_MPI_ERROR( MPI_Allreduce(&in, &out, 1, MPI_DOUBLE_INT, MPI_MINLOC, MPI_COMM_WORLD) );
396   const int sender = out.rank;
397   flops = out.value;
398
399   if (verbose)
400      printf("Process %d has the path with the lowest FLOP count %lf\n", sender, flops);
401
402   // Get the buffer size for optimizerInfo and broadcast it
403   size_t bufSize {0};
404   if (rank == sender)
405   {
406       HANDLE_ERROR( cutensornetContractionOptimizerInfoGetPackedSize(handle, optimizerInfo, &bufSize) );
407   }
408   HANDLE_MPI_ERROR( MPI_Bcast(&bufSize, 1, MPI_INT64_T, sender, MPI_COMM_WORLD) );
409
410   // Allocate a buffer
411   std::vector<char> buffer(bufSize);
412
413   // Pack optimizerInfo on sender and broadcast it
414   if (rank == sender)
415   {
416       HANDLE_ERROR( cutensornetContractionOptimizerInfoPackData(handle, optimizerInfo, buffer.data(), bufSize) );
417   }
418   HANDLE_MPI_ERROR( MPI_Bcast(buffer.data(), bufSize, MPI_CHAR, sender, MPI_COMM_WORLD) );
419
420   // Unpack optimizerInfo from the buffer
421   if (rank != sender)
422   {
423       HANDLE_ERROR( cutensornetUpdateContractionOptimizerInfoFromPackedData(handle, buffer.data(), bufSize, optimizerInfo) );
424   }
425
426   // Query the number of slices the tensor network execution will be split into
427   int64_t numSlices = 0;
428   HANDLE_ERROR( cutensornetContractionOptimizerInfoGetAttribute(
429                  handle,
430                  optimizerInfo,
431                  CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_NUM_SLICES,
432                  &numSlices,
433                  sizeof(numSlices)) );
434   assert(numSlices > 0);
435
436   // Calculate each process's share of the slices
437   int64_t procChunk = numSlices / numProcs;
438   int extra = numSlices % numProcs;
439   int procSliceBegin = rank * procChunk + std::min(rank, extra);
440   int procSliceEnd = (rank == numProcs - 1) ? numSlices : (rank + 1) * procChunk + std::min(rank + 1, extra);

我们现在创建工作空间描述符并分配内存,如 创建工作空间描述符并分配工作空间内存 中所述,并创建 收缩计划和自动调优 张量网络。

接下来,在每个进程上,我们创建一个切片组(参见 cutensornetSliceGroup_t),该切片组对应于其张量网络切片份额。然后,我们将此切片组对象提供给 cutensornetContractSlices() 函数,以在每个进程上获得部分收缩结果。

530   // Create a cutensornetSliceGroup_t object from a range of slice IDs
531   cutensornetSliceGroup_t sliceGroup{};
532   HANDLE_ERROR( cutensornetCreateSliceGroupFromIDRange(handle, procSliceBegin, procSliceEnd, 1, &sliceGroup) );
553      HANDLE_ERROR( cutensornetContractSlices(handle,
554                                 plan,
555                                 rawDataIn_d,
556                                 R_d,
557                                 accumulateOutput,
558                                 workDesc,
559                                 sliceGroup,
560                                 stream) );

最后,我们对部分贡献求和,以获得张量网络收缩的结果。

566      // Perform Allreduce operation on the output tensor
567      HANDLE_CUDA_ERROR( cudaStreamSynchronize(stream) );
568      HANDLE_CUDA_ERROR( cudaMemcpy(R, R_d, sizeR, cudaMemcpyDeviceToHost) ); // restore the output tensor on Host
569      HANDLE_MPI_ERROR( MPI_Allreduce(MPI_IN_PLACE, R, elementsR, floatTypeMPI, MPI_SUM, MPI_COMM_WORLD) );

在终止之前,需要最终确定 MPI 服务。

631   // Shut down MPI service
632   HANDLE_MPI_ERROR( MPI_Finalize() );

完整的 MPI 手动示例 可以在 NVIDIA/cuQuantum 仓库中找到。

代码示例 (GateSplit)

实用技巧

  • 对于调试,可以设置环境变量 CUTENSORNET_LOG_LEVEL=n。级别 n = 0, 1, …, 5 对应于 cutensornetLoggerSetLevel() 中描述和使用的记录器级别。环境变量 CUTENSORNET_LOG_FILE=<filepath> 可用于将日志输出重定向到 <filepath> 而不是 stdout 的自定义文件。