-
Notifications
You must be signed in to change notification settings - Fork 10
GPU dynamic memory allocation #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
167fa2c
f704151
8616caa
3994067
880ea47
cdf4ef9
2e96857
5d5a79b
dc57275
2de48bd
5a4acc2
f8f2a40
3e7fb9e
bdbdec3
da128ee
4ba1c06
9ea9bf9
288c328
ba79475
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -27,87 +27,59 @@ namespace ecolab | |||||
| }; | ||||||
|
|
||||||
| inline __attribute__((noinline)) bool& fatalErrorFlag() { | ||||||
| #ifdef SYCL_LANGUAGE_VERSION | ||||||
| return sycl::ext::oneapi::group_local_memory<FatalErrorFlag>(syclGroup(),false)->flag; | ||||||
| #else | ||||||
| static bool flag; | ||||||
| return flag; | ||||||
| #endif | ||||||
| } | ||||||
|
|
||||||
| // Bounded MPMC circular buffer queue for SYCL using per-slot sequence numbers. | ||||||
| // dequeue() returns ~0U when queue appears empty (non-blocking empty signal). | ||||||
| template <unsigned size> | ||||||
| class Queue | ||||||
| class Stack | ||||||
| { | ||||||
| static_assert((size&(size-1))==0,"size must be power of two"); | ||||||
| constexpr static unsigned mask=size-1; | ||||||
|
|
||||||
| struct Slot | ||||||
| { | ||||||
| unsigned seq; | ||||||
| unsigned value; | ||||||
| }; | ||||||
|
|
||||||
| Slot slots[size]; | ||||||
| unsigned head=size, tail=0; | ||||||
|
|
||||||
| using Atomic=sycl::atomic_ref<unsigned,sycl::memory_order::relaxed,sycl::memory_scope::device>; | ||||||
| unsigned slots[size]; | ||||||
| unsigned top=size; //empty stack, stack grows down | ||||||
|
|
||||||
| using Atomic=sycl::atomic_ref<unsigned,sycl::memory_order::acq_rel,sycl::memory_scope::device>; | ||||||
| CLASSDESC_ACCESS(Stack); | ||||||
| public: | ||||||
| void init() { | ||||||
| top=0; // full stack | ||||||
| for (unsigned i=syclItem().get_global_linear_id(); i<size; | ||||||
| i+=syclItem().get_global_range().size()) { | ||||||
| slots[i].value=i; | ||||||
| slots[i].seq=i+1; | ||||||
| } | ||||||
| i+=syclItem().get_global_range().size()) | ||||||
| slots[i]=i; | ||||||
| } | ||||||
|
|
||||||
| void enqueue(unsigned x) | ||||||
| void push(unsigned x) | ||||||
| { | ||||||
| while (true) | ||||||
| { | ||||||
| Atomic headAtomic(head); | ||||||
| unsigned pos=headAtomic.load(); | ||||||
| Slot& slot=slots[pos & mask]; | ||||||
| Atomic seqAtomic(slot.seq); | ||||||
| unsigned seq=seqAtomic.load(sycl::memory_order::acquire); | ||||||
| int diff=int(seq)-int(pos); | ||||||
|
|
||||||
| if (diff==0 && headAtomic.compare_exchange_strong(pos,pos+1)) | ||||||
| { | ||||||
| slot.value=x; | ||||||
| Atomic publish(slot.seq); | ||||||
| publish.store(pos+1,sycl::memory_order::release); | ||||||
| return; | ||||||
| } | ||||||
| } | ||||||
| slots[--Atomic(top)]=x; | ||||||
| } | ||||||
|
highperformancecoder marked this conversation as resolved.
|
||||||
|
|
||||||
| unsigned dequeue() | ||||||
| unsigned pop() | ||||||
| { | ||||||
| while (true) | ||||||
| { | ||||||
| Atomic tailAtomic(tail); | ||||||
| unsigned pos=tailAtomic.load(); | ||||||
| Slot& slot=slots[pos & mask]; | ||||||
| Atomic seqAtomic(slot.seq); | ||||||
| unsigned seq=seqAtomic.load(sycl::memory_order::acquire); | ||||||
| int diff=int(seq)-int(pos+1); | ||||||
| Atomic t(top); | ||||||
| unsigned p=t++; | ||||||
| if (p>=size) {t=size; return ~0;} // stack empty | ||||||
| return slots[p]; | ||||||
| } | ||||||
|
highperformancecoder marked this conversation as resolved.
|
||||||
|
|
||||||
| if (diff==0 && tailAtomic.compare_exchange_strong(pos,pos+1)) | ||||||
| { | ||||||
| unsigned v=slot.value; | ||||||
| Atomic release(slot.seq); | ||||||
| release.store(pos+size,sycl::memory_order::release); | ||||||
| return v; | ||||||
| } | ||||||
| if (diff<0) | ||||||
| { | ||||||
| return ~0U; // signal buffer empty, don't wait | ||||||
| } | ||||||
| } | ||||||
| // move contents of \a x onto this. Not threadsafe, call from host | ||||||
| void appendAndDiscard(Stack& x) { | ||||||
| top-=size-x.top; | ||||||
| memcpy(slots+top, x.slots+x.top, (size-x.top)*sizeof(slots[0])); | ||||||
| x.top=size; | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
| template <unsigned order> class DeviceAllocator; | ||||||
| /// empty allocator to terminate template recursion | ||||||
| template <> class DeviceAllocator<maxOrder> { | ||||||
| template <> class DeviceAllocator<ecolab::maxOrder> { | ||||||
| public: | ||||||
| void* allocate(size_t sz) { | ||||||
| if (groupLeader()) | ||||||
|
|
@@ -121,30 +93,38 @@ namespace ecolab | |||||
| } | ||||||
| void deallocate(void* p, size_t) {sycl::ext::oneapi::experimental::printf("%p leaked on device\n",p);} | ||||||
| void init() {} | ||||||
| void recycleDiscardPile() {} | ||||||
| }; | ||||||
|
|
||||||
| template <unsigned order=minOrder> class DeviceAllocator | ||||||
| { | ||||||
| constexpr static unsigned pageSize=1<<order; | ||||||
| constexpr static unsigned numPages=poolSize/pageSize; | ||||||
| Queue<numPages> queue; | ||||||
| Stack<numPages> queue; | ||||||
| Stack<numPages> discard; // discard pile | ||||||
| char memory[poolSize]; | ||||||
| DeviceAllocator<order+2> nextAllocator; // next size up allocator | ||||||
| CLASSDESC_ACCESS(DeviceAllocator); | ||||||
| public: | ||||||
| void init() { | ||||||
| for (int pagesLeftToInit=numPages; pagesLeftToInit>0; pagesLeftToInit-=workGroupSize) | ||||||
| syclQ().parallel_for(std::min(workGroupSize,unsigned(pagesLeftToInit)), | ||||||
| [this](size_t) {queue.init();}); | ||||||
| auto chunkOWork=syclQ().get_device(). | ||||||
| get_info<sycl::info::device::max_compute_units>()*workGroupSize; | ||||||
| syclQ().parallel_for(std::min(chunkOWork,unsigned(numPages)), | ||||||
| [this](size_t) {queue.init();}); | ||||||
| nextAllocator.init(); | ||||||
| } | ||||||
| void recycleDiscardPile() { | ||||||
| queue.appendAndDiscard(discard); | ||||||
| nextAllocator.recycleDiscardPile(); | ||||||
| } | ||||||
| // all members of group get the same pointer | ||||||
| void* allocate(size_t size) { | ||||||
| if (size==0) return nullptr; | ||||||
| if (size<=pageSize) { | ||||||
| unsigned offs; | ||||||
| if (groupLeader()) offs=queue.dequeue(); | ||||||
| unsigned offs=~0U; | ||||||
| if (localThreadId()==0) offs=queue.pop(); | ||||||
| #ifdef __SYCL_DEVICE_ONLY__ | ||||||
| offs=sycl::group_broadcast(syclGroup(),offs); | ||||||
| offs=sycl::group_broadcast(syclGroup(),offs,0); | ||||||
| #endif | ||||||
| if (offs!=~0U) | ||||||
| return memory+(offs<<order); | ||||||
|
|
@@ -154,9 +134,15 @@ namespace ecolab | |||||
| void deallocate(void* p, size_t size) { | ||||||
| if (!p) return; | ||||||
| if (p>=memory && p<memory+poolSize) { | ||||||
| groupBarrier(); | ||||||
| #ifdef __SYCL_DEVICE_ONLY__ | ||||||
| if (groupLeader()) | ||||||
| queue.enqueue((reinterpret_cast<char*>(p)-memory)>>order); | ||||||
| // push onto discard pile to avoid race condition | ||||||
| discard.push((reinterpret_cast<char*>(p)-memory)>>order); | ||||||
| #else | ||||||
| // on host, we can push back onto stack. Note this is not | ||||||
| // threadsafe, so not to be used with OpenMP. | ||||||
| queue.push((reinterpret_cast<char*>(p)-memory)>>order); | ||||||
| #endif | ||||||
| return; | ||||||
| } | ||||||
| nextAllocator.deallocate(p,size); | ||||||
|
|
@@ -182,9 +168,14 @@ namespace ecolab | |||||
| using difference_type=std::ptrdiff_t; | ||||||
| using propagate_on_container_move_assignment=std::true_type; | ||||||
|
|
||||||
| DeviceAllocator<>* allocator=&deviceAllocator(); | ||||||
| DeviceAllocator<>* allocator; | ||||||
|
|
||||||
| GlobalDeviceAllocator() = default; // note: default constructor must be called on host | ||||||
| #ifdef __SYCL_DEVICE_ONLY__ | ||||||
| GlobalDeviceAllocator(): allocator(nullptr) {} // = delete; | ||||||
| #else | ||||||
| GlobalDeviceAllocator() // note: default constructor must be called on host | ||||||
| {allocator=&deviceAllocator();} | ||||||
| #endif | ||||||
|
Comment on lines
+171
to
+178
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find device-side default construction of arrays that use GlobalDeviceAllocator.
rg -nP -C4 '\barray\s*<[^>]*(GlobalDeviceAllocator|Allocator<)[^>]*>\s+\w+\s*(;|\{\s*\})' --type=cpp --type=cc -g '!**/build/**'
# Inspect the terminating allocator and allocate() paths for null handling.
fd -t f 'DeviceAllocator.h' | xargs -r sed -n '80,200p'Repository: highperformancecoder/ecolab Length of output: 4390 🏁 Script executed: #!/bin/bash
set -eu
echo '--- GlobalDeviceAllocator and array references ---'
rg -n -C3 'GlobalDeviceAllocator|m_allocator|array\s*\(' include src test examples --glob '!**/build/**' 2>/dev/null || true
echo '--- arrays.h structure and constructors ---'
fd -t f 'arrays.h' . | while read -r f; do
echo "FILE: $f"
ast-grep outline "$f" | head -120
rg -n -C8 'm_allocator|array\s*\(|allocate\s*\(' "$f"
done
echo '--- array declarations and allocator aliases ---'
rg -n -C3 'using .*Allocator|typedef .*Allocator|array\s*<' . \
--glob '*.{h,hpp,c,cpp,cc,cxx}' \
--glob '!**/build/**' \
--glob '!**/dist/**' 2>/dev/null || trueRepository: highperformancecoder/ecolab Length of output: 50384 🏁 Script executed: #!/bin/bash
set -eu
echo '--- All direct GlobalDeviceAllocator uses ---'
rg -n -C5 'GlobalDeviceAllocator' . --glob '!**/build/**' --glob '!**/dist/**'
echo '--- SYCL allocator aliases and device array declarations ---'
fd -t f -e h -e hpp -e cpp -e cc -e cxx . include src test models | while read -r f; do
if rg -q 'DeviceAllocator|GlobalDeviceAllocator|__SYCL_DEVICE_ONLY__|array_ns::array|array<' "$f"; then
echo "FILE: $f"
rg -n -C4 'DeviceAllocator|GlobalDeviceAllocator|__SYCL_DEVICE_ONLY__|array_ns::array|array<' "$f" | head -160
fi
done
echo '--- sycl.h relevant definitions ---'
fd -t f 'sycl.h' . | while read -r f; do
echo "FILE: $f"
sed -n '1,180p' "$f"
done
echo '--- arrays.h include and allocator-selection context ---'
sed -n '1,80p' include/arrays.h
sed -n '1240,1300p' include/arrays.hRepository: highperformancecoder/ecolab Length of output: 50383 🏁 Script executed: #!/bin/bash
set -eu
echo '--- ModelData and EcolabPoint definitions ---'
sed -n '1,125p' models/ecolab_model.h
echo '--- DeviceType definition and uses ---'
rg -n -C8 'template.*DeviceType|struct DeviceType|class DeviceType|DeviceType<' include models src \
--glob '*.{h,hpp,c,cpp,cc,cxx}' | head -240
echo '--- Device-side default construction of GlobalDeviceAllocator-backed objects ---'
rg -n -C8 'UnsignedArray|Allocator<|cell_ids|density|array<[^;]*(GlobalDeviceAllocator|ModelData::Allocator)' \
models/ecolab_model.h models/ecolab_model.cc include \
--glob '*.{h,hpp,c,cpp,cc,cxx}' | head -320
echo '--- Read-only semantic probe of the relevant preprocessor branches ---'
python3 - <<'PY'
from pathlib import Path
allocator = Path("include/DeviceAllocator.h").read_text()
arrays = Path("include/arrays.h").read_text()
model = Path("models/ecolab_model.h").read_text()
checks = {
"device constructor initializes allocator to nullptr":
"GlobalDeviceAllocator(): allocator(nullptr) {}" in allocator,
"device allocate dereferences allocator without a guard":
"{return reinterpret_cast<T*>(allocator->allocate(n*sizeof(T)));}" in allocator,
"array default constructor is defaulted":
"array()=default;" in arrays,
"array stores allocator as a value member":
"A m_allocator;" in arrays,
"EcolabPoint uses GlobalDeviceAllocator under SYCL":
"template <class T> using Allocator=GlobalDeviceAllocator<T>;" in model,
"EcolabPoint has a GlobalDeviceAllocator-backed array member":
"array<int,Allocator<int>> density;" in model,
}
for name, result in checks.items():
print(f"{'PASS' if result else 'FAIL'}: {name}")
PYRepository: highperformancecoder/ecolab Length of output: 32452 Reject null device allocator construction. When 🤖 Prompt for AI Agents |
||||||
| template <class U> | ||||||
| GlobalDeviceAllocator(const GlobalDeviceAllocator<U>& other): | ||||||
| allocator(other.allocator) {} | ||||||
|
|
@@ -206,7 +197,7 @@ namespace ecolab | |||||
| bool operator==(const HostSharedAllocator&) const {return true;} | ||||||
| }; | ||||||
|
|
||||||
| constexpr static unsigned LocalAllocatorSize=30*1024; // 32KiB = half typical local storage | ||||||
| constexpr static unsigned LocalAllocatorSize=8*1024; // 32KiB = half typical local storage | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win The comment contradicts the value.
📝 Proposed comment fix- constexpr static unsigned LocalAllocatorSize=8*1024; // 32KiB = half typical local storage
+ constexpr static unsigned LocalAllocatorSize=8*1024; // 8KiB per work group of local storage📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
|
|
||||||
| struct LocalAllocatorBuffer | ||||||
| { | ||||||
|
|
@@ -226,10 +217,11 @@ namespace ecolab | |||||
| /** | ||||||
| A Local allocator allocates memory from device local memory, | ||||||
| which is shared between threads of a work group, and has the same | ||||||
| lifetime as the kernel | ||||||
| lifetime as the kernel. | ||||||
| LocalAllocatorT so we can expose LocalAllocator as a template alias on both host and device branches | ||||||
| */ | ||||||
| template <class T> | ||||||
| class LocalAllocator | ||||||
| class LocalAllocatorT | ||||||
| { | ||||||
| public: | ||||||
| using value_type=T; | ||||||
|
|
@@ -253,12 +245,18 @@ namespace ecolab | |||||
| char* alloc=b.buffer+offs; | ||||||
| return reinterpret_cast<T*>(alloc); | ||||||
| } | ||||||
| void deallocate(T*,size_t) {} // cleaned up when group exits | ||||||
| template<class U> struct rebind {using other=LocalAllocator<U>;}; | ||||||
| void deallocate(T*p,size_t) {} // cleaned up when group exits | ||||||
| template<class U> struct rebind {using other=LocalAllocatorT<U>;}; | ||||||
| // allocator is stateless | ||||||
| bool operator==(const LocalAllocator&) const {return true;} | ||||||
| bool operator==(const LocalAllocatorT&) const {return true;} | ||||||
| }; | ||||||
| template <class T> using LocalAllocator=LocalAllocatorT<T>; | ||||||
| #else | ||||||
| template <class T> class LocalAllocatorT {}; | ||||||
| template <class T> using LocalAllocator=std::allocator<T>; | ||||||
| #endif | ||||||
|
|
||||||
| } | ||||||
|
|
||||||
| #include "DeviceAllocator.cd" | ||||||
| #endif | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
SYCL sycl::ext::oneapi::this_work_item::get_nd_item requirements nd_range kernel simple range💡 Result:
The function sycl::ext::oneapi::this_work_item::get_nd_item is a SYCL extension used to retrieve a sycl::nd_item object representing the current work-item within a kernel [1][2]. Requirements and Usage: 1. Kernel Type: It is designed specifically for use within ND-range kernels [3][4][5]. It is not supported for simple range kernels (kernels launched with a simple sycl::range) [4]. 2. ND-Range Launch: The kernel must be launched with a sycl::nd_range [3]. 3. Dimensionality: The template parameter Dimensions must match the dimensionality of the currently executing kernel [3]. 4. Context: It is most commonly used within "free function" kernels (a SYCL extension) where the sycl::nd_item is not automatically passed as an argument to the function, providing a way to query work-item information like global IDs or ranges from inside the kernel body [6][4][7]. When used correctly, this function returns a sycl::nd_item instance, allowing access to standard nd_item methods such as get_global_id, get_global_linear_id, get_local_id, and get_global_range [8][1][7]. Attempting to use this function in a context that was not launched as an ND-range kernel, such as a simple range kernel, is not supported and may lead to runtime errors or undefined behavior [4][5].
Citations:
🏁 Script executed:
Repository: highperformancecoder/ecolab
Length of output: 5694
🏁 Script executed:
Repository: highperformancecoder/ecolab
Length of output: 15838
🌐 Web query:
SYCL 2020 queue::parallel_for range overload kernel lambda argument sycl::item sycl::id simple range💡 Result:
In SYCL 2020, the queue::parallel_for method allows you to enqueue a basic data-parallel kernel by specifying a execution range [1][2]. When using the overload that accepts a sycl::range, the kernel lambda function can accept either a sycl::id or a sycl::item as its argument [2]. sycl::id An id represents the unique index of the current work-item within the execution range [2]. It is a simple, lightweight way to access the current thread's coordinates [2][3]. Example using sycl::id: q.submit([&](sycl::handler &h) { h.parallel_for(sycl::range<1>(size), [=](sycl::id<1> idx) { // idx provides the current global index data_accessor[idx] += 5; }); }); sycl::item An item is a more feature-rich object that encapsulates both the work-item's index and the overall execution range [2][3]. It provides methods to query information about the execution space, such as the global range, without requiring additional parameters [4][5]. Example using sycl::item: q.submit([&](sycl::handler &h) { h.parallel_for(sycl::range<1>(size), [=](sycl::item<1> itm) { auto globalId = itm.get_id; // Equivalent to sycl::id auto range = itm.get_range; // Access to the range dimensions data_accessor[globalId] += 5; }); }); Key Considerations for SYCL 2020: - Offsets: Support for passing an offset to parallel_for has been deprecated in SYCL 2020 [6]. Developers are expected to perform any necessary offset arithmetic manually within the kernel lambda [6]. - Captures: When using a lambda function as a kernel, all captures should be by copy ([=]), and the lambda must not be mutable [7]. - Dimension constraints: SYCL typically supports up to 3 dimensions. For higher-dimensional needs, developers must use a 1D range and perform manual index linearization/calculation [6].
Citations:
🌐 Web query:
site:github.khronos.org/SYCL-Reference queue parallel_for range id item kernel function💡 Result:
Relevant Khronos SYCL Reference pages:
sycl::queue::parallel_for— submits a kernel over arangeornd_range; returns an event. (github.khronos.org)sycl::handler::parallel_for— shows kernels receivingsycl::item, with examples usingrangeandid. (github.khronos.org)sycl::item— provides each work-item’s ID and range viaget_id()andget_range(). (github.khronos.org)sycl::id— represents a position within a SYCL range. (github.khronos.org)Basic pattern:
sycl::queue q; q.submit([&](sycl::handler& h) { h.parallel_for<class Kernel>( sycl::range<1>(N), [=](sycl::id<1> i) { // kernel body; i[0] is the work-item index }); });With
sycl::item:Citations:
Pass the simple-range index to
Stack::initsyclItem()callsget_nd_item<1>(), which is not supported by the simple-range kernel at lines 112-113. Initialize the slots with the kernel’ssycl::id<1>orsycl::item<1>argument and use the launch size as the stride. Otherwise, allocator initialization can fail at runtime.🤖 Prompt for AI Agents