diff --git a/EvenPerfectBitScanner/Program.cs b/EvenPerfectBitScanner/Program.cs index b55bce1..bcd7937 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; + GpuPrimeWorkLimiter.SetLimit(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; @@ -117,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 5e9f0a5..b6218c4 100644 --- a/PerfectNumbers.Core/Gpu/GpuContextPool.cs +++ b/PerfectNumbers.Core/Gpu/GpuContextPool.cs @@ -48,7 +48,7 @@ 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() { if (GpuPool.TryDequeue(out var gpu)) { @@ -59,6 +59,7 @@ public static class GpuContextPool return new GpuContextLease(new PooledContext()); } + public static void WarmUpPool(int threadCount) { if (threadCount <= WarmedGpuContextCount) @@ -98,21 +99,30 @@ public static class GpuContextPool GpuPool.Enqueue(ctx); } - 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/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 36187f9..75f2b0d 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,21 +258,46 @@ 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) + { + if (!Pool.TryDequeue(out var lease)) + { + lease = new PrimeTesterGpuContextLease(minBufferCapacity); + } + else + { + lease.EnsureCapacity(minBufferCapacity); + } + + lease.ResetReturnFlag(); + return lease; + } + + internal static void Return(PrimeTesterGpuContextLease lease) { lease.Accelerator.Synchronize(); @@ -304,6 +320,8 @@ public sealed class PrimeTester SharedTables.Clear(); } + + WarmedLeaseCount = 0; } internal static void Clear(Accelerator accelerator) @@ -313,23 +331,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; } }