Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
167fa2c
Temporary checin to ask copilot a question
highperformancecoder Jul 27, 2026
f704151
Ask Copilot another question.
highperformancecoder Jul 27, 2026
8616caa
Commit latest for copilot question.
highperformancecoder Jul 28, 2026
3994067
Sync to repo for copilot question
highperformancecoder Jul 28, 2026
880ea47
Again for copilot conversation.
highperformancecoder Jul 28, 2026
cdf4ef9
More copilot questions: After disabling cow protocol.
highperformancecoder Jul 28, 2026
2e96857
For copilot questioning
highperformancecoder Jul 28, 2026
5d5a79b
Comitting now to put a pin it it. Big reveal is that assorted array m…
highperformancecoder Jul 29, 2026
dc57275
Switching to dell laptop to perform debugging.
highperformancecoder Jul 29, 2026
2de48bd
Implementing DeviceAllocator using a stack rather than a queue, but
highperformancecoder Jul 30, 2026
5a4acc2
Revert to MPMC.
highperformancecoder Jul 30, 2026
f8f2a40
tweaks from dellGPU
highperformancecoder Jul 30, 2026
3e7fb9e
Discard stack
highperformancecoder Aug 2, 2026
bdbdec3
GPU allocation working stably now. Note use of LocalArray in return o…
highperformancecoder Aug 7, 2026
da128ee
revised onDevice() test, but swap algorithm still doesn't work.
highperformancecoder Aug 8, 2026
4ba1c06
chore: code cleanup
highperformancecoder Aug 8, 2026
9ea9bf9
Fix up conventional build.
highperformancecoder Aug 8, 2026
288c328
optimisation:
highperformancecoder Aug 10, 2026
ba79475
chore: address code review comments
highperformancecoder Aug 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion classdesc
Submodule classdesc updated 1 files
+2 −1 classdesc.cc
142 changes: 70 additions & 72 deletions include/DeviceAllocator.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment on lines 52 to 57

Copy link
Copy Markdown

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:

#!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
git ls-files | grep -E '(^|/)(DeviceAllocator\.h|sycl\.h)$' || true
printf '%s\n' '--- relevant symbols and call sites ---'
rg -n -C 6 'class Stack|struct Stack|void init|parallel_for|syclItem|get_nd_item' include/DeviceAllocator.h include/sycl.h 2>/dev/null || true
printf '%s\n' '--- file sizes ---'
wc -l include/DeviceAllocator.h include/sycl.h 2>/dev/null || true

Repository: highperformancecoder/ecolab

Length of output: 5694


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- DeviceAllocator.h surrounding implementation ---'
cat -n include/DeviceAllocator.h | sed -n '1,150p'
printf '%s\n' '--- init call sites ---'
rg -n -C 5 '\.init\(\)|init\(\)' --glob '!include/DeviceAllocator.h' --glob '!include/sycl.h' .
printf '%s\n' '--- parallel_for signatures in repository ---'
rg -n -C 3 'parallel_for\s*\(' --glob '*.h' --glob '*.hpp' --glob '*.cpp' --glob '*.cc' --glob '*.cxx' .

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 a range or nd_range; returns an event. (github.khronos.org)
  • sycl::handler::parallel_for — shows kernels receiving sycl::item, with examples using range and id. (github.khronos.org)
  • sycl::item — provides each work-item’s ID and range via get_id() and get_range(). (github.khronos.org)
  • sycl::id — represents a position within a SYCL range. (github.khronos.org)
  • Defining kernels — explains lambda and function-object kernel functions. (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:

[=](sycl::item<1> item) {
    auto i = item.get_id()[0];
}

Citations:


Pass the simple-range index to Stack::init

syclItem() calls get_nd_item<1>(), which is not supported by the simple-range kernel at lines 112-113. Initialize the slots with the kernel’s sycl::id<1> or sycl::item<1> argument and use the launch size as the stride. Otherwise, allocator initialization can fail at runtime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@include/DeviceAllocator.h` around lines 52 - 57, Update DeviceAllocator’s
Stack::init to accept the simple-range kernel’s sycl::id<1> or sycl::item<1>
argument instead of calling syclItem(). Derive the starting index and stride
from that argument and the kernel launch size, while preserving the existing
slot initialization and top reset behavior.


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;
}
Comment thread
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];
}
Comment thread
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())
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 || true

Repository: 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.h

Repository: 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}")
PY

Repository: highperformancecoder/ecolab

Length of output: 32452


Reject null device allocator construction.

When GlobalDeviceAllocator<T> is default-constructed on the device, allocator is nullptr, but allocate() dereferences it without a check. A default-constructed array<T, GlobalDeviceAllocator<T>> therefore fails on its first non-zero allocation. Delete the device constructor or report a controlled allocation failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@include/DeviceAllocator.h` around lines 171 - 178, Update
GlobalDeviceAllocator’s __SYCL_DEVICE_ONLY__ construction path so it cannot
leave allocator as nullptr: delete the device default constructor or make
allocation fail in a controlled manner before dereferencing allocator. Ensure
allocate() handles any unavailable device allocator safely while preserving the
existing host initialization through deviceAllocator().

template <class U>
GlobalDeviceAllocator(const GlobalDeviceAllocator<U>& other):
allocator(other.allocator) {}
Expand All @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The comment contradicts the value.

LocalAllocatorSize is now 8 KiB, but the comment states 32 KiB. include/ecolab.h derives wg_per_compute_unit from this constant, so a wrong comment misleads occupancy tuning.

📝 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constexpr static unsigned LocalAllocatorSize=8*1024; // 32KiB = half typical local storage
constexpr static unsigned LocalAllocatorSize=8*1024; // 8KiB per work group of local storage
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@include/DeviceAllocator.h` at line 200, Update the comment on
LocalAllocatorSize to accurately state that the value is 8 KiB, replacing the
incorrect 32 KiB description while preserving the constant and its
occupancy-related usage.


struct LocalAllocatorBuffer
{
Expand All @@ -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;
Expand All @@ -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
Loading
Loading