From 3afd9399db6e22437ba08ed790e3d641aad5ad7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Tue, 21 Oct 2025 22:01:50 +0200 Subject: [PATCH] Isolate residue GPU leases and drop try/finally --- ...MersenneNumberDivisorByDivisorGpuTester.cs | 64 +++-- .../Gpu/MersenneNumberResidueGpuTester.cs | 247 ++++++++++++------ 2 files changed, 198 insertions(+), 113 deletions(-) diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs index 84f3644..cb57cfd 100644 --- a/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs +++ b/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs @@ -26,6 +26,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv private readonly ConcurrentDictionary, ArrayView, byte, byte>> _remainderScanKernelCache = new(AcceleratorReferenceComparer.Instance); private readonly ConcurrentDictionary, ArrayView, ArrayView, ArrayView, byte, ArrayView>> _candidateMaskKernelCache = new(AcceleratorReferenceComparer.Instance); private readonly ConcurrentDictionary> _resourcePools = new(AcceleratorReferenceComparer.Instance); + private readonly ConcurrentBag _acceleratorPool = new(); private readonly ConcurrentBag _sessionPool = new(); private Action, ArrayView, ArrayView> GetKernel(Accelerator accelerator) => @@ -95,7 +96,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv ulong processedCount; ulong lastProcessed; - var gpuLease = GpuContextPool.RentPreferred(preferCpu: false); + var gpuLease = RentAccelerator(); var accelerator = gpuLease.Accelerator; Monitor.Enter(gpuLease.ExecutionLock); @@ -104,34 +105,33 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv var resources = RentBatchResources(accelerator, batchCapacity); composite = CheckDivisors( - prime, - allowedMax, - accelerator, - kernel, - resources.DivisorsBuffer, - resources.ExponentBuffer, - resources.HitsBuffer, - resources.DivisorDeltaBuffer, - resources.RemainderDeltaBuffer, - resources.RemainderBuffer, - resources.RemainderDeltaKernel, - resources.RemainderScanKernel, - resources.CandidateMaskKernel, - resources.Divisors, - resources.Exponents, - resources.Hits, - resources.DivisorData, - resources.CycleCandidates, - resources.CycleLengths, - resources.CycleCapacity, - out lastProcessed, - out coveredRange, - out processedCount - ); + prime, + allowedMax, + accelerator, + kernel, + resources.DivisorsBuffer, + resources.ExponentBuffer, + resources.HitsBuffer, + resources.DivisorDeltaBuffer, + resources.RemainderDeltaBuffer, + resources.RemainderBuffer, + resources.RemainderDeltaKernel, + resources.RemainderScanKernel, + resources.CandidateMaskKernel, + resources.Divisors, + resources.Exponents, + resources.Hits, + resources.DivisorData, + resources.CycleCandidates, + resources.CycleLengths, + resources.CycleCapacity, + out lastProcessed, + out coveredRange, + out processedCount); ReturnBatchResources(accelerator, resources); Monitor.Exit(gpuLease.ExecutionLock); - gpuLease.Dispose(); + ReturnAccelerator(gpuLease); if (composite) { @@ -652,6 +652,18 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv } } + private GpuContextPool.GpuContextLease RentAccelerator() + { + if (_acceleratorPool.TryTake(out var lease)) + { + return lease; + } + + return GpuContextPool.RentPreferred(preferCpu: false); + } + + private void ReturnAccelerator(GpuContextPool.GpuContextLease lease) => _acceleratorPool.Add(lease); + private BatchResources RentBatchResources(Accelerator accelerator, int capacity) { var bag = _resourcePools.GetOrAdd(accelerator, static _ => []); diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs index 1358a8b..12f1375 100644 --- a/PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs +++ b/PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs @@ -1,5 +1,11 @@ +using System; using System.Buffers; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Runtime.InteropServices; +using System.Threading; +using System.Runtime.CompilerServices; +using ILGPU; using ILGPU.Runtime; namespace PerfectNumbers.Core.Gpu; @@ -7,6 +13,10 @@ namespace PerfectNumbers.Core.Gpu; public class MersenneNumberResidueGpuTester(bool useGpuOrder) { private readonly bool _useGpuOrder = useGpuOrder; + private readonly ConcurrentBag _acceleratorPool = new(); + private readonly ConcurrentDictionary, ArrayView1D, ArrayView1D, ArrayView1D, ArrayView1D, ArrayView1D>> _pow2ModKernelCache = new(AcceleratorReferenceComparer.Instance); + private readonly ConcurrentDictionary> _resourcePools = new(AcceleratorReferenceComparer.Instance); + // TODO: Integrate MersenneDivisorCycles.Shared to consult cycle lengths for small q (<= 4M) and fast-reject // candidates without launching heavy kernels. Consider a device-visible constant buffer per-accelerator // via GpuKernelPool to avoid host round-trips. @@ -16,106 +26,169 @@ public class MersenneNumberResidueGpuTester(bool useGpuOrder) // GPU residue variant: check 2^p % q == 1 for q = 2*p*k + 1. public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime) { - var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder); - var execution = gpuLease.EnterExecutionScope(); - try + var limiter = GpuPrimeWorkLimiter.Acquire(); + var gpuLease = RentAccelerator(); + var accelerator = gpuLease.Accelerator; + + Monitor.Enter(gpuLease.ExecutionLock); + + var stream = accelerator.CreateStream(); + var kernel = GetPow2ModKernel(accelerator); + var resources = RentResources(accelerator, GpuConstants.ScanBatchSize); + + var orderBuffer = resources.OrderBuffer; + ulong[] orderArray = resources.OrderArray; + int batchSize = resources.Capacity; + + // Ensure device has small cycles and primes tables for in-kernel lookup + var smallCyclesView = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator); + ResiduePrimeViews primeViews = GpuKernelPool.EnsureSmallPrimesOnDevice(accelerator); + + UInt128 kStart = 1UL; + UInt128 limit = maxK + UInt128.One; + byte last = lastIsSeven ? (byte)1 : (byte)0; + twoP.Mod10_8_5_3(out ulong step10, out ulong step8, out ulong step5, out ulong step3); + step10 = step10.Mod10(); + + GpuUInt128 twoPGpu = (GpuUInt128)twoP; + UInt128 batchSize128 = (UInt128)batchSize; + UInt128 fullBatchStep = twoP * batchSize128; + UInt128 q = twoP * kStart + UInt128.One; + + while (kStart < limit && Volatile.Read(ref isPrime)) { - var accelerator = gpuLease.Accelerator; - var stream = gpuLease.Stream; + UInt128 remaining = limit - kStart; + int currentSize = remaining > batchSize128 ? batchSize : (int)remaining; + Span orders = orderArray.AsSpan(0, currentSize); + ref ulong ordersRef = ref MemoryMarshal.GetReference(orders); - // Ensure device has small cycles and primes tables for in-kernel lookup - var smallCyclesView = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator); - ResiduePrimeViews primeViews = GpuKernelPool.EnsureSmallPrimesOnDevice(accelerator); + q.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3); + var kernelArgs = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5); - int batchSize = GpuConstants.ScanBatchSize; - UInt128 kStart = 1UL; - UInt128 limit = maxK + UInt128.One; - byte last = lastIsSeven ? (byte)1 : (byte)0; - var kernel = gpuLease.Pow2ModKernel; - twoP.Mod10_8_5_3(out ulong step10, out ulong step8, out ulong step5, out ulong step3); - step10 = step10.Mod10(); + kernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, 0UL, + kernelArgs, orderBuffer.View, smallCyclesView, primeViews.LastOne, primeViews.LastSeven, primeViews.LastOnePow2, primeViews.LastSevenPow2); - GpuUInt128 twoPGpu = (GpuUInt128)twoP; - - var orderBuffer = accelerator.Allocate1D(batchSize); - // Device memory returned by Allocate1D is not guaranteed to be zeroed. - // Ensure the buffer starts with a known state so that lanes skipped by the - // kernel (for example due to early rejections) do not leave stale garbage - // values that would be interpreted as composite witnesses when copied back - // to the host. This mirrors the explicit zeroing performed by other GPU - // testers such as the order kernels. - orderBuffer.MemSetToZero(); - ulong[] orderArray = ArrayPool.Shared.Rent(batchSize); - - try + stream.Synchronize(); + orderBuffer.View.CopyToCPU(ref ordersRef, currentSize); + if (!Volatile.Read(ref isPrime)) { - UInt128 batchSize128 = (UInt128)batchSize; - UInt128 fullBatchStep = twoP * batchSize128; - UInt128 q = twoP * kStart + UInt128.One; - // TODO: Replace the direct UInt128 multiply/add updates below with the residue-cycle stepping helper so that q advances - // reuse cached cycle increments rather than recomputing twoP multiples on every iteration. + break; + } - while (kStart < limit && Volatile.Read(ref isPrime)) + bool compositeFound = false; + for (int i = 0; i < currentSize; i++) + { + if (orders[i] != 0UL) { - UInt128 remaining = limit - kStart; - int currentSize = remaining > batchSize128 ? batchSize : (int)remaining; - Span orders = orderArray.AsSpan(0, currentSize); - ref ulong ordersRef = ref MemoryMarshal.GetReference(orders); - - q.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3); - var kernelArgs = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5); - - kernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, 0UL, - kernelArgs, orderBuffer.View, smallCyclesView, primeViews.LastOne, primeViews.LastSeven, primeViews.LastOnePow2, primeViews.LastSevenPow2); - - accelerator.Synchronize(); - orderBuffer.View.CopyToCPU(ref ordersRef, currentSize); - if (!Volatile.Read(ref isPrime)) - { - break; - } - - bool compositeFound = false; - for (int i = 0; i < currentSize; i++) - { - if (orders[i] != 0UL) - { - Volatile.Write(ref isPrime, false); - compositeFound = true; - break; - } - } - - if (compositeFound) - { - break; - } - - UInt128 processed = (UInt128)currentSize; - kStart += processed; - if (kStart < limit) - { - if (processed == batchSize128) - { - q += fullBatchStep; - } - else - { - q += twoP * processed; // TODO: Swap this multiplication for the shared cycle stepping helper once residue cycles are mandatory so we can reuse the cached remainder ladder instead of performing UInt128 multiply operations. - } - } + Volatile.Write(ref isPrime, false); + compositeFound = true; + break; } } - finally + + if (compositeFound) { - ArrayPool.Shared.Return(orderArray, clearArray: false); - orderBuffer.Dispose(); + break; + } + + UInt128 processed = (UInt128)currentSize; + kStart += processed; + if (kStart < limit) + { + if (processed == batchSize128) + { + q += fullBatchStep; + } + else + { + q += twoP * processed; // TODO: Swap this multiplication for the shared cycle stepping helper once residue cycles are mandatory so we can reuse the cached remainder ladder instead of performing UInt128 multiply operations. + } } } - finally + + ReturnResources(accelerator, resources); + stream.Dispose(); + Monitor.Exit(gpuLease.ExecutionLock); + ReturnAccelerator(gpuLease); + limiter.Dispose(); + } + + private Action, ArrayView1D, ArrayView1D, ArrayView1D, ArrayView1D, ArrayView1D> GetPow2ModKernel(Accelerator accelerator) => + _pow2ModKernelCache.GetOrAdd( + accelerator, + static acc => + { + var loaded = acc.LoadAutoGroupedStreamKernel, ArrayView1D, ArrayView1D, ArrayView1D, ArrayView1D, ArrayView1D>(Pow2ModKernels.Pow2ModKernelScan); + return (Action, ArrayView1D, ArrayView1D, ArrayView1D, ArrayView1D, ArrayView1D>)Delegate.CreateDelegate( + typeof(Action, ArrayView1D, ArrayView1D, ArrayView1D, ArrayView1D, ArrayView1D>), + loaded.Target, + loaded.Method); + }); + + private GpuContextPool.GpuContextLease RentAccelerator() + { + if (_acceleratorPool.TryTake(out var lease)) { - execution.Dispose(); - gpuLease.Dispose(); + return lease; + } + + return GpuContextPool.RentPreferred(preferCpu: !_useGpuOrder); + } + + private void ReturnAccelerator(GpuContextPool.GpuContextLease lease) => _acceleratorPool.Add(lease); + + private ResidueResources RentResources(Accelerator accelerator, int capacity) + { + var bag = _resourcePools.GetOrAdd(accelerator, static _ => new ConcurrentBag()); + while (bag.TryTake(out var resources)) + { + if (resources.Capacity >= capacity) + { + resources.OrderBuffer.MemSetToZero(); + return resources; + } + + resources.Dispose(); + } + + var created = new ResidueResources(accelerator, capacity); + created.OrderBuffer.MemSetToZero(); + return created; + } + + private void ReturnResources(Accelerator accelerator, ResidueResources resources) => + _resourcePools.GetOrAdd(accelerator, static _ => new ConcurrentBag()).Add(resources); + + private sealed class ResidueResources : IDisposable + { + internal ResidueResources(Accelerator accelerator, int capacity) + { + Capacity = Math.Max(1, capacity); + OrderBuffer = accelerator.Allocate1D(Capacity); + OrderArray = ArrayPool.Shared.Rent(Capacity); + } + + internal MemoryBuffer1D OrderBuffer { get; } + + internal ulong[] OrderArray { get; } + + internal int Capacity { get; } + + public void Dispose() + { + OrderBuffer.Dispose(); + ArrayPool.Shared.Return(OrderArray, clearArray: false); } } + + private sealed class AcceleratorReferenceComparer : IEqualityComparer + { + public static AcceleratorReferenceComparer Instance { get; } = new(); + + public bool Equals(Accelerator? x, Accelerator? y) => ReferenceEquals(x, y); + + public int GetHashCode(Accelerator obj) => RuntimeHelpers.GetHashCode(obj); + } + + }