From b0158a844aed4195449e387422972c671a014f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Wed, 29 Oct 2025 15:13:54 +0100 Subject: [PATCH 01/17] Use divisor squares to bound heuristic combined trial division --- .../Gpu/Kernels/PrimeTesterKernels.cs | 8 +- .../HeuristicCombinedPrimeTester.cs | 281 ++++++++++++------ PerfectNumbers.Core/HeuristicPrimeTester.cs | 210 ++++++------- PerfectNumbers.Core/PrimeTester.cs | 87 +++++- 4 files changed, 351 insertions(+), 235 deletions(-) diff --git a/PerfectNumbers.Core/Gpu/Kernels/PrimeTesterKernels.cs b/PerfectNumbers.Core/Gpu/Kernels/PrimeTesterKernels.cs index f60596a..c86f960 100644 --- a/PerfectNumbers.Core/Gpu/Kernels/PrimeTesterKernels.cs +++ b/PerfectNumbers.Core/Gpu/Kernels/PrimeTesterKernels.cs @@ -68,16 +68,18 @@ internal static class PrimeTesterKernels results[index] = result; } - public static void HeuristicTrialDivisionKernel(Index1D index, ArrayView divisors, ulong n, ArrayView results) + public static void HeuristicTrialDivisionKernel(Index1D index, ArrayView divisors, ulong n, ArrayView resultFlag) { ulong divisor = divisors[index]; if (divisor <= 1UL) { - results[index] = 0; return; } - results[index] = n % divisor == 0UL ? (byte)1 : (byte)0; + if (n % divisor == 0UL) + { + resultFlag[0] = 1; + } } public static void SharesFactorKernel(Index1D index, ArrayView numbers, ArrayView results) diff --git a/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs b/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs index 6175a28..c1d72bd 100644 --- a/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs @@ -17,29 +17,33 @@ public sealed class HeuristicCombinedPrimeTester { HeuristicPrimeSieves.EnsureInitialized(); - CombinedDivisorsEnding1TwoAOneB = BuildCombinedDivisors(1, CombinedDivisorPattern.TwoAOneB); - CombinedDivisorsEnding3TwoAOneB = BuildCombinedDivisors(3, CombinedDivisorPattern.TwoAOneB); - CombinedDivisorsEnding7TwoAOneB = BuildCombinedDivisors(7, CombinedDivisorPattern.TwoAOneB); - CombinedDivisorsEnding9TwoAOneB = BuildCombinedDivisors(9, CombinedDivisorPattern.TwoAOneB); + CombinedDivisorsEnding1TwoAOneB = BuildCombinedDivisors(1, CombinedDivisorPattern.TwoAOneB, out CombinedDivisorsEnding1TwoAOneBSquares); + CombinedDivisorsEnding3TwoAOneB = BuildCombinedDivisors(3, CombinedDivisorPattern.TwoAOneB, out CombinedDivisorsEnding3TwoAOneBSquares); + CombinedDivisorsEnding7TwoAOneB = BuildCombinedDivisors(7, CombinedDivisorPattern.TwoAOneB, out CombinedDivisorsEnding7TwoAOneBSquares); + CombinedDivisorsEnding9TwoAOneB = BuildCombinedDivisors(9, CombinedDivisorPattern.TwoAOneB, out CombinedDivisorsEnding9TwoAOneBSquares); - CombinedDivisorsEnding1OneAOneB = BuildCombinedDivisors(1, CombinedDivisorPattern.OneAOneB); - CombinedDivisorsEnding3OneAOneB = BuildCombinedDivisors(3, CombinedDivisorPattern.OneAOneB); - CombinedDivisorsEnding7OneAOneB = BuildCombinedDivisors(7, CombinedDivisorPattern.OneAOneB); - CombinedDivisorsEnding9OneAOneB = BuildCombinedDivisors(9, CombinedDivisorPattern.OneAOneB); + CombinedDivisorsEnding1OneAOneB = BuildCombinedDivisors(1, CombinedDivisorPattern.OneAOneB, out CombinedDivisorsEnding1OneAOneBSquares); + CombinedDivisorsEnding3OneAOneB = BuildCombinedDivisors(3, CombinedDivisorPattern.OneAOneB, out CombinedDivisorsEnding3OneAOneBSquares); + CombinedDivisorsEnding7OneAOneB = BuildCombinedDivisors(7, CombinedDivisorPattern.OneAOneB, out CombinedDivisorsEnding7OneAOneBSquares); + CombinedDivisorsEnding9OneAOneB = BuildCombinedDivisors(9, CombinedDivisorPattern.OneAOneB, out CombinedDivisorsEnding9OneAOneBSquares); - CombinedDivisorsEnding1ThreeAOneB = BuildCombinedDivisors(1, CombinedDivisorPattern.ThreeAOneB); - CombinedDivisorsEnding3ThreeAOneB = BuildCombinedDivisors(3, CombinedDivisorPattern.ThreeAOneB); - CombinedDivisorsEnding7ThreeAOneB = BuildCombinedDivisors(7, CombinedDivisorPattern.ThreeAOneB); - CombinedDivisorsEnding9ThreeAOneB = BuildCombinedDivisors(9, CombinedDivisorPattern.ThreeAOneB); + CombinedDivisorsEnding1ThreeAOneB = BuildCombinedDivisors(1, CombinedDivisorPattern.ThreeAOneB, out CombinedDivisorsEnding1ThreeAOneBSquares); + CombinedDivisorsEnding3ThreeAOneB = BuildCombinedDivisors(3, CombinedDivisorPattern.ThreeAOneB, out CombinedDivisorsEnding3ThreeAOneBSquares); + CombinedDivisorsEnding7ThreeAOneB = BuildCombinedDivisors(7, CombinedDivisorPattern.ThreeAOneB, out CombinedDivisorsEnding7ThreeAOneBSquares); + CombinedDivisorsEnding9ThreeAOneB = BuildCombinedDivisors(9, CombinedDivisorPattern.ThreeAOneB, out CombinedDivisorsEnding9ThreeAOneBSquares); - CombinedDivisorsEnding1ThreeATwoB = BuildCombinedDivisors(1, CombinedDivisorPattern.ThreeATwoB); - CombinedDivisorsEnding3ThreeATwoB = BuildCombinedDivisors(3, CombinedDivisorPattern.ThreeATwoB); - CombinedDivisorsEnding7ThreeATwoB = BuildCombinedDivisors(7, CombinedDivisorPattern.ThreeATwoB); - CombinedDivisorsEnding9ThreeATwoB = BuildCombinedDivisors(9, CombinedDivisorPattern.ThreeATwoB); + CombinedDivisorsEnding1ThreeATwoB = BuildCombinedDivisors(1, CombinedDivisorPattern.ThreeATwoB, out CombinedDivisorsEnding1ThreeATwoBSquares); + CombinedDivisorsEnding3ThreeATwoB = BuildCombinedDivisors(3, CombinedDivisorPattern.ThreeATwoB, out CombinedDivisorsEnding3ThreeATwoBSquares); + CombinedDivisorsEnding7ThreeATwoB = BuildCombinedDivisors(7, CombinedDivisorPattern.ThreeATwoB, out CombinedDivisorsEnding7ThreeATwoBSquares); + CombinedDivisorsEnding9ThreeATwoB = BuildCombinedDivisors(9, CombinedDivisorPattern.ThreeATwoB, out CombinedDivisorsEnding9ThreeATwoBSquares); CombinedDivisorsEnding1 = CombinedDivisorsEnding1OneAOneB; CombinedDivisorsEnding3 = CombinedDivisorsEnding3OneAOneB; CombinedDivisorsEnding7 = CombinedDivisorsEnding7OneAOneB; CombinedDivisorsEnding9 = CombinedDivisorsEnding9OneAOneB; + CombinedDivisorsEnding1Squares = CombinedDivisorsEnding1OneAOneBSquares; + CombinedDivisorsEnding3Squares = CombinedDivisorsEnding3OneAOneBSquares; + CombinedDivisorsEnding7Squares = CombinedDivisorsEnding7OneAOneBSquares; + CombinedDivisorsEnding9Squares = CombinedDivisorsEnding9OneAOneBSquares; } private const ulong Wheel210 = 210UL; @@ -71,6 +75,31 @@ public sealed class HeuristicCombinedPrimeTester private static readonly uint[] CombinedDivisorsEnding7; private static readonly uint[] CombinedDivisorsEnding9; + private static readonly ulong[] CombinedDivisorsEnding1TwoAOneBSquares; + private static readonly ulong[] CombinedDivisorsEnding3TwoAOneBSquares; + private static readonly ulong[] CombinedDivisorsEnding7TwoAOneBSquares; + private static readonly ulong[] CombinedDivisorsEnding9TwoAOneBSquares; + + private static readonly ulong[] CombinedDivisorsEnding1OneAOneBSquares; + private static readonly ulong[] CombinedDivisorsEnding3OneAOneBSquares; + private static readonly ulong[] CombinedDivisorsEnding7OneAOneBSquares; + private static readonly ulong[] CombinedDivisorsEnding9OneAOneBSquares; + + private static readonly ulong[] CombinedDivisorsEnding1ThreeAOneBSquares; + private static readonly ulong[] CombinedDivisorsEnding3ThreeAOneBSquares; + private static readonly ulong[] CombinedDivisorsEnding7ThreeAOneBSquares; + private static readonly ulong[] CombinedDivisorsEnding9ThreeAOneBSquares; + + private static readonly ulong[] CombinedDivisorsEnding1ThreeATwoBSquares; + private static readonly ulong[] CombinedDivisorsEnding3ThreeATwoBSquares; + private static readonly ulong[] CombinedDivisorsEnding7ThreeATwoBSquares; + private static readonly ulong[] CombinedDivisorsEnding9ThreeATwoBSquares; + + private static readonly ulong[] CombinedDivisorsEnding1Squares; + private static readonly ulong[] CombinedDivisorsEnding3Squares; + private static readonly ulong[] CombinedDivisorsEnding7Squares; + private static readonly ulong[] CombinedDivisorsEnding9Squares; + private static readonly ulong[] HeuristicSmallCycleSnapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot(); internal enum CombinedDivisorPattern : byte @@ -155,21 +184,28 @@ public sealed class HeuristicCombinedPrimeTester private static bool HeuristicTrialDivisionCpu(ulong n, ulong sqrtLimit, byte nMod10) { ReadOnlySpan combinedDivisors = GetCombinedDivisors(nMod10); - if (combinedDivisors.IsEmpty) + ReadOnlySpan combinedDivisorSquares = GetCombinedDivisorSquares(nMod10); + if (combinedDivisors.IsEmpty || combinedDivisorSquares.IsEmpty) { return EvaluateWithOpenNumericFallback(n); } int length = combinedDivisors.Length; + int squaresLength = combinedDivisorSquares.Length; + if (squaresLength < length) + { + length = squaresLength; + } + for (int i = 0; i < length; i++) { - uint entry = combinedDivisors[i]; - if (entry > sqrtLimit) + ulong divisorSquare = combinedDivisorSquares[i]; + if (divisorSquare > n) { break; } - if (n % entry == 0UL) + if (n % combinedDivisors[i] == 0UL) { return false; } @@ -180,98 +216,88 @@ public sealed class HeuristicCombinedPrimeTester private static bool HeuristicTrialDivisionGpuDetectsDivisor(ulong n, ulong sqrtLimit, byte nMod10) { - int batchCapacity = HeuristicGpuDivisorBatchSize; - var divisorPool = ThreadStaticPools.UlongPool; - var hitPool = ThreadStaticPools.BytePool; + int batchCapacity = HeuristicGpuDivisorBatchSize; + var divisorPool = ThreadStaticPools.UlongPool; - ulong[] divisorArray = divisorPool.Rent(batchCapacity); - byte[] hitFlags = hitPool.Rent(batchCapacity); + ulong[] divisorArray = divisorPool.Rent(batchCapacity); - var limiter = GpuPrimeWorkLimiter.Acquire(); - var gpu = PrimeTester.PrimeTesterGpuContextPool.Rent(); - var accelerator = gpu.Accelerator; - var state = PrimeTester.GpuKernelState.GetOrCreate(accelerator); - var scratch = state.RentScratch(batchCapacity, accelerator); + var limiter = GpuPrimeWorkLimiter.Acquire(); + var gpu = PrimeTester.PrimeTesterGpuContextPool.Rent(batchCapacity, 1); + var accelerator = gpu.Accelerator; + var state = gpu.State; + var scratch = gpu.Scratch; - bool compositeDetected = false; - int count = 0; + bool compositeDetected = false; + int count = 0; - var input = scratch.Input; - var output = scratch.Output; + var inputView = scratch.Input.View; + var outputView = scratch.Output.View; - bool ProcessBatch(int length) - { - input.View.CopyFromCPU(ref divisorArray[0], length); - state.HeuristicTrialDivisionKernel(length, input.View, n, output.View); - accelerator.Synchronize(); - output.View.CopyToCPU(ref hitFlags[0], length); + bool ProcessBatch(int length) + { + inputView.CopyFromCPU(ref divisorArray[0], length); + byte compositeFlag = 0; + outputView.CopyFromCPU(ref compositeFlag, 1); + state.HeuristicTrialDivisionKernel(length, inputView, n, outputView); + accelerator.Synchronize(); + outputView.CopyToCPU(ref compositeFlag, 1); - for (int i = 0; i < length; i++) - { - if (hitFlags[i] == 0) - { - continue; - } + return compositeFlag != 0; + } - return true; - // ulong divisor = divisorArray[i]; - // if (divisor > 1UL && n % divisor == 0UL) - // { - // return true; - // } - } + ReadOnlySpan combinedDivisors = GetCombinedDivisors(nMod10); + ReadOnlySpan combinedDivisorSquares = GetCombinedDivisorSquares(nMod10); - return false; - } + if (combinedDivisors.IsEmpty || combinedDivisorSquares.IsEmpty) + { + goto Cleanup; + } - ReadOnlySpan combinedDivisors = GetCombinedDivisors(nMod10); - if (combinedDivisors.IsEmpty) - { - goto Cleanup; - } + int length = combinedDivisors.Length; + int squaresLength = combinedDivisorSquares.Length; + if (squaresLength < length) + { + length = squaresLength; + } - int length = combinedDivisors.Length; - for (int i = 0; i < length; i++) - { - uint entry = combinedDivisors[i]; - if (entry > sqrtLimit) - { - break; - } + for (int i = 0; i < length; i++) + { + ulong divisorSquare = combinedDivisorSquares[i]; + if (divisorSquare > n) + { + break; + } - divisorArray[count] = entry; - count++; + divisorArray[count] = combinedDivisors[i]; + count++; - if (count == batchCapacity) - { - if (ProcessBatch(count)) - { - compositeDetected = true; - goto Cleanup; - } + if (count == batchCapacity) + { + if (ProcessBatch(count)) + { + compositeDetected = true; + goto Cleanup; + } - count = 0; - } - } + count = 0; + } + } - if (!compositeDetected && count > 0) - { - if (ProcessBatch(count)) - { - compositeDetected = true; - goto Cleanup; - } - } + if (count > 0 && ProcessBatch(count)) + { + compositeDetected = true; + goto Cleanup; + } - Cleanup: - state.ReturnScratch(scratch); - divisorPool.Return(divisorArray); - hitPool.Return(hitFlags); - gpu.Dispose(); - limiter.Dispose(); - return compositeDetected; + +Cleanup: + divisorPool.Return(divisorArray); + gpu.Dispose(); + limiter.Dispose(); + return compositeDetected; } + private static bool EvaluateWithOpenNumericFallback(ulong n) { return Prime.Numbers.IsPrime(n); @@ -312,6 +338,18 @@ public sealed class HeuristicCombinedPrimeTester }; } + private static ReadOnlySpan GetCombinedDivisorSquares(byte nMod10) + { + return nMod10 switch + { + 1 => CombinedDivisorsEnding1Squares, + 3 => CombinedDivisorsEnding3Squares, + 7 => CombinedDivisorsEnding7Squares, + 9 => CombinedDivisorsEnding9Squares, + _ => ReadOnlySpan.Empty, + }; + } + internal static ReadOnlySpan GetCombinedDivisors(byte nMod10, CombinedDivisorPattern pattern) { return pattern switch @@ -352,7 +390,47 @@ public sealed class HeuristicCombinedPrimeTester }; } - private static uint[] BuildCombinedDivisors(byte nMod10, CombinedDivisorPattern pattern) + internal static ReadOnlySpan GetCombinedDivisorSquares(byte nMod10, CombinedDivisorPattern pattern) + { + return pattern switch + { + CombinedDivisorPattern.TwoAOneB => nMod10 switch + { + 1 => CombinedDivisorsEnding1TwoAOneBSquares, + 3 => CombinedDivisorsEnding3TwoAOneBSquares, + 7 => CombinedDivisorsEnding7TwoAOneBSquares, + 9 => CombinedDivisorsEnding9TwoAOneBSquares, + _ => ReadOnlySpan.Empty, + }, + CombinedDivisorPattern.OneAOneB => nMod10 switch + { + 1 => CombinedDivisorsEnding1OneAOneBSquares, + 3 => CombinedDivisorsEnding3OneAOneBSquares, + 7 => CombinedDivisorsEnding7OneAOneBSquares, + 9 => CombinedDivisorsEnding9OneAOneBSquares, + _ => ReadOnlySpan.Empty, + }, + CombinedDivisorPattern.ThreeAOneB => nMod10 switch + { + 1 => CombinedDivisorsEnding1ThreeAOneBSquares, + 3 => CombinedDivisorsEnding3ThreeAOneBSquares, + 7 => CombinedDivisorsEnding7ThreeAOneBSquares, + 9 => CombinedDivisorsEnding9ThreeAOneBSquares, + _ => ReadOnlySpan.Empty, + }, + CombinedDivisorPattern.ThreeATwoB => nMod10 switch + { + 1 => CombinedDivisorsEnding1ThreeATwoBSquares, + 3 => CombinedDivisorsEnding3ThreeATwoBSquares, + 7 => CombinedDivisorsEnding7ThreeATwoBSquares, + 9 => CombinedDivisorsEnding9ThreeATwoBSquares, + _ => ReadOnlySpan.Empty, + }, + _ => ReadOnlySpan.Empty, + }; + } + + private static uint[] BuildCombinedDivisors(byte nMod10, CombinedDivisorPattern pattern, out ulong[] squares) { ReadOnlySpan groupA = HeuristicPrimeSieves.GroupADivisors; ReadOnlySpan groupB = GetGroupBDivisors(nMod10); @@ -412,7 +490,16 @@ public sealed class HeuristicCombinedPrimeTester } } - return combined.ToArray(); + uint[] result = combined.ToArray(); + ulong[] resultSquares = new ulong[result.Length]; + for (int i = 0; i < result.Length; i++) + { + ulong divisor = result[i]; + resultSquares[i] = divisor * divisor; + } + + squares = resultSquares; + return result; } diff --git a/PerfectNumbers.Core/HeuristicPrimeTester.cs b/PerfectNumbers.Core/HeuristicPrimeTester.cs index ce7e227..0d9311d 100644 --- a/PerfectNumbers.Core/HeuristicPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicPrimeTester.cs @@ -263,167 +263,139 @@ public sealed class HeuristicPrimeTester int batchCapacity = HeuristicGpuDivisorBatchSize; int interleaveBatchSize = HeuristicDivisorInterleaveBatchSize; var divisorPool = ThreadStaticPools.UlongPool; - var hitPool = ThreadStaticPools.BytePool; ulong[] divisorArray = divisorPool.Rent(batchCapacity); - byte[] hitFlags = hitPool.Rent(batchCapacity); var limiter = GpuPrimeWorkLimiter.Acquire(); - var gpu = PrimeTester.PrimeTesterGpuContextPool.Rent(); + var gpu = PrimeTester.PrimeTesterGpuContextPool.Rent(batchCapacity, 1); var accelerator = gpu.Accelerator; - var state = PrimeTester.GpuKernelState.GetOrCreate(accelerator); - var scratch = state.RentScratch(batchCapacity, accelerator); + var state = gpu.State; + var scratch = gpu.Scratch; bool compositeDetected = false; int count = 0; - // lock (gpu.ExecutionLock) + var inputView = scratch.Input.View; + var outputView = scratch.Output.View; + + bool ProcessBatch(int length) { - var input = scratch.Input; - var output = scratch.Output; + inputView.CopyFromCPU(ref divisorArray[0], length); + byte compositeFlag = 0; + outputView.CopyFromCPU(ref compositeFlag, 1); + state.HeuristicTrialDivisionKernel(length, inputView, n, outputView); + accelerator.Synchronize(); + outputView.CopyToCPU(ref compositeFlag, 1); - bool ProcessBatch(int length) + return compositeFlag != 0; + } + + ReadOnlySpan groupADivisors = HeuristicPrimeSieves.GroupADivisors; + int groupAIndex = 0; + ReadOnlySpan endingOrder = GetGroupBEndingOrder(nMod10); + Span indices = endingOrder.Length <= 8 + ? stackalloc int[endingOrder.Length] + : new int[endingOrder.Length]; + + bool groupAHasMore = groupAIndex < groupADivisors.Length && (ulong)groupADivisors[groupAIndex] <= sqrtLimit; + bool groupBHasMore = !endingOrder.IsEmpty && HasGroupBCandidates(endingOrder, indices, sqrtLimit); + + while (!compositeDetected && (groupAHasMore || groupBHasMore)) + { + if (groupAHasMore) { - if (length <= 0) + int processed = 0; + while (processed < interleaveBatchSize && groupAIndex < groupADivisors.Length) { - return false; - } - - input.View.CopyFromCPU(ref divisorArray[0], length); - state.HeuristicTrialDivisionKernel(length, input.View, n, output.View); - accelerator.Synchronize(); - output.View.CopyToCPU(ref hitFlags[0], length); - - for (int i = 0; i < length; i++) - { - if (hitFlags[i] == 0) - { - continue; - } - - ulong divisor = divisorArray[i]; - if (divisor > 1UL && n % divisor == 0UL) - { - return true; - } - } - - return false; - } - - ReadOnlySpan groupADivisors = HeuristicPrimeSieves.GroupADivisors; - int groupAIndex = 0; - ReadOnlySpan endingOrder = GetGroupBEndingOrder(nMod10); - Span indices = endingOrder.Length <= 8 - ? stackalloc int[endingOrder.Length] - : new int[endingOrder.Length]; - - bool groupAHasMore = groupAIndex < groupADivisors.Length && (ulong)groupADivisors[groupAIndex] <= sqrtLimit; - bool groupBHasMore = !endingOrder.IsEmpty && HasGroupBCandidates(endingOrder, indices, sqrtLimit); - - while (!compositeDetected && (groupAHasMore || groupBHasMore)) - { - if (groupAHasMore) - { - int processed = 0; - while (processed < interleaveBatchSize && groupAIndex < groupADivisors.Length) - { - ulong divisor = (ulong)groupADivisors[groupAIndex]; - if (divisor > sqrtLimit) - { - groupAIndex = groupADivisors.Length; - groupAHasMore = false; - break; - } - - divisorArray[count] = divisor; - count++; - - if (count == batchCapacity) - { - if (ProcessBatch(count)) - { - compositeDetected = true; - goto Cleanup; - } - - count = 0; - } - - groupAIndex++; - processed++; - } - - if (groupAIndex >= groupADivisors.Length) + ulong divisor = (ulong)groupADivisors[groupAIndex]; + if (divisor > sqrtLimit) { + groupAIndex = groupADivisors.Length; groupAHasMore = false; + break; } - else + + divisorArray[count] = divisor; + count++; + + if (count == batchCapacity) { - groupAHasMore = (ulong)groupADivisors[groupAIndex] <= sqrtLimit; - if (!groupAHasMore) + if (ProcessBatch(count)) { - groupAIndex = groupADivisors.Length; + compositeDetected = true; + goto Cleanup; } + + count = 0; } + + groupAIndex++; + processed++; } - if (!compositeDetected && groupBHasMore) + if (groupAIndex >= groupADivisors.Length) { - int processed = 0; - while (processed < interleaveBatchSize) + groupAHasMore = false; + } + else + { + groupAHasMore = (ulong)groupADivisors[groupAIndex] <= sqrtLimit; + if (!groupAHasMore) { - if (!TrySelectNextGroupBDivisor(endingOrder, indices, sqrtLimit, out ulong divisor)) - { - groupBHasMore = false; - break; - } - - divisorArray[count] = divisor; - count++; - - if (count == batchCapacity) - { - if (ProcessBatch(count)) - { - compositeDetected = true; - goto Cleanup; - } - - count = 0; - } - - processed++; - } - - if (groupBHasMore) - { - groupBHasMore = HasGroupBCandidates(endingOrder, indices, sqrtLimit); + groupAIndex = groupADivisors.Length; } } } - if (!compositeDetected && count > 0) + if (!compositeDetected && groupBHasMore) { - if (ProcessBatch(count)) + int processed = 0; + while (processed < interleaveBatchSize) { - compositeDetected = true; - goto Cleanup; + if (!TrySelectNextGroupBDivisor(endingOrder, indices, sqrtLimit, out ulong divisor)) + { + groupBHasMore = false; + break; + } + + divisorArray[count] = divisor; + count++; + + if (count == batchCapacity) + { + if (ProcessBatch(count)) + { + compositeDetected = true; + goto Cleanup; + } + + count = 0; + } + + processed++; } - count = 0; + if (groupBHasMore) + { + groupBHasMore = HasGroupBCandidates(endingOrder, indices, sqrtLimit); + } } } + if (count > 0 && ProcessBatch(count)) + { + compositeDetected = true; + goto Cleanup; + } + Cleanup: - state.ReturnScratch(scratch); divisorPool.Return(divisorArray); - hitPool.Return(hitFlags); gpu.Dispose(); limiter.Dispose(); return compositeDetected; } + private static bool EvaluateWithOpenNumericFallback(ulong n) { return Prime.Numbers.IsPrime(n); diff --git a/PerfectNumbers.Core/PrimeTester.cs b/PerfectNumbers.Core/PrimeTester.cs index 7c4e3af..2e12a00 100644 --- a/PerfectNumbers.Core/PrimeTester.cs +++ b/PerfectNumbers.Core/PrimeTester.cs @@ -128,13 +128,13 @@ public sealed class PrimeTester public static void IsPrimeBatchGpu(ReadOnlySpan values, Span results) { var limiter = GpuPrimeWorkLimiter.Acquire(); - var gpu = PrimeTesterGpuContextPool.Rent(); + var gpu = PrimeTesterGpuContextPool.Rent(GpuBatchSize); + var scratch = gpu.Scratch; var state = gpu.State; var accelerator = gpu.Accelerator; int totalLength = values.Length; - int batchSize = Math.Max(1, GpuBatchSize); + int batchSize = GpuBatchSize; - var scratch = state.RentScratch(batchSize, accelerator); var input = scratch.Input; var output = scratch.Output; ArrayPool pool = ThreadStaticPools.UlongPool; @@ -170,7 +170,6 @@ public sealed class PrimeTester } pool.Return(temp, clearArray: false); - state.ReturnScratch(scratch); gpu.Dispose(); limiter.Dispose(); @@ -185,6 +184,7 @@ public sealed class PrimeTester public Accelerator Accelerator { get; } private KernelState? _kernelState; + private KernelState.ScratchBuffers? _scratch; public PooledContext() { @@ -208,8 +208,51 @@ public sealed class PrimeTester } } + public KernelState.ScratchBuffers GetScratch(int minCapacity, int outputClearLength) + { + if (minCapacity <= 0) + { + minCapacity = 1; + } + + var scratch = _scratch; + var state = KernelState; + if (scratch is null) + { + scratch = state.RentScratch(minCapacity, Accelerator); + _scratch = scratch; + } + else if (scratch.Capacity < minCapacity) + { + scratch.Dispose(); + scratch = state.RentScratch(minCapacity, Accelerator); + _scratch = scratch; + } + + int clearLength = outputClearLength <= 0 ? 0 : outputClearLength; + if (clearLength > 0) + { + int outputLength = (int)scratch.Output.Length; + if (clearLength > outputLength) + { + clearLength = outputLength; + } + + scratch.Output.View.SubView(0, clearLength).MemSetToZero(); + Accelerator.Synchronize(); + } + + return scratch; + } + public void Dispose() { + if (_scratch is not null) + { + _scratch.Dispose(); + _scratch = null; + } + ClearGpuCaches(Accelerator); _kernelState = null; Accelerator.Dispose(); @@ -219,14 +262,14 @@ public sealed class PrimeTester private static readonly ConcurrentQueue Pool = new(); - internal static PrimeTesterGpuContextLease Rent() + internal static PrimeTesterGpuContextLease Rent(int minScratchCapacity = 0, int outputClearLength = 0) { if (Pool.TryDequeue(out var ctx)) { - return new PrimeTesterGpuContextLease(ctx); + return new PrimeTesterGpuContextLease(ctx, minScratchCapacity, outputClearLength); } - return new PrimeTesterGpuContextLease(new PooledContext()); + return new PrimeTesterGpuContextLease(new PooledContext(), minScratchCapacity, outputClearLength); } internal static void DisposeAll() @@ -243,18 +286,30 @@ public sealed class PrimeTester Pool.Enqueue(ctx); } - internal readonly struct PrimeTesterGpuContextLease(PrimeTesterGpuContextPool.PooledContext ctx) + internal readonly struct PrimeTesterGpuContextLease { - private readonly PooledContext? _ctx = ctx; + private readonly PooledContext? _ctx; + private readonly KernelState? _state; + private readonly KernelState.ScratchBuffers? _scratch; - public readonly Accelerator Accelerator => _ctx!.Accelerator; - - - public readonly KernelState State => _ctx!.KernelState; - - public readonly void Dispose() + internal PrimeTesterGpuContextLease(PooledContext ctx, int minScratchCapacity, int outputClearLength) { - Return(_ctx ?? throw new InvalidOperationException("GPU context lease is not initialized.")); + _ctx = ctx; + _state = ctx.KernelState; + _scratch = minScratchCapacity > 0 ? ctx.GetScratch(minScratchCapacity, outputClearLength) : null; + } + + public Accelerator Accelerator => (_ctx ?? throw new InvalidOperationException("GPU context lease is not initialized.")).Accelerator; + + public KernelState State => _state ?? throw new InvalidOperationException("GPU kernel state is not initialized."); + + public KernelState.ScratchBuffers Scratch => _scratch ?? throw new InvalidOperationException("Scratch buffers were not rented for this lease."); + + public void Dispose() + { + var ctx = _ctx ?? throw new InvalidOperationException("GPU context lease is not initialized."); + + Return(ctx); } } } From b753654ebb8fbbe6ebf26ee475590715c712042b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Wed, 29 Oct 2025 15:32:40 +0100 Subject: [PATCH 02/17] Enforce valid divisor selectors in heuristic combined tester --- .../HeuristicCombinedPrimeTester.cs | 43 ++++++------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs b/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs index c1d72bd..689fb47 100644 --- a/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs @@ -185,17 +185,8 @@ public sealed class HeuristicCombinedPrimeTester { ReadOnlySpan combinedDivisors = GetCombinedDivisors(nMod10); ReadOnlySpan combinedDivisorSquares = GetCombinedDivisorSquares(nMod10); - if (combinedDivisors.IsEmpty || combinedDivisorSquares.IsEmpty) - { - return EvaluateWithOpenNumericFallback(n); - } int length = combinedDivisors.Length; - int squaresLength = combinedDivisorSquares.Length; - if (squaresLength < length) - { - length = squaresLength; - } for (int i = 0; i < length; i++) { @@ -248,17 +239,7 @@ public sealed class HeuristicCombinedPrimeTester ReadOnlySpan combinedDivisors = GetCombinedDivisors(nMod10); ReadOnlySpan combinedDivisorSquares = GetCombinedDivisorSquares(nMod10); - if (combinedDivisors.IsEmpty || combinedDivisorSquares.IsEmpty) - { - goto Cleanup; - } - int length = combinedDivisors.Length; - int squaresLength = combinedDivisorSquares.Length; - if (squaresLength < length) - { - length = squaresLength; - } for (int i = 0; i < length; i++) { @@ -334,7 +315,7 @@ Cleanup: 3 => CombinedDivisorsEnding3, 7 => CombinedDivisorsEnding7, 9 => CombinedDivisorsEnding9, - _ => ReadOnlySpan.Empty, + _ => throw new InvalidOperationException($"Unsupported combined divisor selector for digit {nMod10}."), }; } @@ -346,7 +327,7 @@ Cleanup: 3 => CombinedDivisorsEnding3Squares, 7 => CombinedDivisorsEnding7Squares, 9 => CombinedDivisorsEnding9Squares, - _ => ReadOnlySpan.Empty, + _ => throw new InvalidOperationException($"Unsupported combined divisor square selector for digit {nMod10}."), }; } @@ -360,7 +341,7 @@ Cleanup: 3 => CombinedDivisorsEnding3TwoAOneB, 7 => CombinedDivisorsEnding7TwoAOneB, 9 => CombinedDivisorsEnding9TwoAOneB, - _ => ReadOnlySpan.Empty, + _ => throw new InvalidOperationException($"Unsupported combined divisor selector for digit {nMod10}."), }, CombinedDivisorPattern.OneAOneB => nMod10 switch { @@ -368,7 +349,7 @@ Cleanup: 3 => CombinedDivisorsEnding3OneAOneB, 7 => CombinedDivisorsEnding7OneAOneB, 9 => CombinedDivisorsEnding9OneAOneB, - _ => ReadOnlySpan.Empty, + _ => throw new InvalidOperationException($"Unsupported combined divisor selector for digit {nMod10}."), }, CombinedDivisorPattern.ThreeAOneB => nMod10 switch { @@ -376,7 +357,7 @@ Cleanup: 3 => CombinedDivisorsEnding3ThreeAOneB, 7 => CombinedDivisorsEnding7ThreeAOneB, 9 => CombinedDivisorsEnding9ThreeAOneB, - _ => ReadOnlySpan.Empty, + _ => throw new InvalidOperationException($"Unsupported combined divisor selector for digit {nMod10}."), }, CombinedDivisorPattern.ThreeATwoB => nMod10 switch { @@ -384,9 +365,9 @@ Cleanup: 3 => CombinedDivisorsEnding3ThreeATwoB, 7 => CombinedDivisorsEnding7ThreeATwoB, 9 => CombinedDivisorsEnding9ThreeATwoB, - _ => ReadOnlySpan.Empty, + _ => throw new InvalidOperationException($"Unsupported combined divisor selector for digit {nMod10}."), }, - _ => ReadOnlySpan.Empty, + _ => throw new InvalidOperationException($"Unsupported combined divisor pattern: {pattern}."), }; } @@ -400,7 +381,7 @@ Cleanup: 3 => CombinedDivisorsEnding3TwoAOneBSquares, 7 => CombinedDivisorsEnding7TwoAOneBSquares, 9 => CombinedDivisorsEnding9TwoAOneBSquares, - _ => ReadOnlySpan.Empty, + _ => throw new InvalidOperationException($"Unsupported combined divisor square selector for digit {nMod10}."), }, CombinedDivisorPattern.OneAOneB => nMod10 switch { @@ -408,7 +389,7 @@ Cleanup: 3 => CombinedDivisorsEnding3OneAOneBSquares, 7 => CombinedDivisorsEnding7OneAOneBSquares, 9 => CombinedDivisorsEnding9OneAOneBSquares, - _ => ReadOnlySpan.Empty, + _ => throw new InvalidOperationException($"Unsupported combined divisor square selector for digit {nMod10}."), }, CombinedDivisorPattern.ThreeAOneB => nMod10 switch { @@ -416,7 +397,7 @@ Cleanup: 3 => CombinedDivisorsEnding3ThreeAOneBSquares, 7 => CombinedDivisorsEnding7ThreeAOneBSquares, 9 => CombinedDivisorsEnding9ThreeAOneBSquares, - _ => ReadOnlySpan.Empty, + _ => throw new InvalidOperationException($"Unsupported combined divisor square selector for digit {nMod10}."), }, CombinedDivisorPattern.ThreeATwoB => nMod10 switch { @@ -424,9 +405,9 @@ Cleanup: 3 => CombinedDivisorsEnding3ThreeATwoBSquares, 7 => CombinedDivisorsEnding7ThreeATwoBSquares, 9 => CombinedDivisorsEnding9ThreeATwoBSquares, - _ => ReadOnlySpan.Empty, + _ => throw new InvalidOperationException($"Unsupported combined divisor square selector for digit {nMod10}."), }, - _ => ReadOnlySpan.Empty, + _ => throw new InvalidOperationException($"Unsupported combined divisor pattern for squares: {pattern}."), }; } From 13b59abb1c1ae541348e6d74681876d984485203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Wed, 29 Oct 2025 16:25:38 +0100 Subject: [PATCH 03/17] Embed GPU scratch buffers in context leases --- .../HeuristicCombinedPrimeTester.cs | 5 ++- PerfectNumbers.Core/HeuristicPrimeTester.cs | 5 ++- PerfectNumbers.Core/PrimeTester.cs | 35 +++++++++++++------ 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs b/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs index 689fb47..fb0e67c 100644 --- a/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs @@ -216,13 +216,12 @@ public sealed class HeuristicCombinedPrimeTester var gpu = PrimeTester.PrimeTesterGpuContextPool.Rent(batchCapacity, 1); var accelerator = gpu.Accelerator; var state = gpu.State; - var scratch = gpu.Scratch; bool compositeDetected = false; int count = 0; - var inputView = scratch.Input.View; - var outputView = scratch.Output.View; + var inputView = gpu.Input.View; + var outputView = gpu.Output.View; bool ProcessBatch(int length) { diff --git a/PerfectNumbers.Core/HeuristicPrimeTester.cs b/PerfectNumbers.Core/HeuristicPrimeTester.cs index 0d9311d..e1961a5 100644 --- a/PerfectNumbers.Core/HeuristicPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicPrimeTester.cs @@ -270,13 +270,12 @@ public sealed class HeuristicPrimeTester var gpu = PrimeTester.PrimeTesterGpuContextPool.Rent(batchCapacity, 1); var accelerator = gpu.Accelerator; var state = gpu.State; - var scratch = gpu.Scratch; bool compositeDetected = false; int count = 0; - var inputView = scratch.Input.View; - var outputView = scratch.Output.View; + var inputView = gpu.Input.View; + var outputView = gpu.Output.View; bool ProcessBatch(int length) { diff --git a/PerfectNumbers.Core/PrimeTester.cs b/PerfectNumbers.Core/PrimeTester.cs index 2e12a00..6ec9ee6 100644 --- a/PerfectNumbers.Core/PrimeTester.cs +++ b/PerfectNumbers.Core/PrimeTester.cs @@ -129,14 +129,13 @@ public sealed class PrimeTester { var limiter = GpuPrimeWorkLimiter.Acquire(); var gpu = PrimeTesterGpuContextPool.Rent(GpuBatchSize); - var scratch = gpu.Scratch; var state = gpu.State; var accelerator = gpu.Accelerator; int totalLength = values.Length; int batchSize = GpuBatchSize; - var input = scratch.Input; - var output = scratch.Output; + var input = gpu.Input; + var output = gpu.Output; ArrayPool pool = ThreadStaticPools.UlongPool; ulong[] temp = pool.Rent(batchSize); @@ -210,11 +209,6 @@ public sealed class PrimeTester public KernelState.ScratchBuffers GetScratch(int minCapacity, int outputClearLength) { - if (minCapacity <= 0) - { - minCapacity = 1; - } - var scratch = _scratch; var state = KernelState; if (scratch is null) @@ -291,19 +285,37 @@ public sealed class PrimeTester private readonly PooledContext? _ctx; private readonly KernelState? _state; private readonly KernelState.ScratchBuffers? _scratch; + private readonly MemoryBuffer1D? _input; + private readonly MemoryBuffer1D? _output; internal PrimeTesterGpuContextLease(PooledContext ctx, int minScratchCapacity, int outputClearLength) { _ctx = ctx; _state = ctx.KernelState; - _scratch = minScratchCapacity > 0 ? ctx.GetScratch(minScratchCapacity, outputClearLength) : null; + if (minScratchCapacity > 0 || outputClearLength > 0) + { + var scratch = ctx.GetScratch(minScratchCapacity, outputClearLength); + _scratch = scratch; + _input = scratch.Input; + _output = scratch.Output; + } + else + { + _scratch = null; + _input = null; + _output = null; + } } public Accelerator Accelerator => (_ctx ?? throw new InvalidOperationException("GPU context lease is not initialized.")).Accelerator; public KernelState State => _state ?? throw new InvalidOperationException("GPU kernel state is not initialized."); - public KernelState.ScratchBuffers Scratch => _scratch ?? throw new InvalidOperationException("Scratch buffers were not rented for this lease."); + public MemoryBuffer1D Input => _input ?? throw new InvalidOperationException("Input buffer was not rented for this lease."); + + public MemoryBuffer1D Output => _output ?? throw new InvalidOperationException("Output buffer was not rented for this lease."); + + public int ScratchCapacity => _scratch?.Capacity ?? 0; public void Dispose() { @@ -312,7 +324,8 @@ public sealed class PrimeTester Return(ctx); } } - } + + } // Per-accelerator GPU state for prime sieve (kernel + uploaded primes). internal sealed class KernelState From aab785650dbf91c26d4111aebcafbbf5c0952fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Wed, 29 Oct 2025 16:53:22 +0100 Subject: [PATCH 04/17] Manage GPU lease buffers without kernel scratch pool --- .../HeuristicCombinedPrimeTester.cs | 2 +- PerfectNumbers.Core/HeuristicPrimeTester.cs | 2 +- PerfectNumbers.Core/PrimeTester.cs | 212 ++++++++---------- 3 files changed, 97 insertions(+), 119 deletions(-) diff --git a/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs b/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs index fb0e67c..0f59f36 100644 --- a/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs @@ -213,7 +213,7 @@ public sealed class HeuristicCombinedPrimeTester ulong[] divisorArray = divisorPool.Rent(batchCapacity); var limiter = GpuPrimeWorkLimiter.Acquire(); - var gpu = PrimeTester.PrimeTesterGpuContextPool.Rent(batchCapacity, 1); + var gpu = PrimeTester.PrimeTesterGpuContextPool.Rent(batchCapacity); var accelerator = gpu.Accelerator; var state = gpu.State; diff --git a/PerfectNumbers.Core/HeuristicPrimeTester.cs b/PerfectNumbers.Core/HeuristicPrimeTester.cs index e1961a5..364bd62 100644 --- a/PerfectNumbers.Core/HeuristicPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicPrimeTester.cs @@ -267,7 +267,7 @@ public sealed class HeuristicPrimeTester ulong[] divisorArray = divisorPool.Rent(batchCapacity); var limiter = GpuPrimeWorkLimiter.Acquire(); - var gpu = PrimeTester.PrimeTesterGpuContextPool.Rent(batchCapacity, 1); + var gpu = PrimeTester.PrimeTesterGpuContextPool.Rent(batchCapacity); var accelerator = gpu.Accelerator; var state = gpu.State; diff --git a/PerfectNumbers.Core/PrimeTester.cs b/PerfectNumbers.Core/PrimeTester.cs index 6ec9ee6..8c9ddcb 100644 --- a/PerfectNumbers.Core/PrimeTester.cs +++ b/PerfectNumbers.Core/PrimeTester.cs @@ -183,7 +183,6 @@ public sealed class PrimeTester public Accelerator Accelerator { get; } private KernelState? _kernelState; - private KernelState.ScratchBuffers? _scratch; public PooledContext() { @@ -207,46 +206,8 @@ public sealed class PrimeTester } } - public KernelState.ScratchBuffers GetScratch(int minCapacity, int outputClearLength) - { - var scratch = _scratch; - var state = KernelState; - if (scratch is null) - { - scratch = state.RentScratch(minCapacity, Accelerator); - _scratch = scratch; - } - else if (scratch.Capacity < minCapacity) - { - scratch.Dispose(); - scratch = state.RentScratch(minCapacity, Accelerator); - _scratch = scratch; - } - - int clearLength = outputClearLength <= 0 ? 0 : outputClearLength; - if (clearLength > 0) - { - int outputLength = (int)scratch.Output.Length; - if (clearLength > outputLength) - { - clearLength = outputLength; - } - - scratch.Output.View.SubView(0, clearLength).MemSetToZero(); - Accelerator.Synchronize(); - } - - return scratch; - } - public void Dispose() { - if (_scratch is not null) - { - _scratch.Dispose(); - _scratch = null; - } - ClearGpuCaches(Accelerator); _kernelState = null; Accelerator.Dispose(); @@ -254,16 +215,62 @@ public sealed class PrimeTester } } + private sealed class DeviceBuffers : IDisposable + { + public DeviceBuffers(Accelerator accelerator, int capacity) + { + Accelerator = accelerator; + Capacity = Math.Max(1, capacity); + Input = accelerator.Allocate1D(Capacity); + Output = accelerator.Allocate1D(Capacity); + } + + public Accelerator Accelerator { get; } + + public MemoryBuffer1D Input { get; private set; } + + public MemoryBuffer1D Output { get; private set; } + + public int Capacity { get; private set; } + + public void EnsureCapacity(int minCapacity) + { + int required = Math.Max(1, minCapacity); + if (Capacity >= required) + { + return; + } + + DisposeBuffers(); + Capacity = required; + Input = Accelerator.Allocate1D(Capacity); + Output = Accelerator.Allocate1D(Capacity); + } + + private void DisposeBuffers() + { + Output.Dispose(); + Input.Dispose(); + } + + public void Dispose() + { + DisposeBuffers(); + } + } + private static readonly ConcurrentQueue Pool = new(); - internal static PrimeTesterGpuContextLease Rent(int minScratchCapacity = 0, int outputClearLength = 0) + private static readonly ConcurrentQueue BufferPool = new(); + + internal static PrimeTesterGpuContextLease Rent(int minBufferCapacity = 0) { if (Pool.TryDequeue(out var ctx)) { - return new PrimeTesterGpuContextLease(ctx, minScratchCapacity, outputClearLength); + return new PrimeTesterGpuContextLease(ctx, minBufferCapacity); } - return new PrimeTesterGpuContextLease(new PooledContext(), minScratchCapacity, outputClearLength); + return new PrimeTesterGpuContextLease(new PooledContext(), minBufferCapacity); } internal static void DisposeAll() @@ -272,6 +279,37 @@ public sealed class PrimeTester { ctx.Dispose(); } + + while (BufferPool.TryDequeue(out var buffers)) + { + buffers.Dispose(); + } + } + + private static DeviceBuffers? AcquireBuffers(Accelerator accelerator, int minCapacity) + { + if (minCapacity <= 0) + { + return null; + } + + while (BufferPool.TryDequeue(out var buffers)) + { + if (buffers.Accelerator == accelerator) + { + buffers.EnsureCapacity(minCapacity); + return buffers; + } + + buffers.Dispose(); + } + + return new DeviceBuffers(accelerator, minCapacity); + } + + private static void ReturnBuffers(DeviceBuffers buffers) + { + BufferPool.Enqueue(buffers); } private static void Return(PooledContext ctx) @@ -283,50 +321,41 @@ public sealed class PrimeTester internal readonly struct PrimeTesterGpuContextLease { private readonly PooledContext? _ctx; - private readonly KernelState? _state; - private readonly KernelState.ScratchBuffers? _scratch; - private readonly MemoryBuffer1D? _input; - private readonly MemoryBuffer1D? _output; - internal PrimeTesterGpuContextLease(PooledContext ctx, int minScratchCapacity, int outputClearLength) + private readonly KernelState? _state; + + private readonly DeviceBuffers? _buffers; + + internal PrimeTesterGpuContextLease(PooledContext ctx, int minBufferCapacity) { _ctx = ctx; _state = ctx.KernelState; - if (minScratchCapacity > 0 || outputClearLength > 0) - { - var scratch = ctx.GetScratch(minScratchCapacity, outputClearLength); - _scratch = scratch; - _input = scratch.Input; - _output = scratch.Output; - } - else - { - _scratch = null; - _input = null; - _output = null; - } + _buffers = AcquireBuffers(ctx.Accelerator, minBufferCapacity); } public Accelerator Accelerator => (_ctx ?? throw new InvalidOperationException("GPU context lease is not initialized.")).Accelerator; public KernelState State => _state ?? throw new InvalidOperationException("GPU kernel state is not initialized."); - public MemoryBuffer1D Input => _input ?? throw new InvalidOperationException("Input buffer was not rented for this lease."); + public MemoryBuffer1D Input => (_buffers ?? throw new InvalidOperationException("Input buffer was not rented for this lease.")).Input; - public MemoryBuffer1D Output => _output ?? throw new InvalidOperationException("Output buffer was not rented for this lease."); + public MemoryBuffer1D Output => (_buffers ?? throw new InvalidOperationException("Output buffer was not rented for this lease.")).Output; - public int ScratchCapacity => _scratch?.Capacity ?? 0; + public int BufferCapacity => _buffers?.Capacity ?? 0; public void Dispose() { var ctx = _ctx ?? throw new InvalidOperationException("GPU context lease is not initialized."); + if (_buffers is not null) + { + ReturnBuffers(_buffers); + } + Return(ctx); } } - - } - + } // Per-accelerator GPU state for prime sieve (kernel + uploaded primes). internal sealed class KernelState { @@ -343,10 +372,6 @@ public sealed class PrimeTester public MemoryBuffer1D DevicePrimesPow2LastThree; public MemoryBuffer1D DevicePrimesPow2LastNine; public bool IsDisposed; - private readonly ConcurrentBag _scratchPool = []; - // TODO: Replace this ConcurrentBag with the lock-free ring buffer variant validated in - // GpuModularArithmeticBenchmarks so renting scratch buffers stops contending on the bag's internal locks when - // thousands of GPU batches execute per second. public KernelState(Accelerator accelerator) { @@ -396,48 +421,6 @@ public sealed class PrimeTester DevicePrimesPow2LastNine.View.CopyFromCPU(primesPow2LastNine); } - internal sealed class ScratchBuffers - { - public MemoryBuffer1D Input { get; private set; } - public MemoryBuffer1D Output { get; private set; } - public int Capacity { get; private set; } - - public ScratchBuffers(Accelerator accel, int capacity) - { - Capacity = Math.Max(1, capacity); - Input = accel.Allocate1D(Capacity); - Output = accel.Allocate1D(Capacity); - } - - public void Dispose() - { - Output.Dispose(); - Input.Dispose(); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ScratchBuffers RentScratch(int minCapacity, Accelerator accel) - { - while (_scratchPool.TryTake(out var sb)) - { - if (sb.Capacity >= minCapacity) - { - return sb; - } - - sb.Dispose(); - } - - return new ScratchBuffers(accel, minCapacity); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ReturnScratch(ScratchBuffers scratch) - { - _scratchPool.Add(scratch); - } - public void Clear() { if (IsDisposed) @@ -445,11 +428,6 @@ public sealed class PrimeTester return; } - while (_scratchPool.TryTake(out var sb)) - { - sb.Dispose(); - } - DevicePrimesDefault.Dispose(); DevicePrimesLastOne.Dispose(); DevicePrimesLastSeven.Dispose(); From ca53ddc798d407c2f4f3af61a5a69c2627e82d8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Wed, 29 Oct 2025 16:53:29 +0100 Subject: [PATCH 05/17] Inline GPU lease buffers into the context lease --- PerfectNumbers.Core/PrimeTester.cs | 123 ++++++++++------------------- 1 file changed, 40 insertions(+), 83 deletions(-) diff --git a/PerfectNumbers.Core/PrimeTester.cs b/PerfectNumbers.Core/PrimeTester.cs index 8c9ddcb..44e14fb 100644 --- a/PerfectNumbers.Core/PrimeTester.cs +++ b/PerfectNumbers.Core/PrimeTester.cs @@ -215,53 +215,9 @@ public sealed class PrimeTester } } - private sealed class DeviceBuffers : IDisposable - { - public DeviceBuffers(Accelerator accelerator, int capacity) - { - Accelerator = accelerator; - Capacity = Math.Max(1, capacity); - Input = accelerator.Allocate1D(Capacity); - Output = accelerator.Allocate1D(Capacity); - } - - public Accelerator Accelerator { get; } - - public MemoryBuffer1D Input { get; private set; } - - public MemoryBuffer1D Output { get; private set; } - - public int Capacity { get; private set; } - - public void EnsureCapacity(int minCapacity) - { - int required = Math.Max(1, minCapacity); - if (Capacity >= required) - { - return; - } - - DisposeBuffers(); - Capacity = required; - Input = Accelerator.Allocate1D(Capacity); - Output = Accelerator.Allocate1D(Capacity); - } - - private void DisposeBuffers() - { - Output.Dispose(); - Input.Dispose(); - } - - public void Dispose() - { - DisposeBuffers(); - } - } - private static readonly ConcurrentQueue Pool = new(); - private static readonly ConcurrentQueue BufferPool = new(); + private static readonly ConcurrentQueue<(Accelerator Accelerator, MemoryBuffer1D Input, MemoryBuffer1D Output, int Capacity)> BufferPool = new(); internal static PrimeTesterGpuContextLease Rent(int minBufferCapacity = 0) { @@ -280,36 +236,46 @@ public sealed class PrimeTester ctx.Dispose(); } - while (BufferPool.TryDequeue(out var buffers)) + while (BufferPool.TryDequeue(out var entry)) { - buffers.Dispose(); + entry.Output.Dispose(); + entry.Input.Dispose(); } } - private static DeviceBuffers? AcquireBuffers(Accelerator accelerator, int minCapacity) + private static void AcquireBuffers(Accelerator accelerator, int minCapacity, out MemoryBuffer1D input, out MemoryBuffer1D output, out int capacity) { - if (minCapacity <= 0) - { - return null; - } + int required = Math.Max(1, minCapacity); - while (BufferPool.TryDequeue(out var buffers)) + while (BufferPool.TryDequeue(out var entry)) { - if (buffers.Accelerator == accelerator) + if (entry.Accelerator != accelerator) { - buffers.EnsureCapacity(minCapacity); - return buffers; + entry.Output.Dispose(); + entry.Input.Dispose(); + continue; } - buffers.Dispose(); + if (entry.Capacity >= required) + { + input = entry.Input; + output = entry.Output; + capacity = entry.Capacity; + return; + } + + entry.Output.Dispose(); + entry.Input.Dispose(); } - return new DeviceBuffers(accelerator, minCapacity); + capacity = required; + input = accelerator.Allocate1D(capacity); + output = accelerator.Allocate1D(capacity); } - private static void ReturnBuffers(DeviceBuffers buffers) + private static void ReturnBuffers(Accelerator accelerator, MemoryBuffer1D input, MemoryBuffer1D output, int capacity) { - BufferPool.Enqueue(buffers); + BufferPool.Enqueue((accelerator, input, output, capacity)); } private static void Return(PooledContext ctx) @@ -320,39 +286,30 @@ public sealed class PrimeTester internal readonly struct PrimeTesterGpuContextLease { - private readonly PooledContext? _ctx; + private readonly PooledContext _ctx; - private readonly KernelState? _state; + public readonly Accelerator Accelerator; - private readonly DeviceBuffers? _buffers; + public readonly KernelState State; + + public readonly MemoryBuffer1D Input; + + public readonly MemoryBuffer1D Output; + + public readonly int BufferCapacity; internal PrimeTesterGpuContextLease(PooledContext ctx, int minBufferCapacity) { _ctx = ctx; - _state = ctx.KernelState; - _buffers = AcquireBuffers(ctx.Accelerator, minBufferCapacity); + Accelerator = ctx.Accelerator; + State = ctx.KernelState; + AcquireBuffers(Accelerator, minBufferCapacity, out Input, out Output, out BufferCapacity); } - public Accelerator Accelerator => (_ctx ?? throw new InvalidOperationException("GPU context lease is not initialized.")).Accelerator; - - public KernelState State => _state ?? throw new InvalidOperationException("GPU kernel state is not initialized."); - - public MemoryBuffer1D Input => (_buffers ?? throw new InvalidOperationException("Input buffer was not rented for this lease.")).Input; - - public MemoryBuffer1D Output => (_buffers ?? throw new InvalidOperationException("Output buffer was not rented for this lease.")).Output; - - public int BufferCapacity => _buffers?.Capacity ?? 0; - public void Dispose() { - var ctx = _ctx ?? throw new InvalidOperationException("GPU context lease is not initialized."); - - if (_buffers is not null) - { - ReturnBuffers(_buffers); - } - - Return(ctx); + ReturnBuffers(Accelerator, Input, Output, BufferCapacity); + Return(_ctx); } } } From 83a6c6c558700d6dc5c58b303cc854a972dc46bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Wed, 29 Oct 2025 17:05:17 +0100 Subject: [PATCH 06/17] Scope GPU prime tester buffers to leases --- PerfectNumbers.Core/PrimeTester.cs | 78 +++++++++++------------------- 1 file changed, 27 insertions(+), 51 deletions(-) diff --git a/PerfectNumbers.Core/PrimeTester.cs b/PerfectNumbers.Core/PrimeTester.cs index 44e14fb..ab792b9 100644 --- a/PerfectNumbers.Core/PrimeTester.cs +++ b/PerfectNumbers.Core/PrimeTester.cs @@ -1,3 +1,4 @@ +using System; using System.Buffers; using System.Collections.Concurrent; using System.Numerics; @@ -217,8 +218,6 @@ public sealed class PrimeTester private static readonly ConcurrentQueue Pool = new(); - private static readonly ConcurrentQueue<(Accelerator Accelerator, MemoryBuffer1D Input, MemoryBuffer1D Output, int Capacity)> BufferPool = new(); - internal static PrimeTesterGpuContextLease Rent(int minBufferCapacity = 0) { if (Pool.TryDequeue(out var ctx)) @@ -235,47 +234,6 @@ public sealed class PrimeTester { ctx.Dispose(); } - - while (BufferPool.TryDequeue(out var entry)) - { - entry.Output.Dispose(); - entry.Input.Dispose(); - } - } - - private static void AcquireBuffers(Accelerator accelerator, int minCapacity, out MemoryBuffer1D input, out MemoryBuffer1D output, out int capacity) - { - int required = Math.Max(1, minCapacity); - - while (BufferPool.TryDequeue(out var entry)) - { - if (entry.Accelerator != accelerator) - { - entry.Output.Dispose(); - entry.Input.Dispose(); - continue; - } - - if (entry.Capacity >= required) - { - input = entry.Input; - output = entry.Output; - capacity = entry.Capacity; - return; - } - - entry.Output.Dispose(); - entry.Input.Dispose(); - } - - capacity = required; - input = accelerator.Allocate1D(capacity); - output = accelerator.Allocate1D(capacity); - } - - private static void ReturnBuffers(Accelerator accelerator, MemoryBuffer1D input, MemoryBuffer1D output, int capacity) - { - BufferPool.Enqueue((accelerator, input, output, capacity)); } private static void Return(PooledContext ctx) @@ -284,31 +242,49 @@ public sealed class PrimeTester Pool.Enqueue(ctx); } - internal readonly struct PrimeTesterGpuContextLease + internal sealed class PrimeTesterGpuContextLease : IDisposable { private readonly PooledContext _ctx; - public readonly Accelerator Accelerator; + public Accelerator Accelerator { get; } - public readonly KernelState State; + public KernelState State { get; } - public readonly MemoryBuffer1D Input; + public MemoryBuffer1D Input { get; private set; } - public readonly MemoryBuffer1D Output; + public MemoryBuffer1D Output { get; private set; } - public readonly int BufferCapacity; + public int BufferCapacity { get; private set; } internal PrimeTesterGpuContextLease(PooledContext ctx, int minBufferCapacity) { _ctx = ctx; Accelerator = ctx.Accelerator; State = ctx.KernelState; - AcquireBuffers(Accelerator, minBufferCapacity, out Input, out Output, out BufferCapacity); + BufferCapacity = Math.Max(1, minBufferCapacity); + Input = Accelerator.Allocate1D(BufferCapacity); + Output = Accelerator.Allocate1D(BufferCapacity); + } + + public void EnsureCapacity(int minCapacity) + { + if (minCapacity <= BufferCapacity) + { + return; + } + + Input.Dispose(); + Output.Dispose(); + + BufferCapacity = Math.Max(1, minCapacity); + Input = Accelerator.Allocate1D(BufferCapacity); + Output = Accelerator.Allocate1D(BufferCapacity); } public void Dispose() { - ReturnBuffers(Accelerator, Input, Output, BufferCapacity); + Input.Dispose(); + Output.Dispose(); Return(_ctx); } } From 6395b06b1934521279ed64cada2444d9439c65e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Wed, 29 Oct 2025 17:20:21 +0100 Subject: [PATCH 07/17] Optimize single GPU prime path and reuse lease buffers --- PerfectNumbers.Core/PrimeTester.cs | 109 +++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 23 deletions(-) diff --git a/PerfectNumbers.Core/PrimeTester.cs b/PerfectNumbers.Core/PrimeTester.cs index ab792b9..b099fea 100644 --- a/PerfectNumbers.Core/PrimeTester.cs +++ b/PerfectNumbers.Core/PrimeTester.cs @@ -106,14 +106,40 @@ public sealed class PrimeTester return IsPrimeCpu(n, ct); } - Span one = stackalloc ulong[1]; - Span outFlags = stackalloc byte[1]; - one[0] = n; - outFlags[0] = 0; + var limiter = GpuPrimeWorkLimiter.Acquire(); + var gpu = PrimeTesterGpuContextPool.Rent(1); + var state = gpu.State; + var accelerator = gpu.Accelerator; + gpu.EnsureCapacity(1); - IsPrimeBatchGpu(one, outFlags); + ulong value = n; + byte flag = 0; - return outFlags[0] != 0; + var input = gpu.Input; + var output = gpu.Output; + + input.View.CopyFromCPU(ref value, 1); + state.Kernel( + 1, + input.View, + state.DevicePrimesDefault.View, + state.DevicePrimesLastOne.View, + state.DevicePrimesLastSeven.View, + state.DevicePrimesLastThree.View, + state.DevicePrimesLastNine.View, + state.DevicePrimesPow2Default.View, + state.DevicePrimesPow2LastOne.View, + state.DevicePrimesPow2LastSeven.View, + state.DevicePrimesPow2LastThree.View, + state.DevicePrimesPow2LastNine.View, + output.View); + accelerator.Synchronize(); + output.View.CopyToCPU(ref flag, 1); + + gpu.Dispose(); + limiter.Dispose(); + + return flag != 0; } public static bool IsPrimeCpu(ulong n, CancellationToken ct) @@ -185,6 +211,10 @@ public sealed class PrimeTester private KernelState? _kernelState; + internal MemoryBuffer1D? InputBuffer; + internal MemoryBuffer1D? OutputBuffer; + internal int BufferCapacity; + public PooledContext() { Context = Context.CreateDefault(); @@ -207,8 +237,36 @@ public sealed class PrimeTester } } + public void EnsureBufferCapacity(int minCapacity) + { + if (minCapacity < 1) + { + minCapacity = 1; + } + + var input = InputBuffer; + var output = OutputBuffer; + + if (BufferCapacity >= minCapacity && input is not null && output is not null && !input.IsDisposed && !output.IsDisposed) + { + return; + } + + input?.Dispose(); + output?.Dispose(); + + BufferCapacity = minCapacity; + InputBuffer = Accelerator.Allocate1D(BufferCapacity); + OutputBuffer = Accelerator.Allocate1D(BufferCapacity); + } + public void Dispose() { + InputBuffer?.Dispose(); + OutputBuffer?.Dispose(); + InputBuffer = null; + OutputBuffer = null; + BufferCapacity = 0; ClearGpuCaches(Accelerator); _kernelState = null; Accelerator.Dispose(); @@ -246,24 +304,30 @@ public sealed class PrimeTester { private readonly PooledContext _ctx; - public Accelerator Accelerator { get; } + public readonly Accelerator Accelerator; - public KernelState State { get; } + public readonly KernelState State; - public MemoryBuffer1D Input { get; private set; } + public MemoryBuffer1D Input; - public MemoryBuffer1D Output { get; private set; } + public MemoryBuffer1D Output; - public int BufferCapacity { get; private set; } + public int BufferCapacity; internal PrimeTesterGpuContextLease(PooledContext ctx, int minBufferCapacity) { + if (minBufferCapacity < 1) + { + minBufferCapacity = 1; + } + _ctx = ctx; Accelerator = ctx.Accelerator; State = ctx.KernelState; - BufferCapacity = Math.Max(1, minBufferCapacity); - Input = Accelerator.Allocate1D(BufferCapacity); - Output = Accelerator.Allocate1D(BufferCapacity); + ctx.EnsureBufferCapacity(minBufferCapacity); + Input = ctx.InputBuffer ?? throw new InvalidOperationException("GPU context input buffer is not initialized."); + Output = ctx.OutputBuffer ?? throw new InvalidOperationException("GPU context output buffer is not initialized."); + BufferCapacity = ctx.BufferCapacity; } public void EnsureCapacity(int minCapacity) @@ -273,22 +337,21 @@ public sealed class PrimeTester return; } - Input.Dispose(); - Output.Dispose(); - - BufferCapacity = Math.Max(1, minCapacity); - Input = Accelerator.Allocate1D(BufferCapacity); - Output = Accelerator.Allocate1D(BufferCapacity); + _ctx.EnsureBufferCapacity(minCapacity); + Input = _ctx.InputBuffer ?? throw new InvalidOperationException("GPU context input buffer is not initialized."); + Output = _ctx.OutputBuffer ?? throw new InvalidOperationException("GPU context output buffer is not initialized."); + BufferCapacity = _ctx.BufferCapacity; } public void Dispose() { - Input.Dispose(); - Output.Dispose(); + BufferCapacity = _ctx.BufferCapacity; + Input = _ctx.InputBuffer ?? throw new InvalidOperationException("GPU context input buffer is not initialized."); + Output = _ctx.OutputBuffer ?? throw new InvalidOperationException("GPU context output buffer is not initialized."); Return(_ctx); } } - } + } // Per-accelerator GPU state for prime sieve (kernel + uploaded primes). internal sealed class KernelState { From 08499fdda1596110e0f679406a38d7e952973767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Wed, 29 Oct 2025 17:39:38 +0100 Subject: [PATCH 08/17] Refine GPU prime tester leasing --- .../Gpu/Kernels/PrimeTesterKernels.cs | 2 +- PerfectNumbers.Core/PrimeTester.cs | 68 ++++++------------- 2 files changed, 20 insertions(+), 50 deletions(-) diff --git a/PerfectNumbers.Core/Gpu/Kernels/PrimeTesterKernels.cs b/PerfectNumbers.Core/Gpu/Kernels/PrimeTesterKernels.cs index c86f960..b17f7bb 100644 --- a/PerfectNumbers.Core/Gpu/Kernels/PrimeTesterKernels.cs +++ b/PerfectNumbers.Core/Gpu/Kernels/PrimeTesterKernels.cs @@ -48,7 +48,7 @@ internal static class PrimeTesterKernels break; } - int length = XMath.Min((int)primes.Length, (int)primeSquares.Length); + int length = (int)primes.Length; for (int i = 0; i < length; i++) { ulong prime = primes[i]; diff --git a/PerfectNumbers.Core/PrimeTester.cs b/PerfectNumbers.Core/PrimeTester.cs index b099fea..de8b70f 100644 --- a/PerfectNumbers.Core/PrimeTester.cs +++ b/PerfectNumbers.Core/PrimeTester.cs @@ -110,7 +110,6 @@ public sealed class PrimeTester var gpu = PrimeTesterGpuContextPool.Rent(1); var state = gpu.State; var accelerator = gpu.Accelerator; - gpu.EnsureCapacity(1); ulong value = n; byte flag = 0; @@ -211,10 +210,7 @@ public sealed class PrimeTester private KernelState? _kernelState; - internal MemoryBuffer1D? InputBuffer; - internal MemoryBuffer1D? OutputBuffer; - internal int BufferCapacity; - + public PooledContext() { Context = Context.CreateDefault(); @@ -237,36 +233,8 @@ public sealed class PrimeTester } } - public void EnsureBufferCapacity(int minCapacity) - { - if (minCapacity < 1) - { - minCapacity = 1; - } - - var input = InputBuffer; - var output = OutputBuffer; - - if (BufferCapacity >= minCapacity && input is not null && output is not null && !input.IsDisposed && !output.IsDisposed) - { - return; - } - - input?.Dispose(); - output?.Dispose(); - - BufferCapacity = minCapacity; - InputBuffer = Accelerator.Allocate1D(BufferCapacity); - OutputBuffer = Accelerator.Allocate1D(BufferCapacity); - } - public void Dispose() { - InputBuffer?.Dispose(); - OutputBuffer?.Dispose(); - InputBuffer = null; - OutputBuffer = null; - BufferCapacity = 0; ClearGpuCaches(Accelerator); _kernelState = null; Accelerator.Dispose(); @@ -276,7 +244,7 @@ public sealed class PrimeTester private static readonly ConcurrentQueue Pool = new(); - internal static PrimeTesterGpuContextLease Rent(int minBufferCapacity = 0) + internal static PrimeTesterGpuContextLease Rent(int minBufferCapacity = 1) { if (Pool.TryDequeue(out var ctx)) { @@ -316,18 +284,18 @@ public sealed class PrimeTester internal PrimeTesterGpuContextLease(PooledContext ctx, int minBufferCapacity) { - if (minBufferCapacity < 1) - { - minBufferCapacity = 1; - } + // The pool never requests a zero-length buffer, so the old guard stays commented out to document the invariant. + // if (minBufferCapacity < 1) + // { + // minBufferCapacity = 1; + // } _ctx = ctx; Accelerator = ctx.Accelerator; State = ctx.KernelState; - ctx.EnsureBufferCapacity(minBufferCapacity); - Input = ctx.InputBuffer ?? throw new InvalidOperationException("GPU context input buffer is not initialized."); - Output = ctx.OutputBuffer ?? throw new InvalidOperationException("GPU context output buffer is not initialized."); - BufferCapacity = ctx.BufferCapacity; + Input = Accelerator.Allocate1D(minBufferCapacity); + Output = Accelerator.Allocate1D(minBufferCapacity); + BufferCapacity = minBufferCapacity; } public void EnsureCapacity(int minCapacity) @@ -337,17 +305,19 @@ public sealed class PrimeTester return; } - _ctx.EnsureBufferCapacity(minCapacity); - Input = _ctx.InputBuffer ?? throw new InvalidOperationException("GPU context input buffer is not initialized."); - Output = _ctx.OutputBuffer ?? throw new InvalidOperationException("GPU context output buffer is not initialized."); - BufferCapacity = _ctx.BufferCapacity; + Input.Dispose(); + Output.Dispose(); + + Input = Accelerator.Allocate1D(minCapacity); + Output = Accelerator.Allocate1D(minCapacity); + BufferCapacity = minCapacity; } public void Dispose() { - BufferCapacity = _ctx.BufferCapacity; - Input = _ctx.InputBuffer ?? throw new InvalidOperationException("GPU context input buffer is not initialized."); - Output = _ctx.OutputBuffer ?? throw new InvalidOperationException("GPU context output buffer is not initialized."); + Input.Dispose(); + Output.Dispose(); + BufferCapacity = 0; Return(_ctx); } } From 23fa56b3568211aad6d5bccd25e32da8d89d92dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Wed, 29 Oct 2025 18:13:46 +0100 Subject: [PATCH 09/17] Embed kernel state lookup in GPU leases --- PerfectNumbers.Core/PrimeTester.cs | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/PerfectNumbers.Core/PrimeTester.cs b/PerfectNumbers.Core/PrimeTester.cs index de8b70f..44580d0 100644 --- a/PerfectNumbers.Core/PrimeTester.cs +++ b/PerfectNumbers.Core/PrimeTester.cs @@ -208,35 +208,15 @@ public sealed class PrimeTester public Accelerator Accelerator { get; } - private KernelState? _kernelState; - - public PooledContext() { Context = Context.CreateDefault(); Accelerator = Context.GetPreferredDevice(false).CreateAccelerator(Context); - _kernelState = GpuKernelState.GetOrCreate(Accelerator); - } - - public KernelState KernelState - { - get - { - var state = _kernelState; - if (state is null || state.IsDisposed) - { - state = GpuKernelState.GetOrCreate(Accelerator); - _kernelState = state; - } - - return state; - } } public void Dispose() { ClearGpuCaches(Accelerator); - _kernelState = null; Accelerator.Dispose(); Context.Dispose(); } @@ -292,7 +272,7 @@ public sealed class PrimeTester _ctx = ctx; Accelerator = ctx.Accelerator; - State = ctx.KernelState; + State = GpuKernelState.GetOrCreate(Accelerator); Input = Accelerator.Allocate1D(minBufferCapacity); Output = Accelerator.Allocate1D(minBufferCapacity); BufferCapacity = minBufferCapacity; From a4ac82e74d0ad0247981d0dcca7007b1ffb34ec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Wed, 29 Oct 2025 18:13:51 +0100 Subject: [PATCH 10/17] Inline GPU kernel state into prime tester leases --- .../HeuristicCombinedPrimeTester.cs | 36 +- PerfectNumbers.Core/HeuristicPrimeTester.cs | 4 +- PerfectNumbers.Core/PrimeTester.cs | 540 +++++++++--------- 3 files changed, 300 insertions(+), 280 deletions(-) diff --git a/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs b/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs index 0f59f36..a713bb5 100644 --- a/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs @@ -213,29 +213,29 @@ public sealed class HeuristicCombinedPrimeTester ulong[] divisorArray = divisorPool.Rent(batchCapacity); var limiter = GpuPrimeWorkLimiter.Acquire(); - var gpu = PrimeTester.PrimeTesterGpuContextPool.Rent(batchCapacity); - var accelerator = gpu.Accelerator; - var state = gpu.State; + var gpu = PrimeTester.PrimeTesterGpuContextPool.Rent(batchCapacity); + var accelerator = gpu.Accelerator; + var kernel = gpu.HeuristicTrialDivisionKernel; - bool compositeDetected = false; - int count = 0; + bool compositeDetected = false; + int count = 0; - var inputView = gpu.Input.View; - var outputView = gpu.Output.View; + var inputView = gpu.Input.View; + var outputView = gpu.Output.View; - bool ProcessBatch(int length) - { - inputView.CopyFromCPU(ref divisorArray[0], length); - byte compositeFlag = 0; - outputView.CopyFromCPU(ref compositeFlag, 1); - state.HeuristicTrialDivisionKernel(length, inputView, n, outputView); - accelerator.Synchronize(); - outputView.CopyToCPU(ref compositeFlag, 1); + bool ProcessBatch(int length) + { + inputView.CopyFromCPU(ref divisorArray[0], length); + byte compositeFlag = 0; + outputView.CopyFromCPU(ref compositeFlag, 1); + kernel(length, inputView, n, outputView); + accelerator.Synchronize(); + outputView.CopyToCPU(ref compositeFlag, 1); - return compositeFlag != 0; - } + return compositeFlag != 0; + } - ReadOnlySpan combinedDivisors = GetCombinedDivisors(nMod10); + ReadOnlySpan combinedDivisors = GetCombinedDivisors(nMod10); ReadOnlySpan combinedDivisorSquares = GetCombinedDivisorSquares(nMod10); int length = combinedDivisors.Length; diff --git a/PerfectNumbers.Core/HeuristicPrimeTester.cs b/PerfectNumbers.Core/HeuristicPrimeTester.cs index 364bd62..cdaf7e2 100644 --- a/PerfectNumbers.Core/HeuristicPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicPrimeTester.cs @@ -269,7 +269,7 @@ public sealed class HeuristicPrimeTester var limiter = GpuPrimeWorkLimiter.Acquire(); var gpu = PrimeTester.PrimeTesterGpuContextPool.Rent(batchCapacity); var accelerator = gpu.Accelerator; - var state = gpu.State; + var kernel = gpu.HeuristicTrialDivisionKernel; bool compositeDetected = false; int count = 0; @@ -282,7 +282,7 @@ public sealed class HeuristicPrimeTester inputView.CopyFromCPU(ref divisorArray[0], length); byte compositeFlag = 0; outputView.CopyFromCPU(ref compositeFlag, 1); - state.HeuristicTrialDivisionKernel(length, inputView, n, outputView); + kernel(length, inputView, n, outputView); accelerator.Synchronize(); outputView.CopyToCPU(ref compositeFlag, 1); diff --git a/PerfectNumbers.Core/PrimeTester.cs b/PerfectNumbers.Core/PrimeTester.cs index 44580d0..8d9705a 100644 --- a/PerfectNumbers.Core/PrimeTester.cs +++ b/PerfectNumbers.Core/PrimeTester.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; using ILGPU; @@ -97,51 +98,51 @@ public sealed class PrimeTester [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsPrimeGpu(ulong n, CancellationToken ct) - { - bool forceCpu = GpuContextPool.ForceCpu; - bool belowGpuRange = n < 31UL; + { + bool forceCpu = GpuContextPool.ForceCpu; + bool belowGpuRange = n < 31UL; - if (forceCpu || belowGpuRange) - { - return IsPrimeCpu(n, ct); - } + if (forceCpu || belowGpuRange) + { + return IsPrimeCpu(n, ct); + } - var limiter = GpuPrimeWorkLimiter.Acquire(); - var gpu = PrimeTesterGpuContextPool.Rent(1); - var state = gpu.State; - var accelerator = gpu.Accelerator; + var limiter = GpuPrimeWorkLimiter.Acquire(); + var gpu = PrimeTesterGpuContextPool.Rent(1); + var accelerator = gpu.Accelerator; + var kernel = gpu.Kernel; - ulong value = n; - byte flag = 0; + ulong value = n; + byte flag = 0; - var input = gpu.Input; - var output = gpu.Output; + var input = gpu.Input; + var output = gpu.Output; - input.View.CopyFromCPU(ref value, 1); - state.Kernel( - 1, - input.View, - state.DevicePrimesDefault.View, - state.DevicePrimesLastOne.View, - state.DevicePrimesLastSeven.View, - state.DevicePrimesLastThree.View, - state.DevicePrimesLastNine.View, - state.DevicePrimesPow2Default.View, - state.DevicePrimesPow2LastOne.View, - state.DevicePrimesPow2LastSeven.View, - state.DevicePrimesPow2LastThree.View, - state.DevicePrimesPow2LastNine.View, - output.View); - accelerator.Synchronize(); - output.View.CopyToCPU(ref flag, 1); + input.View.CopyFromCPU(ref value, 1); + kernel( + 1, + input.View, + gpu.DevicePrimesDefault.View, + gpu.DevicePrimesLastOne.View, + gpu.DevicePrimesLastSeven.View, + gpu.DevicePrimesLastThree.View, + gpu.DevicePrimesLastNine.View, + gpu.DevicePrimesPow2Default.View, + gpu.DevicePrimesPow2LastOne.View, + gpu.DevicePrimesPow2LastSeven.View, + gpu.DevicePrimesPow2LastThree.View, + gpu.DevicePrimesPow2LastNine.View, + output.View); + accelerator.Synchronize(); + output.View.CopyToCPU(ref flag, 1); - gpu.Dispose(); - limiter.Dispose(); + gpu.Dispose(); + limiter.Dispose(); - return flag != 0; - } + return flag != 0; + } - public static bool IsPrimeCpu(ulong n, CancellationToken ct) +public static bool IsPrimeCpu(ulong n, CancellationToken ct) { // Preserve the legacy entry point for callers that bypass the thread-local caches. return IsPrime(n, ct); @@ -152,276 +153,295 @@ public sealed class PrimeTester [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void IsPrimeBatchGpu(ReadOnlySpan values, Span results) - { - var limiter = GpuPrimeWorkLimiter.Acquire(); - var gpu = PrimeTesterGpuContextPool.Rent(GpuBatchSize); - var state = gpu.State; - var accelerator = gpu.Accelerator; - int totalLength = values.Length; - int batchSize = GpuBatchSize; + { + var limiter = GpuPrimeWorkLimiter.Acquire(); + var gpu = PrimeTesterGpuContextPool.Rent(GpuBatchSize); + var accelerator = gpu.Accelerator; + var kernel = gpu.Kernel; + int totalLength = values.Length; + int batchSize = GpuBatchSize; - var input = gpu.Input; - var output = gpu.Output; - ArrayPool pool = ThreadStaticPools.UlongPool; - ulong[] temp = pool.Rent(batchSize); + var input = gpu.Input; + var output = gpu.Output; + ArrayPool pool = ThreadStaticPools.UlongPool; + ulong[] temp = pool.Rent(batchSize); - int pos = 0; - while (pos < totalLength) - { - int remaining = totalLength - pos; - int count = remaining > batchSize ? batchSize : remaining; + int pos = 0; + while (pos < totalLength) + { + int remaining = totalLength - pos; + int count = remaining > batchSize ? batchSize : remaining; - values.Slice(pos, count).CopyTo(temp); - input.View.CopyFromCPU(ref temp[0], count); + values.Slice(pos, count).CopyTo(temp); + input.View.CopyFromCPU(ref temp[0], count); - state.Kernel( - count, - input.View, - state.DevicePrimesDefault.View, - state.DevicePrimesLastOne.View, - state.DevicePrimesLastSeven.View, - state.DevicePrimesLastThree.View, - state.DevicePrimesLastNine.View, - state.DevicePrimesPow2Default.View, - state.DevicePrimesPow2LastOne.View, - state.DevicePrimesPow2LastSeven.View, - state.DevicePrimesPow2LastThree.View, - state.DevicePrimesPow2LastNine.View, - output.View); - accelerator.Synchronize(); - output.View.CopyToCPU(ref results[pos], count); + kernel( + count, + input.View, + gpu.DevicePrimesDefault.View, + gpu.DevicePrimesLastOne.View, + gpu.DevicePrimesLastSeven.View, + gpu.DevicePrimesLastThree.View, + gpu.DevicePrimesLastNine.View, + gpu.DevicePrimesPow2Default.View, + gpu.DevicePrimesPow2LastOne.View, + gpu.DevicePrimesPow2LastSeven.View, + gpu.DevicePrimesPow2LastThree.View, + gpu.DevicePrimesPow2LastNine.View, + output.View); + accelerator.Synchronize(); + output.View.CopyToCPU(ref results[pos], count); - pos += count; - } + pos += count; + } - pool.Return(temp, clearArray: false); + pool.Return(temp, clearArray: false); - gpu.Dispose(); - limiter.Dispose(); - } + gpu.Dispose(); + limiter.Dispose(); + } - internal static class PrimeTesterGpuContextPool - { - internal sealed class PooledContext - { - public Context Context { get; } + internal static class PrimeTesterGpuContextPool + { + internal sealed class PooledContext + { + public Context Context { get; } - public Accelerator Accelerator { get; } + public Accelerator Accelerator { get; } - public PooledContext() - { - Context = Context.CreateDefault(); - Accelerator = Context.GetPreferredDevice(false).CreateAccelerator(Context); - } + public Action, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView> Kernel { get; } - public void Dispose() - { - ClearGpuCaches(Accelerator); - Accelerator.Dispose(); - Context.Dispose(); - } - } + public Action, ulong, ArrayView> HeuristicTrialDivisionKernel { get; } - private static readonly ConcurrentQueue Pool = new(); + public MemoryBuffer1D DevicePrimesDefault { get; } - internal static PrimeTesterGpuContextLease Rent(int minBufferCapacity = 1) - { - if (Pool.TryDequeue(out var ctx)) - { - return new PrimeTesterGpuContextLease(ctx, minBufferCapacity); - } + public MemoryBuffer1D DevicePrimesLastOne { get; } - return new PrimeTesterGpuContextLease(new PooledContext(), minBufferCapacity); - } + public MemoryBuffer1D DevicePrimesLastSeven { get; } - internal static void DisposeAll() - { - while (Pool.TryDequeue(out var ctx)) - { - ctx.Dispose(); - } - } + public MemoryBuffer1D DevicePrimesLastThree { get; } - private static void Return(PooledContext ctx) - { - ctx.Accelerator.Synchronize(); - Pool.Enqueue(ctx); - } + public MemoryBuffer1D DevicePrimesLastNine { get; } - internal sealed class PrimeTesterGpuContextLease : IDisposable - { - private readonly PooledContext _ctx; + public MemoryBuffer1D DevicePrimesPow2Default { get; } - public readonly Accelerator Accelerator; + public MemoryBuffer1D DevicePrimesPow2LastOne { get; } - public readonly KernelState State; + public MemoryBuffer1D DevicePrimesPow2LastSeven { get; } - public MemoryBuffer1D Input; + public MemoryBuffer1D DevicePrimesPow2LastThree { get; } - public MemoryBuffer1D Output; + public MemoryBuffer1D DevicePrimesPow2LastNine { get; } - public int BufferCapacity; + public MemoryBuffer1D Input { get; private set; } - internal PrimeTesterGpuContextLease(PooledContext ctx, int minBufferCapacity) - { - // The pool never requests a zero-length buffer, so the old guard stays commented out to document the invariant. - // if (minBufferCapacity < 1) - // { - // minBufferCapacity = 1; - // } + public MemoryBuffer1D Output { get; private set; } - _ctx = ctx; - Accelerator = ctx.Accelerator; - State = GpuKernelState.GetOrCreate(Accelerator); - Input = Accelerator.Allocate1D(minBufferCapacity); - Output = Accelerator.Allocate1D(minBufferCapacity); - BufferCapacity = minBufferCapacity; - } + public int BufferCapacity { get; private set; } - public void EnsureCapacity(int minCapacity) - { - if (minCapacity <= BufferCapacity) - { - return; - } + public PooledContext(int minBufferCapacity) + { + Context = Context.CreateDefault(); + Accelerator = Context.GetPreferredDevice(false).CreateAccelerator(Context); + Kernel = Accelerator.LoadAutoGroupedStreamKernel, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView>(PrimeTesterKernels.SmallPrimeSieveKernel); + HeuristicTrialDivisionKernel = Accelerator.LoadAutoGroupedStreamKernel, ulong, ArrayView>(PrimeTesterKernels.HeuristicTrialDivisionKernel); - Input.Dispose(); - Output.Dispose(); + var primesDefault = DivisorGenerator.SmallPrimes; + DevicePrimesDefault = Accelerator.Allocate1D(primesDefault.Length); + DevicePrimesDefault.View.CopyFromCPU(primesDefault); - Input = Accelerator.Allocate1D(minCapacity); - Output = Accelerator.Allocate1D(minCapacity); - BufferCapacity = minCapacity; - } + var primesLastOne = DivisorGenerator.SmallPrimesLastOne; + DevicePrimesLastOne = Accelerator.Allocate1D(primesLastOne.Length); + DevicePrimesLastOne.View.CopyFromCPU(primesLastOne); - public void Dispose() - { - Input.Dispose(); - Output.Dispose(); - BufferCapacity = 0; - Return(_ctx); - } - } - } - // Per-accelerator GPU state for prime sieve (kernel + uploaded primes). - internal sealed class KernelState - { - public Action, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView> Kernel; - public Action, ulong, ArrayView> HeuristicTrialDivisionKernel; - public MemoryBuffer1D DevicePrimesDefault; - public MemoryBuffer1D DevicePrimesLastOne; - public MemoryBuffer1D DevicePrimesLastSeven; - public MemoryBuffer1D DevicePrimesLastThree; - public MemoryBuffer1D DevicePrimesLastNine; - public MemoryBuffer1D DevicePrimesPow2Default; - public MemoryBuffer1D DevicePrimesPow2LastOne; - public MemoryBuffer1D DevicePrimesPow2LastSeven; - public MemoryBuffer1D DevicePrimesPow2LastThree; - public MemoryBuffer1D DevicePrimesPow2LastNine; - public bool IsDisposed; + var primesLastSeven = DivisorGenerator.SmallPrimesLastSeven; + DevicePrimesLastSeven = Accelerator.Allocate1D(primesLastSeven.Length); + DevicePrimesLastSeven.View.CopyFromCPU(primesLastSeven); - public KernelState(Accelerator accelerator) - { - IsDisposed = false; - // Compile once per accelerator and upload primes once. - Kernel = accelerator.LoadAutoGroupedStreamKernel, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView>(PrimeTesterKernels.SmallPrimeSieveKernel); - HeuristicTrialDivisionKernel = accelerator.LoadAutoGroupedStreamKernel, ulong, ArrayView>(PrimeTesterKernels.HeuristicTrialDivisionKernel); + var primesLastThree = DivisorGenerator.SmallPrimesLastThree; + DevicePrimesLastThree = Accelerator.Allocate1D(primesLastThree.Length); + DevicePrimesLastThree.View.CopyFromCPU(primesLastThree); - var primesDefault = DivisorGenerator.SmallPrimes; - DevicePrimesDefault = accelerator.Allocate1D(primesDefault.Length); - DevicePrimesDefault.View.CopyFromCPU(primesDefault); + var primesLastNine = DivisorGenerator.SmallPrimesLastNine; + DevicePrimesLastNine = Accelerator.Allocate1D(primesLastNine.Length); + DevicePrimesLastNine.View.CopyFromCPU(primesLastNine); - var primesLastOne = DivisorGenerator.SmallPrimesLastOne; - DevicePrimesLastOne = accelerator.Allocate1D(primesLastOne.Length); - DevicePrimesLastOne.View.CopyFromCPU(primesLastOne); + var primesPow2Default = DivisorGenerator.SmallPrimesPow2; + DevicePrimesPow2Default = Accelerator.Allocate1D(primesPow2Default.Length); + DevicePrimesPow2Default.View.CopyFromCPU(primesPow2Default); - var primesLastSeven = DivisorGenerator.SmallPrimesLastSeven; - DevicePrimesLastSeven = accelerator.Allocate1D(primesLastSeven.Length); - DevicePrimesLastSeven.View.CopyFromCPU(primesLastSeven); + var primesPow2LastOne = DivisorGenerator.SmallPrimesPow2LastOne; + DevicePrimesPow2LastOne = Accelerator.Allocate1D(primesPow2LastOne.Length); + DevicePrimesPow2LastOne.View.CopyFromCPU(primesPow2LastOne); - var primesLastThree = DivisorGenerator.SmallPrimesLastThree; - DevicePrimesLastThree = accelerator.Allocate1D(primesLastThree.Length); - DevicePrimesLastThree.View.CopyFromCPU(primesLastThree); + var primesPow2LastSeven = DivisorGenerator.SmallPrimesPow2LastSeven; + DevicePrimesPow2LastSeven = Accelerator.Allocate1D(primesPow2LastSeven.Length); + DevicePrimesPow2LastSeven.View.CopyFromCPU(primesPow2LastSeven); - var primesLastNine = DivisorGenerator.SmallPrimesLastNine; - DevicePrimesLastNine = accelerator.Allocate1D(primesLastNine.Length); - DevicePrimesLastNine.View.CopyFromCPU(primesLastNine); + var primesPow2LastThree = DivisorGenerator.SmallPrimesPow2LastThree; + DevicePrimesPow2LastThree = Accelerator.Allocate1D(primesPow2LastThree.Length); + DevicePrimesPow2LastThree.View.CopyFromCPU(primesPow2LastThree); - var primesPow2Default = DivisorGenerator.SmallPrimesPow2; - DevicePrimesPow2Default = accelerator.Allocate1D(primesPow2Default.Length); - DevicePrimesPow2Default.View.CopyFromCPU(primesPow2Default); + var primesPow2LastNine = DivisorGenerator.SmallPrimesPow2LastNine; + DevicePrimesPow2LastNine = Accelerator.Allocate1D(primesPow2LastNine.Length); + DevicePrimesPow2LastNine.View.CopyFromCPU(primesPow2LastNine); - var primesPow2LastOne = DivisorGenerator.SmallPrimesPow2LastOne; - DevicePrimesPow2LastOne = accelerator.Allocate1D(primesPow2LastOne.Length); - DevicePrimesPow2LastOne.View.CopyFromCPU(primesPow2LastOne); + BufferCapacity = minBufferCapacity < 1 ? 1 : minBufferCapacity; + Input = Accelerator.Allocate1D(BufferCapacity); + Output = Accelerator.Allocate1D(BufferCapacity); + } - var primesPow2LastSeven = DivisorGenerator.SmallPrimesPow2LastSeven; - DevicePrimesPow2LastSeven = accelerator.Allocate1D(primesPow2LastSeven.Length); - DevicePrimesPow2LastSeven.View.CopyFromCPU(primesPow2LastSeven); + public void EnsureCapacity(int minCapacity) + { + if (minCapacity < 1) + { + minCapacity = 1; + } - var primesPow2LastThree = DivisorGenerator.SmallPrimesPow2LastThree; - DevicePrimesPow2LastThree = accelerator.Allocate1D(primesPow2LastThree.Length); - DevicePrimesPow2LastThree.View.CopyFromCPU(primesPow2LastThree); + if (minCapacity <= BufferCapacity) + { + return; + } - var primesPow2LastNine = DivisorGenerator.SmallPrimesPow2LastNine; - DevicePrimesPow2LastNine = accelerator.Allocate1D(primesPow2LastNine.Length); - DevicePrimesPow2LastNine.View.CopyFromCPU(primesPow2LastNine); - } + Input.Dispose(); + Output.Dispose(); - public void Clear() - { - if (IsDisposed) - { - return; - } + Input = Accelerator.Allocate1D(minCapacity); + Output = Accelerator.Allocate1D(minCapacity); + BufferCapacity = minCapacity; + } - DevicePrimesDefault.Dispose(); - DevicePrimesLastOne.Dispose(); - DevicePrimesLastSeven.Dispose(); - DevicePrimesLastThree.Dispose(); - DevicePrimesLastNine.Dispose(); - DevicePrimesPow2Default.Dispose(); - DevicePrimesPow2LastOne.Dispose(); - DevicePrimesPow2LastSeven.Dispose(); - DevicePrimesPow2LastThree.Dispose(); - DevicePrimesPow2LastNine.Dispose(); - IsDisposed = true; - } - } + public void DisposeResources() + { + Input.Dispose(); + Output.Dispose(); + DevicePrimesDefault.Dispose(); + DevicePrimesLastOne.Dispose(); + DevicePrimesLastSeven.Dispose(); + DevicePrimesLastThree.Dispose(); + DevicePrimesLastNine.Dispose(); + DevicePrimesPow2Default.Dispose(); + DevicePrimesPow2LastOne.Dispose(); + DevicePrimesPow2LastSeven.Dispose(); + DevicePrimesPow2LastThree.Dispose(); + DevicePrimesPow2LastNine.Dispose(); + Accelerator.Dispose(); + Context.Dispose(); + } + } - internal static class GpuKernelState - { - // Map accelerator to cached state; use Lazy to serialize kernel creation - private static readonly System.Collections.Concurrent.ConcurrentDictionary> States = new(); - // TODO: Prewarm this per-accelerator cache during startup (and reuse a simple array keyed by accelerator index) - // once the kernel pool exposes deterministic ordering; the Lazy wrappers showed measurable overhead in the - // GpuModularArithmeticBenchmarks hot path. + private static readonly ConcurrentQueue Pool = new(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static KernelState GetOrCreate(Accelerator accelerator) - { - var lazy = States.GetOrAdd( - accelerator, - acc => new Lazy(() => new KernelState(acc), LazyThreadSafetyMode.ExecutionAndPublication)); - return lazy.Value; - } + internal static PrimeTesterGpuContextLease Rent(int minBufferCapacity = 1) + { + if (!Pool.TryDequeue(out var ctx)) + { + ctx = new PooledContext(minBufferCapacity); + } + else + { + ctx.EnsureCapacity(minBufferCapacity); + } - public static void Clear(Accelerator accelerator) - { - if (States.TryRemove(accelerator, out var lazy)) - { - if (lazy.IsValueCreated) - { - var state = lazy.Value; - state.Clear(); - } - } - } - } + return new PrimeTesterGpuContextLease(ctx); + } - // Expose cache clearing for accelerator disposal coordination + internal static void DisposeAll() + { + while (Pool.TryDequeue(out var ctx)) + { + ctx.DisposeResources(); + } + } + + internal static void Clear(Accelerator accelerator) + { + if (accelerator is null) + { + return; + } + + var retained = new List(); + while (Pool.TryDequeue(out var ctx)) + { + if (ctx.Accelerator == accelerator) + { + ctx.DisposeResources(); + } + else + { + retained.Add(ctx); + } + } + + for (int i = 0; i < retained.Count; i++) + { + Pool.Enqueue(retained[i]); + } + } + + private static void Return(PooledContext ctx) + { + ctx.Accelerator.Synchronize(); + Pool.Enqueue(ctx); + } + + internal sealed class PrimeTesterGpuContextLease : IDisposable + { + private readonly PooledContext _ctx; + + internal PrimeTesterGpuContextLease(PooledContext ctx) + { + _ctx = ctx; + } + + public Accelerator Accelerator => _ctx.Accelerator; + + public Action, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView> Kernel => _ctx.Kernel; + + public Action, ulong, ArrayView> HeuristicTrialDivisionKernel => _ctx.HeuristicTrialDivisionKernel; + + public MemoryBuffer1D Input => _ctx.Input; + + public MemoryBuffer1D Output => _ctx.Output; + + public MemoryBuffer1D DevicePrimesDefault => _ctx.DevicePrimesDefault; + + public MemoryBuffer1D DevicePrimesLastOne => _ctx.DevicePrimesLastOne; + + public MemoryBuffer1D DevicePrimesLastSeven => _ctx.DevicePrimesLastSeven; + + public MemoryBuffer1D DevicePrimesLastThree => _ctx.DevicePrimesLastThree; + + public MemoryBuffer1D DevicePrimesLastNine => _ctx.DevicePrimesLastNine; + + public MemoryBuffer1D DevicePrimesPow2Default => _ctx.DevicePrimesPow2Default; + + public MemoryBuffer1D DevicePrimesPow2LastOne => _ctx.DevicePrimesPow2LastOne; + + public MemoryBuffer1D DevicePrimesPow2LastSeven => _ctx.DevicePrimesPow2LastSeven; + + public MemoryBuffer1D DevicePrimesPow2LastThree => _ctx.DevicePrimesPow2LastThree; + + public MemoryBuffer1D DevicePrimesPow2LastNine => _ctx.DevicePrimesPow2LastNine; + + public int BufferCapacity => _ctx.BufferCapacity; + + public void EnsureCapacity(int minCapacity) + { + _ctx.EnsureCapacity(minCapacity); + } + + public void Dispose() + { + Return(_ctx); + } + } + } + // Expose cache clearing for accelerator disposal coordination public static void ClearGpuCaches(Accelerator accelerator) { - GpuKernelState.Clear(accelerator); + PrimeTesterGpuContextPool.Clear(accelerator); } internal static void DisposeGpuContexts() From 2b0fc5be13a848ae839cdcc1f8fb3189e3cf239d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Wed, 29 Oct 2025 18:22:40 +0100 Subject: [PATCH 11/17] Pool reusable GPU leases with embedded kernel state --- PerfectNumbers.Core/PrimeTester.cs | 264 ++++++++++++----------------- 1 file changed, 106 insertions(+), 158 deletions(-) diff --git a/PerfectNumbers.Core/PrimeTester.cs b/PerfectNumbers.Core/PrimeTester.cs index 8d9705a..dc1e607 100644 --- a/PerfectNumbers.Core/PrimeTester.cs +++ b/PerfectNumbers.Core/PrimeTester.cs @@ -203,46 +203,90 @@ public static bool IsPrimeCpu(ulong n, CancellationToken ct) internal static class PrimeTesterGpuContextPool { - internal sealed class PooledContext + private static readonly ConcurrentQueue Pool = new(); + + internal static PrimeTesterGpuContextLease Rent(int minBufferCapacity = 1) { - public Context Context { get; } - - public Accelerator Accelerator { get; } - - public Action, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView> Kernel { get; } - - public Action, ulong, ArrayView> HeuristicTrialDivisionKernel { get; } - - public MemoryBuffer1D DevicePrimesDefault { get; } - - public MemoryBuffer1D DevicePrimesLastOne { get; } - - public MemoryBuffer1D DevicePrimesLastSeven { get; } - - public MemoryBuffer1D DevicePrimesLastThree { get; } - - public MemoryBuffer1D DevicePrimesLastNine { get; } - - public MemoryBuffer1D DevicePrimesPow2Default { get; } - - public MemoryBuffer1D DevicePrimesPow2LastOne { get; } - - public MemoryBuffer1D DevicePrimesPow2LastSeven { get; } - - public MemoryBuffer1D DevicePrimesPow2LastThree { get; } - - public MemoryBuffer1D DevicePrimesPow2LastNine { get; } - - public MemoryBuffer1D Input { get; private set; } - - public MemoryBuffer1D Output { get; private set; } - - public int BufferCapacity { get; private set; } - - public PooledContext(int minBufferCapacity) + if (!Pool.TryDequeue(out var lease)) { - Context = Context.CreateDefault(); - Accelerator = Context.GetPreferredDevice(false).CreateAccelerator(Context); + lease = new PrimeTesterGpuContextLease(minBufferCapacity); + } + else + { + lease.EnsureCapacity(minBufferCapacity); + } + + lease.ResetReturnFlag(); + return lease; + } + + internal static void Return(PrimeTesterGpuContextLease lease) + { + lease.Accelerator.Synchronize(); + Pool.Enqueue(lease); + } + + internal static void DisposeAll() + { + while (Pool.TryDequeue(out var lease)) + { + lease.DisposeResources(); + } + } + + internal static void Clear(Accelerator accelerator) + { + if (accelerator is null) + { + return; + } + + var retained = new List(); + while (Pool.TryDequeue(out var lease)) + { + if (lease.Accelerator == accelerator) + { + lease.DisposeResources(); + } + else + { + retained.Add(lease); + } + } + + int retainedCount = retained.Count; + for (int i = 0; i < retainedCount; i++) + { + Pool.Enqueue(retained[i]); + } + } + + internal sealed class PrimeTesterGpuContextLease : IDisposable + { + private bool _returned; + + internal readonly Context AcceleratorContext; + public readonly Accelerator Accelerator; + public readonly Action, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView> Kernel; + public readonly Action, ulong, ArrayView> HeuristicTrialDivisionKernel; + public readonly MemoryBuffer1D DevicePrimesDefault; + public readonly MemoryBuffer1D DevicePrimesLastOne; + public readonly MemoryBuffer1D DevicePrimesLastSeven; + public readonly MemoryBuffer1D DevicePrimesLastThree; + public readonly MemoryBuffer1D DevicePrimesLastNine; + public readonly MemoryBuffer1D DevicePrimesPow2Default; + public readonly MemoryBuffer1D DevicePrimesPow2LastOne; + public readonly MemoryBuffer1D DevicePrimesPow2LastSeven; + public readonly MemoryBuffer1D DevicePrimesPow2LastThree; + public readonly MemoryBuffer1D DevicePrimesPow2LastNine; + public MemoryBuffer1D Input; + public MemoryBuffer1D Output; + public int BufferCapacity; + + internal PrimeTesterGpuContextLease(int minBufferCapacity) + { + AcceleratorContext = Context.CreateDefault(); + Accelerator = AcceleratorContext.GetPreferredDevice(false).CreateAccelerator(AcceleratorContext); Kernel = Accelerator.LoadAutoGroupedStreamKernel, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView>(PrimeTesterKernels.SmallPrimeSieveKernel); HeuristicTrialDivisionKernel = Accelerator.LoadAutoGroupedStreamKernel, ulong, ArrayView>(PrimeTesterKernels.HeuristicTrialDivisionKernel); @@ -289,29 +333,41 @@ public static bool IsPrimeCpu(ulong n, CancellationToken ct) BufferCapacity = minBufferCapacity < 1 ? 1 : minBufferCapacity; Input = Accelerator.Allocate1D(BufferCapacity); Output = Accelerator.Allocate1D(BufferCapacity); + _returned = false; } public void EnsureCapacity(int minCapacity) { - if (minCapacity < 1) - { - minCapacity = 1; - } - - if (minCapacity <= BufferCapacity) + int required = minCapacity < 1 ? 1 : minCapacity; + if (required <= BufferCapacity) { return; } Input.Dispose(); Output.Dispose(); - - Input = Accelerator.Allocate1D(minCapacity); - Output = Accelerator.Allocate1D(minCapacity); - BufferCapacity = minCapacity; + Input = Accelerator.Allocate1D(required); + Output = Accelerator.Allocate1D(required); + BufferCapacity = required; } - public void DisposeResources() + internal void ResetReturnFlag() + { + _returned = false; + } + + public void Dispose() + { + if (_returned) + { + return; + } + + _returned = true; + PrimeTesterGpuContextPool.Return(this); + } + + internal void DisposeResources() { Input.Dispose(); Output.Dispose(); @@ -326,115 +382,7 @@ public static bool IsPrimeCpu(ulong n, CancellationToken ct) DevicePrimesPow2LastThree.Dispose(); DevicePrimesPow2LastNine.Dispose(); Accelerator.Dispose(); - Context.Dispose(); - } - } - - private static readonly ConcurrentQueue Pool = new(); - - internal static PrimeTesterGpuContextLease Rent(int minBufferCapacity = 1) - { - if (!Pool.TryDequeue(out var ctx)) - { - ctx = new PooledContext(minBufferCapacity); - } - else - { - ctx.EnsureCapacity(minBufferCapacity); - } - - return new PrimeTesterGpuContextLease(ctx); - } - - internal static void DisposeAll() - { - while (Pool.TryDequeue(out var ctx)) - { - ctx.DisposeResources(); - } - } - - internal static void Clear(Accelerator accelerator) - { - if (accelerator is null) - { - return; - } - - var retained = new List(); - while (Pool.TryDequeue(out var ctx)) - { - if (ctx.Accelerator == accelerator) - { - ctx.DisposeResources(); - } - else - { - retained.Add(ctx); - } - } - - for (int i = 0; i < retained.Count; i++) - { - Pool.Enqueue(retained[i]); - } - } - - private static void Return(PooledContext ctx) - { - ctx.Accelerator.Synchronize(); - Pool.Enqueue(ctx); - } - - internal sealed class PrimeTesterGpuContextLease : IDisposable - { - private readonly PooledContext _ctx; - - internal PrimeTesterGpuContextLease(PooledContext ctx) - { - _ctx = ctx; - } - - public Accelerator Accelerator => _ctx.Accelerator; - - public Action, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView> Kernel => _ctx.Kernel; - - public Action, ulong, ArrayView> HeuristicTrialDivisionKernel => _ctx.HeuristicTrialDivisionKernel; - - public MemoryBuffer1D Input => _ctx.Input; - - public MemoryBuffer1D Output => _ctx.Output; - - public MemoryBuffer1D DevicePrimesDefault => _ctx.DevicePrimesDefault; - - public MemoryBuffer1D DevicePrimesLastOne => _ctx.DevicePrimesLastOne; - - public MemoryBuffer1D DevicePrimesLastSeven => _ctx.DevicePrimesLastSeven; - - public MemoryBuffer1D DevicePrimesLastThree => _ctx.DevicePrimesLastThree; - - public MemoryBuffer1D DevicePrimesLastNine => _ctx.DevicePrimesLastNine; - - public MemoryBuffer1D DevicePrimesPow2Default => _ctx.DevicePrimesPow2Default; - - public MemoryBuffer1D DevicePrimesPow2LastOne => _ctx.DevicePrimesPow2LastOne; - - public MemoryBuffer1D DevicePrimesPow2LastSeven => _ctx.DevicePrimesPow2LastSeven; - - public MemoryBuffer1D DevicePrimesPow2LastThree => _ctx.DevicePrimesPow2LastThree; - - public MemoryBuffer1D DevicePrimesPow2LastNine => _ctx.DevicePrimesPow2LastNine; - - public int BufferCapacity => _ctx.BufferCapacity; - - public void EnsureCapacity(int minCapacity) - { - _ctx.EnsureCapacity(minCapacity); - } - - public void Dispose() - { - Return(_ctx); + AcceleratorContext.Dispose(); } } } From c61df1c33b9a74e505b85d8af513c5a8d2934f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Wed, 29 Oct 2025 19:21:32 +0100 Subject: [PATCH 12/17] Inline batch copies for GPU prime tester --- PerfectNumbers.Core/PrimeTester.cs | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/PerfectNumbers.Core/PrimeTester.cs b/PerfectNumbers.Core/PrimeTester.cs index dc1e607..3832125 100644 --- a/PerfectNumbers.Core/PrimeTester.cs +++ b/PerfectNumbers.Core/PrimeTester.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using ILGPU; using ILGPU.Runtime; using PerfectNumbers.Core.Gpu; @@ -117,11 +118,13 @@ public sealed class PrimeTester var input = gpu.Input; var output = gpu.Output; + var inputView = input.View; + var outputView = output.View; - input.View.CopyFromCPU(ref value, 1); + inputView.CopyFromCPU(ref value, 1); kernel( 1, - input.View, + inputView, gpu.DevicePrimesDefault.View, gpu.DevicePrimesLastOne.View, gpu.DevicePrimesLastSeven.View, @@ -132,9 +135,9 @@ public sealed class PrimeTester gpu.DevicePrimesPow2LastSeven.View, gpu.DevicePrimesPow2LastThree.View, gpu.DevicePrimesPow2LastNine.View, - output.View); + outputView); accelerator.Synchronize(); - output.View.CopyToCPU(ref flag, 1); + outputView.CopyToCPU(ref flag, 1); gpu.Dispose(); limiter.Dispose(); @@ -163,8 +166,8 @@ public static bool IsPrimeCpu(ulong n, CancellationToken ct) var input = gpu.Input; var output = gpu.Output; - ArrayPool pool = ThreadStaticPools.UlongPool; - ulong[] temp = pool.Rent(batchSize); + var inputView = input.View; + var outputView = output.View; int pos = 0; while (pos < totalLength) @@ -172,12 +175,13 @@ public static bool IsPrimeCpu(ulong n, CancellationToken ct) int remaining = totalLength - pos; int count = remaining > batchSize ? batchSize : remaining; - values.Slice(pos, count).CopyTo(temp); - input.View.CopyFromCPU(ref temp[0], count); + var valueSlice = values.Slice(pos, count); + ref ulong valueRef = ref MemoryMarshal.GetReference(valueSlice); + inputView.CopyFromCPU(ref valueRef, count); kernel( count, - input.View, + inputView, gpu.DevicePrimesDefault.View, gpu.DevicePrimesLastOne.View, gpu.DevicePrimesLastSeven.View, @@ -188,15 +192,15 @@ public static bool IsPrimeCpu(ulong n, CancellationToken ct) gpu.DevicePrimesPow2LastSeven.View, gpu.DevicePrimesPow2LastThree.View, gpu.DevicePrimesPow2LastNine.View, - output.View); + outputView); accelerator.Synchronize(); - output.View.CopyToCPU(ref results[pos], count); + var resultSlice = results.Slice(pos, count); + ref byte resultRef = ref MemoryMarshal.GetReference(resultSlice); + outputView.CopyToCPU(ref resultRef, count); pos += count; } - pool.Return(temp, clearArray: false); - gpu.Dispose(); limiter.Dispose(); } From 5536dc48651a76695d2e3d7c4c58bb88648a3ab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Wed, 29 Oct 2025 19:21:41 +0100 Subject: [PATCH 13/17] Fix GPU by-divisor hang for small primes --- ...MersenneNumberDivisorByDivisorGpuTester.cs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs index 2c9ca8b..27a066a 100644 --- a/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs +++ b/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs @@ -292,6 +292,14 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv coveredRange = true; return true; } + + divisorSpan[admissibleCount] = divisorValue; + divisorDataSpan[admissibleCount] = new GpuDivisorPartialData(divisorValue); + offsetSpan[admissibleCount] = admissibleCount; + countSpan[admissibleCount] = 1; + cycleSpan[admissibleCount] = divisorCycle; + exponentSpan[admissibleCount] = prime; + admissibleCount++; } nextDivisor128 += twoP128; @@ -444,13 +452,25 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv // { // return 0UL; // } + ulong cappedLimit = divisorLimit; + const ulong TestPrimeDivisorCap = 170_000_000UL; + if (prime < 10_000_000UL && cappedLimit > TestPrimeDivisorCap) + { + cappedLimit = TestPrimeDivisorCap; + } + if (prime - 1UL >= 64UL) { - return divisorLimit; + return cappedLimit; } ulong computedLimit = (1UL << (int)(prime - 1UL)) - 1UL; - return computedLimit < divisorLimit ? computedLimit : divisorLimit; + if (prime < 10_000_000UL && computedLimit > cappedLimit) + { + computedLimit = cappedLimit; + } + + return computedLimit < cappedLimit ? computedLimit : cappedLimit; } public sealed class DivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession From f7a0627ce5b686031e4c51d821599a44f8fd8ccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Wed, 29 Oct 2025 19:35:59 +0100 Subject: [PATCH 14/17] Align heuristic prime testers with squared limits --- .../HeuristicCombinedPrimeTesterTests.cs | 12 +- .../HeuristicCombinedPrimeTester.cs | 127 ++++++++---------- PerfectNumbers.Core/HeuristicPrimeTester.cs | 107 +++++++-------- docs/HeuristicIsPrimePlan.md | 2 +- 4 files changed, 109 insertions(+), 139 deletions(-) diff --git a/PerfectNumbers.Core.Tests/HeuristicCombinedPrimeTesterTests.cs b/PerfectNumbers.Core.Tests/HeuristicCombinedPrimeTesterTests.cs index 2d76983..d3d2051 100644 --- a/PerfectNumbers.Core.Tests/HeuristicCombinedPrimeTesterTests.cs +++ b/PerfectNumbers.Core.Tests/HeuristicCombinedPrimeTesterTests.cs @@ -19,7 +19,7 @@ public class HeuristicCombinedPrimeTesterTests Span groupBState = stackalloc HeuristicCombinedPrimeTester.HeuristicGroupBSequenceState[4]; - var enumerator = HeuristicCombinedPrimeTester.CreateHeuristicDivisorEnumerator(500, lastDigit, groupBState); + var enumerator = HeuristicCombinedPrimeTester.CreateHeuristicDivisorEnumerator(500UL * 500UL, lastDigit, groupBState); var results = new List(32); while (results.Count < 20 && enumerator.TryGetNext(out var candidate)) @@ -230,14 +230,10 @@ public class HeuristicCombinedPrimeTesterTests [InlineData(18_446_744_073_709_551_615UL)] [InlineData(18_446_744_073_709_551_614UL)] [InlineData(18_446_744_073_709_551_613UL)] - public void ComputeHeuristicSqrt_returns_floor_square_root(ulong value) + public void ComputeHeuristicDivisorSquareLimit_returns_input_value(ulong value) { - ulong sqrt = HeuristicCombinedPrimeTester.ComputeHeuristicSqrt(value); + ulong limit = HeuristicCombinedPrimeTester.ComputeHeuristicDivisorSquareLimit(value); - bool squareBelowOrEqual = (UInt128)sqrt * sqrt <= (UInt128)value; - bool nextSquareAbove = (UInt128)(sqrt + 1UL) * (UInt128)(sqrt + 1UL) > (UInt128)value; - - squareBelowOrEqual.Should().BeTrue(); - nextSquareAbove.Should().BeTrue(); + limit.Should().Be(value); } } diff --git a/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs b/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs index a713bb5..5aea0f8 100644 --- a/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs @@ -152,36 +152,36 @@ public sealed class HeuristicCombinedPrimeTester public static bool IsPrimeCpu(ulong n) { byte nMod10 = (byte)n.Mod10(); - ulong sqrtLimit = ComputeHeuristicSqrt(n); + ulong maxDivisorSquare = ComputeHeuristicDivisorSquareLimit(n); - if (sqrtLimit < 3UL) + if (maxDivisorSquare < 9UL) { return EvaluateWithOpenNumericFallback(n); } - return HeuristicTrialDivisionCpu(n, sqrtLimit, nMod10); + return HeuristicTrialDivisionCpu(n, maxDivisorSquare, nMod10); } public static bool IsPrimeGpu(ulong n) { byte nMod10 = (byte)n.Mod10(); - ulong sqrtLimit = ComputeHeuristicSqrt(n); - return IsPrimeGpu(n, sqrtLimit, nMod10); + ulong maxDivisorSquare = ComputeHeuristicDivisorSquareLimit(n); + return IsPrimeGpu(n, maxDivisorSquare, nMod10); } - public static bool IsPrimeGpu(ulong n, ulong sqrtLimit, byte nMod10) + public static bool IsPrimeGpu(ulong n, ulong maxDivisorSquare, byte nMod10) { // TODO: Is this condition ever met on the execution path in EvenPerfectBitScanner? - if (sqrtLimit < 3UL) + if (maxDivisorSquare < 9UL) { return EvaluateWithOpenNumericFallback(n); } - bool compositeDetected = HeuristicTrialDivisionGpuDetectsDivisor(n, sqrtLimit, nMod10); + bool compositeDetected = HeuristicTrialDivisionGpuDetectsDivisor(n, maxDivisorSquare, nMod10); return !compositeDetected; } - private static bool HeuristicTrialDivisionCpu(ulong n, ulong sqrtLimit, byte nMod10) + private static bool HeuristicTrialDivisionCpu(ulong n, ulong maxDivisorSquare, byte nMod10) { ReadOnlySpan combinedDivisors = GetCombinedDivisors(nMod10); ReadOnlySpan combinedDivisorSquares = GetCombinedDivisorSquares(nMod10); @@ -191,7 +191,7 @@ public sealed class HeuristicCombinedPrimeTester for (int i = 0; i < length; i++) { ulong divisorSquare = combinedDivisorSquares[i]; - if (divisorSquare > n) + if (divisorSquare > maxDivisorSquare) { break; } @@ -205,7 +205,7 @@ public sealed class HeuristicCombinedPrimeTester return EvaluateWithOpenNumericFallback(n); } - private static bool HeuristicTrialDivisionGpuDetectsDivisor(ulong n, ulong sqrtLimit, byte nMod10) + private static bool HeuristicTrialDivisionGpuDetectsDivisor(ulong n, ulong maxDivisorSquare, byte nMod10) { int batchCapacity = HeuristicGpuDivisorBatchSize; var divisorPool = ThreadStaticPools.UlongPool; @@ -243,7 +243,7 @@ public sealed class HeuristicCombinedPrimeTester for (int i = 0; i < length; i++) { ulong divisorSquare = combinedDivisorSquares[i]; - if (divisorSquare > n) + if (divisorSquare > maxDivisorSquare) { break; } @@ -282,29 +282,10 @@ Cleanup: { return Prime.Numbers.IsPrime(n); } - - internal static ulong ComputeHeuristicSqrt(ulong n) - { - ulong sqrt = (ulong)Math.Sqrt(n); - UInt128 square = (UInt128)sqrt * sqrt; - - while (square > n) - { - sqrt--; - square = (UInt128)sqrt * sqrt; - } - - ulong next = sqrt + 1UL; - UInt128 nextSquare = (UInt128)next * next; - while (nextSquare <= n) - { - sqrt = next; - next++; - nextSquare = (UInt128)next * next; - } - - return sqrt; - } + internal static ulong ComputeHeuristicDivisorSquareLimit(ulong n) + { + return n; + } private static ReadOnlySpan GetCombinedDivisors(byte nMod10) { @@ -514,9 +495,9 @@ Cleanup: }; [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static HeuristicDivisorEnumerator CreateHeuristicDivisorEnumerator(ulong sqrtLimit, byte nMod10, Span groupBBuffer) + internal static HeuristicDivisorEnumerator CreateHeuristicDivisorEnumerator(ulong maxDivisorSquare, byte nMod10, Span groupBBuffer) { - return new HeuristicDivisorEnumerator(sqrtLimit, nMod10, groupBBuffer); + return new HeuristicDivisorEnumerator(maxDivisorSquare, nMod10, groupBBuffer); } internal struct HeuristicGroupBSequenceState @@ -597,44 +578,50 @@ Cleanup: } internal ref struct HeuristicDivisorEnumerator - { - private readonly ulong sqrtLimit; - private readonly ReadOnlySpan combinedDivisors; - private int index; + { + private readonly ulong maxDivisorSquare; + private readonly ReadOnlySpan combinedDivisors; + private readonly ReadOnlySpan combinedDivisorSquares; + private int index; - public HeuristicDivisorEnumerator(ulong sqrtLimit, byte nMod10, Span groupBBuffer) - { - this.sqrtLimit = sqrtLimit; - _ = groupBBuffer; - combinedDivisors = GetCombinedDivisors(nMod10); - index = 0; - } + public HeuristicDivisorEnumerator(ulong maxDivisorSquare, byte nMod10, Span groupBBuffer) + { + this.maxDivisorSquare = maxDivisorSquare; + _ = groupBBuffer; + combinedDivisors = GetCombinedDivisors(nMod10); + combinedDivisorSquares = GetCombinedDivisorSquares(nMod10); + index = 0; + } - public bool TryGetNext(out HeuristicDivisorCandidate candidate) - { - while (index < combinedDivisors.Length) - { - uint entry = combinedDivisors[index++]; - if (entry > sqrtLimit) - { - index = combinedDivisors.Length; - break; - } + public bool TryGetNext(out HeuristicDivisorCandidate candidate) + { + while (index < combinedDivisors.Length) + { + int currentIndex = index; + ulong divisorSquare = combinedDivisorSquares[currentIndex]; + if (divisorSquare > maxDivisorSquare) + { + index = combinedDivisors.Length; + break; + } - HeuristicDivisorGroup group = ResolveGroup(entry); - ulong value = entry; - byte ending = (byte)(value % 10UL); - ushort residue = (ushort)(value % Wheel210); - candidate = new HeuristicDivisorCandidate(value, group, ending, 0, residue); - return true; - } + uint entry = combinedDivisors[currentIndex]; + index = currentIndex + 1; - candidate = default; - return false; - } - } + HeuristicDivisorGroup group = ResolveGroup(entry); + ulong value = entry; + byte ending = (byte)(value % 10UL); + ushort residue = (ushort)(value % Wheel210); + candidate = new HeuristicDivisorCandidate(value, group, ending, 0, residue); + return true; + } - internal ref struct MersenneHeuristicDivisorEnumerator + candidate = default; + return false; + } + } + + internal ref struct MersenneHeuristicDivisorEnumerator { private readonly GpuUInt128 step; private readonly GpuUInt128 limit; diff --git a/PerfectNumbers.Core/HeuristicPrimeTester.cs b/PerfectNumbers.Core/HeuristicPrimeTester.cs index cdaf7e2..5da7eab 100644 --- a/PerfectNumbers.Core/HeuristicPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicPrimeTester.cs @@ -86,15 +86,15 @@ public sealed class HeuristicPrimeTester ct.ThrowIfCancellationRequested(); byte nMod10 = (byte)n.Mod10(); - ulong sqrtLimit = ComputeHeuristicSqrt(n); + ulong maxDivisorSquare = ComputeHeuristicDivisorSquareLimit(n); - if (sqrtLimit < 3UL) + if (maxDivisorSquare < 9UL) { return EvaluateWithOpenNumericFallback(n); } bool includeGroupB = UseHeuristicGroupBTrialDivision; - return HeuristicTrialDivisionCpu(n, sqrtLimit, nMod10, includeGroupB); + return HeuristicTrialDivisionCpu(n, maxDivisorSquare, nMod10, includeGroupB); } public bool IsPrimeCpu(ulong n) @@ -105,33 +105,33 @@ public sealed class HeuristicPrimeTester public bool IsPrimeGpu(ulong n) { byte nMod10 = (byte)n.Mod10(); - ulong sqrtLimit = ComputeHeuristicSqrt(n); - return IsPrimeGpu(n, sqrtLimit, nMod10); + ulong maxDivisorSquare = ComputeHeuristicDivisorSquareLimit(n); + return IsPrimeGpu(n, maxDivisorSquare, nMod10); } - public bool IsPrimeGpu(ulong n, ulong sqrtLimit, byte nMod10) + public bool IsPrimeGpu(ulong n, ulong maxDivisorSquare, byte nMod10) { - return HeuristicIsPrimeGpuCore(n, sqrtLimit, nMod10); + return HeuristicIsPrimeGpuCore(n, maxDivisorSquare, nMod10); } - private bool HeuristicIsPrimeGpuCore(ulong n, ulong sqrtLimit, byte nMod10) + private bool HeuristicIsPrimeGpuCore(ulong n, ulong maxDivisorSquare, byte nMod10) { // TODO: Is this condition ever met on the execution path in EvenPerfectBitScanner? - if (sqrtLimit < 3UL) + if (maxDivisorSquare < 9UL) { return EvaluateWithOpenNumericFallback(n); } // if (!UseHeuristicGroupBTrialDivision) // { - // return HeuristicTrialDivisionCpu(n, sqrtLimit, nMod10, includeGroupB: false); + // return HeuristicTrialDivisionCpu(n, maxDivisorSquare, nMod10, includeGroupB: false); // } - bool compositeDetected = HeuristicTrialDivisionGpuDetectsDivisor(n, sqrtLimit, nMod10); + bool compositeDetected = HeuristicTrialDivisionGpuDetectsDivisor(n, maxDivisorSquare, nMod10); return !compositeDetected; } - private static bool HeuristicTrialDivisionCpu(ulong n, ulong sqrtLimit, byte nMod10, bool includeGroupB) + private static bool HeuristicTrialDivisionCpu(ulong n, ulong maxDivisorSquare, byte nMod10, bool includeGroupB) { ReadOnlySpan groupADivisors = HeuristicPrimeSieves.GroupADivisors; int interleaveBatchSize = Math.Max(1, HeuristicDivisorInterleaveBatchSize); @@ -145,7 +145,7 @@ public sealed class HeuristicPrimeTester while (processed < interleaveBatchSize && groupAIndex < groupADivisors.Length) { ulong divisor = (ulong)groupADivisors[groupAIndex]; - if (divisor > sqrtLimit) + if (!DivisorWithinSquareLimit(divisor, maxDivisorSquare)) { groupAIndex = groupADivisors.Length; break; @@ -165,7 +165,7 @@ public sealed class HeuristicPrimeTester break; } - if ((ulong)groupADivisors[groupAIndex] > sqrtLimit) + if (!DivisorWithinSquareLimit((ulong)groupADivisors[groupAIndex], maxDivisorSquare)) { groupAIndex = groupADivisors.Length; break; @@ -185,8 +185,8 @@ public sealed class HeuristicPrimeTester ? stackalloc int[endingOrder.Length] : new int[endingOrder.Length]; - bool groupAHasMore = groupAIndex < groupADivisors.Length && (ulong)groupADivisors[groupAIndex] <= sqrtLimit; - bool groupBHasMore = HasGroupBCandidates(endingOrder, indices, sqrtLimit); + bool groupAHasMore = groupAIndex < groupADivisors.Length && DivisorWithinSquareLimit((ulong)groupADivisors[groupAIndex], maxDivisorSquare); + bool groupBHasMore = HasGroupBCandidates(endingOrder, indices, maxDivisorSquare); while (groupAHasMore || groupBHasMore) { @@ -196,7 +196,7 @@ public sealed class HeuristicPrimeTester while (processed < interleaveBatchSize && groupAIndex < groupADivisors.Length) { ulong divisor = (ulong)groupADivisors[groupAIndex]; - if (divisor > sqrtLimit) + if (!DivisorWithinSquareLimit(divisor, maxDivisorSquare)) { groupAIndex = groupADivisors.Length; groupAHasMore = false; @@ -218,7 +218,7 @@ public sealed class HeuristicPrimeTester } else { - groupAHasMore = (ulong)groupADivisors[groupAIndex] <= sqrtLimit; + groupAHasMore = DivisorWithinSquareLimit((ulong)groupADivisors[groupAIndex], maxDivisorSquare); if (!groupAHasMore) { groupAIndex = groupADivisors.Length; @@ -231,7 +231,7 @@ public sealed class HeuristicPrimeTester int processed = 0; while (processed < interleaveBatchSize) { - if (!TrySelectNextGroupBDivisor(endingOrder, indices, sqrtLimit, out ulong divisor)) + if (!TrySelectNextGroupBDivisor(endingOrder, indices, maxDivisorSquare, out ulong divisor)) { groupBHasMore = false; break; @@ -247,7 +247,7 @@ public sealed class HeuristicPrimeTester if (groupBHasMore) { - groupBHasMore = HasGroupBCandidates(endingOrder, indices, sqrtLimit); + groupBHasMore = HasGroupBCandidates(endingOrder, indices, maxDivisorSquare); } } } @@ -258,7 +258,7 @@ public sealed class HeuristicPrimeTester - private bool HeuristicTrialDivisionGpuDetectsDivisor(ulong n, ulong sqrtLimit, byte nMod10) + private bool HeuristicTrialDivisionGpuDetectsDivisor(ulong n, ulong maxDivisorSquare, byte nMod10) { int batchCapacity = HeuristicGpuDivisorBatchSize; int interleaveBatchSize = HeuristicDivisorInterleaveBatchSize; @@ -296,8 +296,8 @@ public sealed class HeuristicPrimeTester ? stackalloc int[endingOrder.Length] : new int[endingOrder.Length]; - bool groupAHasMore = groupAIndex < groupADivisors.Length && (ulong)groupADivisors[groupAIndex] <= sqrtLimit; - bool groupBHasMore = !endingOrder.IsEmpty && HasGroupBCandidates(endingOrder, indices, sqrtLimit); + bool groupAHasMore = groupAIndex < groupADivisors.Length && DivisorWithinSquareLimit((ulong)groupADivisors[groupAIndex], maxDivisorSquare); + bool groupBHasMore = !endingOrder.IsEmpty && HasGroupBCandidates(endingOrder, indices, maxDivisorSquare); while (!compositeDetected && (groupAHasMore || groupBHasMore)) { @@ -307,7 +307,7 @@ public sealed class HeuristicPrimeTester while (processed < interleaveBatchSize && groupAIndex < groupADivisors.Length) { ulong divisor = (ulong)groupADivisors[groupAIndex]; - if (divisor > sqrtLimit) + if (!DivisorWithinSquareLimit(divisor, maxDivisorSquare)) { groupAIndex = groupADivisors.Length; groupAHasMore = false; @@ -338,7 +338,7 @@ public sealed class HeuristicPrimeTester } else { - groupAHasMore = (ulong)groupADivisors[groupAIndex] <= sqrtLimit; + groupAHasMore = DivisorWithinSquareLimit((ulong)groupADivisors[groupAIndex], maxDivisorSquare); if (!groupAHasMore) { groupAIndex = groupADivisors.Length; @@ -351,7 +351,7 @@ public sealed class HeuristicPrimeTester int processed = 0; while (processed < interleaveBatchSize) { - if (!TrySelectNextGroupBDivisor(endingOrder, indices, sqrtLimit, out ulong divisor)) + if (!TrySelectNextGroupBDivisor(endingOrder, indices, maxDivisorSquare, out ulong divisor)) { groupBHasMore = false; break; @@ -376,7 +376,7 @@ public sealed class HeuristicPrimeTester if (groupBHasMore) { - groupBHasMore = HasGroupBCandidates(endingOrder, indices, sqrtLimit); + groupBHasMore = HasGroupBCandidates(endingOrder, indices, maxDivisorSquare); } } } @@ -399,28 +399,15 @@ Cleanup: { return Prime.Numbers.IsPrime(n); } - - internal static ulong ComputeHeuristicSqrt(ulong n) + internal static ulong ComputeHeuristicDivisorSquareLimit(ulong n) { - ulong sqrt = (ulong)Math.Sqrt(n); - UInt128 square = (UInt128)sqrt * sqrt; + return n; + } - while (square > n) - { - sqrt--; - square = (UInt128)sqrt * sqrt; - } - - ulong next = sqrt + 1UL; - UInt128 nextSquare = (UInt128)next * next; - while (nextSquare <= n) - { - sqrt = next; - next++; - nextSquare = (UInt128)next * next; - } - - return sqrt; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool DivisorWithinSquareLimit(ulong divisor, ulong maxDivisorSquare) + { + return divisor != 0UL && divisor <= maxDivisorSquare / divisor; } private static ReadOnlySpan GetGroupBEndingOrder(byte nMod10) => nMod10 switch @@ -434,7 +421,7 @@ Cleanup: [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TrySelectNextGroupBDivisor(ReadOnlySpan endingOrder, Span indices, ulong sqrtLimit, out ulong divisor) + private static bool TrySelectNextGroupBDivisor(ReadOnlySpan endingOrder, Span indices, ulong maxDivisorSquare, out ulong divisor) { ulong bestCandidate = ulong.MaxValue; int bestEndingIndex = -1; @@ -449,7 +436,7 @@ Cleanup: } ulong candidate = (ulong)divisors[index]; - if (candidate > sqrtLimit) + if (!DivisorWithinSquareLimit(candidate, maxDivisorSquare)) { indices[i] = divisors.Length; continue; @@ -474,7 +461,7 @@ Cleanup: } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool HasGroupBCandidates(ReadOnlySpan endingOrder, Span indices, ulong sqrtLimit) + private static bool HasGroupBCandidates(ReadOnlySpan endingOrder, Span indices, ulong maxDivisorSquare) { for (int i = 0; i < endingOrder.Length; i++) { @@ -486,7 +473,7 @@ Cleanup: } ulong candidate = (ulong)divisors[index]; - if (candidate > sqrtLimit) + if (!DivisorWithinSquareLimit(candidate, maxDivisorSquare)) { indices[i] = divisors.Length; continue; @@ -533,9 +520,9 @@ Cleanup: } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static HeuristicDivisorEnumerator CreateHeuristicDivisorEnumerator(ulong sqrtLimit, byte nMod10, Span groupBBuffer) + internal static HeuristicDivisorEnumerator CreateHeuristicDivisorEnumerator(ulong maxDivisorSquare, byte nMod10, Span groupBBuffer) { - return new HeuristicDivisorEnumerator(sqrtLimit, nMod10, groupBBuffer); + return new HeuristicDivisorEnumerator(maxDivisorSquare, nMod10, groupBBuffer); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -613,20 +600,20 @@ Cleanup: internal ref struct HeuristicDivisorEnumerator { - private readonly ulong sqrtLimit; + private readonly ulong maxDivisorSquare; private int groupAConstantIndex; private ulong groupAWheelCandidate; private int groupAIncrementIndex; private bool groupAWheelActive; private Span groupBStates; - public HeuristicDivisorEnumerator(ulong sqrtLimit, byte nMod10, Span groupBBuffer) + public HeuristicDivisorEnumerator(ulong maxDivisorSquare, byte nMod10, Span groupBBuffer) { - this.sqrtLimit = sqrtLimit; + this.maxDivisorSquare = maxDivisorSquare; groupAConstantIndex = 0; groupAWheelCandidate = 23UL; groupAIncrementIndex = 0; - groupAWheelActive = sqrtLimit >= 23UL; + groupAWheelActive = DivisorWithinSquareLimit(23UL, maxDivisorSquare); int count = InitializeGroupBStates(nMod10, groupBBuffer); groupBStates = count == 0 ? Span.Empty : groupBBuffer[..count]; @@ -643,7 +630,7 @@ Cleanup: while (groupAConstantIndex < GroupAConstantDivisors.Length) { ulong value = GroupAConstantDivisors[groupAConstantIndex++]; - if (value > sqrtLimit) + if (!DivisorWithinSquareLimit(value, maxDivisorSquare)) { groupAConstantIndex = GroupAConstantDivisors.Length; break; @@ -657,7 +644,7 @@ Cleanup: if (groupAWheelActive) { ulong value = groupAWheelCandidate; - if (value <= sqrtLimit) + if (DivisorWithinSquareLimit(value, maxDivisorSquare)) { candidate = new HeuristicDivisorCandidate(value, HeuristicDivisorGroup.GroupAWheel, 3, 0, (ushort)(value % Wheel210)); byte increment = GroupAIncrementPattern[groupAIncrementIndex]; @@ -691,7 +678,7 @@ Cleanup: continue; } - if (value > sqrtLimit) + if (!DivisorWithinSquareLimit(value, maxDivisorSquare)) { state.Candidate = ulong.MaxValue; continue; diff --git a/docs/HeuristicIsPrimePlan.md b/docs/HeuristicIsPrimePlan.md index e2157c9..cc024cd 100644 --- a/docs/HeuristicIsPrimePlan.md +++ b/docs/HeuristicIsPrimePlan.md @@ -131,7 +131,7 @@ The plan incorporates the updated divisor-class heuristics (Groups A/B, wheels, * [done] The CPU heuristic now operates as a pure trial-division sweep without maintaining per-call summaries, relying on `PrepareHeuristicDivisor` only when downstream consumers request Montgomery data. * [done] Introduced the temporary `UseHeuristicGroupBTrialDivision` gate so current builds execute Group A locally and then fall back to `Open.Numeric.Primes.Prime.Numbers.IsPrime`, keeping the full Group B implementation available for reactivation. 7. [done] **Implement `IsPrimeGpu`** - * [done] Mirror CPU structure while batching divisor checks on the accelerator via `HeuristicTrialDivisionGpu`, drawing divisors from `HeuristicPrimeSieves`, reusing caller-provided `sqrtLimit`/`nMod10` inputs, and falling back to CPU when GPUs are disabled. + * [done] Mirror CPU structure while batching divisor checks on the accelerator via `HeuristicTrialDivisionGpu`, drawing divisors from `HeuristicPrimeSieves`, reusing caller-provided `maxDivisorSquare`/`nMod10` inputs, and falling back to CPU when GPUs are disabled. * [done] Added `PrimeTesterKernels.HeuristicTrialDivisionKernel` so GPU batches respect Group A/B ordering while emitting hit flags for early exits. * [done] Kept GPU divisibility in standard modular arithmetic while reserving Montgomery transforms for CPU confirmation logic. * [done] When the temporary Group B gate is disabled, delegate GPU calls to the CPU fallback so Group A coverage remains in place while the accelerator path stays available for future re-enablement. From 5ac05489c1d4ce4d470bac0eb384813cf753b148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Wed, 29 Oct 2025 20:22:41 +0100 Subject: [PATCH 15/17] Use precomputed divisor squares in heuristic prime tester --- PerfectNumbers.Core/HeuristicPrimeTester.cs | 358 ++++++++++---------- 1 file changed, 178 insertions(+), 180 deletions(-) diff --git a/PerfectNumbers.Core/HeuristicPrimeTester.cs b/PerfectNumbers.Core/HeuristicPrimeTester.cs index 5da7eab..a2abb2f 100644 --- a/PerfectNumbers.Core/HeuristicPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicPrimeTester.cs @@ -21,8 +21,6 @@ public sealed class HeuristicPrimeTester } // TODO: Don't use these tiny arrays. Hard-code the values / checks instead - private static readonly ulong[] GroupAConstantDivisors = [3UL, 7UL, 11UL, 13UL]; - private static readonly byte[] GroupAIncrementPattern = [20, 10]; private static readonly byte[] GroupBEndingOrderMod1 = [9, 1]; private static readonly byte[] GroupBEndingOrderMod3 = [9, 7]; private static readonly byte[] GroupBEndingOrderMod7 = [7, 1]; @@ -30,10 +28,8 @@ public sealed class HeuristicPrimeTester // TODO: Would it speed up things if we implement something like ExponentRemainderStepper, // MersenneDivisorResidueStepper, or CycleRemainderStepper work with these? - private static readonly ushort[] Wheel210ResiduesEnding1 = [1, 11, 31, 41, 61, 71, 101, 121, 131, 151, 181, 191]; - private static readonly ushort[] Wheel210ResiduesEnding7 = [17, 37, 47, 67, 97, 107, 127, 137, 157, 167, 187, 197]; - private static readonly ushort[] Wheel210ResiduesEnding9 = [19, 29, 59, 79, 89, 109, 139, 149, 169, 179, 199, 209]; private const ulong Wheel210 = 210UL; + private const int GroupAConstantCount = 4; internal const int MaxGroupBSequences = 36; private static readonly bool UseHeuristicGroupBTrialDivision = false; // Temporary fallback gate for Group B. @@ -131,26 +127,30 @@ public sealed class HeuristicPrimeTester return !compositeDetected; } + private static bool HeuristicTrialDivisionCpu(ulong n, ulong maxDivisorSquare, byte nMod10, bool includeGroupB) { ReadOnlySpan groupADivisors = HeuristicPrimeSieves.GroupADivisors; + ReadOnlySpan groupADivisorSquares = HeuristicPrimeSieves.GroupADivisorSquares; int interleaveBatchSize = Math.Max(1, HeuristicDivisorInterleaveBatchSize); int groupAIndex = 0; if (!includeGroupB) { - while (groupAIndex < groupADivisors.Length) + int groupALength = groupADivisors.Length; + while (groupAIndex < groupALength) { int processed = 0; - while (processed < interleaveBatchSize && groupAIndex < groupADivisors.Length) + while (processed < interleaveBatchSize && groupAIndex < groupALength) { - ulong divisor = (ulong)groupADivisors[groupAIndex]; - if (!DivisorWithinSquareLimit(divisor, maxDivisorSquare)) + ulong divisorSquare = groupADivisorSquares[groupAIndex]; + if (divisorSquare > maxDivisorSquare) { - groupAIndex = groupADivisors.Length; + groupAIndex = groupALength; break; } + ulong divisor = (ulong)groupADivisors[groupAIndex]; if (n % divisor == 0UL) { return false; @@ -160,14 +160,14 @@ public sealed class HeuristicPrimeTester processed++; } - if (groupAIndex >= groupADivisors.Length) + if (groupAIndex >= groupALength) { break; } - if (!DivisorWithinSquareLimit((ulong)groupADivisors[groupAIndex], maxDivisorSquare)) + if (groupADivisorSquares[groupAIndex] > maxDivisorSquare) { - groupAIndex = groupADivisors.Length; + groupAIndex = groupALength; break; } } @@ -185,7 +185,7 @@ public sealed class HeuristicPrimeTester ? stackalloc int[endingOrder.Length] : new int[endingOrder.Length]; - bool groupAHasMore = groupAIndex < groupADivisors.Length && DivisorWithinSquareLimit((ulong)groupADivisors[groupAIndex], maxDivisorSquare); + bool groupAHasMore = groupAIndex < groupADivisors.Length && groupADivisorSquares[groupAIndex] <= maxDivisorSquare; bool groupBHasMore = HasGroupBCandidates(endingOrder, indices, maxDivisorSquare); while (groupAHasMore || groupBHasMore) @@ -195,14 +195,15 @@ public sealed class HeuristicPrimeTester int processed = 0; while (processed < interleaveBatchSize && groupAIndex < groupADivisors.Length) { - ulong divisor = (ulong)groupADivisors[groupAIndex]; - if (!DivisorWithinSquareLimit(divisor, maxDivisorSquare)) + ulong divisorSquare = groupADivisorSquares[groupAIndex]; + if (divisorSquare > maxDivisorSquare) { groupAIndex = groupADivisors.Length; groupAHasMore = false; break; } + ulong divisor = (ulong)groupADivisors[groupAIndex]; if (n % divisor == 0UL) { return false; @@ -218,7 +219,7 @@ public sealed class HeuristicPrimeTester } else { - groupAHasMore = DivisorWithinSquareLimit((ulong)groupADivisors[groupAIndex], maxDivisorSquare); + groupAHasMore = groupADivisorSquares[groupAIndex] <= maxDivisorSquare; if (!groupAHasMore) { groupAIndex = groupADivisors.Length; @@ -258,6 +259,9 @@ public sealed class HeuristicPrimeTester + + + private bool HeuristicTrialDivisionGpuDetectsDivisor(ulong n, ulong maxDivisorSquare, byte nMod10) { int batchCapacity = HeuristicGpuDivisorBatchSize; @@ -279,24 +283,29 @@ public sealed class HeuristicPrimeTester bool ProcessBatch(int length) { + if (length <= 0) + { + return false; + } + inputView.CopyFromCPU(ref divisorArray[0], length); byte compositeFlag = 0; outputView.CopyFromCPU(ref compositeFlag, 1); kernel(length, inputView, n, outputView); accelerator.Synchronize(); outputView.CopyToCPU(ref compositeFlag, 1); - return compositeFlag != 0; } ReadOnlySpan groupADivisors = HeuristicPrimeSieves.GroupADivisors; + ReadOnlySpan groupADivisorSquares = HeuristicPrimeSieves.GroupADivisorSquares; int groupAIndex = 0; ReadOnlySpan endingOrder = GetGroupBEndingOrder(nMod10); Span indices = endingOrder.Length <= 8 ? stackalloc int[endingOrder.Length] : new int[endingOrder.Length]; - bool groupAHasMore = groupAIndex < groupADivisors.Length && DivisorWithinSquareLimit((ulong)groupADivisors[groupAIndex], maxDivisorSquare); + bool groupAHasMore = groupAIndex < groupADivisors.Length && groupADivisorSquares[groupAIndex] <= maxDivisorSquare; bool groupBHasMore = !endingOrder.IsEmpty && HasGroupBCandidates(endingOrder, indices, maxDivisorSquare); while (!compositeDetected && (groupAHasMore || groupBHasMore)) @@ -306,15 +315,15 @@ public sealed class HeuristicPrimeTester int processed = 0; while (processed < interleaveBatchSize && groupAIndex < groupADivisors.Length) { - ulong divisor = (ulong)groupADivisors[groupAIndex]; - if (!DivisorWithinSquareLimit(divisor, maxDivisorSquare)) + ulong divisorSquare = groupADivisorSquares[groupAIndex]; + if (divisorSquare > maxDivisorSquare) { groupAIndex = groupADivisors.Length; groupAHasMore = false; break; } - divisorArray[count] = divisor; + divisorArray[count] = (ulong)groupADivisors[groupAIndex]; count++; if (count == batchCapacity) @@ -322,7 +331,7 @@ public sealed class HeuristicPrimeTester if (ProcessBatch(count)) { compositeDetected = true; - goto Cleanup; + break; } count = 0; @@ -332,13 +341,18 @@ public sealed class HeuristicPrimeTester processed++; } + if (compositeDetected) + { + break; + } + if (groupAIndex >= groupADivisors.Length) { groupAHasMore = false; } else { - groupAHasMore = DivisorWithinSquareLimit((ulong)groupADivisors[groupAIndex], maxDivisorSquare); + groupAHasMore = groupADivisorSquares[groupAIndex] <= maxDivisorSquare; if (!groupAHasMore) { groupAIndex = groupADivisors.Length; @@ -365,7 +379,7 @@ public sealed class HeuristicPrimeTester if (ProcessBatch(count)) { compositeDetected = true; - goto Cleanup; + break; } count = 0; @@ -374,20 +388,18 @@ public sealed class HeuristicPrimeTester processed++; } - if (groupBHasMore) + if (!compositeDetected && groupBHasMore) { groupBHasMore = HasGroupBCandidates(endingOrder, indices, maxDivisorSquare); } } } - if (count > 0 && ProcessBatch(count)) + if (!compositeDetected && count > 0 && ProcessBatch(count)) { compositeDetected = true; - goto Cleanup; } -Cleanup: divisorPool.Return(divisorArray); gpu.Dispose(); limiter.Dispose(); @@ -395,6 +407,7 @@ Cleanup: } + private static bool EvaluateWithOpenNumericFallback(ulong n) { return Prime.Numbers.IsPrime(n); @@ -404,12 +417,6 @@ Cleanup: return n; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool DivisorWithinSquareLimit(ulong divisor, ulong maxDivisorSquare) - { - return divisor != 0UL && divisor <= maxDivisorSquare / divisor; - } - private static ReadOnlySpan GetGroupBEndingOrder(byte nMod10) => nMod10 switch { 1 => GroupBEndingOrderMod1, @@ -420,6 +427,7 @@ Cleanup: }; + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TrySelectNextGroupBDivisor(ReadOnlySpan endingOrder, Span indices, ulong maxDivisorSquare, out ulong divisor) { @@ -429,19 +437,21 @@ Cleanup: for (int i = 0; i < endingOrder.Length; i++) { ReadOnlySpan divisors = GetGroupBDivisors(endingOrder[i]); + ReadOnlySpan squares = GetGroupBDivisorSquares(endingOrder[i]); int index = indices[i]; if ((uint)index >= (uint)divisors.Length) { continue; } - ulong candidate = (ulong)divisors[index]; - if (!DivisorWithinSquareLimit(candidate, maxDivisorSquare)) + ulong candidateSquare = squares[index]; + if (candidateSquare > maxDivisorSquare) { indices[i] = divisors.Length; continue; } + ulong candidate = (ulong)divisors[index]; if (bestEndingIndex == -1 || candidate < bestCandidate) { bestCandidate = candidate; @@ -455,25 +465,29 @@ Cleanup: return false; } - divisor = bestCandidate; + ReadOnlySpan selectedDivisors = GetGroupBDivisors(endingOrder[bestEndingIndex]); + divisor = (ulong)selectedDivisors[indices[bestEndingIndex]]; indices[bestEndingIndex]++; return true; } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool HasGroupBCandidates(ReadOnlySpan endingOrder, Span indices, ulong maxDivisorSquare) { for (int i = 0; i < endingOrder.Length; i++) { ReadOnlySpan divisors = GetGroupBDivisors(endingOrder[i]); + ReadOnlySpan squares = GetGroupBDivisorSquares(endingOrder[i]); int index = indices[i]; if ((uint)index >= (uint)divisors.Length) { continue; } - ulong candidate = (ulong)divisors[index]; - if (!DivisorWithinSquareLimit(candidate, maxDivisorSquare)) + if (squares[index] > maxDivisorSquare) { indices[i] = divisors.Length; continue; @@ -485,6 +499,8 @@ Cleanup: return false; } + + private static ReadOnlySpan GetGroupBDivisors(byte ending) => ending switch { 1 => DivisorGenerator.SmallPrimesLastOneWithoutLastThree, @@ -494,26 +510,23 @@ Cleanup: _ => ReadOnlySpan.Empty, }; - private static ReadOnlySpan GetWheel210ResiduesForEnding(byte ending) => ending switch + private static ReadOnlySpan GetGroupBDivisorSquares(byte ending) => ending switch { - 1 => Wheel210ResiduesEnding1, - 7 => Wheel210ResiduesEnding7, - 9 => Wheel210ResiduesEnding9, - _ => ReadOnlySpan.Empty, + 1 => DivisorGenerator.SmallPrimesPow2LastOneWithoutLastThree, + 3 => ReadOnlySpan.Empty, + 7 => DivisorGenerator.SmallPrimesPow2LastSevenWithoutLastThree, + 9 => DivisorGenerator.SmallPrimesPow2LastNineWithoutLastThree, + _ => ReadOnlySpan.Empty, }; private static int InitializeGroupBStates(byte nMod10, Span buffer) { ReadOnlySpan endings = GetGroupBEndingOrder(nMod10); - int count = 0; + int count = endings.Length; - for (int priorityIndex = 0; priorityIndex < endings.Length; priorityIndex++) + for (int i = 0; i < count; i++) { - ReadOnlySpan residues = GetWheel210ResiduesForEnding(endings[priorityIndex]); - for (int residueIndex = 0; residueIndex < residues.Length; residueIndex++) - { - buffer[count++] = new HeuristicGroupBSequenceState(residues[residueIndex], endings[priorityIndex], (byte)priorityIndex); - } + buffer[i] = new HeuristicGroupBSequenceState(endings[i], (byte)i); } return count; @@ -598,158 +611,143 @@ Cleanup: return false; } - internal ref struct HeuristicDivisorEnumerator + +internal ref struct HeuristicDivisorEnumerator +{ + private readonly ulong maxDivisorSquare; + private readonly ReadOnlySpan groupADivisors; + private readonly ReadOnlySpan groupADivisorSquares; + private int groupAIndex; + private Span groupBStates; + + public HeuristicDivisorEnumerator(ulong maxDivisorSquare, byte nMod10, Span groupBBuffer) { - private readonly ulong maxDivisorSquare; - private int groupAConstantIndex; - private ulong groupAWheelCandidate; - private int groupAIncrementIndex; - private bool groupAWheelActive; - private Span groupBStates; + this.maxDivisorSquare = maxDivisorSquare; + groupADivisors = HeuristicPrimeSieves.GroupADivisors; + groupADivisorSquares = HeuristicPrimeSieves.GroupADivisorSquares; + groupAIndex = 0; - public HeuristicDivisorEnumerator(ulong maxDivisorSquare, byte nMod10, Span groupBBuffer) + int count = InitializeGroupBStates(nMod10, groupBBuffer); + groupBStates = count == 0 ? Span.Empty : groupBBuffer[..count]; + } + + public bool TryGetNext(out HeuristicDivisorCandidate candidate) + { + while (groupAIndex < groupADivisors.Length) { - this.maxDivisorSquare = maxDivisorSquare; - groupAConstantIndex = 0; - groupAWheelCandidate = 23UL; - groupAIncrementIndex = 0; - groupAWheelActive = DivisorWithinSquareLimit(23UL, maxDivisorSquare); - - int count = InitializeGroupBStates(nMod10, groupBBuffer); - groupBStates = count == 0 ? Span.Empty : groupBBuffer[..count]; - - for (int i = 0; i < groupBStates.Length; i++) + ulong divisorSquare = groupADivisorSquares[groupAIndex]; + if (divisorSquare > maxDivisorSquare) { - groupBStates[i].TryNormalize(); - } - } - - public bool TryGetNext(out HeuristicDivisorCandidate candidate) - { - // TODO: Hard-code constant A divisor checks here instead of using an array - while (groupAConstantIndex < GroupAConstantDivisors.Length) - { - ulong value = GroupAConstantDivisors[groupAConstantIndex++]; - if (!DivisorWithinSquareLimit(value, maxDivisorSquare)) - { - groupAConstantIndex = GroupAConstantDivisors.Length; - break; - } - - // TODO: We're constantly dividing constants mod 10 which is constant too. - candidate = new HeuristicDivisorCandidate(value, HeuristicDivisorGroup.GroupAConstant, (byte)(value % 10UL), 0, (ushort)value); - return true; + groupAIndex = groupADivisors.Length; + break; } - if (groupAWheelActive) - { - ulong value = groupAWheelCandidate; - if (DivisorWithinSquareLimit(value, maxDivisorSquare)) - { - candidate = new HeuristicDivisorCandidate(value, HeuristicDivisorGroup.GroupAWheel, 3, 0, (ushort)(value % Wheel210)); - byte increment = GroupAIncrementPattern[groupAIncrementIndex]; - groupAIncrementIndex ^= 1; + int currentIndex = groupAIndex; + ulong divisor = (ulong)groupADivisors[currentIndex]; + groupAIndex++; - ulong next = value + increment; - if (next <= value) - { - groupAWheelActive = false; - } - else - { - groupAWheelCandidate = next; - } + HeuristicDivisorGroup group = currentIndex < GroupAConstantCount + ? HeuristicDivisorGroup.GroupAConstant + : HeuristicDivisorGroup.GroupAWheel; - return true; - } - - groupAWheelActive = false; - } - - ulong bestCandidate = ulong.MaxValue; - int bestIndex = -1; - - for (int i = 0; i < groupBStates.Length; i++) - { - ref HeuristicGroupBSequenceState state = ref groupBStates[i]; - ulong value = state.Candidate; - if (value == ulong.MaxValue) - { - continue; - } - - if (!DivisorWithinSquareLimit(value, maxDivisorSquare)) - { - state.Candidate = ulong.MaxValue; - continue; - } - - if (bestIndex == -1 || value < bestCandidate || - (value == bestCandidate && state.PriorityIndex < groupBStates[bestIndex].PriorityIndex)) - { - bestCandidate = value; - bestIndex = i; - } - } - - if (bestIndex == -1) - { - candidate = default; - return false; - } - - ref HeuristicGroupBSequenceState bestState = ref groupBStates[bestIndex]; - var result = new HeuristicDivisorCandidate(bestState.Candidate, HeuristicDivisorGroup.GroupB, bestState.Ending, bestState.PriorityIndex, bestState.Residue); - bestState.Advance(); - candidate = result; + candidate = new HeuristicDivisorCandidate( + divisor, + group, + (byte)(divisor % 10UL), + 0, + (ushort)(divisor % Wheel210)); return true; } - } - internal struct HeuristicGroupBSequenceState(ushort residue, byte ending, byte priorityIndex) - { - public ulong Candidate = residue; - public byte PriorityIndex = priorityIndex; - public byte Ending = ending; - public ushort Residue = residue; + ulong bestCandidate = ulong.MaxValue; + int bestIndex = -1; - public void TryNormalize() + for (int i = 0; i < groupBStates.Length; i++) { - ulong candidate = Candidate; - if (candidate == ulong.MaxValue) + ref HeuristicGroupBSequenceState state = ref groupBStates[i]; + if (!state.TryPeek(maxDivisorSquare, out ulong candidateValue)) { - return; + continue; } - while (candidate <= 13UL) + if (bestIndex == -1 || candidateValue < bestCandidate || + (candidateValue == bestCandidate && state.PriorityIndex < groupBStates[bestIndex].PriorityIndex)) { - ulong next = candidate + Wheel210; - if (next <= candidate) - { - Candidate = ulong.MaxValue; - return; - } - - candidate = next; + bestCandidate = candidateValue; + bestIndex = i; } - - Candidate = candidate; } - public void Advance() + if (bestIndex == -1) { - ulong candidate = Candidate; - if (candidate == ulong.MaxValue) - { - return; - } - - ulong next = candidate + Wheel210; - Candidate = next > candidate ? next : ulong.MaxValue; + candidate = default; + return false; } - + ref HeuristicGroupBSequenceState bestState = ref groupBStates[bestIndex]; + bestState.Advance(); + candidate = new HeuristicDivisorCandidate( + bestCandidate, + HeuristicDivisorGroup.GroupB, + bestState.Ending, + bestState.PriorityIndex, + (ushort)(bestCandidate % Wheel210)); + return true; } +} + + + +internal struct HeuristicGroupBSequenceState +{ + public byte Ending; + public byte PriorityIndex; + public int Index; + + public HeuristicGroupBSequenceState(byte ending, byte priorityIndex) + { + Ending = ending; + PriorityIndex = priorityIndex; + Index = 0; + } + + public bool TryPeek(ulong maxDivisorSquare, out ulong divisor) + { + ReadOnlySpan divisors = GetGroupBDivisors(Ending); + ReadOnlySpan squares = GetGroupBDivisorSquares(Ending); + int index = Index; + if ((uint)index >= (uint)divisors.Length) + { + divisor = 0UL; + return false; + } + + if (squares[index] > maxDivisorSquare) + { + Index = divisors.Length; + divisor = 0UL; + return false; + } + + divisor = (ulong)divisors[index]; + return true; + } + + public void Advance() + { + int next = Index + 1; + ReadOnlySpan divisors = GetGroupBDivisors(Ending); + if ((uint)next >= (uint)divisors.Length) + { + Index = divisors.Length; + } + else + { + Index = next; + } + } +} + internal ref struct MersenneHeuristicDivisorEnumerator { From f9504a6a1947f71c090a42712572a099e3417d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Wed, 29 Oct 2025 20:40:40 +0100 Subject: [PATCH 16/17] Use atomic flag writes in heuristic trial division --- .../Gpu/Kernels/PrimeTesterKernels.cs | 12 ++---------- PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs | 10 +++++----- PerfectNumbers.Core/HeuristicPrimeTester.cs | 10 +++++----- PerfectNumbers.Core/PrimeTester.cs | 9 +++++++-- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/PerfectNumbers.Core/Gpu/Kernels/PrimeTesterKernels.cs b/PerfectNumbers.Core/Gpu/Kernels/PrimeTesterKernels.cs index b17f7bb..5b9bf5c 100644 --- a/PerfectNumbers.Core/Gpu/Kernels/PrimeTesterKernels.cs +++ b/PerfectNumbers.Core/Gpu/Kernels/PrimeTesterKernels.cs @@ -68,18 +68,10 @@ internal static class PrimeTesterKernels results[index] = result; } - public static void HeuristicTrialDivisionKernel(Index1D index, ArrayView divisors, ulong n, ArrayView resultFlag) + public static void HeuristicTrialDivisionKernel(Index1D index, ArrayView divisors, ulong n, ArrayView resultFlag) { ulong divisor = divisors[index]; - if (divisor <= 1UL) - { - return; - } - - if (n % divisor == 0UL) - { - resultFlag[0] = 1; - } + Atomic.Or(ref resultFlag[0], divisor <= 1UL ? 0 : (n % divisor == 0UL ? 1 : 0)); } public static void SharesFactorKernel(Index1D index, ArrayView numbers, ArrayView results) diff --git a/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs b/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs index 5aea0f8..72add2f 100644 --- a/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs @@ -221,16 +221,16 @@ public sealed class HeuristicCombinedPrimeTester int count = 0; var inputView = gpu.Input.View; - var outputView = gpu.Output.View; + var flagView = gpu.HeuristicFlag.View; bool ProcessBatch(int length) { inputView.CopyFromCPU(ref divisorArray[0], length); - byte compositeFlag = 0; - outputView.CopyFromCPU(ref compositeFlag, 1); - kernel(length, inputView, n, outputView); + int compositeFlag = 0; + flagView.CopyFromCPU(ref compositeFlag, 1); + kernel(length, inputView, n, flagView); accelerator.Synchronize(); - outputView.CopyToCPU(ref compositeFlag, 1); + flagView.CopyToCPU(ref compositeFlag, 1); return compositeFlag != 0; } diff --git a/PerfectNumbers.Core/HeuristicPrimeTester.cs b/PerfectNumbers.Core/HeuristicPrimeTester.cs index a2abb2f..5d91856 100644 --- a/PerfectNumbers.Core/HeuristicPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicPrimeTester.cs @@ -279,7 +279,7 @@ public sealed class HeuristicPrimeTester int count = 0; var inputView = gpu.Input.View; - var outputView = gpu.Output.View; + var flagView = gpu.HeuristicFlag.View; bool ProcessBatch(int length) { @@ -289,11 +289,11 @@ public sealed class HeuristicPrimeTester } inputView.CopyFromCPU(ref divisorArray[0], length); - byte compositeFlag = 0; - outputView.CopyFromCPU(ref compositeFlag, 1); - kernel(length, inputView, n, outputView); + int compositeFlag = 0; + flagView.CopyFromCPU(ref compositeFlag, 1); + kernel(length, inputView, n, flagView); accelerator.Synchronize(); - outputView.CopyToCPU(ref compositeFlag, 1); + flagView.CopyToCPU(ref compositeFlag, 1); return compositeFlag != 0; } diff --git a/PerfectNumbers.Core/PrimeTester.cs b/PerfectNumbers.Core/PrimeTester.cs index 3832125..f18ca71 100644 --- a/PerfectNumbers.Core/PrimeTester.cs +++ b/PerfectNumbers.Core/PrimeTester.cs @@ -272,7 +272,7 @@ public static bool IsPrimeCpu(ulong n, CancellationToken ct) internal readonly Context AcceleratorContext; public readonly Accelerator Accelerator; public readonly Action, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView> Kernel; - public readonly Action, ulong, ArrayView> HeuristicTrialDivisionKernel; + public readonly Action, ulong, ArrayView> HeuristicTrialDivisionKernel; public readonly MemoryBuffer1D DevicePrimesDefault; public readonly MemoryBuffer1D DevicePrimesLastOne; public readonly MemoryBuffer1D DevicePrimesLastSeven; @@ -285,6 +285,7 @@ public static bool IsPrimeCpu(ulong n, CancellationToken ct) public readonly MemoryBuffer1D DevicePrimesPow2LastNine; public MemoryBuffer1D Input; public MemoryBuffer1D Output; + public MemoryBuffer1D HeuristicFlag; public int BufferCapacity; internal PrimeTesterGpuContextLease(int minBufferCapacity) @@ -292,7 +293,7 @@ public static bool IsPrimeCpu(ulong n, CancellationToken ct) AcceleratorContext = Context.CreateDefault(); Accelerator = AcceleratorContext.GetPreferredDevice(false).CreateAccelerator(AcceleratorContext); Kernel = Accelerator.LoadAutoGroupedStreamKernel, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView>(PrimeTesterKernels.SmallPrimeSieveKernel); - HeuristicTrialDivisionKernel = Accelerator.LoadAutoGroupedStreamKernel, ulong, ArrayView>(PrimeTesterKernels.HeuristicTrialDivisionKernel); + HeuristicTrialDivisionKernel = Accelerator.LoadAutoGroupedStreamKernel, ulong, ArrayView>(PrimeTesterKernels.HeuristicTrialDivisionKernel); var primesDefault = DivisorGenerator.SmallPrimes; DevicePrimesDefault = Accelerator.Allocate1D(primesDefault.Length); @@ -337,6 +338,7 @@ public static bool IsPrimeCpu(ulong n, CancellationToken ct) BufferCapacity = minBufferCapacity < 1 ? 1 : minBufferCapacity; Input = Accelerator.Allocate1D(BufferCapacity); Output = Accelerator.Allocate1D(BufferCapacity); + HeuristicFlag = Accelerator.Allocate1D(BufferCapacity); _returned = false; } @@ -350,8 +352,10 @@ public static bool IsPrimeCpu(ulong n, CancellationToken ct) Input.Dispose(); Output.Dispose(); + HeuristicFlag.Dispose(); Input = Accelerator.Allocate1D(required); Output = Accelerator.Allocate1D(required); + HeuristicFlag = Accelerator.Allocate1D(required); BufferCapacity = required; } @@ -375,6 +379,7 @@ public static bool IsPrimeCpu(ulong n, CancellationToken ct) { Input.Dispose(); Output.Dispose(); + HeuristicFlag.Dispose(); DevicePrimesDefault.Dispose(); DevicePrimesLastOne.Dispose(); DevicePrimesLastSeven.Dispose(); From fc5700f82ccc727f657d6c9bea9d62325090d15a Mon Sep 17 00:00:00 2001 From: mlemanczyk Date: Wed, 29 Oct 2025 22:31:28 +0100 Subject: [PATCH 17/17] IsPrime with cancellation token removed. --- .../PrimeTesterBenchmarks.cs | 14 +- EvenPerfectBitScanner.Tests/ProgramTests.cs | 2 +- EvenPerfectBitScanner/Program.cs | 4 +- .../Gpu/PrimeTesterGpuTests.cs | 4 +- .../PrimeTester/PrimeTesterIsPrimeTests.cs | 12 +- PerfectNumbers.Core/HeuristicPrimeTester.cs | 9 +- PerfectNumbers.Core/MersenneDivisorCycles.cs | 14 +- .../MersennePrimeFactorTester.cs | 4 +- .../PrimeOrderCalculator.Cpu.cs | 20 +- .../PrimeOrderCalculator.Shared.cs | 2 +- PerfectNumbers.Core/PrimeTester.cs | 516 +++++++++--------- 11 files changed, 287 insertions(+), 314 deletions(-) diff --git a/EvenPerfectBitScanner.Benchmarks/PrimeTesterBenchmarks.cs b/EvenPerfectBitScanner.Benchmarks/PrimeTesterBenchmarks.cs index f63e315..db1c2fb 100644 --- a/EvenPerfectBitScanner.Benchmarks/PrimeTesterBenchmarks.cs +++ b/EvenPerfectBitScanner.Benchmarks/PrimeTesterBenchmarks.cs @@ -29,8 +29,7 @@ public class PrimeTesterBenchmarks return; } - CancellationToken cancellationToken = CancellationToken.None; - _ = _heuristicTester.IsPrimeCpu(_candidates[0], cancellationToken); + _ = _heuristicTester.IsPrimeCpu(_candidates[0]); _ = _heuristicTester.IsPrimeGpu(_candidates[0]); _ = Prime.Numbers.IsPrime(_candidates[0]); } @@ -40,12 +39,11 @@ public class PrimeTesterBenchmarks { HeuristicPrimeTester tester = _heuristicTester; ulong[] values = _candidates; - CancellationToken cancellationToken = CancellationToken.None; int primeCount = 0; for (int i = 0; i < values.Length; i++) { - if (tester.IsPrimeCpu(values[i], cancellationToken)) + if (tester.IsPrimeCpu(values[i])) { primeCount++; } @@ -75,7 +73,6 @@ public class PrimeTesterBenchmarks [Benchmark] public int HeuristicCombinedGpu() { - HeuristicCombinedPrimeTester tester = _heuristicCombinedTester; ulong[] values = _candidates; int primeCount = 0; @@ -93,7 +90,6 @@ public class PrimeTesterBenchmarks [Benchmark] public int HeuristicCombinedCpu() { - HeuristicCombinedPrimeTester tester = _heuristicCombinedTester; ulong[] values = _candidates; int primeCount = 0; @@ -115,12 +111,11 @@ public class PrimeTesterBenchmarks { PrimeTester tester = _nonHeuristicTester; ulong[] values = _candidates; - CancellationToken cancellationToken = CancellationToken.None; int primeCount = 0; for (int i = 0; i < values.Length; i++) { - if (tester.IsPrimeGpu(values[i], cancellationToken)) + if (tester.IsPrimeGpu(values[i])) { primeCount++; } @@ -133,12 +128,11 @@ public class PrimeTesterBenchmarks public int NonHeuristicCpu() { ulong[] values = _candidates; - CancellationToken cancellationToken = CancellationToken.None; int primeCount = 0; for (int i = 0; i < values.Length; i++) { - if (PrimeTester.IsPrimeCpu(values[i], cancellationToken)) + if (PrimeTester.IsPrime(values[i])) { primeCount++; } diff --git a/EvenPerfectBitScanner.Tests/ProgramTests.cs b/EvenPerfectBitScanner.Tests/ProgramTests.cs index f08a676..cf16bdb 100644 --- a/EvenPerfectBitScanner.Tests/ProgramTests.cs +++ b/EvenPerfectBitScanner.Tests/ProgramTests.cs @@ -55,7 +55,7 @@ public class ProgramTests [InlineData(81UL, false)] public void IsPrime_identifies_primes_correctly(ulong n, bool expected) { - PrimeTester.IsPrime(n, CancellationToken.None).Should().Be(expected); + PrimeTester.IsPrime(n).Should().Be(expected); } [Fact] diff --git a/EvenPerfectBitScanner/Program.cs b/EvenPerfectBitScanner/Program.cs index 86b4c20..bf755d4 100644 --- a/EvenPerfectBitScanner/Program.cs +++ b/EvenPerfectBitScanner/Program.cs @@ -622,8 +622,8 @@ internal static class Program if (!_cliArguments.UseByDivisor) { if (!(GpuContextPool.ForceCpu - ? PrimeTester.IsPrime(p, CancellationToken.None) - : PrimeTesters.Value!.IsPrimeGpu(p, CancellationToken.None))) + ? PrimeTester.IsPrime(p) + : PrimeTesters.Value!.IsPrimeGpu(p))) { _lastCompositeP = true; return false; diff --git a/PerfectNumbers.Core.Tests/Gpu/PrimeTesterGpuTests.cs b/PerfectNumbers.Core.Tests/Gpu/PrimeTesterGpuTests.cs index 5e8faf9..6213b07 100644 --- a/PerfectNumbers.Core.Tests/Gpu/PrimeTesterGpuTests.cs +++ b/PerfectNumbers.Core.Tests/Gpu/PrimeTesterGpuTests.cs @@ -17,7 +17,7 @@ public class PrimeTesterGpuTests ulong[] primes = [31UL, 61UL, 89UL, 107UL, 127UL, 521UL]; foreach (ulong prime in primes) { - tester.IsPrimeGpu(prime, CancellationToken.None).Should().BeTrue(); + tester.IsPrimeGpu(prime).Should().BeTrue(); } } finally @@ -37,7 +37,7 @@ public class PrimeTesterGpuTests ulong[] composites = [33UL, 39UL, 51UL, 77UL, 91UL]; foreach (ulong composite in composites) { - tester.IsPrimeGpu(composite, CancellationToken.None).Should().BeFalse(); + tester.IsPrimeGpu(composite).Should().BeFalse(); } } finally diff --git a/PerfectNumbers.Core.Tests/PrimeTester/PrimeTesterIsPrimeTests.cs b/PerfectNumbers.Core.Tests/PrimeTester/PrimeTesterIsPrimeTests.cs index e86e1db..f7dd5b7 100644 --- a/PerfectNumbers.Core.Tests/PrimeTester/PrimeTesterIsPrimeTests.cs +++ b/PerfectNumbers.Core.Tests/PrimeTester/PrimeTesterIsPrimeTests.cs @@ -26,7 +26,7 @@ public class PrimeTesterIsPrimeTests { var tester = new PrimeTester(); - PrimeTester.IsPrime(n, CancellationToken.None).Should().Be(expected); + PrimeTester.IsPrime(n).Should().Be(expected); } [Theory] @@ -40,7 +40,7 @@ public class PrimeTesterIsPrimeTests GpuContextPool.ForceCpu = false; var tester = new PrimeTester(); - tester.IsPrimeGpu(n, CancellationToken.None).Should().Be(expected); + tester.IsPrimeGpu(n).Should().Be(expected); } [Fact] @@ -50,9 +50,9 @@ public class PrimeTesterIsPrimeTests var tester = new PrimeTester(); GpuContextPool.ForceCpu = true; - tester.IsPrimeGpu(137UL, CancellationToken.None).Should().BeTrue(); + tester.IsPrimeGpu(137UL).Should().BeTrue(); - tester.IsPrimeGpu(341UL, CancellationToken.None).Should().BeFalse(); + tester.IsPrimeGpu(341UL).Should().BeFalse(); GpuContextPool.ForceCpu = false; } @@ -71,7 +71,7 @@ public class PrimeTesterIsPrimeTests var testers = new ThreadLocal(() => new PrimeTester(), trackAllValues: true); Parallel.For(0, primes.Length, i => { - results[i] = testers.Value!.IsPrimeGpu(primes[i], CancellationToken.None); + results[i] = testers.Value!.IsPrimeGpu(primes[i]); }); results.Should().AllBeEquivalentTo(true); @@ -80,7 +80,7 @@ public class PrimeTesterIsPrimeTests { foreach (ulong prime in primes) { - testers.Value!.IsPrimeGpu(prime, CancellationToken.None).Should().BeTrue(); + testers.Value!.IsPrimeGpu(prime).Should().BeTrue(); } }); } diff --git a/PerfectNumbers.Core/HeuristicPrimeTester.cs b/PerfectNumbers.Core/HeuristicPrimeTester.cs index 5d91856..2f491df 100644 --- a/PerfectNumbers.Core/HeuristicPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicPrimeTester.cs @@ -77,10 +77,8 @@ public sealed class HeuristicPrimeTester public static HeuristicPrimeTester Exclusive => _tester ??= new(); - public bool IsPrimeCpu(ulong n, CancellationToken ct) + public bool IsPrimeCpu(ulong n) { - ct.ThrowIfCancellationRequested(); - byte nMod10 = (byte)n.Mod10(); ulong maxDivisorSquare = ComputeHeuristicDivisorSquareLimit(n); @@ -93,11 +91,6 @@ public sealed class HeuristicPrimeTester return HeuristicTrialDivisionCpu(n, maxDivisorSquare, nMod10, includeGroupB); } - public bool IsPrimeCpu(ulong n) - { - return IsPrimeCpu(n, CancellationToken.None); - } - public bool IsPrimeGpu(ulong n) { byte nMod10 = (byte)n.Mod10(); diff --git a/PerfectNumbers.Core/MersenneDivisorCycles.cs b/PerfectNumbers.Core/MersenneDivisorCycles.cs index 851473c..149bc77 100644 --- a/PerfectNumbers.Core/MersenneDivisorCycles.cs +++ b/PerfectNumbers.Core/MersenneDivisorCycles.cs @@ -411,8 +411,8 @@ public class MersenneDivisorCycles // Atomic.Add(ref _tryFactorIntoCountsInternalHits, 1UL); // Console.WriteLine($"MersenneDivisorCycles.TryFactorIntoCountsInternal hits {Volatile.Read(ref _tryFactorIntoCountsInternalHits)}"); - HeuristicPrimeTester primeTester = _primeTester ??= new(); - if (primeTester.IsPrimeGpu(remaining)) + // HeuristicPrimeTester primeTester = _primeTester ??= new(); + if (PrimeTester.IsPrime(remaining)) // if (PrimeTester.IsPrimeCpu(remaining, CancellationToken.None)) { AddFactor(counts, remaining); @@ -917,8 +917,8 @@ public class MersenneDivisorCycles return CalculateCycleLengthFallback(divisor); } - [ThreadStatic] - private static HeuristicPrimeTester? _primeTester; + // [ThreadStatic] + // private static HeuristicPrimeTester? _primeTester; internal static bool TryCalculateCycleLengthHeuristic(ulong divisor, in MontgomeryDivisorData divisorData, out ulong cycleLength, bool skipPrimeOrderHeuristic = false) { @@ -940,10 +940,10 @@ public class MersenneDivisorCycles // Console.WriteLine($"MersenneDivisorCycles.CalculateCycleLengthFallback hits {Volatile.Read(ref _tryCalculateCycleLengthHeuristicHits)}"); // } - HeuristicPrimeTester primeTester = _primeTester ??= new(); + // HeuristicPrimeTester primeTester = _primeTester ??= new(); // if (!skipPrimeOrderHeuristic && HeuristicPrimeTester.Exclusive.IsPrimeCpu(divisor, CancellationToken.None)) - if (!skipPrimeOrderHeuristic && primeTester.IsPrimeGpu(divisor)) - // if (!skipPrimeOrderHeuristic && PrimeTester.IsPrimeCpu(divisor, CancellationToken.None)) + // if (!skipPrimeOrderHeuristic && primeTester.IsPrimeGpu(divisor)) + if (!skipPrimeOrderHeuristic && PrimeTester.IsPrime(divisor)) { ulong computedOrder = PrimeOrderCalculator.Calculate( divisor, diff --git a/PerfectNumbers.Core/MersennePrimeFactorTester.cs b/PerfectNumbers.Core/MersennePrimeFactorTester.cs index a160a3b..93dcb58 100644 --- a/PerfectNumbers.Core/MersennePrimeFactorTester.cs +++ b/PerfectNumbers.Core/MersennePrimeFactorTester.cs @@ -34,7 +34,7 @@ public static class MersennePrimeFactorTester // Atomic.Add(ref _isPrimeFactorHits, 1UL); // Console.WriteLine($"MersennePrimeFactorTester.IsPrimeFactor hits {Volatile.Read(ref _isPrimeFactorHits)}"); - if (!PrimeTester.IsPrimeCpu(q, ct)) + if (!PrimeTester.IsPrime(q)) { return false; } @@ -247,7 +247,7 @@ public static class MersennePrimeFactorTester // Console.WriteLine($"MersennePrimeFactorTester.Factor64 hits {Volatile.Read(ref _factor64Hits)}"); // if (HeuristicPrimeTester.Exclusive.IsPrime(m, ct)) - if (PrimeTester.IsPrimeCpu(m, ct)) + if (PrimeTester.IsPrime(m)) { dict[m] = 1; continue; diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs index 1be9074..3f5773e 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs @@ -1079,7 +1079,7 @@ internal static partial class PrimeOrderCalculator PartialFactorResult result; PrimeTester primeTester = PrimeTester.Exclusive; CancellationToken ct = CancellationToken.None; - HeuristicPrimeTester? tester = _tester ??= new(); + // HeuristicPrimeTester? tester = _tester ??= new(); if (!gpuFactored) { @@ -1152,7 +1152,7 @@ internal static partial class PrimeOrderCalculator // Console.WriteLine($"Partial factor hits {Volatile.Read(ref _partialFactorHits)}"); // bool isPrime = PrimeTester.IsPrimeCpu(composite, CancellationToken.None); - bool isPrime = tester.IsPrimeGpu(composite); + bool isPrime = PrimeTester.IsPrime(composite); // bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(composite); if (isPrime) @@ -1220,7 +1220,7 @@ internal static partial class PrimeOrderCalculator // Console.WriteLine($"Partial factor pending hits {Volatile.Read(ref _partialFactorPendingHits)}"); // bool isPrime = PrimeTester.IsPrimeCpu(composite, CancellationToken.None); - bool isPrime = tester.IsPrimeGpu(composite); + bool isPrime = PrimeTester.IsPrime(composite); // bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(composite); entry = entry.WithPrimality(isPrime); @@ -1257,7 +1257,7 @@ internal static partial class PrimeOrderCalculator // Atomic.Add(ref _partialFactorCofactorHits, 1UL); // Console.WriteLine($"Partial factor cofactor hits {Volatile.Read(ref _partialFactorCofactorHits)}"); - cofactorIsPrime = tester.IsPrimeGpu(cofactor); + cofactorIsPrime = PrimeTester.IsPrime(cofactor); // cofactorIsPrime = PrimeTester.IsPrimeGpu(cofactor); // cofactorIsPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(cofactor); } @@ -1782,8 +1782,8 @@ internal static partial class PrimeOrderCalculator // private static ulong _factorCompletelyHits; // private static ulong _factorCompletelyPollardRhoHits; - [ThreadStatic] - private static HeuristicPrimeTester? _tester; + // [ThreadStatic] + // private static HeuristicPrimeTester? _tester; private static void FactorCompletely(ulong value, Dictionary counts, bool knownComposite) { @@ -1794,10 +1794,10 @@ internal static partial class PrimeOrderCalculator // Atomic.Add(ref _factorCompletelyHits, 1UL); // Console.WriteLine($"Factor completely hits {Volatile.Read(ref _factorCompletelyHits)}"); - HeuristicPrimeTester tester = _tester ??= new(); + // HeuristicPrimeTester tester = _tester ??= new(); // bool isPrime = primeTester.HeuristicIsPrimeGpu(value); - bool isPrime = tester.IsPrimeGpu(value); + bool isPrime = PrimeTester.IsPrime(value); // bool isPrime = PrimeTester.IsPrimeGpu(value); // bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(value); if (!knownComposite && isPrime) @@ -1813,7 +1813,7 @@ internal static partial class PrimeOrderCalculator // Console.WriteLine($"Factor completely after PollardRho hits {Volatile.Read(ref _factorCompletelyPollardRhoHits)}"); // isPrime = primeTester.HeuristicIsPrimeGpu(factor); - isPrime = tester.IsPrimeGpu(factor); + isPrime = PrimeTester.IsPrime(factor); // isPrime = PrimeTester.IsPrimeGpu(factor); // isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(factor); if (isPrime) @@ -1850,7 +1850,7 @@ internal static partial class PrimeOrderCalculator // Atomic.Add(ref _partialFactorCofactorHits, 1UL); // Console.WriteLine($"Partial factor cofactor hits {Volatile.Read(ref _partialFactorCofactorHits)}"); - isPrime = tester.IsPrimeGpu(quotient); + isPrime = PrimeTester.IsPrime(quotient); // isPrime = PrimeTester.IsPrimeGpu(quotient); // isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(quotient); } diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.Shared.cs b/PerfectNumbers.Core/PrimeOrderCalculator.Shared.cs index 2f923c6..2fe2791 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.Shared.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.Shared.cs @@ -950,7 +950,7 @@ internal static partial class PrimeOrderCalculator if (value <= ulong.MaxValue) { // return HeuristicPrimeTester.Exclusive.IsPrimeCpu((ulong)value, CancellationToken.None); - return PrimeTester.IsPrimeCpu((ulong)value, CancellationToken.None); + return PrimeTester.IsPrime((ulong)value); } if ((value & UInt128.One) == UInt128.Zero) diff --git a/PerfectNumbers.Core/PrimeTester.cs b/PerfectNumbers.Core/PrimeTester.cs index f18ca71..52bb5bf 100644 --- a/PerfectNumbers.Core/PrimeTester.cs +++ b/PerfectNumbers.Core/PrimeTester.cs @@ -1,7 +1,5 @@ -using System; using System.Buffers; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -22,7 +20,7 @@ public sealed class PrimeTester public static PrimeTester Exclusive => _tester ??= new(); - public static bool IsPrime(ulong n, CancellationToken ct) + public static bool IsPrime(ulong n) { if (n <= 1UL) { @@ -92,310 +90,298 @@ public sealed class PrimeTester return result; } - public static bool IsPrimeGpu(ulong n) - { - return Exclusive.IsPrimeGpu(n, CancellationToken.None); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool IsPrimeGpu(ulong n, CancellationToken ct) - { - bool forceCpu = GpuContextPool.ForceCpu; - bool belowGpuRange = n < 31UL; - - if (forceCpu || belowGpuRange) - { - return IsPrimeCpu(n, ct); - } - - var limiter = GpuPrimeWorkLimiter.Acquire(); - var gpu = PrimeTesterGpuContextPool.Rent(1); - var accelerator = gpu.Accelerator; - var kernel = gpu.Kernel; - - ulong value = n; - byte flag = 0; - - var input = gpu.Input; - var output = gpu.Output; - var inputView = input.View; - var outputView = output.View; - - inputView.CopyFromCPU(ref value, 1); - kernel( - 1, - inputView, - gpu.DevicePrimesDefault.View, - gpu.DevicePrimesLastOne.View, - gpu.DevicePrimesLastSeven.View, - gpu.DevicePrimesLastThree.View, - gpu.DevicePrimesLastNine.View, - gpu.DevicePrimesPow2Default.View, - gpu.DevicePrimesPow2LastOne.View, - gpu.DevicePrimesPow2LastSeven.View, - gpu.DevicePrimesPow2LastThree.View, - gpu.DevicePrimesPow2LastNine.View, - outputView); - accelerator.Synchronize(); - outputView.CopyToCPU(ref flag, 1); - - gpu.Dispose(); - limiter.Dispose(); - - return flag != 0; - } - -public static bool IsPrimeCpu(ulong n, CancellationToken ct) + public bool IsPrimeGpu(ulong n) { - // Preserve the legacy entry point for callers that bypass the thread-local caches. - return IsPrime(n, ct); - } + bool forceCpu = GpuContextPool.ForceCpu; + bool belowGpuRange = n < 31UL; + if (forceCpu || belowGpuRange) + { + return IsPrime(n); + } + + var limiter = GpuPrimeWorkLimiter.Acquire(); + var gpu = PrimeTesterGpuContextPool.Rent(1); + var accelerator = gpu.Accelerator; + var kernel = gpu.Kernel; + + ulong value = n; + byte flag = 0; + + var input = gpu.Input; + var output = gpu.Output; + var inputView = input.View; + var outputView = output.View; + + inputView.CopyFromCPU(ref value, 1); + kernel( + 1, + inputView, + gpu.DevicePrimesDefault.View, + gpu.DevicePrimesLastOne.View, + gpu.DevicePrimesLastSeven.View, + gpu.DevicePrimesLastThree.View, + gpu.DevicePrimesLastNine.View, + gpu.DevicePrimesPow2Default.View, + gpu.DevicePrimesPow2LastOne.View, + gpu.DevicePrimesPow2LastSeven.View, + gpu.DevicePrimesPow2LastThree.View, + gpu.DevicePrimesPow2LastNine.View, + outputView); + accelerator.Synchronize(); + outputView.CopyToCPU(ref flag, 1); + + gpu.Dispose(); + limiter.Dispose(); + + return flag != 0; + } public static int GpuBatchSize { get; set; } = 262_144; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void IsPrimeBatchGpu(ReadOnlySpan values, Span results) - { - var limiter = GpuPrimeWorkLimiter.Acquire(); - var gpu = PrimeTesterGpuContextPool.Rent(GpuBatchSize); - var accelerator = gpu.Accelerator; - var kernel = gpu.Kernel; - int totalLength = values.Length; - int batchSize = GpuBatchSize; + { + var limiter = GpuPrimeWorkLimiter.Acquire(); + var gpu = PrimeTesterGpuContextPool.Rent(GpuBatchSize); + var accelerator = gpu.Accelerator; + var kernel = gpu.Kernel; + int totalLength = values.Length; + int batchSize = GpuBatchSize; - var input = gpu.Input; - var output = gpu.Output; - var inputView = input.View; - var outputView = output.View; + var input = gpu.Input; + var output = gpu.Output; + var inputView = input.View; + var outputView = output.View; - int pos = 0; - while (pos < totalLength) - { - int remaining = totalLength - pos; - int count = remaining > batchSize ? batchSize : remaining; + int pos = 0; + while (pos < totalLength) + { + int remaining = totalLength - pos; + int count = remaining > batchSize ? batchSize : remaining; - var valueSlice = values.Slice(pos, count); - ref ulong valueRef = ref MemoryMarshal.GetReference(valueSlice); - inputView.CopyFromCPU(ref valueRef, count); + var valueSlice = values.Slice(pos, count); + ref ulong valueRef = ref MemoryMarshal.GetReference(valueSlice); + inputView.CopyFromCPU(ref valueRef, count); - kernel( - count, - inputView, - gpu.DevicePrimesDefault.View, - gpu.DevicePrimesLastOne.View, - gpu.DevicePrimesLastSeven.View, - gpu.DevicePrimesLastThree.View, - gpu.DevicePrimesLastNine.View, - gpu.DevicePrimesPow2Default.View, - gpu.DevicePrimesPow2LastOne.View, - gpu.DevicePrimesPow2LastSeven.View, - gpu.DevicePrimesPow2LastThree.View, - gpu.DevicePrimesPow2LastNine.View, - outputView); - accelerator.Synchronize(); - var resultSlice = results.Slice(pos, count); - ref byte resultRef = ref MemoryMarshal.GetReference(resultSlice); - outputView.CopyToCPU(ref resultRef, count); + kernel( + count, + inputView, + gpu.DevicePrimesDefault.View, + gpu.DevicePrimesLastOne.View, + gpu.DevicePrimesLastSeven.View, + gpu.DevicePrimesLastThree.View, + gpu.DevicePrimesLastNine.View, + gpu.DevicePrimesPow2Default.View, + gpu.DevicePrimesPow2LastOne.View, + gpu.DevicePrimesPow2LastSeven.View, + gpu.DevicePrimesPow2LastThree.View, + gpu.DevicePrimesPow2LastNine.View, + outputView); + accelerator.Synchronize(); + var resultSlice = results.Slice(pos, count); + ref byte resultRef = ref MemoryMarshal.GetReference(resultSlice); + outputView.CopyToCPU(ref resultRef, count); - pos += count; - } + pos += count; + } - gpu.Dispose(); - limiter.Dispose(); - } + gpu.Dispose(); + limiter.Dispose(); + } - internal static class PrimeTesterGpuContextPool - { - private static readonly ConcurrentQueue Pool = new(); + internal static class PrimeTesterGpuContextPool + { + private static readonly ConcurrentQueue Pool = new(); - internal static PrimeTesterGpuContextLease Rent(int minBufferCapacity = 1) - { - if (!Pool.TryDequeue(out var lease)) - { - lease = new PrimeTesterGpuContextLease(minBufferCapacity); - } - else - { - lease.EnsureCapacity(minBufferCapacity); - } + 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; - } + lease.ResetReturnFlag(); + return lease; + } - internal static void Return(PrimeTesterGpuContextLease lease) - { - lease.Accelerator.Synchronize(); - Pool.Enqueue(lease); - } + internal static void Return(PrimeTesterGpuContextLease lease) + { + lease.Accelerator.Synchronize(); + Pool.Enqueue(lease); + } - internal static void DisposeAll() - { - while (Pool.TryDequeue(out var lease)) - { - lease.DisposeResources(); - } - } + internal static void DisposeAll() + { + while (Pool.TryDequeue(out var lease)) + { + lease.DisposeResources(); + } + } - internal static void Clear(Accelerator accelerator) - { - if (accelerator is null) - { - return; - } + internal static void Clear(Accelerator accelerator) + { + if (accelerator is null) + { + return; + } - var retained = new List(); - while (Pool.TryDequeue(out var lease)) - { - if (lease.Accelerator == accelerator) - { - lease.DisposeResources(); - } - else - { - retained.Add(lease); - } - } + var retained = new List(); + while (Pool.TryDequeue(out var lease)) + { + if (lease.Accelerator == accelerator) + { + lease.DisposeResources(); + } + 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]); + } + } - internal sealed class PrimeTesterGpuContextLease : IDisposable - { - private bool _returned; + internal sealed class PrimeTesterGpuContextLease : IDisposable + { + private bool _returned; - internal readonly Context AcceleratorContext; - public readonly Accelerator Accelerator; - public readonly Action, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView> Kernel; - public readonly Action, ulong, ArrayView> HeuristicTrialDivisionKernel; - public readonly MemoryBuffer1D DevicePrimesDefault; - public readonly MemoryBuffer1D DevicePrimesLastOne; - public readonly MemoryBuffer1D DevicePrimesLastSeven; - public readonly MemoryBuffer1D DevicePrimesLastThree; - public readonly MemoryBuffer1D DevicePrimesLastNine; - public readonly MemoryBuffer1D DevicePrimesPow2Default; - public readonly MemoryBuffer1D DevicePrimesPow2LastOne; - public readonly MemoryBuffer1D DevicePrimesPow2LastSeven; - public readonly MemoryBuffer1D DevicePrimesPow2LastThree; - public readonly MemoryBuffer1D DevicePrimesPow2LastNine; - public MemoryBuffer1D Input; - public MemoryBuffer1D Output; - public MemoryBuffer1D HeuristicFlag; - public int BufferCapacity; + internal readonly Context AcceleratorContext; + public readonly Accelerator Accelerator; + public readonly Action, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView> Kernel; + public readonly Action, ulong, ArrayView> HeuristicTrialDivisionKernel; + public readonly MemoryBuffer1D DevicePrimesDefault; + public readonly MemoryBuffer1D DevicePrimesLastOne; + public readonly MemoryBuffer1D DevicePrimesLastSeven; + public readonly MemoryBuffer1D DevicePrimesLastThree; + public readonly MemoryBuffer1D DevicePrimesLastNine; + public readonly MemoryBuffer1D DevicePrimesPow2Default; + public readonly MemoryBuffer1D DevicePrimesPow2LastOne; + public readonly MemoryBuffer1D DevicePrimesPow2LastSeven; + public readonly MemoryBuffer1D DevicePrimesPow2LastThree; + public readonly MemoryBuffer1D DevicePrimesPow2LastNine; + public MemoryBuffer1D Input; + public MemoryBuffer1D Output; + public MemoryBuffer1D HeuristicFlag; + public int BufferCapacity; - internal PrimeTesterGpuContextLease(int minBufferCapacity) - { - AcceleratorContext = Context.CreateDefault(); - Accelerator = AcceleratorContext.GetPreferredDevice(false).CreateAccelerator(AcceleratorContext); - Kernel = Accelerator.LoadAutoGroupedStreamKernel, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView>(PrimeTesterKernels.SmallPrimeSieveKernel); - HeuristicTrialDivisionKernel = Accelerator.LoadAutoGroupedStreamKernel, ulong, ArrayView>(PrimeTesterKernels.HeuristicTrialDivisionKernel); + internal PrimeTesterGpuContextLease(int minBufferCapacity) + { + AcceleratorContext = Context.CreateDefault(); + Accelerator = AcceleratorContext.GetPreferredDevice(false).CreateAccelerator(AcceleratorContext); + Kernel = Accelerator.LoadAutoGroupedStreamKernel, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView>(PrimeTesterKernels.SmallPrimeSieveKernel); + HeuristicTrialDivisionKernel = Accelerator.LoadAutoGroupedStreamKernel, ulong, ArrayView>(PrimeTesterKernels.HeuristicTrialDivisionKernel); - var primesDefault = DivisorGenerator.SmallPrimes; - DevicePrimesDefault = Accelerator.Allocate1D(primesDefault.Length); - DevicePrimesDefault.View.CopyFromCPU(primesDefault); + var primesDefault = DivisorGenerator.SmallPrimes; + DevicePrimesDefault = Accelerator.Allocate1D(primesDefault.Length); + DevicePrimesDefault.View.CopyFromCPU(primesDefault); - var primesLastOne = DivisorGenerator.SmallPrimesLastOne; - DevicePrimesLastOne = Accelerator.Allocate1D(primesLastOne.Length); - DevicePrimesLastOne.View.CopyFromCPU(primesLastOne); + var primesLastOne = DivisorGenerator.SmallPrimesLastOne; + DevicePrimesLastOne = Accelerator.Allocate1D(primesLastOne.Length); + DevicePrimesLastOne.View.CopyFromCPU(primesLastOne); - var primesLastSeven = DivisorGenerator.SmallPrimesLastSeven; - DevicePrimesLastSeven = Accelerator.Allocate1D(primesLastSeven.Length); - DevicePrimesLastSeven.View.CopyFromCPU(primesLastSeven); + var primesLastSeven = DivisorGenerator.SmallPrimesLastSeven; + DevicePrimesLastSeven = Accelerator.Allocate1D(primesLastSeven.Length); + DevicePrimesLastSeven.View.CopyFromCPU(primesLastSeven); - var primesLastThree = DivisorGenerator.SmallPrimesLastThree; - DevicePrimesLastThree = Accelerator.Allocate1D(primesLastThree.Length); - DevicePrimesLastThree.View.CopyFromCPU(primesLastThree); + var primesLastThree = DivisorGenerator.SmallPrimesLastThree; + DevicePrimesLastThree = Accelerator.Allocate1D(primesLastThree.Length); + DevicePrimesLastThree.View.CopyFromCPU(primesLastThree); - var primesLastNine = DivisorGenerator.SmallPrimesLastNine; - DevicePrimesLastNine = Accelerator.Allocate1D(primesLastNine.Length); - DevicePrimesLastNine.View.CopyFromCPU(primesLastNine); + var primesLastNine = DivisorGenerator.SmallPrimesLastNine; + DevicePrimesLastNine = Accelerator.Allocate1D(primesLastNine.Length); + DevicePrimesLastNine.View.CopyFromCPU(primesLastNine); - var primesPow2Default = DivisorGenerator.SmallPrimesPow2; - DevicePrimesPow2Default = Accelerator.Allocate1D(primesPow2Default.Length); - DevicePrimesPow2Default.View.CopyFromCPU(primesPow2Default); + var primesPow2Default = DivisorGenerator.SmallPrimesPow2; + DevicePrimesPow2Default = Accelerator.Allocate1D(primesPow2Default.Length); + DevicePrimesPow2Default.View.CopyFromCPU(primesPow2Default); - var primesPow2LastOne = DivisorGenerator.SmallPrimesPow2LastOne; - DevicePrimesPow2LastOne = Accelerator.Allocate1D(primesPow2LastOne.Length); - DevicePrimesPow2LastOne.View.CopyFromCPU(primesPow2LastOne); + var primesPow2LastOne = DivisorGenerator.SmallPrimesPow2LastOne; + DevicePrimesPow2LastOne = Accelerator.Allocate1D(primesPow2LastOne.Length); + DevicePrimesPow2LastOne.View.CopyFromCPU(primesPow2LastOne); - var primesPow2LastSeven = DivisorGenerator.SmallPrimesPow2LastSeven; - DevicePrimesPow2LastSeven = Accelerator.Allocate1D(primesPow2LastSeven.Length); - DevicePrimesPow2LastSeven.View.CopyFromCPU(primesPow2LastSeven); + var primesPow2LastSeven = DivisorGenerator.SmallPrimesPow2LastSeven; + DevicePrimesPow2LastSeven = Accelerator.Allocate1D(primesPow2LastSeven.Length); + DevicePrimesPow2LastSeven.View.CopyFromCPU(primesPow2LastSeven); - var primesPow2LastThree = DivisorGenerator.SmallPrimesPow2LastThree; - DevicePrimesPow2LastThree = Accelerator.Allocate1D(primesPow2LastThree.Length); - DevicePrimesPow2LastThree.View.CopyFromCPU(primesPow2LastThree); + var primesPow2LastThree = DivisorGenerator.SmallPrimesPow2LastThree; + DevicePrimesPow2LastThree = Accelerator.Allocate1D(primesPow2LastThree.Length); + DevicePrimesPow2LastThree.View.CopyFromCPU(primesPow2LastThree); - var primesPow2LastNine = DivisorGenerator.SmallPrimesPow2LastNine; - DevicePrimesPow2LastNine = Accelerator.Allocate1D(primesPow2LastNine.Length); - DevicePrimesPow2LastNine.View.CopyFromCPU(primesPow2LastNine); + var primesPow2LastNine = DivisorGenerator.SmallPrimesPow2LastNine; + DevicePrimesPow2LastNine = Accelerator.Allocate1D(primesPow2LastNine.Length); + DevicePrimesPow2LastNine.View.CopyFromCPU(primesPow2LastNine); - BufferCapacity = minBufferCapacity < 1 ? 1 : minBufferCapacity; - Input = Accelerator.Allocate1D(BufferCapacity); - Output = Accelerator.Allocate1D(BufferCapacity); - HeuristicFlag = Accelerator.Allocate1D(BufferCapacity); - _returned = false; - } + BufferCapacity = minBufferCapacity < 1 ? 1 : minBufferCapacity; + Input = Accelerator.Allocate1D(BufferCapacity); + Output = Accelerator.Allocate1D(BufferCapacity); + HeuristicFlag = Accelerator.Allocate1D(BufferCapacity); + _returned = false; + } - public void EnsureCapacity(int minCapacity) - { - int required = minCapacity < 1 ? 1 : minCapacity; - if (required <= BufferCapacity) - { - return; - } + public void EnsureCapacity(int minCapacity) + { + int required = minCapacity < 1 ? 1 : minCapacity; + if (required <= BufferCapacity) + { + return; + } - Input.Dispose(); - Output.Dispose(); - HeuristicFlag.Dispose(); - Input = Accelerator.Allocate1D(required); - Output = Accelerator.Allocate1D(required); - HeuristicFlag = Accelerator.Allocate1D(required); - BufferCapacity = required; - } + Input.Dispose(); + Output.Dispose(); + HeuristicFlag.Dispose(); + Input = Accelerator.Allocate1D(required); + Output = Accelerator.Allocate1D(required); + HeuristicFlag = Accelerator.Allocate1D(required); + BufferCapacity = required; + } - internal void ResetReturnFlag() - { - _returned = false; - } + internal void ResetReturnFlag() + { + _returned = false; + } - public void Dispose() - { - if (_returned) - { - return; - } + public void Dispose() + { + if (_returned) + { + return; + } - _returned = true; - PrimeTesterGpuContextPool.Return(this); - } + _returned = true; + PrimeTesterGpuContextPool.Return(this); + } - internal void DisposeResources() - { - Input.Dispose(); - Output.Dispose(); - HeuristicFlag.Dispose(); - DevicePrimesDefault.Dispose(); - DevicePrimesLastOne.Dispose(); - DevicePrimesLastSeven.Dispose(); - DevicePrimesLastThree.Dispose(); - DevicePrimesLastNine.Dispose(); - DevicePrimesPow2Default.Dispose(); - DevicePrimesPow2LastOne.Dispose(); - DevicePrimesPow2LastSeven.Dispose(); - DevicePrimesPow2LastThree.Dispose(); - DevicePrimesPow2LastNine.Dispose(); - Accelerator.Dispose(); - AcceleratorContext.Dispose(); - } - } - } - // Expose cache clearing for accelerator disposal coordination + internal void DisposeResources() + { + Input.Dispose(); + Output.Dispose(); + HeuristicFlag.Dispose(); + DevicePrimesDefault.Dispose(); + DevicePrimesLastOne.Dispose(); + DevicePrimesLastSeven.Dispose(); + DevicePrimesLastThree.Dispose(); + DevicePrimesLastNine.Dispose(); + DevicePrimesPow2Default.Dispose(); + DevicePrimesPow2LastOne.Dispose(); + DevicePrimesPow2LastSeven.Dispose(); + DevicePrimesPow2LastThree.Dispose(); + DevicePrimesPow2LastNine.Dispose(); + Accelerator.Dispose(); + AcceleratorContext.Dispose(); + } + } + } + // Expose cache clearing for accelerator disposal coordination public static void ClearGpuCaches(Accelerator accelerator) { PrimeTesterGpuContextPool.Clear(accelerator);