[flang-commits] [flang] [flang][cuda] Resolve scalar constant address to device copy for reads (PR #227802)

via flang-commits flang-commits at lists.llvm.org
Thu Oct 1 13:55:00 PDT 2026


https://github.com/yebinchon updated https://github.com/llvm/llvm-project/pull/227802

>From 363b1cfa5955569da3db07d400956d6594e97e20 Mon Sep 17 00:00:00 2001
From: Yebin Chon <ychon at nvidia.com>
Date: Wed, 30 Sep 2026 09:48:46 -0700
Subject: [PATCH 1/3] keep constant scalar reads on host

---
 flang/lib/Lower/Bridge.cpp                   | 32 ++++++++++++++++++++
 flang/test/Lower/CUDA/cuda-data-transfer.cuf | 25 +++++++++++++++
 2 files changed, 57 insertions(+)

diff --git a/flang/lib/Lower/Bridge.cpp b/flang/lib/Lower/Bridge.cpp
index 8bd073a479e19..0081e0e71eb4c 100644
--- a/flang/lib/Lower/Bridge.cpp
+++ b/flang/lib/Lower/Bridge.cpp
@@ -5484,6 +5484,7 @@ class FirConverter : public Fortran::lower::AbstractConverter {
                            hlfir::Entity &lhs, hlfir::Entity &rhs,
                            bool isWholeAllocatableAssignment,
                            bool keepLhsLengthInAllocatableAssignment) {
+
     bool lhsIsDevice = Fortran::evaluate::HasCUDADeviceAttrs(assign.lhs);
     // A managed/unified/device function result is not visible to the symbol
     // collection used by HasCUDADeviceAttrs (a ProcedureRef contributes no
@@ -5587,10 +5588,41 @@ class FirConverter : public Fortran::lower::AbstractConverter {
                                 keepLhsLengthInAllocatableAssignment);
         return;
       }
+      // True iff every CUDA symbol the RHS references is a scalar constant (or
+      // host-resident pinned). A scalar constant keeps a synchronized host
+      // copy, so such a read can be done on the host with no device->host copy
+      // (issuing one would pass the host copy's address as a bogus "device"
+      // source). Array constants live in real device memory with no host copy,
+      // so a read of one still needs the copy.
+      auto rhsIsScalarConstantOnly = [&]() {
+        bool sawConstant = false;
+        for (const Fortran::semantics::Symbol &sym :
+             Fortran::evaluate::CollectCudaSymbols(assign.rhs)) {
+          const Fortran::semantics::Symbol &ultimate = sym.GetUltimate();
+          std::optional<Fortran::common::CUDADataAttr> attr =
+              Fortran::semantics::GetCUDADataAttr(&ultimate);
+          if (!attr || *attr == Fortran::common::CUDADataAttr::Pinned)
+            continue; // host-resident
+          if (*attr == Fortran::common::CUDADataAttr::Constant &&
+              ultimate.Rank() == 0) {
+            sawConstant = true;
+            continue;
+          }
+          return false; // device/managed/unified/array constant -> keep copy
+        }
+        return sawConstant;
+      };
+
       auto transferKindAttr = cuf::DataTransferKindAttr::get(
           builder.getContext(), cuf::DataTransferKind::DeviceHost);
       if (fir::isa_trivial(rhsVal.getType())) {
         fir::StoreOp::create(builder, loc, rhsVal, lhsVal);
+      } else if (rhsIsScalarConstantOnly()) {
+        // Assign on the host: the RHS's only device residence is scalar
+        // constant data, which has a synchronized host copy.
+        hlfir::AssignOp::create(builder, loc, rhs, lhs,
+                                isWholeAllocatableAssignment,
+                                keepLhsLengthInAllocatableAssignment);
       } else {
         cuf::DataTransferOp::create(builder, loc, rhsVal, lhsVal, shape,
                                     transferKindAttr,
diff --git a/flang/test/Lower/CUDA/cuda-data-transfer.cuf b/flang/test/Lower/CUDA/cuda-data-transfer.cuf
index 7cc40773d2789..55db7ab2657fa 100644
--- a/flang/test/Lower/CUDA/cuda-data-transfer.cuf
+++ b/flang/test/Lower/CUDA/cuda-data-transfer.cuf
@@ -29,6 +29,7 @@ module mod1
   real(kind=8), device, allocatable, dimension(:) :: p
 
   real, constant :: c1 = 1.0
+  real, constant :: carr(10)
   real, device :: d1 = 1.0
 
   interface
@@ -795,3 +796,27 @@ end subroutine
 ! CHECK: arith.divf
 ! CHECK: hlfir.assign
 ! CHECK: cuf.data_transfer
+
+! A host read of a scalar constant broadcast into a host array keeps a
+! synchronized host copy, so it is a plain host assignment with no transfer.
+subroutine sub47()
+  use mod1
+  real, allocatable :: e(:)
+  allocate(e(10))
+  e = c1
+end subroutine
+
+! CHECK-LABEL: func.func @_QPsub47()
+! CHECK: hlfir.assign
+! CHECK-NOT: cuf.data_transfer
+
+! An array constant lives in device memory (no host copy), so a host read of
+! one must still be copied from the device.
+subroutine sub48()
+  use mod1
+  real :: harr(10)
+  harr = carr
+end subroutine
+
+! CHECK-LABEL: func.func @_QPsub48()
+! CHECK: cuf.data_transfer %{{.*}} to %{{.*}} {transfer_kind = #cuf.cuda_transfer<device_host>}

>From 5ba37816a9aa47c6d241cf4317e5ad31824c66f5 Mon Sep 17 00:00:00 2001
From: Yebin Chon <ychon at nvidia.com>
Date: Wed, 30 Sep 2026 16:41:22 -0700
Subject: [PATCH 2/3] restore data transfer for host=constant. resolve constant
 address to decive copy

---
 flang/lib/Lower/Bridge.cpp                    | 32 -------
 .../Transforms/CUDA/CUFOpConversion.cpp       | 91 ++++++++++++-------
 flang/test/Fir/CUDA/cuda-data-transfer.fir    | 44 +++++++++
 flang/test/Fir/CUDA/cuda-global-addr.mlir     | 12 ++-
 flang/test/Lower/CUDA/cuda-data-transfer.cuf  | 25 -----
 5 files changed, 109 insertions(+), 95 deletions(-)

diff --git a/flang/lib/Lower/Bridge.cpp b/flang/lib/Lower/Bridge.cpp
index 0081e0e71eb4c..8bd073a479e19 100644
--- a/flang/lib/Lower/Bridge.cpp
+++ b/flang/lib/Lower/Bridge.cpp
@@ -5484,7 +5484,6 @@ class FirConverter : public Fortran::lower::AbstractConverter {
                            hlfir::Entity &lhs, hlfir::Entity &rhs,
                            bool isWholeAllocatableAssignment,
                            bool keepLhsLengthInAllocatableAssignment) {
-
     bool lhsIsDevice = Fortran::evaluate::HasCUDADeviceAttrs(assign.lhs);
     // A managed/unified/device function result is not visible to the symbol
     // collection used by HasCUDADeviceAttrs (a ProcedureRef contributes no
@@ -5588,41 +5587,10 @@ class FirConverter : public Fortran::lower::AbstractConverter {
                                 keepLhsLengthInAllocatableAssignment);
         return;
       }
-      // True iff every CUDA symbol the RHS references is a scalar constant (or
-      // host-resident pinned). A scalar constant keeps a synchronized host
-      // copy, so such a read can be done on the host with no device->host copy
-      // (issuing one would pass the host copy's address as a bogus "device"
-      // source). Array constants live in real device memory with no host copy,
-      // so a read of one still needs the copy.
-      auto rhsIsScalarConstantOnly = [&]() {
-        bool sawConstant = false;
-        for (const Fortran::semantics::Symbol &sym :
-             Fortran::evaluate::CollectCudaSymbols(assign.rhs)) {
-          const Fortran::semantics::Symbol &ultimate = sym.GetUltimate();
-          std::optional<Fortran::common::CUDADataAttr> attr =
-              Fortran::semantics::GetCUDADataAttr(&ultimate);
-          if (!attr || *attr == Fortran::common::CUDADataAttr::Pinned)
-            continue; // host-resident
-          if (*attr == Fortran::common::CUDADataAttr::Constant &&
-              ultimate.Rank() == 0) {
-            sawConstant = true;
-            continue;
-          }
-          return false; // device/managed/unified/array constant -> keep copy
-        }
-        return sawConstant;
-      };
-
       auto transferKindAttr = cuf::DataTransferKindAttr::get(
           builder.getContext(), cuf::DataTransferKind::DeviceHost);
       if (fir::isa_trivial(rhsVal.getType())) {
         fir::StoreOp::create(builder, loc, rhsVal, lhsVal);
-      } else if (rhsIsScalarConstantOnly()) {
-        // Assign on the host: the RHS's only device residence is scalar
-        // constant data, which has a synchronized host copy.
-        hlfir::AssignOp::create(builder, loc, rhs, lhs,
-                                isWholeAllocatableAssignment,
-                                keepLhsLengthInAllocatableAssignment);
       } else {
         cuf::DataTransferOp::create(builder, loc, rhsVal, lhsVal, shape,
                                     transferKindAttr,
diff --git a/flang/lib/Optimizer/Transforms/CUDA/CUFOpConversion.cpp b/flang/lib/Optimizer/Transforms/CUDA/CUFOpConversion.cpp
index 596368d94e434..49af22f2b119c 100644
--- a/flang/lib/Optimizer/Transforms/CUDA/CUFOpConversion.cpp
+++ b/flang/lib/Optimizer/Transforms/CUDA/CUFOpConversion.cpp
@@ -105,6 +105,29 @@ static bool isScalarCudaConstantGlobal(fir::GlobalOp global) {
          fir::isa_trivial(fir::unwrapRefType(global.getType()));
 }
 
+// The fir.addr_of feeding a declared value, or null.
+static fir::AddrOfOp getDeclaredAddrOf(mlir::Value val) {
+  if (auto declareOp = val.getDefiningOp<fir::DeclareOp>())
+    return declareOp.getMemref().getDefiningOp<fir::AddrOfOp>();
+  if (auto declareOp = val.getDefiningOp<hlfir::DeclareOp>())
+    return declareOp.getMemref().getDefiningOp<fir::AddrOfOp>();
+  return {};
+}
+
+// Address of the host shadow when val designates a scalar CUDA constant, null
+// otherwise.
+static fir::AddrOfOp
+getScalarConstantShadowAddr(mlir::Value val, const mlir::SymbolTable &symtab) {
+  fir::AddrOfOp addrOfOp = getDeclaredAddrOf(val);
+  if (!addrOfOp)
+    return {};
+  auto global = symtab.lookup<fir::GlobalOp>(
+      addrOfOp.getSymbol().getRootReference().getValue());
+  if (!isScalarCudaConstantGlobal(global))
+    return {};
+  return addrOfOp;
+}
+
 struct DeclareOpConversion : public mlir::OpRewritePattern<fir::DeclareOp> {
   using OpRewritePattern::OpRewritePattern;
 
@@ -300,6 +323,31 @@ struct CUFDataTransferOpConversion
     mlir::Value modeValue =
         builder.createIntegerConstant(loc, builder.getI32Type(), mode);
 
+    // A host read of a scalar CUDA constant takes the device copy. Its host
+    // shadow is maintained only to service host writes; resolve the source to
+    // the registered device address so the device->host copy reads real device
+    // memory. The host shadow address would be an invalid "device" source for
+    // cudaMemcpy on a strict runtime. This is the read counterpart of the
+    // scalar-constant handling in the host-to-device and device-to-device
+    // paths below.
+    //
+    // Unlike those paths, this rewrites the op source itself and must run
+    // before the descriptor split: a read destination can be a scalar, an
+    // array (scalar broadcast, lowered via CUFDataTransferCstDesc) or a
+    // descriptor, and each of those paths reads op.getSrc() at a different
+    // point below. Fixing up the source here lets every one of them pick up the
+    // device address.
+    if (op.getTransferKind() == cuf::DataTransferKind::DeviceHost) {
+      if (fir::AddrOfOp shadow =
+              getScalarConstantShadowAddr(op.getSrc(), symtab)) {
+        mlir::Value devAddr = cuf::DeviceAddressOp::create(
+            rewriter, loc, op.getSrc().getType(), shadow.getSymbol());
+        rewriter.startOpModification(op);
+        op.getSrcMutable().assign(devAddr);
+        rewriter.finalizeOpModification(op);
+      }
+    }
+
     // Convert data transfer without any descriptor.
     if (!mlir::isa<fir::BaseBoxType>(srcTy) &&
         !mlir::isa<fir::BaseBoxType>(dstTy)) {
@@ -352,41 +400,14 @@ struct CUFDataTransferOpConversion
 
       mlir::Value dst = op.getDst();
       mlir::Value src = op.getSrc();
-      // Scalar CUDA constants keep a host shadow for host reads. Host-to-device
-      // assignments also update the device constant symbol.
-      auto getAddrOf = [](mlir::Value val) -> fir::AddrOfOp {
-        if (auto declareOp = val.getDefiningOp<fir::DeclareOp>())
-          return declareOp.getMemref().getDefiningOp<fir::AddrOfOp>();
-        if (auto declareOp = val.getDefiningOp<hlfir::DeclareOp>())
-          return declareOp.getMemref().getDefiningOp<fir::AddrOfOp>();
-        return {};
-      };
-      // Address of the host shadow when val designates a scalar CUDA constant,
-      // null otherwise.
-      auto getShadowAddrOf = [&](mlir::Value val) -> fir::AddrOfOp {
-        fir::AddrOfOp addrOfOp = getAddrOf(val);
-        if (!addrOfOp)
-          return {};
-        auto global = symtab.lookup<fir::GlobalOp>(
-            addrOfOp.getSymbol().getRootReference().getValue());
-        if (!isScalarCudaConstantGlobal(global))
-          return {};
-        return addrOfOp;
-      };
-      if (op.getTransferKind() == cuf::DataTransferKind::DeviceHost) {
-        if (getShadowAddrOf(src) && fir::isa_ref_type(dst.getType())) {
-          mlir::Value hostValue = fir::LoadOp::create(builder, loc, src);
-          hostValue = createConvertOp(rewriter, loc, dstTy, hostValue);
-          fir::StoreOp::create(builder, loc, hostValue, dst);
-          rewriter.eraseOp(op);
-          return mlir::success();
-        }
-      }
+      // A scalar CUDA constant keeps a host shadow that host-to-device
+      // assignments update (so host writes stay visible); host reads take the
+      // device copy and were redirected to it above.
       if (op.getTransferKind() == cuf::DataTransferKind::HostDevice) {
         // A non-null shadow means the destination is a scalar constant. Keep
-        // its shadow up to date for later host reads, then aim the copy at the
-        // device symbol instead of the shadow.
-        if (fir::AddrOfOp addrOfOp = getShadowAddrOf(dst)) {
+        // its host shadow up to date, then aim the copy at the device symbol
+        // instead of the shadow.
+        if (fir::AddrOfOp addrOfOp = getScalarConstantShadowAddr(dst, symtab)) {
           mlir::Value hostValue = src;
           if (fir::isa_ref_type(src.getType()))
             hostValue = fir::LoadOp::create(builder, loc, src);
@@ -403,8 +424,8 @@ struct CUFDataTransferOpConversion
         // hand is a host address and cannot be an operand of a device to
         // device copy. Route the transfer through the shadow. There are three
         // cases, depending on which side is a scalar constant.
-        fir::AddrOfOp srcShadow = getShadowAddrOf(src);
-        fir::AddrOfOp dstShadow = getShadowAddrOf(dst);
+        fir::AddrOfOp srcShadow = getScalarConstantShadowAddr(src, symtab);
+        fir::AddrOfOp dstShadow = getScalarConstantShadowAddr(dst, symtab);
         if (srcShadow || dstShadow) {
           if (dstShadow) {
             if (srcShadow) {
diff --git a/flang/test/Fir/CUDA/cuda-data-transfer.fir b/flang/test/Fir/CUDA/cuda-data-transfer.fir
index 99ba60ad1e6a5..904f005b948e0 100644
--- a/flang/test/Fir/CUDA/cuda-data-transfer.fir
+++ b/flang/test/Fir/CUDA/cuda-data-transfer.fir
@@ -233,6 +233,50 @@ func.func @_QPsub9() {
 // CHECK: %[[SRC:.*]] = fir.convert %[[LOCAL]] : (!fir.ref<!fir.array<5xi32>>) -> !fir.llvm_ptr<i8>
 // CHECK: fir.call @_FortranACUFDataTransferPtrPtr(%[[DST]], %[[SRC]], %{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}) : (!fir.llvm_ptr<i8>, !fir.llvm_ptr<i8>, i64, i32, !fir.ref<i8>, i32) -> ()
 
+fir.global @_QMmscalEc0 {data_attr = #cuf.cuda<constant>} : i32 {
+  %0 = arith.constant 2 : i32
+  fir.has_value %0 : i32
+}
+func.func @_QPsub_cst_read() {
+  %0 = fir.alloca i32 {bindc_name = "i", uniq_name = "_QFsub_cst_readEi"}
+  %1 = fir.declare %0 {uniq_name = "_QFsub_cst_readEi"} : (!fir.ref<i32>) -> !fir.ref<i32>
+  %2 = fir.address_of(@_QMmscalEc0) : !fir.ref<i32>
+  %3 = fir.declare %2 {data_attr = #cuf.cuda<constant>, uniq_name = "_QMmscalEc0"} : (!fir.ref<i32>) -> !fir.ref<i32>
+  cuf.data_transfer %3 to %1 {transfer_kind = #cuf.cuda_transfer<device_host>} : !fir.ref<i32>, !fir.ref<i32>
+  return
+}
+
+// A host read of a scalar constant resolves to the device copy: the source is
+// routed through _FortranACUFGetDeviceAddress rather than loaded from the host
+// shadow.
+// CHECK-LABEL: func.func @_QPsub_cst_read()
+// CHECK: fir.address_of(@_QMmscalEc0) : !fir.ref<i32>
+// CHECK: %[[GBL:.*]] = fir.address_of(@_QMmscalEc0) : !fir.ref<i32>
+// CHECK: %[[GBL_CONV:.*]] = fir.convert %[[GBL]] : (!fir.ref<i32>) -> !fir.llvm_ptr<i8>
+// CHECK: fir.call @_FortranACUFGetDeviceAddress(%[[GBL_CONV]], %{{.*}}, %{{.*}}) : (!fir.llvm_ptr<i8>, !fir.ref<i8>, i32) -> !fir.llvm_ptr<i8>
+// CHECK: fir.call @_FortranACUFDataTransferPtrPtr(
+
+func.func @_QPsub_cst_bcast() {
+  %c10 = arith.constant 10 : index
+  %0 = fir.alloca !fir.array<10xi32> {bindc_name = "e", uniq_name = "_QFsub_cst_bcastEe"}
+  %1 = fir.shape %c10 : (index) -> !fir.shape<1>
+  %2 = fir.declare %0(%1) {uniq_name = "_QFsub_cst_bcastEe"} : (!fir.ref<!fir.array<10xi32>>, !fir.shape<1>) -> !fir.ref<!fir.array<10xi32>>
+  %3 = fir.address_of(@_QMmscalEc0) : !fir.ref<i32>
+  %4 = fir.declare %3 {data_attr = #cuf.cuda<constant>, uniq_name = "_QMmscalEc0"} : (!fir.ref<i32>) -> !fir.ref<i32>
+  cuf.data_transfer %4 to %2 {transfer_kind = #cuf.cuda_transfer<device_host>} : !fir.ref<i32>, !fir.ref<!fir.array<10xi32>>
+  return
+}
+
+// A scalar constant broadcast into a host array is also read from the device
+// copy: the source is routed through _FortranACUFGetDeviceAddress and then
+// broadcast by _FortranACUFDataTransferCstDesc.
+// CHECK-LABEL: func.func @_QPsub_cst_bcast()
+// CHECK: fir.address_of(@_QMmscalEc0) : !fir.ref<i32>
+// CHECK: %[[GBL:.*]] = fir.address_of(@_QMmscalEc0) : !fir.ref<i32>
+// CHECK: %[[GBL_CONV:.*]] = fir.convert %[[GBL]] : (!fir.ref<i32>) -> !fir.llvm_ptr<i8>
+// CHECK: fir.call @_FortranACUFGetDeviceAddress(%[[GBL_CONV]], %{{.*}}, %{{.*}}) : (!fir.llvm_ptr<i8>, !fir.ref<i8>, i32) -> !fir.llvm_ptr<i8>
+// CHECK: fir.call @_FortranACUFDataTransferCstDesc(
+
 fir.global @_QMmod1Ea {data_attr = #cuf.cuda<device>} : !fir.box<!fir.heap<!fir.array<?xi32>>> {
   %c0 = arith.constant 0 : index
   %0 = fir.zero_bits !fir.heap<!fir.array<?xi32>>
diff --git a/flang/test/Fir/CUDA/cuda-global-addr.mlir b/flang/test/Fir/CUDA/cuda-global-addr.mlir
index 74eadbca407ec..33a29dc22fa35 100644
--- a/flang/test/Fir/CUDA/cuda-global-addr.mlir
+++ b/flang/test/Fir/CUDA/cuda-global-addr.mlir
@@ -170,13 +170,19 @@ module attributes {dlti.dl_spec = #dlti.dl_spec<#dlti.dl_entry<f80, dense<128> :
 // CHECK: fir.call @_FortranACUFGetDeviceAddress
 // CHECK-NOT: fir.load %{{.*}} : !fir.ref<i32>
 // CHECK: fir.call @_QPuse_index
+// A host read of a scalar CUDA constant is copied from the device symbol. The
+// host shadow is never read: the source is resolved to the device address and
+// the value is brought in with a device to host copy (mode 1).
 // CHECK-LABEL: func.func @_QQconstant_scalar_device_to_host()
 // CHECK: %[[ADDR:.*]] = fir.address_of(@_QMcon3Ezzz) : !fir.ref<i32>
 // CHECK: %[[DECL:.*]] = fir.declare %[[ADDR]] {data_attr = #cuf.cuda<constant>, uniq_name = "_QMcon3Ezzz"} : (!fir.ref<i32>) -> !fir.ref<i32>
 // CHECK: %[[DST:.*]] = fir.alloca i32
-// CHECK: %[[VALUE:.*]] = fir.load %[[DECL]] : !fir.ref<i32>
-// CHECK: fir.store %[[VALUE]] to %[[DST]] : !fir.ref<i32>
-// CHECK-NOT: fir.call @_FortranACUFDataTransferPtrPtr
+// CHECK-NOT: fir.load %[[DECL]] : !fir.ref<i32>
+// CHECK: fir.call @_FortranACUFGetDeviceAddress
+// CHECK: %[[DEVADDR:.*]] = fir.convert %{{.*}} : (!fir.llvm_ptr<i8>) -> !fir.ref<i32>
+// CHECK: %[[DSTCONV:.*]] = fir.convert %[[DST]] : (!fir.ref<i32>) -> !fir.llvm_ptr<i8>
+// CHECK: %[[SRCCONV:.*]] = fir.convert %[[DEVADDR]] : (!fir.ref<i32>) -> !fir.llvm_ptr<i8>
+// CHECK: fir.call @_FortranACUFDataTransferPtrPtr(%[[DSTCONV]], %[[SRCCONV]], %{{[^,]*}}, %c1_i32
 
 // -----
 
diff --git a/flang/test/Lower/CUDA/cuda-data-transfer.cuf b/flang/test/Lower/CUDA/cuda-data-transfer.cuf
index 55db7ab2657fa..7cc40773d2789 100644
--- a/flang/test/Lower/CUDA/cuda-data-transfer.cuf
+++ b/flang/test/Lower/CUDA/cuda-data-transfer.cuf
@@ -29,7 +29,6 @@ module mod1
   real(kind=8), device, allocatable, dimension(:) :: p
 
   real, constant :: c1 = 1.0
-  real, constant :: carr(10)
   real, device :: d1 = 1.0
 
   interface
@@ -796,27 +795,3 @@ end subroutine
 ! CHECK: arith.divf
 ! CHECK: hlfir.assign
 ! CHECK: cuf.data_transfer
-
-! A host read of a scalar constant broadcast into a host array keeps a
-! synchronized host copy, so it is a plain host assignment with no transfer.
-subroutine sub47()
-  use mod1
-  real, allocatable :: e(:)
-  allocate(e(10))
-  e = c1
-end subroutine
-
-! CHECK-LABEL: func.func @_QPsub47()
-! CHECK: hlfir.assign
-! CHECK-NOT: cuf.data_transfer
-
-! An array constant lives in device memory (no host copy), so a host read of
-! one must still be copied from the device.
-subroutine sub48()
-  use mod1
-  real :: harr(10)
-  harr = carr
-end subroutine
-
-! CHECK-LABEL: func.func @_QPsub48()
-! CHECK: cuf.data_transfer %{{.*}} to %{{.*}} {transfer_kind = #cuf.cuda_transfer<device_host>}

>From 1aedbd4c0afc236c39f1e5d089fc67ebf64e7e0e Mon Sep 17 00:00:00 2001
From: Yebin Chon <ychon at nvidia.com>
Date: Thu, 1 Oct 2026 12:57:19 -0700
Subject: [PATCH 3/3] add array destination test

---
 flang/test/Fir/CUDA/cuda-data-transfer.fir | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/flang/test/Fir/CUDA/cuda-data-transfer.fir b/flang/test/Fir/CUDA/cuda-data-transfer.fir
index 904f005b948e0..002894bb0ab17 100644
--- a/flang/test/Fir/CUDA/cuda-data-transfer.fir
+++ b/flang/test/Fir/CUDA/cuda-data-transfer.fir
@@ -277,6 +277,26 @@ func.func @_QPsub_cst_bcast() {
 // CHECK: fir.call @_FortranACUFGetDeviceAddress(%[[GBL_CONV]], %{{.*}}, %{{.*}}) : (!fir.llvm_ptr<i8>, !fir.ref<i8>, i32) -> !fir.llvm_ptr<i8>
 // CHECK: fir.call @_FortranACUFDataTransferCstDesc(
 
+func.func @_QPsub_cst_bcast_alloc() {
+  %0 = fir.alloca !fir.box<!fir.heap<!fir.array<?xi32>>> {bindc_name = "e", uniq_name = "_QFsub_cst_bcast_allocEe"}
+  %1:2 = hlfir.declare %0 {fortran_attrs = #fir.var_attrs<allocatable>, uniq_name = "_QFsub_cst_bcast_allocEe"} : (!fir.ref<!fir.box<!fir.heap<!fir.array<?xi32>>>>) -> (!fir.ref<!fir.box<!fir.heap<!fir.array<?xi32>>>>, !fir.ref<!fir.box<!fir.heap<!fir.array<?xi32>>>>)
+  %2 = fir.address_of(@_QMmscalEc0) : !fir.ref<i32>
+  %3 = fir.declare %2 {data_attr = #cuf.cuda<constant>, uniq_name = "_QMmscalEc0"} : (!fir.ref<i32>) -> !fir.ref<i32>
+  cuf.data_transfer %3 to %1#0 {transfer_kind = #cuf.cuda_transfer<device_host>} : !fir.ref<i32>, !fir.ref<!fir.box<!fir.heap<!fir.array<?xi32>>>>
+  return
+}
+
+// A scalar constant broadcast into an allocatable host array goes through the
+// descriptor path, but the source is still resolved to the device copy with
+// _FortranACUFGetDeviceAddress before being broadcast by
+// _FortranACUFDataTransferCstDesc.
+// CHECK-LABEL: func.func @_QPsub_cst_bcast_alloc()
+// CHECK: fir.address_of(@_QMmscalEc0) : !fir.ref<i32>
+// CHECK: %[[GBL:.*]] = fir.address_of(@_QMmscalEc0) : !fir.ref<i32>
+// CHECK: %[[GBL_CONV:.*]] = fir.convert %[[GBL]] : (!fir.ref<i32>) -> !fir.llvm_ptr<i8>
+// CHECK: fir.call @_FortranACUFGetDeviceAddress(%[[GBL_CONV]], %{{.*}}, %{{.*}}) : (!fir.llvm_ptr<i8>, !fir.ref<i8>, i32) -> !fir.llvm_ptr<i8>
+// CHECK: fir.call @_FortranACUFDataTransferCstDesc(
+
 fir.global @_QMmod1Ea {data_attr = #cuf.cuda<device>} : !fir.box<!fir.heap<!fir.array<?xi32>>> {
   %c0 = arith.constant 0 : index
   %0 = fir.zero_bits !fir.heap<!fir.array<?xi32>>



More information about the flang-commits mailing list