From 00d4e9bde2f7a1b5324a1e2f8805989d0526672b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sun, 2 Nov 2025 13:47:57 +0100 Subject: [PATCH 1/2] Add GPU lease warm-up and shared limiter --- EvenPerfectBitScanner/Program.cs | 5 +- PerfectNumbers.Core/Gpu/GpuContextPool.cs | 44 +++++--- .../Gpu/GpuPrimeLeaseLimiter.cs | 37 +++++++ PerfectNumbers.Core/PrimeTester.cs | 101 ++++++++++++------ 4 files changed, 139 insertions(+), 48 deletions(-) create mode 100644 PerfectNumbers.Core/Gpu/GpuPrimeLeaseLimiter.cs diff --git a/EvenPerfectBitScanner/Program.cs b/EvenPerfectBitScanner/Program.cs index b55bce1..c12e9cc 100644 --- a/EvenPerfectBitScanner/Program.cs +++ b/EvenPerfectBitScanner/Program.cs @@ -58,14 +58,15 @@ internal static class Program bool startPrimeProvided = _cliArguments.StartPrimeProvided; int threadCount = Math.Max(1, _cliArguments.ThreadCount); int gpuPrimeBatch = Math.Max(1, _cliArguments.GpuPrimeBatch); + int gpuPrimeThreads = Math.Max(1, _cliArguments.GpuPrimeThreads); UnboundedTaskScheduler.ConfigureThreadCount(threadCount); PrimeTester.GpuBatchSize = gpuPrimeBatch; + GpuPrimeLeaseLimiter.Initialize(gpuPrimeThreads); GpuContextPool.WarmUpPool(threadCount); - PrimeTester.WarmUpGpuKernels(threadCount); + PrimeTester.WarmUpGpuKernels(gpuPrimeThreads); Console.WriteLine("Starting up threads..."); _ = UnboundedTaskScheduler.Instance; int blockSize = Math.Max(1, _cliArguments.BlockSize); - int gpuPrimeThreads = Math.Max(1, _cliArguments.GpuPrimeThreads); GpuKernelType kernelType = _cliArguments.KernelType; bool useBitTransform = _cliArguments.UseBitTransform; bool useOrder = _cliArguments.UseOrder; diff --git a/PerfectNumbers.Core/Gpu/GpuContextPool.cs b/PerfectNumbers.Core/Gpu/GpuContextPool.cs index 5e9f0a5..47c546b 100644 --- a/PerfectNumbers.Core/Gpu/GpuContextPool.cs +++ b/PerfectNumbers.Core/Gpu/GpuContextPool.cs @@ -50,13 +50,23 @@ public static class GpuContextPool // Allows callers to choose CPU/GPU per use-case, decoupled from ForceCpu. public static GpuContextLease Rent() { - if (GpuPool.TryDequeue(out var gpu)) - { - return new GpuContextLease(gpu); - } + GpuPrimeLeaseLimiter.Enter(); - // Create a new accelerator when the pool does not have one available. - return new GpuContextLease(new PooledContext()); + try + { + if (GpuPool.TryDequeue(out var gpu)) + { + return new GpuContextLease(gpu); + } + + // Create a new accelerator when the pool does not have one available. + return new GpuContextLease(new PooledContext()); + } + catch + { + GpuPrimeLeaseLimiter.Exit(); + throw; + } } public static void WarmUpPool(int threadCount) @@ -96,23 +106,33 @@ public static class GpuContextPool { ctx.Accelerator.Synchronize(); GpuPool.Enqueue(ctx); + GpuPrimeLeaseLimiter.Exit(); } - public readonly struct GpuContextLease + public struct GpuContextLease { - private readonly PooledContext _ctx; + private PooledContext? _ctx; + private bool _disposed; internal GpuContextLease(PooledContext ctx) { _ctx = ctx; + _disposed = false; } - public readonly Context Context => _ctx.Context; - public readonly Accelerator Accelerator => _ctx.Accelerator; + public Context Context => _ctx!.Context; + public Accelerator Accelerator => _ctx!.Accelerator; - public readonly void Dispose() + public void Dispose() { - Return(_ctx); + if (_disposed) + { + return; + } + + _disposed = true; + Return(_ctx!); + _ctx = null; } } } diff --git a/PerfectNumbers.Core/Gpu/GpuPrimeLeaseLimiter.cs b/PerfectNumbers.Core/Gpu/GpuPrimeLeaseLimiter.cs new file mode 100644 index 0000000..a444668 --- /dev/null +++ b/PerfectNumbers.Core/Gpu/GpuPrimeLeaseLimiter.cs @@ -0,0 +1,37 @@ +using System.Threading; + +namespace PerfectNumbers.Core.Gpu; + +public static class GpuPrimeLeaseLimiter +{ + private static SemaphoreSlim _semaphore = new(int.MaxValue, int.MaxValue); + private static int _currentLimit = int.MaxValue; + + public static void Initialize(int limit) + { + if (limit <= 0) + { + limit = 1; + } + + if (limit == _currentLimit) + { + return; + } + + var previous = _semaphore; + _semaphore = new SemaphoreSlim(limit, limit); + _currentLimit = limit; + previous.Dispose(); + } + + public static void Enter() + { + _semaphore.Wait(); + } + + public static void Exit() + { + _semaphore.Release(); + } +} diff --git a/PerfectNumbers.Core/PrimeTester.cs b/PerfectNumbers.Core/PrimeTester.cs index 36187f9..100e9f6 100644 --- a/PerfectNumbers.Core/PrimeTester.cs +++ b/PerfectNumbers.Core/PrimeTester.cs @@ -159,18 +159,7 @@ public sealed class PrimeTester return; } - int toWarm = target - WarmedGpuLeaseCount; - var leases = new PrimeTesterGpuContextPool.PrimeTesterGpuContextLease[toWarm]; - for (int i = 0; i < toWarm; i++) - { - leases[i] = PrimeTesterGpuContextPool.Rent(GpuBatchSize); - } - - for (int i = 0; i < toWarm; i++) - { - leases[i].Dispose(); - } - + PrimeTesterGpuContextPool.WarmUp(target, GpuBatchSize); WarmedGpuLeaseCount = target; } } @@ -229,6 +218,8 @@ public sealed class PrimeTester internal static class PrimeTesterGpuContextPool { private static readonly ConcurrentQueue Pool = new(); + private static readonly object WarmUpLock = new(); + private static int WarmedLeaseCount; private static readonly object SharedTablesLock = new(); private static readonly Dictionary SharedTables = new(); @@ -267,25 +258,60 @@ public sealed class PrimeTester } } - internal static PrimeTesterGpuContextLease Rent(int minBufferCapacity = 1) + internal static void WarmUp(int target, int minBufferCapacity) { - if (!Pool.TryDequeue(out var lease)) + if (target <= 0) { - lease = new PrimeTesterGpuContextLease(minBufferCapacity); - } - else - { - lease.EnsureCapacity(minBufferCapacity); + return; } - lease.ResetReturnFlag(); - return lease; + lock (WarmUpLock) + { + if (target <= WarmedLeaseCount) + { + return; + } + + int toCreate = target - WarmedLeaseCount; + for (int i = 0; i < toCreate; i++) + { + Pool.Enqueue(new PrimeTesterGpuContextLease(minBufferCapacity)); + } + + WarmedLeaseCount = target; + } + } + + internal static PrimeTesterGpuContextLease Rent(int minBufferCapacity = 1) + { + GpuPrimeLeaseLimiter.Enter(); + + try + { + if (!Pool.TryDequeue(out var lease)) + { + lease = new PrimeTesterGpuContextLease(minBufferCapacity); + } + else + { + lease.EnsureCapacity(minBufferCapacity); + } + + lease.ResetReturnFlag(); + return lease; + } + catch + { + GpuPrimeLeaseLimiter.Exit(); + throw; + } } internal static void Return(PrimeTesterGpuContextLease lease) { lease.Accelerator.Synchronize(); Pool.Enqueue(lease); + GpuPrimeLeaseLimiter.Exit(); } internal static void DisposeAll() @@ -304,6 +330,8 @@ public sealed class PrimeTester SharedTables.Clear(); } + + WarmedLeaseCount = 0; } internal static void Clear(Accelerator accelerator) @@ -313,23 +341,28 @@ public sealed class PrimeTester return; } - var retained = new List(); - while (Pool.TryDequeue(out var lease)) + lock (WarmUpLock) { - if (lease.Accelerator == accelerator) + var retained = new List(); + while (Pool.TryDequeue(out var lease)) { - lease.DisposeResources(); + if (lease.Accelerator == accelerator) + { + lease.DisposeResources(); + } + else + { + retained.Add(lease); + } } - else - { - retained.Add(lease); - } - } - int retainedCount = retained.Count; - for (int i = 0; i < retainedCount; i++) - { - Pool.Enqueue(retained[i]); + int retainedCount = retained.Count; + for (int i = 0; i < retainedCount; i++) + { + Pool.Enqueue(retained[i]); + } + + WarmedLeaseCount = Pool.Count; } } From 1cbe7bf869fec9679d8da8f4893ae10ef37edb71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sun, 2 Nov 2025 14:02:15 +0100 Subject: [PATCH 2/2] Use GpuPrimeWorkLimiter for GPU lease limiting --- EvenPerfectBitScanner/Program.cs | 5 +-- PerfectNumbers.Core/Gpu/GpuContextPool.cs | 24 ++++-------- .../Gpu/GpuPrimeLeaseLimiter.cs | 37 ------------------- .../Gpu/GpuPrimeWorkLimiter.cs | 33 +++++++++++------ PerfectNumbers.Core/PrimeTester.cs | 36 +++++++----------- 5 files changed, 42 insertions(+), 93 deletions(-) delete mode 100644 PerfectNumbers.Core/Gpu/GpuPrimeLeaseLimiter.cs diff --git a/EvenPerfectBitScanner/Program.cs b/EvenPerfectBitScanner/Program.cs index c12e9cc..bcd7937 100644 --- a/EvenPerfectBitScanner/Program.cs +++ b/EvenPerfectBitScanner/Program.cs @@ -61,7 +61,7 @@ internal static class Program int gpuPrimeThreads = Math.Max(1, _cliArguments.GpuPrimeThreads); UnboundedTaskScheduler.ConfigureThreadCount(threadCount); PrimeTester.GpuBatchSize = gpuPrimeBatch; - GpuPrimeLeaseLimiter.Initialize(gpuPrimeThreads); + GpuPrimeWorkLimiter.SetLimit(gpuPrimeThreads); GpuContextPool.WarmUpPool(threadCount); PrimeTester.WarmUpGpuKernels(gpuPrimeThreads); Console.WriteLine("Starting up threads..."); @@ -118,9 +118,6 @@ internal static class Program return; } - // Apply GPU prime sieve runtime configuration - GpuPrimeWorkLimiter.SetLimit(gpuPrimeThreads); - PrimeTester.GpuBatchSize = gpuPrimeBatch; MersenneDivisorCycles mersenneDivisorCycles = new(); if (!File.Exists(cyclesPath) || continueCyclesGeneration) diff --git a/PerfectNumbers.Core/Gpu/GpuContextPool.cs b/PerfectNumbers.Core/Gpu/GpuContextPool.cs index 47c546b..b6218c4 100644 --- a/PerfectNumbers.Core/Gpu/GpuContextPool.cs +++ b/PerfectNumbers.Core/Gpu/GpuContextPool.cs @@ -48,27 +48,18 @@ public static class GpuContextPool private static int WarmedGpuContextCount; // Allows callers to choose CPU/GPU per use-case, decoupled from ForceCpu. - public static GpuContextLease Rent() + public static GpuContextLease Rent() { - GpuPrimeLeaseLimiter.Enter(); - - try + if (GpuPool.TryDequeue(out var gpu)) { - if (GpuPool.TryDequeue(out var gpu)) - { - return new GpuContextLease(gpu); - } + return new GpuContextLease(gpu); + } - // Create a new accelerator when the pool does not have one available. - return new GpuContextLease(new PooledContext()); - } - catch - { - GpuPrimeLeaseLimiter.Exit(); - throw; - } + // Create a new accelerator when the pool does not have one available. + return new GpuContextLease(new PooledContext()); } + public static void WarmUpPool(int threadCount) { if (threadCount <= WarmedGpuContextCount) @@ -106,7 +97,6 @@ public static class GpuContextPool { ctx.Accelerator.Synchronize(); GpuPool.Enqueue(ctx); - GpuPrimeLeaseLimiter.Exit(); } public struct GpuContextLease diff --git a/PerfectNumbers.Core/Gpu/GpuPrimeLeaseLimiter.cs b/PerfectNumbers.Core/Gpu/GpuPrimeLeaseLimiter.cs deleted file mode 100644 index a444668..0000000 --- a/PerfectNumbers.Core/Gpu/GpuPrimeLeaseLimiter.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Threading; - -namespace PerfectNumbers.Core.Gpu; - -public static class GpuPrimeLeaseLimiter -{ - private static SemaphoreSlim _semaphore = new(int.MaxValue, int.MaxValue); - private static int _currentLimit = int.MaxValue; - - public static void Initialize(int limit) - { - if (limit <= 0) - { - limit = 1; - } - - if (limit == _currentLimit) - { - return; - } - - var previous = _semaphore; - _semaphore = new SemaphoreSlim(limit, limit); - _currentLimit = limit; - previous.Dispose(); - } - - public static void Enter() - { - _semaphore.Wait(); - } - - public static void Exit() - { - _semaphore.Release(); - } -} diff --git a/PerfectNumbers.Core/Gpu/GpuPrimeWorkLimiter.cs b/PerfectNumbers.Core/Gpu/GpuPrimeWorkLimiter.cs index 1d39b7d..48f3abb 100644 --- a/PerfectNumbers.Core/Gpu/GpuPrimeWorkLimiter.cs +++ b/PerfectNumbers.Core/Gpu/GpuPrimeWorkLimiter.cs @@ -1,3 +1,6 @@ +using System; +using System.Threading; + namespace PerfectNumbers.Core.Gpu; public static class GpuPrimeWorkLimiter @@ -10,24 +13,31 @@ public static class GpuPrimeWorkLimiter // TODO: Inline the pooled limiter guard from the GpuLimiterThroughputBenchmarks so prime-sieve // GPU jobs stop allocating Lease instances on every acquisition. var sem = _semaphore; - // sem.Wait(); + sem.Wait(); return new Lease(sem); } public static void SetLimit(int value) { - if (value == _currentLimit) - { - return; - } + if (value <= 0) + { + value = 1; + } - // TODO: Switch to a shared limiter implementation with GpuWorkLimiter so we can coordinate CPU/GPU prime work using - // the same semaphore pool without reallocating per adjustment. - _semaphore = new SemaphoreSlim(value, value); - _currentLimit = value; + if (value == _currentLimit) + { + return; + } + + // TODO: Switch to a shared limiter implementation with GpuWorkLimiter so we can coordinate CPU/GPU prime work using + // the same semaphore pool without reallocating per adjustment. + var previous = _semaphore; + _semaphore = new SemaphoreSlim(value, value); + _currentLimit = value; + previous.Dispose(); } - public sealed class Lease + public sealed class Lease : IDisposable { private readonly SemaphoreSlim _sem; @@ -38,8 +48,7 @@ public static class GpuPrimeWorkLimiter public void Dispose() { - // _sem.Release(); + _sem.Release(); } } } - diff --git a/PerfectNumbers.Core/PrimeTester.cs b/PerfectNumbers.Core/PrimeTester.cs index 100e9f6..75f2b0d 100644 --- a/PerfectNumbers.Core/PrimeTester.cs +++ b/PerfectNumbers.Core/PrimeTester.cs @@ -282,36 +282,26 @@ public sealed class PrimeTester } } - internal static PrimeTesterGpuContextLease Rent(int minBufferCapacity = 1) - { - GpuPrimeLeaseLimiter.Enter(); + internal static PrimeTesterGpuContextLease Rent(int minBufferCapacity = 1) + { + if (!Pool.TryDequeue(out var lease)) + { + lease = new PrimeTesterGpuContextLease(minBufferCapacity); + } + else + { + lease.EnsureCapacity(minBufferCapacity); + } - try - { - if (!Pool.TryDequeue(out var lease)) - { - lease = new PrimeTesterGpuContextLease(minBufferCapacity); - } - else - { - lease.EnsureCapacity(minBufferCapacity); - } + lease.ResetReturnFlag(); + return lease; + } - lease.ResetReturnFlag(); - return lease; - } - catch - { - GpuPrimeLeaseLimiter.Exit(); - throw; - } - } internal static void Return(PrimeTesterGpuContextLease lease) { lease.Accelerator.Synchronize(); Pool.Enqueue(lease); - GpuPrimeLeaseLimiter.Exit(); } internal static void DisposeAll()