From d4767482166845bdf583dd696b9baf971dbe20bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sat, 11 Oct 2025 18:31:46 +0200 Subject: [PATCH] Remove CPU fallback from GPU context pool --- PerfectNumbers.Core/Gpu/GpuContextPool.cs | 265 +++++++++++----------- PerfectNumbers.Core/Gpu/GpuKernelPool.cs | 5 +- 2 files changed, 138 insertions(+), 132 deletions(-) diff --git a/PerfectNumbers.Core/Gpu/GpuContextPool.cs b/PerfectNumbers.Core/Gpu/GpuContextPool.cs index 1361e01..9b0f7c2 100644 --- a/PerfectNumbers.Core/Gpu/GpuContextPool.cs +++ b/PerfectNumbers.Core/Gpu/GpuContextPool.cs @@ -6,156 +6,159 @@ namespace PerfectNumbers.Core.Gpu; public static class GpuContextPool { - private static readonly bool PoolingEnabled = true; - // TODO: Introduce accelerator-specific warmup so pooled contexts precompile the ProcessEightBitWindows kernels and load - // divisor-cycle data before the first scan begins. - // Default device preference for generic GPU kernels (prime scans, NTT, etc.) - public static bool ForceCpu { get; set; } = false; + private static readonly bool PoolingEnabled = true; + // TODO: Introduce accelerator-specific warmup so pooled contexts precompile the ProcessEightBitWindows kernels and load + // divisor-cycle data before the first scan begins. + // Default device preference for generic GPU kernels (prime scans, NTT, etc.) + public static bool ForceCpu { get; set; } = false; - internal sealed class PooledContext + internal sealed class PooledContext + { + private bool disposed; + + public Context Context { get; } + public Accelerator Accelerator { get; } + public bool IsCpu { get; } + public object ExecutionLock { get; } = new(); + + public PooledContext(bool preferCpu) { - private bool disposed; - - public Context Context { get; } - public Accelerator Accelerator { get; } - public bool IsCpu { get; } - public object ExecutionLock { get; } = new(); - - public PooledContext(bool preferCpu) - { - Context = Context.CreateDefault(); - Accelerator = Context.GetPreferredDevice(preferCpu).CreateAccelerator(Context); - IsCpu = Accelerator.AcceleratorType == AcceleratorType.CPU; - // NOTE: Avoid loading/compiling any kernel here to prevent implicit - // CL stream/queue creation during accelerator construction. - // Some OpenCL drivers are fragile when a queue is created immediately - // after device init. Real kernels will be JIT-loaded at first use. - } - - public void Dispose() - { - if (disposed) - { - return; - } - - // Ensure all cached GPU buffers for this accelerator are released. - NttGpuMath.ClearCaches(Accelerator); - // Release PrimeTester GPU state for this accelerator (kernel/device primes) - // Clear any per-accelerator cached resources to avoid releasing - // after the accelerator is destroyed. - PrimeTester.ClearGpuCaches(Accelerator); - - Accelerator.Dispose(); - Context.Dispose(); - disposed = true; - } - } - - private static readonly ConcurrentQueue CpuPool = new(); - private static readonly ConcurrentQueue GpuPool = new(); - private static readonly object CreationLock = new(); - - public static GpuContextLease Rent() - { - return RentPreferred(ForceCpu); - } - - // Allows callers to choose CPU/GPU per use-case, decoupled from ForceCpu. - public static GpuContextLease RentPreferred(bool preferCpu) - { - if (PoolingEnabled) - { - if (preferCpu && CpuPool.TryDequeue(out var cpu)) - { - return new GpuContextLease(cpu, CreationLock); - } - - if (!preferCpu && GpuPool.TryDequeue(out var gpu)) - { - return new GpuContextLease(gpu, CreationLock); - } - } - - // Serialize accelerator creation to avoid concurrent CL device/stream setup across threads. - lock (CreationLock) - { - return new GpuContextLease(new PooledContext(preferCpu), CreationLock); - } - } - - public static void DisposeAll() - { - if (!PoolingEnabled) - { - return; - } - - while (CpuPool.TryDequeue(out var cpu)) - { - cpu.Dispose(); - } - - while (GpuPool.TryDequeue(out var gpu)) - { - gpu.Dispose(); - } - } - - private static void Return(PooledContext ctx) - { - if (PoolingEnabled) - { - ctx.Accelerator.Synchronize(); - if (ctx.IsCpu) - { - CpuPool.Enqueue(ctx); - } - else - { - GpuPool.Enqueue(ctx); - } - } - - else - { - lock (CreationLock) - { - ctx.Dispose(); - } - } + Context = Context.CreateDefault(); + Accelerator = Context.GetPreferredDevice(preferCpu).CreateAccelerator(Context); + IsCpu = Accelerator.AcceleratorType == AcceleratorType.CPU; + // NOTE: Avoid loading/compiling any kernel here to prevent implicit + // CL stream/queue creation during accelerator construction. + // Some OpenCL drivers are fragile when a queue is created immediately + // after device init. Real kernels will be JIT-loaded at first use. } + public void Dispose() + { + if (disposed) + { + return; + } + + // Ensure all cached GPU buffers for this accelerator are released. + NttGpuMath.ClearCaches(Accelerator); + // Release PrimeTester GPU state for this accelerator (kernel/device primes) + // Clear any per-accelerator cached resources to avoid releasing + // after the accelerator is destroyed. + PrimeTester.ClearGpuCaches(Accelerator); + + Accelerator.Dispose(); + Context.Dispose(); + disposed = true; + } + } + + private static readonly ConcurrentQueue CpuPool = new(); + private static readonly ConcurrentQueue GpuPool = new(); + private static readonly object CreationLock = new(); + + public static GpuContextLease Rent() + { + return RentPreferred(ForceCpu); + } + + // Allows callers to choose CPU/GPU per use-case, decoupled from ForceCpu. + public static GpuContextLease RentPreferred(bool preferCpu) + { + if (PoolingEnabled) + { + if (preferCpu && CpuPool.TryDequeue(out var cpu)) + { + return new GpuContextLease(cpu, CreationLock); + } + + if (!preferCpu && GpuPool.TryDequeue(out var gpu)) + { + return new GpuContextLease(gpu, CreationLock); + } + } + + // Serialize accelerator creation to avoid concurrent CL device/stream setup across threads. + lock (CreationLock) + { + return new GpuContextLease(new PooledContext(preferCpu), CreationLock); + } + } + + public static void DisposeAll() + { + if (!PoolingEnabled) + { + return; + } + + while (CpuPool.TryDequeue(out var cpu)) + { + cpu.Dispose(); + } + + while (GpuPool.TryDequeue(out var gpu)) + { + gpu.Dispose(); + } + } + + private static void Return(PooledContext ctx) + { + if (PoolingEnabled) + { + ctx.Accelerator.Synchronize(); + if (ctx.IsCpu) + { + CpuPool.Enqueue(ctx); + } + else + { + GpuPool.Enqueue(ctx); + } + } + + else + { + lock (CreationLock) + { + ctx.Dispose(); + } + } + } + public struct GpuContextLease { private readonly PooledContext _ctx; - private bool disposed; + private bool disposed; - internal GpuContextLease(PooledContext ctx, object kernelInitLock) + internal GpuContextLease(PooledContext ctx, object kernelInitLock) { _ctx = ctx; - KernelInitLock = kernelInitLock; - } + KernelInitLock = kernelInitLock; + } public Context Context => _ctx.Context; public Accelerator Accelerator => _ctx.Accelerator; - public object KernelInitLock { get; } + public object KernelInitLock { get; } - public object ExecutionLock => _ctx.ExecutionLock; + public object ExecutionLock => _ctx.ExecutionLock; + internal void ReturnToPool() + { + if (disposed) + { + return; + } - public void Dispose() - { - if (disposed) - { - return; - } + Return(_ctx); + disposed = true; + } - Return(_ctx); - disposed = true; + public void Dispose() + { + ReturnToPool(); } } } - diff --git a/PerfectNumbers.Core/Gpu/GpuKernelPool.cs b/PerfectNumbers.Core/Gpu/GpuKernelPool.cs index d3b5013..d9a1ac0 100644 --- a/PerfectNumbers.Core/Gpu/GpuKernelPool.cs +++ b/PerfectNumbers.Core/Gpu/GpuKernelPool.cs @@ -396,7 +396,10 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne _stream?.Dispose(); _stream = null; - _gpu?.Dispose(); + if (_gpu.HasValue) + { + _gpu.Value.ReturnToPool(); + } _gpu = null; _executionLock = null;