[clang] [llvm] [WIP][OFFLOAD][OpenMP] Fix USM close mapping, and track declare-target storage under USM (PR #213214)
Abhinav Gaba via cfe-commits
cfe-commits at lists.llvm.org
Tue Aug 11 14:48:34 PDT 2026
https://github.com/abhinavgaba updated https://github.com/llvm/llvm-project/pull/213214
>From 9add159bdad5f6783443df5bdf135b9a1c11b3ba Mon Sep 17 00:00:00 2001
From: "Fine, Gregory" <gregory.fine at intel.com>
Date: Tue, 7 Jul 2026 16:52:13 -0700
Subject: [PATCH 01/29] Fix an issue with nested close, alloc mapping when
using USM
---
offload/libomptarget/OpenMP/Mapping.cpp | 8 +++++
.../unified_shared_memory/close_sub_section.c | 32 +++++++++++++++++++
.../close_sub_section_oob.c | 32 +++++++++++++++++++
3 files changed, 72 insertions(+)
create mode 100644 offload/test/unified_shared_memory/close_sub_section.c
create mode 100644 offload/test/unified_shared_memory/close_sub_section_oob.c
diff --git a/offload/libomptarget/OpenMP/Mapping.cpp b/offload/libomptarget/OpenMP/Mapping.cpp
index 1bb2e424bd083..a9266bf7ca1f9 100644
--- a/offload/libomptarget/OpenMP/Mapping.cpp
+++ b/offload/libomptarget/OpenMP/Mapping.cpp
@@ -282,6 +282,14 @@ TargetPointerResultTy MappingInfoTy::getTargetPointer(
LR.TPR.Flags.IsPresent = false;
LR.TPR.Flags.IsHostPointer = true;
LR.TPR.TargetPointer = HstPtrBegin;
+ // Create a mapping for a case when map(close, alloc:...) is applied to a
+ // subsection of previously mapped allocation. The mapping would prevent
+ // map(close, alloc:...) from creating a new allocation as it would reuse
+ // the mapped allocation instead.
+ HDTTMap->emplace(new HostDataToTargetTy(
+ (uintptr_t)HstPtrBase, (uintptr_t)HstPtrBegin,
+ (uintptr_t)HstPtrBegin + Size, (uintptr_t)HstPtrBegin,
+ (uintptr_t)HstPtrBegin, true, HstPtrName));
}
} else if (HasPresentModifier) {
ODBG(ODT_Mapping) << "Mapping required by 'present' map type modifier does "
diff --git a/offload/test/unified_shared_memory/close_sub_section.c b/offload/test/unified_shared_memory/close_sub_section.c
new file mode 100644
index 0000000000000..06cf444c49283
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_sub_section.c
@@ -0,0 +1,32 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+#pragma omp requires unified_shared_memory
+
+#include <stdio.h>
+
+int main() {
+ double base[10] = {0};
+
+#pragma omp target data map(from : base[0 : 10])
+ {
+// close range covers base[1] and should properly
+// update the budder from the outer mapping.
+#pragma omp target map(close, alloc : base[1 : 9])
+ {
+ base[1] = 99.0;
+ }
+ }
+ // CHECK: base[1] = 99.000000 (expected 99.0)
+ printf("base[1] = %f (expected 99.0)\n", base[1]);
+ return 0;
+}
\ No newline at end of file
diff --git a/offload/test/unified_shared_memory/close_sub_section_oob.c b/offload/test/unified_shared_memory/close_sub_section_oob.c
new file mode 100644
index 0000000000000..1638f4a6f3c4c
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_sub_section_oob.c
@@ -0,0 +1,32 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+#pragma omp requires unified_shared_memory
+
+#include <stdio.h>
+
+int main() {
+ double base[10] = {0};
+
+#pragma omp target data map(from : base[0 : 10])
+ {
+// close on sub-section base[2:8]; access to base[1] is outside the close
+// buffer
+#pragma omp target map(close, alloc : base[2 : 8])
+ {
+ base[1] = 99.0;
+ }
+ }
+ // CHECK: base[1] = 99.000000 (expected 99.0)
+ printf("base[1] = %f (expected 99.0)\n", base[1]);
+ return 0;
+}
\ No newline at end of file
>From 646b2dd87aed73f39b1c0e0382e1260ce8a1451c Mon Sep 17 00:00:00 2001
From: "Fine, Gregory" <gregory.fine at intel.com>
Date: Wed, 8 Jul 2026 15:33:38 -0700
Subject: [PATCH 02/29] Add test
---
.../unified_shared_memory/check_tracking.c | 30 +++++++++++++++++++
1 file changed, 30 insertions(+)
create mode 100644 offload/test/unified_shared_memory/check_tracking.c
diff --git a/offload/test/unified_shared_memory/check_tracking.c b/offload/test/unified_shared_memory/check_tracking.c
new file mode 100644
index 0000000000000..7607e9df90f87
--- /dev/null
+++ b/offload/test/unified_shared_memory/check_tracking.c
@@ -0,0 +1,30 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+#pragma omp requires unified_shared_memory
+#include <omp.h>
+#include <stdio.h>
+
+int main() {
+ int x = 111;
+
+ // CHECK: present when unmapped: 0
+ printf("present when unmapped: %d\n",
+ omp_target_is_present(&x, omp_get_default_device()));
+
+#pragma omp target_enter_data map(alloc : x)
+
+ // CHECK: present after mapping: 1
+ printf("present after mapping: %d\n",
+ omp_target_is_present(&x, omp_get_default_device()));
+ return 0;
+}
\ No newline at end of file
>From 15bbe4e9030dc2f35a7ee210efc917031a55d205 Mon Sep 17 00:00:00 2001
From: "Fine, Gregory" <gregory.fine at intel.com>
Date: Thu, 9 Jul 2026 16:18:41 -0700
Subject: [PATCH 03/29] Add handling of global base
---
offload/libomptarget/OpenMP/Mapping.cpp | 2 +-
.../close_sub_section_global.c | 33 +++++++++++++++++++
.../close_sub_section_global_oob.c | 33 +++++++++++++++++++
3 files changed, 67 insertions(+), 1 deletion(-)
create mode 100644 offload/test/unified_shared_memory/close_sub_section_global.c
create mode 100644 offload/test/unified_shared_memory/close_sub_section_global_oob.c
diff --git a/offload/libomptarget/OpenMP/Mapping.cpp b/offload/libomptarget/OpenMP/Mapping.cpp
index a9266bf7ca1f9..064df0a0c7722 100644
--- a/offload/libomptarget/OpenMP/Mapping.cpp
+++ b/offload/libomptarget/OpenMP/Mapping.cpp
@@ -263,7 +263,7 @@ TargetPointerResultTy MappingInfoTy::getTargetPointer(
"exist for host address " DPxMOD " (%" PRId64 " bytes)",
DPxPTR(HstPtrBegin), Size);
} else if ((PM->getRequirements() & OMP_REQ_UNIFIED_SHARED_MEMORY &&
- !HasCloseModifier) ||
+ (!HasCloseModifier || LR.TPR.getEntry() != nullptr)) ||
(PM->getRequirements() & OMPX_REQ_AUTO_ZERO_COPY)) {
// If unified shared memory is active, implicitly mapped variables that are
diff --git a/offload/test/unified_shared_memory/close_sub_section_global.c b/offload/test/unified_shared_memory/close_sub_section_global.c
new file mode 100644
index 0000000000000..7bf128d636fa8
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_sub_section_global.c
@@ -0,0 +1,33 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+#pragma omp requires unified_shared_memory
+
+#include <stdio.h>
+
+#pragma omp begin declare target
+double base[10] = {0};
+#pragma omp end declare target
+
+int main() {
+
+// close range covers base[1] and should properly
+// update the budder from the outer mapping.
+#pragma omp target map(close, alloc : base[1 : 9])
+ {
+ base[1] = 99.0;
+ }
+#pragma omp target update from(base)
+ // CHECK: base[1] = 99.000000 (expected 99.0)
+ printf("base[1] = %f (expected 99.0)\n", base[1]);
+ return 0;
+}
\ No newline at end of file
diff --git a/offload/test/unified_shared_memory/close_sub_section_global_oob.c b/offload/test/unified_shared_memory/close_sub_section_global_oob.c
new file mode 100644
index 0000000000000..eaf5ff8c74490
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_sub_section_global_oob.c
@@ -0,0 +1,33 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+#pragma omp requires unified_shared_memory
+
+#include <stdio.h>
+
+#pragma omp begin declare target
+double base[10] = {0};
+#pragma omp end declare target
+
+int main() {
+
+// close on sub-section base[2:8]; access to base[1] is outside the close
+// buffer
+#pragma omp target map(close, alloc : base[2 : 8])
+ {
+ base[1] = 99.0;
+ }
+#pragma omp target update from(base)
+ // CHECK: base[1] = 99.000000 (expected 99.0)
+ printf("base[1] = %f (expected 99.0)\n", base[1]);
+ return 0;
+}
\ No newline at end of file
>From 0c5ce50c77cd2966296e451a2c652e33e1c99ab0 Mon Sep 17 00:00:00 2001
From: "Fine, Gregory" <gregory.fine at intel.com>
Date: Mon, 27 Jul 2026 20:38:40 -0700
Subject: [PATCH 04/29] Fix reference counting
---
offload/libomptarget/OpenMP/Mapping.cpp | 34 +++++++++++++++---
offload/libomptarget/PluginManager.cpp | 22 ++++++++++++
.../unified_shared_memory/check_tracking.c | 35 +++++++++++++++----
3 files changed, 80 insertions(+), 11 deletions(-)
diff --git a/offload/libomptarget/OpenMP/Mapping.cpp b/offload/libomptarget/OpenMP/Mapping.cpp
index 064df0a0c7722..b2b04dd8716b9 100644
--- a/offload/libomptarget/OpenMP/Mapping.cpp
+++ b/offload/libomptarget/OpenMP/Mapping.cpp
@@ -263,7 +263,18 @@ TargetPointerResultTy MappingInfoTy::getTargetPointer(
"exist for host address " DPxMOD " (%" PRId64 " bytes)",
DPxPTR(HstPtrBegin), Size);
} else if ((PM->getRequirements() & OMP_REQ_UNIFIED_SHARED_MEMORY &&
- (!HasCloseModifier || LR.TPR.getEntry() != nullptr)) ||
+ (!HasCloseModifier ||
+ // A close mapping should not incur a new
+ // allocation under USM when it is already
+ // "present" on the device. That can either
+ // be due to an overlap with a previously
+ // encountered map (with/without the close
+ // modifier), or it being declare_target
+ // (infinite ref-count).
+ (LR.TPR.getEntry() != nullptr &&
+ (LR.Flags.IsContained || LR.Flags.ExtendsBefore ||
+ LR.Flags.ExtendsAfter ||
+ LR.TPR.getEntry()->isDynRefCountInf())))) ||
(PM->getRequirements() & OMPX_REQ_AUTO_ZERO_COPY)) {
// If unified shared memory is active, implicitly mapped variables that are
@@ -286,10 +297,15 @@ TargetPointerResultTy MappingInfoTy::getTargetPointer(
// subsection of previously mapped allocation. The mapping would prevent
// map(close, alloc:...) from creating a new allocation as it would reuse
// the mapped allocation instead.
- HDTTMap->emplace(new HostDataToTargetTy(
- (uintptr_t)HstPtrBase, (uintptr_t)HstPtrBegin,
- (uintptr_t)HstPtrBegin + Size, (uintptr_t)HstPtrBegin,
- (uintptr_t)HstPtrBegin, true, HstPtrName));
+ LR.TPR.setEntry(
+ HDTTMap
+ ->emplace(new HostDataToTargetTy(
+ (uintptr_t)HstPtrBase, (uintptr_t)HstPtrBegin,
+ (uintptr_t)HstPtrBegin + Size, (uintptr_t)HstPtrBegin,
+ (uintptr_t)HstPtrBegin, HasHoldModifier, HstPtrName))
+ .first->HDTT);
+ if (Device.notifyDataMapped(HstPtrBegin, Size))
+ return TargetPointerResultTy{};
}
} else if (HasPresentModifier) {
ODBG(ODT_Mapping) << "Mapping required by 'present' map type modifier does "
@@ -446,6 +462,14 @@ TargetPointerResultTy MappingInfoTy::getTgtPtrBegin(
"expected correct IsLast prediction for reset");
}
+ if (PM->getRequirements() & OMP_REQ_UNIFIED_SHARED_MEMORY ||
+ PM->getRequirements() & OMPX_REQ_AUTO_ZERO_COPY) {
+ LR.TPR.Flags.IsHostPointer = true;
+ if (!LR.TPR.getEntry()->getTotalRefCount()) {
+ LR.TPR.Flags.IsPresent = false;
+ }
+ }
+
// Increment the number of threads that is using the entry on a
// targetDataEnd, tracking the number of possible "deleters". A thread may
// come to own the entry deletion even if it was not the last one querying
diff --git a/offload/libomptarget/PluginManager.cpp b/offload/libomptarget/PluginManager.cpp
index 41b653a60adfd..d6a27537aedf9 100644
--- a/offload/libomptarget/PluginManager.cpp
+++ b/offload/libomptarget/PluginManager.cpp
@@ -516,6 +516,28 @@ static int loadImagesOntoDevice(DeviceTy &Device) {
CurrDeviceEntryAddr = DevPtr;
}
+ if (PM->getRequirements() & OMP_REQ_UNIFIED_SHARED_MEMORY ||
+ PM->getRequirements() & OMPX_REQ_AUTO_ZERO_COPY) {
+ AsyncInfoTy AsyncInfo(Device);
+ void *DevPtr;
+ Device.retrieveData(&DevPtr, CurrDeviceEntryAddr, sizeof(void *),
+ AsyncInfo, /*Entry=*/nullptr, &HDTTMap);
+ if (AsyncInfo.synchronize() != OFFLOAD_SUCCESS)
+ return OFFLOAD_FAIL;
+
+ ODBG(ODT_Mapping)
+ << "Add mapping from host " << DevPtr << " to device " << DevPtr
+ << " with size " << CurrDeviceEntry->Size;
+
+ HDTTMap->emplace(new HostDataToTargetTy(
+ (uintptr_t)DevPtr /*HstPtrBase*/,
+ (uintptr_t)DevPtr /*HstPtrBegin*/,
+ (uintptr_t)DevPtr + CurrHostEntry->Size /*HstPtrEnd*/,
+ (uintptr_t)DevPtr /*TgtAllocBegin*/,
+ (uintptr_t)DevPtr /*TgtPtrBegin*/, false /*UseHoldRefCount*/,
+ nullptr, true /*IsRefCountINF*/));
+ }
+
ODBG(ODT_Mapping) << "Add mapping from host " << CurrHostEntry->Address
<< " to device " << CurrDeviceEntry->Address
<< " with size " << CurrDeviceEntry->Size
diff --git a/offload/test/unified_shared_memory/check_tracking.c b/offload/test/unified_shared_memory/check_tracking.c
index 7607e9df90f87..808c6befac52b 100644
--- a/offload/test/unified_shared_memory/check_tracking.c
+++ b/offload/test/unified_shared_memory/check_tracking.c
@@ -14,17 +14,40 @@
#include <omp.h>
#include <stdio.h>
+#pragma omp begin declare target
+int x = 111;
+#pragma omp end declare target
+int y = 111;
+
+int present(void *p) {
+ return omp_target_is_present(p, omp_get_default_device());
+}
+
int main() {
- int x = 111;
+ int xl = 111;
// CHECK: present when unmapped: 0
- printf("present when unmapped: %d\n",
- omp_target_is_present(&x, omp_get_default_device()));
+ printf("present when unmapped: %d\n", present(&xl));
-#pragma omp target_enter_data map(alloc : x)
+#pragma omp target_enter_data map(alloc : xl)
// CHECK: present after mapping: 1
- printf("present after mapping: %d\n",
- omp_target_is_present(&x, omp_get_default_device()));
+ printf("present after mapping: %d\n", present(&xl));
+#pragma omp target_exit_data map(from : xl)
+ // CHECK: present after mapping: 0
+ printf("present after mapping: %d\n", present(&xl));
+
+ // CHECK: present when unmapped: 1 0
+ printf("present when unmapped: %d %d\n", present(&x), present(&y));
+
+#pragma omp target_enter_data map(to : x, y)
+
+ // CHECK: present after mapping: 1 1
+ printf("present after mapping: %d %d\n", present(&x), present(&y));
+
+#pragma omp target_exit_data map(from : x, y)
+
+ // CHECK: present after mapping: 1 0
+ printf("present after mapping: %d %d\n", present(&x), present(&y));
return 0;
}
\ No newline at end of file
>From aab9395010b8ac75a92f6be7af6c635fb146c3aa Mon Sep 17 00:00:00 2001
From: "Fine, Gregory" <gregory.fine at intel.com>
Date: Mon, 27 Jul 2026 20:53:36 -0700
Subject: [PATCH 05/29] Add test
---
offload/test/unified_shared_memory/close.c | 35 ++++++++++++++++++++++
1 file changed, 35 insertions(+)
create mode 100644 offload/test/unified_shared_memory/close.c
diff --git a/offload/test/unified_shared_memory/close.c b/offload/test/unified_shared_memory/close.c
new file mode 100644
index 0000000000000..c813663bfbcec
--- /dev/null
+++ b/offload/test/unified_shared_memory/close.c
@@ -0,0 +1,35 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+// This is an implementation-specific test (it does not reflect official OpenMP
+// expectations). The test is to ensure that we do the optimization to
+// "allocate" a new device copy for maps with `close` modifier, but only when a
+// matching entry doesn't already exist on the device.
+
+#include <assert.h>
+
+int x, y;
+#pragma omp requires unified_shared_memory
+
+int main() {
+ int *xaddr_device, *yaddr_device;
+#pragma omp target data map(tofrom : x)
+#pragma omp target map(close, tofrom : x, y) \
+ map(from : xaddr_device, yaddr_device)
+ {
+ xaddr_device = &x;
+ yaddr_device = &y;
+ }
+
+ assert(xaddr_device == &x && "Mapped variable should not allocate on close.");
+ assert(yaddr_device != &y && "Unmapped variable should allocate on close.");
+}
>From 7a383c7cbc74e9b11f0500f9c201ca78bf7ac001 Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Thu, 30 Jul 2026 23:46:46 -0700
Subject: [PATCH 06/29] [OFFLOAD] Fix USM mapping tracking without disabling
device data motion
Reporting every mapping found under unified shared memory as a host pointer also
disables the work a close mapping depends on: the FROM copy-back, target update
and pointer attachment all skip their work for a host pointer. Device writes were
silently dropped, regressing close_modifier.c, close_member.c and associate_ptr.c.
Instead let the reuse entry recorded on the USM host path be reclaimed with the
region that created it, and skip only the device deallocation for it, since its
allocation maps to the host address. That also fixes close_enter_exit.c, where the
entry used to linger with a zero reference count and make a later
map(close, ...) of the same storage look already present.
Gate the close reuse on the containment flags alone. An infinite reference count
is not a usable signal: it is evaluated on whatever entry the lookup landed on, so
it depended on address layout and an unrelated sibling map could displace it.
Adds close_data_motion.c, covering FROM copy-back and target update for a close
mapping under USM, and a CHECK line to close.c, whose RUN line pipes into
FileCheck but which had none.
The declare-target parts are left out. Under USM the offload entry for such a
variable describes the device reference pointer, so the runtime has neither the
variable's address nor its extent and cannot register its storage correctly.
That needs a code-generation change, and the tests that depend on it go with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply at anthropic.com>
---
offload/libomptarget/OpenMP/Mapping.cpp | 37 ++++-----
offload/libomptarget/PluginManager.cpp | 22 ------
offload/libomptarget/omptarget.cpp | 10 ++-
.../unified_shared_memory/check_tracking.c | 35 ++------
offload/test/unified_shared_memory/close.c | 5 ++
.../unified_shared_memory/close_data_motion.c | 79 +++++++++++++++++++
.../close_sub_section_global.c | 33 --------
.../close_sub_section_global_oob.c | 33 --------
8 files changed, 118 insertions(+), 136 deletions(-)
create mode 100644 offload/test/unified_shared_memory/close_data_motion.c
delete mode 100644 offload/test/unified_shared_memory/close_sub_section_global.c
delete mode 100644 offload/test/unified_shared_memory/close_sub_section_global_oob.c
diff --git a/offload/libomptarget/OpenMP/Mapping.cpp b/offload/libomptarget/OpenMP/Mapping.cpp
index b2b04dd8716b9..295447387c607 100644
--- a/offload/libomptarget/OpenMP/Mapping.cpp
+++ b/offload/libomptarget/OpenMP/Mapping.cpp
@@ -264,17 +264,21 @@ TargetPointerResultTy MappingInfoTy::getTargetPointer(
DPxPTR(HstPtrBegin), Size);
} else if ((PM->getRequirements() & OMP_REQ_UNIFIED_SHARED_MEMORY &&
(!HasCloseModifier ||
- // A close mapping should not incur a new
- // allocation under USM when it is already
- // "present" on the device. That can either
- // be due to an overlap with a previously
- // encountered map (with/without the close
- // modifier), or it being declare_target
- // (infinite ref-count).
+ // A close mapping should not incur a new allocation under USM
+ // when the storage it refers to is already on the device, i.e.
+ // when it overlaps a previously encountered map, with or without
+ // the close modifier.
+ //
+ // Storage that is present for the whole program, such as a
+ // declare-target variable, should be covered by this as well but
+ // is not: under USM the entry registered for such a variable
+ // describes the device reference pointer rather than the
+ // variable, so a mapping of the variable does not overlap it.
+ // Communicating the variable's extent is a code-generation
+ // change.
(LR.TPR.getEntry() != nullptr &&
(LR.Flags.IsContained || LR.Flags.ExtendsBefore ||
- LR.Flags.ExtendsAfter ||
- LR.TPR.getEntry()->isDynRefCountInf())))) ||
+ LR.Flags.ExtendsAfter)))) ||
(PM->getRequirements() & OMPX_REQ_AUTO_ZERO_COPY)) {
// If unified shared memory is active, implicitly mapped variables that are
@@ -462,14 +466,6 @@ TargetPointerResultTy MappingInfoTy::getTgtPtrBegin(
"expected correct IsLast prediction for reset");
}
- if (PM->getRequirements() & OMP_REQ_UNIFIED_SHARED_MEMORY ||
- PM->getRequirements() & OMPX_REQ_AUTO_ZERO_COPY) {
- LR.TPR.Flags.IsHostPointer = true;
- if (!LR.TPR.getEntry()->getTotalRefCount()) {
- LR.TPR.Flags.IsPresent = false;
- }
- }
-
// Increment the number of threads that is using the entry on a
// targetDataEnd, tracking the number of possible "deleters". A thread may
// come to own the entry deletion even if it was not the last one querying
@@ -577,7 +573,12 @@ int MappingInfoTy::deallocTgtPtrAndEntry(HostDataToTargetTy *Entry,
return OFFLOAD_FAIL;
}
- int Ret = Device.deleteData((void *)Entry->TgtAllocBegin);
+ // The reuse entry recorded on the unified-shared-memory host path owns no
+ // device allocation: its allocation maps to the host address itself, so it
+ // must not be handed to deleteData().
+ int Ret = OFFLOAD_SUCCESS;
+ if (Entry->TgtAllocBegin != Entry->HstPtrBegin)
+ Ret = Device.deleteData((void *)Entry->TgtAllocBegin);
// Notify the plugin about the unmapped memory.
Ret |= Device.notifyDataUnmapped((void *)Entry->HstPtrBegin);
diff --git a/offload/libomptarget/PluginManager.cpp b/offload/libomptarget/PluginManager.cpp
index d6a27537aedf9..41b653a60adfd 100644
--- a/offload/libomptarget/PluginManager.cpp
+++ b/offload/libomptarget/PluginManager.cpp
@@ -516,28 +516,6 @@ static int loadImagesOntoDevice(DeviceTy &Device) {
CurrDeviceEntryAddr = DevPtr;
}
- if (PM->getRequirements() & OMP_REQ_UNIFIED_SHARED_MEMORY ||
- PM->getRequirements() & OMPX_REQ_AUTO_ZERO_COPY) {
- AsyncInfoTy AsyncInfo(Device);
- void *DevPtr;
- Device.retrieveData(&DevPtr, CurrDeviceEntryAddr, sizeof(void *),
- AsyncInfo, /*Entry=*/nullptr, &HDTTMap);
- if (AsyncInfo.synchronize() != OFFLOAD_SUCCESS)
- return OFFLOAD_FAIL;
-
- ODBG(ODT_Mapping)
- << "Add mapping from host " << DevPtr << " to device " << DevPtr
- << " with size " << CurrDeviceEntry->Size;
-
- HDTTMap->emplace(new HostDataToTargetTy(
- (uintptr_t)DevPtr /*HstPtrBase*/,
- (uintptr_t)DevPtr /*HstPtrBegin*/,
- (uintptr_t)DevPtr + CurrHostEntry->Size /*HstPtrEnd*/,
- (uintptr_t)DevPtr /*TgtAllocBegin*/,
- (uintptr_t)DevPtr /*TgtPtrBegin*/, false /*UseHoldRefCount*/,
- nullptr, true /*IsRefCountINF*/));
- }
-
ODBG(ODT_Mapping) << "Add mapping from host " << CurrHostEntry->Address
<< " to device " << CurrDeviceEntry->Address
<< " with size " << CurrDeviceEntry->Size
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index 27ee173be06f9..7c7ec49603400 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -988,7 +988,15 @@ postProcessingTargetDataEnd(DeviceTy *Device,
int Ret = OFFLOAD_SUCCESS;
for (auto &[HstPtrBegin, DataSize, ArgType, TPR] : EntriesInfo) {
- bool DelEntry = !TPR.isHostPointer();
+ // The reuse entry recorded on the unified-shared-memory host path has no
+ // device allocation, but it does occupy a slot in the mapping table and has
+ // to be reclaimed with the region that created it. Otherwise it lingers with
+ // a zero reference count and a later map(close, ...) of the same storage
+ // finds it and concludes the data is already on the device.
+ const bool IsHostBackedEntry =
+ TPR.getEntry() != nullptr &&
+ TPR.getEntry()->TgtAllocBegin == TPR.getEntry()->HstPtrBegin;
+ bool DelEntry = !TPR.isHostPointer() || IsHostBackedEntry;
// If the last element from the mapper (for end transfer args comes in
// reverse order), do not remove the partial entry, the parent struct still
diff --git a/offload/test/unified_shared_memory/check_tracking.c b/offload/test/unified_shared_memory/check_tracking.c
index 808c6befac52b..7607e9df90f87 100644
--- a/offload/test/unified_shared_memory/check_tracking.c
+++ b/offload/test/unified_shared_memory/check_tracking.c
@@ -14,40 +14,17 @@
#include <omp.h>
#include <stdio.h>
-#pragma omp begin declare target
-int x = 111;
-#pragma omp end declare target
-int y = 111;
-
-int present(void *p) {
- return omp_target_is_present(p, omp_get_default_device());
-}
-
int main() {
- int xl = 111;
+ int x = 111;
// CHECK: present when unmapped: 0
- printf("present when unmapped: %d\n", present(&xl));
+ printf("present when unmapped: %d\n",
+ omp_target_is_present(&x, omp_get_default_device()));
-#pragma omp target_enter_data map(alloc : xl)
+#pragma omp target_enter_data map(alloc : x)
// CHECK: present after mapping: 1
- printf("present after mapping: %d\n", present(&xl));
-#pragma omp target_exit_data map(from : xl)
- // CHECK: present after mapping: 0
- printf("present after mapping: %d\n", present(&xl));
-
- // CHECK: present when unmapped: 1 0
- printf("present when unmapped: %d %d\n", present(&x), present(&y));
-
-#pragma omp target_enter_data map(to : x, y)
-
- // CHECK: present after mapping: 1 1
- printf("present after mapping: %d %d\n", present(&x), present(&y));
-
-#pragma omp target_exit_data map(from : x, y)
-
- // CHECK: present after mapping: 1 0
- printf("present after mapping: %d %d\n", present(&x), present(&y));
+ printf("present after mapping: %d\n",
+ omp_target_is_present(&x, omp_get_default_device()));
return 0;
}
\ No newline at end of file
diff --git a/offload/test/unified_shared_memory/close.c b/offload/test/unified_shared_memory/close.c
index c813663bfbcec..83374cf4c5e67 100644
--- a/offload/test/unified_shared_memory/close.c
+++ b/offload/test/unified_shared_memory/close.c
@@ -16,6 +16,7 @@
// matching entry doesn't already exist on the device.
#include <assert.h>
+#include <stdio.h>
int x, y;
#pragma omp requires unified_shared_memory
@@ -32,4 +33,8 @@ int main() {
assert(xaddr_device == &x && "Mapped variable should not allocate on close.");
assert(yaddr_device != &y && "Unmapped variable should allocate on close.");
+
+ // CHECK: Done!
+ printf("Done!\n");
+ return 0;
}
diff --git a/offload/test/unified_shared_memory/close_data_motion.c b/offload/test/unified_shared_memory/close_data_motion.c
new file mode 100644
index 0000000000000..d5a639d444092
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_data_motion.c
@@ -0,0 +1,79 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+// A close map under unified shared memory gets its own device allocation, so it
+// must retain normal device data-motion semantics: values written on the device
+// have to be copied back for a `from` map, and `target update` on such a
+// mapping must actually transfer data rather than being treated as a no-op.
+//
+// This is easy to break by treating every mapping under USM as a host pointer,
+// because the copy-back and the update path are both skipped for host pointers.
+// When that happens the device writes are silently lost.
+
+#include <stdio.h>
+
+#pragma omp requires unified_shared_memory
+
+#define N 64
+
+int main() {
+ int a[N], b[N];
+
+ for (int i = 0; i < N; ++i) {
+ a[i] = 1;
+ b[i] = 1;
+ }
+
+ // The device gets its own copy of "a" because of close. The writes below must
+ // make it back to the host at the end of the region.
+#pragma omp target map(close, tofrom : a[ : N])
+ {
+ for (int i = 0; i < N; ++i)
+ a[i] += 10;
+ }
+
+ int fails = 0;
+ for (int i = 0; i < N; ++i)
+ if (a[i] != 11)
+ fails++;
+ // CHECK: close tofrom copied back: Succeeded
+ printf("close tofrom copied back: %s\n",
+ (fails == 0) ? "Succeeded" : "Failed");
+
+ // Same, but the data motion is requested explicitly with target update.
+#pragma omp target data map(close, alloc : b[ : N])
+ {
+ // Push the current host values into the device copy.
+#pragma omp target update to(b[ : N])
+
+#pragma omp target map(present, alloc : b[ : N])
+ {
+ for (int i = 0; i < N; ++i)
+ b[i] += 20;
+ }
+
+ // Pull the device values back out. If update is a no-op the host keeps 1.
+#pragma omp target update from(b[ : N])
+ }
+
+ fails = 0;
+ for (int i = 0; i < N; ++i)
+ if (b[i] != 21)
+ fails++;
+ // CHECK: close target update from: Succeeded
+ printf("close target update from: %s\n",
+ (fails == 0) ? "Succeeded" : "Failed");
+
+ // CHECK: Done!
+ printf("Done!\n");
+ return 0;
+}
diff --git a/offload/test/unified_shared_memory/close_sub_section_global.c b/offload/test/unified_shared_memory/close_sub_section_global.c
deleted file mode 100644
index 7bf128d636fa8..0000000000000
--- a/offload/test/unified_shared_memory/close_sub_section_global.c
+++ /dev/null
@@ -1,33 +0,0 @@
-// RUN: %libomptarget-compile-run-and-check-generic
-
-// REQUIRES: unified_shared_memory
-// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
-
-// amdgpu runtime crash
-// Fails on nvptx with error: an illegal memory access was encountered
-// UNSUPPORTED: amdgcn-amd-amdhsa
-// UNSUPPORTED: nvptx64-nvidia-cuda
-// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
-// UNSUPPORTED: intelgpu
-
-#pragma omp requires unified_shared_memory
-
-#include <stdio.h>
-
-#pragma omp begin declare target
-double base[10] = {0};
-#pragma omp end declare target
-
-int main() {
-
-// close range covers base[1] and should properly
-// update the budder from the outer mapping.
-#pragma omp target map(close, alloc : base[1 : 9])
- {
- base[1] = 99.0;
- }
-#pragma omp target update from(base)
- // CHECK: base[1] = 99.000000 (expected 99.0)
- printf("base[1] = %f (expected 99.0)\n", base[1]);
- return 0;
-}
\ No newline at end of file
diff --git a/offload/test/unified_shared_memory/close_sub_section_global_oob.c b/offload/test/unified_shared_memory/close_sub_section_global_oob.c
deleted file mode 100644
index eaf5ff8c74490..0000000000000
--- a/offload/test/unified_shared_memory/close_sub_section_global_oob.c
+++ /dev/null
@@ -1,33 +0,0 @@
-// RUN: %libomptarget-compile-run-and-check-generic
-
-// REQUIRES: unified_shared_memory
-// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
-
-// amdgpu runtime crash
-// Fails on nvptx with error: an illegal memory access was encountered
-// UNSUPPORTED: amdgcn-amd-amdhsa
-// UNSUPPORTED: nvptx64-nvidia-cuda
-// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
-// UNSUPPORTED: intelgpu
-
-#pragma omp requires unified_shared_memory
-
-#include <stdio.h>
-
-#pragma omp begin declare target
-double base[10] = {0};
-#pragma omp end declare target
-
-int main() {
-
-// close on sub-section base[2:8]; access to base[1] is outside the close
-// buffer
-#pragma omp target map(close, alloc : base[2 : 8])
- {
- base[1] = 99.0;
- }
-#pragma omp target update from(base)
- // CHECK: base[1] = 99.000000 (expected 99.0)
- printf("base[1] = %f (expected 99.0)\n", base[1]);
- return 0;
-}
\ No newline at end of file
>From 0966951002d67cd28bce66c16db50c3765bee040 Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Thu, 30 Jul 2026 23:53:06 -0700
Subject: [PATCH 07/29] [OFFLOAD] Attach pointers that are newly mapped on the
USM host path
Pointer attachment is governed by a list item's mapping being new for the
construct (reference count 0 -> 1), not by device memory having been allocated for
it. Under unified shared memory a pointer mapped for the first time stays on the
host path, so it was neither recorded as newly mapped nor considered attachable,
and its deferred ATTACH was skipped.
Record such a mapping in StateInfo::NewMappings, kept apart from NewAllocations so
that the 'present' validation, which is about device allocation, is unaffected, and
let the attach lookup accept a pointer whose storage is shared with the host: that
is the storage the device dereferences, so attaching means writing the device
pointee address into it and restoring the original value at the end of the region
through the existing shadow-pointer mechanism. A host address is still rejected
for the pointee, where it would mean there is nothing to attach to.
This matters when a close mapping gives a pointee its own device allocation while
its pointer is mapped without close: the device copy of the pointer kept the host
pointee address, the kernel wrote host storage, and the close buffer was copied
back over it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply at anthropic.com>
---
offload/include/OpenMP/Mapping.h | 18 ++-
offload/libomptarget/OpenMP/Mapping.cpp | 17 +--
offload/libomptarget/omptarget.cpp | 20 ++-
.../close_ptr_ptee_nested.c | 117 ++++++++++++++++++
.../close_ptr_ptee_samedir.c | 98 +++++++++++++++
5 files changed, 256 insertions(+), 14 deletions(-)
create mode 100644 offload/test/unified_shared_memory/close_ptr_ptee_nested.c
create mode 100644 offload/test/unified_shared_memory/close_ptr_ptee_samedir.c
diff --git a/offload/include/OpenMP/Mapping.h b/offload/include/OpenMP/Mapping.h
index e4024abf26690..aeed707152f00 100644
--- a/offload/include/OpenMP/Mapping.h
+++ b/offload/include/OpenMP/Mapping.h
@@ -502,10 +502,18 @@ struct StateInfoTy {
/// ATTACH map entries for deferred processing until all other maps are done.
llvm::SmallVector<AttachMapInfo> AttachEntries;
- /// Host pointers for which new memory was allocated.
+ /// Host pointers for which new device memory was allocated.
/// Key: host pointer, Value: allocation size.
+ /// Consulted by the 'present' map-type validation.
llvm::DenseMap<void *, int64_t> NewAllocations;
+ /// Host pointers whose mapping was newly created in this construct
+ /// (reference count 0 -> 1) but for which no device memory was allocated,
+ /// i.e. the unified-shared-memory host path. Kept apart from NewAllocations,
+ /// which is about device allocation.
+ /// Key: host pointer, Value: mapped size.
+ llvm::DenseMap<void *, int64_t> NewMappings;
+
/// Host pointers that had a FROM entry, but for which a data transfer was
/// skipped due to the ref-count not being zero.
/// Key: host pointer, Value: data size.
@@ -554,6 +562,14 @@ struct StateInfoTy {
return findEntryForPtr(Ptr, NewAllocations);
}
+ /// Check if a pointer's mapping was newly created in this construct, whether or
+ /// not device memory was allocated for it. Used by pointer attachment.
+ std::optional<std::pair<void *, int64_t>> wasNewlyMapped(void *Ptr) const {
+ if (auto Alloc = findEntryForPtr(Ptr, NewAllocations))
+ return Alloc;
+ return findEntryForPtr(Ptr, NewMappings);
+ }
+
/// Check if a pointer range [Ptr, Ptr+Size) is fully contained within any
/// previously completed FROM transfer.
/// Returns the matching entry if found, otherwise std::nullopt.
diff --git a/offload/libomptarget/OpenMP/Mapping.cpp b/offload/libomptarget/OpenMP/Mapping.cpp
index 295447387c607..752d5510493f0 100644
--- a/offload/libomptarget/OpenMP/Mapping.cpp
+++ b/offload/libomptarget/OpenMP/Mapping.cpp
@@ -301,13 +301,16 @@ TargetPointerResultTy MappingInfoTy::getTargetPointer(
// subsection of previously mapped allocation. The mapping would prevent
// map(close, alloc:...) from creating a new allocation as it would reuse
// the mapped allocation instead.
- LR.TPR.setEntry(
- HDTTMap
- ->emplace(new HostDataToTargetTy(
- (uintptr_t)HstPtrBase, (uintptr_t)HstPtrBegin,
- (uintptr_t)HstPtrBegin + Size, (uintptr_t)HstPtrBegin,
- (uintptr_t)HstPtrBegin, HasHoldModifier, HstPtrName))
- .first->HDTT);
+ auto Emplaced = HDTTMap->emplace(new HostDataToTargetTy(
+ (uintptr_t)HstPtrBase, (uintptr_t)HstPtrBegin,
+ (uintptr_t)HstPtrBegin + Size, (uintptr_t)HstPtrBegin,
+ (uintptr_t)HstPtrBegin, HasHoldModifier, HstPtrName));
+ LR.TPR.setEntry(Emplaced.first->HDTT);
+
+ // The mapping is new for this construct, which is what pointer attachment
+ // is governed by, so record it even though no device memory was allocated.
+ if (Emplaced.second && StateInfo)
+ StateInfo->NewMappings[HstPtrBegin] = Size;
if (Device.notifyDataMapped(HstPtrBegin, Size))
return TargetPointerResultTy{};
}
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index 7c7ec49603400..ebd33384a8b0b 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -860,7 +860,7 @@ int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
// Lambda to check if a pointer was newly allocated
auto WasNewlyAllocated = [&](void *Ptr, const char *PtrName) {
- bool WasNewlyAllocated = StateInfo.wasNewlyAllocated(Ptr).has_value();
+ bool WasNewlyAllocated = StateInfo.wasNewlyMapped(Ptr).has_value();
ODBG(ODT_Mapping) << "Attach " << PtrName << " " << Ptr
<< " was newly allocated: "
<< (WasNewlyAllocated ? "yes" : "no");
@@ -878,9 +878,15 @@ int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
}
// Lambda to perform target pointer lookup and validation
+ // \p AllowHostPointer permits the lookup to succeed for storage shared with
+ // the host. That is correct for the pointer being attached: the device
+ // dereferences the same storage, so attaching writes the device pointee
+ // address into it and the original value is restored at the end of the region
+ // through the shadow-pointer mechanism. It is not correct for the pointee,
+ // where a host address would mean there is nothing to attach to.
auto LookupTargetPointer =
- [&](void *Ptr, int64_t Size,
- const char *PtrType) -> std::optional<TargetPointerResultTy> {
+ [&](void *Ptr, int64_t Size, const char *PtrType,
+ bool AllowHostPointer) -> std::optional<TargetPointerResultTy> {
// ATTACH map-type does not change ref-count, or do any allocation
// We just need to do a lookup for the pointer/pointee.
TargetPointerResultTy TPR = Device.getMappingInfo().getTgtPtrBegin(
@@ -896,7 +902,7 @@ int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
<< PtrType << " not present on device";
return std::nullopt;
}
- if (TPR.Flags.IsHostPointer) {
+ if (TPR.Flags.IsHostPointer && !AllowHostPointer) {
ODBG(ODT_Mapping) << "Skipping ATTACH entry " << EntryIdx
<< ": device version of the " << PtrType
<< " is a host pointer.";
@@ -909,7 +915,8 @@ int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
// Get device version of the pointee (e.g., &p[10]) first, as we can
// release its TPR after extracting the pointer value.
void *TgtPteeBegin = [&]() -> void * {
- if (auto PteeTPROpt = LookupTargetPointer(HstPteeBegin, 0, "pointee"))
+ if (auto PteeTPROpt = LookupTargetPointer(HstPteeBegin, 0, "pointee",
+ /*AllowHostPointer=*/false))
return PteeTPROpt->TargetPointer;
return nullptr;
}();
@@ -919,7 +926,8 @@ int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
// Get device version of the pointer (e.g., &p) next. We need to keep its
// TPR for use in shadow-pointer handling during pointer-attachment.
- auto PtrTPROpt = LookupTargetPointer(HstPtr, PtrSize, "pointer");
+ auto PtrTPROpt = LookupTargetPointer(HstPtr, PtrSize, "pointer",
+ /*AllowHostPointer=*/true);
if (!PtrTPROpt)
continue;
TargetPointerResultTy &PtrTPR = *PtrTPROpt;
diff --git a/offload/test/unified_shared_memory/close_ptr_ptee_nested.c b/offload/test/unified_shared_memory/close_ptr_ptee_nested.c
new file mode 100644
index 0000000000000..c135ef3a7033b
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_ptr_ptee_nested.c
@@ -0,0 +1,117 @@
+// Pointer + pointee mapping with the `close` map-type modifier under unified
+// shared memory, where the pointer `p` and its pointee `p[0:10]` are mapped on
+// SEPARATE, nested `target data` directives.
+//
+// Under USM the `close` modifier forces a real device allocation for the list
+// item even though the memory is shared. Whenever the pointer is (newly) mapped
+// alongside a device-resident pointee, it must be pointer-attached to that
+// device pointee, and its original host value must be restored at the end of
+// the region via shadow-pointer tracking -- otherwise the host pointer would be
+// left pointing at the device allocation, and (for a close pointee) the device
+// buffer would diverge from the host storage the kernel writes through the
+// unattached host pointer.
+//
+// Each variation is run twice: once under LIBOMPTARGET_DEBUG=1 to check the
+// pointer-attachment bookkeeping, and once to check the program results.
+//
+// RUN: %libomptarget-compile-generic -DV1=1
+// RUN: env LIBOMPTARGET_DEBUG=1 %libomptarget-run-generic 2>&1 \
+// RUN: | %fcheck-generic -check-prefixes=ATTACHED
+// RUN: %libomptarget-run-generic 2>&1 \
+// RUN: | %fcheck-generic -check-prefixes=V1,ALL
+//
+// RUN: %libomptarget-compile-generic -DV2=1
+// RUN: env LIBOMPTARGET_DEBUG=1 %libomptarget-run-generic 2>&1 \
+// RUN: | %fcheck-generic -check-prefixes=ATTACHED
+// RUN: %libomptarget-run-generic 2>&1 \
+// RUN: | %fcheck-generic -check-prefixes=V2,ALL
+//
+// RUN: %libomptarget-compile-generic
+// RUN: env LIBOMPTARGET_DEBUG=1 %libomptarget-run-generic 2>&1 \
+// RUN: | %fcheck-generic -check-prefixes=ATTACHED
+// RUN: %libomptarget-run-generic 2>&1 \
+// RUN: | %fcheck-generic -check-prefixes=V3,ALL
+//
+// REQUIRES: unified_shared_memory
+// REQUIRES: libomptarget-debug
+//
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+#include <omp.h>
+#include <stdio.h>
+
+#pragma omp requires unified_shared_memory
+
+int x[10] = {0};
+int *p = &x[0];
+int *p_device = NULL;
+int **paddr_device = NULL;
+
+int main() {
+ printf("Before tgt: p %s p_host\n", p == &x[0] ? "==" : "!=");
+
+#if V1
+#pragma omp target data map(tofrom : p)
+#elif V2
+#pragma omp target data map(close, tofrom : p)
+#else // V3
+#pragma omp target data map(close, tofrom : p[0 : 10])
+#endif
+ {
+#if V1
+#pragma omp target data map(close, tofrom : p[0 : 10])
+#elif V2
+#pragma omp target data map(tofrom : p[0 : 10])
+#else // V3
+#pragma omp target data map(tofrom : p) map(tofrom : p[0 : 10])
+#endif
+ {
+#pragma omp target map(present, alloc : p) map(from : p_device, paddr_device)
+ {
+ p_device = p;
+ paddr_device = &p;
+ p[0] = 111;
+ }
+ }
+ }
+
+ printf("In tgt: p_device %s p_host\n", p_device == &x[0] ? "==" : "!=");
+ printf("In tgt: paddr_device %s &p_host\n", paddr_device == &p ? "==" : "!=");
+ printf("After tgt: p %s p_host\n", p == &x[0] ? "==" : "!=");
+ printf("p[0] = %d\n", x[0]);
+
+ // When the pointee has a device allocation of its own, the pointer is attached
+ // to it and its original host value is restored at the end of the region.
+ // ATTACHED: ATTACH entry {{.*}} processed successfully
+ // ATTACHED: Restoring host pointer
+
+
+ // ALL: Before tgt: p == p_host
+
+ // The pointer is attached to the close-allocated device pointee, so its
+ // device value differs from the host address.
+ // V1: In tgt: p_device != p_host
+ // V1: In tgt: paddr_device == &p_host
+
+ // p itself is close-allocated, giving the pointer variable its own device
+ // storage (so &p differs on the device); the pointee stays on the USM host
+ // path, so the device pointer value equals the host address.
+ // V2: In tgt: p_device == p_host
+ // V2: In tgt: paddr_device != &p_host
+
+ // The close pointee (mapped by the enclosing region) has a device
+ // allocation, and p -- newly mapped on the USM host path by the inner region
+ // -- is attached to it, so its device value differs from the host address
+ // while the pointer variable itself stays on the host path (&p matches).
+ // V3: In tgt: p_device != p_host
+ // V3: In tgt: paddr_device == &p_host
+
+ // The host pointer must be intact afterwards and the kernel's write must be
+ // visible on the host.
+ // ALL: After tgt: p == p_host
+ // ALL: p[0] = 111
+}
diff --git a/offload/test/unified_shared_memory/close_ptr_ptee_samedir.c b/offload/test/unified_shared_memory/close_ptr_ptee_samedir.c
new file mode 100644
index 0000000000000..ae8cb7985881a
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_ptr_ptee_samedir.c
@@ -0,0 +1,98 @@
+// Pointer + pointee mapping with the `close` map-type modifier under unified
+// shared memory, where the pointer `p` and its pointee `p[0:10]` are mapped on
+// the SAME `target data` directive.
+//
+// Companion to close_ptr_ptee_nested.c; see that file for the rationale. Here
+// the pointer and pointee are processed together in a single targetDataBegin,
+// so pointer-attachment sees both the pointer and the (close-allocated) pointee
+// in the same construct.
+//
+// Each variation is run twice: once under LIBOMPTARGET_DEBUG=1 to check the
+// pointer-attachment bookkeeping, and once to check the program results.
+//
+// RUN: %libomptarget-compile-generic -DV1=1
+// RUN: env LIBOMPTARGET_DEBUG=1 %libomptarget-run-generic 2>&1 \
+// RUN: | %fcheck-generic -check-prefixes=ATTACHED
+// RUN: %libomptarget-run-generic 2>&1 \
+// RUN: | %fcheck-generic -check-prefixes=V1,ALL
+//
+// RUN: %libomptarget-compile-generic -DV2=1
+// RUN: env LIBOMPTARGET_DEBUG=1 %libomptarget-run-generic 2>&1 \
+// RUN: | %fcheck-generic -check-prefixes=ATTACHED
+// RUN: %libomptarget-run-generic 2>&1 \
+// RUN: | %fcheck-generic -check-prefixes=V2,ALL
+//
+// RUN: %libomptarget-compile-generic
+// RUN: env LIBOMPTARGET_DEBUG=1 %libomptarget-run-generic 2>&1 \
+// RUN: | %fcheck-generic -check-prefixes=ATTACHED
+// RUN: %libomptarget-run-generic 2>&1 \
+// RUN: | %fcheck-generic -check-prefixes=V3,ALL
+//
+// REQUIRES: unified_shared_memory
+// REQUIRES: libomptarget-debug
+//
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+#include <omp.h>
+#include <stdio.h>
+
+#pragma omp requires unified_shared_memory
+
+int x[10] = {0};
+int *p = &x[0];
+int *p_device = NULL;
+int **paddr_device = NULL;
+
+int main() {
+ printf("Before tgt: p %s p_host\n", p == &x[0] ? "==" : "!=");
+
+#if V1
+#pragma omp target data map(tofrom : p) map(close, tofrom : p[0 : 10])
+#elif V2
+#pragma omp target data map(close, tofrom : p) map(tofrom : p[0 : 10])
+#else // V3
+#pragma omp target data map(close, tofrom : p) map(close, tofrom : p[0 : 10])
+#endif
+ {
+#pragma omp target map(present, alloc : p) map(from : p_device, paddr_device)
+ {
+ p_device = p;
+ paddr_device = &p;
+ p[0] = 111;
+ }
+ }
+
+ printf("In tgt: p_device %s p_host\n", p_device == &x[0] ? "==" : "!=");
+ printf("In tgt: paddr_device %s &p_host\n", paddr_device == &p ? "==" : "!=");
+ printf("After tgt: p %s p_host\n", p == &x[0] ? "==" : "!=");
+ printf("p[0] = %d\n", x[0]);
+
+ // When the pointee has a device allocation of its own, the pointer is attached
+ // to it and its original host value is restored at the end of the region.
+ // ATTACHED: ATTACH entry {{.*}} processed successfully
+ // ATTACHED: Restoring host pointer
+
+
+ // ALL: Before tgt: p == p_host
+
+ // The close pointee is device-allocated and p (host path) is attached to it.
+ // V1: In tgt: p_device != p_host
+ // V1: In tgt: paddr_device == &p_host
+
+ // p is close-allocated (own device storage, so &p differs); the pointee is a
+ // plain USM mapping, so the device pointer value equals the host address.
+ // V2: In tgt: p_device == p_host
+ // V2: In tgt: paddr_device != &p_host
+
+ // Both p and the pointee are close-allocated, so both the device address of p
+ // and its attached value differ from the host.
+ // V3: In tgt: p_device != p_host
+ // V3: In tgt: paddr_device != &p_host
+
+ // ALL: After tgt: p == p_host
+ // ALL: p[0] = 111
+}
>From a2adef1ecfac71c811e965f36f74113456bce4dc Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Thu, 30 Jul 2026 23:59:52 -0700
Subject: [PATCH 08/29] [OpenMP] Communicate declare-target variable size under
unified shared memory
Under unified shared memory a declare-target variable is represented on the device
by a reference pointer to the host storage, and the offload entry emitted for it
describes that pointer:
no USM: { Address = &arr, Size = sizeof(arr) }
with USM: { Address = &arr_decl_tgt_ref_ptr, Size = sizeof(void *) }
Address and Size have to keep describing the pointer, since that is what
get_global() looks up and data_submit() writes, so the size of the variable itself
is communicated in addition, in the entry's spare Data field. The variable's
address does not need to be sent: the host copy of the reference pointer holds it,
so the runtime obtains it with a plain dereference.
With that, the runtime registers the variable's storage with its real extent, and:
- omp_target_is_present() reports declare-target storage as present, for its
whole extent rather than its first sizeof(void *) bytes.
- A close mapping of such storage is recognized as overlapping it, so it reuses
the existing device buffer instead of getting its own. That fixes the lost
write when the mapping covers part of the object or is reached through a
pointer into it, and lets close_sub_section_global{,_oob}.c pass, the latter of
which used to abort.
Old and new components interoperate: the size only ever occupies a field that was
previously zero, and the runtime acts on it only when it is set. An old binary on a
new runtime behaves as it did before, and a new binary on an old runtime has the
size ignored.
Flang needs no separate change, reaching the same
OpenMPIRBuilder::registerTargetGlobalVariable() through OpenMPToLLVMIRTranslation;
the size is derived from the LLVM module rather than from anything
language-specific.
Co-Authored-By: Claude Opus 5 (1M context) <noreply at anthropic.com>
---
.../declare_target_local_usm_codegen.cpp | 2 +-
..._target_requires_unified_shared_memory.cpp | 4 +-
.../llvm/Frontend/OpenMP/OMPIRBuilder.h | 17 ++++-
llvm/lib/Frontend/OpenMP/OMPIRBuilder.cpp | 33 +++++++--
offload/include/OpenMP/Mapping.h | 4 +-
offload/libomptarget/OpenMP/Mapping.cpp | 3 +-
offload/libomptarget/PluginManager.cpp | 30 ++++++++
offload/libomptarget/omptarget.cpp | 12 +--
.../unified_shared_memory/check_tracking.c | 38 ++++++++--
.../close_alloc_declare_target_lifetime.c | 74 +++++++++++++++++++
.../close_ptr_ptee_nested.c | 9 +--
.../close_ptr_ptee_samedir.c | 9 +--
.../close_sub_section_global.c | 33 +++++++++
.../close_sub_section_global_oob.c | 33 +++++++++
.../declare_target_map_extent.c | 67 +++++++++++++++++
.../declare_target_tracking.c | 65 ++++++++++++++++
16 files changed, 397 insertions(+), 36 deletions(-)
create mode 100644 offload/test/unified_shared_memory/close_alloc_declare_target_lifetime.c
create mode 100644 offload/test/unified_shared_memory/close_sub_section_global.c
create mode 100644 offload/test/unified_shared_memory/close_sub_section_global_oob.c
create mode 100644 offload/test/unified_shared_memory/declare_target_map_extent.c
create mode 100644 offload/test/unified_shared_memory/declare_target_tracking.c
diff --git a/clang/test/OpenMP/declare_target_local_usm_codegen.cpp b/clang/test/OpenMP/declare_target_local_usm_codegen.cpp
index d97d6f409d265..2b199f1d4aaa3 100644
--- a/clang/test/OpenMP/declare_target_local_usm_codegen.cpp
+++ b/clang/test/OpenMP/declare_target_local_usm_codegen.cpp
@@ -31,7 +31,7 @@ int enter_var;
// enter_var with USM: pointer-reference indirection
// HOST-DAG: @enter_var_decl_tgt_ref_ptr = weak global ptr @enter_var
-// HOST-DAG: @.offloading.entry.enter_var_decl_tgt_ref_ptr = weak constant %struct.__tgt_offload_entry { i64 0, i16 1, i16 1, i32 0, ptr @enter_var_decl_tgt_ref_ptr, ptr @.offloading.entry_name{{.*}}, i64 8, i64 0, ptr null }, section "llvm_offload_entries"
+// HOST-DAG: @.offloading.entry.enter_var_decl_tgt_ref_ptr = weak constant %struct.__tgt_offload_entry { i64 0, i16 1, i16 1, i32 0, ptr @enter_var_decl_tgt_ref_ptr, ptr @.offloading.entry_name{{.*}}, i64 8, i64 4, ptr null }, section "llvm_offload_entries"
// Device: local_var is a direct global, enter_var uses ref ptr
// DEVICE-DAG: @local_var = protected addrspace(1) global i32 0
diff --git a/clang/test/OpenMP/nvptx_target_requires_unified_shared_memory.cpp b/clang/test/OpenMP/nvptx_target_requires_unified_shared_memory.cpp
index b39007a712994..e0e4c1f1ea66b 100644
--- a/clang/test/OpenMP/nvptx_target_requires_unified_shared_memory.cpp
+++ b/clang/test/OpenMP/nvptx_target_requires_unified_shared_memory.cpp
@@ -47,10 +47,10 @@ int bar(int n){
// CHECK-HOST: [[OFFLOAD_MAPTYPES:@.+]] = private unnamed_addr constant [3 x i64] [i64 800, i64 800, i64 288]
// CHECK-HOST: [[OMP_OFFLOAD_ENTRY_LINK_VAR_PTR_NAME:@.+]] = internal unnamed_addr constant [21 x i8]
-// CHECK-HOST: [[OMP_OFFLOAD_ENTRY_LINK_VAR_PTR:@.+]] = weak constant %struct.__tgt_offload_entry { i64 0, i16 1, i16 1, i32 1, ptr [[VAR_DECL_TGT_LINK_PTR]], ptr [[OMP_OFFLOAD_ENTRY_LINK_VAR_PTR_NAME]], i64 8, i64 0, ptr null }, section "llvm_offload_entries"
+// CHECK-HOST: [[OMP_OFFLOAD_ENTRY_LINK_VAR_PTR:@.+]] = weak constant %struct.__tgt_offload_entry { i64 0, i16 1, i16 1, i32 1, ptr [[VAR_DECL_TGT_LINK_PTR]], ptr [[OMP_OFFLOAD_ENTRY_LINK_VAR_PTR_NAME]], i64 8, i64 8, ptr null }, section "llvm_offload_entries"
// CHECK-HOST: [[OMP_OFFLOAD_ENTRY_TO_VAR_PTR_NAME:@.+]] = internal unnamed_addr constant [24 x i8]
-// CHECK-HOST: [[OMP_OFFLOAD_ENTRY_TO_VAR_PTR:@.+]] = weak constant %struct.__tgt_offload_entry { i64 0, i16 1, i16 1, i32 0, ptr [[VAR_DECL_TGT_TO_PTR]], ptr @.offloading.entry_name.1, i64 8, i64 0, ptr null }, section "llvm_offload_entries"
+// CHECK-HOST: [[OMP_OFFLOAD_ENTRY_TO_VAR_PTR:@.+]] = weak constant %struct.__tgt_offload_entry { i64 0, i16 1, i16 1, i32 0, ptr [[VAR_DECL_TGT_TO_PTR]], ptr @.offloading.entry_name.1, i64 8, i64 8, ptr null }, section "llvm_offload_entries"
// CHECK-HOST: [[N_CASTED:%.+]] = alloca i64
// CHECK-HOST: [[SUM_CASTED:%.+]] = alloca i64
diff --git a/llvm/include/llvm/Frontend/OpenMP/OMPIRBuilder.h b/llvm/include/llvm/Frontend/OpenMP/OMPIRBuilder.h
index 965ac358c259e..2277e28e5cd1f 100644
--- a/llvm/include/llvm/Frontend/OpenMP/OMPIRBuilder.h
+++ b/llvm/include/llvm/Frontend/OpenMP/OMPIRBuilder.h
@@ -423,6 +423,9 @@ class OffloadEntriesInfoManager {
class OffloadEntryInfoDeviceGlobalVar final : public OffloadEntryInfo {
/// Type of the global variable.
int64_t VarSize;
+ /// Size of the variable a device reference pointer refers to, or 0 when the
+ /// entry describes the variable itself. See createOffloadEntry().
+ int64_t PointeeSize = 0;
GlobalValue::LinkageTypes Linkage;
const std::string VarName;
@@ -445,6 +448,8 @@ class OffloadEntriesInfoManager {
int64_t getVarSize() const { return VarSize; }
StringRef getVarName() const { return VarName; }
void setVarSize(int64_t Size) { VarSize = Size; }
+ int64_t getPointeeSize() const { return PointeeSize; }
+ void setPointeeSize(int64_t Size) { PointeeSize = Size; }
GlobalValue::LinkageTypes getLinkage() const { return Linkage; }
void setLinkage(GlobalValue::LinkageTypes LT) { Linkage = LT; }
static bool classof(const OffloadEntryInfo *Info) {
@@ -460,7 +465,8 @@ class OffloadEntriesInfoManager {
/// Register device global variable entry.
LLVM_ABI void registerDeviceGlobalVarEntryInfo(
StringRef VarName, Constant *Addr, int64_t VarSize,
- OMPTargetGlobalVarEntryKind Flags, GlobalValue::LinkageTypes Linkage);
+ OMPTargetGlobalVarEntryKind Flags, GlobalValue::LinkageTypes Linkage,
+ int64_t PointeeSize = 0);
/// Checks if the variable with the given name has been registered already.
bool hasDeviceGlobalVarEntryInfo(StringRef VarName) const {
return OffloadEntriesDeviceGlobalVar.count(VarName) > 0;
@@ -3014,9 +3020,16 @@ class OpenMPIRBuilder {
/// Creates offloading entry for the provided entry ID \a ID, address \a
/// Addr, size \a Size, and flags \a Flags.
+ ///
+ /// \a PointeeSize is nonzero only for a declare-target variable under unified
+ /// shared memory, where \a Addr is a device reference pointer to the host
+ /// variable and \a Size is therefore the size of that pointer. It carries the
+ /// size of the variable itself, which the runtime needs in order to register
+ /// the variable's storage rather than the pointer's.
LLVM_ABI void createOffloadEntry(Constant *ID, Constant *Addr, uint64_t Size,
int32_t Flags, GlobalValue::LinkageTypes,
- StringRef Name = "");
+ StringRef Name = "",
+ uint64_t PointeeSize = 0);
/// The kind of errors that can occur when emitting the offload entries and
/// metadata.
diff --git a/llvm/lib/Frontend/OpenMP/OMPIRBuilder.cpp b/llvm/lib/Frontend/OpenMP/OMPIRBuilder.cpp
index f8c1999fe1b89..f58e45779a8e4 100644
--- a/llvm/lib/Frontend/OpenMP/OMPIRBuilder.cpp
+++ b/llvm/lib/Frontend/OpenMP/OMPIRBuilder.cpp
@@ -11874,11 +11874,12 @@ std::unique_ptr<CodeExtractor> DeviceSharedMemOutlineInfo::createCodeExtractor(
void OpenMPIRBuilder::createOffloadEntry(Constant *ID, Constant *Addr,
uint64_t Size, int32_t Flags,
GlobalValue::LinkageTypes,
- StringRef Name) {
+ StringRef Name, uint64_t PointeeSize) {
if (!Config.isGPU()) {
llvm::offloading::emitOffloadingEntry(
M, object::OffloadKind::OFK_OpenMP, ID,
- Name.empty() ? Addr->getName() : Name, Size, Flags, /*Data=*/0);
+ Name.empty() ? Addr->getName() : Name, Size, Flags,
+ /*Data=*/PointeeSize);
return;
}
// TODO: Add support for global variables on the device after declare target
@@ -12051,7 +12052,8 @@ void OpenMPIRBuilder::createOffloadEntriesAndInfoMetadata(
Flags, CE->getLinkage(), CE->getVarName());
else
createOffloadEntry(CE->getAddress(), CE->getAddress(), CE->getVarSize(),
- Flags, CE->getLinkage());
+ Flags, CE->getLinkage(), /*Name=*/"",
+ CE->getPointeeSize());
} else {
llvm_unreachable("Unsupported entry kind.");
@@ -12224,6 +12226,7 @@ void OpenMPIRBuilder::registerTargetGlobalVariable(
OffloadEntriesInfoManager::OMPTargetGlobalVarEntryKind Flags;
StringRef VarName;
int64_t VarSize;
+ int64_t PointeeSize = 0;
GlobalValue::LinkageTypes Linkage;
if ((CaptureClause == OffloadEntriesInfoManager::OMPTargetGlobalVarEntryTo ||
@@ -12280,10 +12283,25 @@ void OpenMPIRBuilder::registerTargetGlobalVariable(
}
VarSize = M.getDataLayout().getPointerSize();
Linkage = GlobalValue::WeakAnyLinkage;
+
+ // Under unified shared memory Addr is a device reference pointer to the
+ // host variable, so VarSize above is the size of that pointer. Record the
+ // size of the variable itself as well: the runtime needs it to register the
+ // variable's storage, which is what a map of the variable refers to.
+ //
+ // This only applies to storage that lives on the host for the whole
+ // program. A "declare target link" variable without unified shared memory
+ // is also reached through a reference pointer, but its storage is on the
+ // device, so there is nothing for the runtime to register here.
+ if (Config.hasRequiresUnifiedSharedMemory() && !IsDeclaration &&
+ !Config.isTargetDevice())
+ if (GlobalValue *LlvmVal = M.getNamedValue(MangledName))
+ PointeeSize = divideCeil(
+ M.getDataLayout().getTypeSizeInBits(LlvmVal->getValueType()), 8);
}
- OffloadInfoManager.registerDeviceGlobalVarEntryInfo(VarName, Addr, VarSize,
- Flags, Linkage);
+ OffloadInfoManager.registerDeviceGlobalVarEntryInfo(
+ VarName, Addr, VarSize, Flags, Linkage, PointeeSize);
}
/// Loads all the offload entries information from the host IR
@@ -12728,7 +12746,8 @@ void OffloadEntriesInfoManager::initializeDeviceGlobalVarEntryInfo(
void OffloadEntriesInfoManager::registerDeviceGlobalVarEntryInfo(
StringRef VarName, Constant *Addr, int64_t VarSize,
- OMPTargetGlobalVarEntryKind Flags, GlobalValue::LinkageTypes Linkage) {
+ OMPTargetGlobalVarEntryKind Flags, GlobalValue::LinkageTypes Linkage,
+ int64_t PointeeSize) {
if (OMPBuilder->Config.isTargetDevice()) {
// This could happen if the device compilation is invoked standalone.
if (!hasDeviceGlobalVarEntryInfo(VarName))
@@ -12742,6 +12761,7 @@ void OffloadEntriesInfoManager::registerDeviceGlobalVarEntryInfo(
return;
}
Entry.setVarSize(VarSize);
+ Entry.setPointeeSize(PointeeSize);
Entry.setLinkage(Linkage);
Entry.setAddress(Addr);
} else {
@@ -12764,6 +12784,7 @@ void OffloadEntriesInfoManager::registerDeviceGlobalVarEntryInfo(
else
OffloadEntriesDeviceGlobalVar.try_emplace(
VarName, OffloadingEntriesNum, Addr, VarSize, Flags, Linkage, "");
+ OffloadEntriesDeviceGlobalVar[VarName].setPointeeSize(PointeeSize);
++OffloadingEntriesNum;
}
}
diff --git a/offload/include/OpenMP/Mapping.h b/offload/include/OpenMP/Mapping.h
index aeed707152f00..4f20cc5490143 100644
--- a/offload/include/OpenMP/Mapping.h
+++ b/offload/include/OpenMP/Mapping.h
@@ -562,8 +562,8 @@ struct StateInfoTy {
return findEntryForPtr(Ptr, NewAllocations);
}
- /// Check if a pointer's mapping was newly created in this construct, whether or
- /// not device memory was allocated for it. Used by pointer attachment.
+ /// Check if a pointer's mapping was newly created in this construct, whether
+ /// or not device memory was allocated for it. Used by pointer attachment.
std::optional<std::pair<void *, int64_t>> wasNewlyMapped(void *Ptr) const {
if (auto Alloc = findEntryForPtr(Ptr, NewAllocations))
return Alloc;
diff --git a/offload/libomptarget/OpenMP/Mapping.cpp b/offload/libomptarget/OpenMP/Mapping.cpp
index 752d5510493f0..dbf88118000fa 100644
--- a/offload/libomptarget/OpenMP/Mapping.cpp
+++ b/offload/libomptarget/OpenMP/Mapping.cpp
@@ -308,7 +308,8 @@ TargetPointerResultTy MappingInfoTy::getTargetPointer(
LR.TPR.setEntry(Emplaced.first->HDTT);
// The mapping is new for this construct, which is what pointer attachment
- // is governed by, so record it even though no device memory was allocated.
+ // is governed by, so record it even though no device memory was
+ // allocated.
if (Emplaced.second && StateInfo)
StateInfo->NewMappings[HstPtrBegin] = Size;
if (Device.notifyDataMapped(HstPtrBegin, Size))
diff --git a/offload/libomptarget/PluginManager.cpp b/offload/libomptarget/PluginManager.cpp
index 41b653a60adfd..3d3befe095a3f 100644
--- a/offload/libomptarget/PluginManager.cpp
+++ b/offload/libomptarget/PluginManager.cpp
@@ -516,6 +516,36 @@ static int loadImagesOntoDevice(DeviceTy &Device) {
CurrDeviceEntryAddr = DevPtr;
}
+ // Under unified shared memory a declare-target variable is represented
+ // on the device by a reference pointer to the host storage, so the
+ // entry below describes that pointer. Code generation records the size
+ // of the variable itself in Data; register its storage as well, so that
+ // it is reported as present and a mapping of it, or of part of it, is
+ // recognized as referring to storage already on the device.
+ //
+ // The host copy of the reference pointer holds the variable's address,
+ // so no device read is needed to find it.
+ if ((PM->getRequirements() & OMP_REQ_UNIFIED_SHARED_MEMORY ||
+ PM->getRequirements() & OMPX_REQ_AUTO_ZERO_COPY) &&
+ CurrHostEntry->Data) {
+ void *HostVar = *reinterpret_cast<void **>(CurrHostEntry->Address);
+ uint64_t VarSize = CurrHostEntry->Data;
+
+ ODBG(ODT_Mapping)
+ << "Add mapping from host " << HostVar << " to device " << HostVar
+ << " with size " << VarSize << ", name \""
+ << CurrDeviceEntry->SymbolName << "\" (declare-target storage)";
+
+ HDTTMap->emplace(new HostDataToTargetTy(
+ (uintptr_t)HostVar, (uintptr_t)HostVar,
+ (uintptr_t)HostVar + VarSize, (uintptr_t)HostVar,
+ (uintptr_t)HostVar, false /*UseHoldRefCount*/,
+ CurrHostEntry->SymbolName, true /*IsRefCountINF*/));
+
+ if (Device.notifyDataMapped(HostVar, VarSize))
+ return OFFLOAD_FAIL;
+ }
+
ODBG(ODT_Mapping) << "Add mapping from host " << CurrHostEntry->Address
<< " to device " << CurrDeviceEntry->Address
<< " with size " << CurrDeviceEntry->Size
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index ebd33384a8b0b..917b865da53f5 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -881,9 +881,9 @@ int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
// \p AllowHostPointer permits the lookup to succeed for storage shared with
// the host. That is correct for the pointer being attached: the device
// dereferences the same storage, so attaching writes the device pointee
- // address into it and the original value is restored at the end of the region
- // through the shadow-pointer mechanism. It is not correct for the pointee,
- // where a host address would mean there is nothing to attach to.
+ // address into it and the original value is restored at the end of the
+ // region through the shadow-pointer mechanism. It is not correct for the
+ // pointee, where a host address would mean there is nothing to attach to.
auto LookupTargetPointer =
[&](void *Ptr, int64_t Size, const char *PtrType,
bool AllowHostPointer) -> std::optional<TargetPointerResultTy> {
@@ -998,9 +998,9 @@ postProcessingTargetDataEnd(DeviceTy *Device,
for (auto &[HstPtrBegin, DataSize, ArgType, TPR] : EntriesInfo) {
// The reuse entry recorded on the unified-shared-memory host path has no
// device allocation, but it does occupy a slot in the mapping table and has
- // to be reclaimed with the region that created it. Otherwise it lingers with
- // a zero reference count and a later map(close, ...) of the same storage
- // finds it and concludes the data is already on the device.
+ // to be reclaimed with the region that created it. Otherwise it lingers
+ // with a zero reference count and a later map(close, ...) of the same
+ // storage finds it and concludes the data is already on the device.
const bool IsHostBackedEntry =
TPR.getEntry() != nullptr &&
TPR.getEntry()->TgtAllocBegin == TPR.getEntry()->HstPtrBegin;
diff --git a/offload/test/unified_shared_memory/check_tracking.c b/offload/test/unified_shared_memory/check_tracking.c
index 7607e9df90f87..8b9c1964578cb 100644
--- a/offload/test/unified_shared_memory/check_tracking.c
+++ b/offload/test/unified_shared_memory/check_tracking.c
@@ -14,17 +14,43 @@
#include <omp.h>
#include <stdio.h>
+#pragma omp begin declare target
+int x = 111;
+#pragma omp end declare target
+int y = 111;
+
+int present(void *p) {
+ return omp_target_is_present(p, omp_get_default_device());
+}
+
int main() {
- int x = 111;
+ int xl = 111;
// CHECK: present when unmapped: 0
- printf("present when unmapped: %d\n",
- omp_target_is_present(&x, omp_get_default_device()));
+ printf("present when unmapped: %d\n", present(&xl));
-#pragma omp target_enter_data map(alloc : x)
+#pragma omp target_enter_data map(alloc : xl)
// CHECK: present after mapping: 1
- printf("present after mapping: %d\n",
- omp_target_is_present(&x, omp_get_default_device()));
+ printf("present after mapping: %d\n", present(&xl));
+#pragma omp target_exit_data map(from : xl)
+ // CHECK: present after mapping: 0
+ printf("present after mapping: %d\n", present(&xl));
+
+ // x is declare-target, so it is in the device data environment from the
+ // start, whereas y is an ordinary global and is not mapped yet.
+ // CHECK: present when unmapped: 1 0
+ printf("present when unmapped: %d %d\n", present(&x), present(&y));
+
+#pragma omp target_enter_data map(to : x, y)
+
+ // CHECK: present after mapping: 1 1
+ printf("present after mapping: %d %d\n", present(&x), present(&y));
+
+#pragma omp target_exit_data map(from : x, y)
+
+ // x stays present after the exit, being declare-target; y does not.
+ // CHECK: present after mapping: 1 0
+ printf("present after mapping: %d %d\n", present(&x), present(&y));
return 0;
}
\ No newline at end of file
diff --git a/offload/test/unified_shared_memory/close_alloc_declare_target_lifetime.c b/offload/test/unified_shared_memory/close_alloc_declare_target_lifetime.c
new file mode 100644
index 0000000000000..80bd58d8acff0
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_alloc_declare_target_lifetime.c
@@ -0,0 +1,74 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+// A declare-target variable is in the device data environment for the whole
+// program, so its storage outlives any individual mapping of it, and a
+// `target update` on it after some unrelated region has ended is valid user
+// code.
+//
+// This test documents that the implementation-specific optimization of giving a
+// `close` mapping its own device allocation conflicts with that: with
+// map(close, alloc : ...) there is no copy-back and the separate allocation is
+// released at the end of the region, so a value the device wrote there is lost.
+//
+// The optimization applies only to storage that is not already on the device. A
+// mapping that lies within such storage stays on the host path and so shares
+// its device buffer, which is what the containment check in getTargetPointer()
+// expresses. For a declare-target variable that requires knowing the variable's
+// extent, which code generation communicates in its offload entry: under
+// unified shared memory the entry otherwise describes only the device reference
+// pointer.
+
+#include <stdio.h>
+
+#pragma omp requires unified_shared_memory
+
+#pragma omp begin declare target
+double part[10] = {0};
+double viaptr[10] = {0};
+#pragma omp end declare target
+
+// An unrelated variable, mapped alongside the close mapping below. Its entry is
+// what makes the lookup for the close mapping find a neighbor, so that the
+// mapping is not left on the host path.
+double *probe;
+
+int main() {
+ // (A) A close mapping of part of a declare-target object.
+ //
+ // CHECK: A: 11.000000
+#pragma omp target map(close, alloc : part[3 : 4]) map(from : probe)
+ {
+ probe = &part[3];
+ part[3] = 11.0;
+ }
+ // Legal: "part" is still in the device data environment.
+#pragma omp target update from(part[3 : 1])
+ printf("A: %f\n", part[3]);
+
+ // (B) The same, reached through a pointer into the middle of the object, so
+ // the mapping carries no reference to the declare-target variable at all.
+ //
+ // CHECK: B: 22.000000
+ double *p = &viaptr[6];
+#pragma omp target map(close, alloc : p[0 : 3]) map(from : probe)
+ {
+ probe = &p[0];
+ p[0] = 22.0;
+ }
+#pragma omp target update from(viaptr[6 : 1])
+ printf("B: %f\n", viaptr[6]);
+
+ // CHECK: Done!
+ printf("Done!\n");
+ return 0;
+}
diff --git a/offload/test/unified_shared_memory/close_ptr_ptee_nested.c b/offload/test/unified_shared_memory/close_ptr_ptee_nested.c
index c135ef3a7033b..62d363efa8ed8 100644
--- a/offload/test/unified_shared_memory/close_ptr_ptee_nested.c
+++ b/offload/test/unified_shared_memory/close_ptr_ptee_nested.c
@@ -84,11 +84,10 @@ int main() {
printf("After tgt: p %s p_host\n", p == &x[0] ? "==" : "!=");
printf("p[0] = %d\n", x[0]);
- // When the pointee has a device allocation of its own, the pointer is attached
- // to it and its original host value is restored at the end of the region.
- // ATTACHED: ATTACH entry {{.*}} processed successfully
- // ATTACHED: Restoring host pointer
-
+ // When the pointee has a device allocation of its own, the pointer is
+ // attached to it and its original host value is restored at the end of the
+ // region. ATTACHED: ATTACH entry {{.*}} processed successfully ATTACHED:
+ // Restoring host pointer
// ALL: Before tgt: p == p_host
diff --git a/offload/test/unified_shared_memory/close_ptr_ptee_samedir.c b/offload/test/unified_shared_memory/close_ptr_ptee_samedir.c
index ae8cb7985881a..78bdc326e1da3 100644
--- a/offload/test/unified_shared_memory/close_ptr_ptee_samedir.c
+++ b/offload/test/unified_shared_memory/close_ptr_ptee_samedir.c
@@ -71,11 +71,10 @@ int main() {
printf("After tgt: p %s p_host\n", p == &x[0] ? "==" : "!=");
printf("p[0] = %d\n", x[0]);
- // When the pointee has a device allocation of its own, the pointer is attached
- // to it and its original host value is restored at the end of the region.
- // ATTACHED: ATTACH entry {{.*}} processed successfully
- // ATTACHED: Restoring host pointer
-
+ // When the pointee has a device allocation of its own, the pointer is
+ // attached to it and its original host value is restored at the end of the
+ // region. ATTACHED: ATTACH entry {{.*}} processed successfully ATTACHED:
+ // Restoring host pointer
// ALL: Before tgt: p == p_host
diff --git a/offload/test/unified_shared_memory/close_sub_section_global.c b/offload/test/unified_shared_memory/close_sub_section_global.c
new file mode 100644
index 0000000000000..7bf128d636fa8
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_sub_section_global.c
@@ -0,0 +1,33 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+#pragma omp requires unified_shared_memory
+
+#include <stdio.h>
+
+#pragma omp begin declare target
+double base[10] = {0};
+#pragma omp end declare target
+
+int main() {
+
+// close range covers base[1] and should properly
+// update the budder from the outer mapping.
+#pragma omp target map(close, alloc : base[1 : 9])
+ {
+ base[1] = 99.0;
+ }
+#pragma omp target update from(base)
+ // CHECK: base[1] = 99.000000 (expected 99.0)
+ printf("base[1] = %f (expected 99.0)\n", base[1]);
+ return 0;
+}
\ No newline at end of file
diff --git a/offload/test/unified_shared_memory/close_sub_section_global_oob.c b/offload/test/unified_shared_memory/close_sub_section_global_oob.c
new file mode 100644
index 0000000000000..eaf5ff8c74490
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_sub_section_global_oob.c
@@ -0,0 +1,33 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+#pragma omp requires unified_shared_memory
+
+#include <stdio.h>
+
+#pragma omp begin declare target
+double base[10] = {0};
+#pragma omp end declare target
+
+int main() {
+
+// close on sub-section base[2:8]; access to base[1] is outside the close
+// buffer
+#pragma omp target map(close, alloc : base[2 : 8])
+ {
+ base[1] = 99.0;
+ }
+#pragma omp target update from(base)
+ // CHECK: base[1] = 99.000000 (expected 99.0)
+ printf("base[1] = %f (expected 99.0)\n", base[1]);
+ return 0;
+}
\ No newline at end of file
diff --git a/offload/test/unified_shared_memory/declare_target_map_extent.c b/offload/test/unified_shared_memory/declare_target_map_extent.c
new file mode 100644
index 0000000000000..e44532d70274b
--- /dev/null
+++ b/offload/test/unified_shared_memory/declare_target_map_extent.c
@@ -0,0 +1,67 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+// Mapping a declare-target aggregate, or a subsection of one that starts at its
+// beginning, must work under unified shared memory.
+//
+// If the runtime registers a mapping for the storage of a declare-target
+// variable, the extent it registers has to be the extent of the variable. Using
+// the size of the offload entry instead describes the device reference pointer,
+// i.e. sizeof(void *), and then a map of the real object looks like an attempt
+// to extend an existing, smaller mapping, which is rejected:
+//
+// explicit extension not allowed: host address specified is ... (80 bytes),
+// but device allocation maps to host at ... (8 bytes)
+//
+// That aborts the program rather than producing a wrong value, and it happens
+// for a plain map as well as for a close one.
+
+#include <stdio.h>
+
+#pragma omp requires unified_shared_memory
+
+#pragma omp begin declare target
+double base[10] = {0};
+#pragma omp end declare target
+
+int main() {
+ // A plain map of the whole declare-target array.
+#pragma omp target map(tofrom : base[0 : 10])
+ {
+ base[0] = 7.0;
+ }
+ // CHECK: plain whole-array map: base[0] = 7.000000
+ printf("plain whole-array map: base[0] = %f\n", base[0]);
+
+ // A close map of the whole array.
+#pragma omp target map(close, alloc : base[0 : 10])
+ {
+ base[3] = 5.0;
+ }
+#pragma omp target update from(base)
+ // CHECK: close whole-array map: base[3] = 5.000000
+ printf("close whole-array map: base[3] = %f\n", base[3]);
+
+ // A close map of a subsection that starts at the beginning of the array, so
+ // it overlaps whatever was registered for the variable itself.
+#pragma omp target map(close, alloc : base[0 : 9])
+ {
+ base[1] = 99.0;
+ }
+#pragma omp target update from(base)
+ // CHECK: close leading subsection: base[1] = 99.000000
+ printf("close leading subsection: base[1] = %f\n", base[1]);
+
+ // CHECK: Done!
+ printf("Done!\n");
+ return 0;
+}
diff --git a/offload/test/unified_shared_memory/declare_target_tracking.c b/offload/test/unified_shared_memory/declare_target_tracking.c
new file mode 100644
index 0000000000000..fc3d68b27a064
--- /dev/null
+++ b/offload/test/unified_shared_memory/declare_target_tracking.c
@@ -0,0 +1,65 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+// A `declare target` variable is in the device data environment for its whole
+// extent, so omp_target_is_present must report it as present for every byte of
+// it, not just for its first few bytes.
+//
+// Under unified shared memory such a variable is represented on the device by a
+// reference pointer to the host storage, and the offload entry for it describes
+// that *pointer*: it gives neither the variable's address nor its extent.
+//
+// Under unified shared memory such a variable is represented on the device by a
+// reference pointer to the host storage, and code generation communicates the
+// variable's own extent so the runtime can register its storage.
+
+#include <omp.h>
+#include <stdio.h>
+
+#pragma omp requires unified_shared_memory
+
+#pragma omp begin declare target
+int scalar = 111;
+int arr[64] = {0};
+#pragma omp end declare target
+
+static int present(void *P) {
+ return omp_target_is_present(P, omp_get_default_device());
+}
+
+int main() {
+ // Make sure the device image (and with it the declare-target registration)
+ // has been loaded before querying presence.
+#pragma omp target
+ {
+ }
+
+ // CHECK: scalar present: 1
+ printf("scalar present: %d\n", present(&scalar));
+
+ // Every element of a declare-target array is present, including the last one.
+ // CHECK: arr present first/mid/last: 1 1 1
+ printf("arr present first/mid/last: %d %d %d\n", present(&arr[0]),
+ present(&arr[32]), present(&arr[63]));
+
+ int fails = 0;
+ for (int i = 0; i < 64; ++i)
+ if (!present(&arr[i]))
+ fails++;
+ // CHECK: arr present for all elements: Succeeded
+ printf("arr present for all elements: %s\n",
+ (fails == 0) ? "Succeeded" : "Failed");
+
+ // CHECK: Done!
+ printf("Done!\n");
+ return 0;
+}
>From c5bf90f1b403a14713f616d9043653202e2505eb Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Tue, 4 Aug 2026 22:21:43 -0700
Subject: [PATCH 09/29] [OFFLOAD] Give a pointer device storage before
attaching it, under USM
Pointer attachment assigns the corresponding pointer. Under unified shared memory
the corresponding storage may be the original storage, and then the assignment is
observable through the original pointer: the host can no longer use it, and once
the pointee's storage is released it is left dangling. A close mapping of a
pointee makes this reachable, since the pointee gets a device buffer while its
pointer may still be on the host path.
Give the entry holding the pointer a device allocation of its own instead, so
that the pointer has a corresponding pointer distinct from the original. The
whole entry is given storage rather than just the pointer, because the device
address of anything inside it is defined as the entry's device address plus an
offset -- for a structure member, or for a pointer inside an untyped byte range,
there is nothing else it could be derived from. Its contents are copied so that
whatever else it holds is present on the device.
The entry keeps its identity, so its reference count continues to govern its
lifetime and the existing shadow pointer still protects the original pointer from
a later from-transfer of the surrounding storage.
Giving an entry storage moves it, so anything already attached to a pointee
within it designates its previous device address. Those pointers are attached
again, which may require giving a further entry storage, so this repeats until
nothing is left; it terminates because an entry can only be given an allocation
once. Finding them needs the pointee-to-pointer direction, which the shadow
pointers do not provide, so attachment now records it in
MappingInfoTy::AttachedPointers. Only pointers that are actually attached are
recorded, so this stays empty for mappings that never involve attachment.
The optimization of leaving mappings on the host path is preserved: storage is
given to an entry only when a pointer inside it is about to be attached.
Adds tests for a pointer that is a structure member, two pointers to one pointee
of which only one has device storage, a host read while a pointer is attached, a
structure mapped as an untyped byte range, and a chain where one pointer lives
inside another's pointee.
Co-Authored-By: Claude Opus 5 (1M context) <noreply at anthropic.com>
---
offload/include/OpenMP/Mapping.h | 95 +++++++++++++++-
offload/libomptarget/OpenMP/Mapping.cpp | 74 +++++++++++-
offload/libomptarget/omptarget.cpp | 105 +++++++++++++++---
.../close_attach_host_deref.c | 80 +++++++++++++
.../close_attach_pointer_chain.c | 91 +++++++++++++++
.../close_attach_struct_byte_view.c | 73 ++++++++++++
.../close_ptee_attached_host_read.c | 93 ++++++++++++++++
.../close_ptee_struct_member.c | 85 ++++++++++++++
.../close_ptee_two_pointers.c | 83 ++++++++++++++
.../close_ptr_ptee_nested.c | 14 ++-
.../close_ptr_ptee_samedir.c | 12 +-
11 files changed, 777 insertions(+), 28 deletions(-)
create mode 100644 offload/test/unified_shared_memory/close_attach_host_deref.c
create mode 100644 offload/test/unified_shared_memory/close_attach_pointer_chain.c
create mode 100644 offload/test/unified_shared_memory/close_attach_struct_byte_view.c
create mode 100644 offload/test/unified_shared_memory/close_ptee_attached_host_read.c
create mode 100644 offload/test/unified_shared_memory/close_ptee_struct_member.c
create mode 100644 offload/test/unified_shared_memory/close_ptee_two_pointers.c
diff --git a/offload/include/OpenMP/Mapping.h b/offload/include/OpenMP/Mapping.h
index 4f20cc5490143..af9e6f3acd3d2 100644
--- a/offload/include/OpenMP/Mapping.h
+++ b/offload/include/OpenMP/Mapping.h
@@ -117,8 +117,31 @@ struct HostDataToTargetTy {
const uintptr_t HstPtrEnd; // non-inclusive.
const map_var_info_t HstPtrName; // Optional source name of mapped variable.
- const uintptr_t TgtAllocBegin; // allocated target memory
- const uintptr_t TgtPtrBegin; // mapped target memory = TgtAllocBegin + padding
+ // Not const: an entry whose storage is the host storage can later be given a
+ // device allocation, see giveDeviceAllocation().
+ uintptr_t TgtAllocBegin; // allocated target memory
+ uintptr_t TgtPtrBegin; // mapped target memory = TgtAllocBegin + padding
+
+ /// Whether this entry's storage is the host storage itself, i.e. it owns no
+ /// device allocation. That is the case for the entries recorded on the
+ /// unified-shared-memory host path.
+ bool isHostBacked() const { return TgtPtrBegin == HstPtrBegin; }
+
+ /// Give a host-backed entry a device allocation, so that it stops sharing
+ /// storage with the original. Used when a pointer inside this entry's storage
+ /// is about to be attached: attachment assigns the corresponding pointer, and
+ /// while the corresponding storage is the original storage that assignment
+ /// would be observable through the original pointer.
+ ///
+ /// The entry keeps its identity, so its reference count continues to govern
+ /// its lifetime, and any shadow pointer recorded for it is transferred back
+ /// to the host by the same data motion as for any other entry.
+ void giveDeviceAllocation(uintptr_t NewTgtAllocBegin,
+ uintptr_t NewTgtPtrBegin) {
+ assert(isHostBacked() && "Entry already owns a device allocation");
+ TgtAllocBegin = NewTgtAllocBegin;
+ TgtPtrBegin = NewTgtPtrBegin;
+ }
private:
static const uint64_t INFRefCount = ~(uint64_t)0;
@@ -495,6 +518,25 @@ struct AttachMapInfo {
MapType(Type), Pointername(Name) {}
};
+/// A pointer that has been attached to a pointee, recorded so that it can be
+/// attached again if the pointee's entry is later given a device allocation and
+/// its device address therefore changes.
+///
+/// The pointee is the key of MappingInfoTy::AttachedPointers, so only what is
+/// needed to redo the attachment is kept here.
+struct AttachedPointerTy {
+ /// Original address of the pointer itself, e.g. &p or &s.p.
+ void **HstPtrAddr;
+ /// Original address of the pointee base, which may differ from the pointee
+ /// address that keys the index, e.g. for map(p[10:5]).
+ void *HstPteeBase;
+ /// Size of the pointer, which is larger than a pointer for a descriptor.
+ int64_t PtrSize;
+
+ AttachedPointerTy(void **HstPtrAddr, void *HstPteeBase, int64_t PtrSize)
+ : HstPtrAddr(HstPtrAddr), HstPteeBase(HstPteeBase), PtrSize(PtrSize) {}
+};
+
/// Structure to track new allocations, ATTACH entries, DELETE entries and
/// skipped FROM data transfer information for a given construct, across
/// recursive calls (for handling mappers) to targetDataBegin/targetDataEnd.
@@ -665,6 +707,23 @@ struct MappingInfoTy {
/// The type used to access the HDTT map.
using HDTTMapAccessorTy = decltype(HostDataToTargetMap)::AccessorTy;
+ /// Which pointers are attached to a given pointee, keyed by the pointee's
+ /// original address.
+ ///
+ /// Attachment records a shadow pointer on the entry holding the pointer,
+ /// which gives the pointer's pointee but not the reverse. This index provides
+ /// the reverse direction, which is needed when a pointee's entry is given a
+ /// device allocation after something has already been attached to it: the
+ /// attached pointers designate its previous device address and have to be
+ /// attached again. See giveEntryDeviceAllocation().
+ ///
+ /// Only pointers that are actually attached are recorded, so this stays empty
+ /// for mappings that never involve pointer attachment.
+ ///
+ /// Accessed under the HDTT map accessor.
+ llvm::DenseMap<void *, llvm::SmallVector<AttachedPointerTy, 2>>
+ AttachedPointers;
+
/// Lookup the mapping of \p HstPtrBegin in \p HDTTMap. The accessor ensures
/// exclusive access to the HDTT map.
LookupResult lookupMapping(HDTTMapAccessorTy &HDTTMap, void *HstPtrBegin,
@@ -719,6 +778,38 @@ struct MappingInfoTy {
[[nodiscard]] int eraseMapEntry(HDTTMapAccessorTy &HDTTMap,
HostDataToTargetTy *Entry, int64_t Size);
+ /// Record that \p HstPtrAddr is attached to the pointee at \p HstPteeBegin.
+ /// See AttachedPointers.
+ void recordAttachedPointer(void *HstPteeBegin, void **HstPtrAddr,
+ void *HstPteeBase, int64_t PtrSize);
+
+ /// Give \p Entry, which must be host-backed, a device allocation and copy the
+ /// current contents of its storage into it, so that it stops sharing storage
+ /// with the original.
+ ///
+ /// This is needed before attaching a pointer that lies within \p Entry's
+ /// storage: attachment assigns the corresponding pointer, and while the
+ /// corresponding storage is the original storage that assignment is
+ /// observable through the original pointer. The whole entry has to be given
+ /// storage rather than just the pointer, because the device address of
+ /// anything inside it is defined as this entry's device address plus an
+ /// offset.
+ ///
+ /// The contents are copied so that whatever else the storage holds -- other
+ /// structure members, for instance -- is present on the device.
+ ///
+ /// Anything already attached to a pointee inside \p Entry designated its
+ /// previous device address, so those pointers have to be attached again. They
+ /// are appended to \p ToReattach as (pointee, pointer) pairs rather than
+ /// attached here, since performing an attachment is the caller's job.
+ ///
+ /// \p HDTTMap must be held by the caller. Returns \c OFFLOAD_SUCCESS if the
+ /// entry now owns a device allocation, and \c OFFLOAD_FAIL otherwise.
+ [[nodiscard]] int giveEntryDeviceAllocation(
+ HDTTMapAccessorTy &HDTTMap, HostDataToTargetTy *Entry,
+ AsyncInfoTy &AsyncInfo,
+ llvm::SmallVectorImpl<std::pair<void *, AttachedPointerTy>> &ToReattach);
+
/// Deallocate the \p Entry from the device memory and delete it. Return \c
/// OFFLOAD_SUCCESS if the deallocation operations executed successfully, and
/// return \c OFFLOAD_FAIL otherwise.
diff --git a/offload/libomptarget/OpenMP/Mapping.cpp b/offload/libomptarget/OpenMP/Mapping.cpp
index dbf88118000fa..ed0751a1027eb 100644
--- a/offload/libomptarget/OpenMP/Mapping.cpp
+++ b/offload/libomptarget/OpenMP/Mapping.cpp
@@ -561,6 +561,73 @@ int MappingInfoTy::eraseMapEntry(HDTTMapAccessorTy &HDTTMap,
return OFFLOAD_SUCCESS;
}
+void MappingInfoTy::recordAttachedPointer(void *HstPteeBegin, void **HstPtrAddr,
+ void *HstPteeBase, int64_t PtrSize) {
+ auto &Attached = AttachedPointers[HstPteeBegin];
+ for (const AttachedPointerTy &A : Attached)
+ if (A.HstPtrAddr == HstPtrAddr)
+ return;
+ Attached.emplace_back(HstPtrAddr, HstPteeBase, PtrSize);
+}
+
+int MappingInfoTy::giveEntryDeviceAllocation(
+ HDTTMapAccessorTy &HDTTMap, HostDataToTargetTy *Entry,
+ AsyncInfoTy &AsyncInfo,
+ llvm::SmallVectorImpl<std::pair<void *, AttachedPointerTy>> &ToReattach) {
+ assert(Entry && "Trying to allocate for a null entry.");
+ assert(Entry->isHostBacked() && "Entry already owns a device allocation");
+
+ void *HstPtrBegin = reinterpret_cast<void *>(Entry->HstPtrBegin);
+ int64_t Size = Entry->HstPtrEnd - Entry->HstPtrBegin;
+
+ uintptr_t TgtAllocBegin =
+ reinterpret_cast<uintptr_t>(Device.allocData(Size, HstPtrBegin));
+ if (!TgtAllocBegin) {
+ REPORT() << "Failed to allocate device memory for " << HstPtrBegin << ".";
+ return OFFLOAD_FAIL;
+ }
+
+ Entry->giveDeviceAllocation(TgtAllocBegin, TgtAllocBegin);
+
+ INFO(OMP_INFOTYPE_MAPPING_CHANGED, Device.DeviceID,
+ "Allocating device memory for existing map entry with "
+ "HstPtrBegin=" DPxMOD ", TgtPtrBegin=" DPxMOD ", Size=%" PRId64 "\n",
+ DPxPTR(Entry->HstPtrBegin), DPxPTR(TgtAllocBegin), Size);
+ ODBG(ODT_Mapping) << "Allocating device memory for existing map entry (hst:"
+ << HstPtrBegin
+ << ") -> (tgt:" << reinterpret_cast<void *>(TgtAllocBegin)
+ << "), Size=" << Size;
+
+ // The storage was shared with the host until now, so bring its current
+ // contents over: it may hold data other than the pointer being attached.
+ int Ret = Device.submitData(reinterpret_cast<void *>(TgtAllocBegin),
+ HstPtrBegin, Size, AsyncInfo, Entry);
+ if (Ret != OFFLOAD_SUCCESS) {
+ REPORT() << "Copying data to device failed.";
+ return OFFLOAD_FAIL;
+ }
+
+ if (Device.notifyDataMapped(HstPtrBegin, Size))
+ return OFFLOAD_FAIL;
+
+ // The entry's device address has changed, so anything already attached to a
+ // pointee within it designates the previous one. Collect those pointers for
+ // the caller to attach again.
+ for (auto &[HstPteeBegin, Attached] : AttachedPointers) {
+ if (reinterpret_cast<uintptr_t>(HstPteeBegin) < Entry->HstPtrBegin ||
+ reinterpret_cast<uintptr_t>(HstPteeBegin) >= Entry->HstPtrEnd)
+ continue;
+ for (const AttachedPointerTy &A : Attached) {
+ ODBG(ODT_Mapping) << "Pointer " << A.HstPtrAddr
+ << " is attached to pointee " << HstPteeBegin
+ << " within the entry, so it needs attaching again";
+ ToReattach.emplace_back(HstPteeBegin, A);
+ }
+ }
+
+ return OFFLOAD_SUCCESS;
+}
+
int MappingInfoTy::deallocTgtPtrAndEntry(HostDataToTargetTy *Entry,
int64_t Size) {
assert(Entry && "Trying to deallocate a null entry.");
@@ -577,11 +644,10 @@ int MappingInfoTy::deallocTgtPtrAndEntry(HostDataToTargetTy *Entry,
return OFFLOAD_FAIL;
}
- // The reuse entry recorded on the unified-shared-memory host path owns no
- // device allocation: its allocation maps to the host address itself, so it
- // must not be handed to deleteData().
+ // A host-backed entry owns no device allocation: its allocation maps to the
+ // host address itself, so it must not be handed to deleteData().
int Ret = OFFLOAD_SUCCESS;
- if (Entry->TgtAllocBegin != Entry->HstPtrBegin)
+ if (!Entry->isHostBacked())
Ret = Device.deleteData((void *)Entry->TgtAllocBegin);
// Notify the plugin about the unmapped memory.
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index 917b865da53f5..88e12c96c8f77 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -453,6 +453,13 @@ static int performPointerAttachment(DeviceTy &Device, AsyncInfoTy &AsyncInfo,
void *TgtPteeBase =
calculateTargetPointeeBase(HstPteeBase, HstPteeBegin, TgtPteeBegin);
+ // Record the pointer against its pointee, so that it can be attached again if
+ // the pointee's entry is later given a device allocation. Recorded even when
+ // the shadow pointer below turns out to be a duplicate, since the pointee's
+ // address may still change afterwards.
+ Device.getMappingInfo().recordAttachedPointer(HstPteeBegin, HstPtrAddr,
+ HstPteeBase, HstPtrSize);
+
// Add shadow pointer tracking
if (!PtrTPR.getEntry()->addShadowPointer(
ShadowPtrInfoTy{HstPtrAddr, TgtPtrAddr, TgtPteeBase, HstPtrSize})) {
@@ -878,15 +885,9 @@ int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
}
// Lambda to perform target pointer lookup and validation
- // \p AllowHostPointer permits the lookup to succeed for storage shared with
- // the host. That is correct for the pointer being attached: the device
- // dereferences the same storage, so attaching writes the device pointee
- // address into it and the original value is restored at the end of the
- // region through the shadow-pointer mechanism. It is not correct for the
- // pointee, where a host address would mean there is nothing to attach to.
auto LookupTargetPointer =
- [&](void *Ptr, int64_t Size, const char *PtrType,
- bool AllowHostPointer) -> std::optional<TargetPointerResultTy> {
+ [&](void *Ptr, int64_t Size,
+ const char *PtrType) -> std::optional<TargetPointerResultTy> {
// ATTACH map-type does not change ref-count, or do any allocation
// We just need to do a lookup for the pointer/pointee.
TargetPointerResultTy TPR = Device.getMappingInfo().getTgtPtrBegin(
@@ -902,7 +903,7 @@ int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
<< PtrType << " not present on device";
return std::nullopt;
}
- if (TPR.Flags.IsHostPointer && !AllowHostPointer) {
+ if (TPR.Flags.IsHostPointer) {
ODBG(ODT_Mapping) << "Skipping ATTACH entry " << EntryIdx
<< ": device version of the " << PtrType
<< " is a host pointer.";
@@ -915,8 +916,7 @@ int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
// Get device version of the pointee (e.g., &p[10]) first, as we can
// release its TPR after extracting the pointer value.
void *TgtPteeBegin = [&]() -> void * {
- if (auto PteeTPROpt = LookupTargetPointer(HstPteeBegin, 0, "pointee",
- /*AllowHostPointer=*/false))
+ if (auto PteeTPROpt = LookupTargetPointer(HstPteeBegin, 0, "pointee"))
return PteeTPROpt->TargetPointer;
return nullptr;
}();
@@ -926,10 +926,86 @@ int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
// Get device version of the pointer (e.g., &p) next. We need to keep its
// TPR for use in shadow-pointer handling during pointer-attachment.
- auto PtrTPROpt = LookupTargetPointer(HstPtr, PtrSize, "pointer",
- /*AllowHostPointer=*/true);
+ auto PtrTPROpt = LookupTargetPointer(HstPtr, PtrSize, "pointer");
if (!PtrTPROpt)
continue;
+
+ // Attachment assigns the corresponding pointer. If the entry holding the
+ // pointer is host-backed then the corresponding storage *is* the original
+ // storage, so that assignment is observable through the original pointer:
+ // the host can no longer use it, and once the pointee's storage is released
+ // it is left dangling.
+ //
+ // Give that entry a device allocation of its own instead, so that the
+ // pointer has a corresponding pointer distinct from the original. The whole
+ // entry is given storage, since the device address of anything inside it is
+ // defined as the entry's device address plus an offset, and its contents
+ // are copied so that whatever else it holds is present on the device.
+ if (PtrTPROpt->getEntry() && PtrTPROpt->getEntry()->isHostBacked()) {
+ HostDataToTargetTy *Entry = PtrTPROpt->getEntry();
+ // Release the TPR's hold on the entry before taking the map accessor.
+ PtrTPROpt.reset();
+
+ // Giving an entry a device allocation moves it, so anything already
+ // attached to a pointee within it has to be attached again. Doing so may
+ // require giving a further entry an allocation, so keep going until
+ // nothing is left, which terminates because an entry can only be given an
+ // allocation once.
+ SmallVector<std::pair<void *, AttachedPointerTy>> ToReattach;
+ {
+ MappingInfoTy::HDTTMapAccessorTy HDTTMap =
+ Device.getMappingInfo().HostDataToTargetMap.getExclusiveAccessor();
+ if (Device.getMappingInfo().giveEntryDeviceAllocation(
+ HDTTMap, Entry, AsyncInfo, ToReattach) != OFFLOAD_SUCCESS)
+ return OFFLOAD_FAIL;
+ }
+
+ while (!ToReattach.empty()) {
+ auto [ReHstPteeBegin, ReAttached] = ToReattach.pop_back_val();
+
+ auto RePteeTPROpt =
+ LookupTargetPointer(ReHstPteeBegin, 0, "re-attach pointee");
+ if (!RePteeTPROpt)
+ continue;
+ void *ReTgtPteeBegin = RePteeTPROpt->TargetPointer;
+ RePteeTPROpt.reset();
+
+ auto RePtrTPROpt = LookupTargetPointer(
+ ReAttached.HstPtrAddr, ReAttached.PtrSize, "re-attach pointer");
+ if (!RePtrTPROpt)
+ continue;
+
+ if (RePtrTPROpt->getEntry() &&
+ RePtrTPROpt->getEntry()->isHostBacked()) {
+ HostDataToTargetTy *ReEntry = RePtrTPROpt->getEntry();
+ RePtrTPROpt.reset();
+ {
+ MappingInfoTy::HDTTMapAccessorTy HDTTMap =
+ Device.getMappingInfo()
+ .HostDataToTargetMap.getExclusiveAccessor();
+ if (Device.getMappingInfo().giveEntryDeviceAllocation(
+ HDTTMap, ReEntry, AsyncInfo, ToReattach) != OFFLOAD_SUCCESS)
+ return OFFLOAD_FAIL;
+ }
+ RePtrTPROpt = LookupTargetPointer(
+ ReAttached.HstPtrAddr, ReAttached.PtrSize, "re-attach pointer");
+ if (!RePtrTPROpt)
+ continue;
+ }
+
+ Ret = performPointerAttachment(
+ Device, AsyncInfo, ReAttached.HstPtrAddr, ReAttached.HstPteeBase,
+ ReHstPteeBegin,
+ reinterpret_cast<void **>(RePtrTPROpt->TargetPointer),
+ ReTgtPteeBegin, ReAttached.PtrSize, *RePtrTPROpt);
+ if (Ret != OFFLOAD_SUCCESS)
+ return OFFLOAD_FAIL;
+ }
+
+ PtrTPROpt = LookupTargetPointer(HstPtr, PtrSize, "pointer");
+ if (!PtrTPROpt)
+ continue;
+ }
TargetPointerResultTy &PtrTPR = *PtrTPROpt;
void **TgtPtrBase = reinterpret_cast<void **>(PtrTPR.TargetPointer);
@@ -1002,8 +1078,7 @@ postProcessingTargetDataEnd(DeviceTy *Device,
// with a zero reference count and a later map(close, ...) of the same
// storage finds it and concludes the data is already on the device.
const bool IsHostBackedEntry =
- TPR.getEntry() != nullptr &&
- TPR.getEntry()->TgtAllocBegin == TPR.getEntry()->HstPtrBegin;
+ TPR.getEntry() != nullptr && TPR.getEntry()->isHostBacked();
bool DelEntry = !TPR.isHostPointer() || IsHostBackedEntry;
// If the last element from the mapper (for end transfer args comes in
diff --git a/offload/test/unified_shared_memory/close_attach_host_deref.c b/offload/test/unified_shared_memory/close_attach_host_deref.c
new file mode 100644
index 0000000000000..cc05226625ea4
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_attach_host_deref.c
@@ -0,0 +1,80 @@
+// RUN: %libomptarget-compile-generic
+// RUN: %libomptarget-run-generic 2>&1 | %fcheck-generic
+//
+// RUN: %libomptarget-compile-generic -DVIA_ALWAYS=1
+// RUN: env LIBOMPTARGET_TREAT_ATTACH_AUTO_AS_ALWAYS=1 \
+// RUN: %libomptarget-run-generic 2>&1 | %fcheck-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+// Dereferencing a pointer on the host while its corresponding pointer is in the
+// attached state, under unified shared memory.
+//
+// The pointer is mapped without close, so its corresponding storage is the
+// original storage. The pointee is mapped with close, so it gets a device
+// buffer of its own, and pointer attachment then writes that device address
+// into the corresponding pointer -- which is the original pointer. A host
+// dereference afterwards goes through the device address.
+//
+// Two orderings are covered. In the default configuration the pointee is mapped
+// with close after the pointer, so the attachment is triggered by the pointee
+// becoming newly mapped. With VIA_ALWAYS the pointee already has device storage
+// and the attachment is triggered separately, by a map of the zero-length array
+// section under LIBOMPTARGET_TREAT_ATTACH_AUTO_AS_ALWAYS (OpenMP 6.0 has no
+// attach map-type-modifier for C/C++, so the environment variable stands in for
+// attach(always)).
+//
+// FIXME: the value checked below is the one produced today; the expected value
+// is given alongside it. Reading x[0] through p is the natural thing for a
+// program to do here, and it does not modify the pointer, so nothing in the
+// OpenMP 6.0 map clause restrictions forbids it.
+
+#include <stdio.h>
+
+#pragma omp requires unified_shared_memory
+
+int x[10];
+int *p = &x[0];
+
+int main() {
+ for (int i = 0; i < 10; ++i)
+ x[i] = 42;
+
+ // The unified_shared_memory requirement is registered when a device image is
+ // loaded, so the program needs a target region for it to take effect.
+#pragma omp target
+ {
+ }
+
+ // CHECK: before: p == &x[0]
+ printf("before: p %s &x[0]\n", p == &x[0] ? "==" : "!=");
+
+#if VIA_ALWAYS
+ // Pointer and pointee both get their storage first, then attachment is
+ // triggered on its own.
+#pragma omp target enter data map(alloc : p)
+#pragma omp target enter data map(close, alloc : x[0 : 10])
+#pragma omp target enter data map(alloc : p[0 : 0])
+#else
+ // The pointee is newly mapped with close, which triggers the attachment.
+#pragma omp target enter data map(alloc : p)
+#pragma omp target enter data map(close, alloc : p[0 : 10])
+#endif
+
+ // The pointer's entry was given a device allocation, so attachment wrote the
+ // device pointee address there and the original p is untouched.
+ // CHECK: after attach: p == &x[0]
+ printf("after attach: p %s &x[0]\n", p == &x[0] ? "==" : "!=");
+
+ // CHECK: Done!
+ printf("Done!\n");
+ return 0;
+}
diff --git a/offload/test/unified_shared_memory/close_attach_pointer_chain.c b/offload/test/unified_shared_memory/close_attach_pointer_chain.c
new file mode 100644
index 0000000000000..9678d8dba2233
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_attach_pointer_chain.c
@@ -0,0 +1,91 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+// A chain of two pointers, where the second one lives inside what the first one
+// points to: p1 refers to a structure whose member p2 refers to an array.
+//
+// p1 is attached to the structure first. Giving the structure device storage
+// afterwards -- because a close mapping of what p2 refers to needs p2 to have a
+// corresponding pointer distinct from the original -- moves the structure's
+// device address, so the value already attached to p1 no longer designates it.
+// Nothing revisits p1, so the device copy of p1 keeps pointing at the original
+// structure, and device code reaching p2 through p1 does not see the close
+// buffer.
+//
+// Any mechanism that gives an entry device storage after a pointer has been
+// attached to it therefore has to revisit the pointers already attached to that
+// entry, transitively.
+//
+// FIXME: the values checked below are the ones produced today; the expected
+// value is given alongside each.
+
+#include <stdio.h>
+
+#pragma omp requires unified_shared_memory
+
+struct Inner {
+ int *p2;
+ int pad;
+};
+
+int leaf[10];
+struct Inner inner;
+struct Inner *p1 = &inner;
+
+struct Inner *p1_device;
+int *p2_device;
+
+int main() {
+ for (int i = 0; i < 10; ++i)
+ leaf[i] = 42;
+ inner.p2 = &leaf[0];
+
+ // The unified_shared_memory requirement is registered when a device image is
+ // loaded, so the program needs a target region for it to take effect.
+#pragma omp target
+ {
+ }
+
+ // p1 is attached to the structure.
+#pragma omp target enter data map(alloc : p1)
+#pragma omp target enter data map(alloc : p1[0 : 1])
+
+ // The close mapping needs p2, which lives inside the structure, to have a
+ // corresponding pointer distinct from the original.
+#pragma omp target enter data map(close, alloc : p1->p2[0 : 10])
+
+#pragma omp target map(present, alloc : p1, p1[0 : 1]) \
+ map(from : p1_device, p2_device)
+ {
+ p1_device = p1;
+ p2_device = p1->p2;
+ }
+
+ // p1 was attached to the structure before it was given device storage, so it
+ // was attached again afterwards and designates the device copy.
+ // CHECK: device p1 != &inner
+ printf("device p1 %s &inner\n", p1_device == &inner ? "==" : "!=");
+
+ // So p2 is read from the device copy of the structure, where it designates
+ // the close buffer.
+ // CHECK: device p1->p2 != &leaf[0]
+ printf("device p1->p2 %s &leaf[0]\n", p2_device == &leaf[0] ? "==" : "!=");
+
+ // The original pointers are intact.
+ // CHECK: host: p1 == &inner, inner.p2 == &leaf[0]
+ printf("host: p1 %s &inner, inner.p2 %s &leaf[0]\n",
+ p1 == &inner ? "==" : "!=", inner.p2 == &leaf[0] ? "==" : "!=");
+
+ // CHECK: Done!
+ printf("Done!\n");
+ return 0;
+}
diff --git a/offload/test/unified_shared_memory/close_attach_struct_byte_view.c b/offload/test/unified_shared_memory/close_attach_struct_byte_view.c
new file mode 100644
index 0000000000000..907c4eb02c880
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_attach_struct_byte_view.c
@@ -0,0 +1,73 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+// A structure containing a pointer, mapped as an untyped byte range rather than
+// by its own name, and then a close mapping of what the member points to.
+//
+// The byte range is mapped without close, so its corresponding storage is the
+// original storage. Attachment for the member then writes the device pointee
+// address into the original member, since a member's device address is the
+// structure's device address plus the member offset.
+//
+// This is the same situation as close_ptee_struct_member.c, but the mapped
+// entry carries no type information: the runtime sees only a range of bytes
+// that happens to contain a pointer. Anything that gives the pointer storage of
+// its own would have to promote this whole range, which is the only storage the
+// member's device address can be derived from.
+//
+// FIXME: the value checked below is the one produced today; the expected value
+// is given alongside it.
+
+#include <stdio.h>
+
+#pragma omp requires unified_shared_memory
+
+int arr[10];
+
+struct S {
+ int x;
+ int y;
+ int *p;
+};
+
+struct S s;
+char *buf = (char *)&s;
+
+int main() {
+ for (int i = 0; i < 10; ++i)
+ arr[i] = 42;
+ s.p = &arr[0];
+
+ // The unified_shared_memory requirement is registered when a device image is
+ // loaded, so the program needs a target region for it to take effect.
+#pragma omp target
+ {
+ }
+
+ // CHECK: before: s.p == &arr[0]
+ printf("before: s.p %s &arr[0]\n", s.p == &arr[0] ? "==" : "!=");
+
+ // The structure is mapped as plain bytes, so it stays on the host path.
+#pragma omp target enter data map(alloc : buf[0 : sizeof(struct S)])
+
+ // The pointee is newly mapped with close, which triggers the attachment.
+#pragma omp target enter data map(close, alloc : s.p[0 : 10])
+
+ // The byte range was given a device allocation as a whole, so attachment
+ // wrote into that and the original s.p is untouched.
+ // CHECK: after attach: s.p == &arr[0]
+ printf("after attach: s.p %s &arr[0]\n", s.p == &arr[0] ? "==" : "!=");
+
+ // CHECK: Done!
+ printf("Done!\n");
+ return 0;
+}
diff --git a/offload/test/unified_shared_memory/close_ptee_attached_host_read.c b/offload/test/unified_shared_memory/close_ptee_attached_host_read.c
new file mode 100644
index 0000000000000..9394c646decd6
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_ptee_attached_host_read.c
@@ -0,0 +1,93 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+// The pointee already has device storage before the pointers are mapped, so no
+// allocation happens for it on the inner construct and there is nothing to
+// undo: the pointee is simply already on the device.
+//
+// p2 is mapped without close, so its corresponding storage is the original
+// storage and attachment writes the device pointee address into the original
+// p2. The host can then observe that value for as long as p2 remains attached,
+// which under OpenMP 6.0 lasts until the pointer's storage is removed from the
+// device data environment -- there is no detachment.
+//
+// This case is therefore not fixed by declining to allocate the pointee: it is
+// about attachment through shared storage, not about the close allocation.
+//
+// Note what OpenMP 6.0 does and does not say here. Attachment assigns the
+// corresponding pointer (7.9.6), and the corresponding storage may share
+// storage with the original (7.9.6, 1.3.2), in which case the assignment is
+// observable on the host. Nothing preserves the original value during that
+// window, and nothing restores it: the map-exiting sequence has no detach step.
+// The one place the specification confronts the same situation, for self maps,
+// requires runtime error termination when "the list item is a pointer that
+// would be assigned a different value as a result of pointer attachment"
+// (7.9.6), which suggests the intent is for this configuration not to arise
+// rather than for the host value to be preserved.
+//
+// FIXME: the values checked below are the ones produced today; the expected
+// value is given alongside each, on the reading that a program should be able
+// to use the original pointer while it happens to be attached.
+
+#include <stdio.h>
+
+#pragma omp requires unified_shared_memory
+
+int arr[10] = {0};
+
+int *p1 = &arr[0];
+int *p2 = &arr[0];
+
+int *p1_device, *p2_device;
+
+int main() {
+ // The pointee gets device storage before either pointer is mapped.
+#pragma omp target enter data map(close, alloc : arr[0 : 10])
+
+ // CHECK: before: p1 == &arr[0], p2 == &arr[0]
+ printf("before: p1 %s &arr[0], p2 %s &arr[0]\n",
+ p1 == &arr[0] ? "==" : "!=", p2 == &arr[0] ? "==" : "!=");
+
+#pragma omp target data map(close, alloc : p1, p1[0 : 0]) \
+ map(alloc : p2, p2[0 : 0])
+ {
+#pragma omp target map(present, alloc : p1, p2) map(from : p1_device, p2_device)
+ {
+ p1_device = p1;
+ p2_device = p2;
+ }
+
+ // Read on the host while both pointers are still attached.
+ //
+ // p1 has device storage of its own, so its original is unaffected.
+ // CHECK: inside: p1 == &arr[0]
+ printf("inside: p1 %s &arr[0]\n", p1 == &arr[0] ? "==" : "!=");
+
+ // CHECK: inside: p2 == &arr[0]
+ printf("inside: p2 %s &arr[0]\n", p2 == &arr[0] ? "==" : "!=");
+ }
+
+ // CHECK: in tgt: p1 != &arr[0], p2 != &arr[0]
+ printf(
+ "in tgt: p1 %s &arr[0], p2 %s &arr[0]\n",
+ p1_device == &arr[0] ? "==" : "!=", p2_device == &arr[0] ? "==" : "!=");
+
+ // CHECK: after: p1 == &arr[0]
+ printf("after: p1 %s &arr[0]\n", p1 == &arr[0] ? "==" : "!=");
+
+ // CHECK: after: p2 == &arr[0]
+ printf("after: p2 %s &arr[0]\n", p2 == &arr[0] ? "==" : "!=");
+
+ // CHECK: Done!
+ printf("Done!\n");
+ return 0;
+}
diff --git a/offload/test/unified_shared_memory/close_ptee_struct_member.c b/offload/test/unified_shared_memory/close_ptee_struct_member.c
new file mode 100644
index 0000000000000..aa512bc421a1e
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_ptee_struct_member.c
@@ -0,0 +1,85 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+// A close mapping of a pointee whose pointer is a structure member, where the
+// structure itself stays on the unified-shared-memory host path.
+//
+// Pointer attachment writes the device pointee address into the corresponding
+// pointer, which for a member is an interior address of the structure's own
+// storage: the device address of s.p is defined as the device address of s plus
+// the member offset. When s shares storage with the original, that write lands
+// in the original s.p, and device code that reaches the member through the
+// structure base -- as bar() does below -- observes it.
+//
+// The close pointee therefore gets a device buffer that the kernel writes,
+// while nothing copies it back to the original storage, so the write is lost.
+//
+// FIXME: the values checked below are the ones produced today; the expected
+// value is given alongside each. Note that the pointer cannot be given storage
+// of its own to attach into, precisely because a member's device address is
+// derived from the structure's: see the comment above.
+
+#include <stdio.h>
+
+#pragma omp requires unified_shared_memory
+
+int arr[10] = {0};
+
+struct S {
+ int a;
+ int *p;
+ int b;
+};
+
+struct S s = {1, &arr[0], 2};
+
+int *p_device;
+
+#pragma omp begin declare target
+// Reaches the member through the structure base, so it must see the attached
+// value at the device address of s plus the offset of p.
+void bar(struct S *ps) { ps->p[0] = 77; }
+#pragma omp end declare target
+
+int main() {
+ // CHECK: before: s.p == &arr[0]
+ printf("before: s.p %s &arr[0]\n", s.p == &arr[0] ? "==" : "!=");
+
+#pragma omp target data map(tofrom : s)
+ {
+#pragma omp target enter data map(close, to : s.p[0 : 10])
+
+#pragma omp target map(present, alloc : s) map(from : p_device)
+ {
+ p_device = s.p;
+ bar(&s);
+ }
+ }
+
+ // The member pointer was attached to the close buffer.
+ // CHECK: in tgt: s.p != &arr[0]
+ printf("in tgt: s.p %s &arr[0]\n", p_device == &arr[0] ? "==" : "!=");
+
+ // The original member pointer is intact afterwards.
+ // CHECK: after: s.p == &arr[0]
+ printf("after: s.p %s &arr[0]\n", s.p == &arr[0] ? "==" : "!=");
+
+ // bar() wrote through the attached member pointer, i.e. into the device
+ // buffer for the pointee. That buffer is mapped with alloc, which never
+ // assigns, so the value is not copied back.
+ // CHECK: arr[0] = 0
+ printf("arr[0] = %d\n", arr[0]);
+
+ // CHECK: Done!
+ printf("Done!\n");
+ return 0;
+}
diff --git a/offload/test/unified_shared_memory/close_ptee_two_pointers.c b/offload/test/unified_shared_memory/close_ptee_two_pointers.c
new file mode 100644
index 0000000000000..8b242292fcd59
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_ptee_two_pointers.c
@@ -0,0 +1,83 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+// Two pointers to the same pointee, where one of them has device storage of its
+// own and the other stays on the unified-shared-memory host path.
+//
+// p1 is mapped with close, so it gets a real device allocation and attachment
+// writes into that, leaving the original p1 untouched. p2 is mapped without
+// close, so its corresponding storage is the original storage, and attachment
+// writes the device pointee address into the original p2.
+//
+// So a single close pointee cannot be made correct by giving its pointers
+// storage of their own: whether that is possible depends on how each pointer
+// was mapped, and here one of them was not mapped in a way that provides it.
+//
+// FIXME: the values checked below are the ones produced today; the expected
+// value is given alongside each.
+
+#include <stdio.h>
+
+#pragma omp requires unified_shared_memory
+
+int arr[10] = {0};
+
+// Both point to the same pointee.
+int *p1 = &arr[0];
+int *p2 = &arr[0];
+
+int *p1_device, *p2_device;
+
+int main() {
+ // CHECK: before: p1 == &arr[0], p2 == &arr[0]
+ printf("before: p1 %s &arr[0], p2 %s &arr[0]\n",
+ p1 == &arr[0] ? "==" : "!=", p2 == &arr[0] ? "==" : "!=");
+
+ // p1 gets device storage of its own, p2 does not.
+#pragma omp target data map(close, alloc : p1) map(alloc : p2)
+ {
+ // The pointee is newly mapped here, with close, so it gets a device buffer.
+#pragma omp target data map(close, alloc : p1[0 : 10]) map(p2[0 : 0])
+ {
+#pragma omp target map(present, alloc : p1, p2) map(from : p1_device, p2_device)
+ {
+ p1_device = p1;
+ p2_device = p2;
+ p1[0] = 55;
+ }
+ }
+ }
+
+ // Both pointers were attached to the close buffer.
+ // CHECK: in tgt: p1 != &arr[0], p2 != &arr[0]
+ printf(
+ "in tgt: p1 %s &arr[0], p2 %s &arr[0]\n",
+ p1_device == &arr[0] ? "==" : "!=", p2_device == &arr[0] ? "==" : "!=");
+
+ // p1 has device storage of its own, so the original is intact.
+ // CHECK: after: p1 == &arr[0]
+ printf("after: p1 %s &arr[0]\n", p1 == &arr[0] ? "==" : "!=");
+
+ // p2 had no device storage of its own, so its entry was given one before the
+ // attachment, leaving the original intact as well.
+ // CHECK: after: p2 == &arr[0]
+ printf("after: p2 %s &arr[0]\n", p2 == &arr[0] ? "==" : "!=");
+
+ // The pointee is mapped with alloc, which never assigns, so the value written
+ // into the device buffer is not copied back.
+ // CHECK: arr[0] = 0
+ printf("arr[0] = %d\n", arr[0]);
+
+ // CHECK: Done!
+ printf("Done!\n");
+ return 0;
+}
diff --git a/offload/test/unified_shared_memory/close_ptr_ptee_nested.c b/offload/test/unified_shared_memory/close_ptr_ptee_nested.c
index 62d363efa8ed8..aff17d3bdb533 100644
--- a/offload/test/unified_shared_memory/close_ptr_ptee_nested.c
+++ b/offload/test/unified_shared_memory/close_ptr_ptee_nested.c
@@ -86,15 +86,21 @@ int main() {
// When the pointee has a device allocation of its own, the pointer is
// attached to it and its original host value is restored at the end of the
- // region. ATTACHED: ATTACH entry {{.*}} processed successfully ATTACHED:
- // Restoring host pointer
+ // region.
+ //
+ // clang-format off
+ // ATTACHED: ATTACH entry {{.*}} processed successfully
+ // ATTACHED: Restoring host pointer
+ // clang-format on
// ALL: Before tgt: p == p_host
// The pointer is attached to the close-allocated device pointee, so its
// device value differs from the host address.
+ // A pointer that gets attached is given device storage of its own first, so
+ // its address on the device always differs from the host address.
// V1: In tgt: p_device != p_host
- // V1: In tgt: paddr_device == &p_host
+ // V1: In tgt: paddr_device != &p_host
// p itself is close-allocated, giving the pointer variable its own device
// storage (so &p differs on the device); the pointee stays on the USM host
@@ -107,7 +113,7 @@ int main() {
// -- is attached to it, so its device value differs from the host address
// while the pointer variable itself stays on the host path (&p matches).
// V3: In tgt: p_device != p_host
- // V3: In tgt: paddr_device == &p_host
+ // V3: In tgt: paddr_device != &p_host
// The host pointer must be intact afterwards and the kernel's write must be
// visible on the host.
diff --git a/offload/test/unified_shared_memory/close_ptr_ptee_samedir.c b/offload/test/unified_shared_memory/close_ptr_ptee_samedir.c
index 78bdc326e1da3..5496062fd8c02 100644
--- a/offload/test/unified_shared_memory/close_ptr_ptee_samedir.c
+++ b/offload/test/unified_shared_memory/close_ptr_ptee_samedir.c
@@ -73,14 +73,20 @@ int main() {
// When the pointee has a device allocation of its own, the pointer is
// attached to it and its original host value is restored at the end of the
- // region. ATTACHED: ATTACH entry {{.*}} processed successfully ATTACHED:
- // Restoring host pointer
+ // region.
+ //
+ // clang-format off
+ // ATTACHED: ATTACH entry {{.*}} processed successfully
+ // ATTACHED: Restoring host pointer
+ // clang-format on
// ALL: Before tgt: p == p_host
// The close pointee is device-allocated and p (host path) is attached to it.
+ // A pointer that gets attached is given device storage of its own first, so
+ // its address on the device always differs from the host address.
// V1: In tgt: p_device != p_host
- // V1: In tgt: paddr_device == &p_host
+ // V1: In tgt: paddr_device != &p_host
// p is close-allocated (own device storage, so &p differs); the pointee is a
// plain USM mapping, so the device pointer value equals the host address.
>From bbacf250625e43a63ca0e6736ad16a88aca8c245 Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Wed, 5 Aug 2026 12:47:10 -0700
Subject: [PATCH 10/29] [OFFLOAD] Revert giving an already-mapped entry a
device allocation
Reverts the runtime part of "Give a pointer device storage before attaching it,
under USM", keeping its tests.
The device address of a mapped list item must not change while it is mapped. A
program can obtain it with omp_get_mapped_ptr, from use_device_ptr or
use_device_addr, or have passed it to a device already, so giving an entry a
device allocation after it is already present invalidates whatever was obtained
for it beforehand:
#pragma omp target enter data map(alloc : p)
void *Before = omp_get_mapped_ptr(&p, dev);
#pragma omp target enter data map(close, alloc : p[0:10])
void *After = omp_get_mapped_ptr(&p, dev); // differs from Before
Whether an entry's storage is shared with the original therefore has to be decided
when the entry is created, which is what the next change does. The recursive
re-attachment that came with the reverted approach goes away with it, since
nothing moves an entry any more.
Adds attach_mapped_ptr_stability.c, which checks the property that was violated.
The tests for the cases the reverted change addressed are kept, with their checks
returned to the behavior without it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply at anthropic.com>
---
offload/include/OpenMP/Mapping.h | 95 +---------------
offload/libomptarget/OpenMP/Mapping.cpp | 74 +-----------
offload/libomptarget/omptarget.cpp | 105 +++---------------
.../attach_mapped_ptr_stability.c | 60 ++++++++++
.../close_attach_host_deref.c | 7 +-
.../close_attach_pointer_chain.c | 15 +--
.../close_attach_struct_byte_view.c | 7 +-
.../close_ptee_attached_host_read.c | 8 +-
.../close_ptee_two_pointers.c | 7 +-
.../close_ptr_ptee_nested.c | 6 +-
.../close_ptr_ptee_samedir.c | 4 +-
11 files changed, 110 insertions(+), 278 deletions(-)
create mode 100644 offload/test/unified_shared_memory/attach_mapped_ptr_stability.c
diff --git a/offload/include/OpenMP/Mapping.h b/offload/include/OpenMP/Mapping.h
index af9e6f3acd3d2..4f20cc5490143 100644
--- a/offload/include/OpenMP/Mapping.h
+++ b/offload/include/OpenMP/Mapping.h
@@ -117,31 +117,8 @@ struct HostDataToTargetTy {
const uintptr_t HstPtrEnd; // non-inclusive.
const map_var_info_t HstPtrName; // Optional source name of mapped variable.
- // Not const: an entry whose storage is the host storage can later be given a
- // device allocation, see giveDeviceAllocation().
- uintptr_t TgtAllocBegin; // allocated target memory
- uintptr_t TgtPtrBegin; // mapped target memory = TgtAllocBegin + padding
-
- /// Whether this entry's storage is the host storage itself, i.e. it owns no
- /// device allocation. That is the case for the entries recorded on the
- /// unified-shared-memory host path.
- bool isHostBacked() const { return TgtPtrBegin == HstPtrBegin; }
-
- /// Give a host-backed entry a device allocation, so that it stops sharing
- /// storage with the original. Used when a pointer inside this entry's storage
- /// is about to be attached: attachment assigns the corresponding pointer, and
- /// while the corresponding storage is the original storage that assignment
- /// would be observable through the original pointer.
- ///
- /// The entry keeps its identity, so its reference count continues to govern
- /// its lifetime, and any shadow pointer recorded for it is transferred back
- /// to the host by the same data motion as for any other entry.
- void giveDeviceAllocation(uintptr_t NewTgtAllocBegin,
- uintptr_t NewTgtPtrBegin) {
- assert(isHostBacked() && "Entry already owns a device allocation");
- TgtAllocBegin = NewTgtAllocBegin;
- TgtPtrBegin = NewTgtPtrBegin;
- }
+ const uintptr_t TgtAllocBegin; // allocated target memory
+ const uintptr_t TgtPtrBegin; // mapped target memory = TgtAllocBegin + padding
private:
static const uint64_t INFRefCount = ~(uint64_t)0;
@@ -518,25 +495,6 @@ struct AttachMapInfo {
MapType(Type), Pointername(Name) {}
};
-/// A pointer that has been attached to a pointee, recorded so that it can be
-/// attached again if the pointee's entry is later given a device allocation and
-/// its device address therefore changes.
-///
-/// The pointee is the key of MappingInfoTy::AttachedPointers, so only what is
-/// needed to redo the attachment is kept here.
-struct AttachedPointerTy {
- /// Original address of the pointer itself, e.g. &p or &s.p.
- void **HstPtrAddr;
- /// Original address of the pointee base, which may differ from the pointee
- /// address that keys the index, e.g. for map(p[10:5]).
- void *HstPteeBase;
- /// Size of the pointer, which is larger than a pointer for a descriptor.
- int64_t PtrSize;
-
- AttachedPointerTy(void **HstPtrAddr, void *HstPteeBase, int64_t PtrSize)
- : HstPtrAddr(HstPtrAddr), HstPteeBase(HstPteeBase), PtrSize(PtrSize) {}
-};
-
/// Structure to track new allocations, ATTACH entries, DELETE entries and
/// skipped FROM data transfer information for a given construct, across
/// recursive calls (for handling mappers) to targetDataBegin/targetDataEnd.
@@ -707,23 +665,6 @@ struct MappingInfoTy {
/// The type used to access the HDTT map.
using HDTTMapAccessorTy = decltype(HostDataToTargetMap)::AccessorTy;
- /// Which pointers are attached to a given pointee, keyed by the pointee's
- /// original address.
- ///
- /// Attachment records a shadow pointer on the entry holding the pointer,
- /// which gives the pointer's pointee but not the reverse. This index provides
- /// the reverse direction, which is needed when a pointee's entry is given a
- /// device allocation after something has already been attached to it: the
- /// attached pointers designate its previous device address and have to be
- /// attached again. See giveEntryDeviceAllocation().
- ///
- /// Only pointers that are actually attached are recorded, so this stays empty
- /// for mappings that never involve pointer attachment.
- ///
- /// Accessed under the HDTT map accessor.
- llvm::DenseMap<void *, llvm::SmallVector<AttachedPointerTy, 2>>
- AttachedPointers;
-
/// Lookup the mapping of \p HstPtrBegin in \p HDTTMap. The accessor ensures
/// exclusive access to the HDTT map.
LookupResult lookupMapping(HDTTMapAccessorTy &HDTTMap, void *HstPtrBegin,
@@ -778,38 +719,6 @@ struct MappingInfoTy {
[[nodiscard]] int eraseMapEntry(HDTTMapAccessorTy &HDTTMap,
HostDataToTargetTy *Entry, int64_t Size);
- /// Record that \p HstPtrAddr is attached to the pointee at \p HstPteeBegin.
- /// See AttachedPointers.
- void recordAttachedPointer(void *HstPteeBegin, void **HstPtrAddr,
- void *HstPteeBase, int64_t PtrSize);
-
- /// Give \p Entry, which must be host-backed, a device allocation and copy the
- /// current contents of its storage into it, so that it stops sharing storage
- /// with the original.
- ///
- /// This is needed before attaching a pointer that lies within \p Entry's
- /// storage: attachment assigns the corresponding pointer, and while the
- /// corresponding storage is the original storage that assignment is
- /// observable through the original pointer. The whole entry has to be given
- /// storage rather than just the pointer, because the device address of
- /// anything inside it is defined as this entry's device address plus an
- /// offset.
- ///
- /// The contents are copied so that whatever else the storage holds -- other
- /// structure members, for instance -- is present on the device.
- ///
- /// Anything already attached to a pointee inside \p Entry designated its
- /// previous device address, so those pointers have to be attached again. They
- /// are appended to \p ToReattach as (pointee, pointer) pairs rather than
- /// attached here, since performing an attachment is the caller's job.
- ///
- /// \p HDTTMap must be held by the caller. Returns \c OFFLOAD_SUCCESS if the
- /// entry now owns a device allocation, and \c OFFLOAD_FAIL otherwise.
- [[nodiscard]] int giveEntryDeviceAllocation(
- HDTTMapAccessorTy &HDTTMap, HostDataToTargetTy *Entry,
- AsyncInfoTy &AsyncInfo,
- llvm::SmallVectorImpl<std::pair<void *, AttachedPointerTy>> &ToReattach);
-
/// Deallocate the \p Entry from the device memory and delete it. Return \c
/// OFFLOAD_SUCCESS if the deallocation operations executed successfully, and
/// return \c OFFLOAD_FAIL otherwise.
diff --git a/offload/libomptarget/OpenMP/Mapping.cpp b/offload/libomptarget/OpenMP/Mapping.cpp
index ed0751a1027eb..dbf88118000fa 100644
--- a/offload/libomptarget/OpenMP/Mapping.cpp
+++ b/offload/libomptarget/OpenMP/Mapping.cpp
@@ -561,73 +561,6 @@ int MappingInfoTy::eraseMapEntry(HDTTMapAccessorTy &HDTTMap,
return OFFLOAD_SUCCESS;
}
-void MappingInfoTy::recordAttachedPointer(void *HstPteeBegin, void **HstPtrAddr,
- void *HstPteeBase, int64_t PtrSize) {
- auto &Attached = AttachedPointers[HstPteeBegin];
- for (const AttachedPointerTy &A : Attached)
- if (A.HstPtrAddr == HstPtrAddr)
- return;
- Attached.emplace_back(HstPtrAddr, HstPteeBase, PtrSize);
-}
-
-int MappingInfoTy::giveEntryDeviceAllocation(
- HDTTMapAccessorTy &HDTTMap, HostDataToTargetTy *Entry,
- AsyncInfoTy &AsyncInfo,
- llvm::SmallVectorImpl<std::pair<void *, AttachedPointerTy>> &ToReattach) {
- assert(Entry && "Trying to allocate for a null entry.");
- assert(Entry->isHostBacked() && "Entry already owns a device allocation");
-
- void *HstPtrBegin = reinterpret_cast<void *>(Entry->HstPtrBegin);
- int64_t Size = Entry->HstPtrEnd - Entry->HstPtrBegin;
-
- uintptr_t TgtAllocBegin =
- reinterpret_cast<uintptr_t>(Device.allocData(Size, HstPtrBegin));
- if (!TgtAllocBegin) {
- REPORT() << "Failed to allocate device memory for " << HstPtrBegin << ".";
- return OFFLOAD_FAIL;
- }
-
- Entry->giveDeviceAllocation(TgtAllocBegin, TgtAllocBegin);
-
- INFO(OMP_INFOTYPE_MAPPING_CHANGED, Device.DeviceID,
- "Allocating device memory for existing map entry with "
- "HstPtrBegin=" DPxMOD ", TgtPtrBegin=" DPxMOD ", Size=%" PRId64 "\n",
- DPxPTR(Entry->HstPtrBegin), DPxPTR(TgtAllocBegin), Size);
- ODBG(ODT_Mapping) << "Allocating device memory for existing map entry (hst:"
- << HstPtrBegin
- << ") -> (tgt:" << reinterpret_cast<void *>(TgtAllocBegin)
- << "), Size=" << Size;
-
- // The storage was shared with the host until now, so bring its current
- // contents over: it may hold data other than the pointer being attached.
- int Ret = Device.submitData(reinterpret_cast<void *>(TgtAllocBegin),
- HstPtrBegin, Size, AsyncInfo, Entry);
- if (Ret != OFFLOAD_SUCCESS) {
- REPORT() << "Copying data to device failed.";
- return OFFLOAD_FAIL;
- }
-
- if (Device.notifyDataMapped(HstPtrBegin, Size))
- return OFFLOAD_FAIL;
-
- // The entry's device address has changed, so anything already attached to a
- // pointee within it designates the previous one. Collect those pointers for
- // the caller to attach again.
- for (auto &[HstPteeBegin, Attached] : AttachedPointers) {
- if (reinterpret_cast<uintptr_t>(HstPteeBegin) < Entry->HstPtrBegin ||
- reinterpret_cast<uintptr_t>(HstPteeBegin) >= Entry->HstPtrEnd)
- continue;
- for (const AttachedPointerTy &A : Attached) {
- ODBG(ODT_Mapping) << "Pointer " << A.HstPtrAddr
- << " is attached to pointee " << HstPteeBegin
- << " within the entry, so it needs attaching again";
- ToReattach.emplace_back(HstPteeBegin, A);
- }
- }
-
- return OFFLOAD_SUCCESS;
-}
-
int MappingInfoTy::deallocTgtPtrAndEntry(HostDataToTargetTy *Entry,
int64_t Size) {
assert(Entry && "Trying to deallocate a null entry.");
@@ -644,10 +577,11 @@ int MappingInfoTy::deallocTgtPtrAndEntry(HostDataToTargetTy *Entry,
return OFFLOAD_FAIL;
}
- // A host-backed entry owns no device allocation: its allocation maps to the
- // host address itself, so it must not be handed to deleteData().
+ // The reuse entry recorded on the unified-shared-memory host path owns no
+ // device allocation: its allocation maps to the host address itself, so it
+ // must not be handed to deleteData().
int Ret = OFFLOAD_SUCCESS;
- if (!Entry->isHostBacked())
+ if (Entry->TgtAllocBegin != Entry->HstPtrBegin)
Ret = Device.deleteData((void *)Entry->TgtAllocBegin);
// Notify the plugin about the unmapped memory.
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index 88e12c96c8f77..917b865da53f5 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -453,13 +453,6 @@ static int performPointerAttachment(DeviceTy &Device, AsyncInfoTy &AsyncInfo,
void *TgtPteeBase =
calculateTargetPointeeBase(HstPteeBase, HstPteeBegin, TgtPteeBegin);
- // Record the pointer against its pointee, so that it can be attached again if
- // the pointee's entry is later given a device allocation. Recorded even when
- // the shadow pointer below turns out to be a duplicate, since the pointee's
- // address may still change afterwards.
- Device.getMappingInfo().recordAttachedPointer(HstPteeBegin, HstPtrAddr,
- HstPteeBase, HstPtrSize);
-
// Add shadow pointer tracking
if (!PtrTPR.getEntry()->addShadowPointer(
ShadowPtrInfoTy{HstPtrAddr, TgtPtrAddr, TgtPteeBase, HstPtrSize})) {
@@ -885,9 +878,15 @@ int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
}
// Lambda to perform target pointer lookup and validation
+ // \p AllowHostPointer permits the lookup to succeed for storage shared with
+ // the host. That is correct for the pointer being attached: the device
+ // dereferences the same storage, so attaching writes the device pointee
+ // address into it and the original value is restored at the end of the
+ // region through the shadow-pointer mechanism. It is not correct for the
+ // pointee, where a host address would mean there is nothing to attach to.
auto LookupTargetPointer =
- [&](void *Ptr, int64_t Size,
- const char *PtrType) -> std::optional<TargetPointerResultTy> {
+ [&](void *Ptr, int64_t Size, const char *PtrType,
+ bool AllowHostPointer) -> std::optional<TargetPointerResultTy> {
// ATTACH map-type does not change ref-count, or do any allocation
// We just need to do a lookup for the pointer/pointee.
TargetPointerResultTy TPR = Device.getMappingInfo().getTgtPtrBegin(
@@ -903,7 +902,7 @@ int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
<< PtrType << " not present on device";
return std::nullopt;
}
- if (TPR.Flags.IsHostPointer) {
+ if (TPR.Flags.IsHostPointer && !AllowHostPointer) {
ODBG(ODT_Mapping) << "Skipping ATTACH entry " << EntryIdx
<< ": device version of the " << PtrType
<< " is a host pointer.";
@@ -916,7 +915,8 @@ int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
// Get device version of the pointee (e.g., &p[10]) first, as we can
// release its TPR after extracting the pointer value.
void *TgtPteeBegin = [&]() -> void * {
- if (auto PteeTPROpt = LookupTargetPointer(HstPteeBegin, 0, "pointee"))
+ if (auto PteeTPROpt = LookupTargetPointer(HstPteeBegin, 0, "pointee",
+ /*AllowHostPointer=*/false))
return PteeTPROpt->TargetPointer;
return nullptr;
}();
@@ -926,86 +926,10 @@ int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
// Get device version of the pointer (e.g., &p) next. We need to keep its
// TPR for use in shadow-pointer handling during pointer-attachment.
- auto PtrTPROpt = LookupTargetPointer(HstPtr, PtrSize, "pointer");
+ auto PtrTPROpt = LookupTargetPointer(HstPtr, PtrSize, "pointer",
+ /*AllowHostPointer=*/true);
if (!PtrTPROpt)
continue;
-
- // Attachment assigns the corresponding pointer. If the entry holding the
- // pointer is host-backed then the corresponding storage *is* the original
- // storage, so that assignment is observable through the original pointer:
- // the host can no longer use it, and once the pointee's storage is released
- // it is left dangling.
- //
- // Give that entry a device allocation of its own instead, so that the
- // pointer has a corresponding pointer distinct from the original. The whole
- // entry is given storage, since the device address of anything inside it is
- // defined as the entry's device address plus an offset, and its contents
- // are copied so that whatever else it holds is present on the device.
- if (PtrTPROpt->getEntry() && PtrTPROpt->getEntry()->isHostBacked()) {
- HostDataToTargetTy *Entry = PtrTPROpt->getEntry();
- // Release the TPR's hold on the entry before taking the map accessor.
- PtrTPROpt.reset();
-
- // Giving an entry a device allocation moves it, so anything already
- // attached to a pointee within it has to be attached again. Doing so may
- // require giving a further entry an allocation, so keep going until
- // nothing is left, which terminates because an entry can only be given an
- // allocation once.
- SmallVector<std::pair<void *, AttachedPointerTy>> ToReattach;
- {
- MappingInfoTy::HDTTMapAccessorTy HDTTMap =
- Device.getMappingInfo().HostDataToTargetMap.getExclusiveAccessor();
- if (Device.getMappingInfo().giveEntryDeviceAllocation(
- HDTTMap, Entry, AsyncInfo, ToReattach) != OFFLOAD_SUCCESS)
- return OFFLOAD_FAIL;
- }
-
- while (!ToReattach.empty()) {
- auto [ReHstPteeBegin, ReAttached] = ToReattach.pop_back_val();
-
- auto RePteeTPROpt =
- LookupTargetPointer(ReHstPteeBegin, 0, "re-attach pointee");
- if (!RePteeTPROpt)
- continue;
- void *ReTgtPteeBegin = RePteeTPROpt->TargetPointer;
- RePteeTPROpt.reset();
-
- auto RePtrTPROpt = LookupTargetPointer(
- ReAttached.HstPtrAddr, ReAttached.PtrSize, "re-attach pointer");
- if (!RePtrTPROpt)
- continue;
-
- if (RePtrTPROpt->getEntry() &&
- RePtrTPROpt->getEntry()->isHostBacked()) {
- HostDataToTargetTy *ReEntry = RePtrTPROpt->getEntry();
- RePtrTPROpt.reset();
- {
- MappingInfoTy::HDTTMapAccessorTy HDTTMap =
- Device.getMappingInfo()
- .HostDataToTargetMap.getExclusiveAccessor();
- if (Device.getMappingInfo().giveEntryDeviceAllocation(
- HDTTMap, ReEntry, AsyncInfo, ToReattach) != OFFLOAD_SUCCESS)
- return OFFLOAD_FAIL;
- }
- RePtrTPROpt = LookupTargetPointer(
- ReAttached.HstPtrAddr, ReAttached.PtrSize, "re-attach pointer");
- if (!RePtrTPROpt)
- continue;
- }
-
- Ret = performPointerAttachment(
- Device, AsyncInfo, ReAttached.HstPtrAddr, ReAttached.HstPteeBase,
- ReHstPteeBegin,
- reinterpret_cast<void **>(RePtrTPROpt->TargetPointer),
- ReTgtPteeBegin, ReAttached.PtrSize, *RePtrTPROpt);
- if (Ret != OFFLOAD_SUCCESS)
- return OFFLOAD_FAIL;
- }
-
- PtrTPROpt = LookupTargetPointer(HstPtr, PtrSize, "pointer");
- if (!PtrTPROpt)
- continue;
- }
TargetPointerResultTy &PtrTPR = *PtrTPROpt;
void **TgtPtrBase = reinterpret_cast<void **>(PtrTPR.TargetPointer);
@@ -1078,7 +1002,8 @@ postProcessingTargetDataEnd(DeviceTy *Device,
// with a zero reference count and a later map(close, ...) of the same
// storage finds it and concludes the data is already on the device.
const bool IsHostBackedEntry =
- TPR.getEntry() != nullptr && TPR.getEntry()->isHostBacked();
+ TPR.getEntry() != nullptr &&
+ TPR.getEntry()->TgtAllocBegin == TPR.getEntry()->HstPtrBegin;
bool DelEntry = !TPR.isHostPointer() || IsHostBackedEntry;
// If the last element from the mapper (for end transfer args comes in
diff --git a/offload/test/unified_shared_memory/attach_mapped_ptr_stability.c b/offload/test/unified_shared_memory/attach_mapped_ptr_stability.c
new file mode 100644
index 0000000000000..6edbf0e63cc7a
--- /dev/null
+++ b/offload/test/unified_shared_memory/attach_mapped_ptr_stability.c
@@ -0,0 +1,60 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+// The device address of a mapped list item must not change while it is mapped.
+//
+// A program can obtain it with omp_get_mapped_ptr, and can also hold it from
+// use_device_ptr or use_device_addr, or have passed it to a device already. So
+// whether an entry's storage is shared with the original can only be decided when
+// the entry is created: giving an already-present entry a device allocation later
+// would invalidate every device address obtained for it beforehand.
+//
+// This matters for pointer attachment under unified shared memory, where a pointer
+// whose storage is shared with the original cannot be attached without writing a
+// device address into the original pointer. Resolving that by giving the pointer
+// storage after the fact is what this test rules out.
+
+#include <omp.h>
+#include <stdio.h>
+
+#pragma omp requires unified_shared_memory
+
+int arr[10];
+int *p = &arr[0];
+
+int main() {
+ int dev = omp_get_default_device();
+
+ // The unified_shared_memory requirement is registered when a device image is
+ // loaded, so the program needs a target region for it to take effect.
+#pragma omp target
+ {
+ }
+
+#pragma omp target enter data map(alloc : p)
+
+ void *Before = omp_get_mapped_ptr(&p, dev);
+
+ // Attaching p to a pointee that has its own device storage must not change the
+ // device address of p itself. Here the pointee is newly mapped with close,
+ // which is what prescribes the attachment.
+#pragma omp target enter data map(close, alloc : p[0 : 10])
+
+ void *After = omp_get_mapped_ptr(&p, dev);
+
+ // CHECK: device address of p is stable: yes
+ printf("device address of p is stable: %s\n", Before == After ? "yes" : "no");
+
+ // CHECK: Done!
+ printf("Done!\n");
+ return 0;
+}
diff --git a/offload/test/unified_shared_memory/close_attach_host_deref.c b/offload/test/unified_shared_memory/close_attach_host_deref.c
index cc05226625ea4..eb8544cabf536 100644
--- a/offload/test/unified_shared_memory/close_attach_host_deref.c
+++ b/offload/test/unified_shared_memory/close_attach_host_deref.c
@@ -69,9 +69,10 @@ int main() {
#pragma omp target enter data map(close, alloc : p[0 : 10])
#endif
- // The pointer's entry was given a device allocation, so attachment wrote the
- // device pointee address there and the original p is untouched.
- // CHECK: after attach: p == &x[0]
+ // EXPECTED: after attach: p == &x[0]
+ // CHECK: after attach: p != &x[0]
+ // FIXME: the pointer's storage is shared with the original, so attachment wrote
+ // the device pointee address into the original p.
printf("after attach: p %s &x[0]\n", p == &x[0] ? "==" : "!=");
// CHECK: Done!
diff --git a/offload/test/unified_shared_memory/close_attach_pointer_chain.c b/offload/test/unified_shared_memory/close_attach_pointer_chain.c
index 9678d8dba2233..9a73068829e04 100644
--- a/offload/test/unified_shared_memory/close_attach_pointer_chain.c
+++ b/offload/test/unified_shared_memory/close_attach_pointer_chain.c
@@ -70,18 +70,19 @@ int main() {
p2_device = p1->p2;
}
- // p1 was attached to the structure before it was given device storage, so it
- // was attached again afterwards and designates the device copy.
- // CHECK: device p1 != &inner
+ // EXPECTED: device p1 != &inner
+ // CHECK: device p1 == &inner
+ // FIXME: p1's storage is shared with the original, so it was never given a
+ // device value and still designates the original structure.
printf("device p1 %s &inner\n", p1_device == &inner ? "==" : "!=");
- // So p2 is read from the device copy of the structure, where it designates
- // the close buffer.
// CHECK: device p1->p2 != &leaf[0]
printf("device p1->p2 %s &leaf[0]\n", p2_device == &leaf[0] ? "==" : "!=");
- // The original pointers are intact.
- // CHECK: host: p1 == &inner, inner.p2 == &leaf[0]
+ // EXPECTED: host: p1 == &inner, inner.p2 == &leaf[0]
+ // CHECK: host: p1 == &inner, inner.p2 != &leaf[0]
+ // FIXME: the structure's storage is shared with the original, so attachment of
+ // its member wrote the device pointee address into the original inner.p2.
printf("host: p1 %s &inner, inner.p2 %s &leaf[0]\n",
p1 == &inner ? "==" : "!=", inner.p2 == &leaf[0] ? "==" : "!=");
diff --git a/offload/test/unified_shared_memory/close_attach_struct_byte_view.c b/offload/test/unified_shared_memory/close_attach_struct_byte_view.c
index 907c4eb02c880..a98a5c5d50e75 100644
--- a/offload/test/unified_shared_memory/close_attach_struct_byte_view.c
+++ b/offload/test/unified_shared_memory/close_attach_struct_byte_view.c
@@ -62,9 +62,10 @@ int main() {
// The pointee is newly mapped with close, which triggers the attachment.
#pragma omp target enter data map(close, alloc : s.p[0 : 10])
- // The byte range was given a device allocation as a whole, so attachment
- // wrote into that and the original s.p is untouched.
- // CHECK: after attach: s.p == &arr[0]
+ // EXPECTED: after attach: s.p == &arr[0]
+ // CHECK: after attach: s.p != &arr[0]
+ // FIXME: the byte range's storage is shared with the original, so attachment
+ // wrote the device pointee address into the original s.p.
printf("after attach: s.p %s &arr[0]\n", s.p == &arr[0] ? "==" : "!=");
// CHECK: Done!
diff --git a/offload/test/unified_shared_memory/close_ptee_attached_host_read.c b/offload/test/unified_shared_memory/close_ptee_attached_host_read.c
index 9394c646decd6..ab9d2a201006b 100644
--- a/offload/test/unified_shared_memory/close_ptee_attached_host_read.c
+++ b/offload/test/unified_shared_memory/close_ptee_attached_host_read.c
@@ -72,7 +72,9 @@ int main() {
// CHECK: inside: p1 == &arr[0]
printf("inside: p1 %s &arr[0]\n", p1 == &arr[0] ? "==" : "!=");
- // CHECK: inside: p2 == &arr[0]
+ // EXPECTED: inside: p2 == &arr[0]
+ // CHECK: inside: p2 != &arr[0]
+ // FIXME: the original p2 holds the device pointee address here.
printf("inside: p2 %s &arr[0]\n", p2 == &arr[0] ? "==" : "!=");
}
@@ -84,7 +86,9 @@ int main() {
// CHECK: after: p1 == &arr[0]
printf("after: p1 %s &arr[0]\n", p1 == &arr[0] ? "==" : "!=");
- // CHECK: after: p2 == &arr[0]
+ // EXPECTED: after: p2 == &arr[0]
+ // CHECK: after: p2 != &arr[0]
+ // FIXME: nothing restores the original p2.
printf("after: p2 %s &arr[0]\n", p2 == &arr[0] ? "==" : "!=");
// CHECK: Done!
diff --git a/offload/test/unified_shared_memory/close_ptee_two_pointers.c b/offload/test/unified_shared_memory/close_ptee_two_pointers.c
index 8b242292fcd59..4e298d44ec72a 100644
--- a/offload/test/unified_shared_memory/close_ptee_two_pointers.c
+++ b/offload/test/unified_shared_memory/close_ptee_two_pointers.c
@@ -67,9 +67,10 @@ int main() {
// CHECK: after: p1 == &arr[0]
printf("after: p1 %s &arr[0]\n", p1 == &arr[0] ? "==" : "!=");
- // p2 had no device storage of its own, so its entry was given one before the
- // attachment, leaving the original intact as well.
- // CHECK: after: p2 == &arr[0]
+ // EXPECTED: after: p2 == &arr[0]
+ // CHECK: after: p2 != &arr[0]
+ // FIXME: p2's storage is shared with the original, so attachment wrote the
+ // device pointee address into the original p2.
printf("after: p2 %s &arr[0]\n", p2 == &arr[0] ? "==" : "!=");
// The pointee is mapped with alloc, which never assigns, so the value written
diff --git a/offload/test/unified_shared_memory/close_ptr_ptee_nested.c b/offload/test/unified_shared_memory/close_ptr_ptee_nested.c
index aff17d3bdb533..6453849b2a346 100644
--- a/offload/test/unified_shared_memory/close_ptr_ptee_nested.c
+++ b/offload/test/unified_shared_memory/close_ptr_ptee_nested.c
@@ -97,10 +97,8 @@ int main() {
// The pointer is attached to the close-allocated device pointee, so its
// device value differs from the host address.
- // A pointer that gets attached is given device storage of its own first, so
- // its address on the device always differs from the host address.
// V1: In tgt: p_device != p_host
- // V1: In tgt: paddr_device != &p_host
+ // V1: In tgt: paddr_device == &p_host
// p itself is close-allocated, giving the pointer variable its own device
// storage (so &p differs on the device); the pointee stays on the USM host
@@ -113,7 +111,7 @@ int main() {
// -- is attached to it, so its device value differs from the host address
// while the pointer variable itself stays on the host path (&p matches).
// V3: In tgt: p_device != p_host
- // V3: In tgt: paddr_device != &p_host
+ // V3: In tgt: paddr_device == &p_host
// The host pointer must be intact afterwards and the kernel's write must be
// visible on the host.
diff --git a/offload/test/unified_shared_memory/close_ptr_ptee_samedir.c b/offload/test/unified_shared_memory/close_ptr_ptee_samedir.c
index 5496062fd8c02..59464c7dd5e00 100644
--- a/offload/test/unified_shared_memory/close_ptr_ptee_samedir.c
+++ b/offload/test/unified_shared_memory/close_ptr_ptee_samedir.c
@@ -83,10 +83,8 @@ int main() {
// ALL: Before tgt: p == p_host
// The close pointee is device-allocated and p (host path) is attached to it.
- // A pointer that gets attached is given device storage of its own first, so
- // its address on the device always differs from the host address.
// V1: In tgt: p_device != p_host
- // V1: In tgt: paddr_device != &p_host
+ // V1: In tgt: paddr_device == &p_host
// p is close-allocated (own device storage, so &p differs); the pointee is a
// plain USM mapping, so the device pointer value equals the host address.
>From 2e738331da7a93dcd2bedb1932dc73dd4a84866e Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Wed, 5 Aug 2026 12:58:16 -0700
Subject: [PATCH 11/29] [OFFLOAD] Keep a pointee on the host path when its
pointer cannot be attached
Pointer attachment assigns the corresponding pointer. Under unified shared memory
that may be the original storage, and then the assignment writes a device address
into the original pointer: the host can no longer use it, and once the pointee's
storage is released it is left dangling.
Whether an entry's storage is shared with the original can only be decided when the
entry is created, since the device address of a mapped list item must not change
while it is mapped. The pointer is generally already mapped by the time the
attachment is prescribed, so it is the pointee that has to be decided: if this
construct will attach a pointer to it whose own storage is shared with the
original, the pointee is kept on the host path, and the address attached to the
pointer is then the original one.
The map types are inspected once before any entry is created to find those
pointees, which costs nothing for constructs that prescribe no attachment. A
pointer that is not mapped at all is not attached, and one that has a device
allocation of its own can be attached without touching the original, so neither
places any requirement on the pointee.
This gives up the separate device buffer only for a pointee whose pointer cannot
receive a device address, which is where that buffer could not have been used
correctly anyway. Mappings that involve no attachment are unaffected.
A case remains that a creation-time decision cannot address: when the pointer and
the pointee are both already present, neither can be given a different backing, so
attachment still writes into the original pointer. close_attach_host_deref.c covers
it under LIBOMPTARGET_TREAT_ATTACH_AUTO_AS_ALWAYS and it wants a diagnostic.
Co-Authored-By: Claude Opus 5 (1M context) <noreply at anthropic.com>
---
offload/include/OpenMP/Mapping.h | 8 ++++
offload/libomptarget/OpenMP/Mapping.cpp | 14 +++++-
offload/libomptarget/omptarget.cpp | 44 +++++++++++++++++++
.../attach_mapped_ptr_stability.c | 21 ++++-----
.../close_attach_host_deref.c | 20 ++++++---
.../close_attach_pointer_chain.c | 14 +++---
.../close_attach_struct_byte_view.c | 6 +--
.../close_ptee_struct_member.c | 11 +++--
.../close_ptee_two_pointers.c | 17 +++----
.../close_ptr_ptee_nested.c | 6 +--
10 files changed, 113 insertions(+), 48 deletions(-)
diff --git a/offload/include/OpenMP/Mapping.h b/offload/include/OpenMP/Mapping.h
index 4f20cc5490143..668c8e2c23114 100644
--- a/offload/include/OpenMP/Mapping.h
+++ b/offload/include/OpenMP/Mapping.h
@@ -21,6 +21,7 @@
#include <mutex>
#include <string>
+#include "llvm/ADT/SmallPtrSet.h"
#include "llvm/ADT/SmallSet.h"
struct DeviceTy;
@@ -502,6 +503,13 @@ struct StateInfoTy {
/// ATTACH map entries for deferred processing until all other maps are done.
llvm::SmallVector<AttachMapInfo> AttachEntries;
+ /// Pointees that this construct will attach a pointer to whose own storage is
+ /// shared with the original. Such a pointee has to stay on the host path, so
+ /// that attaching the pointer does not write a device address into the
+ /// original pointer. Populated before any entry is created, since the device
+ /// address of a mapped list item must not change while it is mapped.
+ llvm::SmallPtrSet<void *, 4> PointeesToKeepOnHostPath;
+
/// Host pointers for which new device memory was allocated.
/// Key: host pointer, Value: allocation size.
/// Consulted by the 'present' map-type validation.
diff --git a/offload/libomptarget/OpenMP/Mapping.cpp b/offload/libomptarget/OpenMP/Mapping.cpp
index dbf88118000fa..f56a9938ef9c6 100644
--- a/offload/libomptarget/OpenMP/Mapping.cpp
+++ b/offload/libomptarget/OpenMP/Mapping.cpp
@@ -278,7 +278,19 @@ TargetPointerResultTy MappingInfoTy::getTargetPointer(
// change.
(LR.TPR.getEntry() != nullptr &&
(LR.Flags.IsContained || LR.Flags.ExtendsBefore ||
- LR.Flags.ExtendsAfter)))) ||
+ LR.Flags.ExtendsAfter)) ||
+ // A pointer whose storage is shared with the original cannot be
+ // attached to a device allocation: the assignment would write a
+ // device address into the original pointer. When this construct
+ // will attach such a pointer to this storage, keep it on the
+ // host path, so that the address attached to the pointer is the
+ // original one.
+ //
+ // Giving the pointer a device allocation instead is not possible
+ // here, because it is already mapped and the device address of a
+ // mapped list item must not change.
+ (StateInfo &&
+ StateInfo->PointeesToKeepOnHostPath.contains(HstPtrBegin)))) ||
(PM->getRequirements() & OMPX_REQ_AUTO_ZERO_COPY)) {
// If unified shared memory is active, implicitly mapped variables that are
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index 917b865da53f5..d742ddad319b5 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -515,6 +515,50 @@ int targetDataBegin(ident_t *Loc, DeviceTy &Device, int32_t ArgNum,
StateInfoTy *StateInfo, bool FromMapper) {
assert(StateInfo && "StateInfo must be available for targetDataBegin for "
"handling ATTACH and TO/TOFROM map-types.");
+
+ // Whether an entry's storage is shared with the original can only be decided
+ // when the entry is created, since the device address of a mapped list item
+ // must not change while it is mapped. So before creating anything, find the
+ // pointees that this construct will attach a pointer to whose own storage is
+ // shared with the original: giving such a pointee a device allocation would
+ // mean writing a device address into the original pointer.
+ //
+ // Only the map types are inspected here, so this costs nothing for constructs
+ // that prescribe no attachment.
+ for (int32_t I = 0; I < ArgNum; ++I) {
+ if (!(ArgTypes[I] & OMP_TGT_MAPTYPE_ATTACH) ||
+ (ArgTypes[I] & OMP_TGT_MAPTYPE_PRIVATE))
+ continue;
+
+ void **HstPtr = reinterpret_cast<void **>(ArgsBase[I]);
+ void *HstPteeBegin = Args[I];
+
+ // The pointee only has to stay on the host path if the pointer will really
+ // be attached, and if doing so would write into storage shared with the
+ // original. A pointer that is not mapped at all is not attached, and one
+ // that has a device allocation of its own can be attached without touching
+ // the original, so neither places any requirement on the pointee.
+ MappingInfoTy::HDTTMapAccessorTy HDTTMap =
+ Device.getMappingInfo().HostDataToTargetMap.getExclusiveAccessor();
+ LookupResult LR =
+ Device.getMappingInfo().lookupMapping(HDTTMap, HstPtr, sizeof(void *));
+ const bool PtrIsMapped = LR.Flags.IsContained && LR.TPR.getEntry();
+ const bool PtrIsHostBacked =
+ PtrIsMapped &&
+ LR.TPR.getEntry()->TgtPtrBegin == LR.TPR.getEntry()->HstPtrBegin;
+ LR.TPR.setEntry(nullptr);
+ HDTTMap.destroy();
+
+ if (!PtrIsHostBacked)
+ continue;
+
+ ODBG(ODT_Mapping) << "Pointee " << HstPteeBegin
+ << " will have a pointer attached to it whose storage is "
+ "shared with the original, so it has to stay on the "
+ "host path";
+ StateInfo->PointeesToKeepOnHostPath.insert(HstPteeBegin);
+ }
+
// process each input.
for (int32_t I = 0; I < ArgNum; ++I) {
// Ignore private variables and arrays - there is no mapping for them.
diff --git a/offload/test/unified_shared_memory/attach_mapped_ptr_stability.c b/offload/test/unified_shared_memory/attach_mapped_ptr_stability.c
index 6edbf0e63cc7a..751e9801c94d1 100644
--- a/offload/test/unified_shared_memory/attach_mapped_ptr_stability.c
+++ b/offload/test/unified_shared_memory/attach_mapped_ptr_stability.c
@@ -14,14 +14,15 @@
//
// A program can obtain it with omp_get_mapped_ptr, and can also hold it from
// use_device_ptr or use_device_addr, or have passed it to a device already. So
-// whether an entry's storage is shared with the original can only be decided when
-// the entry is created: giving an already-present entry a device allocation later
-// would invalidate every device address obtained for it beforehand.
+// whether an entry's storage is shared with the original can only be decided
+// when the entry is created: giving an already-present entry a device
+// allocation later would invalidate every device address obtained for it
+// beforehand.
//
-// This matters for pointer attachment under unified shared memory, where a pointer
-// whose storage is shared with the original cannot be attached without writing a
-// device address into the original pointer. Resolving that by giving the pointer
-// storage after the fact is what this test rules out.
+// This matters for pointer attachment under unified shared memory, where a
+// pointer whose storage is shared with the original cannot be attached without
+// writing a device address into the original pointer. Resolving that by giving
+// the pointer storage after the fact is what this test rules out.
#include <omp.h>
#include <stdio.h>
@@ -44,9 +45,9 @@ int main() {
void *Before = omp_get_mapped_ptr(&p, dev);
- // Attaching p to a pointee that has its own device storage must not change the
- // device address of p itself. Here the pointee is newly mapped with close,
- // which is what prescribes the attachment.
+ // Attaching p to a pointee that has its own device storage must not change
+ // the device address of p itself. Here the pointee is newly mapped with
+ // close, which is what prescribes the attachment.
#pragma omp target enter data map(close, alloc : p[0 : 10])
void *After = omp_get_mapped_ptr(&p, dev);
diff --git a/offload/test/unified_shared_memory/close_attach_host_deref.c b/offload/test/unified_shared_memory/close_attach_host_deref.c
index eb8544cabf536..6957c5acba944 100644
--- a/offload/test/unified_shared_memory/close_attach_host_deref.c
+++ b/offload/test/unified_shared_memory/close_attach_host_deref.c
@@ -1,9 +1,11 @@
// RUN: %libomptarget-compile-generic
-// RUN: %libomptarget-run-generic 2>&1 | %fcheck-generic
+// RUN: %libomptarget-run-generic 2>&1 \
+// RUN: | %fcheck-generic -check-prefixes=CHECK,DEFAULT
//
// RUN: %libomptarget-compile-generic -DVIA_ALWAYS=1
// RUN: env LIBOMPTARGET_TREAT_ATTACH_AUTO_AS_ALWAYS=1 \
-// RUN: %libomptarget-run-generic 2>&1 | %fcheck-generic
+// RUN: %libomptarget-run-generic 2>&1 \
+// RUN: | %fcheck-generic -check-prefixes=CHECK,ALWAYS
// REQUIRES: unified_shared_memory
// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
@@ -69,10 +71,18 @@ int main() {
#pragma omp target enter data map(close, alloc : p[0 : 10])
#endif
+#if VIA_ALWAYS
// EXPECTED: after attach: p == &x[0]
- // CHECK: after attach: p != &x[0]
- // FIXME: the pointer's storage is shared with the original, so attachment wrote
- // the device pointee address into the original p.
+ // ALWAYS: after attach: p != &x[0]
+ // FIXME: here the pointer and the pointee were both already present when the
+ // attachment was prescribed, so neither could be given a different backing at
+ // creation time, and the device address was written into the original p. This
+ // case needs a diagnostic rather than a silent choice.
+#else
+ // The pointee was kept on the host path, because attaching the pointer would
+ // otherwise have written a device address into the original p.
+ // DEFAULT: after attach: p == &x[0]
+#endif
printf("after attach: p %s &x[0]\n", p == &x[0] ? "==" : "!=");
// CHECK: Done!
diff --git a/offload/test/unified_shared_memory/close_attach_pointer_chain.c b/offload/test/unified_shared_memory/close_attach_pointer_chain.c
index 9a73068829e04..4f7e6a42cc86b 100644
--- a/offload/test/unified_shared_memory/close_attach_pointer_chain.c
+++ b/offload/test/unified_shared_memory/close_attach_pointer_chain.c
@@ -70,19 +70,15 @@ int main() {
p2_device = p1->p2;
}
- // EXPECTED: device p1 != &inner
- // CHECK: device p1 == &inner
- // FIXME: p1's storage is shared with the original, so it was never given a
- // device value and still designates the original structure.
+ // The structure was kept on the host path, so p1 designates it there and the
+ // original p1 is unchanged.
+ // CHECK: device p1 == &inner
printf("device p1 %s &inner\n", p1_device == &inner ? "==" : "!=");
- // CHECK: device p1->p2 != &leaf[0]
+ // CHECK: device p1->p2 == &leaf[0]
printf("device p1->p2 %s &leaf[0]\n", p2_device == &leaf[0] ? "==" : "!=");
- // EXPECTED: host: p1 == &inner, inner.p2 == &leaf[0]
- // CHECK: host: p1 == &inner, inner.p2 != &leaf[0]
- // FIXME: the structure's storage is shared with the original, so attachment of
- // its member wrote the device pointee address into the original inner.p2.
+ // CHECK: host: p1 == &inner, inner.p2 == &leaf[0]
printf("host: p1 %s &inner, inner.p2 %s &leaf[0]\n",
p1 == &inner ? "==" : "!=", inner.p2 == &leaf[0] ? "==" : "!=");
diff --git a/offload/test/unified_shared_memory/close_attach_struct_byte_view.c b/offload/test/unified_shared_memory/close_attach_struct_byte_view.c
index a98a5c5d50e75..9c26db0016c95 100644
--- a/offload/test/unified_shared_memory/close_attach_struct_byte_view.c
+++ b/offload/test/unified_shared_memory/close_attach_struct_byte_view.c
@@ -62,10 +62,8 @@ int main() {
// The pointee is newly mapped with close, which triggers the attachment.
#pragma omp target enter data map(close, alloc : s.p[0 : 10])
- // EXPECTED: after attach: s.p == &arr[0]
- // CHECK: after attach: s.p != &arr[0]
- // FIXME: the byte range's storage is shared with the original, so attachment
- // wrote the device pointee address into the original s.p.
+ // The pointee was kept on the host path, so the original s.p is unchanged.
+ // CHECK: after attach: s.p == &arr[0]
printf("after attach: s.p %s &arr[0]\n", s.p == &arr[0] ? "==" : "!=");
// CHECK: Done!
diff --git a/offload/test/unified_shared_memory/close_ptee_struct_member.c b/offload/test/unified_shared_memory/close_ptee_struct_member.c
index aa512bc421a1e..dc4c5cd934cf6 100644
--- a/offload/test/unified_shared_memory/close_ptee_struct_member.c
+++ b/offload/test/unified_shared_memory/close_ptee_struct_member.c
@@ -65,18 +65,17 @@ int main() {
}
}
- // The member pointer was attached to the close buffer.
- // CHECK: in tgt: s.p != &arr[0]
+ // The pointee was kept on the host path, since attaching the member pointer
+ // would otherwise have written a device address into the original s.p.
+ // CHECK: in tgt: s.p == &arr[0]
printf("in tgt: s.p %s &arr[0]\n", p_device == &arr[0] ? "==" : "!=");
// The original member pointer is intact afterwards.
// CHECK: after: s.p == &arr[0]
printf("after: s.p %s &arr[0]\n", s.p == &arr[0] ? "==" : "!=");
- // bar() wrote through the attached member pointer, i.e. into the device
- // buffer for the pointee. That buffer is mapped with alloc, which never
- // assigns, so the value is not copied back.
- // CHECK: arr[0] = 0
+ // bar() wrote through the member pointer, which designates arr itself.
+ // CHECK: arr[0] = 77
printf("arr[0] = %d\n", arr[0]);
// CHECK: Done!
diff --git a/offload/test/unified_shared_memory/close_ptee_two_pointers.c b/offload/test/unified_shared_memory/close_ptee_two_pointers.c
index 4e298d44ec72a..82ffc63444066 100644
--- a/offload/test/unified_shared_memory/close_ptee_two_pointers.c
+++ b/offload/test/unified_shared_memory/close_ptee_two_pointers.c
@@ -57,25 +57,22 @@ int main() {
}
}
- // Both pointers were attached to the close buffer.
- // CHECK: in tgt: p1 != &arr[0], p2 != &arr[0]
+ // p2 has no device storage of its own, so the pointee was kept on the host
+ // path and both pointers designate it there.
+ // CHECK: in tgt: p1 == &arr[0], p2 == &arr[0]
printf(
"in tgt: p1 %s &arr[0], p2 %s &arr[0]\n",
p1_device == &arr[0] ? "==" : "!=", p2_device == &arr[0] ? "==" : "!=");
- // p1 has device storage of its own, so the original is intact.
// CHECK: after: p1 == &arr[0]
printf("after: p1 %s &arr[0]\n", p1 == &arr[0] ? "==" : "!=");
- // EXPECTED: after: p2 == &arr[0]
- // CHECK: after: p2 != &arr[0]
- // FIXME: p2's storage is shared with the original, so attachment wrote the
- // device pointee address into the original p2.
+ // CHECK: after: p2 == &arr[0]
printf("after: p2 %s &arr[0]\n", p2 == &arr[0] ? "==" : "!=");
- // The pointee is mapped with alloc, which never assigns, so the value written
- // into the device buffer is not copied back.
- // CHECK: arr[0] = 0
+ // The pointee stayed on the host path, so the write is to arr itself and
+ // there is no separate buffer for it to be stranded in.
+ // CHECK: arr[0] = 55
printf("arr[0] = %d\n", arr[0]);
// CHECK: Done!
diff --git a/offload/test/unified_shared_memory/close_ptr_ptee_nested.c b/offload/test/unified_shared_memory/close_ptr_ptee_nested.c
index 6453849b2a346..d6d9bcfc2bd85 100644
--- a/offload/test/unified_shared_memory/close_ptr_ptee_nested.c
+++ b/offload/test/unified_shared_memory/close_ptr_ptee_nested.c
@@ -95,9 +95,9 @@ int main() {
// ALL: Before tgt: p == p_host
- // The pointer is attached to the close-allocated device pointee, so its
- // device value differs from the host address.
- // V1: In tgt: p_device != p_host
+ // The pointer has no device storage of its own, so the pointee is kept on the
+ // host path and the pointer designates it there.
+ // V1: In tgt: p_device == p_host
// V1: In tgt: paddr_device == &p_host
// p itself is close-allocated, giving the pointer variable its own device
>From 96f08bf05de56fb964a269ccd8c22140978af868 Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Thu, 6 Aug 2026 19:04:37 -0700
Subject: [PATCH 12/29] [OFFLOAD] Settle attachment storage before attaching,
under USM
Attachment assigns the corresponding pointer. Under unified shared memory a
pointer's corresponding storage is normally the original storage, so attaching
it to a device-resident pointee writes a device address into the original
pointer, which the program is still using. Restoring it when the region ends
does not make that window correct.
Decide which side holds device storage before performing any attachment: a
pointee allocated by this construct gives that allocation up, otherwise a
pointer whose mapping this construct created takes an allocation of its own,
and otherwise the attachment is reported as an error. Only a mapping created
by the current construct may change, since the device address of one that was
already present may already have been obtained by the program.
Deciding this as a pre-pass over all the entries, rather than per-entry while
attaching, keeps the outcome from depending on the order the entries happen to
be in: a pointee could otherwise be attached to through one pointer and only
then demoted for another, stranding the first.
---
offload/include/OpenMP/Mapping.h | 86 +++++++-
offload/libomptarget/OpenMP/Mapping.cpp | 87 ++++++--
offload/libomptarget/omptarget.cpp | 199 ++++++++++++++----
.../close_attach_host_deref.c | 49 ++---
.../close_ptee_attached_host_read.c | 41 ++--
.../close_ptr_ptee_nested.c | 13 +-
.../close_ptr_ptee_samedir.c | 10 +-
7 files changed, 366 insertions(+), 119 deletions(-)
diff --git a/offload/include/OpenMP/Mapping.h b/offload/include/OpenMP/Mapping.h
index 668c8e2c23114..c04ba8fd3123c 100644
--- a/offload/include/OpenMP/Mapping.h
+++ b/offload/include/OpenMP/Mapping.h
@@ -118,8 +118,35 @@ struct HostDataToTargetTy {
const uintptr_t HstPtrEnd; // non-inclusive.
const map_var_info_t HstPtrName; // Optional source name of mapped variable.
- const uintptr_t TgtAllocBegin; // allocated target memory
- const uintptr_t TgtPtrBegin; // mapped target memory = TgtAllocBegin + padding
+ // Not const: an entry created for this construct can be returned to sharing
+ // storage with the original, see shareStorageWithOriginal().
+ uintptr_t TgtAllocBegin; // allocated target memory
+ uintptr_t TgtPtrBegin; // mapped target memory = TgtAllocBegin + padding
+
+ /// Whether this entry's storage is the host storage itself, i.e. it owns no
+ /// device allocation. That is the case for the entries recorded on the
+ /// unified-shared-memory host path.
+ bool isHostBacked() const { return TgtPtrBegin == HstPtrBegin; }
+
+ /// Give this entry a device allocation, so that its storage stops being
+ /// shared with the original. The caller performs the allocation; see
+ /// MappingInfoTy::giveEntryDeviceStorage(), which is the only intended caller
+ /// and documents when this is legal.
+ void takeDeviceStorage(uintptr_t NewTgtAllocBegin, uintptr_t NewTgtPtrBegin) {
+ assert(isHostBacked() && "Entry already owns a device allocation");
+ TgtAllocBegin = NewTgtAllocBegin;
+ TgtPtrBegin = NewTgtPtrBegin;
+ }
+
+ /// Return this entry's storage to being shared with the original. The caller
+ /// releases the device allocation; see
+ /// MappingInfoTy::shareEntryStorageWithOriginal(), which is the only intended
+ /// caller and documents when this is legal.
+ void shareStorageWithOriginal() {
+ assert(!isHostBacked() && "Entry is already shared with the original");
+ TgtAllocBegin = HstPtrBegin;
+ TgtPtrBegin = HstPtrBegin;
+ }
private:
static const uint64_t INFRefCount = ~(uint64_t)0;
@@ -503,13 +530,6 @@ struct StateInfoTy {
/// ATTACH map entries for deferred processing until all other maps are done.
llvm::SmallVector<AttachMapInfo> AttachEntries;
- /// Pointees that this construct will attach a pointer to whose own storage is
- /// shared with the original. Such a pointee has to stay on the host path, so
- /// that attaching the pointer does not write a device address into the
- /// original pointer. Populated before any entry is created, since the device
- /// address of a mapped list item must not change while it is mapped.
- llvm::SmallPtrSet<void *, 4> PointeesToKeepOnHostPath;
-
/// Host pointers for which new device memory was allocated.
/// Key: host pointer, Value: allocation size.
/// Consulted by the 'present' map-type validation.
@@ -673,6 +693,26 @@ struct MappingInfoTy {
/// The type used to access the HDTT map.
using HDTTMapAccessorTy = decltype(HostDataToTargetMap)::AccessorTy;
+ /// Which pointers are attached to a given pointee, keyed by the pointee's
+ /// original address.
+ ///
+ /// Attachment records a shadow pointer on the entry holding the pointer,
+ /// which gives a pointer's pointee but not the reverse. This index provides
+ /// the reverse direction, which is needed to tell whether giving an entry a
+ /// device allocation is permissible: a pointer that is attached to storage
+ /// within that entry designates its current address, so the entry cannot move
+ /// unless that pointer can be attached again.
+ ///
+ /// Only pointers that are actually attached are recorded, so this stays empty
+ /// for mappings that never involve pointer attachment.
+ ///
+ /// Accessed under the HDTT map accessor.
+ llvm::DenseMap<void *, llvm::SmallVector<void **, 2>> AttachedPointers;
+
+ /// Record that \p HstPtrAddr is attached to the pointee at \p HstPteeBegin.
+ /// See AttachedPointers.
+ void recordAttachedPointer(void *HstPteeBegin, void **HstPtrAddr);
+
/// Lookup the mapping of \p HstPtrBegin in \p HDTTMap. The accessor ensures
/// exclusive access to the HDTT map.
LookupResult lookupMapping(HDTTMapAccessorTy &HDTTMap, void *HstPtrBegin,
@@ -727,6 +767,34 @@ struct MappingInfoTy {
[[nodiscard]] int eraseMapEntry(HDTTMapAccessorTy &HDTTMap,
HostDataToTargetTy *Entry, int64_t Size);
+ /// Give \p Entry, whose storage is shared with the original, a device
+ /// allocation and copy the current contents of its storage into it.
+ ///
+ /// Only legal for an entry whose mapping was created by the construct that is
+ /// currently being processed, since its device address cannot have been
+ /// observed yet. \p HDTTMap must be held by the caller.
+ [[nodiscard]] int giveEntryDeviceStorage(HDTTMapAccessorTy &HDTTMap,
+ HostDataToTargetTy *Entry,
+ AsyncInfoTy &AsyncInfo);
+
+ /// Return the storage of \p Entry to being shared with the original,
+ /// releasing the device allocation it was created with.
+ ///
+ /// This is only legal for an entry created by the construct that is currently
+ /// being processed: its device address cannot have been observed yet, whereas
+ /// changing the device address of an entry that was already present would
+ /// invalidate whatever the program obtained for it, from omp_get_mapped_ptr
+ /// or otherwise.
+ ///
+ /// Used when a pointer is to be attached to \p Entry but the pointer's own
+ /// storage is shared with the original: the attachment would then write a
+ /// device address into the original pointer, so the pointee keeps host
+ /// storage instead and the attached address is the original one.
+ ///
+ /// \p HDTTMap must be held by the caller.
+ [[nodiscard]] int shareEntryStorageWithOriginal(HDTTMapAccessorTy &HDTTMap,
+ HostDataToTargetTy *Entry);
+
/// Deallocate the \p Entry from the device memory and delete it. Return \c
/// OFFLOAD_SUCCESS if the deallocation operations executed successfully, and
/// return \c OFFLOAD_FAIL otherwise.
diff --git a/offload/libomptarget/OpenMP/Mapping.cpp b/offload/libomptarget/OpenMP/Mapping.cpp
index f56a9938ef9c6..6d96467cb92f8 100644
--- a/offload/libomptarget/OpenMP/Mapping.cpp
+++ b/offload/libomptarget/OpenMP/Mapping.cpp
@@ -278,19 +278,7 @@ TargetPointerResultTy MappingInfoTy::getTargetPointer(
// change.
(LR.TPR.getEntry() != nullptr &&
(LR.Flags.IsContained || LR.Flags.ExtendsBefore ||
- LR.Flags.ExtendsAfter)) ||
- // A pointer whose storage is shared with the original cannot be
- // attached to a device allocation: the assignment would write a
- // device address into the original pointer. When this construct
- // will attach such a pointer to this storage, keep it on the
- // host path, so that the address attached to the pointer is the
- // original one.
- //
- // Giving the pointer a device allocation instead is not possible
- // here, because it is already mapped and the device address of a
- // mapped list item must not change.
- (StateInfo &&
- StateInfo->PointeesToKeepOnHostPath.contains(HstPtrBegin)))) ||
+ LR.Flags.ExtendsAfter)))) ||
(PM->getRequirements() & OMPX_REQ_AUTO_ZERO_COPY)) {
// If unified shared memory is active, implicitly mapped variables that are
@@ -573,6 +561,79 @@ int MappingInfoTy::eraseMapEntry(HDTTMapAccessorTy &HDTTMap,
return OFFLOAD_SUCCESS;
}
+void MappingInfoTy::recordAttachedPointer(void *HstPteeBegin,
+ void **HstPtrAddr) {
+ auto &Attached = AttachedPointers[HstPteeBegin];
+ if (!llvm::is_contained(Attached, HstPtrAddr))
+ Attached.push_back(HstPtrAddr);
+}
+
+int MappingInfoTy::giveEntryDeviceStorage(HDTTMapAccessorTy &HDTTMap,
+ HostDataToTargetTy *Entry,
+ AsyncInfoTy &AsyncInfo) {
+ assert(Entry && "Trying to allocate for a null entry.");
+ assert(Entry->isHostBacked() && "Entry already owns a device allocation");
+
+ void *HstPtrBegin = reinterpret_cast<void *>(Entry->HstPtrBegin);
+ int64_t Size = Entry->HstPtrEnd - Entry->HstPtrBegin;
+
+ uintptr_t TgtAllocBegin =
+ reinterpret_cast<uintptr_t>(Device.allocData(Size, HstPtrBegin));
+ if (!TgtAllocBegin) {
+ REPORT() << "Failed to allocate device memory for " << HstPtrBegin << ".";
+ return OFFLOAD_FAIL;
+ }
+
+ Entry->takeDeviceStorage(TgtAllocBegin, TgtAllocBegin);
+
+ INFO(OMP_INFOTYPE_MAPPING_CHANGED, Device.DeviceID,
+ "Allocating device memory for map entry with HstPtrBegin=" DPxMOD
+ ", TgtPtrBegin=" DPxMOD ", Size=%" PRId64
+ ", so that its storage is not shared with the original\n",
+ DPxPTR(Entry->HstPtrBegin), DPxPTR(TgtAllocBegin), Size);
+ ODBG(ODT_Mapping) << "Allocating device memory for map entry (hst:"
+ << HstPtrBegin
+ << ") -> (tgt:" << reinterpret_cast<void *>(TgtAllocBegin)
+ << "), so that its storage is not shared with the original";
+
+ // The storage was shared with the host until now, so bring its current
+ // contents over: it may hold data besides the pointer being attached.
+ if (Device.submitData(reinterpret_cast<void *>(TgtAllocBegin), HstPtrBegin,
+ Size, AsyncInfo, Entry) != OFFLOAD_SUCCESS) {
+ REPORT() << "Copying data to device failed.";
+ return OFFLOAD_FAIL;
+ }
+
+ return Device.notifyDataMapped(HstPtrBegin, Size);
+}
+
+int MappingInfoTy::shareEntryStorageWithOriginal(HDTTMapAccessorTy &HDTTMap,
+ HostDataToTargetTy *Entry) {
+ assert(Entry && "Trying to share storage of a null entry.");
+ assert(!Entry->isHostBacked() &&
+ "Entry storage is already shared with the original");
+
+ void *HstPtrBegin = reinterpret_cast<void *>(Entry->HstPtrBegin);
+ int64_t Size = Entry->HstPtrEnd - Entry->HstPtrBegin;
+ void *TgtAllocBegin = reinterpret_cast<void *>(Entry->TgtAllocBegin);
+
+ INFO(OMP_INFOTYPE_MAPPING_CHANGED, Device.DeviceID,
+ "Releasing the device allocation of map entry with HstPtrBegin=" DPxMOD
+ ", TgtPtrBegin=" DPxMOD ", Size=%" PRId64
+ ", so that its storage is shared with the original\n",
+ DPxPTR(Entry->HstPtrBegin), DPxPTR(Entry->TgtPtrBegin), Size);
+ ODBG(ODT_Mapping) << "Releasing the device allocation of map entry (hst:"
+ << HstPtrBegin << ") -> (tgt:" << TgtAllocBegin
+ << "), so that its storage is shared with the original";
+
+ Entry->shareStorageWithOriginal();
+
+ if (Device.notifyDataUnmapped(HstPtrBegin))
+ return OFFLOAD_FAIL;
+
+ return Device.deleteData(TgtAllocBegin);
+}
+
int MappingInfoTy::deallocTgtPtrAndEntry(HostDataToTargetTy *Entry,
int64_t Size) {
assert(Entry && "Trying to deallocate a null entry.");
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index d742ddad319b5..2c3d939f398a7 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -453,6 +453,11 @@ static int performPointerAttachment(DeviceTy &Device, AsyncInfoTy &AsyncInfo,
void *TgtPteeBase =
calculateTargetPointeeBase(HstPteeBase, HstPteeBegin, TgtPteeBegin);
+ // Record the pointer against its pointee, so that it can be told later
+ // whether the pointee's entry may take a device allocation. Recorded even
+ // when the shadow pointer below turns out to be a duplicate.
+ Device.getMappingInfo().recordAttachedPointer(HstPteeBegin, HstPtrAddr);
+
// Add shadow pointer tracking
if (!PtrTPR.getEntry()->addShadowPointer(
ShadowPtrInfoTy{HstPtrAddr, TgtPtrAddr, TgtPteeBase, HstPtrSize})) {
@@ -516,49 +521,6 @@ int targetDataBegin(ident_t *Loc, DeviceTy &Device, int32_t ArgNum,
assert(StateInfo && "StateInfo must be available for targetDataBegin for "
"handling ATTACH and TO/TOFROM map-types.");
- // Whether an entry's storage is shared with the original can only be decided
- // when the entry is created, since the device address of a mapped list item
- // must not change while it is mapped. So before creating anything, find the
- // pointees that this construct will attach a pointer to whose own storage is
- // shared with the original: giving such a pointee a device allocation would
- // mean writing a device address into the original pointer.
- //
- // Only the map types are inspected here, so this costs nothing for constructs
- // that prescribe no attachment.
- for (int32_t I = 0; I < ArgNum; ++I) {
- if (!(ArgTypes[I] & OMP_TGT_MAPTYPE_ATTACH) ||
- (ArgTypes[I] & OMP_TGT_MAPTYPE_PRIVATE))
- continue;
-
- void **HstPtr = reinterpret_cast<void **>(ArgsBase[I]);
- void *HstPteeBegin = Args[I];
-
- // The pointee only has to stay on the host path if the pointer will really
- // be attached, and if doing so would write into storage shared with the
- // original. A pointer that is not mapped at all is not attached, and one
- // that has a device allocation of its own can be attached without touching
- // the original, so neither places any requirement on the pointee.
- MappingInfoTy::HDTTMapAccessorTy HDTTMap =
- Device.getMappingInfo().HostDataToTargetMap.getExclusiveAccessor();
- LookupResult LR =
- Device.getMappingInfo().lookupMapping(HDTTMap, HstPtr, sizeof(void *));
- const bool PtrIsMapped = LR.Flags.IsContained && LR.TPR.getEntry();
- const bool PtrIsHostBacked =
- PtrIsMapped &&
- LR.TPR.getEntry()->TgtPtrBegin == LR.TPR.getEntry()->HstPtrBegin;
- LR.TPR.setEntry(nullptr);
- HDTTMap.destroy();
-
- if (!PtrIsHostBacked)
- continue;
-
- ODBG(ODT_Mapping) << "Pointee " << HstPteeBegin
- << " will have a pointer attached to it whose storage is "
- "shared with the original, so it has to stay on the "
- "host path";
- StateInfo->PointeesToKeepOnHostPath.insert(HstPteeBegin);
- }
-
// process each input.
for (int32_t I = 0; I < ArgNum; ++I) {
// Ignore private variables and arrays - there is no mapping for them.
@@ -854,6 +816,138 @@ int targetDataBegin(ident_t *Loc, DeviceTy &Device, int32_t ArgNum,
///
/// For this purpose, we insert a data_fence before the first
/// pointer-attachment, (3), to ensure that all pending transfers finish first.
+/// Settle which side of each pending attachment holds device storage, before
+/// any attachment is performed.
+///
+/// Attachment assigns the corresponding pointer. If the storage holding the
+/// pointer is shared with the original then that assignment writes a device
+/// address into the original pointer, which the program is still using: the
+/// host cannot dereference it while the attachment lasts, and another task
+/// reading it concurrently sees a device address. Restoring it at the end of
+/// the region does not make that window correct.
+///
+/// One of the two sides has to change its storage so that this does not happen,
+/// and only a mapping created by this construct can, since the device address
+/// of a mapping that was already present may have been obtained by the program.
+/// So, for each pending attachment whose pointer is shared with the original:
+///
+/// - if the pointee's storage was allocated for this construct, it gives that
+/// allocation up and shares storage with the original, which makes the
+/// address to be attached the original one;
+/// - otherwise, if the pointer's mapping was created by this construct, the
+/// pointer takes a device allocation, so that the assignment does not reach
+/// the original pointer;
+/// - otherwise neither side can change, and processAttachEntries reports it.
+///
+/// This runs as a pre-pass, over all the entries, for two reasons.
+///
+/// Giving up an allocation moves the pointee's device address, which
+/// invalidates any pointer already attached to it. Deciding per-entry while
+/// interleaving attachments would make the outcome depend on the order the
+/// entries happen to be in: a pointee could be attached to through one pointer
+/// and only then be demoted for another, stranding the first. Settling every
+/// entry first means a demotion cannot strand an attachment, so it never has to
+/// be undone.
+///
+/// The demotions are also all applied before any upgrade is considered. A
+/// demotion removes the very disparity that would make the other side need an
+/// upgrade, so this avoids giving a pointer a device allocation that a
+/// subsequent demotion would have made unnecessary.
+static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
+ AsyncInfoTy &AsyncInfo) {
+ MappingInfoTy &MappingInfo = Device.getMappingInfo();
+ MappingInfoTy::HDTTMapAccessorTy HDTTMap =
+ MappingInfo.HostDataToTargetMap.getExclusiveAccessor();
+
+ auto FindEntry = [&](void *HstPtrBegin,
+ int64_t Size) -> HostDataToTargetTy * {
+ LookupResult LR = MappingInfo.lookupMapping(HDTTMap, HstPtrBegin, Size);
+ HostDataToTargetTy *Entry =
+ LR.Flags.IsContained ? LR.TPR.getEntry() : nullptr;
+ // The result holds a lock on the entry until it is destroyed, and the
+ // callers below need to look the same entry up again.
+ LR.TPR.setEntry(nullptr);
+ return Entry;
+ };
+
+ // An attachment only needs settling if the pointer is shared with the
+ // original, and the pointee has device storage for its address to differ.
+ auto NeedsSettling = [&](void *HstPtr, int64_t PtrSize,
+ void *HstPteeBegin) -> bool {
+ HostDataToTargetTy *PtrEntry = FindEntry(HstPtr, PtrSize);
+ if (!PtrEntry || !PtrEntry->isHostBacked())
+ return false;
+ HostDataToTargetTy *PteeEntry = FindEntry(HstPteeBegin, /*Size=*/0);
+ return PteeEntry && !PteeEntry->isHostBacked();
+ };
+
+ for (const auto &AttachEntry : StateInfo.AttachEntries) {
+ void **HstPtr = reinterpret_cast<void **>(AttachEntry.PointerBase);
+ void *HstPteeBegin = AttachEntry.PointeeBegin;
+ if (!NeedsSettling(HstPtr, AttachEntry.PointerSize, HstPteeBegin))
+ continue;
+
+ // Only a pointee allocated for this construct may give its allocation up.
+ if (!StateInfo.wasNewlyAllocated(HstPteeBegin).has_value())
+ continue;
+
+ HostDataToTargetTy *PteeEntry = FindEntry(HstPteeBegin, /*Size=*/0);
+ if (MappingInfo.shareEntryStorageWithOriginal(HDTTMap, PteeEntry) !=
+ OFFLOAD_SUCCESS)
+ return OFFLOAD_FAIL;
+ }
+
+ for (const auto &AttachEntry : StateInfo.AttachEntries) {
+ void **HstPtr = reinterpret_cast<void **>(AttachEntry.PointerBase);
+ if (!NeedsSettling(HstPtr, AttachEntry.PointerSize,
+ AttachEntry.PointeeBegin))
+ continue;
+
+ // Give the pointer, and whatever points at it, device storage. A pointer
+ // attached to storage within an entry designates that entry's current
+ // address, so an entry cannot take a device allocation while such a pointer
+ // is itself unable to hold a device address.
+ SmallVector<std::pair<void **, int64_t>> ToUpgrade;
+ ToUpgrade.emplace_back(HstPtr, AttachEntry.PointerSize);
+
+ while (!ToUpgrade.empty()) {
+ auto [UpHstPtr, UpPtrSize] = ToUpgrade.pop_back_val();
+
+ HostDataToTargetTy *UpEntry = FindEntry(UpHstPtr, UpPtrSize);
+ if (!UpEntry || !UpEntry->isHostBacked())
+ continue;
+
+ if (!StateInfo.wasNewlyMapped(UpHstPtr).has_value()) {
+ MESSAGE("pointer " DPxMOD " cannot be attached to " DPxMOD
+ ": its corresponding pointer is the original pointer, and "
+ "neither it nor the pointee could be given storage that would "
+ "make the attachment possible\n",
+ DPxPTR(HstPtr), DPxPTR(AttachEntry.PointeeBegin));
+ REPORT() << "Storage at " << UpHstPtr
+ << " was already mapped with its original storage, so it "
+ "cannot be given a device allocation now.";
+ return OFFLOAD_FAIL;
+ }
+
+ if (MappingInfo.giveEntryDeviceStorage(HDTTMap, UpEntry, AsyncInfo) !=
+ OFFLOAD_SUCCESS)
+ return OFFLOAD_FAIL;
+
+ // Anything attached to storage within the entry just moved, so it has to
+ // be able to hold a device address too.
+ for (auto &[AttachedPtee, AttachedPtrs] : MappingInfo.AttachedPointers) {
+ if (reinterpret_cast<uintptr_t>(AttachedPtee) < UpEntry->HstPtrBegin ||
+ reinterpret_cast<uintptr_t>(AttachedPtee) >= UpEntry->HstPtrEnd)
+ continue;
+ for (void **AttachedPtr : AttachedPtrs)
+ ToUpgrade.emplace_back(AttachedPtr, sizeof(void *));
+ }
+ }
+ }
+
+ return OFFLOAD_SUCCESS;
+}
+
int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
AsyncInfoTy &AsyncInfo) {
// Report all tracked allocations from both main loop and ATTACH processing
@@ -874,6 +968,11 @@ int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
ODBG(ODT_Mapping) << "Processing " << StateInfo.AttachEntries.size()
<< " deferred ATTACH map entries";
+ // Decide which side of each attachment holds device storage before performing
+ // any of them, so that a demotion cannot strand an earlier attachment.
+ if (settleAttachStorage(Device, StateInfo, AsyncInfo) != OFFLOAD_SUCCESS)
+ return OFFLOAD_FAIL;
+
bool TreatAttachAutoAsAlways = MappingConfig::get().TreatAttachAutoAsAlways;
if (TreatAttachAutoAsAlways)
ODBG(ODT_Mapping) << "Treating ATTACH(auto) as ATTACH(always) because "
@@ -974,6 +1073,20 @@ int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
/*AllowHostPointer=*/true);
if (!PtrTPROpt)
continue;
+
+ // The storage on both sides was settled by settleAttachStorage, so an
+ // attachment that would write a device address into an original pointer is
+ // one that could not be settled at all.
+ if (PtrTPROpt->getEntry() && PtrTPROpt->getEntry()->isHostBacked() &&
+ TgtPteeBegin != HstPteeBegin) {
+ MESSAGE("pointer " DPxMOD " cannot be attached to " DPxMOD
+ ": its corresponding pointer is the original pointer, so the "
+ "attachment would assign a device address to the original "
+ "pointer\n",
+ DPxPTR(HstPtr), DPxPTR(HstPteeBegin));
+ return OFFLOAD_FAIL;
+ }
+
TargetPointerResultTy &PtrTPR = *PtrTPROpt;
void **TgtPtrBase = reinterpret_cast<void **>(PtrTPR.TargetPointer);
diff --git a/offload/test/unified_shared_memory/close_attach_host_deref.c b/offload/test/unified_shared_memory/close_attach_host_deref.c
index 6957c5acba944..286bb35f1de4f 100644
--- a/offload/test/unified_shared_memory/close_attach_host_deref.c
+++ b/offload/test/unified_shared_memory/close_attach_host_deref.c
@@ -4,8 +4,8 @@
//
// RUN: %libomptarget-compile-generic -DVIA_ALWAYS=1
// RUN: env LIBOMPTARGET_TREAT_ATTACH_AUTO_AS_ALWAYS=1 \
-// RUN: %libomptarget-run-generic 2>&1 \
-// RUN: | %fcheck-generic -check-prefixes=CHECK,ALWAYS
+// RUN: %libomptarget-run-fail-generic 2>&1 \
+// RUN: | %fcheck-generic -check-prefixes=ALWAYS
// REQUIRES: unified_shared_memory
// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
@@ -26,18 +26,23 @@
// into the corresponding pointer -- which is the original pointer. A host
// dereference afterwards goes through the device address.
//
-// Two orderings are covered. In the default configuration the pointee is mapped
-// with close after the pointer, so the attachment is triggered by the pointee
-// becoming newly mapped. With VIA_ALWAYS the pointee already has device storage
-// and the attachment is triggered separately, by a map of the zero-length array
-// section under LIBOMPTARGET_TREAT_ATTACH_AUTO_AS_ALWAYS (OpenMP 6.0 has no
-// attach map-type-modifier for C/C++, so the environment variable stands in for
-// attach(always)).
+// Two orderings are covered, and they differ in whether the situation can be
+// resolved at all.
+//
+// In the default configuration the pointee is mapped with close after the
+// pointer, so its allocation was made by that construct and can be given up:
+// the pointee returns to sharing storage with the original, the attached address
+// is the original one, and the original p is left alone.
//
-// FIXME: the value checked below is the one produced today; the expected value
-// is given alongside it. Reading x[0] through p is the natural thing for a
-// program to do here, and it does not modify the pointer, so nothing in the
-// OpenMP 6.0 map clause restrictions forbids it.
+// With VIA_ALWAYS the pointee already has device storage and the attachment is
+// triggered separately, by a map of the zero-length array section under
+// LIBOMPTARGET_TREAT_ATTACH_AUTO_AS_ALWAYS (OpenMP 6.0 has no attach
+// map-type-modifier for C/C++, so the environment variable stands in for
+// attach(always)). Both sides were already present, so neither can change its
+// storage now: the pointee's allocation may already have been handed to the
+// program, and so may p's device address. There is no way to attach without
+// assigning a device address to the original p, so the runtime reports it
+// instead of doing so silently.
#include <stdio.h>
@@ -64,6 +69,11 @@ int main() {
// triggered on its own.
#pragma omp target enter data map(alloc : p)
#pragma omp target enter data map(close, alloc : x[0 : 10])
+ //
+ // clang-format off
+ // ALWAYS: cannot be attached to
+ // ALWAYS: was already mapped with its original storage
+ // clang-format on
#pragma omp target enter data map(alloc : p[0 : 0])
#else
// The pointee is newly mapped with close, which triggers the attachment.
@@ -71,21 +81,12 @@ int main() {
#pragma omp target enter data map(close, alloc : p[0 : 10])
#endif
-#if VIA_ALWAYS
- // EXPECTED: after attach: p == &x[0]
- // ALWAYS: after attach: p != &x[0]
- // FIXME: here the pointer and the pointee were both already present when the
- // attachment was prescribed, so neither could be given a different backing at
- // creation time, and the device address was written into the original p. This
- // case needs a diagnostic rather than a silent choice.
-#else
- // The pointee was kept on the host path, because attaching the pointer would
+ // The pointee gave its allocation up, because attaching the pointer would
// otherwise have written a device address into the original p.
// DEFAULT: after attach: p == &x[0]
-#endif
printf("after attach: p %s &x[0]\n", p == &x[0] ? "==" : "!=");
- // CHECK: Done!
+ // DEFAULT: Done!
printf("Done!\n");
return 0;
}
diff --git a/offload/test/unified_shared_memory/close_ptee_attached_host_read.c b/offload/test/unified_shared_memory/close_ptee_attached_host_read.c
index ab9d2a201006b..862d376d42155 100644
--- a/offload/test/unified_shared_memory/close_ptee_attached_host_read.c
+++ b/offload/test/unified_shared_memory/close_ptee_attached_host_read.c
@@ -11,32 +11,30 @@
// UNSUPPORTED: intelgpu
// The pointee already has device storage before the pointers are mapped, so no
-// allocation happens for it on the inner construct and there is nothing to
-// undo: the pointee is simply already on the device.
+// allocation happens for it on the inner construct and there is nothing for the
+// pointee to give up: it is simply already on the device.
//
-// p2 is mapped without close, so its corresponding storage is the original
-// storage and attachment writes the device pointee address into the original
-// p2. The host can then observe that value for as long as p2 remains attached,
-// which under OpenMP 6.0 lasts until the pointer's storage is removed from the
-// device data environment -- there is no detachment.
+// p2 is mapped without close, so its corresponding storage would be the original
+// storage, and attaching it would write the device pointee address into the
+// original p2. The host could then observe that value for as long as p2 remained
+// attached, which under OpenMP 6.0 lasts until the pointer's storage is removed
+// from the device data environment -- there is no detachment.
//
-// This case is therefore not fixed by declining to allocate the pointee: it is
-// about attachment through shared storage, not about the close allocation.
+// This case is therefore not resolved from the pointee's side. It is resolved
+// from the pointer's side: p2's own mapping is created by this construct, so p2
+// can be given a device allocation of its own, and the assignment then reaches
+// that instead of the original p2.
//
// Note what OpenMP 6.0 does and does not say here. Attachment assigns the
// corresponding pointer (7.9.6), and the corresponding storage may share
-// storage with the original (7.9.6, 1.3.2), in which case the assignment is
-// observable on the host. Nothing preserves the original value during that
+// storage with the original (7.9.6, 1.3.2), in which case the assignment would
+// be observable on the host. Nothing preserves the original value during that
// window, and nothing restores it: the map-exiting sequence has no detach step.
// The one place the specification confronts the same situation, for self maps,
// requires runtime error termination when "the list item is a pointer that
// would be assigned a different value as a result of pointer attachment"
-// (7.9.6), which suggests the intent is for this configuration not to arise
-// rather than for the host value to be preserved.
-//
-// FIXME: the values checked below are the ones produced today; the expected
-// value is given alongside each, on the reading that a program should be able
-// to use the original pointer while it happens to be attached.
+// (7.9.6), which indicates the intent is for this configuration not to arise --
+// so the implementation has to keep it from arising.
#include <stdio.h>
@@ -72,9 +70,8 @@ int main() {
// CHECK: inside: p1 == &arr[0]
printf("inside: p1 %s &arr[0]\n", p1 == &arr[0] ? "==" : "!=");
- // EXPECTED: inside: p2 == &arr[0]
- // CHECK: inside: p2 != &arr[0]
- // FIXME: the original p2 holds the device pointee address here.
+ // p2 was given device storage of its own, so its original is unaffected too.
+ // CHECK: inside: p2 == &arr[0]
printf("inside: p2 %s &arr[0]\n", p2 == &arr[0] ? "==" : "!=");
}
@@ -86,9 +83,7 @@ int main() {
// CHECK: after: p1 == &arr[0]
printf("after: p1 %s &arr[0]\n", p1 == &arr[0] ? "==" : "!=");
- // EXPECTED: after: p2 == &arr[0]
- // CHECK: after: p2 != &arr[0]
- // FIXME: nothing restores the original p2.
+ // CHECK: after: p2 == &arr[0]
printf("after: p2 %s &arr[0]\n", p2 == &arr[0] ? "==" : "!=");
// CHECK: Done!
diff --git a/offload/test/unified_shared_memory/close_ptr_ptee_nested.c b/offload/test/unified_shared_memory/close_ptr_ptee_nested.c
index d6d9bcfc2bd85..a25baf73262b6 100644
--- a/offload/test/unified_shared_memory/close_ptr_ptee_nested.c
+++ b/offload/test/unified_shared_memory/close_ptr_ptee_nested.c
@@ -106,12 +106,15 @@ int main() {
// V2: In tgt: p_device == p_host
// V2: In tgt: paddr_device != &p_host
- // The close pointee (mapped by the enclosing region) has a device
- // allocation, and p -- newly mapped on the USM host path by the inner region
- // -- is attached to it, so its device value differs from the host address
- // while the pointer variable itself stays on the host path (&p matches).
+ // The close pointee (mapped by the enclosing region) has a device allocation,
+ // and p is newly mapped by the inner region. Attaching p to that allocation
+ // would assign a device address to the original p, since USM otherwise shares
+ // its storage; the pointee cannot give the allocation up, because it was made
+ // by the enclosing region and may already have been handed to the program. So
+ // p, whose mapping this construct did create, is given device storage of its
+ // own (&p differs), and the attachment writes into that.
// V3: In tgt: p_device != p_host
- // V3: In tgt: paddr_device == &p_host
+ // V3: In tgt: paddr_device != &p_host
// The host pointer must be intact afterwards and the kernel's write must be
// visible on the host.
diff --git a/offload/test/unified_shared_memory/close_ptr_ptee_samedir.c b/offload/test/unified_shared_memory/close_ptr_ptee_samedir.c
index 59464c7dd5e00..48f6e49bcd29c 100644
--- a/offload/test/unified_shared_memory/close_ptr_ptee_samedir.c
+++ b/offload/test/unified_shared_memory/close_ptr_ptee_samedir.c
@@ -82,8 +82,14 @@ int main() {
// ALL: Before tgt: p == p_host
- // The close pointee is device-allocated and p (host path) is attached to it.
- // V1: In tgt: p_device != p_host
+ // p shares its storage with the original under USM, so attaching it to a
+ // device-allocated pointee would assign a device address to the original p.
+ // The pointee's allocation was made by this construct, so it gives it up and
+ // shares storage with the original instead: the attached value is then the
+ // host address, and p itself needs no storage of its own (&p matches). The
+ // close request on the pointee is not honoured, which it need not be -- close
+ // is a hint, and correctness of the original pointer takes priority.
+ // V1: In tgt: p_device == p_host
// V1: In tgt: paddr_device == &p_host
// p is close-allocated (own device storage, so &p differs); the pointee is a
>From 49ebcdf4fe6b2dfa41098b1e6fd490b76ee34588 Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Thu, 6 Aug 2026 19:07:46 -0700
Subject: [PATCH 13/29] [OFFLOAD] Assert that demotion cannot strand an
attached pointer
---
offload/libomptarget/omptarget.cpp | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index 2c3d939f398a7..124a64d23e833 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -892,6 +892,21 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
continue;
HostDataToTargetTy *PteeEntry = FindEntry(HstPteeBegin, /*Size=*/0);
+#ifndef NDEBUG
+ // Giving the allocation up moves the pointee's device address, which would
+ // strand any pointer already attached to it. That cannot happen here: the
+ // allocation was made by this construct, so no earlier construct could have
+ // attached to it, and this pre-pass runs before any attachment is performed
+ // for this one.
+ for (auto &[AttachedPtee, AttachedPtrs] : MappingInfo.AttachedPointers) {
+ (void)AttachedPtrs;
+ assert((reinterpret_cast<uintptr_t>(AttachedPtee) <
+ PteeEntry->HstPtrBegin ||
+ reinterpret_cast<uintptr_t>(AttachedPtee) >=
+ PteeEntry->HstPtrEnd) &&
+ "Releasing an allocation that a pointer is already attached to");
+ }
+#endif
if (MappingInfo.shareEntryStorageWithOriginal(HDTTMap, PteeEntry) !=
OFFLOAD_SUCCESS)
return OFFLOAD_FAIL;
>From dbd8c683208570a25ccee9e6e34fc640e459692c Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Sun, 9 Aug 2026 17:19:42 -0700
Subject: [PATCH 14/29] [OFFLOAD] Cascade the attach upgrade through pending
attachments
Giving a pointer device storage moves that pointer's own address, so whatever
points at it has to be able to hold a device address too. The walk backwards
only consulted the attachments already recorded from earlier constructs, but
the attachments the current construct is about to perform have the same
consequence and are not recorded yet, so a chain mapped on a single construct
stopped cascading after one level and then reported a failure it could have
settled.
Consider the pending entries as well, and add a five-level pointer chain test
covering which end of the chain pre-exists and how, including the conflicts
that originate from a level in the middle.
---
offload/libomptarget/omptarget.cpp | 20 +-
.../close_attach_pointer_chain5.c | 283 ++++++++++++++++++
2 files changed, 300 insertions(+), 3 deletions(-)
create mode 100644 offload/test/unified_shared_memory/close_attach_pointer_chain5.c
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index 124a64d23e833..4c3800d1fa05b 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -949,14 +949,28 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
return OFFLOAD_FAIL;
// Anything attached to storage within the entry just moved, so it has to
- // be able to hold a device address too.
+ // be able to hold a device address too. That covers both the attachments
+ // already in place from earlier constructs and the ones this construct is
+ // about to perform, which are not recorded yet.
+ auto IsInsideUpEntry = [&](void *HstPteeBegin) {
+ return reinterpret_cast<uintptr_t>(HstPteeBegin) >=
+ UpEntry->HstPtrBegin &&
+ reinterpret_cast<uintptr_t>(HstPteeBegin) < UpEntry->HstPtrEnd;
+ };
+
for (auto &[AttachedPtee, AttachedPtrs] : MappingInfo.AttachedPointers) {
- if (reinterpret_cast<uintptr_t>(AttachedPtee) < UpEntry->HstPtrBegin ||
- reinterpret_cast<uintptr_t>(AttachedPtee) >= UpEntry->HstPtrEnd)
+ if (!IsInsideUpEntry(AttachedPtee))
continue;
for (void **AttachedPtr : AttachedPtrs)
ToUpgrade.emplace_back(AttachedPtr, sizeof(void *));
}
+
+ for (const auto &Pending : StateInfo.AttachEntries) {
+ if (!IsInsideUpEntry(Pending.PointeeBegin))
+ continue;
+ ToUpgrade.emplace_back(reinterpret_cast<void **>(Pending.PointerBase),
+ Pending.PointerSize);
+ }
}
}
diff --git a/offload/test/unified_shared_memory/close_attach_pointer_chain5.c b/offload/test/unified_shared_memory/close_attach_pointer_chain5.c
new file mode 100644
index 0000000000000..5e88c3c8670f1
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_attach_pointer_chain5.c
@@ -0,0 +1,283 @@
+// A five-level pointer chain, p1 -> p2 -> p3 -> p4 -> p5 -> x, where the
+// intermediate levels are all mapped and attached on one inner construct while
+// the ends may already be mapped from earlier ones.
+//
+// Under unified shared memory a mapping's corresponding storage is normally the
+// original storage, so attaching a pointer would assign a device address to the
+// original pointer. One side has to hold device storage instead, and only a
+// mapping created by the current construct may change: the device address of one
+// that was already present may already have been obtained by the program.
+//
+// The chain makes the two directions visible, because a decision at one level
+// propagates to its neighbours:
+//
+// - giving a pointee's allocation up (demotion) removes the disparity at that
+// level outright, and needs nothing from the rest of the chain;
+// - giving a pointer device storage (upgrade) moves that pointer's own address,
+// so whatever points at it must be able to hold a device address too, which
+// walks backwards towards the root -- including through attachments this
+// construct has not performed yet.
+//
+// So a device-backed leaf drags the whole chain to device storage, while a
+// device-backed root needs nothing of the levels below it. When both ends are
+// already present and pull in opposite directions, neither can move and the
+// runtime reports it rather than assigning a device address to an original
+// pointer.
+//
+// The scenarios below cover each combination of which end pre-exists and how,
+// and then the same conflicts originating from a level in the middle.
+//
+// RUN: %libomptarget-compile-generic -DS=1
+// RUN: %libomptarget-run-fail-generic 2>&1 | %fcheck-generic -check-prefixes=S1
+//
+// RUN: %libomptarget-compile-generic -DS=2
+// RUN: %libomptarget-run-generic 2>&1 | %fcheck-generic -check-prefixes=S2,OK
+//
+// RUN: %libomptarget-compile-generic -DS=3
+// RUN: %libomptarget-run-generic 2>&1 | %fcheck-generic -check-prefixes=S3,OK
+//
+// RUN: %libomptarget-compile-generic -DS=4
+// RUN: %libomptarget-run-generic 2>&1 | %fcheck-generic -check-prefixes=S4,OK
+//
+// RUN: %libomptarget-compile-generic -DS=5
+// RUN: %libomptarget-run-generic 2>&1 | %fcheck-generic -check-prefixes=S5,OK
+//
+// RUN: %libomptarget-compile-generic -DS=6
+// RUN: %libomptarget-run-generic 2>&1 | %fcheck-generic -check-prefixes=S6,OK
+//
+// RUN: %libomptarget-compile-generic -DS=7
+// RUN: %libomptarget-run-generic 2>&1 | %fcheck-generic -check-prefixes=S7,OK
+//
+// RUN: %libomptarget-compile-generic -DS=8
+// RUN: %libomptarget-run-generic 2>&1 | %fcheck-generic -check-prefixes=S8,OK
+//
+// RUN: %libomptarget-compile-generic -DS=9
+// RUN: %libomptarget-run-fail-generic 2>&1 | %fcheck-generic -check-prefixes=S9
+//
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+//
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+#include <stdio.h>
+
+#pragma omp requires unified_shared_memory
+
+// p1 -> p2 -> p3 -> p4 -> p5 -> x
+int x[10];
+int *p5 = &x[0];
+int **p4 = &p5;
+int ***p3 = &p4;
+int ****p2 = &p3;
+int *****p1 = &p2;
+
+void *dev_addr[5];
+void *dev_val[5];
+void *dev_chain_leaf;
+int dev_read;
+
+int main(void) {
+ for (int i = 0; i < 10; ++i)
+ x[i] = 42;
+
+#pragma omp target
+ {
+ }
+
+ // ---- pre-existing mappings ----
+#if S == 1
+ // x device-backed (close), p1 host-backed. Both already present.
+#pragma omp target enter data map(close, to : x[0 : 10])
+#pragma omp target enter data map(alloc : p1)
+#elif S == 2
+ // reverse: p1 device-backed (close), x host-backed.
+#pragma omp target enter data map(close, alloc : p1)
+#pragma omp target enter data map(to : x[0 : 10])
+#elif S == 3
+ // only the leaf x exists, device-backed.
+#pragma omp target enter data map(close, to : x[0 : 10])
+#elif S == 4
+ // only the root p1 exists, host-backed.
+#pragma omp target enter data map(alloc : p1)
+#elif S == 5
+ // only the root p1 exists, device-backed.
+#pragma omp target enter data map(close, alloc : p1)
+#elif S == 6
+ // both ends exist and are device-backed.
+#pragma omp target enter data map(close, to : x[0 : 10])
+#pragma omp target enter data map(close, alloc : p1)
+#elif S == 7
+ // a middle level pre-exists, device-backed: p3's storage holds the pointer p3,
+ // so this is the pointer that p2 will be attached to.
+#pragma omp target enter data map(close, alloc : p3)
+#elif S == 8
+ // a middle level pre-exists sharing storage with the original.
+#pragma omp target enter data map(alloc : p3)
+#elif S == 9
+ // the leaf is device-backed and a middle level pre-exists sharing storage with
+ // the original, so the upgrade walking up from the leaf runs into it.
+#pragma omp target enter data map(close, to : x[0 : 10])
+#pragma omp target enter data map(alloc : p3)
+#endif
+
+ // ---- inner: all intermediate levels mapped + all attachments ----
+#if S == 4 || S == 5 || S == 7 || S == 8
+ // x is not pre-mapped here, so close on the leaf makes it newly device-backed.
+#pragma omp target data map(alloc : p1) \
+ map(alloc : p1[0 : 1], p2[0 : 1], p3[0 : 1], p4[0 : 1]) \
+ map(close, to : p5[0 : 10])
+#else
+#pragma omp target data map(alloc : p1) \
+ map(alloc : p1[0 : 1], p2[0 : 1], p3[0 : 1], p4[0 : 1]) \
+ map(alloc : p5[0 : 10])
+#endif
+ {
+#pragma omp target map(present, alloc : p1, p2, p3, p4, p5) \
+ map(from : dev_addr, dev_val, dev_chain_leaf, dev_read)
+ {
+ dev_addr[0] = (void *)&p1;
+ dev_addr[1] = (void *)&p2;
+ dev_addr[2] = (void *)&p3;
+ dev_addr[3] = (void *)&p4;
+ dev_addr[4] = (void *)&p5;
+ dev_val[0] = (void *)p1;
+ dev_val[1] = (void *)p2;
+ dev_val[2] = (void *)p3;
+ dev_val[3] = (void *)p4;
+ dev_val[4] = (void *)p5;
+ // reach the leaf through the whole chain
+ dev_chain_leaf = (void *)(****p1);
+ dev_read = *****p1;
+ *****p1 = 777;
+ }
+ }
+
+ void *host_addr[5] = {(void *)&p1, (void *)&p2, (void *)&p3, (void *)&p4,
+ (void *)&p5};
+ void *want_val[5] = {(void *)&p2, (void *)&p3, (void *)&p4, (void *)&p5,
+ (void *)&x[0]};
+ void *have_val[5] = {(void *)p1, (void *)p2, (void *)p3, (void *)p4,
+ (void *)p5};
+ const char *nm[5] = {"p1", "p2", "p3", "p4", "p5"};
+
+ printf("scenario %d\n", S);
+ for (int i = 0; i < 5; ++i)
+ printf(" %s: storage=%-6s devval=%-9s host_restored=%s\n", nm[i],
+ dev_addr[i] == host_addr[i] ? "shared" : "device",
+ dev_val[i] == want_val[i] ? "host-addr" : "device-addr",
+ have_val[i] == want_val[i] ? "yes" : "NO");
+ printf(" chain: %s\n",
+ dev_chain_leaf == dev_val[4] ? "consistent" : "STRANDED");
+ printf(" dev_read=%d (want 42) x[0]=%d (want 777)\n", dev_read, x[0]);
+
+ // Both ends are already present and pull in opposite directions: the leaf x is
+ // device-backed so it cannot give its allocation up, which forces every level
+ // up to p1 onto device storage, but p1 is already present sharing storage with
+ // the original and so cannot take an allocation now.
+ //
+ // clang-format off
+ // S1: cannot be attached to
+ // S1: was already mapped with its original storage
+ // clang-format on
+
+ // p1 is already device-backed, so the attachment into it does not touch an
+ // original pointer, and x shares storage with the original, so no level below
+ // needs to change.
+ // S2: scenario 2
+ // S2: p1: storage=device devval=host-addr host_restored=yes
+ // S2: p2: storage=shared devval=host-addr host_restored=yes
+ // S2: p3: storage=shared devval=host-addr host_restored=yes
+ // S2: p4: storage=shared devval=host-addr host_restored=yes
+ // S2: p5: storage=shared devval=host-addr host_restored=yes
+
+ // Only the leaf pre-exists, device-backed. It cannot be demoted, so p5 must
+ // take device storage, and that walks backwards through the pending
+ // attachments to every level up to p1 -- all of which this construct maps, so
+ // all of them can.
+ // S3: scenario 3
+ // S3: p1: storage=device devval=device-addr host_restored=yes
+ // S3: p2: storage=device devval=device-addr host_restored=yes
+ // S3: p3: storage=device devval=device-addr host_restored=yes
+ // S3: p4: storage=device devval=device-addr host_restored=yes
+ // S3: p5: storage=device devval=device-addr host_restored=yes
+
+ // Only the root pre-exists, sharing storage with the original. The leaf's
+ // allocation is made by this construct, so it is given up instead, and the
+ // whole chain stays on the host path.
+ // S4: scenario 4
+ // S4: p1: storage=shared devval=host-addr host_restored=yes
+ // S4: p2: storage=shared devval=host-addr host_restored=yes
+ // S4: p3: storage=shared devval=host-addr host_restored=yes
+ // S4: p4: storage=shared devval=host-addr host_restored=yes
+ // S4: p5: storage=shared devval=host-addr host_restored=yes
+
+ // Only the root pre-exists, device-backed. Demoting the leaf still settles
+ // every level below, and p1's own storage is left as it was.
+ // S5: scenario 5
+ // S5: p1: storage=device devval=host-addr host_restored=yes
+ // S5: p2: storage=shared devval=host-addr host_restored=yes
+ // S5: p3: storage=shared devval=host-addr host_restored=yes
+ // S5: p4: storage=shared devval=host-addr host_restored=yes
+ // S5: p5: storage=shared devval=host-addr host_restored=yes
+
+ // Both ends pre-exist device-backed, so they agree: the leaf drags the chain
+ // onto device storage and p1 was already there.
+ // S6: scenario 6
+ // S6: p1: storage=device devval=device-addr host_restored=yes
+ // S6: p2: storage=device devval=device-addr host_restored=yes
+ // S6: p3: storage=device devval=device-addr host_restored=yes
+ // S6: p4: storage=device devval=device-addr host_restored=yes
+ // S6: p5: storage=device devval=device-addr host_restored=yes
+
+ // A device-backed level in the middle settles everything below it, and drags
+ // only the levels above it: p2 is attached to p3's storage, which is already
+ // device-backed, so p2 -- and then p1 -- must take device storage, while p4 and
+ // p5 never see a disparity and stay put. The cascade stops where the chain
+ // stops needing it.
+ // S7: scenario 7
+ // S7: p1: storage=device devval=device-addr host_restored=yes
+ // S7: p2: storage=device devval=device-addr host_restored=yes
+ // S7: p3: storage=device devval=host-addr host_restored=yes
+ // S7: p4: storage=shared devval=host-addr host_restored=yes
+ // S7: p5: storage=shared devval=host-addr host_restored=yes
+
+ // A middle level sharing storage with the original conflicts with nothing: the
+ // leaf's allocation is given up and the whole chain stays on the host path.
+ // S8: scenario 8
+ // S8: p1: storage=shared devval=host-addr host_restored=yes
+ // S8: p2: storage=shared devval=host-addr host_restored=yes
+ // S8: p3: storage=shared devval=host-addr host_restored=yes
+ // S8: p4: storage=shared devval=host-addr host_restored=yes
+ // S8: p5: storage=shared devval=host-addr host_restored=yes
+
+ // The blocker need not be at the root. Here the device-backed leaf forces the
+ // upgrade to walk up the chain, and it reaches p3, which was already mapped
+ // sharing storage with the original and so cannot take an allocation now.
+ //
+ // clang-format off
+ // S9: cannot be attached to
+ // S9: was already mapped with its original storage
+ // clang-format on
+
+ // Wherever the chain ends up, it must be internally consistent: reaching the
+ // leaf through all five levels must arrive at the same storage p5 designates,
+ // and every original pointer must be intact afterwards.
+ // OK: chain: consistent
+
+ // The kernel's write lands in whatever storage the leaf ended up in. Where the
+ // chain was dragged onto device storage that is the device buffer, and neither
+ // alloc nor to copies it back, so the original x is unchanged.
+ // S2: dev_read=42 (want 42) x[0]=777 (want 777)
+ // S3: dev_read=42 (want 42) x[0]=42 (want 777)
+ // S4: dev_read=42 (want 42) x[0]=777 (want 777)
+ // S5: dev_read=42 (want 42) x[0]=777 (want 777)
+ // S6: dev_read=42 (want 42) x[0]=42 (want 777)
+ // S7: dev_read=42 (want 42) x[0]=777 (want 777)
+ // S8: dev_read=42 (want 42) x[0]=777 (want 777)
+ return 0;
+}
>From 8b6254a55ed218e05ef2f794af46b290a7b38988 Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Sun, 9 Aug 2026 22:22:57 -0700
Subject: [PATCH 15/29] [OFFLOAD] Drop attached-pointer records when an entry
is unmapped
The reverse index from a pointee to the pointers attached to it was never
pruned, so it kept growing for the lifetime of the process and could be
consulted for host addresses that had since been reused by an unrelated
allocation.
Remove the records covered by an entry's storage when that entry is erased,
both the entries keyed on it as a pointee and the pointers recorded within it.
---
offload/libomptarget/OpenMP/Mapping.cpp | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/offload/libomptarget/OpenMP/Mapping.cpp b/offload/libomptarget/OpenMP/Mapping.cpp
index 6d96467cb92f8..873cd3ecf5271 100644
--- a/offload/libomptarget/OpenMP/Mapping.cpp
+++ b/offload/libomptarget/OpenMP/Mapping.cpp
@@ -558,6 +558,27 @@ int MappingInfoTy::eraseMapEntry(HDTTMapAccessorTy &HDTTMap,
return OFFLOAD_FAIL;
}
+ // The storage is no longer mapped, so nothing can be attached to it, and the
+ // host addresses it covers may be reused by an unrelated allocation. Drop the
+ // records that fall within it, both as a pointee and as a pointer.
+ auto IsInEntry = [&](const void *HstAddr) {
+ uintptr_t Addr = reinterpret_cast<uintptr_t>(HstAddr);
+ return Addr >= Entry->HstPtrBegin && Addr < Entry->HstPtrEnd;
+ };
+
+ llvm::SmallVector<void *> EmptiedPointees;
+ for (auto &[HstPteeBegin, AttachedPtrs] : AttachedPointers) {
+ if (IsInEntry(HstPteeBegin)) {
+ EmptiedPointees.push_back(HstPteeBegin);
+ continue;
+ }
+ llvm::erase_if(AttachedPtrs, IsInEntry);
+ if (AttachedPtrs.empty())
+ EmptiedPointees.push_back(HstPteeBegin);
+ }
+ for (void *HstPteeBegin : EmptiedPointees)
+ AttachedPointers.erase(HstPteeBegin);
+
return OFFLOAD_SUCCESS;
}
>From 284c77c2422f9af477cde2c7fb8ab8d23c8fafa3 Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Sun, 9 Aug 2026 23:15:06 -0700
Subject: [PATCH 16/29] [OFFLOAD] Name the conflicting variables when
attachment cannot be settled
The message only gave the two addresses, which does not say which mapping is
the blocker or what to change. Report the pointer, the pointee and the entry
that could not be given a device allocation, note when the blocker was reached
by cascading from another pointer, and say which modifier to change.
The names come from the map-variable info the runtime already carries for
LIBOMPTARGET_INFO, so this costs nothing when the program was compiled without
debug info, where the addresses are still printed.
Also wait for outstanding transfers before releasing a device allocation on
demotion: an initial 'to' transfer into that allocation may still be queued,
and the free is not ordered against it.
---
offload/include/OpenMP/Mapping.h | 6 +-
offload/libomptarget/OpenMP/Mapping.cpp | 10 ++-
offload/libomptarget/omptarget.cpp | 65 ++++++++++++++-----
.../close_attach_host_deref.c | 4 +-
.../close_attach_pointer_chain5.c | 8 +--
5 files changed, 69 insertions(+), 24 deletions(-)
diff --git a/offload/include/OpenMP/Mapping.h b/offload/include/OpenMP/Mapping.h
index c04ba8fd3123c..15f90fe09d429 100644
--- a/offload/include/OpenMP/Mapping.h
+++ b/offload/include/OpenMP/Mapping.h
@@ -791,9 +791,13 @@ struct MappingInfoTy {
/// device address into the original pointer, so the pointee keeps host
/// storage instead and the attached address is the original one.
///
+ /// Any transfer already queued into the allocation being released has to
+ /// complete before it is freed, so \p AsyncInfo is synchronized first.
+ ///
/// \p HDTTMap must be held by the caller.
[[nodiscard]] int shareEntryStorageWithOriginal(HDTTMapAccessorTy &HDTTMap,
- HostDataToTargetTy *Entry);
+ HostDataToTargetTy *Entry,
+ AsyncInfoTy &AsyncInfo);
/// Deallocate the \p Entry from the device memory and delete it. Return \c
/// OFFLOAD_SUCCESS if the deallocation operations executed successfully, and
diff --git a/offload/libomptarget/OpenMP/Mapping.cpp b/offload/libomptarget/OpenMP/Mapping.cpp
index 873cd3ecf5271..e9f4bf0f4abc1 100644
--- a/offload/libomptarget/OpenMP/Mapping.cpp
+++ b/offload/libomptarget/OpenMP/Mapping.cpp
@@ -629,7 +629,8 @@ int MappingInfoTy::giveEntryDeviceStorage(HDTTMapAccessorTy &HDTTMap,
}
int MappingInfoTy::shareEntryStorageWithOriginal(HDTTMapAccessorTy &HDTTMap,
- HostDataToTargetTy *Entry) {
+ HostDataToTargetTy *Entry,
+ AsyncInfoTy &AsyncInfo) {
assert(Entry && "Trying to share storage of a null entry.");
assert(!Entry->isHostBacked() &&
"Entry storage is already shared with the original");
@@ -652,6 +653,13 @@ int MappingInfoTy::shareEntryStorageWithOriginal(HDTTMapAccessorTy &HDTTMap,
if (Device.notifyDataUnmapped(HstPtrBegin))
return OFFLOAD_FAIL;
+ // A transfer into this allocation may already be in flight, and the free
+ // below is not ordered against it, so wait for it to complete first.
+ if (Device.synchronize(AsyncInfo) != OFFLOAD_SUCCESS) {
+ REPORT() << "Failed to synchronize before releasing a device allocation.";
+ return OFFLOAD_FAIL;
+ }
+
return Device.deleteData(TgtAllocBegin);
}
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index 4c3800d1fa05b..4a165370449e4 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -870,6 +870,15 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
return Entry;
};
+ // Names are only available when the program was compiled with -g; they make
+ // the diagnostics below usable, and cost nothing when absent.
+ auto NameOf = [](map_var_info_t Name) -> std::string {
+ return Name ? getNameFromMapping(Name) : "unknown";
+ };
+ auto NameOfEntry = [&](HostDataToTargetTy *Entry) -> std::string {
+ return Entry ? NameOf(Entry->HstPtrName) : "unknown";
+ };
+
// An attachment only needs settling if the pointer is shared with the
// original, and the pointee has device storage for its address to differ.
auto NeedsSettling = [&](void *HstPtr, int64_t PtrSize,
@@ -907,8 +916,8 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
"Releasing an allocation that a pointer is already attached to");
}
#endif
- if (MappingInfo.shareEntryStorageWithOriginal(HDTTMap, PteeEntry) !=
- OFFLOAD_SUCCESS)
+ if (MappingInfo.shareEntryStorageWithOriginal(HDTTMap, PteeEntry,
+ AsyncInfo) != OFFLOAD_SUCCESS)
return OFFLOAD_FAIL;
}
@@ -922,25 +931,49 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
// attached to storage within an entry designates that entry's current
// address, so an entry cannot take a device allocation while such a pointer
// is itself unable to hold a device address.
- SmallVector<std::pair<void **, int64_t>> ToUpgrade;
- ToUpgrade.emplace_back(HstPtr, AttachEntry.PointerSize);
+ // Each item carries the pointer that made it need device storage, so a
+ // failure can name the chain that led there rather than just its end.
+ struct UpgradeItem {
+ void **HstPtr;
+ int64_t PtrSize;
+ void **ReachedFrom;
+ };
+ SmallVector<UpgradeItem> ToUpgrade;
+ ToUpgrade.push_back({HstPtr, AttachEntry.PointerSize, nullptr});
while (!ToUpgrade.empty()) {
- auto [UpHstPtr, UpPtrSize] = ToUpgrade.pop_back_val();
+ auto [UpHstPtr, UpPtrSize, ReachedFrom] = ToUpgrade.pop_back_val();
HostDataToTargetTy *UpEntry = FindEntry(UpHstPtr, UpPtrSize);
if (!UpEntry || !UpEntry->isHostBacked())
continue;
if (!StateInfo.wasNewlyMapped(UpHstPtr).has_value()) {
- MESSAGE("pointer " DPxMOD " cannot be attached to " DPxMOD
- ": its corresponding pointer is the original pointer, and "
- "neither it nor the pointee could be given storage that would "
- "make the attachment possible\n",
- DPxPTR(HstPtr), DPxPTR(AttachEntry.PointeeBegin));
- REPORT() << "Storage at " << UpHstPtr
- << " was already mapped with its original storage, so it "
- "cannot be given a device allocation now.";
+ std::string PtrName = NameOf(AttachEntry.Pointername);
+ std::string PteeName =
+ NameOfEntry(FindEntry(AttachEntry.PointeeBegin, /*Size=*/0));
+ std::string BlockerName = NameOfEntry(UpEntry);
+
+ MESSAGE("cannot attach pointer %s (" DPxMOD ") to %s (" DPxMOD
+ ") under unified shared memory: %s has a device allocation, so "
+ "the attachment would assign a device address to the original "
+ "pointer\n",
+ PtrName.c_str(), DPxPTR(HstPtr), PteeName.c_str(),
+ DPxPTR(AttachEntry.PointeeBegin), PteeName.c_str());
+ if (ReachedFrom)
+ MESSAGE(" %s (" DPxMOD ") has to hold a device address as well, "
+ "because the pointer at " DPxMOD " points into its storage\n",
+ BlockerName.c_str(), DPxPTR(UpHstPtr), DPxPTR(ReachedFrom));
+ MESSAGE(" but %s (" DPxMOD ") was mapped by an enclosing construct "
+ "with its storage shared with the original, and its device "
+ "address may already have been obtained, so it cannot be given "
+ "a device allocation now\n",
+ BlockerName.c_str(), DPxPTR(UpHstPtr));
+ MESSAGE(" map %s with the close modifier where it is first mapped, or "
+ "drop the close modifier from %s\n",
+ BlockerName.c_str(), PteeName.c_str());
+ REPORT() << "Unsatisfiable pointer attachment under unified shared "
+ "memory.";
return OFFLOAD_FAIL;
}
@@ -962,14 +995,14 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
if (!IsInsideUpEntry(AttachedPtee))
continue;
for (void **AttachedPtr : AttachedPtrs)
- ToUpgrade.emplace_back(AttachedPtr, sizeof(void *));
+ ToUpgrade.push_back({AttachedPtr, sizeof(void *), UpHstPtr});
}
for (const auto &Pending : StateInfo.AttachEntries) {
if (!IsInsideUpEntry(Pending.PointeeBegin))
continue;
- ToUpgrade.emplace_back(reinterpret_cast<void **>(Pending.PointerBase),
- Pending.PointerSize);
+ ToUpgrade.push_back({reinterpret_cast<void **>(Pending.PointerBase),
+ Pending.PointerSize, UpHstPtr});
}
}
}
diff --git a/offload/test/unified_shared_memory/close_attach_host_deref.c b/offload/test/unified_shared_memory/close_attach_host_deref.c
index 286bb35f1de4f..094b04cc6739d 100644
--- a/offload/test/unified_shared_memory/close_attach_host_deref.c
+++ b/offload/test/unified_shared_memory/close_attach_host_deref.c
@@ -71,8 +71,8 @@ int main() {
#pragma omp target enter data map(close, alloc : x[0 : 10])
//
// clang-format off
- // ALWAYS: cannot be attached to
- // ALWAYS: was already mapped with its original storage
+ // ALWAYS: cannot attach pointer
+ // ALWAYS: was mapped by an enclosing construct with its storage shared with the original
// clang-format on
#pragma omp target enter data map(alloc : p[0 : 0])
#else
diff --git a/offload/test/unified_shared_memory/close_attach_pointer_chain5.c b/offload/test/unified_shared_memory/close_attach_pointer_chain5.c
index 5e88c3c8670f1..c401752e094ca 100644
--- a/offload/test/unified_shared_memory/close_attach_pointer_chain5.c
+++ b/offload/test/unified_shared_memory/close_attach_pointer_chain5.c
@@ -181,8 +181,8 @@ int main(void) {
// the original and so cannot take an allocation now.
//
// clang-format off
- // S1: cannot be attached to
- // S1: was already mapped with its original storage
+ // S1: cannot attach pointer
+ // S1: was mapped by an enclosing construct with its storage shared with the original
// clang-format on
// p1 is already device-backed, so the attachment into it does not touch an
@@ -260,8 +260,8 @@ int main(void) {
// sharing storage with the original and so cannot take an allocation now.
//
// clang-format off
- // S9: cannot be attached to
- // S9: was already mapped with its original storage
+ // S9: cannot attach pointer
+ // S9: was mapped by an enclosing construct with its storage shared with the original
// clang-format on
// Wherever the chain ends up, it must be internally consistent: reaching the
>From 4b4336acc30e182cf781051e9978841c1dd0d5bb Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Mon, 10 Aug 2026 01:01:45 -0700
Subject: [PATCH 17/29] [OFFLOAD] Only transfer the 'to' ranges when a mapping
becomes device-bound
Giving an entry a device allocation for pointer attachment copied the whole
entry over. That gave the program values it cannot rely on: only the ranges
mapped with the TO map type were ever specified to have a corresponding value
on the device, and the rest of the entry happened to look right merely because
the storage had been shared with the original.
Record the TO ranges whose transfer target was the original storage, and bring
only those over. Also rename isHostBacked to isHostBound, and say host-bound
and device-bound in the comments and diagnostics throughout.
The unsatisfiable-attachment message is now a single line naming the mapping
that cannot be changed, without suggesting fixes.
---
offload/include/OpenMP/Mapping.h | 24 +++++++---
offload/libomptarget/OpenMP/Mapping.cpp | 46 ++++++++++++++-----
offload/libomptarget/omptarget.cpp | 46 ++++++++-----------
.../close_attach_host_deref.c | 4 +-
.../close_attach_pointer_chain5.c | 8 ++--
5 files changed, 79 insertions(+), 49 deletions(-)
diff --git a/offload/include/OpenMP/Mapping.h b/offload/include/OpenMP/Mapping.h
index 15f90fe09d429..8e5cebecf1007 100644
--- a/offload/include/OpenMP/Mapping.h
+++ b/offload/include/OpenMP/Mapping.h
@@ -126,14 +126,14 @@ struct HostDataToTargetTy {
/// Whether this entry's storage is the host storage itself, i.e. it owns no
/// device allocation. That is the case for the entries recorded on the
/// unified-shared-memory host path.
- bool isHostBacked() const { return TgtPtrBegin == HstPtrBegin; }
+ bool isHostBound() const { return TgtPtrBegin == HstPtrBegin; }
/// Give this entry a device allocation, so that its storage stops being
/// shared with the original. The caller performs the allocation; see
/// MappingInfoTy::giveEntryDeviceStorage(), which is the only intended caller
/// and documents when this is legal.
void takeDeviceStorage(uintptr_t NewTgtAllocBegin, uintptr_t NewTgtPtrBegin) {
- assert(isHostBacked() && "Entry already owns a device allocation");
+ assert(isHostBound() && "Entry already owns a device allocation");
TgtAllocBegin = NewTgtAllocBegin;
TgtPtrBegin = NewTgtPtrBegin;
}
@@ -143,7 +143,7 @@ struct HostDataToTargetTy {
/// MappingInfoTy::shareEntryStorageWithOriginal(), which is the only intended
/// caller and documents when this is legal.
void shareStorageWithOriginal() {
- assert(!isHostBacked() && "Entry is already shared with the original");
+ assert(!isHostBound() && "Entry is already shared with the original");
TgtAllocBegin = HstPtrBegin;
TgtPtrBegin = HstPtrBegin;
}
@@ -542,6 +542,14 @@ struct StateInfoTy {
/// Key: host pointer, Value: mapped size.
llvm::DenseMap<void *, int64_t> NewMappings;
+ /// Ranges mapped with the TO map type on this construct whose storage was
+ /// shared with the original, so that no transfer was performed for them.
+ /// If such storage is later given a device allocation for pointer attachment,
+ /// these are the ranges whose contents have to be brought over: the rest of
+ /// the entry was never specified to have a corresponding value on the device.
+ /// Key: host pointer, Value: mapped size.
+ llvm::DenseMap<void *, int64_t> HostPathToRanges;
+
/// Host pointers that had a FROM entry, but for which a data transfer was
/// skipped due to the ref-count not being zero.
/// Key: host pointer, Value: data size.
@@ -773,9 +781,13 @@ struct MappingInfoTy {
/// Only legal for an entry whose mapping was created by the construct that is
/// currently being processed, since its device address cannot have been
/// observed yet. \p HDTTMap must be held by the caller.
- [[nodiscard]] int giveEntryDeviceStorage(HDTTMapAccessorTy &HDTTMap,
- HostDataToTargetTy *Entry,
- AsyncInfoTy &AsyncInfo);
+ /// \p ToRanges gives the ranges mapped with the TO map type on this construct
+ /// while the storage was shared with the original; only those are brought
+ /// over, see StateInfoTy::HostPathToRanges.
+ [[nodiscard]] int
+ giveEntryDeviceStorage(HDTTMapAccessorTy &HDTTMap, HostDataToTargetTy *Entry,
+ AsyncInfoTy &AsyncInfo,
+ const llvm::DenseMap<void *, int64_t> &ToRanges);
/// Return the storage of \p Entry to being shared with the original,
/// releasing the device allocation it was created with.
diff --git a/offload/libomptarget/OpenMP/Mapping.cpp b/offload/libomptarget/OpenMP/Mapping.cpp
index e9f4bf0f4abc1..090bd72b2077d 100644
--- a/offload/libomptarget/OpenMP/Mapping.cpp
+++ b/offload/libomptarget/OpenMP/Mapping.cpp
@@ -385,6 +385,16 @@ TargetPointerResultTy MappingInfoTy::getTargetPointer(
return WasNewlyAllocated;
};
+ // Record TO ranges whose transfer target is the original storage itself,
+ // which is the normal case under unified shared memory. Such a transfer
+ // leaves nothing behind in a device allocation, so if this storage is later
+ // given one to make a pointer attachment possible, these are the ranges that
+ // have to be brought over then. Anything else in the entry was never
+ // specified to have a corresponding value on the device.
+ if (StateInfo && HasFlagTo && Size != 0 &&
+ LR.TPR.TargetPointer == HstPtrBegin)
+ StateInfo->HostPathToRanges[HstPtrBegin] = Size;
+
// Even if this isn't a new entry, we still need to do a data-transfer if
// the pointer was newly allocated on the current target region.
if (LR.TPR.TargetPointer && !LR.TPR.Flags.IsHostPointer && HasFlagTo &&
@@ -589,11 +599,11 @@ void MappingInfoTy::recordAttachedPointer(void *HstPteeBegin,
Attached.push_back(HstPtrAddr);
}
-int MappingInfoTy::giveEntryDeviceStorage(HDTTMapAccessorTy &HDTTMap,
- HostDataToTargetTy *Entry,
- AsyncInfoTy &AsyncInfo) {
+int MappingInfoTy::giveEntryDeviceStorage(
+ HDTTMapAccessorTy &HDTTMap, HostDataToTargetTy *Entry,
+ AsyncInfoTy &AsyncInfo, const llvm::DenseMap<void *, int64_t> &ToRanges) {
assert(Entry && "Trying to allocate for a null entry.");
- assert(Entry->isHostBacked() && "Entry already owns a device allocation");
+ assert(Entry->isHostBound() && "Entry already owns a device allocation");
void *HstPtrBegin = reinterpret_cast<void *>(Entry->HstPtrBegin);
int64_t Size = Entry->HstPtrEnd - Entry->HstPtrBegin;
@@ -617,12 +627,26 @@ int MappingInfoTy::giveEntryDeviceStorage(HDTTMapAccessorTy &HDTTMap,
<< ") -> (tgt:" << reinterpret_cast<void *>(TgtAllocBegin)
<< "), so that its storage is not shared with the original";
- // The storage was shared with the host until now, so bring its current
- // contents over: it may hold data besides the pointer being attached.
- if (Device.submitData(reinterpret_cast<void *>(TgtAllocBegin), HstPtrBegin,
- Size, AsyncInfo, Entry) != OFFLOAD_SUCCESS) {
- REPORT() << "Copying data to device failed.";
- return OFFLOAD_FAIL;
+ // Bring over the parts of the storage that were mapped with the TO map type
+ // on this construct. Those had no transfer performed for them, because the
+ // storage was shared with the original at the time. The rest of the entry is
+ // deliberately left alone: it was never specified to have a corresponding
+ // value on the device, and copying it would give the program a value it
+ // cannot rely on under a device allocation.
+ for (const auto &[ToBegin, ToSize] : ToRanges) {
+ uintptr_t Begin = reinterpret_cast<uintptr_t>(ToBegin);
+ if (Begin < Entry->HstPtrBegin || Begin + ToSize > Entry->HstPtrEnd)
+ continue;
+ void *TgtBegin =
+ reinterpret_cast<void *>(TgtAllocBegin + (Begin - Entry->HstPtrBegin));
+ ODBG(ODT_Mapping) << "Moving " << ToSize
+ << " bytes mapped with 'to' (hst:" << ToBegin
+ << ") -> (tgt:" << TgtBegin << ")";
+ if (Device.submitData(TgtBegin, ToBegin, ToSize, AsyncInfo, Entry) !=
+ OFFLOAD_SUCCESS) {
+ REPORT() << "Copying data to device failed.";
+ return OFFLOAD_FAIL;
+ }
}
return Device.notifyDataMapped(HstPtrBegin, Size);
@@ -632,7 +656,7 @@ int MappingInfoTy::shareEntryStorageWithOriginal(HDTTMapAccessorTy &HDTTMap,
HostDataToTargetTy *Entry,
AsyncInfoTy &AsyncInfo) {
assert(Entry && "Trying to share storage of a null entry.");
- assert(!Entry->isHostBacked() &&
+ assert(!Entry->isHostBound() &&
"Entry storage is already shared with the original");
void *HstPtrBegin = reinterpret_cast<void *>(Entry->HstPtrBegin);
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index 4a165370449e4..9426f706eead1 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -884,10 +884,10 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
auto NeedsSettling = [&](void *HstPtr, int64_t PtrSize,
void *HstPteeBegin) -> bool {
HostDataToTargetTy *PtrEntry = FindEntry(HstPtr, PtrSize);
- if (!PtrEntry || !PtrEntry->isHostBacked())
+ if (!PtrEntry || !PtrEntry->isHostBound())
return false;
HostDataToTargetTy *PteeEntry = FindEntry(HstPteeBegin, /*Size=*/0);
- return PteeEntry && !PteeEntry->isHostBacked();
+ return PteeEntry && !PteeEntry->isHostBound();
};
for (const auto &AttachEntry : StateInfo.AttachEntries) {
@@ -945,39 +945,33 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
auto [UpHstPtr, UpPtrSize, ReachedFrom] = ToUpgrade.pop_back_val();
HostDataToTargetTy *UpEntry = FindEntry(UpHstPtr, UpPtrSize);
- if (!UpEntry || !UpEntry->isHostBacked())
+ if (!UpEntry || !UpEntry->isHostBound())
continue;
if (!StateInfo.wasNewlyMapped(UpHstPtr).has_value()) {
std::string PtrName = NameOf(AttachEntry.Pointername);
- std::string PteeName =
- NameOfEntry(FindEntry(AttachEntry.PointeeBegin, /*Size=*/0));
std::string BlockerName = NameOfEntry(UpEntry);
- MESSAGE("cannot attach pointer %s (" DPxMOD ") to %s (" DPxMOD
- ") under unified shared memory: %s has a device allocation, so "
- "the attachment would assign a device address to the original "
- "pointer\n",
- PtrName.c_str(), DPxPTR(HstPtr), PteeName.c_str(),
- DPxPTR(AttachEntry.PointeeBegin), PteeName.c_str());
+ // Say which mapping cannot hold a device address, and why. When the
+ // upgrade cascaded, that is not the pointer of this attach entry but
+ // something that points into its storage.
if (ReachedFrom)
- MESSAGE(" %s (" DPxMOD ") has to hold a device address as well, "
- "because the pointer at " DPxMOD " points into its storage\n",
- BlockerName.c_str(), DPxPTR(UpHstPtr), DPxPTR(ReachedFrom));
- MESSAGE(" but %s (" DPxMOD ") was mapped by an enclosing construct "
- "with its storage shared with the original, and its device "
- "address may already have been obtained, so it cannot be given "
- "a device allocation now\n",
- BlockerName.c_str(), DPxPTR(UpHstPtr));
- MESSAGE(" map %s with the close modifier where it is first mapped, or "
- "drop the close modifier from %s\n",
- BlockerName.c_str(), PteeName.c_str());
- REPORT() << "Unsatisfiable pointer attachment under unified shared "
- "memory.";
+ MESSAGE("could not do pointer attachment for %s (" DPxMOD
+ "): the pointee is device-bound, so %s (" DPxMOD
+ ") would have to be device-bound as well, but it has an "
+ "existing host-bound mapping that cannot be changed\n",
+ PtrName.c_str(), DPxPTR(HstPtr), BlockerName.c_str(),
+ DPxPTR(UpHstPtr));
+ else
+ MESSAGE("could not do pointer attachment for %s (" DPxMOD
+ "): the pointer has an existing host-bound mapping and the "
+ "pointee is device-bound, so neither can be changed\n",
+ PtrName.c_str(), DPxPTR(HstPtr));
return OFFLOAD_FAIL;
}
- if (MappingInfo.giveEntryDeviceStorage(HDTTMap, UpEntry, AsyncInfo) !=
+ if (MappingInfo.giveEntryDeviceStorage(HDTTMap, UpEntry, AsyncInfo,
+ StateInfo.HostPathToRanges) !=
OFFLOAD_SUCCESS)
return OFFLOAD_FAIL;
@@ -1139,7 +1133,7 @@ int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
// The storage on both sides was settled by settleAttachStorage, so an
// attachment that would write a device address into an original pointer is
// one that could not be settled at all.
- if (PtrTPROpt->getEntry() && PtrTPROpt->getEntry()->isHostBacked() &&
+ if (PtrTPROpt->getEntry() && PtrTPROpt->getEntry()->isHostBound() &&
TgtPteeBegin != HstPteeBegin) {
MESSAGE("pointer " DPxMOD " cannot be attached to " DPxMOD
": its corresponding pointer is the original pointer, so the "
diff --git a/offload/test/unified_shared_memory/close_attach_host_deref.c b/offload/test/unified_shared_memory/close_attach_host_deref.c
index 094b04cc6739d..e7db94e437f76 100644
--- a/offload/test/unified_shared_memory/close_attach_host_deref.c
+++ b/offload/test/unified_shared_memory/close_attach_host_deref.c
@@ -71,8 +71,8 @@ int main() {
#pragma omp target enter data map(close, alloc : x[0 : 10])
//
// clang-format off
- // ALWAYS: cannot attach pointer
- // ALWAYS: was mapped by an enclosing construct with its storage shared with the original
+ // ALWAYS: could not do pointer attachment
+ // ALWAYS-SAME: host-bound mapping and the pointee is device-bound
// clang-format on
#pragma omp target enter data map(alloc : p[0 : 0])
#else
diff --git a/offload/test/unified_shared_memory/close_attach_pointer_chain5.c b/offload/test/unified_shared_memory/close_attach_pointer_chain5.c
index c401752e094ca..abd16cd0cc81e 100644
--- a/offload/test/unified_shared_memory/close_attach_pointer_chain5.c
+++ b/offload/test/unified_shared_memory/close_attach_pointer_chain5.c
@@ -181,8 +181,8 @@ int main(void) {
// the original and so cannot take an allocation now.
//
// clang-format off
- // S1: cannot attach pointer
- // S1: was mapped by an enclosing construct with its storage shared with the original
+ // S1: could not do pointer attachment
+ // S1-SAME: would have to be device-bound as well
// clang-format on
// p1 is already device-backed, so the attachment into it does not touch an
@@ -260,8 +260,8 @@ int main(void) {
// sharing storage with the original and so cannot take an allocation now.
//
// clang-format off
- // S9: cannot attach pointer
- // S9: was mapped by an enclosing construct with its storage shared with the original
+ // S9: could not do pointer attachment
+ // S9-SAME: would have to be device-bound as well
// clang-format on
// Wherever the chain ends up, it must be internally consistent: reaching the
>From b3dd4ecd111859fdfc6ebd222a75c4861ba30f85 Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Mon, 10 Aug 2026 01:10:31 -0700
Subject: [PATCH 18/29] [OFFLOAD] Settle only the attachments that will happen,
and release to a fixpoint
Two ways the settling pass reported a conflict for a configuration that has
none.
It considered every deferred attach entry, but an attachment is only performed
when one of the two sides was newly mapped by this construct, or when it is
unconditional. A construct that maps neither side afresh assigns nothing, so
there is nothing to settle; apply the same test that processAttachEntries does.
It also considered the releases only once. Releasing one pointee's allocation
can leave a second attachment unsettled, because the storage just released may
itself hold a pointer whose own pointee is device-bound; the second attachment
then took the other branch and failed on a mapping that could not change.
Repeat the releases until none is left to do. Each one releases an allocation
made by this construct and none are created there, so this terminates.
Both cases came out of review rather than testing, so add a test for each.
---
offload/libomptarget/omptarget.cpp | 46 +++++++++++----
.../close_attach_settle_eligibility.c | 51 ++++++++++++++++
.../close_attach_settle_fixpoint.c | 59 +++++++++++++++++++
3 files changed, 146 insertions(+), 10 deletions(-)
create mode 100644 offload/test/unified_shared_memory/close_attach_settle_eligibility.c
create mode 100644 offload/test/unified_shared_memory/close_attach_settle_fixpoint.c
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index 9426f706eead1..cf1036178c9e0 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -879,6 +879,21 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
return Entry ? NameOf(Entry->HstPtrName) : "unknown";
};
+ const bool TreatAttachAutoAsAlways =
+ MappingConfig::get().TreatAttachAutoAsAlways;
+
+ // Attachment is only performed when one of the two sides was newly mapped by
+ // this construct, or when it is unconditional. processAttachEntries applies
+ // the same test; settling an entry it will skip would report a conflict for
+ // an assignment that never happens.
+ auto WillAttach = [&](const AttachMapInfo &AttachEntry) {
+ if ((AttachEntry.MapType & OMP_TGT_MAPTYPE_ALWAYS) ||
+ TreatAttachAutoAsAlways)
+ return true;
+ return StateInfo.wasNewlyMapped(AttachEntry.PointeeBegin).has_value() ||
+ StateInfo.wasNewlyMapped(AttachEntry.PointerBase).has_value();
+ };
+
// An attachment only needs settling if the pointer is shared with the
// original, and the pointee has device storage for its address to differ.
auto NeedsSettling = [&](void *HstPtr, int64_t PtrSize,
@@ -890,17 +905,25 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
return PteeEntry && !PteeEntry->isHostBound();
};
- for (const auto &AttachEntry : StateInfo.AttachEntries) {
- void **HstPtr = reinterpret_cast<void **>(AttachEntry.PointerBase);
- void *HstPteeBegin = AttachEntry.PointeeBegin;
- if (!NeedsSettling(HstPtr, AttachEntry.PointerSize, HstPteeBegin))
- continue;
+ // Demoting one pointee can leave another attachment unsettled: the demoted
+ // storage may itself hold a pointer whose own pointee is device-bound. So
+ // keep going until a pass demotes nothing. Each demotion releases an
+ // allocation made by this construct, and none are created here, so this
+ // terminates.
+ for (bool Demoted = true; Demoted;) {
+ Demoted = false;
+ for (const auto &AttachEntry : StateInfo.AttachEntries) {
+ void **HstPtr = reinterpret_cast<void **>(AttachEntry.PointerBase);
+ void *HstPteeBegin = AttachEntry.PointeeBegin;
+ if (!WillAttach(AttachEntry) ||
+ !NeedsSettling(HstPtr, AttachEntry.PointerSize, HstPteeBegin))
+ continue;
- // Only a pointee allocated for this construct may give its allocation up.
- if (!StateInfo.wasNewlyAllocated(HstPteeBegin).has_value())
- continue;
+ // Only a pointee allocated for this construct may give its allocation up.
+ if (!StateInfo.wasNewlyAllocated(HstPteeBegin).has_value())
+ continue;
- HostDataToTargetTy *PteeEntry = FindEntry(HstPteeBegin, /*Size=*/0);
+ HostDataToTargetTy *PteeEntry = FindEntry(HstPteeBegin, /*Size=*/0);
#ifndef NDEBUG
// Giving the allocation up moves the pointee's device address, which would
// strand any pointer already attached to it. That cannot happen here: the
@@ -919,11 +942,14 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
if (MappingInfo.shareEntryStorageWithOriginal(HDTTMap, PteeEntry,
AsyncInfo) != OFFLOAD_SUCCESS)
return OFFLOAD_FAIL;
+ Demoted = true;
+ }
}
for (const auto &AttachEntry : StateInfo.AttachEntries) {
void **HstPtr = reinterpret_cast<void **>(AttachEntry.PointerBase);
- if (!NeedsSettling(HstPtr, AttachEntry.PointerSize,
+ if (!WillAttach(AttachEntry) ||
+ !NeedsSettling(HstPtr, AttachEntry.PointerSize,
AttachEntry.PointeeBegin))
continue;
diff --git a/offload/test/unified_shared_memory/close_attach_settle_eligibility.c b/offload/test/unified_shared_memory/close_attach_settle_eligibility.c
new file mode 100644
index 0000000000000..4dab34ccae405
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_attach_settle_eligibility.c
@@ -0,0 +1,51 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+// An attachment is only performed when one of the two sides was newly mapped by
+// the construct that prescribes it. Deciding which side holds device storage has
+// to apply that same test: a construct that maps neither side afresh performs no
+// assignment, so there is no conflict to resolve even when the pointer is
+// host-bound and the pointee device-bound.
+//
+// Here p and x are both mapped by earlier constructs, so the map of the
+// zero-length array section prescribes nothing and must not be reported as an
+// unsatisfiable attachment.
+
+#include <stdio.h>
+
+#pragma omp requires unified_shared_memory
+
+int x = 7;
+int *p = &x;
+
+int main(void) {
+ // The unified_shared_memory requirement is registered when a device image is
+ // loaded, so the program needs a target region for it to take effect.
+#pragma omp target
+ {
+ }
+
+ // p is host-bound, x is device-bound, and both are already present.
+#pragma omp target enter data map(alloc : p)
+#pragma omp target enter data map(close, to : x)
+
+#pragma omp target data map(alloc : p[0 : 1])
+ {
+ }
+
+ // CHECK: p == &x
+ printf("p %s &x\n", p == &x ? "==" : "!=");
+
+ // CHECK: Done!
+ printf("Done!\n");
+ return 0;
+}
diff --git a/offload/test/unified_shared_memory/close_attach_settle_fixpoint.c b/offload/test/unified_shared_memory/close_attach_settle_fixpoint.c
new file mode 100644
index 0000000000000..7b42e089aefcc
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_attach_settle_fixpoint.c
@@ -0,0 +1,59 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+// Releasing one pointee's device allocation can leave a second attachment
+// unsettled, because the storage just released may itself hold a pointer whose
+// own pointee is device-bound. The releases therefore have to be repeated until
+// none is left to do, before any pointer is given device storage.
+//
+// Here a is host-bound and already present, so it can never hold a device
+// address, while b and its pointee are both newly mapped with close:
+//
+// a -> b -> x
+//
+// Releasing b for the sake of a's attachment leaves b host-bound, which in turn
+// makes b's own attachment to the device-bound x unsettled. Releasing x as well
+// settles everything with all three host-bound. Considering the releases only
+// once would instead give b device storage again and then fail on a, reporting a
+// conflict for a configuration that has a solution.
+
+#include <stdio.h>
+
+#pragma omp requires unified_shared_memory
+
+int x = 9;
+int *b = &x;
+int **a = &b;
+
+int main(void) {
+ // The unified_shared_memory requirement is registered when a device image is
+ // loaded, so the program needs a target region for it to take effect.
+#pragma omp target
+ {
+ }
+
+ // The pre-existing host-bound mapping that cannot be changed later.
+#pragma omp target enter data map(alloc : a)
+
+ // Attaches b to x, and a to b.
+#pragma omp target data map(close, to : b) map(close, to : b[0 : 1]) \
+ map(alloc : a[0 : 1])
+ {
+ }
+
+ // CHECK: a == &b, b == &x
+ printf("a %s &b, b %s &x\n", a == &b ? "==" : "!=", b == &x ? "==" : "!=");
+
+ // CHECK: Done!
+ printf("Done!\n");
+ return 0;
+}
>From c0c2b067351f00e0822ebebae213d66938d732e3 Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Mon, 10 Aug 2026 01:53:01 -0700
Subject: [PATCH 19/29] [OFFLOAD] Settle the forced attachments before the ones
that have a choice
An attachment whose pointee was allocated by an enclosing construct can only be
settled by giving the pointer device storage: the pointee cannot give up an
allocation whose address the program may already have obtained. An attachment
whose pointee this construct allocated can be settled either way, and releasing
that allocation is preferred because it settles the attachment outright rather
than propagating to everything that points at the pointer.
Doing the releases first meant a release could be undone in effect: with two
pointers in one structure, one of them forced that structure to hold device
addresses after the other's pointee had already been released, so the release
achieved nothing and its close request went unmet for no reason. Settle the
forced attachments first, so the release pass sees the storage they imply.
Also note in the debug output when a mapping reuses the original storage that
way, since a close modifier on it is then not honoured.
---
offload/libomptarget/OpenMP/Mapping.cpp | 4 +
offload/libomptarget/omptarget.cpp | 95 +++++++++++--------
.../close_attach_settle_no_waste.c | 92 ++++++++++++++++++
3 files changed, 150 insertions(+), 41 deletions(-)
create mode 100644 offload/test/unified_shared_memory/close_attach_settle_no_waste.c
diff --git a/offload/libomptarget/OpenMP/Mapping.cpp b/offload/libomptarget/OpenMP/Mapping.cpp
index 090bd72b2077d..8762b7d5a241a 100644
--- a/offload/libomptarget/OpenMP/Mapping.cpp
+++ b/offload/libomptarget/OpenMP/Mapping.cpp
@@ -671,6 +671,10 @@ int MappingInfoTy::shareEntryStorageWithOriginal(HDTTMapAccessorTy &HDTTMap,
ODBG(ODT_Mapping) << "Releasing the device allocation of map entry (hst:"
<< HstPtrBegin << ") -> (tgt:" << TgtAllocBegin
<< "), so that its storage is shared with the original";
+ ODBG(ODT_Mapping) << "Mapping of " << HstPtrBegin
+ << " reuses the original storage, despite a close modifier "
+ "if one was given, because a pointer whose own storage "
+ "is the original is attached to it";
Entry->shareStorageWithOriginal();
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index cf1036178c9e0..746aabac74dfb 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -905,47 +905,11 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
return PteeEntry && !PteeEntry->isHostBound();
};
- // Demoting one pointee can leave another attachment unsettled: the demoted
- // storage may itself hold a pointer whose own pointee is device-bound. So
- // keep going until a pass demotes nothing. Each demotion releases an
- // allocation made by this construct, and none are created here, so this
- // terminates.
- for (bool Demoted = true; Demoted;) {
- Demoted = false;
- for (const auto &AttachEntry : StateInfo.AttachEntries) {
- void **HstPtr = reinterpret_cast<void **>(AttachEntry.PointerBase);
- void *HstPteeBegin = AttachEntry.PointeeBegin;
- if (!WillAttach(AttachEntry) ||
- !NeedsSettling(HstPtr, AttachEntry.PointerSize, HstPteeBegin))
- continue;
-
- // Only a pointee allocated for this construct may give its allocation up.
- if (!StateInfo.wasNewlyAllocated(HstPteeBegin).has_value())
- continue;
-
- HostDataToTargetTy *PteeEntry = FindEntry(HstPteeBegin, /*Size=*/0);
-#ifndef NDEBUG
- // Giving the allocation up moves the pointee's device address, which would
- // strand any pointer already attached to it. That cannot happen here: the
- // allocation was made by this construct, so no earlier construct could have
- // attached to it, and this pre-pass runs before any attachment is performed
- // for this one.
- for (auto &[AttachedPtee, AttachedPtrs] : MappingInfo.AttachedPointers) {
- (void)AttachedPtrs;
- assert((reinterpret_cast<uintptr_t>(AttachedPtee) <
- PteeEntry->HstPtrBegin ||
- reinterpret_cast<uintptr_t>(AttachedPtee) >=
- PteeEntry->HstPtrEnd) &&
- "Releasing an allocation that a pointer is already attached to");
- }
-#endif
- if (MappingInfo.shareEntryStorageWithOriginal(HDTTMap, PteeEntry,
- AsyncInfo) != OFFLOAD_SUCCESS)
- return OFFLOAD_FAIL;
- Demoted = true;
- }
- }
-
+ // Give a pointer device storage only where the pointee cannot give its own
+ // allocation up: it was made by an enclosing construct, so its device address
+ // may already have been obtained by the program. Those are the attachments
+ // that have no other way to be settled, so they are settled first, and the
+ // releases below then see the storage they forced.
for (const auto &AttachEntry : StateInfo.AttachEntries) {
void **HstPtr = reinterpret_cast<void **>(AttachEntry.PointerBase);
if (!WillAttach(AttachEntry) ||
@@ -953,6 +917,9 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
AttachEntry.PointeeBegin))
continue;
+ if (StateInfo.wasNewlyAllocated(AttachEntry.PointeeBegin).has_value())
+ continue;
+
// Give the pointer, and whatever points at it, device storage. A pointer
// attached to storage within an entry designates that entry's current
// address, so an entry cannot take a device allocation while such a pointer
@@ -1027,6 +994,52 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
}
}
+ // Whatever is still unsettled has a pointee this construct allocated, so it
+ // can be released. That is preferred over giving the pointer device storage:
+ // it settles the attachment outright, whereas an upgrade propagates to
+ // everything that points at the pointer.
+ //
+ // Releasing one pointee can leave another attachment unsettled, because the
+ // storage just released may itself hold a pointer whose own pointee is
+ // device-bound. So keep going until a pass releases nothing. Each release
+ // frees an allocation made by this construct, and none are created here, so
+ // this terminates.
+ for (bool Demoted = true; Demoted;) {
+ Demoted = false;
+ for (const auto &AttachEntry : StateInfo.AttachEntries) {
+ void **HstPtr = reinterpret_cast<void **>(AttachEntry.PointerBase);
+ void *HstPteeBegin = AttachEntry.PointeeBegin;
+ if (!WillAttach(AttachEntry) ||
+ !NeedsSettling(HstPtr, AttachEntry.PointerSize, HstPteeBegin))
+ continue;
+
+ // Only a pointee allocated for this construct may give its allocation up.
+ if (!StateInfo.wasNewlyAllocated(HstPteeBegin).has_value())
+ continue;
+
+ HostDataToTargetTy *PteeEntry = FindEntry(HstPteeBegin, /*Size=*/0);
+#ifndef NDEBUG
+ // Giving the allocation up moves the pointee's device address, which
+ // would strand any pointer already attached to it. That cannot happen
+ // here: the allocation was made by this construct, so no earlier
+ // construct could have attached to it, and this pre-pass runs before any
+ // attachment is performed for this one.
+ for (auto &[AttachedPtee, AttachedPtrs] : MappingInfo.AttachedPointers) {
+ (void)AttachedPtrs;
+ assert((reinterpret_cast<uintptr_t>(AttachedPtee) <
+ PteeEntry->HstPtrBegin ||
+ reinterpret_cast<uintptr_t>(AttachedPtee) >=
+ PteeEntry->HstPtrEnd) &&
+ "Releasing an allocation that a pointer is already attached to");
+ }
+#endif
+ if (MappingInfo.shareEntryStorageWithOriginal(
+ HDTTMap, PteeEntry, AsyncInfo) != OFFLOAD_SUCCESS)
+ return OFFLOAD_FAIL;
+ Demoted = true;
+ }
+ }
+
return OFFLOAD_SUCCESS;
}
diff --git a/offload/test/unified_shared_memory/close_attach_settle_no_waste.c b/offload/test/unified_shared_memory/close_attach_settle_no_waste.c
new file mode 100644
index 0000000000000..2530386cfaf6a
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_attach_settle_no_waste.c
@@ -0,0 +1,92 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+// Which side of an attachment changes has to be decided for the attachments
+// that have no choice before those that do.
+//
+// A pointee whose allocation was made by an enclosing construct cannot give it
+// up, so such an attachment can only be settled by giving the pointer device
+// storage. A pointee this construct allocated can be released instead, which is
+// preferred because it settles that attachment outright.
+//
+// Here both pointers live in the same structure, so the forced decision for one
+// of them determines the storage of the other's pointer too:
+//
+// s.p1 -> a1 a1 allocated by this construct, so it could be released
+// s.p2 -> a2 a2 allocated earlier, so s must hold a device address
+//
+// Deciding the releases first would release a1 while s was still the original
+// storage, and the s.p2 attachment would then give s device storage anyway --
+// leaving a1 released for nothing and its close request unmet. Settling the
+// forced attachment first leaves nothing for the release pass to do.
+
+#include <omp.h>
+#include <stdio.h>
+
+#pragma omp requires unified_shared_memory
+
+int a1[10], a2[10];
+
+struct S {
+ int *p1;
+ int *p2;
+};
+
+struct S s;
+
+int main(void) {
+ for (int i = 0; i < 10; ++i) {
+ a1[i] = 1;
+ a2[i] = 2;
+ }
+ s.p1 = &a1[0];
+ s.p2 = &a2[0];
+
+ // The unified_shared_memory requirement is registered when a device image is
+ // loaded, so the program needs a target region for it to take effect.
+#pragma omp target
+ {
+ }
+
+ int dev = omp_get_default_device();
+
+ // a2 is allocated here, so it cannot be released by the construct below.
+#pragma omp target enter data map(close, to : a2[0 : 10])
+
+#pragma omp target enter data map(alloc : s) map(close, to : s.p1[0 : 10]) \
+ map(alloc : s.p2[0 : 0])
+
+ // s holds device addresses, so a1 never needed releasing and its close
+ // request is met.
+ // CHECK: s is device-bound
+ printf("s is %s\n", omp_get_mapped_ptr(&s, dev) == (void *)&s ? "host-bound"
+ : "device-bound");
+
+ // CHECK: a1 is device-bound
+ printf("a1 is %s\n", omp_get_mapped_ptr(&a1[0], dev) == (void *)&a1[0]
+ ? "host-bound"
+ : "device-bound");
+
+ // CHECK: a2 is device-bound
+ printf("a2 is %s\n", omp_get_mapped_ptr(&a2[0], dev) == (void *)&a2[0]
+ ? "host-bound"
+ : "device-bound");
+
+ // The original pointers are intact either way.
+ // CHECK: host: s.p1 == &a1[0], s.p2 == &a2[0]
+ printf("host: s.p1 %s &a1[0], s.p2 %s &a2[0]\n",
+ s.p1 == &a1[0] ? "==" : "!=", s.p2 == &a2[0] ? "==" : "!=");
+
+ // CHECK: Done!
+ printf("Done!\n");
+ return 0;
+}
>From 7a48caa2cde8d21393883104c33f3f4a86edac51 Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Mon, 10 Aug 2026 02:01:51 -0700
Subject: [PATCH 20/29] [OFFLOAD] Work out the forced device storage
transitively
The obligation to hold device addresses propagates along a chain of
attachments, so looking only at the attachments whose pointee is immediately
unreleasable is not enough: a pointee this construct allocated can still be
required to keep its allocation because something below it cannot give one up.
Releasing it reintroduced a conflict that then had no resolution.
Compute the whole forced set from the unreleasable pointees before changing any
storage, settle those attachments, and exclude that set from the releases.
---
offload/libomptarget/omptarget.cpp | 52 ++++++++++++++---
.../close_attach_settle_forced_closure.c | 58 +++++++++++++++++++
2 files changed, 103 insertions(+), 7 deletions(-)
create mode 100644 offload/test/unified_shared_memory/close_attach_settle_forced_closure.c
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index 746aabac74dfb..b2043dd134257 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -905,11 +905,46 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
return PteeEntry && !PteeEntry->isHostBound();
};
- // Give a pointer device storage only where the pointee cannot give its own
- // allocation up: it was made by an enclosing construct, so its device address
- // may already have been obtained by the program. Those are the attachments
- // that have no other way to be settled, so they are settled first, and the
- // releases below then see the storage they forced.
+ // An attachment whose pointee cannot give its allocation up -- it was made by
+ // an enclosing construct, so its device address may already have been
+ // obtained by the program -- can only be settled by giving the pointer device
+ // storage. That obligation propagates: the pointer's storage then holds a
+ // device address, so anything attached to it must hold one too. Work the
+ // whole set out before touching anything, because a pointee reached this way
+ // must not be released even though this construct allocated it.
+ llvm::DenseSet<HostDataToTargetTy *> ForcedDevice;
+ {
+ SmallVector<HostDataToTargetTy *> Worklist;
+ auto Force = [&](HostDataToTargetTy *Entry) {
+ if (Entry && ForcedDevice.insert(Entry).second)
+ Worklist.push_back(Entry);
+ };
+
+ for (const auto &AttachEntry : StateInfo.AttachEntries) {
+ if (!WillAttach(AttachEntry) ||
+ StateInfo.wasNewlyAllocated(AttachEntry.PointeeBegin).has_value())
+ continue;
+ HostDataToTargetTy *PteeEntry =
+ FindEntry(AttachEntry.PointeeBegin, /*Size=*/0);
+ if (PteeEntry && !PteeEntry->isHostBound())
+ Force(PteeEntry);
+ }
+
+ while (!Worklist.empty()) {
+ HostDataToTargetTy *Entry = Worklist.pop_back_val();
+ for (const auto &AttachEntry : StateInfo.AttachEntries) {
+ if (!WillAttach(AttachEntry))
+ continue;
+ uintptr_t Ptee = reinterpret_cast<uintptr_t>(AttachEntry.PointeeBegin);
+ if (Ptee < Entry->HstPtrBegin || Ptee >= Entry->HstPtrEnd)
+ continue;
+ Force(FindEntry(AttachEntry.PointerBase, AttachEntry.PointerSize));
+ }
+ }
+ }
+
+ // Settle those attachments first, so that the releases below see the storage
+ // this obligation implies instead of undoing their own work.
for (const auto &AttachEntry : StateInfo.AttachEntries) {
void **HstPtr = reinterpret_cast<void **>(AttachEntry.PointerBase);
if (!WillAttach(AttachEntry) ||
@@ -917,7 +952,7 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
AttachEntry.PointeeBegin))
continue;
- if (StateInfo.wasNewlyAllocated(AttachEntry.PointeeBegin).has_value())
+ if (!ForcedDevice.contains(FindEntry(AttachEntry.PointeeBegin, /*Size=*/0)))
continue;
// Give the pointer, and whatever points at it, device storage. A pointer
@@ -1013,11 +1048,14 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
!NeedsSettling(HstPtr, AttachEntry.PointerSize, HstPteeBegin))
continue;
- // Only a pointee allocated for this construct may give its allocation up.
+ // Only a pointee allocated for this construct may give its allocation up,
+ // and not one whose storage is required to hold device addresses.
if (!StateInfo.wasNewlyAllocated(HstPteeBegin).has_value())
continue;
HostDataToTargetTy *PteeEntry = FindEntry(HstPteeBegin, /*Size=*/0);
+ if (ForcedDevice.contains(PteeEntry))
+ continue;
#ifndef NDEBUG
// Giving the allocation up moves the pointee's device address, which
// would strand any pointer already attached to it. That cannot happen
diff --git a/offload/test/unified_shared_memory/close_attach_settle_forced_closure.c b/offload/test/unified_shared_memory/close_attach_settle_forced_closure.c
new file mode 100644
index 0000000000000..4640278b883f0
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_attach_settle_forced_closure.c
@@ -0,0 +1,58 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+// The obligation to hold device addresses propagates along a chain, so it has to
+// be worked out transitively before any storage is changed.
+//
+// a -> b -> x
+//
+// x was allocated by an earlier construct, so it cannot give that allocation up,
+// which means b must hold a device address. b is allocated by this construct, so
+// it would otherwise be a candidate for release on account of a -- but releasing
+// it would reintroduce the very conflict on x that cannot be resolved. b is
+// reachable from x, so it has to be left alone and a upgraded instead.
+//
+// Looking only at attachments whose pointee is immediately unreleasable misses
+// this, because b starts out device-bound and so the a -> b attachment looks
+// like an ordinary candidate for release.
+
+#include <omp.h>
+#include <stdio.h>
+
+#pragma omp requires unified_shared_memory
+
+int x = 9;
+int *b = &x;
+int **a = &b;
+
+int main(void) {
+ // The unified_shared_memory requirement is registered when a device image is
+ // loaded, so the program needs a target region for it to take effect.
+#pragma omp target
+ {
+ }
+
+ // Allocated here, so the construct below cannot release it.
+#pragma omp target enter data map(close, to : x)
+
+#pragma omp target data map(close, to : b) map(alloc : a) \
+ map(alloc : b[0 : 1], a[0 : 1])
+ {
+ }
+
+ // CHECK: a == &b, b == &x
+ printf("a %s &b, b %s &x\n", a == &b ? "==" : "!=", b == &x ? "==" : "!=");
+
+ // CHECK: Done!
+ printf("Done!\n");
+ return 0;
+}
>From 0ad1e21aef5259ba17ed0cb767b14943af9d57ed Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Mon, 10 Aug 2026 02:23:18 -0700
Subject: [PATCH 21/29] [OFFLOAD] Document the settling phases, and that
PTR_AND_OBJ is not covered
The comment still described the earlier order, where releases came first. Say
what the three phases are and why they are in that order, with a diagram, and
why no decision is ever reversed.
Also record that attachments for PTR_AND_OBJ entries bypass this, since they are
performed as the mapping is processed. That encoding is on its way out; mapper
members no longer lower to it after a recent change to mapper code generation,
which leaves C++ by-reference captures.
---
offload/libomptarget/omptarget.cpp | 89 +++++++++++++++++++++---------
1 file changed, 64 insertions(+), 25 deletions(-)
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index b2043dd134257..60da695044e67 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -828,31 +828,70 @@ int targetDataBegin(ident_t *Loc, DeviceTy &Device, int32_t ArgNum,
///
/// One of the two sides has to change its storage so that this does not happen,
/// and only a mapping created by this construct can, since the device address
-/// of a mapping that was already present may have been obtained by the program.
-/// So, for each pending attachment whose pointer is shared with the original:
+/// of a mapping that was already present may have been obtained by the program,
+/// through omp_get_mapped_ptr, use_device_ptr, or having been passed to an
+/// earlier kernel.
///
-/// - if the pointee's storage was allocated for this construct, it gives that
-/// allocation up and shares storage with the original, which makes the
-/// address to be attached the original one;
-/// - otherwise, if the pointer's mapping was created by this construct, the
-/// pointer takes a device allocation, so that the assignment does not reach
-/// the original pointer;
-/// - otherwise neither side can change, and processAttachEntries reports it.
+/// Both changes are available:
///
-/// This runs as a pre-pass, over all the entries, for two reasons.
+/// - releasing the pointee's allocation, so that the address to be attached is
+/// the original one and the assignment is a no-op;
+/// - giving the pointer a device allocation, so that the assignment does not
+/// reach the original pointer.
///
-/// Giving up an allocation moves the pointee's device address, which
-/// invalidates any pointer already attached to it. Deciding per-entry while
-/// interleaving attachments would make the outcome depend on the order the
-/// entries happen to be in: a pointee could be attached to through one pointer
-/// and only then be demoted for another, stranding the first. Settling every
-/// entry first means a demotion cannot strand an attachment, so it never has to
-/// be undone.
+/// Releasing is preferred, because it settles that attachment outright, whereas
+/// giving a pointer device storage moves that pointer's own address and so
+/// propagates to everything attached to it. But an attachment whose pointee is
+/// device-bound from an enclosing construct has no choice: the pointee cannot
+/// be released, so the pointer must be upgraded. Those obligations therefore
+/// have to be known before anything is released, or a release can turn out to
+/// have been pointless -- and worse, releasing something that was obliged to
+/// stay device-bound reintroduces a conflict that then has no resolution.
///
-/// The demotions are also all applied before any upgrade is considered. A
-/// demotion removes the very disparity that would make the other side need an
-/// upgrade, so this avoids giving a pointer a device allocation that a
-/// subsequent demotion would have made unnecessary.
+/// Hence three phases, none of which mutates before the obligations are known:
+///
+/// attach entries
+/// |
+/// v
+/// +-------------------+ an attachment only happens if a side was newly
+/// | 0. eligible? | mapped here, or it is unconditional; settling one
+/// +-------------------+ that will be skipped would report a false conflict
+/// |
+/// v
+/// +-------------------+ seed: pointees that are device-bound and NOT
+/// | 1. forced set | allocated here, so immovable
+/// | (no mutation) | propagate: anything attached inside a forced entry
+/// +-------------------+ is forced too, transitively
+/// |
+/// v
+/// +-------------------+ upgrade those pointers, cascading backwards over
+/// | 2. forced | the recorded attachments and the pending ones
+/// | upgrades | reaching a pre-existing host-bound entry => error
+/// +-------------------+
+/// |
+/// v
+/// +-------------------+ everything still unsettled has a pointee this
+/// | 3. releases, | construct allocated, so release it; skip the
+/// | to a fixpoint | forced set. Repeat: a release can leave another
+/// +-------------------+ attachment unsettled, since the storage just
+/// | released may itself hold a pointer whose own
+/// v pointee is device-bound
+/// all settled
+///
+/// Each phase moves in one direction only, and phase 3 never touches what phase
+/// 2 forced, so no decision is ever reversed. Phase 2 terminates because an
+/// upgrade makes an entry device-bound and only host-bound entries are visited;
+/// phase 3 terminates because each release frees an allocation made by this
+/// construct and none are created here.
+///
+/// Attachments for PTR_AND_OBJ map entries are not covered: those are performed
+/// as the mapping is processed, before this runs, so such a pointer can still
+/// be assigned a device address in its original storage. That encoding is on
+/// its way out, as attachment becomes the way every pointer mapping is
+/// expressed, and the remaining users are shrinking: declare-mapper members no
+/// longer lower to it after a recent change to mapper code generation, which
+/// leaves C++ by-reference captures. Deferring these the same way is what fixes
+/// it, once they are no longer emitted at all.
static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
AsyncInfoTy &AsyncInfo) {
MappingInfoTy &MappingInfo = Device.getMappingInfo();
@@ -1039,8 +1078,8 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
// device-bound. So keep going until a pass releases nothing. Each release
// frees an allocation made by this construct, and none are created here, so
// this terminates.
- for (bool Demoted = true; Demoted;) {
- Demoted = false;
+ for (bool Released = true; Released;) {
+ Released = false;
for (const auto &AttachEntry : StateInfo.AttachEntries) {
void **HstPtr = reinterpret_cast<void **>(AttachEntry.PointerBase);
void *HstPteeBegin = AttachEntry.PointeeBegin;
@@ -1074,7 +1113,7 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
if (MappingInfo.shareEntryStorageWithOriginal(
HDTTMap, PteeEntry, AsyncInfo) != OFFLOAD_SUCCESS)
return OFFLOAD_FAIL;
- Demoted = true;
+ Released = true;
}
}
@@ -1102,7 +1141,7 @@ int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
<< " deferred ATTACH map entries";
// Decide which side of each attachment holds device storage before performing
- // any of them, so that a demotion cannot strand an earlier attachment.
+ // any of them, so that a release cannot strand an earlier attachment.
if (settleAttachStorage(Device, StateInfo, AsyncInfo) != OFFLOAD_SUCCESS)
return OFFLOAD_FAIL;
>From 68bcd448ea6910f8e39316b0d94351a00f377aac Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Mon, 10 Aug 2026 02:28:24 -0700
Subject: [PATCH 22/29] [OFFLOAD] Note that the mapping table is held across
all settling phases
Recorded as a known cost: the decisions are taken against entry pointers, so the
table has to stay stable across the phases, which serialises concurrent
constructs on the same device -- including across allocations, transfers, and
the wait before a release frees storage.
---
offload/libomptarget/omptarget.cpp | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index 60da695044e67..2c7dbbb038a1c 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -884,6 +884,13 @@ int targetDataBegin(ident_t *Loc, DeviceTy &Device, int32_t ArgNum,
/// phase 3 terminates because each release frees an allocation made by this
/// construct and none are created here.
///
+/// The mapping table is held for all three phases, because the decisions are
+/// taken against entry pointers and the table has to stay stable across them.
+/// That serialises concurrent constructs on the same device for the duration,
+/// including across the allocations, transfers and the wait before a release
+/// frees storage. Worth revisiting if that contention is ever measured to
+/// matter, but not separable from the decisions themselves.
+///
/// Attachments for PTR_AND_OBJ map entries are not covered: those are performed
/// as the mapping is processed, before this runs, so such a pointer can still
/// be assigned a device address in its original storage. That encoding is on
>From db4a001b39eb6d7e91dffa8ee98493499ecc6ebc Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Mon, 10 Aug 2026 15:39:51 -0700
Subject: [PATCH 23/29] [OFFLOAD] Hold back transfers into storage that
attachment may release
An allocation made for a close mapping under unified shared memory may still be
released, when a pointer that has to keep the original storage is attached to
it. The initial transfer was issued as the mapping was processed, so it was
copied into storage that was then freed -- and the release had to wait for that
copy to complete before it could free the storage, stalling the construct in the
middle.
Hold such a transfer back until the storage is settled, then issue it or drop it
according to what the entry became. On a 4MB close pointee whose allocation is
released, this takes the construct from 0.272ms to 0.018ms per iteration, and
the cost stops scaling with the size of the mapping. Measured on the x86_64 host
plugin, where a transfer and a free are both cheap; a real device queue should
gain at least as much.
Both callers now enter attachment processing when only deferred transfers are
pending, since those still have to be issued when nothing is attached.
---
offload/include/OpenMP/Mapping.h | 11 +++
offload/libomptarget/OpenMP/Mapping.cpp | 18 +++++
offload/libomptarget/interface.cpp | 7 +-
offload/libomptarget/omptarget.cpp | 39 +++++++++-
.../close_attach_deferred_transfer.c | 77 +++++++++++++++++++
5 files changed, 147 insertions(+), 5 deletions(-)
create mode 100644 offload/test/unified_shared_memory/close_attach_deferred_transfer.c
diff --git a/offload/include/OpenMP/Mapping.h b/offload/include/OpenMP/Mapping.h
index 8e5cebecf1007..a1fa79a809cd4 100644
--- a/offload/include/OpenMP/Mapping.h
+++ b/offload/include/OpenMP/Mapping.h
@@ -550,6 +550,17 @@ struct StateInfoTy {
/// Key: host pointer, Value: mapped size.
llvm::DenseMap<void *, int64_t> HostPathToRanges;
+ /// Initial TO transfers into an allocation that settling may release, held
+ /// back until the decisions are made so that a released allocation is never
+ /// copied into. Flushed by processAttachEntries, which drops the ones whose
+ /// entry ended up sharing storage with the original.
+ struct DeferredSubmitTy {
+ void *HstPtrBegin;
+ int64_t Size;
+ HostDataToTargetTy *Entry;
+ };
+ llvm::SmallVector<DeferredSubmitTy> DeferredSubmits;
+
/// Host pointers that had a FROM entry, but for which a data transfer was
/// skipped due to the ref-count not being zero.
/// Key: host pointer, Value: data size.
diff --git a/offload/libomptarget/OpenMP/Mapping.cpp b/offload/libomptarget/OpenMP/Mapping.cpp
index 8762b7d5a241a..fe84ac8311b8e 100644
--- a/offload/libomptarget/OpenMP/Mapping.cpp
+++ b/offload/libomptarget/OpenMP/Mapping.cpp
@@ -423,6 +423,24 @@ TargetPointerResultTy MappingInfoTy::getTargetPointer(
return std::move(LR.TPR);
}
+ // An allocation made for a close mapping under unified shared memory may
+ // still be released, if a pointer that has to keep the original storage is
+ // attached to it. Copying into it first would be wasted, and the release
+ // would then have to wait for that copy before it could free the storage.
+ // So hold the transfer back; processAttachEntries issues it once the
+ // storage is settled, or drops it if the allocation was released.
+ if (StateInfo && HasCloseModifier &&
+ PM->getRequirements() & OMP_REQ_UNIFIED_SHARED_MEMORY) {
+ ODBG(ODT_Mapping) << "Deferring the transfer of " << Size
+ << " bytes (hst:" << HstPtrBegin
+ << ") -> (tgt:" << LR.TPR.TargetPointer
+ << ") until the storage for pointer attachment is "
+ "settled";
+ StateInfo->DeferredSubmits.push_back(
+ {HstPtrBegin, Size, LR.TPR.getEntry()});
+ return std::move(LR.TPR);
+ }
+
ODBG(ODT_Mapping) << "Moving " << Size << " bytes (hst:" << HstPtrBegin
<< ") -> (tgt:" << LR.TPR.TargetPointer << ")";
diff --git a/offload/libomptarget/interface.cpp b/offload/libomptarget/interface.cpp
index 184a696d884df..b66c4a0b3c92f 100644
--- a/offload/libomptarget/interface.cpp
+++ b/offload/libomptarget/interface.cpp
@@ -182,8 +182,11 @@ targetData(ident_t *Loc, int64_t DeviceId, int32_t ArgNum, void **ArgsBase,
StateInfo.get(), /*FromMapper=*/false);
if (Rc == OFFLOAD_SUCCESS) {
- // Process deferred ATTACH entries BEFORE synchronization
- if (StateInfo && !StateInfo->AttachEntries.empty())
+ // Process deferred ATTACH entries BEFORE synchronization. Also entered with
+ // no such entries, because transfers held back for storage that attachment
+ // might have released still have to be issued.
+ if (StateInfo && (!StateInfo->AttachEntries.empty() ||
+ !StateInfo->DeferredSubmits.empty()))
Rc = processAttachEntries(*DeviceOrErr, *StateInfo, AsyncInfo);
if (Rc == OFFLOAD_SUCCESS)
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index 2c7dbbb038a1c..3d59b28c0c568 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -1127,6 +1127,34 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
return OFFLOAD_SUCCESS;
}
+/// Issue the transfers that were held back until the storage was settled.
+///
+/// An entry that ended up sharing storage with the original needs none: the
+/// device reads the original storage, which already holds the data. Anything
+/// else has a device allocation that is now known to be staying.
+static int flushDeferredSubmits(DeviceTy &Device, StateInfoTy &StateInfo,
+ AsyncInfoTy &AsyncInfo) {
+ for (const auto &[HstPtrBegin, Size, Entry] : StateInfo.DeferredSubmits) {
+ if (Entry->isHostBound()) {
+ ODBG(ODT_Mapping) << "Dropping the deferred transfer of " << Size
+ << " bytes (hst:" << HstPtrBegin
+ << "): its storage is shared with the original";
+ continue;
+ }
+ void *TgtPtrBegin = reinterpret_cast<void *>(Entry->TgtPtrBegin);
+ ODBG(ODT_Mapping) << "Moving " << Size
+ << " deferred bytes (hst:" << HstPtrBegin
+ << ") -> (tgt:" << TgtPtrBegin << ")";
+ if (Device.submitData(TgtPtrBegin, HstPtrBegin, Size, AsyncInfo, Entry) !=
+ OFFLOAD_SUCCESS) {
+ REPORT() << "Copying data to device failed.";
+ return OFFLOAD_FAIL;
+ }
+ }
+ StateInfo.DeferredSubmits.clear();
+ return OFFLOAD_SUCCESS;
+}
+
int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
AsyncInfoTy &AsyncInfo) {
// Report all tracked allocations from both main loop and ATTACH processing
@@ -1142,7 +1170,7 @@ int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
}
if (StateInfo.AttachEntries.empty())
- return OFFLOAD_SUCCESS;
+ return flushDeferredSubmits(Device, StateInfo, AsyncInfo);
ODBG(ODT_Mapping) << "Processing " << StateInfo.AttachEntries.size()
<< " deferred ATTACH map entries";
@@ -1152,6 +1180,9 @@ int processAttachEntries(DeviceTy &Device, StateInfoTy &StateInfo,
if (settleAttachStorage(Device, StateInfo, AsyncInfo) != OFFLOAD_SUCCESS)
return OFFLOAD_FAIL;
+ if (flushDeferredSubmits(Device, StateInfo, AsyncInfo) != OFFLOAD_SUCCESS)
+ return OFFLOAD_FAIL;
+
bool TreatAttachAutoAsAlways = MappingConfig::get().TreatAttachAutoAsAlways;
if (TreatAttachAutoAsAlways)
ODBG(ODT_Mapping) << "Treating ATTACH(auto) as ATTACH(always) because "
@@ -2413,8 +2444,10 @@ static int processDataBefore(ident_t *Loc, int64_t DeviceId, void *HostPtr,
return OFFLOAD_FAIL;
}
- // Process collected ATTACH entries
- if (!StateInfo.AttachEntries.empty()) {
+ // Process collected ATTACH entries. Also entered with none, because transfers
+ // held back for storage that attachment might have released still have to be
+ // issued.
+ if (!StateInfo.AttachEntries.empty() || !StateInfo.DeferredSubmits.empty()) {
Ret = processAttachEntries(*DeviceOrErr, StateInfo, AsyncInfo);
if (Ret != OFFLOAD_SUCCESS) {
REPORT() << "Failed to process ATTACH entries.";
diff --git a/offload/test/unified_shared_memory/close_attach_deferred_transfer.c b/offload/test/unified_shared_memory/close_attach_deferred_transfer.c
new file mode 100644
index 0000000000000..a6cd8222a1f4a
--- /dev/null
+++ b/offload/test/unified_shared_memory/close_attach_deferred_transfer.c
@@ -0,0 +1,77 @@
+// RUN: %libomptarget-compile-run-and-check-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+// An allocation made for a close mapping under unified shared memory may still
+// be released, if a pointer that has to keep the original storage is attached to
+// it. The initial transfer into such an allocation is therefore held back until
+// the storage is settled, and either issued or dropped.
+//
+// Both outcomes have to leave the right data visible, which is what this checks.
+// Where the allocation is released the device reads the original storage, which
+// already holds the values; where it is kept the transfer has to have happened.
+
+#include <stdio.h>
+
+#pragma omp requires unified_shared_memory
+
+int released[10];
+int kept[10];
+int *p_released = &released[0];
+int *p_kept = &kept[0];
+
+int sum_released, sum_kept;
+
+int main(void) {
+ for (int i = 0; i < 10; ++i) {
+ released[i] = i + 1;
+ kept[i] = i + 1;
+ }
+
+ // The unified_shared_memory requirement is registered when a device image is
+ // loaded, so the program needs a target region for it to take effect.
+#pragma omp target
+ {
+ }
+
+ // The pointer is mapped afresh alongside it, so the allocation for this close
+ // mapping is released and its transfer dropped.
+#pragma omp target enter data map(alloc : p_released) \
+ map(close, to : p_released[0 : 10])
+
+ // Here the pointee is allocated by its own construct, so the pointer is given
+ // device storage instead and the transfer is issued.
+#pragma omp target enter data map(close, to : kept[0 : 10])
+#pragma omp target enter data map(alloc : p_kept) map(alloc : p_kept[0 : 0])
+
+#pragma omp target map(present, alloc : p_released, p_kept) \
+ map(from : sum_released, sum_kept)
+ {
+ sum_released = 0;
+ sum_kept = 0;
+ for (int i = 0; i < 10; ++i) {
+ sum_released += p_released[i];
+ sum_kept += p_kept[i];
+ }
+ }
+
+ // CHECK: released: 55, kept: 55
+ printf("released: %d, kept: %d\n", sum_released, sum_kept);
+
+ // CHECK: host: p_released == &released[0], p_kept == &kept[0]
+ printf("host: p_released %s &released[0], p_kept %s &kept[0]\n",
+ p_released == &released[0] ? "==" : "!=",
+ p_kept == &kept[0] ? "==" : "!=");
+
+ // CHECK: Done!
+ printf("Done!\n");
+ return 0;
+}
>From 7734303327c72ee48a65e630e5fd37f540234e10 Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Mon, 10 Aug 2026 15:54:15 -0700
Subject: [PATCH 24/29] [OFFLOAD] Skip the attachment-settling bookkeeping
without unified shared memory
Without unified shared memory a mapping never shares storage with the original,
so attachment cannot assign a device address to an original pointer and there is
nothing to settle. Leave settleAttachStorage before it takes the mapping table,
which it would otherwise hold for the duration of all three phases, and stop
recording the pointee-to-pointer index that only settling consults.
The tracking of new allocations is left as it is: the 'present' map-type
validation consults it whether or not unified shared memory is in effect.
---
offload/libomptarget/omptarget.cpp | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index 3d59b28c0c568..52a615698936c 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -455,8 +455,10 @@ static int performPointerAttachment(DeviceTy &Device, AsyncInfoTy &AsyncInfo,
// Record the pointer against its pointee, so that it can be told later
// whether the pointee's entry may take a device allocation. Recorded even
- // when the shadow pointer below turns out to be a duplicate.
- Device.getMappingInfo().recordAttachedPointer(HstPteeBegin, HstPtrAddr);
+ // when the shadow pointer below turns out to be a duplicate. Only settling
+ // consults this, so there is nothing to record without unified shared memory.
+ if (PM->getRequirements() & OMP_REQ_UNIFIED_SHARED_MEMORY)
+ Device.getMappingInfo().recordAttachedPointer(HstPteeBegin, HstPtrAddr);
// Add shadow pointer tracking
if (!PtrTPR.getEntry()->addShadowPointer(
@@ -901,6 +903,13 @@ int targetDataBegin(ident_t *Loc, DeviceTy &Device, int32_t ArgNum,
/// it, once they are no longer emitted at all.
static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
AsyncInfoTy &AsyncInfo) {
+ // Without unified shared memory a mapping never shares storage with the
+ // original, so attachment cannot assign a device address to an original
+ // pointer and there is nothing to settle. Leave before taking the mapping
+ // table, which the phases below would otherwise hold for their duration.
+ if (!(PM->getRequirements() & OMP_REQ_UNIFIED_SHARED_MEMORY))
+ return OFFLOAD_SUCCESS;
+
MappingInfoTy &MappingInfo = Device.getMappingInfo();
MappingInfoTy::HDTTMapAccessorTy HDTTMap =
MappingInfo.HostDataToTargetMap.getExclusiveAccessor();
>From acb1dee4ff2b507dee29fac56c13f2a483e93712 Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Mon, 10 Aug 2026 16:02:52 -0700
Subject: [PATCH 25/29] [OFFLOAD] Also settle attachments under auto zero-copy
Auto zero-copy makes mappings share storage with the original just as unified
shared memory does, and it is set on APUs when the program requested no
requirement at all, so testing for unified shared memory alone left those
attachments unsettled -- able to assign a device address to an original pointer.
Add mayShareStorageWithOriginal() next to the requirement flags, which is the
condition the rest of the mapping code already spells out by hand, and use it
for the settling guard, the attached-pointer index, and the deferred transfers.
---
offload/include/Shared/Requirements.h | 9 +++++++++
offload/libomptarget/OpenMP/Mapping.cpp | 2 +-
offload/libomptarget/omptarget.cpp | 15 ++++++++-------
3 files changed, 18 insertions(+), 8 deletions(-)
diff --git a/offload/include/Shared/Requirements.h b/offload/include/Shared/Requirements.h
index b16a1650f0c40..66f9c30fa5f4e 100644
--- a/offload/include/Shared/Requirements.h
+++ b/offload/include/Shared/Requirements.h
@@ -41,6 +41,15 @@ enum OpenMPOffloadingRequiresDirFlags : int64_t {
OMPX_REQ_AUTO_ZERO_COPY = 0x020
};
+/// Whether a mapping's corresponding storage can be the original storage
+/// itself, which is what makes pointer attachment able to assign a device
+/// address to an original pointer. True under unified shared memory, and also
+/// under auto zero-copy, which is set on APUs when no other requirement is.
+inline bool mayShareStorageWithOriginal(int64_t Requirements) {
+ return Requirements &
+ (OMP_REQ_UNIFIED_SHARED_MEMORY | OMPX_REQ_AUTO_ZERO_COPY);
+}
+
class RequirementCollection {
int64_t SetFlags = OMP_REQ_UNDEFINED;
diff --git a/offload/libomptarget/OpenMP/Mapping.cpp b/offload/libomptarget/OpenMP/Mapping.cpp
index fe84ac8311b8e..794b33961155d 100644
--- a/offload/libomptarget/OpenMP/Mapping.cpp
+++ b/offload/libomptarget/OpenMP/Mapping.cpp
@@ -430,7 +430,7 @@ TargetPointerResultTy MappingInfoTy::getTargetPointer(
// So hold the transfer back; processAttachEntries issues it once the
// storage is settled, or drops it if the allocation was released.
if (StateInfo && HasCloseModifier &&
- PM->getRequirements() & OMP_REQ_UNIFIED_SHARED_MEMORY) {
+ mayShareStorageWithOriginal(PM->getRequirements())) {
ODBG(ODT_Mapping) << "Deferring the transfer of " << Size
<< " bytes (hst:" << HstPtrBegin
<< ") -> (tgt:" << LR.TPR.TargetPointer
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index 52a615698936c..c5f702978f092 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -456,8 +456,9 @@ static int performPointerAttachment(DeviceTy &Device, AsyncInfoTy &AsyncInfo,
// Record the pointer against its pointee, so that it can be told later
// whether the pointee's entry may take a device allocation. Recorded even
// when the shadow pointer below turns out to be a duplicate. Only settling
- // consults this, so there is nothing to record without unified shared memory.
- if (PM->getRequirements() & OMP_REQ_UNIFIED_SHARED_MEMORY)
+ // consults this, so there is nothing to record when no mapping can share
+ // storage with the original.
+ if (mayShareStorageWithOriginal(PM->getRequirements()))
Device.getMappingInfo().recordAttachedPointer(HstPteeBegin, HstPtrAddr);
// Add shadow pointer tracking
@@ -903,11 +904,11 @@ int targetDataBegin(ident_t *Loc, DeviceTy &Device, int32_t ArgNum,
/// it, once they are no longer emitted at all.
static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
AsyncInfoTy &AsyncInfo) {
- // Without unified shared memory a mapping never shares storage with the
- // original, so attachment cannot assign a device address to an original
- // pointer and there is nothing to settle. Leave before taking the mapping
- // table, which the phases below would otherwise hold for their duration.
- if (!(PM->getRequirements() & OMP_REQ_UNIFIED_SHARED_MEMORY))
+ // When no mapping can share storage with the original, attachment cannot
+ // assign a device address to an original pointer and there is nothing to
+ // settle. Leave before taking the mapping table, which the phases below would
+ // otherwise hold for their duration.
+ if (!mayShareStorageWithOriginal(PM->getRequirements()))
return OFFLOAD_SUCCESS;
MappingInfoTy &MappingInfo = Device.getMappingInfo();
>From 894e8fb418a03c911ab7ee8f6b079193d50bb464 Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Mon, 10 Aug 2026 16:18:10 -0700
Subject: [PATCH 26/29] [OFFLOAD] Re-check the clobber guard when a deferred
transfer is issued
The check that stops a copy overwriting an attached pointer ran when the
transfer was recorded, so it could not see a pointer attached to that storage
later in the same mapping -- which is what PTR_AND_OBJ entries do, as they attach
while the mapping is processed. Repeat the check at the point the transfer is
issued, and drop the copy if a pointer now lies within it.
---
offload/libomptarget/omptarget.cpp | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index c5f702978f092..bb9cbe25ec33b 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -1151,6 +1151,24 @@ static int flushDeferredSubmits(DeviceTy &Device, StateInfoTy &StateInfo,
<< "): its storage is shared with the original";
continue;
}
+ // The clobber check that guards this copy ran when the transfer was
+ // recorded, so it did not see a pointer attached to this storage later in
+ // the mapping. Repeat it: such a pointer holds a device address, and copying
+ // the original storage over it would put the host address back.
+ auto FailOnPtrFound = [HstPtrBegin = HstPtrBegin,
+ Size = Size](ShadowPtrInfoTy &SP) {
+ if (SP.HstPtrAddr >= HstPtrBegin &&
+ SP.HstPtrAddr < (void *)((char *)HstPtrBegin + Size))
+ return OFFLOAD_FAIL;
+ return OFFLOAD_SUCCESS;
+ };
+ if (Entry->foreachShadowPointerInfo(FailOnPtrFound) == OFFLOAD_FAIL) {
+ ODBG(ODT_Mapping) << "Dropping the deferred transfer of " << Size
+ << " bytes (hst:" << HstPtrBegin
+ << "): a pointer is attached within it";
+ continue;
+ }
+
void *TgtPtrBegin = reinterpret_cast<void *>(Entry->TgtPtrBegin);
ODBG(ODT_Mapping) << "Moving " << Size
<< " deferred bytes (hst:" << HstPtrBegin
>From fbd70841fabdec7410b521f9ea9535358523a772 Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Mon, 10 Aug 2026 16:26:08 -0700
Subject: [PATCH 27/29] [OFFLOAD] Test an unsatisfiable attachment that does
not involve close
The close modifier is only one way to get a device-bound pointee.
omp_target_associate_ptr gives a variable device storage with no map modifier at
all, and a three-level chain over it reaches the same conflict: the associated
pointee cannot be changed, which forces the pointer that refers to it to hold a
device address, which in turn forces the pointer above that -- already present
sharing storage with the original, so unable to.
---
.../associate_ptr_attach_conflict.c | 90 +++++++++++++++++++
1 file changed, 90 insertions(+)
create mode 100644 offload/test/unified_shared_memory/associate_ptr_attach_conflict.c
diff --git a/offload/test/unified_shared_memory/associate_ptr_attach_conflict.c b/offload/test/unified_shared_memory/associate_ptr_attach_conflict.c
new file mode 100644
index 0000000000000..74cb6f7c759ab
--- /dev/null
+++ b/offload/test/unified_shared_memory/associate_ptr_attach_conflict.c
@@ -0,0 +1,90 @@
+// RUN: %libomptarget-compile-generic
+// RUN: %libomptarget-run-fail-generic 2>&1 | %fcheck-generic
+
+// REQUIRES: unified_shared_memory
+// UNSUPPORTED: clang-6, clang-7, clang-8, clang-9
+
+// amdgpu runtime crash
+// Fails on nvptx with error: an illegal memory access was encountered
+// UNSUPPORTED: amdgcn-amd-amdhsa
+// UNSUPPORTED: nvptx64-nvidia-cuda
+// UNSUPPORTED: nvptx64-nvidia-cuda-LTO
+// UNSUPPORTED: intelgpu
+
+// An attachment that cannot be satisfied is not particular to the close
+// modifier. All it takes is a pointee whose corresponding storage is device
+// memory while the pointer's corresponding storage is the original storage, and
+// omp_target_associate_ptr gives the pointee such storage without any map
+// modifier being involved.
+//
+// Three levels, p1 -> p2 -> x:
+//
+// - x is associated with device memory, so it is device-bound, and nothing can
+// change that: the association is the program's own and its address has been
+// handed out;
+// - p1 is mapped by a construct of its own, so by the time anything is attached
+// to it, it is already present with its storage shared with the original;
+// - the last construct maps p2 afresh and prescribes both attachments.
+//
+// p2 must hold a device address, because x is device-bound and cannot be
+// changed. That makes p2's own storage device memory, so p1 must hold a device
+// address too -- but p1 is already present sharing storage with the original, so
+// it cannot. Neither side of that attachment can move, and assigning a device
+// address to the original p1 is not an option, so the runtime reports it.
+//
+#include <omp.h>
+#include <stdio.h>
+
+#pragma omp requires unified_shared_memory
+
+int x[10];
+int *p2;
+int **p1;
+
+int main(void) {
+ // The unified_shared_memory requirement is registered when a device image is
+ // loaded, so the program needs a target region for it to take effect.
+#pragma omp target
+ {
+ }
+
+ int dev = omp_get_default_device();
+
+ int *x_device = (int *)omp_target_alloc(sizeof(int) * 10, dev);
+ if (!x_device) {
+ fprintf(stderr, "omp_target_alloc failed\n");
+ return 1;
+ }
+ if (omp_target_associate_ptr(&x[0], x_device, sizeof(int) * 10, 0, dev)) {
+ fprintf(stderr, "omp_target_associate_ptr failed\n");
+ return 1;
+ }
+
+ // CHECK: x is device-bound
+ fprintf(stderr, "x is %s\n",
+ omp_get_mapped_ptr(&x[0], dev) == (void *)&x[0] ? "host-bound"
+ : "device-bound");
+
+ p2 = &x[0];
+ p1 = &p2;
+
+ // Mapped on its own construct, so it is already present below.
+#pragma omp target enter data map(alloc : p1)
+
+ // CHECK: p1 is host-bound
+ fprintf(stderr, "p1 is %s\n",
+ omp_get_mapped_ptr(&p1, dev) == (void *)&p1 ? "host-bound"
+ : "device-bound");
+
+ // p2 is new here, and both attachments are prescribed. No close modifier.
+ //
+ // clang-format off
+ // CHECK: could not do pointer attachment
+ // CHECK-SAME: would have to be device-bound as well
+ // clang-format on
+#pragma omp target enter data map(alloc : p2) map(alloc : p2[0 : 10]) \
+ map(alloc : p1[0 : 1])
+
+ fprintf(stderr, "unreachable\n");
+ return 0;
+}
>From 99763290bec34804b1d02520c2586fbb52ec1e0d Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Mon, 10 Aug 2026 18:05:20 -0700
Subject: [PATCH 28/29] [OFFLOAD] Find the entry again when issuing a deferred
transfer
A deferred transfer remembered a pointer to the map entry it was going to copy
into. That is not safe for the window it spans: a delete map type resets an
entry's reference count and drops it to zero, so another thread can erase and
destroy the entry before the transfer is issued. None of the comparable places
that outlive a lookup rely on a bare entry pointer -- a target pointer result
holds the entry's lock, post-processing is covered by the data-end thread count,
and the attached-pointer index stores host addresses and is pruned on erase.
Record only the host address and size, and find the entry again under the
mapping table when the transfer happens, holding the table across the checks and
the copy so the entry cannot be erased in between. Drop the transfer if the
mapping is gone by then.
Also treat a shadow pointer as overlapping the copy when any part of it lies in
the copied range, rather than only when it starts there.
---
offload/include/OpenMP/Mapping.h | 5 ++-
offload/libomptarget/OpenMP/Mapping.cpp | 3 +-
offload/libomptarget/omptarget.cpp | 53 +++++++++++++++++++++----
3 files changed, 50 insertions(+), 11 deletions(-)
diff --git a/offload/include/OpenMP/Mapping.h b/offload/include/OpenMP/Mapping.h
index a1fa79a809cd4..5c1c8ad1818e4 100644
--- a/offload/include/OpenMP/Mapping.h
+++ b/offload/include/OpenMP/Mapping.h
@@ -554,10 +554,13 @@ struct StateInfoTy {
/// back until the decisions are made so that a released allocation is never
/// copied into. Flushed by processAttachEntries, which drops the ones whose
/// entry ended up sharing storage with the original.
+ /// Identified by host address and size, not by a pointer to the entry: the
+ /// entry can be erased before the transfer is issued, by another thread
+ /// deleting the mapping, so it has to be looked up again under the mapping
+ /// table when the transfer happens.
struct DeferredSubmitTy {
void *HstPtrBegin;
int64_t Size;
- HostDataToTargetTy *Entry;
};
llvm::SmallVector<DeferredSubmitTy> DeferredSubmits;
diff --git a/offload/libomptarget/OpenMP/Mapping.cpp b/offload/libomptarget/OpenMP/Mapping.cpp
index 794b33961155d..b999f36995a87 100644
--- a/offload/libomptarget/OpenMP/Mapping.cpp
+++ b/offload/libomptarget/OpenMP/Mapping.cpp
@@ -436,8 +436,7 @@ TargetPointerResultTy MappingInfoTy::getTargetPointer(
<< ") -> (tgt:" << LR.TPR.TargetPointer
<< ") until the storage for pointer attachment is "
"settled";
- StateInfo->DeferredSubmits.push_back(
- {HstPtrBegin, Size, LR.TPR.getEntry()});
+ StateInfo->DeferredSubmits.push_back({HstPtrBegin, Size});
return std::move(LR.TPR);
}
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index f5c6ccaf79fe1..4b24c01cd2690 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -1149,21 +1149,52 @@ static int settleAttachStorage(DeviceTy &Device, StateInfoTy &StateInfo,
/// else has a device allocation that is now known to be staying.
static int flushDeferredSubmits(DeviceTy &Device, StateInfoTy &StateInfo,
AsyncInfoTy &AsyncInfo) {
- for (const auto &[HstPtrBegin, Size, Entry] : StateInfo.DeferredSubmits) {
+ if (StateInfo.DeferredSubmits.empty())
+ return OFFLOAD_SUCCESS;
+
+ MappingInfoTy &MappingInfo = Device.getMappingInfo();
+
+ // Hold the mapping table across the whole flush. The entry has to be found
+ // again rather than remembered, because another thread deleting the mapping
+ // can erase it in between, and the checks below and the transfer itself all
+ // have to see the same entry: releasing the table between looking it up and
+ // copying into it would let it be erased in that window instead.
+ MappingInfoTy::HDTTMapAccessorTy HDTTMap =
+ MappingInfo.HostDataToTargetMap.getExclusiveAccessor();
+
+ for (const auto &[HstPtrBegin, Size] : StateInfo.DeferredSubmits) {
+ LookupResult LR = MappingInfo.lookupMapping(HDTTMap, HstPtrBegin, Size);
+ HostDataToTargetTy *Entry =
+ LR.Flags.IsContained ? LR.TPR.getEntry() : nullptr;
+
+ if (!Entry) {
+ ODBG(ODT_Mapping) << "Dropping the deferred transfer of " << Size
+ << " bytes (hst:" << HstPtrBegin
+ << "): the mapping is gone";
+ LR.TPR.setEntry(nullptr);
+ continue;
+ }
+
if (Entry->isHostBound()) {
ODBG(ODT_Mapping) << "Dropping the deferred transfer of " << Size
<< " bytes (hst:" << HstPtrBegin
<< "): its storage is shared with the original";
+ LR.TPR.setEntry(nullptr);
continue;
}
+
// The clobber check that guards this copy ran when the transfer was
// recorded, so it did not see a pointer attached to this storage later in
- // the mapping. Repeat it: such a pointer holds a device address, and copying
- // the original storage over it would put the host address back.
+ // the mapping. Repeat it: such a pointer holds a device address, and
+ // copying the original storage over it would put the host address back. A
+ // shadow pointer counts as overlapping if any of it lies in the copied
+ // range, not only if it starts there.
auto FailOnPtrFound = [HstPtrBegin = HstPtrBegin,
Size = Size](ShadowPtrInfoTy &SP) {
- if (SP.HstPtrAddr >= HstPtrBegin &&
- SP.HstPtrAddr < (void *)((char *)HstPtrBegin + Size))
+ char *SPBegin = reinterpret_cast<char *>(SP.HstPtrAddr);
+ char *SPEnd = SPBegin + SP.PtrSize;
+ char *Begin = reinterpret_cast<char *>(HstPtrBegin);
+ if (SPBegin < Begin + Size && Begin < SPEnd)
return OFFLOAD_FAIL;
return OFFLOAD_SUCCESS;
};
@@ -1171,19 +1202,25 @@ static int flushDeferredSubmits(DeviceTy &Device, StateInfoTy &StateInfo,
ODBG(ODT_Mapping) << "Dropping the deferred transfer of " << Size
<< " bytes (hst:" << HstPtrBegin
<< "): a pointer is attached within it";
+ LR.TPR.setEntry(nullptr);
continue;
}
- void *TgtPtrBegin = reinterpret_cast<void *>(Entry->TgtPtrBegin);
+ void *TgtPtrBegin = reinterpret_cast<void *>(
+ Entry->TgtPtrBegin +
+ (reinterpret_cast<uintptr_t>(HstPtrBegin) - Entry->HstPtrBegin));
ODBG(ODT_Mapping) << "Moving " << Size
<< " deferred bytes (hst:" << HstPtrBegin
<< ") -> (tgt:" << TgtPtrBegin << ")";
- if (Device.submitData(TgtPtrBegin, HstPtrBegin, Size, AsyncInfo, Entry) !=
- OFFLOAD_SUCCESS) {
+ int Ret =
+ Device.submitData(TgtPtrBegin, HstPtrBegin, Size, AsyncInfo, Entry);
+ LR.TPR.setEntry(nullptr);
+ if (Ret != OFFLOAD_SUCCESS) {
REPORT() << "Copying data to device failed.";
return OFFLOAD_FAIL;
}
}
+
StateInfo.DeferredSubmits.clear();
return OFFLOAD_SUCCESS;
}
>From 8bcb4fba3454c1e0654e6e18633269f0a0af1971 Mon Sep 17 00:00:00 2001
From: Abhinav Gaba <abhinav.gaba at intel.com>
Date: Mon, 10 Aug 2026 18:16:04 -0700
Subject: [PATCH 29/29] [OFFLOAD] Compare addresses as integers in the
deferred-transfer overlap test
Relational comparison of pointers into unrelated objects is not defined, so
compare the range endpoints as uintptr_t, which is what the mapping lookup
itself does.
---
offload/libomptarget/omptarget.cpp | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/offload/libomptarget/omptarget.cpp b/offload/libomptarget/omptarget.cpp
index 4b24c01cd2690..98e79b0094c01 100644
--- a/offload/libomptarget/omptarget.cpp
+++ b/offload/libomptarget/omptarget.cpp
@@ -1191,9 +1191,9 @@ static int flushDeferredSubmits(DeviceTy &Device, StateInfoTy &StateInfo,
// range, not only if it starts there.
auto FailOnPtrFound = [HstPtrBegin = HstPtrBegin,
Size = Size](ShadowPtrInfoTy &SP) {
- char *SPBegin = reinterpret_cast<char *>(SP.HstPtrAddr);
- char *SPEnd = SPBegin + SP.PtrSize;
- char *Begin = reinterpret_cast<char *>(HstPtrBegin);
+ uintptr_t SPBegin = reinterpret_cast<uintptr_t>(SP.HstPtrAddr);
+ uintptr_t SPEnd = SPBegin + SP.PtrSize;
+ uintptr_t Begin = reinterpret_cast<uintptr_t>(HstPtrBegin);
if (SPBegin < Begin + Size && Begin < SPEnd)
return OFFLOAD_FAIL;
return OFFLOAD_SUCCESS;
More information about the cfe-commits
mailing list