diff --git a/PerfectNumbers.Core.Tests/MersenneNumberOrderGpuTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberOrderGpuTesterTests.cs index 0751886..3af8b5b 100644 --- a/PerfectNumbers.Core.Tests/MersenneNumberOrderGpuTesterTests.cs +++ b/PerfectNumbers.Core.Tests/MersenneNumberOrderGpuTesterTests.cs @@ -1,4 +1,9 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; using FluentAssertions; +using PerfectNumbers.Core; using PerfectNumbers.Core.Gpu; using Xunit; using UInt128 = System.UInt128; @@ -28,6 +33,60 @@ public class MersenneNumberOrderGpuTesterTests RunCase(tester, 127UL, 1_000UL, expectedPrime: true); } + [Fact] + [Trait("Category", "Fast")] + public void Scan_order_gpu_handles_inaccurate_small_cycle_entries() + { + const ulong exponent = 23UL; + const ulong divisor = 47UL; // 47 | M_23 + + string tempPath = Path.GetTempFileName(); + try + { + using (var writer = new BinaryWriter(File.Open(tempPath, FileMode.Create, FileAccess.Write, FileShare.Read))) + { + writer.Write(divisor); + writer.Write((ulong)uint.MaxValue); + } + + var cycles = MersenneDivisorCycles.Shared; + var tableField = typeof(MersenneDivisorCycles).GetField("_table", BindingFlags.NonPublic | BindingFlags.Instance)!; + var smallCyclesField = typeof(MersenneDivisorCycles).GetField("_smallCycles", BindingFlags.NonPublic | BindingFlags.Instance)!; + + var originalTable = ((List<(ulong Divisor, ulong Cycle)>)tableField.GetValue(cycles)!).Select(x => x).ToList(); + var originalSmall = (uint[]?)smallCyclesField.GetValue(cycles); + uint[]? backupSmall = originalSmall is null ? null : (uint[])originalSmall.Clone(); + + cycles.LoadFrom(tempPath); + GpuContextPool.DisposeAll(); + + try + { + var tester = new MersenneNumberOrderGpuTester(GpuKernelType.Pow2Mod, useGpuOrder: false); + + bool isPrime = true; + tester.Scan( + exponent, + (UInt128)exponent << 1, + LastDigitIsSeven(exponent), + (UInt128)10UL, + ref isPrime); + + isPrime.Should().BeFalse(); + } + finally + { + tableField.SetValue(cycles, originalTable); + smallCyclesField.SetValue(cycles, backupSmall); + GpuContextPool.DisposeAll(); + } + } + finally + { + File.Delete(tempPath); + } + } + private static void RunCase(MersenneNumberOrderGpuTester tester, ulong exponent, ulong maxK, bool expectedPrime) { bool isPrime = true; diff --git a/PerfectNumbers.Core.Tests/MersenneNumberResidueGpuTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberResidueGpuTesterTests.cs index 8867345..a721d33 100644 --- a/PerfectNumbers.Core.Tests/MersenneNumberResidueGpuTesterTests.cs +++ b/PerfectNumbers.Core.Tests/MersenneNumberResidueGpuTesterTests.cs @@ -1,4 +1,8 @@ +using System.IO; +using System.Linq; +using System.Reflection; using FluentAssertions; +using PerfectNumbers.Core; using PerfectNumbers.Core.Gpu; using Xunit; using UInt128 = System.UInt128; @@ -25,6 +29,66 @@ public class MersenneNumberResidueGpuTesterTests RunCase(tester, 127UL, 1_001UL, expectedPrime: true); } + [Fact] + [Trait("Category", "Fast")] + public void Scan_residue_gpu_handles_inaccurate_small_cycle_entries() + { + const ulong exponent = 23UL; + const ulong divisor = 47UL; // 47 | M_23 + + string tempPath = Path.GetTempFileName(); + try + { + using (var writer = new BinaryWriter(File.Open(tempPath, FileMode.Create, FileAccess.Write, FileShare.Read))) + { + writer.Write(divisor); + writer.Write((ulong)uint.MaxValue); + } + + var cycles = MersenneDivisorCycles.Shared; + var tableField = typeof(MersenneDivisorCycles).GetField("_table", BindingFlags.NonPublic | BindingFlags.Instance)!; + var smallCyclesField = typeof(MersenneDivisorCycles).GetField("_smallCycles", BindingFlags.NonPublic | BindingFlags.Instance)!; + + var originalTable = ((List<(ulong Divisor, ulong Cycle)>)tableField.GetValue(cycles)!).Select(x => x).ToList(); + var originalSmall = (uint[]?)smallCyclesField.GetValue(cycles); + uint[]? backupSmall = originalSmall is null ? null : (uint[])originalSmall.Clone(); + + cycles.LoadFrom(tempPath); + GpuContextPool.DisposeAll(); + + try + { + var tester = new MersenneNumberResidueGpuTester(useGpuOrder: false); + + bool isPrime = true; + bool exhausted = false; + + tester.Scan( + exponent, + (UInt128)exponent << 1, + lastIsSeven: true, + perSetLimit: (UInt128)10UL, + setCount: UInt128.One, + overallLimit: (UInt128)10UL, + ref isPrime, + ref exhausted); + + isPrime.Should().BeFalse(); + exhausted.Should().BeTrue(); + } + finally + { + tableField.SetValue(cycles, originalTable); + smallCyclesField.SetValue(cycles, backupSmall); + GpuContextPool.DisposeAll(); + } + } + finally + { + File.Delete(tempPath); + } + } + [Theory] [InlineData(false)] [InlineData(true)] diff --git a/PerfectNumbers.Core.Tests/MersenneNumberResidueIntegrationTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberResidueIntegrationTests.cs index 903b04b..e6770b7 100644 --- a/PerfectNumbers.Core.Tests/MersenneNumberResidueIntegrationTests.cs +++ b/PerfectNumbers.Core.Tests/MersenneNumberResidueIntegrationTests.cs @@ -51,6 +51,38 @@ public class MersenneNumberResidueIntegrationTests } } + [Theory] + [InlineData(107UL)] + [InlineData(127UL)] + [Trait("Category", "Fast")] + public void Residue_gpu_mode_matches_cli_limits_for_known_primes(ulong exponent) + { + var tester = new MersenneNumberTester( + useIncremental: true, + useOrderCache: false, + kernelType: GpuKernelType.Pow2Mod, + useModuloWorkaround: false, + useOrder: false, + useGpuLucas: false, + useGpuScan: true, + useGpuOrder: true, + useResidue: true, + maxK: (UInt128)5_000_000UL, + residueDivisorSets: (UInt128)1_024UL); + + try + { + bool isPrime = tester.IsMersennePrime(exponent, out bool divisorsExhausted); + + isPrime.Should().BeTrue(); + divisorsExhausted.Should().BeTrue(); + } + finally + { + GpuContextPool.DisposeAll(); + } + } + [Theory] [MemberData(nameof(ResidueThreadCounts))] [Trait("Category", "Fast")] diff --git a/PerfectNumbers.Core/Gpu/GpuKernelPool.cs b/PerfectNumbers.Core/Gpu/GpuKernelPool.cs index 9082f4d..f73a69a 100644 --- a/PerfectNumbers.Core/Gpu/GpuKernelPool.cs +++ b/PerfectNumbers.Core/Gpu/GpuKernelPool.cs @@ -246,10 +246,14 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne if (q.High == 0UL && q.Low < (ulong)smallCycles.Length) { uint cyc = smallCycles[(int)q.Low]; - if (cyc != 0U && (exponent % cyc) != 0UL) + if (cyc != 0U) { - orders[index] = 0UL; - return; + ulong cycle = cyc; + if (cycle <= exponent && (exponent % cycle) != 0UL) + { + orders[index] = 0UL; + return; + } } } @@ -396,10 +400,14 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne if (q.High == 0UL && q.Low < (ulong)smallCycles.Length) { uint cyc = smallCycles[(int)q.Low]; - if (cyc != 0U && (exponent % cyc) != 0UL) + if (cyc != 0U) { - orders[index] = 0UL; - return; + ulong cycle = cyc; + if (cycle <= exponent && (exponent % cycle) != 0UL) + { + orders[index] = 0UL; + return; + } } } if (GpuUInt128.Pow2Minus1Mod(exponent, q) != GpuUInt128.Zero) @@ -524,9 +532,13 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne if (q.High == 0UL && q.Low < (ulong)smallCycles.Length) { uint cyc = smallCycles[(int)q.Low]; - if (cyc != 0U && (exponent % (ulong)cyc) != 0UL) + if (cyc != 0U) { - return; + ulong cycle = cyc; + if (cycle <= exponent && (exponent % cycle) != 0UL) + { + return; + } } } GpuUInt128 phi = q - GpuUInt128.One; @@ -603,9 +615,13 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne if (q.High == 0UL && q.Low < (ulong)smallCycles.Length) { uint cyc = smallCycles[(int)q.Low]; - if (cyc != 0U && (exponent % (ulong)cyc) != 0UL) + if (cyc != 0U) { - return; + ulong cycle = cyc; + if (cycle <= exponent && (exponent % cycle) != 0UL) + { + return; + } } } if (GpuUInt128.Pow2Mod(exponent, q) != GpuUInt128.One)