Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
56 changes: 53 additions & 3 deletions source/dcompute/driver/cuda/buffer.d
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,50 @@ struct Buffer(T)
// Host memory associated with this buffer
T[] hostMemory;

// Shared atomic reference count for the owned CUdeviceptr (see rcCreate/
// rcRetain/rcRelease in dcompute.driver.cuda). null when this Buffer does
// not own a handle (e.g. a non-owning view created via
// UnifiedBuffer.asBuffer). Copies share the same counter so the underlying
// device allocation is freed exactly once, when the last owner dies.
private shared(int)* _rc;

this(size_t elems)
{
status = cast(Status)cuMemAlloc(&raw,elems * T.sizeof);
checkErrors();
hostMemory = null;
if (raw != 0)
_rc = rcCreate();
}

this(T[] arr)
{
status = cast(Status)cuMemAlloc(&raw,arr.length * T.sizeof);
checkErrors();
hostMemory = arr;
if (raw != 0)
_rc = rcCreate();
}

// Copies bump the shared refcount so the handle survives until the last
// copy is destroyed.
this(this)
{
rcRetain(_rc);
}

// Last owner frees the device allocation. The CUresult is deliberately
// ignored: throwing from a destructor (which may run as a GC finalizer, on
// a thread without a current CUDA context, or after driver teardown at
// program exit) is unsafe — in those cases cuMemFree simply reports an
// error we cannot act on anyway.
~this()
{
if (rcRelease(_rc) && raw != 0)
cuMemFree(raw);
raw = 0;
// hostMemory is a GC-owned slice: never free it, just drop the reference.
hostMemory = null;
}
void copy(Copy c)()
{
Expand All @@ -34,13 +66,31 @@ struct Buffer(T)
}
checkErrors();
}
alias hostArgOf(U : GlobalPointer!T) = raw;
alias hostArgOf(U : GlobalPointer!T) = raw;
void release()
{
status = cast(Status)cuMemFree(raw);
checkErrors();
if (_rc !is null)
{
// Owning buffer: drop this copy's ownership. Only the LAST owner
// actually frees — with other copies still alive the free is
// deferred to the last copy's release()/~this(), so the surviving
// copies keep a valid handle and nothing is freed twice.
if (rcRelease(_rc) && raw != 0)
{
status = cast(Status)cuMemFree(raw);
checkErrors();
}
}
else if (raw != 0)
{
// Non-owning / manually assembled buffer: legacy behaviour, free
// immediately (the caller manages the lifetime by hand).
status = cast(Status)cuMemFree(raw);
checkErrors();
}
raw = 0;
hostMemory = null;
// Handle and ownership are both gone; ~this is now a guaranteed no-op.
}
}

Expand Down
54 changes: 51 additions & 3 deletions source/dcompute/driver/cuda/context.d
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,38 @@ import dcompute.driver.cuda;
struct Context
{
CUcontext raw;

// Shared atomic refcount for an OWNED CUcontext (one created via
// cuCtxCreate); see rcCreate/rcRetain/rcRelease in dcompute.driver.cuda.
// null = non-owning view. The static factory paths pop() and current()
// intentionally leave _rc null: they reference an existing context owned
// elsewhere (e.g. the runtime's _defaultContext), so a transient copy
// must NOT destroy it.
private shared(int)* _rc;

this(Device dev, uint flags = 0)
{
status = cast(Status)cuCtxCreate(&raw, flags,dev.raw);
checkErrors();
if (raw !is null)
_rc = rcCreate();
}


this(this)
{
rcRetain(_rc);
}

// Last owner destroys the context. The CUresult is deliberately ignored:
// a destructor must not throw (it may run as a GC finalizer or during
// shutdown after the driver is already torn down).
~this()
{
if (rcRelease(_rc) && raw !is null)
cuCtxDestroy(raw);
raw = null;
}

static void push(Context ctx)
{
status = cast(Status)cuCtxPushCurrent(ctx.raw);
Expand All @@ -19,13 +45,17 @@ struct Context

static Context pop()
{
// ret._rc stays null: this references an EXISTING context being popped
// off the stack, owned elsewhere. A non-owning view must not destroy it.
Context ret;
status = cast(Status)cuCtxPopCurrent(&ret.raw);
checkErrors();
return ret;
}
static @property Context current()
{
// ret._rc stays null: non-owning view of the current context (owned by
// whoever created it, e.g. the runtime). Its destructor is a no-op.
Context ret;
status = cast(Status)cuCtxGetCurrent(&ret.raw);
checkErrors();
Expand Down Expand Up @@ -104,9 +134,27 @@ struct Context
checkErrors();
}

// Deprecated: cuCtxDetach is a legacy refcount-decrement API. The RAII
// destructor (~this) uses cuCtxDestroy for owned contexts. For an OWNED
// context this drops one owner and only the last owner actually detaches
// (so surviving copies never see a destroyed handle); the handle and
// ownership are always cleared here, making a later ~this a no-op.
void detach()
{
status = cast(Status)cuCtxDetach(raw);
checkErrors();
if (_rc !is null)
{
if (rcRelease(_rc) && raw !is null)
{
status = cast(Status)cuCtxDetach(raw);
checkErrors();
}
}
else if (raw !is null)
{
// Non-owning view: legacy behaviour, detach immediately.
status = cast(Status)cuCtxDetach(raw);
checkErrors();
}
raw = null;
}
}
37 changes: 36 additions & 1 deletion source/dcompute/driver/cuda/event.d
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,40 @@ import dcompute.driver.cuda;
struct Event
{
CUevent raw;


// Shared atomic refcount for an OWNED CUevent (see rcCreate/rcRetain/
// rcRelease in dcompute.driver.cuda). null = non-owning view.
// Default-constructed Events keep _rc null, so ~this is a no-op for them.
private shared(int)* _rc;

/**
* Create an event.
*
* Params:
* flags = CU_EVENT_* creation flags (pass 0 / CU_EVENT_DEFAULT for the
* default behaviour; CU_EVENT_BLOCKING_SYNC,
* CU_EVENT_DISABLE_TIMING, ... to opt in to other modes).
*/
this(uint flags)
{
status = cast(Status)cuEventCreate(&raw, flags);
checkErrors();
if (raw !is null)
_rc = rcCreate();
}

this(this)
{
rcRetain(_rc);
}

// Last owner destroys the event. The CUresult is deliberately ignored:
// a destructor must not throw (it may run as a GC finalizer or during
// shutdown after the driver is already torn down).
~this()
{
if (rcRelease(_rc) && raw !is null)
cuEventDestroy(raw);
raw = null;
}
}
53 changes: 53 additions & 0 deletions source/dcompute/driver/cuda/package.d
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,59 @@ public import dcompute.driver.cuda.queue;
public import dcompute.driver.cuda.unified_buffer;
public import dcompute.driver.cuda.runtime;

// ---------------------------------------------------------------------------
// Shared atomic refcount helpers for the RAII driver wrappers (Buffer, Queue,
// Context, Program, Event).
//
// The counter is allocated with C malloc, NOT the GC: struct destructors also
// run as GC finalizers, and during a GC sweep other GC blocks (which a
// GC-allocated counter would be) may already have been freed — touching them
// from a finalizer is undefined behaviour. malloc'd memory stays valid until
// we free() it ourselves, which happens exactly once, when the count hits 0.
// ---------------------------------------------------------------------------

/// Allocate a new refcount initialised to 1 (a single owner).
/// Returns null on out-of-memory; callers treat null as "non-owning", so the
/// handle is then never auto-destroyed (a leak, never a crash/double-free).
package shared(int)* rcCreate() nothrow @nogc
{
import core.stdc.stdlib : malloc;
auto rc = cast(shared(int)*) malloc(int.sizeof);
if (rc !is null)
{
import core.atomic : atomicStore;
atomicStore(*rc, 1);
}
return rc;
}

/// Postblit helper: one more owner shares the handle. Safe on null.
package void rcRetain(shared(int)* rc) nothrow @nogc
{
import core.atomic : atomicOp;
if (rc !is null)
atomicOp!"+="(*rc, 1);
}

/// Destructor/release helper: drop one owner. Returns true iff this was the
/// LAST owner — the caller must then destroy the CUDA handle. Frees the
/// counter and nulls the caller's pointer either way, so any subsequent
/// ~this()/release() on the same struct is a guaranteed no-op.
package bool rcRelease(ref shared(int)* rc) nothrow @nogc
{
import core.atomic : atomicOp;
if (rc is null)
return false;
immutable last = atomicOp!"-="(*rc, 1) == 0;
if (last)
{
import core.stdc.stdlib : free;
free(cast(void*) rc);
}
rc = null;
return last;
}

enum Copy
{
hostToDevice,
Expand Down
51 changes: 48 additions & 3 deletions source/dcompute/driver/cuda/program.d
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,29 @@ import std.string;
struct Program
{
CUmodule raw;


// Shared atomic refcount for an OWNED CUmodule (see rcCreate/rcRetain/
// rcRelease in dcompute.driver.cuda). null = non-owning. The
// fromFile/fromString/fromModule factories DO create a module and set
// _rc, so the returned Program owns it. Copies share the counter.
private shared(int)* _rc;

this(this)
{
rcRetain(_rc);
}

// Last owner unloads the module. The CUresult is deliberately ignored (a
// destructor must not throw) — this matters for globalProgram-style
// usage, where the dtor may run at shutdown after the CUDA context is
// torn down; a cuModuleUnload error there is harmless.
~this()
{
if (rcRelease(_rc) && raw !is null)
cuModuleUnload(raw);
raw = null;
}

Kernel!void getKernelByName(immutable(char)* name)
{
Kernel!void ret;
Expand All @@ -28,6 +50,8 @@ struct Program
Program ret;
status = cast(Status)cuModuleLoad(&ret.raw,name.toStringz);
checkErrors();
if (ret.raw !is null)
ret._rc = rcCreate(); // freshly loaded module: take ownership
return ret;
}

Expand All @@ -36,6 +60,8 @@ struct Program
Program ret;
status = cast(Status)cuModuleLoadData(&ret.raw,name.toStringz);
checkErrors();
if (ret.raw !is null)
ret._rc = rcCreate(); // freshly loaded module: take ownership
return ret;
}

Expand Down Expand Up @@ -76,6 +102,8 @@ struct Program
Program ret;
mixin("status = cast(Status)cuModuleLoadData(&ret.raw, &" ~ symbolName ~ ");");
checkErrors();
if (ret.raw !is null)
ret._rc = rcCreate(); // freshly loaded module: take ownership
return ret;
}
else
Expand All @@ -94,10 +122,27 @@ struct Program
//cuModuleLoadDataEx
//cuModuleLoadFatBinary

// Drops this copy's ownership; only the LAST owner actually unloads the
// module, so surviving copies never see an unloaded handle and nothing is
// unloaded twice. Handle and ownership are cleared either way, making a
// later ~this a guaranteed no-op.
void unload()
{
status = cast(Status)cuModuleUnload(raw);
checkErrors();
if (_rc !is null)
{
if (rcRelease(_rc) && raw !is null)
{
status = cast(Status)cuModuleUnload(raw);
checkErrors();
}
}
else if (raw !is null)
{
// Non-owning view: legacy behaviour, unload immediately.
status = cast(Status)cuModuleUnload(raw);
checkErrors();
}
raw = null;
}

//TODO: linkstate
Expand Down
Loading