Code example (distributed truncated SVD with MPI and NCCL)#
Starting with cuTensorNet v2.14.0, tensor QR and SVD can operate on tensors distributed across multiple
processes and GPUs. This example demonstrates a distributed truncated SVD on the bond-swapping step
of the anisotropic tensor renormalization group (ATRG), where two half-tensors are contracted over
mode i and the result is decomposed:
Unlike Code example (Contraction with automatic distributed slicing), which enables distributed parallelization inside the contraction path finder and executor, this sample performs a collective tensor decomposition on distributed tensor descriptors created with cutensornetCreateDistributedTensorDescriptor(). The full code can be found in the NVIDIA/cuQuantum repository (here).
To run the sample:
Build with MPI support and execute with one GPU per MPI process, e.g.
mpirun -n 4 ./decompose_example_mpi_nccl.Set the environment variable
$CUTENSORNET_COMM_LIBto the path of the MPI distributed interface library (libcutensornet_distributed_interface_mpi.so), as described in the installation guide.NCCL and cuSOLVERMp are required; see distributed tensor decomposition requirements.
Note
Distributed SVD requires CUTENSORNET_TENSOR_SVD_ALGO_GESVDP; GESVD (including the
config default), GESVDJ, and GESVDR return CUTENSORNET_STATUS_NOT_SUPPORTED.
Gate split remains local-only.
Mixed local and distributed operands in a single call return CUTENSORNET_STATUS_INVALID_VALUE.
MPI and GPU setup#
As in the Code example (Contraction with automatic distributed slicing), we initialize MPI, assign one GPU per process, create a cuTensorNet library handle, and bind a duplicated MPI communicator with cutensornetDistributedResetConfiguration(). All subsequent distributed calls are collective over this communicator.
132 /*******************************************
133 * MPI setup: one GPU per process
134 ********************************************/
135
136 HANDLE_MPI_ERROR( MPI_Init(&argc, &argv) );
137
138 int rank{-1}, numRanks{0};
139 HANDLE_MPI_ERROR( MPI_Comm_rank(MPI_COMM_WORLD, &rank) );
140 HANDLE_MPI_ERROR( MPI_Comm_size(MPI_COMM_WORLD, &numRanks) );
141 const bool verbose = (rank == 0);
142
143 // The device must be selected before cutensornetCreate().
144 int numDevices{0};
145 HANDLE_CUDA_ERROR( cudaGetDeviceCount(&numDevices) );
146 HANDLE_CUDA_ERROR( cudaSetDevice(rank % numDevices) );
147
148 if (verbose)
149 printf("cuTensorNet-vers:%ld\n", cutensornetGetVersion());
150
151 cutensornetHandle_t handle;
152 HANDLE_ERROR( cutensornetCreate(&handle) );
153
154 // Bind a duplicated MPI communicator; it must stay alive until the next
155 // reset. All distributed calls below are collective over it.
156 MPI_Comm cutnComm;
157 HANDLE_MPI_ERROR( MPI_Comm_dup(MPI_COMM_WORLD, &cutnComm) );
158 HANDLE_ERROR( cutensornetDistributedResetConfiguration(handle, &cutnComm, sizeof(cutnComm)) );
Build the global tensor on the host#
The sample builds the global tensor Theta on every rank purely to produce input data for
the scatter step below.
161 /*******************************************************
162 * Build Theta = sum_i B[i,u,v,a] C[i,x,y,b] on the host
163 ********************************************************/
164
165 const int64_t D = 7; // spatial degrees of freedom (u, v, x, y); kept extent of g
166 const int64_t CHI = 13; // vertical bond dimension (a, b)
167 const int64_t CHI_T = 6; // temporal bond dimension (i), contracted away below
168
169 std::vector<double> bHalf(CHI_T * D * D * CHI);
170 std::vector<double> cHalf(CHI_T * D * D * CHI);
171 uint64_t rngState = 42;
172 for (auto& value: bHalf) value = pseudoRandom(rngState);
173 for (auto& value: cHalf) value = pseudoRandom(rngState);
174
175 // Theta in Fortran order over modes (u, v, a, x, y, b); B and C in
176 // Fortran order over (i, u, v, a) and (i, x, y, b).
177 const size_t elementsTheta = D * D * CHI * D * D * CHI;
178 std::vector<double> theta(elementsTheta, 0.0);
179 for (int64_t b = 0; b < CHI; ++b)
180 for (int64_t y = 0; y < D; ++y)
181 for (int64_t x = 0; x < D; ++x)
182 for (int64_t a = 0; a < CHI; ++a)
183 for (int64_t v = 0; v < D; ++v)
184 for (int64_t u = 0; u < D; ++u)
185 {
186 double sum = 0.0;
187 for (int64_t i = 0; i < CHI_T; ++i)
188 sum += bHalf[i + CHI_T * (u + D * (v + D * a))]
189 * cHalf[i + CHI_T * (x + D * (y + D * b))];
190 theta[u + D * (v + D * (a + CHI * (x + D * (y + D * b))))] = sum;
191 }
Create distributed tensor descriptors#
Each operand is described by its global extents, a per-mode process grid (nranksPerMode), and
an optional block-cyclic layout. Along each distributed mode, a positive block size assigns
consecutive blocks round-robin across process-grid coordinates, while block size zero selects one
near-even contiguous slab per coordinate. A process-grid extent of one leaves that mode undistributed.
Every rank must create the same descriptors in the same order.
194 /*******************************************************
195 * Create the distributed tensor descriptors (collective)
196 ********************************************************/
197
198 const cudaDataType_t typeData = CUDA_R_64F;
199
200 std::vector<int32_t> modesTheta{'u','v','a','x','y','b'};
201 std::vector<int32_t> modesX{'a','x','y','g'};
202 std::vector<int32_t> modesY{'g','u','v','b'};
203
204 std::vector<int64_t> extentsTheta{D, D, CHI, D, D, CHI};
205 // The shared extent of the outputs at creation time is the truncation
206 // cap: the bond swap keeps g = D of the D*D*CHI available singular
207 // values. This example truncates to that cap; a value-based cutoff
208 // (CUTENSORNET_TENSOR_SVD_CONFIG_ABS_CUTOFF / _REL_CUTOFF /
209 // _DISCARDED_WEIGHT_CUTOFF) would instead let the retained extent be
210 // decided by the spectrum, bounded above by this cap.
211 std::vector<int64_t> extentsX{CHI, D, D, D};
212 std::vector<int64_t> extentsY{D, D, D, CHI};
213
214 // Theta is split over u, X over x, and Y over u. The shared mode g
215 // remains undistributed. Positive block sizes select round-robin blocks;
216 // zero selects slabs. NULL strides select compact Fortran-order shards.
217 const int64_t BS = 2;
218 std::vector<int64_t> gridTheta{numRanks, 1, 1, 1, 1, 1};
219 std::vector<int64_t> gridX{1, numRanks, 1, 1};
220 std::vector<int64_t> gridY{1, numRanks, 1, 1};
221 std::vector<int64_t> blocksTheta{BS, 0, 0, 0, 0, 0};
222 std::vector<int64_t> blocksX{0, BS, 0, 0};
223 std::vector<int64_t> blocksY{0, BS, 0, 0};
224
225 cutensornetTensorDescriptor_t descTheta, descX, descY;
226 // Every rank creates the same descriptors in the same order.
227 HANDLE_ERROR( cutensornetCreateDistributedTensorDescriptor(handle,
228 modesTheta.size(), extentsTheta.data(), NULL, blocksTheta.data(), NULL,
229 gridTheta.data(), modesTheta.data(), typeData, &descTheta) );
230 HANDLE_ERROR( cutensornetCreateDistributedTensorDescriptor(handle,
231 modesX.size(), extentsX.data(), NULL, blocksX.data(), NULL,
232 gridX.data(), modesX.data(), typeData, &descX) );
233 HANDLE_ERROR( cutensornetCreateDistributedTensorDescriptor(handle,
234 modesY.size(), extentsY.data(), NULL, blocksY.data(), NULL,
235 gridY.data(), modesY.data(), typeData, &descY) );
236
237 if (verbose)
238 printf("Created distributed descriptors: Theta split over u across %d rank(s); "
239 "bond swap keeps g = %ld of %ld singular values\n",
240 numRanks, D, D * D * CHI);
Prepare rank-local device buffers#
Rank-local layout details can be queried with cutensornetTensorDescriptorGetAttribute(). This sample scatters from the replicated host tensor built above: each rank packs its owned elements into compact Fortran-order host storage, then copies the buffer to the device. The decomposition APIs only require rank-local device memory in that layout; you can populate those buffers on the device directly, without assembling a global tensor on the host.
243 /*******************************************************
244 * Allocate the local shards and scatter Theta
245 ********************************************************/
246
247 // Rank-local queries return each compact Fortran-order shard's size.
248 size_t thetaLocalBytes{0}, xLocalBytes{0}, yLocalBytes{0};
249 HANDLE_ERROR( cutensornetTensorDescriptorGetAttribute(handle, descTheta,
250 CUTENSORNET_TENSOR_DESCRIPTOR_LOCAL_DATA_SIZE,
251 &thetaLocalBytes, sizeof(thetaLocalBytes)) );
252 HANDLE_ERROR( cutensornetTensorDescriptorGetAttribute(handle, descX,
253 CUTENSORNET_TENSOR_DESCRIPTOR_LOCAL_DATA_SIZE,
254 &xLocalBytes, sizeof(xLocalBytes)) );
255 HANDLE_ERROR( cutensornetTensorDescriptorGetAttribute(handle, descY,
256 CUTENSORNET_TENSOR_DESCRIPTOR_LOCAL_DATA_SIZE,
257 &yLocalBytes, sizeof(yLocalBytes)) );
258
259 // This rank's shard of Theta: the block-cyclically owned runs of the u
260 // mode (with BS=2 and two ranks, mode u of extent 7 has blocks [0:2)
261 // [2:4) [4:6) [6:7); rank 0 owns blocks 0 and 2, rank 1 owns 1 and 3),
262 // packed consecutively in Fortran order over the local extents.
263 const std::vector<OwnedRun> uRuns = ownedRuns(D, BS, numRanks, rank);
264 const int64_t uCount = ownedCount(uRuns);
265 std::vector<double> thetaLocal(thetaLocalBytes / sizeof(double));
266 for (int64_t b = 0; b < CHI; ++b)
267 for (int64_t y = 0; y < D; ++y)
268 for (int64_t x = 0; x < D; ++x)
269 for (int64_t a = 0; a < CHI; ++a)
270 for (int64_t v = 0; v < D; ++v)
271 for (const auto& run: uRuns)
272 for (int64_t o = 0; o < run.count; ++o)
273 {
274 const int64_t u = run.globalStart + o;
275 const int64_t uLoc = run.localStart + o;
276 thetaLocal[uLoc + uCount * (v + D * (a + CHI * (x + D * (y + D * b))))] =
277 theta[u + D * (v + D * (a + CHI * (x + D * (y + D * b))))];
278 }
279
280 void *dTheta{nullptr}, *dX{nullptr}, *dS{nullptr}, *dY{nullptr};
281 const size_t sBytes = D * sizeof(double); // replicated on every rank
282 if (thetaLocalBytes > 0)
283 HANDLE_CUDA_ERROR( cudaMalloc(&dTheta, thetaLocalBytes) );
284 if (xLocalBytes > 0)
285 HANDLE_CUDA_ERROR( cudaMalloc(&dX, xLocalBytes) );
286 HANDLE_CUDA_ERROR( cudaMalloc(&dS, sBytes) );
287 if (yLocalBytes > 0)
288 HANDLE_CUDA_ERROR( cudaMalloc(&dY, yLocalBytes) );
289 if (thetaLocalBytes > 0)
290 HANDLE_CUDA_ERROR( cudaMemcpy(dTheta, thetaLocal.data(), thetaLocalBytes,
291 cudaMemcpyHostToDevice) );
292
293 // Initialize the output buffers before the SVD overwrites them.
294 {
295 std::vector<double> init(std::max(xLocalBytes, yLocalBytes) / sizeof(double));
296 for (auto& value: init) value = pseudoRandom(rngState);
297 if (xLocalBytes > 0)
298 HANDLE_CUDA_ERROR( cudaMemcpy(dX, init.data(), xLocalBytes,
299 cudaMemcpyHostToDevice) );
300 if (yLocalBytes > 0)
301 HANDLE_CUDA_ERROR( cudaMemcpy(dY, init.data(), yLocalBytes,
302 cudaMemcpyHostToDevice) );
303 }
SVD configuration and workspace query#
The workspace and execution workflow matches the local SVD example: create an SVD config and info object, query workspace sizes collectively, allocate scratch memory, and call cutensornetTensorSVD(). Fixed-extent truncation is specified by the shared extent of the output descriptors at creation time.
306 /*******************************************************
307 * SVD config/info and workspace (same calls as local)
308 ********************************************************/
309
310 // No cutoffs are set here, so the SVD truncates to the cap the output
311 // descriptors were created with. Distributed SVD requires GESVDP; the
312 // config default (GESVD) returns CUTENSORNET_STATUS_NOT_SUPPORTED.
313 cutensornetTensorSVDConfig_t svdConfig;
314 HANDLE_ERROR( cutensornetCreateTensorSVDConfig(handle, &svdConfig) );
315 const cutensornetTensorSVDAlgo_t svdAlgo = CUTENSORNET_TENSOR_SVD_ALGO_GESVDP;
316 HANDLE_ERROR( cutensornetTensorSVDConfigSetAttribute(handle, svdConfig,
317 CUTENSORNET_TENSOR_SVD_CONFIG_ALGO, &svdAlgo, sizeof(svdAlgo)) );
318 cutensornetTensorSVDInfo_t svdInfo;
319 HANDLE_ERROR( cutensornetCreateTensorSVDInfo(handle, &svdInfo) );
320
321 cutensornetWorkspaceDescriptor_t workDesc;
322 HANDLE_ERROR( cutensornetCreateWorkspaceDescriptor(handle, &workDesc) );
323 // Collective: every rank queries with the same descriptor trio.
324 HANDLE_ERROR( cutensornetWorkspaceComputeSVDSizes(handle, descTheta, descX, descY,
325 svdConfig, workDesc) );
326 int64_t deviceWorkspaceSize{0}, hostWorkspaceSize{0};
327 HANDLE_ERROR( cutensornetWorkspaceGetMemorySize(handle, workDesc,
328 CUTENSORNET_WORKSIZE_PREF_MIN, CUTENSORNET_MEMSPACE_DEVICE,
329 CUTENSORNET_WORKSPACE_SCRATCH, &deviceWorkspaceSize) );
330 HANDLE_ERROR( cutensornetWorkspaceGetMemorySize(handle, workDesc,
331 CUTENSORNET_WORKSIZE_PREF_MIN, CUTENSORNET_MEMSPACE_HOST,
332 CUTENSORNET_WORKSPACE_SCRATCH, &hostWorkspaceSize) );
333
334 void* devWork{nullptr};
335 void* hostWork{nullptr};
336 if (deviceWorkspaceSize > 0)
337 HANDLE_CUDA_ERROR( cudaMalloc(&devWork, deviceWorkspaceSize) );
338 if (hostWorkspaceSize > 0)
339 hostWork = malloc(hostWorkspaceSize);
340 HANDLE_ERROR( cutensornetWorkspaceSetMemory(handle, workDesc,
341 CUTENSORNET_MEMSPACE_DEVICE, CUTENSORNET_WORKSPACE_SCRATCH,
342 devWork, deviceWorkspaceSize) );
343 HANDLE_ERROR( cutensornetWorkspaceSetMemory(handle, workDesc,
344 CUTENSORNET_MEMSPACE_HOST, CUTENSORNET_WORKSPACE_SCRATCH,
345 hostWork, hostWorkspaceSize) );
Execution#
cutensornetTensorSVD() updates the output descriptors to the realized reduced extent after truncation. Recreate them before a later call if truncation can change that extent. Singular values are replicated on every rank.
348 /*******************************************************
349 * Execution: the distributed bond-swap SVD
350 ********************************************************/
351
352 cudaStream_t stream;
353 HANDLE_CUDA_ERROR( cudaStreamCreate(&stream) );
354
355 // Fixed-extent truncation: Kred == Kcap. After value-based truncation,
356 // descX/descY describe Kred; recreate them at Kcap before calling again.
357 HANDLE_ERROR( cutensornetTensorSVD(handle,
358 descTheta, dTheta,
359 descX, dX,
360 dS,
361 descY, dY,
362 svdConfig, svdInfo, workDesc, stream) );
363
364 // Outputs become visible to work enqueued on `stream` after the call.
365 std::vector<double> xLocal(xLocalBytes / sizeof(double));
366 std::vector<double> sHost(D);
367 std::vector<double> yLocal(yLocalBytes / sizeof(double));
368 if (xLocalBytes > 0)
369 HANDLE_CUDA_ERROR( cudaMemcpyAsync(xLocal.data(), dX, xLocalBytes,
370 cudaMemcpyDeviceToHost, stream) );
371 HANDLE_CUDA_ERROR( cudaMemcpyAsync(sHost.data(), dS, sBytes,
372 cudaMemcpyDeviceToHost, stream) );
373 if (yLocalBytes > 0)
374 HANDLE_CUDA_ERROR( cudaMemcpyAsync(yLocal.data(), dY, yLocalBytes,
375 cudaMemcpyDeviceToHost, stream) );
376 HANDLE_CUDA_ERROR( cudaStreamSynchronize(stream) );
377
378 int64_t reducedExtent{0};
379 double discardedWeight{0.0};
380 HANDLE_ERROR( cutensornetTensorSVDInfoGetAttribute(handle, svdInfo,
381 CUTENSORNET_TENSOR_SVD_INFO_REDUCED_EXTENT,
382 &reducedExtent, sizeof(reducedExtent)) );
383 HANDLE_ERROR( cutensornetTensorSVDInfoGetAttribute(handle, svdInfo,
384 CUTENSORNET_TENSOR_SVD_INFO_DISCARDED_WEIGHT,
385 &discardedWeight, sizeof(discardedWeight)) );
Verification#
The sample reassembles the distributed factors on the host and checks that the reconstruction residual matches the reported discarded weight, and that singular values are identical on every rank.
388 /*******************************************************
389 * Verification
390 ********************************************************/
391
392 // The singular values are replicated: every rank must hold the values
393 // rank 0 holds.
394 std::vector<double> sRoot(sHost);
395 HANDLE_MPI_ERROR( MPI_Bcast(sRoot.data(), D, MPI_DOUBLE, 0, MPI_COMM_WORLD) );
396 double sReplicationError = 0.0;
397 for (int64_t g = 0; g < D; ++g)
398 sReplicationError = std::max(sReplicationError, std::fabs(sHost[g] - sRoot[g]));
399
400 // Reassemble X and Y on every rank (zero-fill + allreduce over the
401 // block-cyclically owned entries), then reconstruct the truncated
402 // Theta. X is split over its x mode, Y over its u mode, both with the
403 // same block size, so both reuse the u-mode ownership runs.
404 const size_t elementsX = CHI * D * D * D;
405 const size_t elementsY = D * D * D * CHI;
406 std::vector<double> xFull(elementsX, 0.0);
407 std::vector<double> yFull(elementsY, 0.0);
408 for (int64_t g = 0; g < D; ++g)
409 for (int64_t y = 0; y < D; ++y)
410 for (const auto& run: uRuns)
411 for (int64_t o = 0; o < run.count; ++o)
412 for (int64_t a = 0; a < CHI; ++a)
413 {
414 const int64_t x = run.globalStart + o;
415 const int64_t xLoc = run.localStart + o;
416 xFull[a + CHI * (x + D * (y + D * g))] =
417 xLocal[a + CHI * (xLoc + uCount * (y + D * g))];
418 }
419 for (int64_t b = 0; b < CHI; ++b)
420 for (int64_t v = 0; v < D; ++v)
421 for (const auto& run: uRuns)
422 for (int64_t o = 0; o < run.count; ++o)
423 for (int64_t g = 0; g < D; ++g)
424 {
425 const int64_t u = run.globalStart + o;
426 const int64_t uLoc = run.localStart + o;
427 yFull[g + D * (u + D * (v + D * b))] =
428 yLocal[g + D * (uLoc + uCount * (v + D * b))];
429 }
430 HANDLE_MPI_ERROR( MPI_Allreduce(MPI_IN_PLACE, xFull.data(), elementsX,
431 MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD) );
432 HANDLE_MPI_ERROR( MPI_Allreduce(MPI_IN_PLACE, yFull.data(), elementsY,
433 MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD) );
434
435 // The relative squared residual of the reconstruction must equal the
436 // reported discarded weight: both are the weight of the singular values
437 // beyond the cap.
438 double residual2 = 0.0, thetaNorm2 = 0.0;
439 for (int64_t b = 0; b < CHI; ++b)
440 for (int64_t y = 0; y < D; ++y)
441 for (int64_t x = 0; x < D; ++x)
442 for (int64_t a = 0; a < CHI; ++a)
443 for (int64_t v = 0; v < D; ++v)
444 for (int64_t u = 0; u < D; ++u)
445 {
446 double rec = 0.0;
447 for (int64_t g = 0; g < D; ++g)
448 rec += xFull[a + CHI * (x + D * (y + D * g))] * sHost[g]
449 * yFull[g + D * (u + D * (v + D * b))];
450 const double ref = theta[u + D * (v + D * (a + CHI * (x + D * (y + D * b))))];
451 residual2 += (rec - ref) * (rec - ref);
452 thetaNorm2 += ref * ref;
453 }
454 const double relativeResidual2 = residual2 / thetaNorm2;
455
456 if (verbose)
457 {
458 printf("reduced extent: %ld (cap %ld)\n", reducedExtent, D);
459 printf("leading singular values:");
460 for (int64_t g = 0; g < std::min<int64_t>(D, 4); ++g)
461 printf(" %.6f", sHost[g]);
462 printf(" ...\n");
463 printf("truncation: relative residual^2 = %.6f, reported discarded weight = %.6f\n",
464 relativeResidual2, discardedWeight);
465 }
466 if (sReplicationError > 1e-12)
467 {
468 printf("Error: singular values differ across ranks (%e)\n", sReplicationError);
469 MPI_Abort(MPI_COMM_WORLD, 1);
470 }
471 if (std::fabs(relativeResidual2 - discardedWeight) > 1e-10)
472 {
473 printf("Error: reconstruction inconsistent with discarded weight\n");
474 MPI_Abort(MPI_COMM_WORLD, 1);
475 }
476 if (verbose)
477 printf("Distributed bond-swap SVD verified.\n");
Free resources#
After the computation, free CUDA, library, and MPI resources.
480 /*******************************************************
481 * Free resources
482 ********************************************************/
483
484 HANDLE_CUDA_ERROR( cudaStreamDestroy(stream) );
485 HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descTheta) );
486 HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descX) );
487 HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descY) );
488 HANDLE_ERROR( cutensornetDestroyTensorSVDConfig(svdConfig) );
489 HANDLE_ERROR( cutensornetDestroyTensorSVDInfo(svdInfo) );
490 HANDLE_ERROR( cutensornetDestroyWorkspaceDescriptor(workDesc) );
491 HANDLE_ERROR( cutensornetDestroy(handle) );
492
493 if (dTheta) HANDLE_CUDA_ERROR( cudaFree(dTheta) );
494 if (dX) HANDLE_CUDA_ERROR( cudaFree(dX) );
495 HANDLE_CUDA_ERROR( cudaFree(dS) );
496 if (dY) HANDLE_CUDA_ERROR( cudaFree(dY) );
497 if (devWork) HANDLE_CUDA_ERROR( cudaFree(devWork) );
498 if (hostWork) free(hostWork);
499
500 HANDLE_MPI_ERROR( MPI_Comm_free(&cutnComm) );
501 HANDLE_MPI_ERROR( MPI_Finalize() );
502
503 if (verbose)
504 printf("Free resource and exit.\n");
505
506 return 0;
507}