diff --git a/PerfectNumbers.Core.Tests/MersenneNumberResidueCpuTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberResidueCpuTesterTests.cs index 8270f37..c6fb890 100644 --- a/PerfectNumbers.Core.Tests/MersenneNumberResidueCpuTesterTests.cs +++ b/PerfectNumbers.Core.Tests/MersenneNumberResidueCpuTesterTests.cs @@ -33,19 +33,52 @@ public class MersenneNumberResidueCpuTesterTests } } + [Theory] + [InlineData(31UL)] + [Trait("Category", "Fast")] + public void Scan_matches_cli_residue_configuration_for_primes(ulong exponent) + { + RunCase(new MersenneNumberResidueCpuTester(), exponent, 32UL, expectedPrime: true); + } + + [Theory] + [InlineData(31UL, 1_024UL, 1UL)] + [InlineData(31UL, 512UL, 2UL)] + [InlineData(31UL, 256UL, 4UL)] + [InlineData(127UL, 256UL, 1UL)] + [InlineData(127UL, 128UL, 2UL)] + [InlineData(127UL, 64UL, 4UL)] + [Trait("Category", "Fast")] + public void Scan_handles_multiple_residue_sets_for_known_primes(ulong exponent, ulong perSetLimit, ulong setCount) + { + ulong overallLimit = perSetLimit * setCount; + RunCase(new MersenneNumberResidueCpuTester(), exponent, perSetLimit, setCount, overallLimit, expectedPrime: true); + } + private static void RunCase(MersenneNumberResidueCpuTester tester, ulong exponent, ulong maxK, bool expectedPrime) + { + RunCase(tester, exponent, maxK, 1UL, maxK, expectedPrime); + } + + private static void RunCase( + MersenneNumberResidueCpuTester tester, + ulong exponent, + ulong perSetLimit, + ulong setCount, + ulong overallLimit, + bool expectedPrime) { bool isPrime = true; bool exhausted = false; tester.Scan( - exponent, - (UInt128)exponent << 1, - LastDigitIsSeven(exponent), - (UInt128)maxK, - UInt128.One, - (UInt128)maxK, - ref isPrime, - ref exhausted); + exponent, + (UInt128)exponent << 1, + LastDigitIsSeven(exponent), + (UInt128)perSetLimit, + (UInt128)setCount, + (UInt128)overallLimit, + ref isPrime, + ref exhausted); isPrime.Should().Be(expectedPrime); exhausted.Should().BeTrue(); } diff --git a/PerfectNumbers.Core.Tests/MersenneNumberResidueGpuTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberResidueGpuTesterTests.cs index e3e1fed..6c473f2 100644 --- a/PerfectNumbers.Core.Tests/MersenneNumberResidueGpuTesterTests.cs +++ b/PerfectNumbers.Core.Tests/MersenneNumberResidueGpuTesterTests.cs @@ -39,6 +39,32 @@ public class MersenneNumberResidueGpuTesterTests } } + [Theory] + [InlineData(31UL)] + [Trait("Category", "Fast")] + public void Scan_matches_cli_residue_configuration_for_primes(ulong exponent) + { + RunCliCase(useGpuOrder: false, exponent); + RunCliCase(useGpuOrder: true, exponent); + } + + [Theory] + [InlineData(31UL, 1_024UL, 1UL)] + [InlineData(31UL, 256UL, 4UL)] + [InlineData(107UL, 512UL, 1UL)] + [InlineData(107UL, 128UL, 4UL)] + [InlineData(107UL, 15_625UL, 64UL)] + [InlineData(127UL, 512UL, 1UL)] + [InlineData(127UL, 128UL, 4UL)] + [Trait("Category", "Fast")] + public void Scan_handles_multiple_residue_sets_for_known_primes(ulong exponent, ulong perSetLimit, ulong setCount) + { + ulong overallLimit = perSetLimit * setCount; + + RunCase(new MersenneNumberResidueGpuTester(useGpuOrder: false), exponent, perSetLimit, setCount, overallLimit, expectedPrime: true); + RunCase(new MersenneNumberResidueGpuTester(useGpuOrder: true), exponent, perSetLimit, setCount, overallLimit, expectedPrime: true); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -86,19 +112,36 @@ public class MersenneNumberResidueGpuTesterTests exhausted.Should().BeTrue(); } + private static void RunCliCase(bool useGpuOrder, ulong exponent) + { + var tester = new MersenneNumberResidueGpuTester(useGpuOrder); + RunCase(tester, exponent, 8UL, expectedPrime: true); + } + private static void RunCase(MersenneNumberResidueGpuTester tester, ulong exponent, ulong maxK, bool expectedPrime) + { + RunCase(tester, exponent, maxK, 1UL, maxK, expectedPrime); + } + + private static void RunCase( + MersenneNumberResidueGpuTester tester, + ulong exponent, + ulong perSetLimit, + ulong setCount, + ulong overallLimit, + bool expectedPrime) { bool isPrime = true; bool exhausted = false; tester.Scan( - exponent, - (UInt128)exponent << 1, - LastDigitIsSeven(exponent), - (UInt128)maxK, - UInt128.One, - (UInt128)maxK, - ref isPrime, - ref exhausted); + exponent, + (UInt128)exponent << 1, + LastDigitIsSeven(exponent), + (UInt128)perSetLimit, + (UInt128)setCount, + (UInt128)overallLimit, + ref isPrime, + ref exhausted); isPrime.Should().Be(expectedPrime); exhausted.Should().BeTrue(); } diff --git a/PerfectNumbers.Core.Tests/MersenneNumberResidueIntegrationTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberResidueIntegrationTests.cs new file mode 100644 index 0000000..b4041e3 --- /dev/null +++ b/PerfectNumbers.Core.Tests/MersenneNumberResidueIntegrationTests.cs @@ -0,0 +1,172 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using PerfectNumbers.Core; +using PerfectNumbers.Core.Gpu; +using Xunit; +using UInt128 = System.UInt128; + +namespace PerfectNumbers.Core.Tests; + +public class MersenneNumberResidueIntegrationTests +{ + public static IEnumerable ResidueThreadCounts() + { + yield return new object[] { 1 }; + yield return new object[] { 2 }; + yield return new object[] { 4 }; + } + + [Theory] + [MemberData(nameof(ResidueThreadCounts))] + [Trait("Category", "Fast")] + public void Residue_gpu_mode_matches_cli_configuration_for_known_primes(int threadCount) + { + ulong[] exponents = MersennePrimeTestData.Exponents; + ulong[][] partitions = PartitionExponents(exponents, threadCount); + var results = new ConcurrentDictionary(); + UInt128 residueSetCount = (UInt128)PerfectNumberConstants.ExtraDivisorCycleSearchLimit; + + try + { + Task[] tasks = partitions.Select(partition => Task.Run(() => + { + if (partition.Length == 0) + { + return; + } + + var tester = new MersenneNumberTester( + useIncremental: false, + useOrderCache: false, + kernelType: GpuKernelType.Pow2Mod, + useModuloWorkaround: false, + useOrder: false, + useGpuLucas: false, + useGpuScan: true, + useGpuOrder: true, + useResidue: true, + maxK: (UInt128)1_024UL, + residueDivisorSets: residueSetCount); + + foreach (ulong exponent in partition) + { + bool isPrime = tester.IsMersennePrime(exponent, out bool divisorsExhausted); + results[exponent] = (isPrime, divisorsExhausted); + } + })).ToArray(); + + Task.WaitAll(tasks); + } + finally + { + GpuContextPool.DisposeAll(); + } + + results.Keys.Should().BeEquivalentTo(exponents); + foreach (ulong exponent in exponents) + { + results[exponent].Should().Be((true, true)); + } + } + + [Theory] + [MemberData(nameof(ResidueThreadCounts))] + [Trait("Category", "Fast")] + public void Residue_cpu_mode_matches_cli_configuration_for_known_primes(int threadCount) + { + ulong[] exponents = MersennePrimeTestData.Exponents; + ulong[][] partitions = PartitionExponents(exponents, threadCount); + var results = new ConcurrentDictionary(); + UInt128 residueSetCount = (UInt128)PerfectNumberConstants.ExtraDivisorCycleSearchLimit; + + Task[] tasks = partitions.Select(partition => Task.Run(() => + { + if (partition.Length == 0) + { + return; + } + + var tester = new MersenneNumberTester( + useIncremental: false, + useOrderCache: false, + kernelType: GpuKernelType.Pow2Mod, + useModuloWorkaround: false, + useOrder: false, + useGpuLucas: false, + useGpuScan: false, + useGpuOrder: false, + useResidue: true, + maxK: (UInt128)1_024UL, + residueDivisorSets: residueSetCount); + + foreach (ulong exponent in partition) + { + bool isPrime = tester.IsMersennePrime(exponent, out bool divisorsExhausted); + results[exponent] = (isPrime, divisorsExhausted); + } + })).ToArray(); + + Task.WaitAll(tasks); + + results.Keys.Should().BeEquivalentTo(exponents); + foreach (ulong exponent in exponents) + { + results[exponent].Should().Be((true, true)); + } + } + + [Fact] + [Trait("Category", "Fast")] + public void Residue_cpu_mode_respects_filter_configuration() + { + var filter = new HashSet(MersennePrimeTestData.Exponents); + ulong[] candidates = [31UL, 37UL, 73UL]; + + var tester = new MersenneNumberTester( + useIncremental: false, + useOrderCache: false, + kernelType: GpuKernelType.Pow2Mod, + useModuloWorkaround: false, + useOrder: false, + useGpuLucas: false, + useGpuScan: false, + useGpuOrder: false, + useResidue: true, + maxK: (UInt128)32UL, + residueDivisorSets: (UInt128)4UL); + + List processed = new(); + List skipped = new(); + + filter.Count.Should().Be(MersennePrimeTestData.Exponents.Length); + + foreach (ulong exponent in candidates) + { + if (!filter.Contains(exponent)) + { + skipped.Add(exponent); + continue; + } + + bool isPrime = tester.IsMersennePrime(exponent, out bool divisorsExhausted); + isPrime.Should().BeTrue(); + divisorsExhausted.Should().BeTrue(); + processed.Add(exponent); + } + + processed.Should().ContainSingle().Which.Should().Be(31UL); + skipped.Should().BeEquivalentTo(new[] { 37UL, 73UL }); + } + + private static ulong[][] PartitionExponents(ulong[] exponents, int threadCount) + { + return Enumerable + .Range(0, threadCount) + .Select(partition => exponents.Where((_, index) => index % threadCount == partition).ToArray()) + .ToArray(); + } +} +