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
84 changes: 60 additions & 24 deletions src/allocator/allocator.c
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,6 @@ int free_raw(CUdeviceptr dptr) {
int remove_chunk_async(
allocated_list *a_list, CUdeviceptr dptr, CUstream hStream) {
size_t t_size;
if (a_list->length == 0) {
return -1;
}
allocated_list_entry *val;
for (val = a_list->head; val != NULL; val = val->next) {
if (val->entry->address == dptr) {
Expand All @@ -253,7 +250,9 @@ int remove_chunk_async(
return 0;
}
}
return -1;
/* Not tracked by libvgpu: free it for real instead of returning -1, which
* leaked and surfaced as an "unrecognized error code". */
return CUDA_OVERRIDE_CALL(cuda_library_entry, cuMemFreeAsync, dptr, hStream);
}

int free_raw_async(CUdeviceptr dptr, CUstream hStream) {
Expand All @@ -263,14 +262,40 @@ int free_raw_async(CUdeviceptr dptr, CUstream hStream) {
return tmp;
}

/* Track an allocated async chunk and account it against its pool's high-water
* limit. Call with mutex held. */
static int account_async_chunk(allocated_list_entry *e, size_t size, CUmemoryPool pool) {
size_t poollimit;
CUresult res = CUDA_OVERRIDE_CALL(cuda_library_entry, cuMemPoolGetAttribute, pool,
CU_MEMPOOL_ATTR_RESERVED_MEM_HIGH, &poollimit);
if (res != CUDA_SUCCESS) {
LOG_ERROR("cuMemPoolGetAttribute failed res=%d", res);
return res;
}
if (poollimit != 0) {
if (poollimit > device_allocasync->limit) {
size_t allocsize = (poollimit - device_allocasync->limit < size)
? poollimit - device_allocasync->limit : size;
add_gpu_device_memory_usage(getpid(), e->entry->dev, allocsize, 2);
device_allocasync->limit = device_allocasync->limit + allocsize;
e->entry->length = allocsize;
} else {
e->entry->length = 0;
}
} else {
e->entry->length = 0;
}
LIST_ADD(device_allocasync, e);
return 0;
}
Comment on lines +265 to +290

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

Leaks the driver allocation and the tracking entry when cuMemPoolGetAttribute fails.

By the time account_async_chunk is called, the real device allocation (cuMemAllocAsync/cuMemAllocFromPoolAsync) has already succeeded. If cuMemPoolGetAttribute fails, the function returns early without LIST_ADD-ing e into device_allocasync and without freeing e or the underlying device pointer. The result: e (host memory) leaks, and the already-allocated device memory becomes permanently untracked and unfreeable — the exact class of leak this PR set out to fix, just relocated to a rarer failure path. This matches cppcheck's nullPointerOutOfResources/leak signals around this region.

🐛 Proposed fix — release what was already allocated on this failure path
-static int account_async_chunk(allocated_list_entry *e, size_t size, CUmemoryPool pool) {
+static int account_async_chunk(allocated_list_entry *e, size_t size, CUmemoryPool pool, CUstream hStream) {
     size_t poollimit;
     CUresult res = CUDA_OVERRIDE_CALL(cuda_library_entry,cuMemPoolGetAttribute,pool,CU_MEMPOOL_ATTR_RESERVED_MEM_HIGH,&poollimit);
     if (res != CUDA_SUCCESS) {
         LOG_ERROR("cuMemPoolGetAttribute failed res=%d",res);
+        /* The real allocation already succeeded; don't strand it. */
+        CUDA_OVERRIDE_CALL(cuda_library_entry,cuMemFreeAsync,e->entry->address,hStream);
+        free(e);
         return res;
     }

Callers need the extra hStream argument:

-    return account_async_chunk(e, size, pool);
+    return account_async_chunk(e, size, pool, hStream);

(applies to both call sites in add_chunk_async and add_chunk_from_pool_async)

📝 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
/* Track an allocated async chunk and account it against its pool's high-water
* limit. Call with mutex held. */
static int account_async_chunk(allocated_list_entry *e, size_t size, CUmemoryPool pool) {
size_t poollimit;
CUresult res = CUDA_OVERRIDE_CALL(cuda_library_entry,cuMemPoolGetAttribute,pool,CU_MEMPOOL_ATTR_RESERVED_MEM_HIGH,&poollimit);
if (res != CUDA_SUCCESS) {
LOG_ERROR("cuMemPoolGetAttribute failed res=%d",res);
return res;
}
if (poollimit != 0) {
if (poollimit> device_allocasync->limit) {
size_t allocsize = (poollimit-device_allocasync->limit < size)? poollimit-device_allocasync->limit : size;
add_gpu_device_memory_usage(getpid(), e->entry->dev, allocsize, 2);
device_allocasync->limit=device_allocasync->limit+allocsize;
e->entry->length=allocsize;
}else{
e->entry->length=0;
}
}else{
e->entry->length=0;
}
LIST_ADD(device_allocasync,e);
return 0;
}
/* Track an allocated async chunk and account it against its pool's high-water
* limit. Call with mutex held. */
static int account_async_chunk(allocated_list_entry *e, size_t size, CUmemoryPool pool, CUstream hStream) {
size_t poollimit;
CUresult res = CUDA_OVERRIDE_CALL(cuda_library_entry,cuMemPoolGetAttribute,pool,CU_MEMPOOL_ATTR_RESERVED_MEM_HIGH,&poollimit);
if (res != CUDA_SUCCESS) {
LOG_ERROR("cuMemPoolGetAttribute failed res=%d",res);
/* The real allocation already succeeded; don't strand it. */
CUDA_OVERRIDE_CALL(cuda_library_entry,cuMemFreeAsync,e->entry->address,hStream);
free(e);
return res;
}
if (poollimit != 0) {
if (poollimit> device_allocasync->limit) {
size_t allocsize = (poollimit-device_allocasync->limit < size)? poollimit-device_allocasync->limit : size;
add_gpu_device_memory_usage(getpid(), e->entry->dev, allocsize, 2);
device_allocasync->limit=device_allocasync->limit+allocsize;
e->entry->length=allocsize;
}else{
e->entry->length=0;
}
}else{
e->entry->length=0;
}
LIST_ADD(device_allocasync,e);
return 0;
}
🧰 Tools
🪛 Cppcheck (2.21.0)

[warning] 269-269: If resource allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfResources)


[warning] 271-271: If resource allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfResources)

🤖 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 `@src/allocator/allocator.c` around lines 265 - 288, The early return in
account_async_chunk leaves both the tracking entry e and the already-created
async device allocation unhandled when cuMemPoolGetAttribute fails. Update
account_async_chunk and its callers (add_chunk_async and
add_chunk_from_pool_async) so the failure path frees or rolls back the allocated
device memory, releases the host tracking entry, and does not skip cleanup
before returning the CUDA error.

Source: Linters/SAST tools


int add_chunk_async(CUdeviceptr *address, size_t size, CUstream hStream) {
size_t addr=0;
size_t allocsize;
size_t addr = 0;
CUresult res = CUDA_SUCCESS;
CUdevice dev;
cuCtxGetDevice(&dev);
if (oom_check(dev,size))
return -1;
if (oom_check(dev, size))
return CUDA_ERROR_OUT_OF_MEMORY;
Comment on lines +265 to +298

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect other consumers of device_allocasync->limit / oom_check to confirm
# whether cross-pool undercounting can actually bypass the configured memory limit.
rg -n 'device_allocasync->limit|oom_check\(' src/allocator/allocator.c src/allocator/*.h

Repository: Project-HAMi/HAMi-core

Length of output: 1010


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== allocator.c outline =="
ast-grep outline src/allocator/allocator.c --view expanded || true

echo
echo "== relevant sections =="
sed -n '1,220p' src/allocator/allocator.c
echo
sed -n '220,420p' src/allocator/allocator.c

echo
echo "== allocator.h relevant definitions =="
sed -n '1,260p' src/allocator/allocator.h

Repository: Project-HAMi/HAMi-core

Length of output: 19457


Track async memory per pool, not through a shared counter. CU_MEMPOOL_ATTR_RESERVED_MEM_HIGH is per-pool, but account_async_chunk() compares it against the global device_allocasync->limit, so one pool can suppress accounting in another and later frees subtract 0. Multi-pool async allocations then disappear from the process GPU-memory limit.

🧰 Tools
🪛 Cppcheck (2.21.0)

[warning] 269-269: If resource allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfResources)


[warning] 271-271: If resource allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfResources)

🤖 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 `@src/allocator/allocator.c` around lines 265 - 296, The async accounting in
account_async_chunk() is using the shared device_allocasync->limit for
CU_MEMPOOL_ATTR_RESERVED_MEM_HIGH, which mixes different pools together. Update
the accounting to track high-water usage per CUmemoryPool (using the pool passed
into account_async_chunk and the chunk’s pool association) instead of a global
counter, and make the corresponding free/unaccount path subtract against the
same per-pool state so allocations from one pool do not suppress or erase
accounting for another.


allocated_list_entry *e;
INIT_ALLOCATED_LIST_ENTRY(e, addr, size, dev);
Expand All @@ -280,31 +305,34 @@ int add_chunk_async(CUdeviceptr *address, size_t size, CUstream hStream) {
return res;
}
*address = e->entry->address;
/* cuMemAllocAsync draws from the device's default pool. */
CUmemoryPool pool;
res = CUDA_OVERRIDE_CALL(cuda_library_entry,cuDeviceGetMemPool,&pool,dev);
if (res != CUDA_SUCCESS) {
LOG_ERROR("cuDeviceGetMemPool failed res=%d",res);
return res;
Comment on lines +308 to 313

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

Roll back the allocation on default-pool lookup failure.
Line 300 already created the driver allocation, so returning here leaks the device memory and the initialized tracking entry.

🧰 Tools
🪛 Cppcheck (2.21.0)

[warning] 308-308: If resource allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfResources)


[warning] 310-310: If resource allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfResources)

🤖 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 `@src/allocator/allocator.c` around lines 306 - 311, The default-pool lookup
failure path in allocator.c leaks the already-created driver allocation and
tracking entry. In the cuMemAllocAsync flow around cuDeviceGetMemPool, add
cleanup before returning when res != CUDA_SUCCESS: release the allocation
created earlier and undo the initialized bookkeeping so the allocator state
stays consistent. Use the existing cuMemAllocAsync path and the surrounding
allocation/tracking symbols in allocator.c to locate the rollback point.

}
size_t poollimit;
res = CUDA_OVERRIDE_CALL(cuda_library_entry,cuMemPoolGetAttribute,pool,CU_MEMPOOL_ATTR_RESERVED_MEM_HIGH,&poollimit);
return account_async_chunk(e, size, pool);
}

int add_chunk_from_pool_async(CUdeviceptr *address, size_t size, CUmemoryPool pool, CUstream hStream) {
size_t addr = 0;
CUresult res = CUDA_SUCCESS;
CUdevice dev;
cuCtxGetDevice(&dev);
if (oom_check(dev, size))
return CUDA_ERROR_OUT_OF_MEMORY;

allocated_list_entry *e;
INIT_ALLOCATED_LIST_ENTRY(e, addr, size, dev);
res = CUDA_OVERRIDE_CALL(cuda_library_entry, cuMemAllocFromPoolAsync, &e->entry->address, size, pool, hStream);
if (res != CUDA_SUCCESS) {
LOG_ERROR("cuMemPoolGetAttribute failed res=%d",res);
LOG_ERROR("cuMemAllocFromPoolAsync failed res=%d", res);
return res;
Comment on lines +326 to 331

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- src/allocator/allocator.c around lines 280-360 ---'
sed -n '280,360p' src/allocator/allocator.c | cat -n

echo '--- search for INIT_ALLOCATED_LIST_ENTRY ---'
rg -n "INIT_ALLOCATED_LIST_ENTRY|allocated_list_entry|allocated_list" src

echo '--- search for destructor/free/cleanup related to allocated_list_entry ---'
rg -n "free.*allocated_list_entry|destroy.*allocated_list_entry|cleanup.*allocated_list_entry|INIT_ALLOCATED_LIST_ENTRY|TRACKING_ENTRY|allocated_list_entry.*free" src

Repository: Project-HAMi/HAMi-core

Length of output: 6294


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- src/allocator/allocator.h around INIT_ALLOCATED_LIST_ENTRY ---'
sed -n '80,150p' src/allocator/allocator.h | cat -n

echo '--- src/allocator/allocator.c remove/cleanup paths ---'
sed -n '150,280p' src/allocator/allocator.c | cat -n

Repository: Project-HAMi/HAMi-core

Length of output: 10365


Release the temporary entry on cuMemAllocFromPoolAsync failure. INIT_ALLOCATED_LIST_ENTRY allocates e before this call, so the early return leaks e, e->entry, and e->entry->allocHandle; free them with the same cleanup pattern used by LIST_REMOVE.

🧰 Tools
🪛 Cppcheck (2.21.0)

[error] 325-325: Memory leak

(memleak)


[warning] 325-325: If resource allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfResources)


[warning] 326-326: If resource allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfResources)


[warning] 328-328: If resource allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfResources)

🤖 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 `@src/allocator/allocator.c` around lines 324 - 329, The
cuMemAllocFromPoolAsync failure path in allocator logic leaks the temporary
allocated_list_entry created by INIT_ALLOCATED_LIST_ENTRY, along with its nested
entry and allocHandle. Update the failure branch in the allocation routine that
calls CUDA_OVERRIDE_CALL on cuMemAllocFromPoolAsync to use the same cleanup
pattern as LIST_REMOVE before returning, so e, e->entry, and
e->entry->allocHandle are released on error.

Source: Linters/SAST tools

}
if (poollimit != 0) {
if (poollimit> device_allocasync->limit) {
allocsize = (poollimit-device_allocasync->limit < size)? poollimit-device_allocasync->limit : size;
cuCtxGetDevice(&dev);
add_gpu_device_memory_usage(getpid(), dev, allocsize, 2);
device_allocasync->limit=device_allocasync->limit+allocsize;
e->entry->length=allocsize;
}else{
e->entry->length=0;
}
}
LIST_ADD(device_allocasync,e);
return 0;
*address = e->entry->address;
/* Account against the caller's pool, not the default. */
return account_async_chunk(e, size, pool);
}

int allocate_async_raw(CUdeviceptr *dptr, size_t size, CUstream hStream) {
Expand All @@ -314,3 +342,11 @@ int allocate_async_raw(CUdeviceptr *dptr, size_t size, CUstream hStream) {
pthread_mutex_unlock(&mutex);
return tmp;
}

int allocate_from_pool_async_raw(CUdeviceptr *dptr, size_t size, CUmemoryPool pool, CUstream hStream) {
int tmp;
pthread_mutex_lock(&mutex);
tmp = add_chunk_from_pool_async(dptr, size, pool, hStream);
pthread_mutex_unlock(&mutex);
return tmp;
}
1 change: 1 addition & 0 deletions src/allocator/allocator.h
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ int free_raw(CUdeviceptr dptr);
int add_chunk_only(CUdeviceptr address, size_t size, CUdevice dev);
int remove_chunk_only(CUdeviceptr address);
int allocate_async_raw(CUdeviceptr *dptr, size_t size, CUstream hStream);
int allocate_from_pool_async_raw(CUdeviceptr *dptr, size_t size, CUmemoryPool pool, CUstream hStream);
int free_raw_async(CUdeviceptr dptr, CUstream hStream);

// Checks memory type
Expand Down
4 changes: 2 additions & 2 deletions src/cuda/memory.c
Original file line number Diff line number Diff line change
Expand Up @@ -687,8 +687,8 @@ CUresult cuMemPoolDestroy(CUmemoryPool pool) {
}

CUresult cuMemAllocFromPoolAsync(CUdeviceptr *dptr, size_t bytesize, CUmemoryPool pool, CUstream hStream) {
LOG_DEBUG("cuMemAllocFromPoolAsync");
return CUDA_OVERRIDE_CALL(cuda_library_entry,cuMemAllocFromPoolAsync,dptr,bytesize,pool,hStream);
LOG_DEBUG("cuMemAllocFromPoolAsync:%ld", bytesize);
return allocate_from_pool_async_raw(dptr, bytesize, pool, hStream);
}

CUresult cuMemPoolExportToShareableHandle(void *handle_out, CUmemoryPool pool, CUmemAllocationHandleType handleType, unsigned long long flags) {
Expand Down
124 changes: 124 additions & 0 deletions test/test_alloc_pool_async.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Reproducer for Project-HAMi/HAMi-core issue #93 (the same untracked-free leak
// class reported in #67): cuMemAllocFromPoolAsync allocations are invisible to
// libvgpu, so the matching cuMemFreeAsync is rejected.
//
// Mechanism (src/cuda/memory.c + src/allocator/allocator.c on main):
// cuMemFreeAsync() -> free_raw_async() -> remove_chunk_async() walks the
// device_allocasync list for the pointer. cuMemAllocAsync() registers there
// (via add_chunk_async); cuMemAllocFromPoolAsync() is a bare pass-through and
// never does. For a pool-explicit allocation the pointer is not found, so
// remove_chunk_async() returns -1 WITHOUT calling the real cuMemFreeAsync:
// (a) -1 surfaces as a CUresult -> "unrecognized error code", and
// (b) the real free is skipped -> the device allocation leaks.
//
// Path [A] (default pool) is the control: it IS tracked, so it still frees
// cleanly under libvgpu. Path [B] (explicit pool) is the bug. On stock CUDA
// (no LD_PRELOAD) both paths return CUDA_SUCCESS.
//
// Build (standalone, no HAMi headers needed):
// gcc test_alloc_pool_async.c -o test_alloc_pool_async \
// -I/usr/local/cuda/include -L/usr/local/cuda/lib64/stubs -lcuda
// Or drop into HAMi-core/test/ and `make build-in-docker` (auto-discovered).
//
// Run baseline (no libvgpu, expect PASS):
// ./test_alloc_pool_async
// Run under libvgpu (expect FAIL until fixed):
// LD_PRELOAD=/path/to/libvgpu.so CUDA_DEVICE_MEMORY_LIMIT=2g \
// LIBCUDA_LOG_LEVEL=1 ./test_alloc_pool_async

#include <cuda.h>
#include <stdio.h>
#include <string.h>

#ifndef TEST_DEVICE_ID
#define TEST_DEVICE_ID 0
#endif

#define ALLOC_BYTES (32ull * 1024 * 1024) /* 32 MiB */

/* Abort-on-error for setup calls that must succeed for the test to be valid. */
#define CHECK_DRV(call) \
do { \
CUresult _e = (call); \
if (_e != CUDA_SUCCESS) { \
const char *_n = NULL; \
cuGetErrorName(_e, &_n); \
fprintf(stderr, "FATAL %s:%d: %s -> %d (%s)\n", __FILE__, \
__LINE__, #call, (int)_e, _n ? _n : "?"); \
return 2; \
} \
} while (0)

/* Print a CUresult, flagging codes the driver itself does not recognise
(exactly what a caller sees as "unrecognized error code"). */
static void describe(const char *label, CUresult res) {
const char *name = NULL;
CUresult q = cuGetErrorName(res, &name);
if (q != CUDA_SUCCESS || name == NULL)
printf(" %-34s -> %d <unrecognized error code>\n", label, (int)res);
else
printf(" %-34s -> %d (%s)\n", label, (int)res, name);
}

int main(void) {
CHECK_DRV(cuInit(0));

CUdevice dev;
CHECK_DRV(cuDeviceGet(&dev, TEST_DEVICE_ID));

CUcontext ctx;
#if CUDA_VERSION >= 13000
CHECK_DRV(cuCtxCreate(&ctx, NULL, 0, dev));
#else
CHECK_DRV(cuCtxCreate(&ctx, 0, dev));
#endif

CUstream stream;
CHECK_DRV(cuStreamCreate(&stream, CU_STREAM_NON_BLOCKING));

int failures = 0;

/* [A] Control: default-pool async path. libvgpu tracks this one. */
printf("[A] cuMemAllocAsync + cuMemFreeAsync (default pool)\n");
{
CUdeviceptr d = 0;
CHECK_DRV(cuMemAllocAsync(&d, ALLOC_BYTES, stream));
CUresult f = cuMemFreeAsync(d, stream);
describe("cuMemFreeAsync", f);
CHECK_DRV(cuStreamSynchronize(stream));
if (f != CUDA_SUCCESS)
failures++;
}

/* [B] Bug: explicit-pool async path. libvgpu never tracks this one. */
printf("[B] cuMemAllocFromPoolAsync + cuMemFreeAsync (explicit pool)\n");
{
CUmemPoolProps props;
memset(&props, 0, sizeof(props));
props.allocType = CU_MEM_ALLOCATION_TYPE_PINNED;
props.location.type = CU_MEM_LOCATION_TYPE_DEVICE;
props.location.id = dev;

CUmemoryPool pool;
CHECK_DRV(cuMemPoolCreate(&pool, &props));

CUdeviceptr d = 0;
CHECK_DRV(cuMemAllocFromPoolAsync(&d, ALLOC_BYTES, pool, stream));
CUresult f = cuMemFreeAsync(d, stream);
describe("cuMemFreeAsync", f);
if (f != CUDA_SUCCESS)
failures++;

/* Best-effort teardown; a rejected free leaves the allocation live. */
cuStreamSynchronize(stream);
cuMemPoolDestroy(pool);
}

printf("\nResult: %s\n",
failures ? "FAIL - pool-async free rejected (issue #93 reproduced)"
: "PASS - both async free paths accepted");

cuStreamDestroy(stream);
cuCtxDestroy(ctx);
return failures ? 1 : 0;
}
Loading