diff --git a/EvenPerfectBitScanner.Tests/ProgramTests.cs b/EvenPerfectBitScanner.Tests/ProgramTests.cs index c4e4182..2afa0cf 100644 --- a/EvenPerfectBitScanner.Tests/ProgramTests.cs +++ b/EvenPerfectBitScanner.Tests/ProgramTests.cs @@ -360,6 +360,48 @@ public class ProgramTests } } + [Fact] + [Trait("Category", "Fast")] + public void Residue_mode_accepts_known_primes_in_parallel() + { + var useDivisorField = typeof(Program).GetField("_useDivisor", BindingFlags.NonPublic | BindingFlags.Static)!; + var mersenneField = typeof(Program).GetField("MersenneTesters", BindingFlags.NonPublic | BindingFlags.Static)!; + var primeField = typeof(Program).GetField("PrimeTesters", BindingFlags.NonPublic | BindingFlags.Static)!; + var residueField = typeof(Program).GetField("PResidue", BindingFlags.NonPublic | BindingFlags.Static)!; + var gcdFilterField = typeof(Program).GetField("_useGcdFilter", BindingFlags.NonPublic | BindingFlags.Static)!; + var forceCpuProp = typeof(GpuContextPool).GetProperty("ForceCpu"); + + useDivisorField.SetValue(null, false); + gcdFilterField.SetValue(null, false); + mersenneField.SetValue(null, new ThreadLocal(() => new MersenneNumberTester( + useIncremental: true, + useResidue: true, + maxK: 5_000_000UL, + residueDivisorSets: PerfectNumberConstants.ExtraDivisorCycleSearchLimit), trackAllValues: true)); + primeField.SetValue(null, new ThreadLocal(() => new PrimeTester(), trackAllValues: true)); + residueField.SetValue(null, new ThreadLocal(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true)); + forceCpuProp!.SetValue(null, false); + + try + { + ulong[] primes = [107UL, 127UL, 521UL, 607UL]; + Parallel.ForEach(primes, prime => + { + Program.IsEvenPerfectCandidate(prime, 0UL, out bool searched, out bool detailed).Should().BeTrue(); + searched.Should().BeTrue(); + detailed.Should().BeTrue(); + }); + } + finally + { + useDivisorField.SetValue(null, false); + mersenneField.SetValue(null, null); + primeField.SetValue(null, null); + residueField.SetValue(null, null); + forceCpuProp!.SetValue(null, false); + } + } + [Fact] public void Residue_mode_with_default_limits_accepts_large_known_primes() { diff --git a/PerfectNumbers.Core.Tests/PrimeTester/PrimeTesterIsPrimeTests.cs b/PerfectNumbers.Core.Tests/PrimeTester/PrimeTesterIsPrimeTests.cs index 9b2b6a9..7f583ac 100644 --- a/PerfectNumbers.Core.Tests/PrimeTester/PrimeTesterIsPrimeTests.cs +++ b/PerfectNumbers.Core.Tests/PrimeTester/PrimeTesterIsPrimeTests.cs @@ -36,5 +36,33 @@ public class PrimeTesterIsPrimeTests GpuContextPool.ForceCpu = false; } + + [Fact] + [Trait("Category", "Fast")] + public void IsPrimeGpu_handles_parallel_calls_for_known_primes() + { + GpuContextPool.ForceCpu = false; + GpuPrimeWorkLimiter.SetLimit(64); + PrimeTester.GpuBatchSize = 256_000; + + ulong[] primes = [107UL, 127UL, 521UL, 607UL]; + var results = new bool[primes.Length]; + + var testers = new ThreadLocal(() => new PrimeTester(), trackAllValues: true); + Parallel.For(0, primes.Length, i => + { + results[i] = testers.Value!.IsPrimeGpu(primes[i], CancellationToken.None); + }); + + results.Should().AllBeEquivalentTo(true); + + Parallel.For(0, Environment.ProcessorCount * 16, _ => + { + foreach (ulong prime in primes) + { + testers.Value!.IsPrimeGpu(prime, CancellationToken.None).Should().BeTrue(); + } + }); + } } diff --git a/PerfectNumbers.Core/PrimeTester.cs b/PerfectNumbers.Core/PrimeTester.cs index 97efd4d..5d83d5a 100644 --- a/PerfectNumbers.Core/PrimeTester.cs +++ b/PerfectNumbers.Core/PrimeTester.cs @@ -83,48 +83,53 @@ public sealed class PrimeTester(bool useInternal = false) // Limit concurrency and declare variables outside loops for performance and reuse var limiter = GpuPrimeWorkLimiter.Acquire(); var gpu = GpuContextPool.Rent(); - var accelerator = gpu.Accelerator; - // Get per-accelerator cached kernel and device primes buffer. - var state = GpuKernelState.GetOrCreate(accelerator); - - MemoryBuffer1D input; - MemoryBuffer1D output; - int totalLength, batchSize, pos, remaining, count; - ulong[] temp; - - totalLength = values.Length; - batchSize = Math.Max(1, GpuBatchSize); - - // Rent per-call scratch buffers sized to current batch (thread-safe) - var scratch = state.RentScratch(batchSize, accelerator); - input = scratch.Input; - output = scratch.Output; - - // Reusable host buffer per call - temp = ArrayPool.Shared.Rent(batchSize); - - pos = 0; - while (pos < totalLength) + try { - remaining = totalLength - pos; - count = remaining > batchSize ? batchSize : remaining; + var accelerator = gpu.Accelerator; - // Copy chunk to host temp then to device - values.Slice(pos, count).CopyTo(temp); - input.View.CopyFromCPU(ref temp[0], count); + // Get per-accelerator cached kernel and device primes buffer. + var state = GpuKernelState.GetOrCreate(accelerator); + int totalLength = values.Length; + int batchSize = Math.Max(1, GpuBatchSize); - state.Kernel(count, input.View, state.DevicePrimes.View, output.View); - accelerator.Synchronize(); - output.View.CopyToCPU(ref results[pos], count); + lock (gpu.ExecutionLock) + { + var scratch = state.RentScratch(batchSize, accelerator); + var input = scratch.Input; + var output = scratch.Output; + ulong[] temp = ArrayPool.Shared.Rent(batchSize); - pos += count; + try + { + 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); + + state.Kernel(count, input.View, state.DevicePrimes.View, output.View); + accelerator.Synchronize(); + output.View.CopyToCPU(ref results[pos], count); + + pos += count; + } + } + finally + { + ArrayPool.Shared.Return(temp); + state.ReturnScratch(scratch); + } + } + } + finally + { + gpu.Dispose(); + limiter.Dispose(); } - - ArrayPool.Shared.Return(temp); - state.ReturnScratch(scratch); - gpu.Dispose(); - limiter.Dispose(); } // Per-accelerator GPU state for prime sieve (kernel + uploaded primes).