[flang-commits] [flang] [llvm] [flang][cuda] Fix managed descriptor memory leaks for device variables (PR #207843)

via flang-commits flang-commits at lists.llvm.org
Tue Jul 7 13:52:57 PDT 2026


https://github.com/khaki3 updated https://github.com/llvm/llvm-project/pull/207843

>From bff0562f07cd9839ba8fe980d2c3bdf5e673b660 Mon Sep 17 00:00:00 2001
From: Kazuaki Matsumura <kmatsumura at nvidia.com>
Date: Mon, 6 Jul 2026 14:02:12 -0700
Subject: [PATCH 1/2] [flang][cuda] Fix managed descriptor memory leaks for
 device variables

Two related leaks of the 48-byte managed CUF descriptor allocated by
_FortranACUFAllocDescriptor:

- ConvertVariable.cpp: free the device variable's CUF storage when it goes
  out of scope in the main program too. This cuf.free pairs with the
  cuf.alloc emitted at instantiation and reclaims compiler-managed storage
  (the managed descriptor for allocatables, or device data for a fixed-size
  array); it is storage cleanup, not Fortran finalization. It was skipped
  for MainProgram-scoped variables, reusing the finalization exemption from
  needDeallocationOrFinalization (llvm#66326, "Do not finalize main program
  variables"). But that exemption is a standard rule about finalizing user
  data, not about freeing internal storage, so reusing it here just leaks
  the descriptor at program exit. nvfortran frees this storage and
  compute-sanitizer reports it as leaked otherwise, so the free must be
  emitted in the main program as well.

- CodeGen.cpp (LoadOpConversion): do not allocate a managed descriptor
  snapshot when loading a CUFAllocDescriptor-sourced box that is only used
  on the host. The existing isUsedByGPULaunchFunc() fallback still forces
  managed storage when the loaded box is passed to a kernel, so device use
  is unaffected; host-only snapshots now use a stack alloca.

Update Fir/CUDA/cuda-code-gen.mlir descriptor counts and
Lower/CUDA/cuda-return0{1,2}.cuf expectations to reflect that main program
device variables are now freed.
---
 flang/lib/Lower/ConvertVariable.cpp     | 6 +++---
 flang/lib/Optimizer/CodeGen/CodeGen.cpp | 6 ++----
 flang/test/Fir/CUDA/cuda-code-gen.mlir  | 6 +++---
 flang/test/Lower/CUDA/cuda-return01.cuf | 2 +-
 flang/test/Lower/CUDA/cuda-return02.cuf | 2 ++
 5 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/flang/lib/Lower/ConvertVariable.cpp b/flang/lib/Lower/ConvertVariable.cpp
index 2834531dccd96..1e9a08952f7f6 100644
--- a/flang/lib/Lower/ConvertVariable.cpp
+++ b/flang/lib/Lower/ConvertVariable.cpp
@@ -1281,9 +1281,9 @@ static void instantiateLocal(Fortran::lower::AbstractConverter &converter,
     fir::ExtendedValue exv =
         converter.getSymbolExtendedValue(var.getSymbol(), &symMap);
     auto *sym = &var.getSymbol();
-    const Fortran::semantics::Scope &owner = sym->owner();
-    if (owner.kind() != Fortran::semantics::Scope::Kind::MainProgram &&
-        dataAttr.getValue() != cuf::DataAttribute::Shared) {
+    // Free the CUF storage, including in the main program: this is storage
+    // cleanup, not finalization, so it must not be skipped there.
+    if (dataAttr.getValue() != cuf::DataAttribute::Shared) {
       converter.getFctCtx().attachCleanup([builder, loc, exv, sym]() {
         cuf::DataAttributeAttr dataAttr =
             Fortran::lower::translateSymbolCUFDataAttribute(
diff --git a/flang/lib/Optimizer/CodeGen/CodeGen.cpp b/flang/lib/Optimizer/CodeGen/CodeGen.cpp
index 74ad23a050fae..5a4d4e9442e91 100644
--- a/flang/lib/Optimizer/CodeGen/CodeGen.cpp
+++ b/flang/lib/Optimizer/CodeGen/CodeGen.cpp
@@ -3668,10 +3668,8 @@ struct LoadOpConversion : public fir::FIROpConversion<fir::LoadOp> {
       if (auto callOp = mlir::dyn_cast_or_null<mlir::LLVM::CallOp>(
               inputBoxStorage.getDefiningOp())) {
         if (callOp.getCallee() &&
-            ((*callOp.getCallee())
-                 .starts_with(RTNAME_STRING(CUFAllocDescriptor)) ||
-             (*callOp.getCallee()).starts_with("__tgt_acc_get_deviceptr"))) {
-          // CUDA Fortran local descriptor are allocated in managed memory. So
+            (*callOp.getCallee()).starts_with("__tgt_acc_get_deviceptr")) {
+          // The device pointer descriptor is allocated in managed memory, so
           // new storage must be allocated the same way.
           auto mod = load->getParentOfType<mlir::ModuleOp>();
           newBoxStorage =
diff --git a/flang/test/Fir/CUDA/cuda-code-gen.mlir b/flang/test/Fir/CUDA/cuda-code-gen.mlir
index d1e8154b719ab..43623b4a00c5d 100644
--- a/flang/test/Fir/CUDA/cuda-code-gen.mlir
+++ b/flang/test/Fir/CUDA/cuda-code-gen.mlir
@@ -18,7 +18,7 @@ module attributes {dlti.dl_spec = #dlti.dl_spec<#dlti.dl_entry<f80, dense<128> :
   }
 
   // CHECK-LABEL: llvm.func @_QQmain()
-  // CHECK-COUNT-2: llvm.call @_FortranACUFAllocDescriptor 
+  // CHECK-COUNT-1: llvm.call @_FortranACUFAllocDescriptor
 
   fir.global linkonce @_QQclX3C737464696E3E00 constant : !fir.char<1,8> {
     %0 = fir.string_lit "<stdin>\00"(8) : !fir.char<1,8>
@@ -125,7 +125,7 @@ module attributes {dlti.dl_spec = #dlti.dl_spec<f80 = dense<128> : vector<2xi64>
 }
 
 // CHECK-LABEL: llvm.func @_QQmain()
-// CHECK-COUNT-4: llvm.call @_FortranACUFAllocDescriptor
+// CHECK-COUNT-3: llvm.call @_FortranACUFAllocDescriptor
 
 // -----
 
@@ -169,7 +169,7 @@ module attributes {dlti.dl_spec = #dlti.dl_spec<!llvm.ptr<270> = dense<32> : vec
 }
 
 // CHECK-LABEL: llvm.func @_QQmain()
-// CHECK-COUNT-3: llvm.call @_FortranACUFAllocDescriptor
+// CHECK-COUNT-2: llvm.call @_FortranACUFAllocDescriptor
 
 // -----
 
diff --git a/flang/test/Lower/CUDA/cuda-return01.cuf b/flang/test/Lower/CUDA/cuda-return01.cuf
index ed7c640a71082..9f9c9d4adda9f 100644
--- a/flang/test/Lower/CUDA/cuda-return01.cuf
+++ b/flang/test/Lower/CUDA/cuda-return01.cuf
@@ -30,4 +30,4 @@ end
 
 ! CHECK-LABEL: func.func @_QQmain() attributes {fir.bindc_name = "MAIN"}
 ! CHECK: cuf.alloc !fir.box<!fir.heap<!fir.array<?xi32>>> {bindc_name = "a", data_attr = #cuf.cuda<device>, uniq_name = "_QFEa"} -> !fir.ref<!fir.box<!fir.heap<!fir.array<?xi32>>>>
-! CHECK-NOT: cuf.free
+! CHECK: cuf.free
diff --git a/flang/test/Lower/CUDA/cuda-return02.cuf b/flang/test/Lower/CUDA/cuda-return02.cuf
index e54818444e49c..04ee46d8cf378 100644
--- a/flang/test/Lower/CUDA/cuda-return02.cuf
+++ b/flang/test/Lower/CUDA/cuda-return02.cuf
@@ -17,8 +17,10 @@ end
 ! CHECK: %[[DECL:.*]]:2 = hlfir.declare
 ! CHECK: cf.cond_br %{{.*}}, ^bb1, ^bb2
 ! CHECK-NEXT: ^bb1:
+! CHECK-NEXT: cuf.free %[[DECL]]#0 : !fir.ref<!fir.array<10xi32>>
 ! CHECK-NEXT: return
 ! CHECK-NEXT: ^bb2:
+! CHECK-NEXT: cuf.free %[[DECL]]#0 : !fir.ref<!fir.array<10xi32>>
 ! CHECK-NEXT: return
 ! CHECK-NEXT: }
 

>From 71cbae8e28d86bea411325b6e57ac63e0f899d4b Mon Sep 17 00:00:00 2001
From: Kazuaki Matsumura <kmatsumura at nvidia.com>
Date: Tue, 7 Jul 2026 13:49:39 -0700
Subject: [PATCH 2/2] [flang-rt][cuda] Tolerate device reset/teardown in CUF
 frees

Freeing device variables at end of the main program can race with a
cudaDeviceReset() or CUDA runtime teardown that already released the
allocation, so cudaFree then returns cudaErrorCudartUnloading or
cudaErrorInvalidValue. Add CUDA_REPORT_IF_ERROR_ALLOW_TEARDOWN[_LOC] and
use it in the CUF free paths (CUFMemFree, CUFFreeDevice, CUFFreeManaged,
hence CUFFreeDescriptor) so the freeing stays a no-op instead of crashing.
---
 flang-rt/lib/cuda/allocator.cpp           |  8 ++++---
 flang-rt/lib/cuda/memory.cpp              |  3 ++-
 flang/include/flang/Runtime/CUDA/common.h | 27 +++++++++++++++++++++++
 flang/lib/Optimizer/CodeGen/CodeGen.cpp   |  3 ++-
 4 files changed, 36 insertions(+), 5 deletions(-)

diff --git a/flang-rt/lib/cuda/allocator.cpp b/flang-rt/lib/cuda/allocator.cpp
index a0214c6004de2..d9c342e77ef51 100644
--- a/flang-rt/lib/cuda/allocator.cpp
+++ b/flang-rt/lib/cuda/allocator.cpp
@@ -198,9 +198,9 @@ void CUFFreeDevice(void *p) {
   if (pos >= 0) {
     cudaStream_t stream = deviceAllocations[pos].stream;
     eraseAllocation(pos);
-    CUDA_REPORT_IF_ERROR(cudaFreeAsync(p, stream));
+    CUDA_REPORT_IF_ERROR_ALLOW_TEARDOWN(cudaFreeAsync(p, stream));
   } else {
-    CUDA_REPORT_IF_ERROR(cudaFree(p));
+    CUDA_REPORT_IF_ERROR_ALLOW_TEARDOWN(cudaFree(p));
   }
 }
 
@@ -213,7 +213,9 @@ void *CUFAllocManaged(std::size_t sizeInBytes,
   return reinterpret_cast<void *>(p);
 }
 
-void CUFFreeManaged(void *p) { CUDA_REPORT_IF_ERROR(cudaFree(p)); }
+void CUFFreeManaged(void *p) {
+  CUDA_REPORT_IF_ERROR_ALLOW_TEARDOWN(cudaFree(p));
+}
 
 void *CUFAllocUnified(std::size_t sizeInBytes,
     [[maybe_unused]] std::size_t alignment,
diff --git a/flang-rt/lib/cuda/memory.cpp b/flang-rt/lib/cuda/memory.cpp
index 05302ee47e093..8fc01cdd4dabb 100644
--- a/flang-rt/lib/cuda/memory.cpp
+++ b/flang-rt/lib/cuda/memory.cpp
@@ -202,7 +202,8 @@ void RTDEF(CUFMemFree)(
     return;
   if (type == kMemTypeDevice || type == kMemTypeManaged ||
       type == kMemTypeUnified) {
-    CUDA_REPORT_IF_ERROR_LOC(cudaFree(ptr), sourceFile, sourceLine);
+    CUDA_REPORT_IF_ERROR_ALLOW_TEARDOWN_LOC(
+        cudaFree(ptr), sourceFile, sourceLine);
   } else if (type == kMemTypePinned) {
     CUDA_REPORT_IF_ERROR_LOC(cudaFreeHost(ptr), sourceFile, sourceLine);
   } else {
diff --git a/flang/include/flang/Runtime/CUDA/common.h b/flang/include/flang/Runtime/CUDA/common.h
index 36e47b0c13655..ab36dac4e7dda 100644
--- a/flang/include/flang/Runtime/CUDA/common.h
+++ b/flang/include/flang/Runtime/CUDA/common.h
@@ -34,6 +34,33 @@ static constexpr unsigned kDeviceToDevice = 2;
     terminator.Crash("'%s' failed with '%s'", #expr, name); \
   }(expr)
 
+// Like CUDA_REPORT_IF_ERROR, but tolerates a cudaDeviceReset() or runtime
+// teardown having already freed the allocation, so CUF frees stay a no-op.
+#define CUDA_REPORT_IF_ERROR_ALLOW_TEARDOWN(expr) \
+  [](cudaError_t err) { \
+    if (err == cudaSuccess || err == cudaErrorCudartUnloading || \
+        err == cudaErrorInvalidValue) \
+      return; \
+    const char *name = cudaGetErrorName(err); \
+    if (!name) \
+      name = "<unknown>"; \
+    Fortran::runtime::Terminator terminator{__FILE__, __LINE__}; \
+    terminator.Crash("'%s' failed with '%s'", #expr, name); \
+  }(expr)
+
+// _LOC variant of CUDA_REPORT_IF_ERROR_ALLOW_TEARDOWN.
+#define CUDA_REPORT_IF_ERROR_ALLOW_TEARDOWN_LOC(expr, file, line) \
+  [](cudaError_t err, const char *sourceFile, int sourceLine) { \
+    if (err == cudaSuccess || err == cudaErrorCudartUnloading || \
+        err == cudaErrorInvalidValue) \
+      return; \
+    const char *name = cudaGetErrorName(err); \
+    if (!name) \
+      name = "<unknown>"; \
+    Fortran::runtime::Terminator terminator{sourceFile, sourceLine}; \
+    terminator.Crash("'%s' failed with '%s'", #expr, name); \
+  }(expr, file, line)
+
 #define CUDA_REPORT_IF_ERROR_LOC(expr, file, line) \
   [](cudaError_t err, const char *sourceFile, int sourceLine) { \
     if (err == cudaSuccess) \
diff --git a/flang/lib/Optimizer/CodeGen/CodeGen.cpp b/flang/lib/Optimizer/CodeGen/CodeGen.cpp
index 5a4d4e9442e91..0a8a2a21c4bda 100644
--- a/flang/lib/Optimizer/CodeGen/CodeGen.cpp
+++ b/flang/lib/Optimizer/CodeGen/CodeGen.cpp
@@ -3670,7 +3670,8 @@ struct LoadOpConversion : public fir::FIROpConversion<fir::LoadOp> {
         if (callOp.getCallee() &&
             (*callOp.getCallee()).starts_with("__tgt_acc_get_deviceptr")) {
           // The device pointer descriptor is allocated in managed memory, so
-          // new storage must be allocated the same way.
+          // new storage must be allocated the same way. A CUFAllocDescriptor
+          // source is handled below if the load is used by a GPU launch.
           auto mod = load->getParentOfType<mlir::ModuleOp>();
           newBoxStorage =
               genCUFAllocDescriptor(loc, rewriter, mod, boxTy, lowerTy());



More information about the flang-commits mailing list