From 6c94609612e6db63d695698e686842a107771260 Mon Sep 17 00:00:00 2001 From: Hugh Perkins Date: Sat, 9 May 2026 12:34:36 +0000 Subject: [PATCH 1/2] [AMDGPU] Trim default mempool on qd.reset() `AMDGPUContext` (and `CUDAContext`, symmetrically) configures the device's default async-alloc mempool with a 128 MiB release threshold at construction. After `qd.reset()`, any device allocations that were serviced from this pool get returned to the pool, but pages remain cached in the pool until either an allocation request needs them or the threshold is exceeded. From the kernel driver's perspective this means up to 128 MiB of "freed" pages stay reserved per process, which to co-tenants on the same device looks indistinguishable from memory still in use. This silently violates the user-facing contract of `qd.reset()` ("release everything I allocated") and on AMDGPU was a contributing factor to multi-process `HSA_STATUS_ERROR_OUT_OF_RESOURCES` failures under `pytest-xdist`: 8 workers each holding 128 MiB of stranded mempool cache adds up to ~1 GiB of aggregate VRAM that no process is actually using but no process can hand back without an explicit trim call (see perso_hugh/doc/amdgpu_test_suite_oom_followup.md). Add a `trim_default_mem_pool()` method to both contexts that calls `hipMemPoolTrimTo` / `cuMemPoolTrimTo` with `min_bytes_to_keep = 0`, and invoke it from `LlvmRuntimeExecutor::finalize` after the existing `synchronize()`. No-op on devices without mempool support. The next `qd.init()` re-allocates from the driver instead of from the cached pool, which is irrelevant against the much heavier work `qd.init()` already does (LLVM JIT, HSACO load). --- quadrants/rhi/amdgpu/amdgpu_context.cpp | 10 +++++++++ quadrants/rhi/amdgpu/amdgpu_context.h | 12 +++++++++++ .../rhi/amdgpu/amdgpu_driver_functions.inc.h | 1 + quadrants/rhi/cuda/cuda_context.cpp | 9 ++++++++ quadrants/rhi/cuda/cuda_context.h | 7 +++++++ .../rhi/cuda/cuda_driver_functions.inc.h | 1 + .../runtime/llvm/llvm_runtime_executor.cpp | 21 +++++++++++++++++++ 7 files changed, 61 insertions(+) diff --git a/quadrants/rhi/amdgpu/amdgpu_context.cpp b/quadrants/rhi/amdgpu/amdgpu_context.cpp index 5748895a41..7f0200d464 100644 --- a/quadrants/rhi/amdgpu/amdgpu_context.cpp +++ b/quadrants/rhi/amdgpu/amdgpu_context.cpp @@ -205,6 +205,16 @@ void AMDGPUContext::launch(void *func, } } +void AMDGPUContext::trim_default_mem_pool() { + if (!supports_mem_pool_) { + return; + } + void *default_mem_pool = nullptr; + driver_.device_get_default_mem_pool(&default_mem_pool, 0 /*device ordinal*/); + // `min_bytes_to_keep = 0` releases every cached page back to the driver. + driver_.mem_pool_trim_to(default_mem_pool, 0u); +} + AMDGPUContext::~AMDGPUContext() { // Currently unreachable: singleton is heap-allocated via `new` in get_instance() and never deleted. for (auto *s : stream_pool_) { diff --git a/quadrants/rhi/amdgpu/amdgpu_context.h b/quadrants/rhi/amdgpu/amdgpu_context.h index 9283afa078..44a8857b8f 100644 --- a/quadrants/rhi/amdgpu/amdgpu_context.h +++ b/quadrants/rhi/amdgpu/amdgpu_context.h @@ -79,6 +79,18 @@ class AMDGPUContext { return supports_mem_pool_; } + // Force the default device memory pool to release every cached page back to + // the driver. Called by `LlvmRuntimeExecutor::finalize` (i.e. `qd.reset()`) + // to align actual driver-visible free VRAM with the user-facing contract + // that `qd.reset()` releases everything Quadrants allocated. Without this, + // up to the configured release threshold (128 MiB at construction) of + // freed pages stays cached in the pool and shows up as "used" to other + // processes on the same VF, materially raising the chance of + // multi-process `HSA_STATUS_ERROR_OUT_OF_RESOURCES` failures across + // pytest-xdist workers. No-op if the device does not advertise mempool + // support. + void trim_default_mem_pool(); + ~AMDGPUContext(); class ContextGuard { diff --git a/quadrants/rhi/amdgpu/amdgpu_driver_functions.inc.h b/quadrants/rhi/amdgpu/amdgpu_driver_functions.inc.h index c94a7f14db..55107161ff 100644 --- a/quadrants/rhi/amdgpu/amdgpu_driver_functions.inc.h +++ b/quadrants/rhi/amdgpu/amdgpu_driver_functions.inc.h @@ -42,6 +42,7 @@ PER_AMDGPU_FUNCTION(mem_alloc_host, hipHostMalloc, void **, std::size_t, uint32) PER_AMDGPU_FUNCTION(mem_free_host, hipHostFree, void *); PER_AMDGPU_FUNCTION(device_get_default_mem_pool, hipDeviceGetDefaultMemPool, void **, int); PER_AMDGPU_FUNCTION(mem_pool_set_attribute, hipMemPoolSetAttribute, void *, uint32, void *); +PER_AMDGPU_FUNCTION(mem_pool_trim_to, hipMemPoolTrimTo, void *, std::size_t); PER_AMDGPU_FUNCTION(mem_get_info, hipMemGetInfo, std::size_t *, std::size_t *); PER_AMDGPU_FUNCTION(mem_get_attribute, hipPointerGetAttribute, void *, uint32, void *); PER_AMDGPU_FUNCTION(mem_get_attributes, hipPointerGetAttributes, void *, void *); diff --git a/quadrants/rhi/cuda/cuda_context.cpp b/quadrants/rhi/cuda/cuda_context.cpp index d1a266a1a9..f4d7c73409 100644 --- a/quadrants/rhi/cuda/cuda_context.cpp +++ b/quadrants/rhi/cuda/cuda_context.cpp @@ -109,6 +109,15 @@ std::string CUDAContext::get_device_name() { return str; } +void CUDAContext::trim_default_mem_pool() { + if (!supports_mem_pool_) { + return; + } + void *default_mem_pool = nullptr; + driver_.device_get_default_mem_pool(&default_mem_pool, device_); + driver_.mem_pool_trim_to(default_mem_pool, 0u); +} + int64_t CUDAContext::get_clock_rate_khz() const { int clock_rate_khz = 0; driver_.device_get_attribute(&clock_rate_khz, CU_DEVICE_ATTRIBUTE_CLOCK_RATE, device_); diff --git a/quadrants/rhi/cuda/cuda_context.h b/quadrants/rhi/cuda/cuda_context.h index aafb3ed12b..f5239f4b8d 100644 --- a/quadrants/rhi/cuda/cuda_context.h +++ b/quadrants/rhi/cuda/cuda_context.h @@ -89,6 +89,13 @@ class CUDAContext { return supports_mem_pool_; } + // Force the default device memory pool to release every cached page back + // to the driver. Symmetric with the AMDGPU side and called from + // `LlvmRuntimeExecutor::finalize` (i.e. `qd.reset()`); see + // `AMDGPUContext::trim_default_mem_pool` for the rationale. No-op when + // mempool support is unavailable. + void trim_default_mem_pool(); + // True when the device can coherently dereference plain host pointers (`malloc` / `new`) from kernel code via HMM / // system-allocated memory. Maps `CU_DEVICE_ATTRIBUTE_PAGEABLE_MEMORY_ACCESS` directly - 1 on Linux with an // HMM-capable driver + kernel (open-source nvidia module or 535+ with HMM enabled), 0 on Turing and older parts, diff --git a/quadrants/rhi/cuda/cuda_driver_functions.inc.h b/quadrants/rhi/cuda/cuda_driver_functions.inc.h index b4164b7c33..90361b2832 100644 --- a/quadrants/rhi/cuda/cuda_driver_functions.inc.h +++ b/quadrants/rhi/cuda/cuda_driver_functions.inc.h @@ -41,6 +41,7 @@ PER_CUDA_FUNCTION(mem_advise, cuMemAdvise, void *, std::size_t, uint32, uint32); PER_CUDA_FUNCTION(mem_get_info, cuMemGetInfo_v2, std::size_t *, std::size_t *); PER_CUDA_FUNCTION(mem_get_attribute, cuPointerGetAttribute, void *, uint32, void *); PER_CUDA_FUNCTION(mem_pool_set_attribute, cuMemPoolSetAttribute, void *, uint32, void *); +PER_CUDA_FUNCTION(mem_pool_trim_to, cuMemPoolTrimTo, void *, std::size_t); // Module and kernels PER_CUDA_FUNCTION(module_get_function, cuModuleGetFunction, void **, void *, const char *); diff --git a/quadrants/runtime/llvm/llvm_runtime_executor.cpp b/quadrants/runtime/llvm/llvm_runtime_executor.cpp index c604758dd5..48fce77284 100644 --- a/quadrants/runtime/llvm/llvm_runtime_executor.cpp +++ b/quadrants/runtime/llvm/llvm_runtime_executor.cpp @@ -612,6 +612,27 @@ void LlvmRuntimeExecutor::finalize() { // Release unused memory from cuda memory pool synchronize(); + + // Trim the driver's default async-alloc mempool back to the driver. The + // mempool's release-threshold (set to 128 MiB at context construction) + // would otherwise keep up to that many bytes of freed pages cached, which + // shows up to other co-tenant processes on the same device as memory + // still in use. Aligning post-`qd.reset()` driver-visible free VRAM with + // the user-facing contract that `qd.reset()` releases everything + // Quadrants allocated materially reduces multi-process + // `HSA_STATUS_ERROR_OUT_OF_RESOURCES` failures across `pytest-xdist` + // workers (see perso_hugh/doc/amdgpu_test_suite_oom_followup.md). No-op + // on devices without mempool support. +#if defined(QD_WITH_CUDA) + if (config_.arch == Arch::cuda) { + CUDAContext::get_instance().trim_default_mem_pool(); + } +#endif +#if defined(QD_WITH_AMDGPU) + if (config_.arch == Arch::amdgpu) { + AMDGPUContext::get_instance().trim_default_mem_pool(); + } +#endif } finalized_ = true; } From 444e4a8ad4e6f6af5f858948a38c9d031dfabaaa Mon Sep 17 00:00:00 2001 From: Hugh Perkins Date: Sat, 9 May 2026 13:39:42 +0000 Subject: [PATCH 2/2] [AMDGPU] Reflow trim_default_mem_pool comments to 120c --- quadrants/rhi/amdgpu/amdgpu_context.h | 16 ++++++---------- quadrants/rhi/cuda/cuda_context.h | 8 +++----- quadrants/runtime/llvm/llvm_runtime_executor.cpp | 16 ++++++---------- 3 files changed, 15 insertions(+), 25 deletions(-) diff --git a/quadrants/rhi/amdgpu/amdgpu_context.h b/quadrants/rhi/amdgpu/amdgpu_context.h index 44a8857b8f..28a5f3dea6 100644 --- a/quadrants/rhi/amdgpu/amdgpu_context.h +++ b/quadrants/rhi/amdgpu/amdgpu_context.h @@ -79,16 +79,12 @@ class AMDGPUContext { return supports_mem_pool_; } - // Force the default device memory pool to release every cached page back to - // the driver. Called by `LlvmRuntimeExecutor::finalize` (i.e. `qd.reset()`) - // to align actual driver-visible free VRAM with the user-facing contract - // that `qd.reset()` releases everything Quadrants allocated. Without this, - // up to the configured release threshold (128 MiB at construction) of - // freed pages stays cached in the pool and shows up as "used" to other - // processes on the same VF, materially raising the chance of - // multi-process `HSA_STATUS_ERROR_OUT_OF_RESOURCES` failures across - // pytest-xdist workers. No-op if the device does not advertise mempool - // support. + // Force the default device memory pool to release every cached page back to the driver. Called by + // `LlvmRuntimeExecutor::finalize` (i.e. `qd.reset()`) to align actual driver-visible free VRAM with the user-facing + // contract that `qd.reset()` releases everything Quadrants allocated. Without this, up to the configured release + // threshold (128 MiB at construction) of freed pages stays cached in the pool and shows up as "used" to other + // processes on the same VF, materially raising the chance of multi-process `HSA_STATUS_ERROR_OUT_OF_RESOURCES` + // failures across pytest-xdist workers. No-op if the device does not advertise mempool support. void trim_default_mem_pool(); ~AMDGPUContext(); diff --git a/quadrants/rhi/cuda/cuda_context.h b/quadrants/rhi/cuda/cuda_context.h index f5239f4b8d..e2ebf07117 100644 --- a/quadrants/rhi/cuda/cuda_context.h +++ b/quadrants/rhi/cuda/cuda_context.h @@ -89,11 +89,9 @@ class CUDAContext { return supports_mem_pool_; } - // Force the default device memory pool to release every cached page back - // to the driver. Symmetric with the AMDGPU side and called from - // `LlvmRuntimeExecutor::finalize` (i.e. `qd.reset()`); see - // `AMDGPUContext::trim_default_mem_pool` for the rationale. No-op when - // mempool support is unavailable. + // Force the default device memory pool to release every cached page back to the driver. Symmetric with the AMDGPU + // side and called from `LlvmRuntimeExecutor::finalize` (i.e. `qd.reset()`); see + // `AMDGPUContext::trim_default_mem_pool` for the rationale. No-op when mempool support is unavailable. void trim_default_mem_pool(); // True when the device can coherently dereference plain host pointers (`malloc` / `new`) from kernel code via HMM / diff --git a/quadrants/runtime/llvm/llvm_runtime_executor.cpp b/quadrants/runtime/llvm/llvm_runtime_executor.cpp index 48fce77284..1b6a714aac 100644 --- a/quadrants/runtime/llvm/llvm_runtime_executor.cpp +++ b/quadrants/runtime/llvm/llvm_runtime_executor.cpp @@ -613,16 +613,12 @@ void LlvmRuntimeExecutor::finalize() { // Release unused memory from cuda memory pool synchronize(); - // Trim the driver's default async-alloc mempool back to the driver. The - // mempool's release-threshold (set to 128 MiB at context construction) - // would otherwise keep up to that many bytes of freed pages cached, which - // shows up to other co-tenant processes on the same device as memory - // still in use. Aligning post-`qd.reset()` driver-visible free VRAM with - // the user-facing contract that `qd.reset()` releases everything - // Quadrants allocated materially reduces multi-process - // `HSA_STATUS_ERROR_OUT_OF_RESOURCES` failures across `pytest-xdist` - // workers (see perso_hugh/doc/amdgpu_test_suite_oom_followup.md). No-op - // on devices without mempool support. + // Trim the driver's default async-alloc mempool back to the driver. The mempool's release-threshold (set to 128 + // MiB at context construction) would otherwise keep up to that many bytes of freed pages cached, which shows up to + // other co-tenant processes on the same device as memory still in use. Aligning post-`qd.reset()` driver-visible + // free VRAM with the user-facing contract that `qd.reset()` releases everything Quadrants allocated materially + // reduces multi-process `HSA_STATUS_ERROR_OUT_OF_RESOURCES` failures across `pytest-xdist` workers (see + // perso_hugh/doc/amdgpu_test_suite_oom_followup.md). No-op on devices without mempool support. #if defined(QD_WITH_CUDA) if (config_.arch == Arch::cuda) { CUDAContext::get_instance().trim_default_mem_pool();