[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