diff --git a/EvenPerfectBitScanner.Benchmarks/Pow2ModBenchmarks.cs b/EvenPerfectBitScanner.Benchmarks/Pow2ModBenchmarks.cs index 6245095..a50be82 100644 --- a/EvenPerfectBitScanner.Benchmarks/Pow2ModBenchmarks.cs +++ b/EvenPerfectBitScanner.Benchmarks/Pow2ModBenchmarks.cs @@ -28,18 +28,18 @@ public class Pow2ModBenchmarks private const long OneShiftedLeft60 = 1L << 60; private const ulong Pow2WindowFallbackThreshold = 32UL; - private MontgomeryDivisorData[] _smallDivisors = Array.Empty(); - private MontgomeryDivisorData[] _largeDivisors = Array.Empty(); - private MontgomeryDivisorData[] _veryLargeDivisors = Array.Empty(); - private UInt128[] _wideModuli = Array.Empty(); - private UInt128[] _wideExponents = Array.Empty(); - private ulong[] _smallExponents = Array.Empty(); - private ulong[] _largeExponents = Array.Empty(); - private ulong[] _veryLargeExponents = Array.Empty(); - private ulong[] _smallCycles = Array.Empty(); - private ulong[] _largeCycles = Array.Empty(); - private ulong[] _veryLargeCycles = Array.Empty(); - private UInt128[] _wideCycles = Array.Empty(); + private MontgomeryDivisorData[] _smallDivisors = []; + private MontgomeryDivisorData[] _largeDivisors = []; + private MontgomeryDivisorData[] _veryLargeDivisors = []; + private UInt128[] _wideModuli = []; + private UInt128[] _wideExponents = []; + private ulong[] _smallExponents = []; + private ulong[] _largeExponents = []; + private ulong[] _veryLargeExponents = []; + private ulong[] _smallCycles = []; + private ulong[] _largeCycles = []; + private ulong[] _veryLargeCycles = []; + private UInt128[] _wideCycles = []; private ulong? _previousPrimeOrder; private UInt128? _previousWidePrimeOrder; @@ -82,7 +82,7 @@ public class Pow2ModBenchmarks for (int i = 0; i < SampleCount; i++) { - checksum ^= exponents[i].Pow2MontgomeryModWindowedCpu(divisors[i], keepMontgomery: false); + checksum ^= exponents[i].Pow2MontgomeryModWindowedConvertToStandardCpu(divisors[i]); } return checksum; @@ -120,7 +120,7 @@ public class Pow2ModBenchmarks for (int i = 0; i < SampleCount; i++) { - checksum ^= exponents[i].Pow2MontgomeryModWithCycleCpu(cycles[i], divisors[i]); + checksum ^= exponents[i].Pow2MontgomeryModWithCycleConvertToStandardCpu(cycles[i], divisors[i]); } return checksum; @@ -162,7 +162,7 @@ public class Pow2ModBenchmarks for (int i = 0; i < SampleCount; i++) { ulong cycle = MersenneDivisorCycles.CalculateCycleLengthGpu(divisors[i].Modulus); - checksum ^= exponents[i].Pow2MontgomeryModWithCycleCpu(cycle, divisors[i]); + checksum ^= exponents[i].Pow2MontgomeryModWithCycleConvertToStandardCpu(cycle, divisors[i]); } return checksum; @@ -200,8 +200,8 @@ public class Pow2ModBenchmarks var gpu = PrimeOrderCalculatorAccelerator.Rent(1); for (int i = 0; i < SampleCount; i++) { - ulong cycle = CalculateCycleLengthWithHeuristics(gpu, divisors[i].Modulus); - checksum ^= exponents[i].Pow2MontgomeryModWithCycleCpu(cycle, divisors[i]); + ulong cycle = CalculateCycleLengthWithHeuristicsCpu(divisors[i].Modulus); + checksum ^= exponents[i].Pow2MontgomeryModWithCycleConvertToStandardCpu(cycle, divisors[i]); } PrimeOrderCalculatorAccelerator.Return(gpu); @@ -221,7 +221,7 @@ public class Pow2ModBenchmarks var gpu = PrimeOrderCalculatorAccelerator.Rent(1); for (int i = 0; i < SampleCount; i++) { - ulong cycle = CalculateCycleLengthWithHeuristics(gpu, divisors[i].Modulus); + ulong cycle = CalculateCycleLengthWithHeuristicsGpu(gpu, divisors[i].Modulus); checksum ^= exponents[i].Pow2MontgomeryModWithCycleGpu(gpu, cycle, divisors[i]); } @@ -230,14 +230,30 @@ public class Pow2ModBenchmarks } [Benchmark] - public UInt128 HeuristicWideCycleComputation() + public UInt128 HeuristicWideCycleComputationCpu() { UInt128 checksum = UInt128.Zero; var gpu = PrimeOrderCalculatorAccelerator.Rent(1); for (int i = 0; i < SampleCount; i++) { - UInt128 cycle = CalculateCycleLengthWithHeuristics(gpu, _wideModuli[i]); + UInt128 cycle = CalculateCycleLengthWithHeuristicsCpu(_wideModuli[i]); + checksum ^= cycle; + } + + PrimeOrderCalculatorAccelerator.Return(gpu); + return checksum; + } + + [Benchmark] + public UInt128 HeuristicWideCycleComputationGpu() + { + UInt128 checksum = UInt128.Zero; + + var gpu = PrimeOrderCalculatorAccelerator.Rent(1); + for (int i = 0; i < SampleCount; i++) + { + UInt128 cycle = CalculateCycleLengthWithHeuristicsGpu(gpu, _wideModuli[i]); checksum ^= cycle; } @@ -484,7 +500,7 @@ public class Pow2ModBenchmarks Console.WriteLine($"Calculating small cycle {smallModulus}"); smallDivisors[i] = CreateMontgomeryDivisorData(smallModulus); smallExponents[i] = NextSmallExponent(random); - smallCycles[i] = MersenneDivisorCycles.CalculateCycleLength(gpu, smallModulus, MontgomeryDivisorDataPool.Shared.FromModulus(smallModulus)); + smallCycles[i] = MersenneDivisorCycles.CalculateCycleLengthCpu(smallModulus, MontgomeryDivisorDataPool.Shared.FromModulus(smallModulus)); (ulong largeModulus, ulong largeCycle) = NextLargeModulusAndCycle(random); Console.WriteLine($"Calculating large cycle {largeModulus}"); @@ -496,13 +512,13 @@ public class Pow2ModBenchmarks Console.WriteLine($"Calculating very large cycle {veryLargeModulus} with heuristics"); veryLargeDivisors[i] = CreateMontgomeryDivisorData(veryLargeModulus); veryLargeExponents[i] = NextVeryLargeExponent(heuristicRandom); - veryLargeCycles[i] = CalculateCycleLengthWithHeuristics(gpu, veryLargeModulus, ref previousPrimeOrder); + veryLargeCycles[i] = CalculateCycleLengthWithHeuristicsCpu(veryLargeModulus, ref previousPrimeOrder); UInt128 wideModulus = NextWideOddModulus(wideRandom); Console.WriteLine($"Calculating wide cycle {wideModulus} with heuristics"); wideModuli[i] = wideModulus; wideExponents[i] = NextWideExponent(wideRandom); - wideCycles[i] = CalculateCycleLengthWithHeuristics(gpu, wideModulus, ref previousWidePrimeOrder); + wideCycles[i] = CalculateCycleLengthWithHeuristicsCpu(wideModulus, ref previousWidePrimeOrder); } SampleCollections collections = new SampleCollections( @@ -595,21 +611,19 @@ public class Pow2ModBenchmarks return ((UInt128)high << 64) | low; } - private static ulong CalculateCycleLengthWithHeuristics(PrimeOrderCalculatorAccelerator gpu, ulong modulus, ref ulong? previousPrimeOrder) + private static ulong CalculateCycleLengthWithHeuristicsCpu(ulong modulus, ref ulong? previousPrimeOrder) { MontgomeryDivisorData divisorData = MontgomeryDivisorDataPool.Shared.FromModulus(modulus); if (modulus <= 1UL || (modulus & 1UL) == 0UL) { - return MersenneDivisorCycles.CalculateCycleLength(gpu, modulus, divisorData); + return MersenneDivisorCycles.CalculateCycleLengthCpu(modulus, divisorData); } - ulong order = PrimeOrderCalculator.Calculate( - gpu, + ulong order = PrimeOrderCalculator.CalculateCpu( modulus, previousPrimeOrder, divisorData, - PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault, - PrimeOrderCalculator.PrimeOrderHeuristicDevice.Cpu); + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); if (order != 0UL) { @@ -618,22 +632,20 @@ public class Pow2ModBenchmarks } previousPrimeOrder = null; - return MersenneDivisorCycles.CalculateCycleLength(gpu, modulus, divisorData); + return MersenneDivisorCycles.CalculateCycleLengthCpu(modulus, divisorData); } - private static UInt128 CalculateCycleLengthWithHeuristics(PrimeOrderCalculatorAccelerator gpu, UInt128 modulus, ref UInt128? previousWidePrimeOrder) + private static UInt128 CalculateCycleLengthWithHeuristicsCpu(UInt128 modulus, ref UInt128? previousWidePrimeOrder) { if (modulus <= UInt128.One || (modulus & UInt128.One) == UInt128.Zero) { - return MersenneDivisorCycles.GetCycle(gpu, modulus); + return MersenneDivisorCycles.GetCycleCpu(modulus); } - UInt128 order = PrimeOrderCalculator.Calculate( - gpu, + UInt128 order = PrimeOrderCalculator.CalculateCpu( modulus, previousWidePrimeOrder, - PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault, - PrimeOrderCalculator.PrimeOrderHeuristicDevice.Cpu); + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); if (order != UInt128.Zero) { @@ -642,7 +654,7 @@ public class Pow2ModBenchmarks } previousWidePrimeOrder = null; - return MersenneDivisorCycles.GetCycle(gpu, modulus); + return MersenneDivisorCycles.GetCycleCpu(modulus); } } @@ -666,24 +678,22 @@ public class Pow2ModBenchmarks montgomeryTwoSquared); } - private ulong CalculateCycleLengthWithHeuristics(PrimeOrderCalculatorAccelerator gpu, ulong modulus, ulong fallbackOrder = 0UL) + private ulong CalculateCycleLengthWithHeuristicsCpu(ulong modulus, ulong fallbackOrder = 0UL) { MontgomeryDivisorData divisorData = MontgomeryDivisorDataPool.Shared.FromModulus(modulus); if (modulus <= 1UL || (modulus & 1UL) == 0UL) { - return fallbackOrder != 0UL ? fallbackOrder : MersenneDivisorCycles.CalculateCycleLength(gpu, modulus, divisorData); + return fallbackOrder != 0UL ? fallbackOrder : MersenneDivisorCycles.CalculateCycleLengthCpu(modulus, divisorData); } #if DEBUG Console.WriteLine("Trying heuristic. Prime order calculation"); #endif - ulong order = PrimeOrderCalculator.Calculate( - gpu, + ulong order = PrimeOrderCalculator.CalculateCpu( modulus, _previousPrimeOrder, divisorData, - PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault, - PrimeOrderCalculator.PrimeOrderHeuristicDevice.Cpu); + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); if (order != 0UL) { @@ -694,25 +704,53 @@ public class Pow2ModBenchmarks _previousPrimeOrder = null; Console.WriteLine($"Heuristic failed for {modulus}, falling back to full cycle calculation"); - return fallbackOrder != 0UL ? fallbackOrder : MersenneDivisorCycles.CalculateCycleLength(gpu, modulus, divisorData); + return fallbackOrder != 0UL ? fallbackOrder : MersenneDivisorCycles.CalculateCycleLengthCpu(modulus, divisorData); } - private UInt128 CalculateCycleLengthWithHeuristics(PrimeOrderCalculatorAccelerator gpu, UInt128 modulus) + private ulong CalculateCycleLengthWithHeuristicsGpu(PrimeOrderCalculatorAccelerator gpu, ulong modulus, ulong fallbackOrder = 0UL) + { + MontgomeryDivisorData divisorData = MontgomeryDivisorDataPool.Shared.FromModulus(modulus); + if (modulus <= 1UL || (modulus & 1UL) == 0UL) + { + return fallbackOrder != 0UL ? fallbackOrder : MersenneDivisorCycles.CalculateCycleLengthGpu(gpu, modulus, divisorData); + } + +#if DEBUG + Console.WriteLine("Trying heuristic. Prime order calculation"); +#endif + ulong order = PrimeOrderCalculator.CalculateGpu( + gpu, + modulus, + _previousPrimeOrder, + divisorData, + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); + + if (order != 0UL) + { + _previousPrimeOrder = order; + return order; + } + + _previousPrimeOrder = null; + + Console.WriteLine($"Heuristic failed for {modulus}, falling back to full cycle calculation"); + return fallbackOrder != 0UL ? fallbackOrder : MersenneDivisorCycles.CalculateCycleLengthGpu(gpu, modulus, divisorData); + } + + private UInt128 CalculateCycleLengthWithHeuristicsCpu(UInt128 modulus) { if (modulus <= UInt128.One || (modulus & UInt128.One) == UInt128.Zero) { - return MersenneDivisorCycles.GetCycle(gpu, modulus); + return MersenneDivisorCycles.GetCycleCpu(modulus); } #if DEBUG Console.WriteLine("Trying heuristic. Wide prime order calculation"); #endif - UInt128 order = PrimeOrderCalculator.Calculate( - gpu, + UInt128 order = PrimeOrderCalculator.CalculateCpu( modulus, _previousWidePrimeOrder, - PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault, - PrimeOrderCalculator.PrimeOrderHeuristicDevice.Cpu); + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); if (order != UInt128.Zero) { @@ -723,7 +761,35 @@ public class Pow2ModBenchmarks _previousWidePrimeOrder = null; Console.WriteLine($"Wide heuristic failed for {modulus}, falling back to full cycle calculation"); - return MersenneDivisorCycles.GetCycle(gpu, modulus); + return MersenneDivisorCycles.GetCycleCpu(modulus); + } + + private UInt128 CalculateCycleLengthWithHeuristicsGpu(PrimeOrderCalculatorAccelerator gpu, UInt128 modulus) + { + if (modulus <= UInt128.One || (modulus & UInt128.One) == UInt128.Zero) + { + return MersenneDivisorCycles.GetCycleGpu(gpu, modulus); + } + +#if DEBUG + Console.WriteLine("Trying heuristic. Wide prime order calculation"); +#endif + UInt128 order = PrimeOrderCalculator.CalculateGpu( + gpu, + modulus, + _previousWidePrimeOrder, + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); + + if (order != UInt128.Zero) + { + _previousWidePrimeOrder = order; + return order; + } + + _previousWidePrimeOrder = null; + + Console.WriteLine($"Wide heuristic failed for {modulus}, falling back to full cycle calculation"); + return MersenneDivisorCycles.GetCycleGpu(gpu, modulus); } private static ulong ComputeMontgomeryResidue(ulong value, ulong modulus) => (ulong)((UInt128)value * (UInt128.One << 64) % modulus); diff --git a/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModBenchmarks.cs b/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModBenchmarks.cs index 5a3b629..19e72d5 100644 --- a/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModBenchmarks.cs +++ b/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModBenchmarks.cs @@ -39,7 +39,7 @@ public class Pow2MontgomeryModBenchmarks ulong smallModulus = NextSmallOddModulus(); _smallDivisors[i] = CreateMontgomeryDivisorData(smallModulus); _smallExponents[i] = NextSmallExponent(); - _smallCycles[i] = MersenneDivisorCycles.CalculateCycleLength(gpu, smallModulus, MontgomeryDivisorDataPool.Shared.FromModulus(smallModulus)); + _smallCycles[i] = MersenneDivisorCycles.CalculateCycleLengthGpu(gpu, smallModulus, MontgomeryDivisorDataPool.Shared.FromModulus(smallModulus)); Console.WriteLine($"Calculating large cycle {i + 1} with heuristics"); (ulong largeModulus, ulong largeCycle) = NextLargeModulusAndCycle(); @@ -62,7 +62,7 @@ public class Pow2MontgomeryModBenchmarks for (int i = 0; i < SampleCount; i++) { - checksum ^= exponents[i].Pow2MontgomeryModWindowedCpu(divisors[i], keepMontgomery: false); + checksum ^= exponents[i].Pow2MontgomeryModWindowedConvertToStandardCpu(divisors[i]); } return checksum; @@ -100,7 +100,7 @@ public class Pow2MontgomeryModBenchmarks for (int i = 0; i < SampleCount; i++) { - checksum ^= exponents[i].Pow2MontgomeryModWithCycleCpu(cycles[i], divisors[i]); + checksum ^= exponents[i].Pow2MontgomeryModWithCycleConvertToStandardCpu(cycles[i], divisors[i]); } return checksum; diff --git a/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModWindowedBenchmarkData.cs b/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModWindowedBenchmarkData.cs index afb8550..fbd9045 100644 --- a/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModWindowedBenchmarkData.cs +++ b/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModWindowedBenchmarkData.cs @@ -67,7 +67,7 @@ internal static class Pow2MontgomeryModWindowedBenchmarkInputsProvider Pow2MontgomeryModWindowedBenchmarkSeed seed = seeds[i]; ulong modulus = 2UL * seed.Multiplier * seed.Exponent + 1UL; MontgomeryDivisorData divisor = MontgomeryDivisorDataPool.Shared.FromModulus(modulus); - ulong cycleLength = MersenneDivisorCycles.CalculateCycleLength(gpu, modulus, divisor); + ulong cycleLength = MersenneDivisorCycles.CalculateCycleLengthGpu(gpu, modulus, divisor); ulong reducedExponent = seed.Exponent % cycleLength; cases[i] = new Pow2MontgomeryModWindowedBenchmarkCase( seed.Exponent, diff --git a/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModWindowedKeepMontgomeryCpuBenchmarks.cs b/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModWindowedKeepMontgomeryCpuBenchmarks.cs index 99ad30a..793e23e 100644 --- a/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModWindowedKeepMontgomeryCpuBenchmarks.cs +++ b/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModWindowedKeepMontgomeryCpuBenchmarks.cs @@ -32,7 +32,7 @@ public class Pow2MontgomeryModWindowedKeepMontgomeryCpuBenchmarks for (int i = 0; i < cases.Length; i++) { ref readonly Pow2MontgomeryModWindowedBenchmarkCase current = ref cases[i]; - checksum ^= current.Exponent.Pow2MontgomeryModWindowedCpu(current.Divisor, keepMontgomery: false); + checksum ^= current.Exponent.Pow2MontgomeryModWindowedConvertToStandardCpu(current.Divisor); } return checksum; @@ -50,7 +50,7 @@ public class Pow2MontgomeryModWindowedKeepMontgomeryCpuBenchmarks for (int i = 0; i < cases.Length; i++) { ref readonly Pow2MontgomeryModWindowedBenchmarkCase current = ref cases[i]; - checksum ^= current.Exponent.Pow2MontgomeryModWindowedCpu(current.Divisor, keepMontgomery: true); + checksum ^= current.Exponent.Pow2MontgomeryModWindowedKeepMontgomeryCpu(current.Divisor); } return checksum; @@ -68,7 +68,7 @@ public class Pow2MontgomeryModWindowedKeepMontgomeryCpuBenchmarks for (int i = 0; i < cases.Length; i++) { ref readonly Pow2MontgomeryModWindowedBenchmarkCase current = ref cases[i]; - checksum ^= current.Exponent.Pow2MontgomeryModWithCycleCpu(current.CycleLength, current.Divisor); + checksum ^= current.Exponent.Pow2MontgomeryModWithCycleConvertToStandardCpu(current.CycleLength, current.Divisor); } return checksum; diff --git a/EvenPerfectBitScanner.Tests/ProgramTests.cs b/EvenPerfectBitScanner.Tests/ProgramTests.cs index 9e3e315..b0b2437 100644 --- a/EvenPerfectBitScanner.Tests/ProgramTests.cs +++ b/EvenPerfectBitScanner.Tests/ProgramTests.cs @@ -542,7 +542,7 @@ public class ProgramTests string output = writer.ToString(); output.Should().Contain("Usage:"); - output.Should().Contain("--mersenne-device=cpu|gpu"); + output.Should().Contain("--mersenne-device=cpu|gpu|hybrid"); output.Should().Contain("--primes-device=cpu|gpu"); } } diff --git a/EvenPerfectBitScanner/Candidates/CandidatesCalculator.cs b/EvenPerfectBitScanner/Candidates/CandidatesCalculator.cs index d6eac5c..965b2f5 100644 --- a/EvenPerfectBitScanner/Candidates/CandidatesCalculator.cs +++ b/EvenPerfectBitScanner/Candidates/CandidatesCalculator.cs @@ -54,7 +54,7 @@ internal static class CandidatesCalculator return trackers.Value!; } - internal static bool IsCompositeByResidues(PrimeOrderCalculatorAccelerator gpu, ulong p) + internal static bool IsCompositeByResiduesCpu(ulong p) { ModResidueTracker tracker = AcquireResidueTracker(); // Use ModResidueTracker with a small set of primes to pre-filter composite p. @@ -72,7 +72,57 @@ internal static class CandidatesCalculator break; } - if (tracker.MergeOrAppend(gpu, p, primes[primeIndex], out bool divisible) && divisible) + if (tracker.MergeOrAppendCpu(p, primes[primeIndex], out bool divisible) && divisible) + { + return true; + } + } + + return false; + } + + internal static bool IsCompositeByResiduesGpu(PrimeOrderCalculatorAccelerator gpu, ulong p) + { + ModResidueTracker tracker = AcquireResidueTracker(); + // Use ModResidueTracker with a small set of primes to pre-filter composite p. + tracker.BeginMerge(p); + // TODO: Integrate the divisor-cycle cache here so the small-prime sweep reuses precomputed remainders instead of + // running MergeOrAppend for every candidate and missing the cycle-accelerated early exits. + // Use the small prime list from PerfectNumbers.Core to the extent of sqrt(p) + var primes = PrimesGenerator.SmallPrimes; + var primesPow2 = PrimesGenerator.SmallPrimesPow2; + int len = primes.Length; + for (int primeIndex = 0; primeIndex < len; primeIndex++) + { + if (primesPow2[primeIndex] > p) + { + break; + } + + if (tracker.MergeOrAppendGpu(gpu, p, primes[primeIndex], out bool divisible) && divisible) + { + return true; + } + } + + return false; + } + + internal static bool IsCompositeByResiduesHybrid(PrimeOrderCalculatorAccelerator gpu, ulong p) + { + ModResidueTracker tracker = AcquireResidueTracker(); + tracker.BeginMerge(p); + var primes = PrimesGenerator.SmallPrimes; + var primesPow2 = PrimesGenerator.SmallPrimesPow2; + int len = primes.Length; + for (int primeIndex = 0; primeIndex < len; primeIndex++) + { + if (primesPow2[primeIndex] > p) + { + break; + } + + if (tracker.MergeOrAppendHybrid(gpu, p, primes[primeIndex], out bool divisible) && divisible) { return true; } diff --git a/EvenPerfectBitScanner/CliArguments.cs b/EvenPerfectBitScanner/CliArguments.cs index 0fb64ec..426ef70 100644 --- a/EvenPerfectBitScanner/CliArguments.cs +++ b/EvenPerfectBitScanner/CliArguments.cs @@ -27,8 +27,8 @@ internal readonly struct CliArguments internal readonly bool UseGcdFilter; internal readonly bool TestMode; internal readonly bool UseGpuCycles; - internal readonly bool UseMersenneOnGpu; - internal readonly bool UseOrderOnGpu; + internal readonly ComputationDevice MersenneDevice; + internal readonly ComputationDevice OrderDevice; internal readonly int ScanBatchSize; internal readonly int SliceSize; internal readonly ulong ResidueKMax; @@ -72,8 +72,8 @@ internal readonly struct CliArguments bool useGcdFilter, bool testMode, bool useGpuCycles, - bool useMersenneOnGpu, - bool useOrderOnGpu, + ComputationDevice mersenneDevice, + ComputationDevice orderDevice, int scanBatchSize, int sliceSize, ulong residueKMax, @@ -116,8 +116,8 @@ internal readonly struct CliArguments UseGcdFilter = useGcdFilter; TestMode = testMode; UseGpuCycles = useGpuCycles; - UseMersenneOnGpu = useMersenneOnGpu; - UseOrderOnGpu = useOrderOnGpu; + MersenneDevice = mersenneDevice; + OrderDevice = orderDevice; ScanBatchSize = scanBatchSize; SliceSize = sliceSize; ResidueKMax = residueKMax; @@ -165,8 +165,8 @@ internal readonly struct CliArguments bool useGcdFilter = false; bool testMode = false; bool useGpuCycles = true; - bool useMersenneOnGpu = true; - bool useOrderOnGpu = true; + ComputationDevice mersenneDevice = ComputationDevice.Gpu; + ComputationDevice orderDevice = ComputationDevice.Gpu; int scanBatchSize = 2_097_152; int sliceSize = 32; ulong residueKMax = 5_000_000UL; @@ -319,7 +319,8 @@ internal readonly struct CliArguments if (argument.StartsWith("--mersenne-device=", StringComparison.OrdinalIgnoreCase)) { - useMersenneOnGpu = !argument.AsSpan("--mersenne-device=".Length).Equals("cpu", StringComparison.OrdinalIgnoreCase); + ReadOnlySpan value = argument.AsSpan("--mersenne-device=".Length); + mersenneDevice = ParseDevice(value); continue; } @@ -389,7 +390,8 @@ internal readonly struct CliArguments if (argument.StartsWith("--order-device=", StringComparison.OrdinalIgnoreCase)) { - useOrderOnGpu = !argument.AsSpan("--order-device=".Length).Equals("cpu", StringComparison.OrdinalIgnoreCase); + ReadOnlySpan value = argument.AsSpan("--order-device=".Length); + orderDevice = ParseDevice(value); continue; } @@ -445,7 +447,7 @@ internal readonly struct CliArguments if (argument.StartsWith("--lucas=", StringComparison.OrdinalIgnoreCase)) { - useMersenneOnGpu = !argument.AsSpan("--lucas=".Length).Equals("cpu", StringComparison.OrdinalIgnoreCase); + mersenneDevice = ParseDevice(argument.AsSpan("--lucas=".Length)); continue; } @@ -565,8 +567,8 @@ internal readonly struct CliArguments useGcdFilter, testMode, useGpuCycles, - useMersenneOnGpu, - useOrderOnGpu, + mersenneDevice, + orderDevice, scanBatchSize, sliceSize, residueKMax, @@ -603,10 +605,10 @@ internal readonly struct CliArguments Console.WriteLine(" --block-size= values processed per thread batch"); Console.WriteLine(" --mersenne=pow2mod|incremental|lucas|residue|divisor|bydivisor Mersenne test method"); Console.WriteLine(" --residue-max-k= max k for residue Mersenne test (q = 2*p*k + 1)"); - Console.WriteLine(" --mersenne-device=cpu|gpu Device for Mersenne method (default gpu)"); + Console.WriteLine(" --mersenne-device=cpu|gpu|hybrid Device for Mersenne method (default gpu)"); Console.WriteLine(" --primes-device=cpu|gpu Device for prime-scan kernels (default gpu)"); Console.WriteLine(" --gpu-prime-batch= Batch size for GPU primality sieve (default 262144)"); - Console.WriteLine(" --order-device=cpu|gpu Device for order computations (default gpu)"); + Console.WriteLine(" --order-device=cpu|gpu|hybrid Device for order computations (default gpu)"); Console.WriteLine(" --ntt=reference|staged GPU NTT backend (default staged)"); Console.WriteLine(" --mod-reduction=auto|uint128|mont64|barrett128 staged NTT reduction (default auto)"); Console.WriteLine(" --gpu-prime-threads= max concurrent GPU prime checks (default 1)"); @@ -632,4 +634,19 @@ internal readonly struct CliArguments Console.WriteLine(" --min-k= starting k for --mersenne=bydivisor divisor scan (default 1)"); Console.WriteLine(" --help, -help, --?, -?, /? show this help message"); } + + private static ComputationDevice ParseDevice(ReadOnlySpan value) + { + if (value.Equals("cpu", StringComparison.OrdinalIgnoreCase)) + { + return ComputationDevice.Cpu; + } + + if (value.Equals("hybrid", StringComparison.OrdinalIgnoreCase)) + { + return ComputationDevice.Hybrid; + } + + return ComputationDevice.Gpu; + } } diff --git a/EvenPerfectBitScanner/Program.cs b/EvenPerfectBitScanner/Program.cs index 3c5f0fc..06c85c1 100644 --- a/EvenPerfectBitScanner/Program.cs +++ b/EvenPerfectBitScanner/Program.cs @@ -83,8 +83,11 @@ internal static class Program ? PrimeTransformMode.Bit : (useResidue ? PrimeTransformMode.Add : PrimeTransformMode.AddPrimes); CandidatesCalculator.Configure(transformMode); - bool mersenneOnGpu = _cliArguments.UseMersenneOnGpu; - bool orderOnGpu = _cliArguments.UseOrderOnGpu; + ComputationDevice mersenneDevice = _cliArguments.MersenneDevice; + ComputationDevice orderDevice = _cliArguments.OrderDevice; + bool mersenneOnGpu = mersenneDevice == ComputationDevice.Gpu; + bool orderOnGpu = orderDevice == ComputationDevice.Gpu; + bool orderOnHybrid = orderDevice == ComputationDevice.Hybrid; int scanBatchSize = Math.Max(1, _cliArguments.ScanBatchSize); int sliceSize = Math.Max(1, _cliArguments.SliceSize); ulong residueKMax = _cliArguments.ResidueKMax; @@ -165,7 +168,6 @@ internal static class Program } DivisorCycleCache.SetDivisorCyclesBatchSize(cyclesBatchSize); - DivisorCycleCache.Shared.ConfigureGeneratorDevice(useGpuCycles); // TODO: Keep a single cached block loaded from disk and honor the configured device when // computing ad-hoc cycles for divisors that fall outside that snapshot instead of queuing // generation of additional blocks. @@ -194,13 +196,25 @@ internal static class Program useOrder: useOrder, useGpuLucas: mersenneOnGpu, useGpuScan: mersenneOnGpu, - useGpuOrder: orderOnGpu, + useGpuOrder: orderOnGpu || orderOnHybrid, useResidue: useResidue, - maxK: residueKMax); + maxK: residueKMax, + orderDevice: orderDevice); if (!useLucas) { Console.WriteLine("Warming up orders"); - tester.WarmUpOrders(currentP, orderWarmupLimitOverride ?? 5_000_000UL); + if (orderDevice == ComputationDevice.Hybrid) + { + tester.WarmUpOrdersHybrid(currentP, orderWarmupLimitOverride ?? 5_000_000UL); + } + else if (orderDevice == ComputationDevice.Gpu) + { + tester.WarmUpOrdersGpu(currentP, orderWarmupLimitOverride ?? 5_000_000UL); + } + else + { + tester.WarmUpOrdersCpu(currentP, orderWarmupLimitOverride ?? 5_000_000UL); + } } return tester; @@ -216,7 +230,7 @@ internal static class Program { _byDivisorTester = mersenneOnGpu ? new MersenneNumberDivisorByDivisorGpuTester() - : new MersenneNumberDivisorByDivisorCpuTester(); + : new MersenneNumberDivisorByDivisorCpuTester(orderDevice); _byDivisorTester.MinK = _cliArguments.MinK; _byDivisorTester.BatchSize = scanBatchSize; } @@ -245,6 +259,20 @@ internal static class Program Console.WriteLine("Initialization..."); // Compose a results file name that encodes configuration (before opening file) + string mersenneDeviceLabel = mersenneDevice switch + { + ComputationDevice.Hybrid => "hybrid", + ComputationDevice.Gpu => "gpu", + _ => "cpu", + }; + + string orderDeviceLabel = orderDevice switch + { + ComputationDevice.Hybrid => "hybrid", + ComputationDevice.Gpu => "gpu", + _ => "cpu", + }; + var builtName = CalculationResultsFile.BuildFileName( useBitTransform, threadCount, @@ -257,14 +285,14 @@ internal static class Program useOrder, _cliArguments.UseGcdFilter, NttGpuMath.GpuTransformBackend, - gpuPrimeThreads, - sliceSize, - scanBatchSize, - orderWarmupLimitOverride ?? 5_000_000UL, - NttGpuMath.ReductionMode, - mersenneOnGpu ? "gpu" : "cpu", - _runPrimesOnCpu ? "cpu" : "gpu", - orderOnGpu ? "gpu" : "cpu"); + gpuPrimeThreads, + sliceSize, + scanBatchSize, + orderWarmupLimitOverride ?? 5_000_000UL, + NttGpuMath.ReductionMode, + mersenneDeviceLabel, + _runPrimesOnCpu ? "cpu" : "gpu", + orderDeviceLabel); if (!string.IsNullOrEmpty(_cliArguments.ResultsPrefix)) { @@ -447,7 +475,18 @@ internal static class Program if (!useLucas) { Console.WriteLine("Warming up orders..."); - MersenneTesters.Value!.WarmUpOrders(currentP, orderWarmupLimitOverride ?? 5_000_000UL); + if (orderDevice == ComputationDevice.Hybrid) + { + MersenneTesters.Value!.WarmUpOrdersHybrid(currentP, orderWarmupLimitOverride ?? 5_000_000UL); + } + else if (orderDevice == ComputationDevice.Gpu) + { + MersenneTesters.Value!.WarmUpOrdersGpu(currentP, orderWarmupLimitOverride ?? 5_000_000UL); + } + else + { + MersenneTesters.Value!.WarmUpOrdersCpu(currentP, orderWarmupLimitOverride ?? 5_000_000UL); + } } } @@ -619,7 +658,12 @@ internal static class Program } // Fast residue-based composite check for p using small primes - if (!_cliArguments.UseDivisor && !_cliArguments.UseByDivisor && CandidatesCalculator.IsCompositeByResidues(gpu, p)) + if (!_cliArguments.UseDivisor && !_cliArguments.UseByDivisor && + ( + (_cliArguments.OrderDevice == ComputationDevice.Gpu && CandidatesCalculator.IsCompositeByResiduesGpu(gpu, p)) || + (_cliArguments.OrderDevice == ComputationDevice.Hybrid && CandidatesCalculator.IsCompositeByResiduesHybrid(gpu, p)) || + (_cliArguments.OrderDevice == ComputationDevice.Cpu && CandidatesCalculator.IsCompositeByResiduesCpu(p)) + )) { searchedMersenne = false; detailedCheck = false; diff --git a/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs b/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs index 5a4cc1a..f4ebe65 100644 --- a/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs +++ b/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs @@ -398,7 +398,7 @@ public class PrimeOrderGpuHeuristicsTests PrimeOrderGpuHeuristics.OverflowRegistry[prime] = 0; MontgomeryDivisorData divisorData = MontgomeryDivisorDataPool.Shared.FromModulus(prime); - ulong result = PrimeOrderCalculator.Calculate(gpu, prime, null, divisorData, PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault, PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu); + ulong result = PrimeOrderCalculator.CalculateGpu(gpu, prime, null, divisorData, PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); result.Should().Be(3UL); } diff --git a/PerfectNumbers.Core.Tests/Gpu/ULongExtensionsGpuTests.cs b/PerfectNumbers.Core.Tests/Gpu/ULongExtensionsGpuTests.cs index f3ce19f..a52d4cb 100644 --- a/PerfectNumbers.Core.Tests/Gpu/ULongExtensionsGpuTests.cs +++ b/PerfectNumbers.Core.Tests/Gpu/ULongExtensionsGpuTests.cs @@ -19,7 +19,7 @@ public class ULongExtensionsGpuTests public void Pow2ModWindowedGpu_matches_montgomery_cpu_for_known_values(ulong exponent, ulong modulus) { MontgomeryDivisorData divisor = MontgomeryDivisorDataPool.Shared.FromModulus(modulus); - ulong expected = exponent.Pow2MontgomeryModWindowedCpu(divisor, keepMontgomery: false); + ulong expected = exponent.Pow2MontgomeryModWindowedConvertToStandardCpu(divisor); exponent.Pow2ModWindowedGpu(modulus).Should().Be(expected); } @@ -55,7 +55,7 @@ public class ULongExtensionsGpuTests MontgomeryDivisorData divisor = MontgomeryDivisorDataPool.Shared.FromModulus(modulus); foreach (ulong exponent in exponents) { - ulong expected = exponent.Pow2MontgomeryModWindowedCpu(divisor, keepMontgomery: false); + ulong expected = exponent.Pow2MontgomeryModWindowedConvertToStandardCpu(divisor); exponent.Pow2ModWindowedGpu(modulus).Should().Be(expected); } } diff --git a/PerfectNumbers.Core.Tests/MersenneDivisorCyclesGenerateGpuTests.cs b/PerfectNumbers.Core.Tests/MersenneDivisorCyclesGenerateGpuTests.cs index ff73525..125a4b1 100644 --- a/PerfectNumbers.Core.Tests/MersenneDivisorCyclesGenerateGpuTests.cs +++ b/PerfectNumbers.Core.Tests/MersenneDivisorCyclesGenerateGpuTests.cs @@ -41,7 +41,7 @@ public class MersenneDivisorCyclesGenerateGpuTests { expected.Should().Contain(pair.divisor); MontgomeryDivisorData divisorData = MontgomeryDivisorDataPool.Shared.FromModulus(pair.divisor); - pair.cycle.Should().Be(MersenneDivisorCycles.CalculateCycleLength(gpu, pair.divisor, divisorData)); + pair.cycle.Should().Be(MersenneDivisorCycles.CalculateCycleLengthCpu(pair.divisor, divisorData)); } } finally @@ -59,7 +59,6 @@ public class MersenneDivisorCyclesGenerateGpuTests public void Generate_produces_unique_cycles_for_expected_divisors() { string path = Path.GetTempFileName(); - var gpu = PrimeOrderCalculatorAccelerator.Rent(1); try { MersenneDivisorCycles.Generate(path, maxDivisor: 50UL, threads: 4); @@ -92,7 +91,7 @@ public class MersenneDivisorCyclesGenerateGpuTests { uniqueDivisors.Add(pair.divisor).Should().BeTrue(); MontgomeryDivisorData divisorData = MontgomeryDivisorDataPool.Shared.FromModulus(pair.divisor); - pair.cycle.Should().Be(MersenneDivisorCycles.CalculateCycleLength(gpu, pair.divisor, divisorData)); + pair.cycle.Should().Be(MersenneDivisorCycles.CalculateCycleLengthCpu(pair.divisor, divisorData)); } uniqueDivisors.Should().BeEquivalentTo(expected); @@ -102,7 +101,6 @@ public class MersenneDivisorCyclesGenerateGpuTests } finally { - PrimeOrderCalculatorAccelerator.Return(gpu); if (File.Exists(path)) { File.Delete(path); diff --git a/PerfectNumbers.Core.Tests/MersenneDivisorCyclesTests.cs b/PerfectNumbers.Core.Tests/MersenneDivisorCyclesTests.cs index 29e5964..d2e2d23 100644 --- a/PerfectNumbers.Core.Tests/MersenneDivisorCyclesTests.cs +++ b/PerfectNumbers.Core.Tests/MersenneDivisorCyclesTests.cs @@ -23,7 +23,7 @@ public class MersenneDivisorCyclesTests try { MontgomeryDivisorData divisorData = MontgomeryDivisorDataPool.Shared.FromModulus(divisor); - MersenneDivisorCycles.CalculateCycleLength(gpu, divisor, divisorData).Should().Be(expected); + MersenneDivisorCycles.CalculateCycleLengthCpu(divisor, divisorData).Should().Be(expected); MersenneDivisorCycles.CalculateCycleLengthGpu(divisor).Should().Be(expected); } finally diff --git a/PerfectNumbers.Core.Tests/MersenneNumberDivisorCpuTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberDivisorCpuTesterTests.cs index 0006bc6..c5a127a 100644 --- a/PerfectNumbers.Core.Tests/MersenneNumberDivisorCpuTesterTests.cs +++ b/PerfectNumbers.Core.Tests/MersenneNumberDivisorCpuTesterTests.cs @@ -46,17 +46,17 @@ public class MersenneNumberDivisorCpuTesterTests var session = tester.CreateDivisorSession(gpu); try { - ulong[] primes = { 5UL, 7UL, 11UL, 13UL }; + ulong[] primes = [5UL, 7UL, 11UL, 13UL]; byte[] hits = new byte[primes.Length]; - ulong cycle23 = MersenneDivisorCycles.CalculateCycleLength(gpu, 23UL, MontgomeryDivisorDataPool.Shared.FromModulus(23UL)); + ulong cycle23 = MersenneDivisorCycles.CalculateCycleLengthCpu(23UL, MontgomeryDivisorDataPool.Shared.FromModulus(23UL)); session.CheckDivisor(23UL, MontgomeryDivisorDataPool.Shared.FromModulus(23UL), cycle23, primes, hits); - hits.Should().ContainInOrder(new byte[] { 0, 0, 1, 0 }); + hits.Should().ContainInOrder([0, 0, 1, 0]); Array.Fill(hits, (byte)0); - ulong cycle31 = MersenneDivisorCycles.CalculateCycleLength(gpu, 31UL, MontgomeryDivisorDataPool.Shared.FromModulus(31UL)); + ulong cycle31 = MersenneDivisorCycles.CalculateCycleLengthCpu(31UL, MontgomeryDivisorDataPool.Shared.FromModulus(31UL)); session.CheckDivisor(31UL, MontgomeryDivisorDataPool.Shared.FromModulus(31UL), cycle31, primes, hits); - hits.Should().ContainInOrder(new byte[] { 1, 0, 0, 0 }); + hits.Should().ContainInOrder([1, 0, 0, 0]); } finally { @@ -76,14 +76,14 @@ public class MersenneNumberDivisorCpuTesterTests var session = tester.CreateDivisorSession(gpu); try { - ulong[] exponents = { 6UL, 7UL, 9UL, 10UL, 12UL }; + ulong[] exponents = [6UL, 7UL, 9UL, 10UL, 12UL]; byte[] hits = new byte[exponents.Length]; MontgomeryDivisorData divisorData = MontgomeryDivisorDataPool.Shared.FromModulus(7UL); - ulong cycle = MersenneDivisorCycles.CalculateCycleLength(gpu, 7UL, divisorData); + ulong cycle = MersenneDivisorCycles.CalculateCycleLengthCpu(7UL, divisorData); session.CheckDivisor(7UL, divisorData, cycle, exponents, hits); - hits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 }); + hits.Should().Equal([1, 0, 1, 0, 1]); } finally { @@ -103,14 +103,14 @@ public class MersenneNumberDivisorCpuTesterTests var session = tester.CreateDivisorSession(gpu); try { - ulong[] exponents = { 10UL, 11UL, 20UL, 21UL, 30UL }; + ulong[] exponents = [10UL, 11UL, 20UL, 21UL, 30UL]; byte[] hits = new byte[exponents.Length]; MontgomeryDivisorData divisorData = MontgomeryDivisorDataPool.Shared.FromModulus(11UL); - ulong cycle = MersenneDivisorCycles.CalculateCycleLength(gpu, 11UL, divisorData); + ulong cycle = MersenneDivisorCycles.CalculateCycleLengthCpu(11UL, divisorData); session.CheckDivisor(11UL, divisorData, cycle, exponents, hits); - hits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 }); + hits.Should().Equal([1, 0, 1, 0, 1]); } finally { diff --git a/PerfectNumbers.Core.Tests/MersenneNumberDivisorGpuTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberDivisorGpuTesterTests.cs index da7ef8a..b8f1bd4 100644 --- a/PerfectNumbers.Core.Tests/MersenneNumberDivisorGpuTesterTests.cs +++ b/PerfectNumbers.Core.Tests/MersenneNumberDivisorGpuTesterTests.cs @@ -128,7 +128,7 @@ public class MersenneNumberDivisorGpuTesterTests tester.DivisorLimit.Should().Be(0UL); - ulong[] primes = { 31UL, 37UL }; + ulong[] primes = [31UL, 37UL]; ulong[] allowed = new ulong[primes.Length]; tester.PrepareCandidates(primes, allowed); @@ -228,12 +228,12 @@ public class MersenneNumberDivisorGpuTesterTests var session = tester.CreateDivisorSession(gpu); try { - ulong[] primes = { 31UL, 37UL, 41UL }; + ulong[] primes = [31UL, 37UL, 41UL]; byte[] hits = new byte[primes.Length]; session.CheckDivisor(223UL, MontgomeryDivisorDataPool.Shared.FromModulus(223UL), 0UL, primes, hits); - hits.Should().ContainInOrder(new byte[] { 0, 1, 0 }); + hits.Should().ContainInOrder([0, 1, 0]); } finally { @@ -253,17 +253,17 @@ public class MersenneNumberDivisorGpuTesterTests var session = tester.CreateDivisorSession(gpu); try { - ulong[] primes = { 31UL, 37UL, 41UL, 43UL }; + ulong[] primes = [31UL, 37UL, 41UL, 43UL]; byte[] hits = new byte[primes.Length]; - ulong cycle223 = MersenneDivisorCycles.CalculateCycleLength(gpu, 223UL, MontgomeryDivisorDataPool.Shared.FromModulus(223UL)); + ulong cycle223 = MersenneDivisorCycles.CalculateCycleLengthGpu(gpu, 223UL, MontgomeryDivisorDataPool.Shared.FromModulus(223UL)); session.CheckDivisor(223UL, MontgomeryDivisorDataPool.Shared.FromModulus(223UL), cycle223, primes, hits); - hits.Should().ContainInOrder(new byte[] { 0, 1, 0, 0 }); + hits.Should().ContainInOrder([0, 1, 0, 0]); Array.Fill(hits, (byte)0); - ulong cycle13367 = MersenneDivisorCycles.CalculateCycleLength(gpu, 13367UL, MontgomeryDivisorDataPool.Shared.FromModulus(13367UL)); + ulong cycle13367 = MersenneDivisorCycles.CalculateCycleLengthGpu(gpu, 13367UL, MontgomeryDivisorDataPool.Shared.FromModulus(13367UL)); session.CheckDivisor(13367UL, MontgomeryDivisorDataPool.Shared.FromModulus(13367UL), cycle13367, primes, hits); - hits.Should().ContainInOrder(new byte[] { 0, 0, 1, 0 }); + hits.Should().ContainInOrder([0, 0, 1, 0]); } finally { @@ -287,13 +287,13 @@ public class MersenneNumberDivisorGpuTesterTests var session = tester.CreateDivisorSession(gpu); try { - ulong[] primes = { 31UL, 37UL, 41UL, 43UL, 47UL }; + ulong[] primes = [31UL, 37UL, 41UL, 43UL, 47UL]; byte[] hits = new byte[primes.Length]; - ulong cycle223 = MersenneDivisorCycles.CalculateCycleLength(gpu, 223UL, MontgomeryDivisorDataPool.Shared.FromModulus(223UL)); + ulong cycle223 = MersenneDivisorCycles.CalculateCycleLengthGpu(gpu, 223UL, MontgomeryDivisorDataPool.Shared.FromModulus(223UL)); session.CheckDivisor(223UL, MontgomeryDivisorDataPool.Shared.FromModulus(223UL), cycle223, primes, hits); - hits.Should().ContainInOrder(new byte[] { 0, 1, 0, 0, 0 }); + hits.Should().ContainInOrder([0, 1, 0, 0, 0]); } finally { @@ -316,15 +316,15 @@ public class MersenneNumberDivisorGpuTesterTests var session = tester.CreateDivisorSession(gpu); try { - ulong[] exponents = { 6UL, 7UL, 9UL, 10UL, 12UL }; + ulong[] exponents = [6UL, 7UL, 9UL, 10UL, 12UL]; byte[] hits = new byte[exponents.Length]; MontgomeryDivisorData divisorData = MontgomeryDivisorDataPool.Shared.FromModulus(7UL); - ulong cycle = MersenneDivisorCycles.CalculateCycleLength(gpu, 7UL, divisorData); + ulong cycle = MersenneDivisorCycles.CalculateCycleLengthGpu(gpu, 7UL, divisorData); session.CheckDivisor(7UL, divisorData, cycle, exponents, hits); - hits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 }); + hits.Should().Equal([1, 0, 1, 0, 1]); } finally { @@ -347,15 +347,15 @@ public class MersenneNumberDivisorGpuTesterTests var session = tester.CreateDivisorSession(gpu); try { - ulong[] exponents = { 10UL, 11UL, 20UL, 21UL, 30UL }; + ulong[] exponents = [10UL, 11UL, 20UL, 21UL, 30UL]; byte[] hits = new byte[exponents.Length]; MontgomeryDivisorData divisorData = MontgomeryDivisorDataPool.Shared.FromModulus(11UL); - ulong cycle = MersenneDivisorCycles.CalculateCycleLength(gpu, 11UL, divisorData); + ulong cycle = MersenneDivisorCycles.CalculateCycleLengthGpu(gpu, 11UL, divisorData); session.CheckDivisor(11UL, divisorData, cycle, exponents, hits); - hits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 }); + hits.Should().Equal([1, 0, 1, 0, 1]); } finally { @@ -382,7 +382,7 @@ public class MersenneNumberDivisorGpuTesterTests ulong[] patchedSmall = new ulong[PerfectNumberConstants.MaxQForDivisorCycles + 1]; MontgomeryDivisorData divisorData = MontgomeryDivisorDataPool.Shared.FromModulus(191UL); - patchedSmall[191] = MersenneDivisorCycles.CalculateCycleLength(gpu, 191UL, divisorData); + patchedSmall[191] = MersenneDivisorCycles.CalculateCycleLengthGpu(gpu, 191UL, divisorData); tableField.SetValue(cycles, patchedTable); smallCyclesField.SetValue(cycles, patchedSmall); @@ -423,13 +423,13 @@ public class MersenneNumberDivisorGpuTesterTests var session = tester.CreateDivisorSession(gpu); try { - ulong[] primes = { 31UL, 37UL, 41UL, 43UL, 47UL }; + ulong[] primes = [31UL, 37UL, 41UL, 43UL, 47UL]; byte[] hits = new byte[primes.Length]; - ulong cycle223 = MersenneDivisorCycles.CalculateCycleLength(gpu, 223UL, MontgomeryDivisorDataPool.Shared.FromModulus(223UL)); + ulong cycle223 = MersenneDivisorCycles.CalculateCycleLengthGpu(gpu, 223UL, MontgomeryDivisorDataPool.Shared.FromModulus(223UL)); session.CheckDivisor(223UL, MontgomeryDivisorDataPool.Shared.FromModulus(223UL), cycle223, primes, hits); - hits.Should().ContainInOrder(new byte[] { 0, 1, 0, 0, 0 }); + hits.Should().ContainInOrder([0, 1, 0, 0, 0]); } finally { diff --git a/PerfectNumbers.Core.Tests/MersenneNumberResidueCpuTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberResidueCpuTesterTests.cs index a51b054..9e02706 100644 --- a/PerfectNumbers.Core.Tests/MersenneNumberResidueCpuTesterTests.cs +++ b/PerfectNumbers.Core.Tests/MersenneNumberResidueCpuTesterTests.cs @@ -25,15 +25,8 @@ public class MersenneNumberResidueCpuTesterTests private static void RunCase(MersenneNumberResidueCpuTester tester, ulong exponent, ulong maxK, bool expectedPrime) { bool isPrime = true; - var gpu = PrimeOrderCalculatorAccelerator.Rent(1); - try - { - tester.Scan(gpu, exponent, (UInt128)exponent << 1, GetLastDigit(exponent), (UInt128)maxK, ref isPrime); - isPrime.Should().Be(expectedPrime); - } - finally - { - PrimeOrderCalculatorAccelerator.Return(gpu); - } + tester.Scan(exponent, (UInt128)exponent << 1, GetLastDigit(exponent), (UInt128)maxK, ref isPrime); + + isPrime.Should().Be(expectedPrime); } } diff --git a/PerfectNumbers.Core.Tests/MersenneNumberResidueGpuTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberResidueGpuTesterTests.cs index a550584..0fcb8d2 100644 --- a/PerfectNumbers.Core.Tests/MersenneNumberResidueGpuTesterTests.cs +++ b/PerfectNumbers.Core.Tests/MersenneNumberResidueGpuTesterTests.cs @@ -1,5 +1,6 @@ using FluentAssertions; using PerfectNumbers.Core.Gpu; +using PerfectNumbers.Core.Gpu.Accelerators; using Xunit; using UInt128 = System.UInt128; @@ -7,27 +8,36 @@ namespace PerfectNumbers.Core.Tests; public class MersenneNumberResidueGpuTesterTests { - private static LastDigit GetLastDigit(ulong exponent) => (exponent & 3UL) == 3UL ? LastDigit.Seven : LastDigit.One; + private static LastDigit GetLastDigit(ulong exponent) => (exponent & 3UL) == 3UL ? LastDigit.Seven : LastDigit.One; - [Theory] - [InlineData(false)] - [InlineData(true)] - [Trait("Category", "Fast")] - public void Scan_handles_various_prime_exponents(bool useGpuOrder) - { - var tester = new MersenneNumberResidueGpuTester(useGpuOrder); + [Theory] + [InlineData(false)] + [InlineData(true)] + [Trait("Category", "Fast")] + public void Scan_handles_various_prime_exponents(bool useGpuOrder) + { + var tester = new MersenneNumberResidueGpuTester(useGpuOrder); - RunCase(tester, 23UL, 2UL, expectedPrime: false); - RunCase(tester, 29UL, 37UL, expectedPrime: false); - RunCase(tester, 31UL, 1_000UL, expectedPrime: true); - RunCase(tester, 89UL, 1_001UL, expectedPrime: true); - RunCase(tester, 127UL, 1_001UL, expectedPrime: true); - } + RunCase(tester, 23UL, 2UL, expectedPrime: false); + RunCase(tester, 29UL, 37UL, expectedPrime: false); + RunCase(tester, 31UL, 1_000UL, expectedPrime: true); + RunCase(tester, 89UL, 1_001UL, expectedPrime: true); + RunCase(tester, 127UL, 1_001UL, expectedPrime: true); + } - private static void RunCase(MersenneNumberResidueGpuTester tester, ulong exponent, ulong maxK, bool expectedPrime) - { - bool isPrime = true; - tester.Scan(exponent, (UInt128)exponent << 1, GetLastDigit(exponent), (UInt128)maxK, ref isPrime); - isPrime.Should().Be(expectedPrime); - } + private static void RunCase(MersenneNumberResidueGpuTester tester, ulong exponent, ulong maxK, bool expectedPrime) + { + bool isPrime = true; + var gpu = PrimeOrderCalculatorAccelerator.Rent(1); + try + { + tester.Scan(gpu, exponent, (UInt128)exponent << 1, GetLastDigit(exponent), (UInt128)maxK, ref isPrime); + + isPrime.Should().Be(expectedPrime); + } + finally + { + PrimeOrderCalculatorAccelerator.Return(gpu); + } + } } diff --git a/PerfectNumbers.Core.Tests/MersenneNumberTester/MersenneNumberTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberTester/MersenneNumberTesterTests.cs index 9619f78..c81993b 100644 --- a/PerfectNumbers.Core.Tests/MersenneNumberTester/MersenneNumberTesterTests.cs +++ b/PerfectNumbers.Core.Tests/MersenneNumberTester/MersenneNumberTesterTests.cs @@ -27,10 +27,27 @@ public class MersenneNumberTesterTests [Theory] [InlineData(125UL)] - public void WarmUpOrders_populates_cache_without_affecting_results(ulong p) + public void WarmUpOrdersCpu_populates_cache_without_affecting_results(ulong p) { var tester = new MersenneNumberTester(useOrderCache: true, useGpuOrder: true); - tester.WarmUpOrders(p, 1_000UL); + tester.WarmUpOrdersCpu(p, 1_000UL); + var gpu = PrimeOrderCalculatorAccelerator.Rent(1); + try + { + tester.IsMersennePrime(gpu, p).Should().BeFalse(); + } + finally + { + PrimeOrderCalculatorAccelerator.Return(gpu); + } + } + + [Theory] + [InlineData(125UL)] + public void WarmUpOrdersGpu_populates_cache_without_affecting_results(ulong p) + { + var tester = new MersenneNumberTester(useOrderCache: true, useGpuOrder: true); + tester.WarmUpOrdersGpu(p, 1_000UL); var gpu = PrimeOrderCalculatorAccelerator.Rent(1); try { @@ -45,12 +62,20 @@ public class MersenneNumberTesterTests [Theory] [InlineData(125UL, true)] [InlineData(127UL, false)] - public void SharesFactorWithExponentMinusOne_detects_common_factors(ulong p, bool expected) + public void SharesFactorWithExponentMinusOneCpu_detects_common_factors(ulong p, bool expected) + { + p.SharesFactorWithExponentMinusOneCpu().Should().Be(expected); + } + + [Theory] + [InlineData(125UL, true)] + [InlineData(127UL, false)] + public void SharesFactorWithExponentMinusOneGpu_detects_common_factors(ulong p, bool expected) { var gpu = PrimeOrderCalculatorAccelerator.Rent(1); try { - p.SharesFactorWithExponentMinusOne(gpu).Should().Be(expected); + p.SharesFactorWithExponentMinusOneGpu(gpu).Should().Be(expected); } finally { diff --git a/PerfectNumbers.Core.Tests/ModResidueTrackerTests.cs b/PerfectNumbers.Core.Tests/ModResidueTrackerTests.cs index 230fb8e..aa9ccf0 100644 --- a/PerfectNumbers.Core.Tests/ModResidueTrackerTests.cs +++ b/PerfectNumbers.Core.Tests/ModResidueTrackerTests.cs @@ -8,16 +8,26 @@ namespace PerfectNumbers.Core.Tests; public class ModResidueTrackerTests { [Fact] - public void IsDivisible_advances_residue_for_identity() + public void IsDivisibleCpu_advances_residue_for_identity() + { + var tracker = new ModResidueTracker(ResidueModel.Identity); + + tracker.IsDivisibleCpu(5, 2).Should().BeFalse(); + tracker.IsDivisibleCpu(6, 2).Should().BeTrue(); + tracker.IsDivisibleCpu(7, 2).Should().BeFalse(); + } + + [Fact] + public void IsDivisibleGpu_advances_residue_for_identity() { var tracker = new ModResidueTracker(ResidueModel.Identity); var gpu = PrimeOrderCalculatorAccelerator.Rent(1); try { - tracker.IsDivisible(gpu, 5, 2).Should().BeFalse(); - tracker.IsDivisible(gpu, 6, 2).Should().BeTrue(); - tracker.IsDivisible(gpu, 7, 2).Should().BeFalse(); + tracker.IsDivisibleGpu(gpu, 5, 2).Should().BeFalse(); + tracker.IsDivisibleGpu(gpu, 6, 2).Should().BeTrue(); + tracker.IsDivisibleGpu(gpu, 7, 2).Should().BeFalse(); } finally { @@ -31,14 +41,26 @@ public class ModResidueTrackerTests [InlineData(5UL, 7UL, false)] [InlineData(11UL, 23UL, true)] [InlineData(11UL, 31UL, false)] - public void IsDivisible_handles_mersenne_model(ulong p, ulong divisor, bool expected) + public void IsDivisibleCpu_handles_mersenne_model(ulong p, ulong divisor, bool expected) + { + var tracker = new ModResidueTracker(ResidueModel.Mersenne); + tracker.IsDivisibleCpu(p, divisor).Should().Be(expected); + } + + [Theory] + [InlineData(3UL, 7UL, true)] + [InlineData(5UL, 31UL, true)] + [InlineData(5UL, 7UL, false)] + [InlineData(11UL, 23UL, true)] + [InlineData(11UL, 31UL, false)] + public void IsDivisibleGpu_handles_mersenne_model(ulong p, ulong divisor, bool expected) { var tracker = new ModResidueTracker(ResidueModel.Mersenne); var gpu = PrimeOrderCalculatorAccelerator.Rent(1); try { - tracker.IsDivisible(gpu, p, divisor).Should().Be(expected); + tracker.IsDivisibleGpu(gpu, p, divisor).Should().Be(expected); } finally { @@ -47,7 +69,31 @@ public class ModResidueTrackerTests } [Fact] - public void MergeOrAppend_tracks_existing_and_new_divisors() + public void MergeOrAppendCpu_tracks_existing_and_new_divisors() + { + var tracker = new ModResidueTracker(ResidueModel.Identity); + + tracker.BeginMerge(10); + tracker.MergeOrAppendCpu(10, 3, out bool d3).Should().BeTrue(); + d3.Should().BeFalse(); + tracker.MergeOrAppendCpu(10, 5, out bool d5).Should().BeTrue(); + d5.Should().BeTrue(); + + tracker.BeginMerge(10); + tracker.MergeOrAppendCpu(10, 4, out bool d4).Should().BeTrue(); + d4.Should().BeFalse(); + + tracker.BeginMerge(11); + tracker.MergeOrAppendCpu(11, 3, out d3).Should().BeTrue(); + d3.Should().BeFalse(); + tracker.MergeOrAppendCpu(11, 4, out d4).Should().BeTrue(); + d4.Should().BeFalse(); + tracker.MergeOrAppendCpu(11, 5, out d5).Should().BeTrue(); + d5.Should().BeFalse(); + } + + [Fact] + public void MergeOrAppendGpu_tracks_existing_and_new_divisors() { var tracker = new ModResidueTracker(ResidueModel.Identity); var gpu = PrimeOrderCalculatorAccelerator.Rent(1); @@ -55,21 +101,21 @@ public class ModResidueTrackerTests try { tracker.BeginMerge(10); - tracker.MergeOrAppend(gpu, 10, 3, out bool d3).Should().BeTrue(); + tracker.MergeOrAppendGpu(gpu, 10, 3, out bool d3).Should().BeTrue(); d3.Should().BeFalse(); - tracker.MergeOrAppend(gpu, 10, 5, out bool d5).Should().BeTrue(); + tracker.MergeOrAppendGpu(gpu, 10, 5, out bool d5).Should().BeTrue(); d5.Should().BeTrue(); tracker.BeginMerge(10); - tracker.MergeOrAppend(gpu, 10, 4, out bool d4).Should().BeTrue(); + tracker.MergeOrAppendGpu(gpu, 10, 4, out bool d4).Should().BeTrue(); d4.Should().BeFalse(); tracker.BeginMerge(11); - tracker.MergeOrAppend(gpu, 11, 3, out d3).Should().BeTrue(); + tracker.MergeOrAppendGpu(gpu, 11, 3, out d3).Should().BeTrue(); d3.Should().BeFalse(); - tracker.MergeOrAppend(gpu, 11, 4, out d4).Should().BeTrue(); + tracker.MergeOrAppendGpu(gpu, 11, 4, out d4).Should().BeTrue(); d4.Should().BeFalse(); - tracker.MergeOrAppend(gpu, 11, 5, out d5).Should().BeTrue(); + tracker.MergeOrAppendGpu(gpu, 11, 5, out d5).Should().BeTrue(); d5.Should().BeFalse(); } finally @@ -79,15 +125,26 @@ public class ModResidueTrackerTests } [Fact] - public void IsDivisible_requires_non_decreasing_numbers() + public void IsDivisibleCpu_requires_non_decreasing_numbers() + { + var tracker = new ModResidueTracker(ResidueModel.Identity); + + tracker.IsDivisibleCpu(5, 2); + + Action act = () => tracker.IsDivisibleCpu(4, 2); + act.Should().Throw(); + } + + [Fact] + public void IsDivisibleGpu_requires_non_decreasing_numbers() { var tracker = new ModResidueTracker(ResidueModel.Identity); var gpu = PrimeOrderCalculatorAccelerator.Rent(1); try { - tracker.IsDivisible(gpu, 5, 2); + tracker.IsDivisibleGpu(gpu, 5, 2); - Action act = () => tracker.IsDivisible(gpu, 4, 2); + Action act = () => tracker.IsDivisibleGpu(gpu, 4, 2); act.Should().Throw(); } finally diff --git a/PerfectNumbers.Core.Tests/PrimeOrderCalculatorTests.cs b/PerfectNumbers.Core.Tests/PrimeOrderCalculatorTests.cs index e5e1d0a..0633928 100644 --- a/PerfectNumbers.Core.Tests/PrimeOrderCalculatorTests.cs +++ b/PerfectNumbers.Core.Tests/PrimeOrderCalculatorTests.cs @@ -45,13 +45,12 @@ public class PrimeOrderCalculatorTests ulong expected = ComputeOrderByDoubling(prime); var gpu = PrimeOrderCalculatorAccelerator.Rent(1); - ulong result = PrimeOrderCalculator.Calculate( + ulong result = PrimeOrderCalculator.CalculateGpu( gpu, prime, previousOrder: null, MontgomeryDivisorDataPool.Shared.FromModulus(prime), - PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault, - PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu); + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); PrimeOrderCalculatorAccelerator.Return(gpu); @@ -66,21 +65,19 @@ public class PrimeOrderCalculatorTests ulong previousOrder = ComputeOrderByDoubling(previousPrime); var gpu = PrimeOrderCalculatorAccelerator.Rent(1); - ulong heuristic = PrimeOrderCalculator.Calculate( + ulong heuristic = PrimeOrderCalculator.CalculateGpu( gpu, prime, previousOrder, MontgomeryDivisorDataPool.Shared.FromModulus(prime), - PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault, - PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu); + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); - ulong strict = PrimeOrderCalculator.Calculate( + ulong strict = PrimeOrderCalculator.CalculateGpu( gpu, prime, previousOrder: null, MontgomeryDivisorDataPool.Shared.FromModulus(prime), - PrimeOrderCalculator.PrimeOrderSearchConfig.StrictDefault, - PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu); + PrimeOrderCalculator.PrimeOrderSearchConfig.StrictDefault); PrimeOrderCalculatorAccelerator.Return(gpu); @@ -94,13 +91,12 @@ public class PrimeOrderCalculatorTests public void Calculate_handles_trivial_primes(ulong prime, ulong expectedOrder) { var gpu = PrimeOrderCalculatorAccelerator.Rent(1); - ulong result = PrimeOrderCalculator.Calculate( + ulong result = PrimeOrderCalculator.CalculateGpu( gpu, prime, previousOrder: null, MontgomeryDivisorDataPool.Shared.FromModulus(prime), - PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault, - PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu); + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); PrimeOrderCalculatorAccelerator.Return(gpu); result.Should().Be(expectedOrder); @@ -117,13 +113,12 @@ public class PrimeOrderCalculatorTests mode: PrimeOrderCalculator.PrimeOrderMode.Heuristic); var gpu = PrimeOrderCalculatorAccelerator.Rent(1); - ulong result = PrimeOrderCalculator.Calculate( + ulong result = PrimeOrderCalculator.CalculateGpu( gpu, prime: 13UL, previousOrder: null, MontgomeryDivisorDataPool.Shared.FromModulus(13UL), - config, - PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu); + config); PrimeOrderCalculatorAccelerator.Return(gpu); result.Should().Be(ComputeOrderByDoubling(13UL)); @@ -143,13 +138,12 @@ public class PrimeOrderCalculatorTests try { MontgomeryDivisorData divisorData = MontgomeryDivisorDataPool.Shared.FromModulus(239UL); - ulong heuristic = PrimeOrderCalculator.Calculate( + ulong heuristic = PrimeOrderCalculator.CalculateGpu( gpu, prime: 239UL, previousOrder: null, divisorData, - heuristicConfig, - PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu); + heuristicConfig); heuristic.Should().Be(ComputeOrderByDoubling(239UL)); @@ -159,13 +153,12 @@ public class PrimeOrderCalculatorTests maxPowChecks: 1, mode: PrimeOrderCalculator.PrimeOrderMode.Strict); - ulong strict = PrimeOrderCalculator.Calculate( + ulong strict = PrimeOrderCalculator.CalculateGpu( gpu, prime: 239UL, previousOrder: null, divisorData, - strictConfig, - PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu); + strictConfig); strict.Should().Be(heuristic); } @@ -182,12 +175,11 @@ public class PrimeOrderCalculatorTests UInt128 prime = UInt128.Parse("18446744073709641691"); var gpu = PrimeOrderCalculatorAccelerator.Rent(1); - UInt128 result = PrimeOrderCalculator.Calculate( + UInt128 result = PrimeOrderCalculator.CalculateGpu( gpu, prime, previousOrder: null, - PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault, - PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu); + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); PrimeOrderCalculatorAccelerator.Return(gpu); result.Should().Be((UInt128)1229782938247309446UL); @@ -201,7 +193,7 @@ public class PrimeOrderCalculatorTests var gpu = PrimeOrderCalculatorAccelerator.Rent(1); try { - var mersenneDivisorCycles = MersenneDivisorCycles.GetCycle(gpu, prime).Should().Be((UInt128)1229782938247309446UL); + var mersenneDivisorCycles = MersenneDivisorCycles.GetCycleGpu(gpu, prime).Should().Be((UInt128)1229782938247309446UL); } finally { diff --git a/PerfectNumbers.Core.Tests/UInt128Extensions/UInt128ExtensionsTests.cs b/PerfectNumbers.Core.Tests/UInt128Extensions/UInt128ExtensionsTests.cs index 6194fe9..a6fcfa7 100644 --- a/PerfectNumbers.Core.Tests/UInt128Extensions/UInt128ExtensionsTests.cs +++ b/PerfectNumbers.Core.Tests/UInt128Extensions/UInt128ExtensionsTests.cs @@ -51,14 +51,26 @@ public class UInt128ExtensionsTests [InlineData(3UL)] [InlineData(5UL)] [InlineData(7UL)] - public void CalculateOrder_matches_naive(ulong q) + public void CalculateOrderCpu_matches_naive(ulong q) + { + UInt128 q128 = q; + ulong expected = NaiveOrder(q); + q128.CalculateOrderCpu().Should().Be(expected); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(3UL)] + [InlineData(5UL)] + [InlineData(7UL)] + public void CalculateOrderGpu_matches_naive(ulong q) { UInt128 q128 = q; ulong expected = NaiveOrder(q); var gpu = PrimeOrderCalculatorAccelerator.Rent(1); try { - q128.CalculateOrder(gpu).Should().Be(expected); + q128.CalculateOrderGpu(gpu).Should().Be(expected); } finally { diff --git a/PerfectNumbers.Core.Tests/ULongExtensionsTests.cs b/PerfectNumbers.Core.Tests/ULongExtensionsTests.cs index 84c433c..ff29699 100644 --- a/PerfectNumbers.Core.Tests/ULongExtensionsTests.cs +++ b/PerfectNumbers.Core.Tests/ULongExtensionsTests.cs @@ -68,14 +68,27 @@ public class ULongExtensionsTests [InlineData(5UL)] [InlineData(7UL)] [InlineData(11UL)] - public void CalculateOrder_matches_naive_algorithm(ulong q) + public void CalculateOrderCpu_matches_naive_algorithm(ulong q) + { + ulong expected = NaiveOrder(q); + + q.CalculateOrderCpu().Should().Be(expected); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(3UL)] + [InlineData(5UL)] + [InlineData(7UL)] + [InlineData(11UL)] + public void CalculateOrderGpu_matches_naive_algorithm(ulong q) { ulong expected = NaiveOrder(q); var gpu = PrimeOrderCalculatorAccelerator.Rent(1); try { - q.CalculateOrder(gpu).Should().Be(expected); + q.CalculateOrderGpu(gpu).Should().Be(expected); } finally { @@ -119,12 +132,22 @@ public class ULongExtensionsTests [InlineData(15UL, true)] [InlineData(7UL, false)] [InlineData(9UL, false)] - public void SharesFactorWithExponentMinusOne_detects_relationship(ulong exponent, bool expected) + public void SharesFactorWithExponentMinusOneCpu_detects_relationship(ulong exponent, bool expected) + { + exponent.SharesFactorWithExponentMinusOneCpu().Should().Be(expected); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(15UL, true)] + [InlineData(7UL, false)] + [InlineData(9UL, false)] + public void SharesFactorWithExponentMinusOneGpu_detects_relationship(ulong exponent, bool expected) { var gpu = PrimeOrderCalculatorAccelerator.Rent(1); try { - exponent.SharesFactorWithExponentMinusOne(gpu).Should().Be(expected); + exponent.SharesFactorWithExponentMinusOneGpu(gpu).Should().Be(expected); } finally { @@ -213,15 +236,24 @@ public class ULongExtensionsTests [Theory] [Trait("Category", "Fast")] [InlineData(11UL, 13UL)] - public void PowModCrt_matches_BigInteger(ulong exponent, ulong modulus) + public void PowModCrtCpu_matches_BigInteger(ulong exponent, ulong modulus) + { + UInt128 expected = (UInt128)BigInteger.ModPow(2, exponent, modulus); + + exponent.PowModCrtCpu((UInt128)modulus).Should().Be(expected); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(11UL, 13UL)] + public void PowModCrtGpu_matches_BigInteger(ulong exponent, ulong modulus) { - MersenneDivisorCycles cycles = MersenneDivisorCycles.Shared; UInt128 expected = (UInt128)BigInteger.ModPow(2, exponent, modulus); var gpu = PrimeOrderCalculatorAccelerator.Rent(1); try { - exponent.PowModCrt(gpu, (UInt128)modulus, cycles).Should().Be(expected); + exponent.PowModCrtGpu(gpu, (UInt128)modulus).Should().Be(expected); } finally { diff --git a/PerfectNumbers.Core/ComputationDevice.cs b/PerfectNumbers.Core/ComputationDevice.cs new file mode 100644 index 0000000..3ba4120 --- /dev/null +++ b/PerfectNumbers.Core/ComputationDevice.cs @@ -0,0 +1,8 @@ +namespace PerfectNumbers.Core; + +public enum ComputationDevice +{ + Cpu, + Hybrid, + Gpu, +} diff --git a/PerfectNumbers.Core/Cpu/MersenneCpuDivisorScanSession.cs b/PerfectNumbers.Core/Cpu/MersenneCpuDivisorScanSession.cs index 134fa50..eac7187 100644 --- a/PerfectNumbers.Core/Cpu/MersenneCpuDivisorScanSession.cs +++ b/PerfectNumbers.Core/Cpu/MersenneCpuDivisorScanSession.cs @@ -2,11 +2,24 @@ using PerfectNumbers.Core.Gpu.Accelerators; namespace PerfectNumbers.Core.Cpu; -internal sealed class MersenneCpuDivisorScanSession(PrimeOrderCalculatorAccelerator gpu) : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession +internal sealed class MersenneCpuDivisorScanSession(PrimeOrderCalculatorAccelerator gpu, ComputationDevice orderDevice) : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession { - public void Reset() - { - } + private Func _getCycleLength = orderDevice switch + { + ComputationDevice.Gpu => divisor => DivisorCycleCache.Shared.GetCycleLengthGpu(gpu, divisor), + ComputationDevice.Hybrid => divisor => DivisorCycleCache.Shared.GetCycleLengthHybrid(gpu, divisor), + _ => static (divisor) => DivisorCycleCache.Shared.GetCycleLengthCpu(divisor), + }; + + public void Configure(PrimeOrderCalculatorAccelerator gpu, ComputationDevice orderDevice) + { + _getCycleLength = orderDevice switch + { + ComputationDevice.Gpu => divisor => DivisorCycleCache.Shared.GetCycleLengthGpu(gpu, divisor), + ComputationDevice.Hybrid => divisor => DivisorCycleCache.Shared.GetCycleLengthHybrid(gpu, divisor), + _ => static (divisor) => DivisorCycleCache.Shared.GetCycleLengthCpu(divisor), + }; + } public void CheckDivisor( ulong divisor, @@ -29,15 +42,15 @@ internal sealed class MersenneCpuDivisorScanSession(PrimeOrderCalculatorAccelera // cachedData = MontgomeryDivisorData.FromModulus(divisor); } - if (divisorCycle == 0UL) - { - divisorCycle = DivisorCycleCache.Shared.GetCycleLength(gpu, divisor); - if (divisorCycle == 0UL) - { - // DivisorCycleCache guarantees a positive cycle for divisors greater than one. - throw new InvalidOperationException($"Divisor cycle solver returned zero for divisor {divisor}."); - } - } + if (divisorCycle == 0UL) + { + divisorCycle = _getCycleLength(divisor); + if (divisorCycle == 0UL) + { + // DivisorCycleCache guarantees a positive cycle for divisors greater than one. + throw new InvalidOperationException($"Divisor cycle solver returned zero for divisor {divisor}."); + } + } // Keep these remainder steppers in place so future updates continue reusing the previously computed residues. // They are critical for avoiding repeated full Montgomery exponentiation work when scanning divisors. diff --git a/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs b/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs index 2d0832f..5075e5b 100644 --- a/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs +++ b/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs @@ -4,12 +4,40 @@ using PerfectNumbers.Core.Gpu.Accelerators; namespace PerfectNumbers.Core.Cpu; -public sealed class MersenneNumberDivisorByDivisorCpuTester() : IMersenneNumberDivisorByDivisorTester +public sealed class MersenneNumberDivisorByDivisorCpuTester(ComputationDevice orderDevice = ComputationDevice.Hybrid) : IMersenneNumberDivisorByDivisorTester { private ulong _divisorLimit; private int _batchSize = 1_024; private ulong _minK = 1UL; + private readonly TryCycleLengthDelegate _tryCalculateCycleLengthForExponent = orderDevice switch + { + ComputationDevice.Gpu => TryCalculateCycleLengthForExponentGpu, + ComputationDevice.Hybrid => TryCalculateCycleLengthForExponentHybrid, + _ => TryCalculateCycleLengthForExponentCpu, + }; + + private readonly CalculateCycleLengthDelegate _calculateCycleLength = orderDevice switch + { + ComputationDevice.Gpu => CalculateCycleLengthGpu, + ComputationDevice.Hybrid => CalculateCycleLengthHybrid, + _ => CalculateCycleLengthCpu, + }; + + private delegate bool TryCycleLengthDelegate( + ulong divisor, + PrimeOrderCalculatorAccelerator gpu, + ulong exponent, + in MontgomeryDivisorData divisorData, + out ulong cycleLength, + out bool primeOrderFailed); + + private delegate ulong CalculateCycleLengthDelegate( + ulong divisor, + PrimeOrderCalculatorAccelerator gpu, + in MontgomeryDivisorData divisorData, + bool skipPrimeOrderHeuristic); + [ThreadStatic] private static GpuUInt128WorkSet _divisorScanGpuWorkSet; @@ -96,15 +124,17 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester() : IMersenneNumberD public IMersenneNumberDivisorByDivisorTester.IDivisorScanSession CreateDivisorSession(PrimeOrderCalculatorAccelerator gpu) { MersenneCpuDivisorScanSession? session = ThreadStaticPools.RentMersenneCpuDivisorSession(); + if (session is not null) { + session.Configure(gpu, orderDevice); return session; } - return new MersenneCpuDivisorScanSession(gpu); + return new MersenneCpuDivisorScanSession(gpu, orderDevice); } - private static bool CheckDivisors( + private bool CheckDivisors( PrimeOrderCalculatorAccelerator gpu, ulong prime, ulong allowedMax, @@ -269,9 +299,9 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester() : IMersenneNumberD MontgomeryDivisorData divisorData = divisorPool.FromModulus(candidate); ulong divisorCycle; // Divisors generated from 2 * k * p + 1 exceed the small-cycle snapshot when p >= 138,000,000, so the short path below never runs. - if (!MersenneDivisorCycles.TryCalculateCycleLengthForExponentCpu( - gpu, + if (!_tryCalculateCycleLengthForExponent( candidate, + gpu, prime, divisorData, out ulong computedCycle, @@ -279,9 +309,9 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester() : IMersenneNumberD { // Divisors produced by 2 * k * p + 1 always exceed PerfectNumberConstants.MaxQForDivisorCycles // for the exponents scanned here, so skip the unused cache fallback and compute directly. - divisorCycle = MersenneDivisorCycles.CalculateCycleLength( - gpu, + divisorCycle = _calculateCycleLength( candidate, + gpu, divisorData, skipPrimeOrderHeuristic: primeOrderFailed); } @@ -319,7 +349,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester() : IMersenneNumberD return false; } - private static bool CheckDivisors64Range( + private bool CheckDivisors64Range( PrimeOrderCalculatorAccelerator gpu, ulong prime, ulong step, @@ -388,7 +418,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester() : IMersenneNumberD out foundDivisor); } - private static bool CheckDivisors64( + private bool CheckDivisors64( PrimeOrderCalculatorAccelerator gpu, ulong prime, ulong step, @@ -418,17 +448,17 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester() : IMersenneNumberD { MontgomeryDivisorData divisorData = divisorPool.FromModulus(divisor); ulong divisorCycle; - if (!MersenneDivisorCycles.TryCalculateCycleLengthForExponentCpu( - gpu, + if (!_tryCalculateCycleLengthForExponent( divisor, + gpu, prime, divisorData, out ulong computedCycle, out bool primeOrderFailed) || computedCycle == 0UL) { - divisorCycle = MersenneDivisorCycles.CalculateCycleLength( - gpu, + divisorCycle = _calculateCycleLength( divisor, + gpu, divisorData, skipPrimeOrderHeuristic: primeOrderFailed); } @@ -473,17 +503,17 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester() : IMersenneNumberD { MontgomeryDivisorData divisorData = divisorPool.FromModulus(divisor); ulong divisorCycle; - if (!MersenneDivisorCycles.TryCalculateCycleLengthForExponentCpu( - gpu, + if (!_tryCalculateCycleLengthForExponent( divisor, + gpu, prime, divisorData, out ulong computedCycle, out bool primeOrderFailed) || computedCycle == 0UL) { - divisorCycle = MersenneDivisorCycles.CalculateCycleLength( - gpu, + divisorCycle = _calculateCycleLength( divisor, + gpu, divisorData, skipPrimeOrderHeuristic: primeOrderFailed); } @@ -525,10 +555,80 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester() : IMersenneNumberD private static byte CheckDivisor(ulong prime, ulong divisorCycle, in MontgomeryDivisorData divisorData) { - ulong residue = prime.Pow2MontgomeryModWithCycleCpu(divisorCycle, divisorData); + ulong residue = prime.Pow2MontgomeryModWithCycleConvertToStandardCpu(divisorCycle, divisorData); return residue == 1UL ? (byte)1 : (byte)0; } + private static bool TryCalculateCycleLengthForExponentCpu( + ulong divisor, + PrimeOrderCalculatorAccelerator gpu, + ulong exponent, + in MontgomeryDivisorData divisorData, + out ulong cycleLength, + out bool primeOrderFailed) => MersenneDivisorCycles.TryCalculateCycleLengthForExponentCpu( + divisor, + exponent, + divisorData, + out cycleLength, + out primeOrderFailed); + + private static bool TryCalculateCycleLengthForExponentHybrid( + ulong divisor, + PrimeOrderCalculatorAccelerator gpu, + ulong exponent, + in MontgomeryDivisorData divisorData, + out ulong cycleLength, + out bool primeOrderFailed) => MersenneDivisorCycles.TryCalculateCycleLengthForExponentHybrid( + gpu, + divisor, + exponent, + divisorData, + out cycleLength, + out primeOrderFailed); + + private static bool TryCalculateCycleLengthForExponentGpu( + ulong divisor, + PrimeOrderCalculatorAccelerator gpu, + ulong exponent, + in MontgomeryDivisorData divisorData, + out ulong cycleLength, + out bool primeOrderFailed) => MersenneDivisorCycles.TryCalculateCycleLengthForExponentGpu( + gpu, + divisor, + exponent, + divisorData, + out cycleLength, + out primeOrderFailed); + + private static ulong CalculateCycleLengthCpu( + ulong divisor, + PrimeOrderCalculatorAccelerator gpu, + in MontgomeryDivisorData divisorData, + bool skipPrimeOrderHeuristic) => MersenneDivisorCycles.CalculateCycleLengthCpu( + divisor, + divisorData, + skipPrimeOrderHeuristic); + + private static ulong CalculateCycleLengthHybrid( + ulong divisor, + PrimeOrderCalculatorAccelerator gpu, + in MontgomeryDivisorData divisorData, + bool skipPrimeOrderHeuristic) => MersenneDivisorCycles.CalculateCycleLengthHybrid( + gpu, + divisor, + divisorData, + skipPrimeOrderHeuristic); + + private static ulong CalculateCycleLengthGpu( + ulong divisor, + PrimeOrderCalculatorAccelerator gpu, + in MontgomeryDivisorData divisorData, + bool skipPrimeOrderHeuristic) => MersenneDivisorCycles.CalculateCycleLengthGpu( + gpu, + divisor, + divisorData, + skipPrimeOrderHeuristic); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static byte AddMod3(byte value, byte delta) { diff --git a/PerfectNumbers.Core/Cpu/MersenneNumberIncrementalCpuTester.cs b/PerfectNumbers.Core/Cpu/MersenneNumberIncrementalCpuTester.cs index fecd156..7be16a5 100644 --- a/PerfectNumbers.Core/Cpu/MersenneNumberIncrementalCpuTester.cs +++ b/PerfectNumbers.Core/Cpu/MersenneNumberIncrementalCpuTester.cs @@ -31,7 +31,7 @@ public class MersenneNumberIncrementalCpuTester(GpuKernelType kernelType) { // TODO: Replace this direct GetCycle call with DivisorCycleCache.Lookup so we reuse the single snapshot block // and compute missing cycles on the configured device without queuing additional block generation. - qCycle = MersenneDivisorCycles.GetCycle(gpu, q); + qCycle = MersenneDivisorCycles.GetCycleCpu(q); shouldCheck = auto.Mod10R != 5UL; if (shouldCheck) { diff --git a/PerfectNumbers.Core/Cpu/MersenneNumberLucasLehmerCpuTester.cs b/PerfectNumbers.Core/Cpu/MersenneNumberLucasLehmerCpuTester.cs index 9440643..3e2f59b 100644 --- a/PerfectNumbers.Core/Cpu/MersenneNumberLucasLehmerCpuTester.cs +++ b/PerfectNumbers.Core/Cpu/MersenneNumberLucasLehmerCpuTester.cs @@ -22,7 +22,7 @@ public class MersenneNumberLucasLehmerCpuTester return false; } - if ((exponent & 3UL) == 1UL && exponent.SharesFactorWithExponentMinusOne(gpu)) + if ((exponent & 3UL) == 1UL && exponent.SharesFactorWithExponentMinusOneCpu()) { return false; } @@ -34,7 +34,7 @@ public class MersenneNumberLucasLehmerCpuTester for (UInt128 i = UInt128.Zero; i < limit; i++) { // Lucas–Lehmer squares the evolving residue, so we stay on the Montgomery powmod path rather than the pow2-specific helper. - s = s.PowModWithCycle(two, MersenneDivisorCycles.GetCycle(gpu, m)) - two; + s = s.PowModWithCycle(two, MersenneDivisorCycles.GetCycleCpu(m)) - two; if (s < UInt128.Zero) { s += m; diff --git a/PerfectNumbers.Core/Cpu/MersenneNumberOrderCpuTester.cs b/PerfectNumbers.Core/Cpu/MersenneNumberOrderCpuTester.cs index 1a8f64b..1f7f1f8 100644 --- a/PerfectNumbers.Core/Cpu/MersenneNumberOrderCpuTester.cs +++ b/PerfectNumbers.Core/Cpu/MersenneNumberOrderCpuTester.cs @@ -15,7 +15,7 @@ public class MersenneNumberOrderCpuTester(GpuKernelType kernelType) while (k <= maxK && Volatile.Read(ref isPrime)) { - qCycle = MersenneDivisorCycles.GetCycle(gpu, q); + qCycle = MersenneDivisorCycles.GetCycleCpu(q); // TODO: When this lookup misses the snapshot, invoke the configured device to compute the // single required cycle on demand without persisting it so the CPU order path retains the // cycle-stepping speedups while honoring the no-extra-cache constraint for large divisors. diff --git a/PerfectNumbers.Core/Cpu/MersenneNumberResidueCpuTester.cs b/PerfectNumbers.Core/Cpu/MersenneNumberResidueCpuTester.cs index 380d115..7529d62 100644 --- a/PerfectNumbers.Core/Cpu/MersenneNumberResidueCpuTester.cs +++ b/PerfectNumbers.Core/Cpu/MersenneNumberResidueCpuTester.cs @@ -7,7 +7,7 @@ public class MersenneNumberResidueCpuTester private ModResidueTracker? _mersenneResidueTracker; // CPU residue variant using tracker + unrolled residue updates (no method calls in the loop). - public void Scan(PrimeOrderCalculatorAccelerator gpu, ulong exponent, UInt128 twoP, LastDigit lastDigit, UInt128 maxK, ref bool isPrime) + public void Scan(ulong exponent, UInt128 twoP, LastDigit lastDigit, UInt128 maxK, ref bool isPrime) { // Initialize/update Mersenne residue tracker and start a merge walk for ascending q divisors @@ -94,7 +94,7 @@ public class MersenneNumberResidueCpuTester // (and below) so every qualifying q reuses precomputed lengths instead of recomputing via // tracker.MergeOrAppend. // cycle-based quick check for small q - if (localIsPrime && tracker.MergeOrAppend(gpu, exponent, q, out isDivider) && isDivider && + if (localIsPrime && tracker.MergeOrAppendCpu(exponent, q, out isDivider) && isDivider && (q <= ulong.MaxValue ? ((ulong)q).IsPrimeCandidate() : q.IsPrimeCandidate())) { localIsPrime = false; @@ -107,7 +107,7 @@ public class MersenneNumberResidueCpuTester ((allowMask >> (int)r10_1) & 1) != 0 && (r8_1 == 1UL || r8_1 == 7UL) && r3_1 != 0UL && r5_1 != 0UL && localIsPrime) { - if (localIsPrime && tracker.MergeOrAppend(gpu, exponent, qIncremental, out isDivider) && isDivider && + if (localIsPrime && tracker.MergeOrAppendCpu(exponent, qIncremental, out isDivider) && isDivider && (qIncremental <= ulong.MaxValue ? ((ulong)qIncremental).IsPrimeCandidate() : qIncremental.IsPrimeCandidate())) { localIsPrime = false; @@ -120,7 +120,7 @@ public class MersenneNumberResidueCpuTester ((allowMask >> (int)r10_2) & 1) != 0 && (r8_2 == 1UL || r8_2 == 7UL) && r3_2 != 0UL && r5_2 != 0UL && localIsPrime) { - if (localIsPrime && tracker.MergeOrAppend(gpu, exponent, qIncremental, out isDivider) && isDivider && + if (localIsPrime && tracker.MergeOrAppendCpu(exponent, qIncremental, out isDivider) && isDivider && (qIncremental <= ulong.MaxValue ? ((ulong)qIncremental).IsPrimeCandidate() : qIncremental.IsPrimeCandidate())) { localIsPrime = false; @@ -133,7 +133,7 @@ public class MersenneNumberResidueCpuTester ((allowMask >> (int)r10_3) & 1) != 0 && (r8_3 == 1UL || r8_3 == 7UL) && r3_3 != 0UL && r5_3 != 0UL && localIsPrime) { - if (localIsPrime && tracker.MergeOrAppend(gpu, exponent, qIncremental, out isDivider) && isDivider && + if (localIsPrime && tracker.MergeOrAppendCpu(exponent, qIncremental, out isDivider) && isDivider && (qIncremental <= ulong.MaxValue ? ((ulong)qIncremental).IsPrimeCandidate() : qIncremental.IsPrimeCandidate())) { localIsPrime = false; diff --git a/PerfectNumbers.Core/DivisorCycleCache.cs b/PerfectNumbers.Core/DivisorCycleCache.cs index 00bf481..aedfa4f 100644 --- a/PerfectNumbers.Core/DivisorCycleCache.cs +++ b/PerfectNumbers.Core/DivisorCycleCache.cs @@ -15,7 +15,6 @@ public sealed class DivisorCycleCache private readonly int _acceleratorIndex; private readonly Action, ArrayView1D, ArrayView1D, ArrayView1D, ArrayView1D> _gpuKernel; private volatile ulong[] _snapshot; - private volatile bool _useGpuGeneration = true; private readonly int _divisorCyclesBatchSize; private static int _sharedDivisorCyclesBatchSize = GpuConstants.GpuCycleStepsPerInvocation; @@ -47,23 +46,18 @@ public sealed class DivisorCycleCache _snapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot(); } - public void ConfigureGeneratorDevice(bool useGpu) - { - _useGpuGeneration = useGpu; - } - public void RefreshSnapshot() { _snapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot(); } - public ulong GetCycleLength(PrimeOrderCalculatorAccelerator gpu, ulong divisor, bool skipPrimeOrderHeuristic = false) + public ulong GetCycleLengthCpu(ulong divisor, bool skipPrimeOrderHeuristic = false) { if (!skipPrimeOrderHeuristic) { Span result = stackalloc ulong[1]; ReadOnlySpan singleDivisor = stackalloc ulong[1] { divisor }; - GetCycleLengths(gpu, singleDivisor, result); + GetCycleLengthsCpu(singleDivisor, result); return result[0]; } @@ -90,12 +84,88 @@ public sealed class DivisorCycleCache Queue divisorPool = MontgomeryDivisorDataPool.Shared; MontgomeryDivisorData divisorData = divisorPool.FromModulus(divisor); - cycleLength = MersenneDivisorCycles.CalculateCycleLength(gpu, divisor, divisorData, skipPrimeOrderHeuristic: true); + cycleLength = MersenneDivisorCycles.CalculateCycleLengthCpu(divisor, divisorData, skipPrimeOrderHeuristic: true); divisorPool.Return(divisorData); return cycleLength; } - public void GetCycleLengths(PrimeOrderCalculatorAccelerator gpu,ReadOnlySpan divisors, Span cycles) + public ulong GetCycleLengthHybrid(PrimeOrderCalculatorAccelerator gpu, ulong divisor, bool skipPrimeOrderHeuristic = false) + { + if (!skipPrimeOrderHeuristic) + { + Span result = stackalloc ulong[1]; + ReadOnlySpan singleDivisor = stackalloc ulong[1] { divisor }; + GetCycleLengthsHybrid(gpu, singleDivisor, result); + return result[0]; + } + + // The skipPrimeOrderHeuristic path is only used for divisors produced by the CPU by-divisor scanner, + // which always generates values greater than one. Keep this guard disabled here to avoid repeating the + // validation already performed by the general GetCycleLengths path. + // if (divisor <= 1UL) + // { + // throw new InvalidDataException("Divisor must be > 1"); + // } + + ulong[] snapshot = _snapshot; + ulong cycleLength; + if (divisor < (ulong)snapshot.Length) + { + cycleLength = snapshot[divisor]; + if (cycleLength == 0UL) + { + throw new InvalidDataException($"Divisor cycle is missing for {divisor}"); + } + + return cycleLength; + } + + Queue divisorPool = MontgomeryDivisorDataPool.Shared; + MontgomeryDivisorData divisorData = divisorPool.FromModulus(divisor); + cycleLength = MersenneDivisorCycles.CalculateCycleLengthHybrid(gpu, divisor, divisorData, skipPrimeOrderHeuristic: true); + divisorPool.Return(divisorData); + return cycleLength; + } + + public ulong GetCycleLengthGpu(PrimeOrderCalculatorAccelerator gpu, ulong divisor, bool skipPrimeOrderHeuristic = false) + { + if (!skipPrimeOrderHeuristic) + { + Span result = stackalloc ulong[1]; + ReadOnlySpan singleDivisor = stackalloc ulong[1] { divisor }; + GetCycleLengthsGpu(singleDivisor, result); + return result[0]; + } + + // The skipPrimeOrderHeuristic path is only used for divisors produced by the CPU by-divisor scanner, + // which always generates values greater than one. Keep this guard disabled here to avoid repeating the + // validation already performed by the general GetCycleLengths path. + // if (divisor <= 1UL) + // { + // throw new InvalidDataException("Divisor must be > 1"); + // } + + ulong[] snapshot = _snapshot; + ulong cycleLength; + if (divisor < (ulong)snapshot.Length) + { + cycleLength = snapshot[divisor]; + if (cycleLength == 0UL) + { + throw new InvalidDataException($"Divisor cycle is missing for {divisor}"); + } + + return cycleLength; + } + + Queue divisorPool = MontgomeryDivisorDataPool.Shared; + MontgomeryDivisorData divisorData = divisorPool.FromModulus(divisor); + cycleLength = MersenneDivisorCycles.CalculateCycleLengthGpu(gpu, divisor, divisorData, skipPrimeOrderHeuristic: true); + divisorPool.Return(divisorData); + return cycleLength; + } + + public void GetCycleLengthsCpu(ReadOnlySpan divisors, Span cycles) { ulong[] snapshot = _snapshot; int length = divisors.Length; @@ -133,16 +203,103 @@ public sealed class DivisorCycleCache if (missingCount != 0) { ReadOnlySpan missingIndices = missingBuffer[..missingCount]; - if (_useGpuGeneration) + ComputeCyclesCpu(divisors, cycles, missingIndices); + } + + if (rentedMissing is not null) + { + ThreadStaticPools.IntPool.Return(rentedMissing, clearArray: false); + } + } + + public void GetCycleLengthsHybrid(PrimeOrderCalculatorAccelerator gpu, ReadOnlySpan divisors, Span cycles) + { + ulong[] snapshot = _snapshot; + int length = divisors.Length; + int[]? rentedMissing = null; + Span missingBuffer = length <= StackBufferThreshold + ? stackalloc int[length] + : new Span(rentedMissing = ThreadStaticPools.IntPool.Rent(length), 0, length); + + int missingCount = 0; + + for (int i = 0; i < length; i++) + { + ulong divisor = divisors[i]; + if (divisor <= 1UL) { - ComputeCyclesGpu(divisors, cycles, missingIndices); + throw new InvalidDataException("Divisor must be > 1"); + } + + if (divisor < (ulong)snapshot.Length) + { + ulong cached = snapshot[divisor]; + if (cached == 0UL) + { + throw new InvalidDataException($"Divisor cycle is missing for {divisor}"); + } + + cycles[i] = cached; } else { - ComputeCyclesCpu(gpu, divisors, cycles, missingIndices); + missingBuffer[missingCount++] = i; } } + if (missingCount != 0) + { + ReadOnlySpan missingIndices = missingBuffer[..missingCount]; + ComputeCyclesHybrid(gpu, divisors, cycles, missingIndices); + } + + if (rentedMissing is not null) + { + ThreadStaticPools.IntPool.Return(rentedMissing, clearArray: false); + } + } + + public void GetCycleLengthsGpu(ReadOnlySpan divisors, Span cycles) + { + ulong[] snapshot = _snapshot; + int length = divisors.Length; + int[]? rentedMissing = null; + Span missingBuffer = length <= StackBufferThreshold + ? stackalloc int[length] + : new Span(rentedMissing = ThreadStaticPools.IntPool.Rent(length), 0, length); + + int missingCount = 0; + + for (int i = 0; i < length; i++) + { + ulong divisor = divisors[i]; + if (divisor <= 1UL) + { + throw new InvalidDataException("Divisor must be > 1"); + } + + if (divisor < (ulong)snapshot.Length) + { + ulong cached = snapshot[divisor]; + if (cached == 0UL) + { + throw new InvalidDataException($"Divisor cycle is missing for {divisor}"); + } + + cycles[i] = cached; + } + else + { + missingBuffer[missingCount++] = i; + } + } + + if (missingCount != 0) + { + ReadOnlySpan missingIndices = missingBuffer[..missingCount]; + ComputeCyclesGpu(divisors, cycles, missingIndices); + } + if (rentedMissing is not null) { ThreadStaticPools.IntPool.Return(rentedMissing, clearArray: false); @@ -187,7 +344,7 @@ public sealed class DivisorCycleCache } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ComputeCyclesCpu(PrimeOrderCalculatorAccelerator gpu, ReadOnlySpan divisors, Span cycles, ReadOnlySpan indices) + private static void ComputeCyclesCpu(ReadOnlySpan divisors, Span cycles, ReadOnlySpan indices) { Queue divisorPool = MontgomeryDivisorDataPool.Shared; for (int i = 0; i < indices.Length; i++) @@ -195,7 +352,21 @@ public sealed class DivisorCycleCache int targetIndex = indices[i]; ulong divisor = divisors[targetIndex]; MontgomeryDivisorData divisorData = divisorPool.FromModulus(divisor); - cycles[targetIndex] = MersenneDivisorCycles.CalculateCycleLength(gpu, divisor, divisorData); + cycles[targetIndex] = MersenneDivisorCycles.CalculateCycleLengthCpu(divisor, divisorData); + divisorPool.Return(divisorData); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ComputeCyclesHybrid(PrimeOrderCalculatorAccelerator gpu, ReadOnlySpan divisors, Span cycles, ReadOnlySpan indices) + { + Queue divisorPool = MontgomeryDivisorDataPool.Shared; + for (int i = 0; i < indices.Length; i++) + { + int targetIndex = indices[i]; + ulong divisor = divisors[targetIndex]; + MontgomeryDivisorData divisorData = divisorPool.FromModulus(divisor); + cycles[targetIndex] = MersenneDivisorCycles.CalculateCycleLengthHybrid(gpu, divisor, divisorData); divisorPool.Return(divisorData); } } diff --git a/PerfectNumbers.Core/ExponentRemainderStepperCpu.cs b/PerfectNumbers.Core/ExponentRemainderStepperCpu.cs index afa681b..83e960f 100644 --- a/PerfectNumbers.Core/ExponentRemainderStepperCpu.cs +++ b/PerfectNumbers.Core/ExponentRemainderStepperCpu.cs @@ -23,16 +23,14 @@ internal struct ExponentRemainderStepperCpu _currentMontgomery = divisor.MontgomeryOne; } - public bool MatchesDivisor(in MontgomeryDivisorData divisor) - { - return _modulus == divisor.Modulus && _nPrime == divisor.NPrime && _montgomeryOne == divisor.MontgomeryOne; - } + public readonly bool MatchesDivisor(in MontgomeryDivisorData divisor) + => _modulus == divisor.Modulus && _nPrime == divisor.NPrime && _montgomeryOne == divisor.MontgomeryOne; - /// - /// Clears the cached Montgomery residue so the stepper can be reused for another divisor. - /// Callers must reinitialize the stepper before consuming any residues after a reset. - /// - public void Reset() + /// + /// Clears the cached Montgomery residue so the stepper can be reused for another divisor. + /// Callers must reinitialize the stepper before consuming any residues after a reset. + /// + public void Reset() { PreviousExponent = 0UL; _currentMontgomery = _montgomeryOne; @@ -73,7 +71,7 @@ internal struct ExponentRemainderStepperCpu // TODO: Once divisor cycle lengths are mandatory, pull the delta multiplier from the // single-block divisor-cycle snapshot so we can skip the powmod entirely and reuse the // cached Montgomery residue ladder highlighted in MersenneDivisorCycleLengthGpuBenchmarks. - ulong multiplier = delta.Pow2MontgomeryModWindowedCpu(_divisor, keepMontgomery: true); + ulong multiplier = delta.Pow2MontgomeryModWindowedKeepMontgomeryCpu(_divisor); _currentMontgomery = _currentMontgomery.MontgomeryMultiplyCpu(multiplier, _modulus, _nPrime); PreviousExponent = exponent; return ReduceCurrent(); @@ -91,7 +89,7 @@ internal struct ExponentRemainderStepperCpu ulong delta = exponent - PreviousExponent; // TODO: Reuse the divisor-cycle derived Montgomery delta once the cache exposes single-cycle // lookups so this branch also avoids recomputing powmods when the snapshot lacks the divisor. - ulong multiplier = delta.Pow2MontgomeryModWindowedCpu(_divisor, keepMontgomery: true); + ulong multiplier = delta.Pow2MontgomeryModWindowedKeepMontgomeryCpu(_divisor); _currentMontgomery = _currentMontgomery.MontgomeryMultiplyCpu(multiplier, _modulus, _nPrime); PreviousExponent = exponent; return _currentMontgomery == _montgomeryOne; @@ -134,9 +132,9 @@ internal struct ExponentRemainderStepperCpu private void InitializeCpuState(ulong exponent) { - _currentMontgomery = exponent.Pow2MontgomeryModWindowedCpu(_divisor, keepMontgomery: true); + _currentMontgomery = exponent.Pow2MontgomeryModWindowedKeepMontgomeryCpu(_divisor); PreviousExponent = exponent; } - private ulong ReduceCurrent() => _currentMontgomery.MontgomeryMultiplyCpu(1UL, _modulus, _nPrime); + private readonly ulong ReduceCurrent() => _currentMontgomery.MontgomeryMultiplyCpu(1UL, _modulus, _nPrime); } diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs index b28e598..355636a 100644 --- a/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs +++ b/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs @@ -511,13 +511,12 @@ public sealed partial class MersenneNumberDivisorByDivisorGpuTester : IMersenneN return composite; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ulong ResolveDivisorCycle(PrimeOrderCalculatorAccelerator gpu, ulong divisor, ulong prime, in MontgomeryDivisorData divisorData) { - if (!MersenneDivisorCycles.TryCalculateCycleLengthForExponentCpu(gpu, divisor, prime, divisorData, out ulong computedCycle, out bool primeOrderFailed) || computedCycle == 0UL) + if (!MersenneDivisorCycles.TryCalculateCycleLengthForExponentGpu(gpu, divisor, prime, divisorData, out ulong computedCycle, out bool primeOrderFailed) || computedCycle == 0UL) { - return MersenneDivisorCycles.CalculateCycleLength(gpu, divisor, divisorData, skipPrimeOrderHeuristic: primeOrderFailed); + return MersenneDivisorCycles.CalculateCycleLengthGpu(gpu, divisor, divisorData, skipPrimeOrderHeuristic: primeOrderFailed); } return computedCycle; @@ -729,9 +728,9 @@ public sealed partial class MersenneNumberDivisorByDivisorGpuTester : IMersenneN var gpu = _primeOrderCalculatorAccelerator; if (cycle == 0UL) { - if (!MersenneDivisorCycles.TryCalculateCycleLengthForExponentCpu(gpu, divisor, firstPrime, divisorData, out ulong computedCycle, out bool primeOrderFailed) || computedCycle == 0UL) + if (!MersenneDivisorCycles.TryCalculateCycleLengthForExponentCpu(divisor, firstPrime, divisorData, out ulong computedCycle, out bool primeOrderFailed) || computedCycle == 0UL) { - cycle = MersenneDivisorCycles.CalculateCycleLength(gpu, divisor, divisorData, skipPrimeOrderHeuristic: primeOrderFailed); + cycle = MersenneDivisorCycles.CalculateCycleLengthGpu(gpu, divisor, divisorData, skipPrimeOrderHeuristic: primeOrderFailed); } else { diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberDivisorGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberDivisorGpuTester.cs index ff187d0..b2ae032 100644 --- a/PerfectNumbers.Core/Gpu/MersenneNumberDivisorGpuTester.cs +++ b/PerfectNumbers.Core/Gpu/MersenneNumberDivisorGpuTester.cs @@ -125,7 +125,6 @@ public sealed class MersenneNumberDivisorGpuTester } UInt128 kMul2; - var divisorCycles = MersenneDivisorCycles.Shared; UInt128 maxK2 = UInt128.MaxValue / ((UInt128)p << 1); ulong limit = divisorCyclesSearchLimit; if ((UInt128)limit > maxK2) @@ -143,7 +142,7 @@ public sealed class MersenneNumberDivisorGpuTester continue; } - UInt128 cycle128 = MersenneDivisorCycles.GetCycle(gpu, d); + UInt128 cycle128 = MersenneDivisorCycles.GetCycleGpu(gpu, d); if ((UInt128)p % cycle128 != UInt128.Zero) { continue; diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberLucasLehmerGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberLucasLehmerGpuTester.cs index 09d7aee..79bdbb1 100644 --- a/PerfectNumbers.Core/Gpu/MersenneNumberLucasLehmerGpuTester.cs +++ b/PerfectNumbers.Core/Gpu/MersenneNumberLucasLehmerGpuTester.cs @@ -143,7 +143,7 @@ public class MersenneNumberLucasLehmerGpuTester return false; } - if ((exponent & 3UL) == 1UL && exponent.SharesFactorWithExponentMinusOne(gpu)) + if ((exponent & 3UL) == 1UL && exponent.SharesFactorWithExponentMinusOneGpu(gpu)) { return false; } diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs index c990e50..01ad758 100644 --- a/PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs +++ b/PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs @@ -2,6 +2,7 @@ using System.Runtime.InteropServices; using System.Runtime.CompilerServices; using ILGPU; using ILGPU.Runtime; +using PerfectNumbers.Core.Gpu.Accelerators; namespace PerfectNumbers.Core.Gpu; @@ -21,18 +22,16 @@ public class MersenneNumberResidueGpuTester(bool useGpuOrder) // TODO: When cycles are missing for larger q values, compute only the single required cycle on the device // requested by the caller, skip queuing additional block generation, and keep the snapshot cache untouched. - private static readonly Accelerator[] _accelerators = AcceleratorPool.Shared.Accelerators; - // GPU residue variant: check 2^p % q == 1 for q = 2*p*k + 1. - public void Scan(ulong exponent, UInt128 twoP, LastDigit lastDigit, UInt128 maxK, ref bool isPrime) + public void Scan(PrimeOrderCalculatorAccelerator gpu, ulong exponent, UInt128 twoP, LastDigit lastDigit, UInt128 maxK, ref bool isPrime) { // GpuPrimeWorkLimiter.Acquire(); - var acceleratorIndex = AcceleratorPool.Shared.Rent(); - var accelerator = _accelerators[acceleratorIndex]; + var acceleratorIndex = gpu.AcceleratorIndex; + var accelerator = gpu.Accelerator; // Monitor.Enter(gpuLease.ExecutionLock); - var stream = accelerator.CreateStream(); + var stream = AcceleratorStreamPool.Rent(acceleratorIndex); var kernel = GetPow2ModKernel(accelerator); var resources = RentResources(accelerator, GpuConstants.ScanBatchSize); var gpuKernels = GpuKernelPool.GetOrAddKernels(acceleratorIndex, stream, KernelType.SmallCycles | KernelType.SmallPrimes); @@ -109,7 +108,7 @@ public class MersenneNumberResidueGpuTester(bool useGpuOrder) } ReturnResources(resources); - stream.Dispose(); + AcceleratorStreamPool.Return(acceleratorIndex, stream); // Monitor.Exit(gpuLease.ExecutionLock); gpuKernels.Dispose(); // GpuPrimeWorkLimiter.Release(); diff --git a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs index 39ea7f6..7b2430e 100644 --- a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs +++ b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs @@ -248,18 +248,9 @@ internal static partial class PrimeOrderGpuHeuristics { order = 0UL; - // GpuPrimeWorkLimiter.Acquire(); int acceleratorIndex = gpu.AcceleratorIndex; var smallPrimesView = gpu.SmallPrimeFactorPrimes; var smallSquaresView = gpu.SmallPrimeFactorSquares; - // var phiFactorBufferView = gpu.PhiFactorBufferView; - // var phiExponentBufferView = gpu.PhiExponentBufferView; - // var workFactorBufferView = gpu.WorkFactorBufferView; - // var workExponentBufferView = gpu.WorkExponentBufferView; - // var candidateBufferView = gpu.CandidateBufferView; - // var stackIndexBufferView = gpu.StackIndexBufferView; - // var stackExponentBufferView = gpu.StackExponentBufferView; - // var stackProductBufferView = gpu.StackProductBufferView; var resultBufferView = gpu.OutputUlongView2; var statusBufferView = gpu.OutputByteView; @@ -274,14 +265,6 @@ internal static partial class PrimeOrderGpuHeuristics AcceleratorStream stream = AcceleratorStreamPool.Rent(acceleratorIndex); // TODO: Remove the cleaning after the order kernel is modified to always set the result. - // phiFactorBufferView.MemSetToZero(stream); - // phiExponentBufferView.MemSetToZero(stream); - // workFactorBufferView.MemSetToZero(stream); - // workExponentBufferView.MemSetToZero(stream); - // candidateBufferView.MemSetToZero(stream); - // stackIndexBufferView.MemSetToZero(stream); - // stackExponentBufferView.MemSetToZero(stream); - // stackProductBufferView.MemSetToZero(stream); resultBufferView.MemSetToZero(stream); statusBufferView.MemSetToZero(stream); @@ -307,7 +290,6 @@ internal static partial class PrimeOrderGpuHeuristics stream.Synchronize(); AcceleratorStreamPool.Return(acceleratorIndex, stream); - // GpuPrimeWorkLimiter.Release(); PrimeOrderKernelStatus kernelStatus = (PrimeOrderKernelStatus)status; if (kernelStatus == PrimeOrderKernelStatus.Fallback) diff --git a/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs b/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs index aa04dcc..51ee784 100644 --- a/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs @@ -479,7 +479,48 @@ public sealed class HeuristicCombinedPrimeTester } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static ulong ResolveHeuristicCycleLength( + internal static ulong ResolveHeuristicCycleLengthCpu( + ulong exponent, + in HeuristicDivisorPreparation preparation, + out bool cycleFromHint, + out bool cycleComputed, + out bool primeOrderFailed) + { + if (preparation.HasCycleLengthHint && preparation.CycleLengthHint != 0UL) + { + cycleFromHint = true; + cycleComputed = true; + primeOrderFailed = false; + return preparation.CycleLengthHint; + } + + ulong divisor = preparation.Candidate.Value; + MontgomeryDivisorData divisorData = preparation.DivisorData; + + bool trySuccess = MersenneDivisorCycles.TryCalculateCycleLengthForExponentCpu( + divisor, + exponent, + divisorData, + out ulong computedCycle, + out bool primeOrderFailedLocal); + + if (trySuccess && computedCycle != 0UL) + { + cycleFromHint = false; + cycleComputed = true; + primeOrderFailed = primeOrderFailedLocal; + return computedCycle; + } + + primeOrderFailed = primeOrderFailedLocal || !trySuccess || computedCycle == 0UL; + ulong resolvedCycle = MersenneDivisorCycles.CalculateCycleLengthCpu(divisor, divisorData, skipPrimeOrderHeuristic: primeOrderFailed); + cycleFromHint = false; + cycleComputed = resolvedCycle != 0UL; + return resolvedCycle; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ulong ResolveHeuristicCycleLengthGpu( PrimeOrderCalculatorAccelerator gpu, ulong exponent, in HeuristicDivisorPreparation preparation, @@ -499,7 +540,6 @@ public sealed class HeuristicCombinedPrimeTester MontgomeryDivisorData divisorData = preparation.DivisorData; bool trySuccess = MersenneDivisorCycles.TryCalculateCycleLengthForExponentCpu( - gpu, divisor, exponent, divisorData, @@ -515,7 +555,7 @@ public sealed class HeuristicCombinedPrimeTester } primeOrderFailed = primeOrderFailedLocal || !trySuccess || computedCycle == 0UL; - ulong resolvedCycle = MersenneDivisorCycles.CalculateCycleLength(gpu, divisor, divisorData, skipPrimeOrderHeuristic: primeOrderFailed); + ulong resolvedCycle = MersenneDivisorCycles.CalculateCycleLengthGpu(gpu, divisor, divisorData, skipPrimeOrderHeuristic: primeOrderFailed); cycleFromHint = false; cycleComputed = resolvedCycle != 0UL; return resolvedCycle; diff --git a/PerfectNumbers.Core/HeuristicPrimeTester.cs b/PerfectNumbers.Core/HeuristicPrimeTester.cs index 1d63c28..b5f46e5 100644 --- a/PerfectNumbers.Core/HeuristicPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicPrimeTester.cs @@ -449,7 +449,48 @@ public sealed class HeuristicPrimeTester } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static ulong ResolveHeuristicCycleLength( + internal static ulong ResolveHeuristicCycleLengthCpu( + ulong exponent, + in HeuristicDivisorPreparation preparation, + out bool cycleFromHint, + out bool cycleComputed, + out bool primeOrderFailed) + { + if (preparation.HasCycleLengthHint && preparation.CycleLengthHint != 0UL) + { + cycleFromHint = true; + cycleComputed = true; + primeOrderFailed = false; + return preparation.CycleLengthHint; + } + + ulong divisor = preparation.Candidate.Value; + MontgomeryDivisorData divisorData = preparation.DivisorData; + + bool trySuccess = MersenneDivisorCycles.TryCalculateCycleLengthForExponentCpu( + divisor, + exponent, + divisorData, + out ulong computedCycle, + out bool primeOrderFailedLocal); + + if (trySuccess && computedCycle != 0UL) + { + cycleFromHint = false; + cycleComputed = true; + primeOrderFailed = primeOrderFailedLocal; + return computedCycle; + } + + primeOrderFailed = primeOrderFailedLocal || !trySuccess || computedCycle == 0UL; + ulong resolvedCycle = MersenneDivisorCycles.CalculateCycleLengthCpu(divisor, divisorData, skipPrimeOrderHeuristic: primeOrderFailed); + cycleFromHint = false; + cycleComputed = resolvedCycle != 0UL; + return resolvedCycle; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ulong ResolveHeuristicCycleLengthGpu( PrimeOrderCalculatorAccelerator gpu, ulong exponent, in HeuristicDivisorPreparation preparation, @@ -469,7 +510,6 @@ public sealed class HeuristicPrimeTester MontgomeryDivisorData divisorData = preparation.DivisorData; bool trySuccess = MersenneDivisorCycles.TryCalculateCycleLengthForExponentCpu( - gpu, divisor, exponent, divisorData, @@ -485,7 +525,7 @@ public sealed class HeuristicPrimeTester } primeOrderFailed = primeOrderFailedLocal || !trySuccess || computedCycle == 0UL; - ulong resolvedCycle = MersenneDivisorCycles.CalculateCycleLength(gpu, divisor, divisorData, skipPrimeOrderHeuristic: primeOrderFailed); + ulong resolvedCycle = MersenneDivisorCycles.CalculateCycleLengthGpu(gpu, divisor, divisorData, skipPrimeOrderHeuristic: primeOrderFailed); cycleFromHint = false; cycleComputed = resolvedCycle != 0UL; return resolvedCycle; diff --git a/PerfectNumbers.Core/MersenneDivisorCycles.cs b/PerfectNumbers.Core/MersenneDivisorCycles.cs index 70b0931..ca5d664 100644 --- a/PerfectNumbers.Core/MersenneDivisorCycles.cs +++ b/PerfectNumbers.Core/MersenneDivisorCycles.cs @@ -75,17 +75,93 @@ public class MersenneDivisorCycles } } - if (TryCalculateCycleLengthHeuristic(gpu, divisor, divisorData, out ulong heuristicCycle) && heuristicCycle != 0UL) + if (TryCalculateCycleLengthHeuristicCpu(divisor, divisorData, out ulong heuristicCycle) && heuristicCycle != 0UL) { return heuristicCycle == exponent; } - if (TryCalculateCycleLengthForExponentCpu(gpu, divisor, exponent, divisorData, out ulong cycleLength, out bool _) && cycleLength != 0UL) + if (TryCalculateCycleLengthForExponentCpu(divisor, exponent, divisorData, out ulong cycleLength, out bool _) && cycleLength != 0UL) { return cycleLength == exponent; } - return exponent.Pow2MontgomeryModWindowedCpu(divisorData, keepMontgomery: false) == 1UL; + return exponent.Pow2MontgomeryModWindowedConvertToStandardCpu(divisorData) == 1UL; + } + + public static bool CycleEqualsExponentGpu(PrimeOrderCalculatorAccelerator gpu, ulong divisor, in MontgomeryDivisorData divisorData, ulong exponent) + { + if (CycleEqualsExponentForMersenneCandidate(divisor, divisorData, exponent)) + { + return true; + } + + if (exponent <= 1UL || divisor <= 1UL || (divisor & 1UL) == 0UL) + { + return false; + } + + if (divisor <= PerfectNumberConstants.MaxQForDivisorCycles) + { + var small = Shared._smallCycles; + if (small is not null) + { + ulong cached = small[(int)divisor]; + if (cached != 0UL) + { + return cached == exponent; + } + } + } + + if (TryCalculateCycleLengthHeuristicGpu(gpu, divisor, divisorData, out ulong heuristicCycle) && heuristicCycle != 0UL) + { + return heuristicCycle == exponent; + } + + if (TryCalculateCycleLengthForExponentCpu(divisor, exponent, divisorData, out ulong cycleLength, out bool _) && cycleLength != 0UL) + { + return cycleLength == exponent; + } + + return exponent.Pow2MontgomeryModWindowedConvertToStandardCpu(divisorData) == 1UL; + } + + public static bool CycleEqualsExponentHybrid(PrimeOrderCalculatorAccelerator gpu, ulong divisor, in MontgomeryDivisorData divisorData, ulong exponent) + { + if (CycleEqualsExponentForMersenneCandidate(divisor, divisorData, exponent)) + { + return true; + } + + if (exponent <= 1UL || divisor <= 1UL || (divisor & 1UL) == 0UL) + { + return false; + } + + if (divisor <= PerfectNumberConstants.MaxQForDivisorCycles) + { + var small = Shared._smallCycles; + if (small is not null) + { + ulong cached = small[(int)divisor]; + if (cached != 0UL) + { + return cached == exponent; + } + } + } + + if (TryCalculateCycleLengthHeuristicHybrid(gpu, divisor, divisorData, out ulong heuristicCycle) && heuristicCycle != 0UL) + { + return heuristicCycle == exponent; + } + + if (TryCalculateCycleLengthForExponentCpu(divisor, exponent, divisorData, out ulong cycleLength, out bool _) && cycleLength != 0UL) + { + return cycleLength == exponent; + } + + return exponent.Pow2MontgomeryModWindowedConvertToStandardCpu(divisorData) == 1UL; } public static bool CycleEqualsExponentForMersenneCandidate(ulong divisor, in MontgomeryDivisorData divisorData, ulong exponent) @@ -111,7 +187,7 @@ public class MersenneDivisorCycles } } - return exponent.Pow2MontgomeryModWindowedCpu(divisorData, keepMontgomery: false) == 1UL; + return exponent.Pow2MontgomeryModWindowedConvertToStandardCpu(divisorData) == 1UL; } private static bool IsValidMersenneDivisorCandidate(ulong divisor, ulong exponent) @@ -163,7 +239,7 @@ public class MersenneDivisorCycles } } - public ulong GetCycle(PrimeOrderCalculatorAccelerator gpu, ulong divisor, in MontgomeryDivisorData divisorData) + public ulong GetCycle(ulong divisor, in MontgomeryDivisorData divisorData) { // Fast-path: in-memory array for small divisors if (divisor <= PerfectNumberConstants.MaxQForDivisorCycles) @@ -208,10 +284,106 @@ public class MersenneDivisorCycles // TODO: Route this miss to an ephemeral single-cycle computation on the device selected by the current // configuration (and skip persisting the result) when the divisor falls outside the in-memory snapshot // so we respect the large-p memory limits and avoid extra cache locks. - return CalculateCycleLength(gpu, divisor, divisorData); + return CalculateCycleLengthCpu(divisor, divisorData); } - public static UInt128 GetCycle(PrimeOrderCalculatorAccelerator gpu, UInt128 divisor) + public ulong GetCycleGpu(PrimeOrderCalculatorAccelerator gpu, ulong divisor, in MontgomeryDivisorData divisorData) + { + // Fast-path: in-memory array for small divisors + if (divisor <= PerfectNumberConstants.MaxQForDivisorCycles) + { + var arr = _smallCycles; + if (arr is not null) + { + ulong cached = arr[(int)divisor]; + if (cached != 0UL) + { + return cached; + } + } + } + + // Binary search for divisor in sorted _table + int mid, right, left = 0; + ulong d; + List<(ulong divisor, ulong cycleLength)> cycles = _table; + + right = cycles.Count - 1; + while (left <= right) + { + mid = left + ((right - left) >> 1); + d = cycles[mid].divisor; + + if (d == divisor) + { + return cycles[mid].cycleLength; + } + + if (d < divisor) + { + left = mid + 1; + } + else + { + right = mid - 1; + } + } + + // TODO: Route this miss to an ephemeral single-cycle computation on the device selected by the current + // configuration (and skip persisting the result) when the divisor falls outside the in-memory snapshot + // so we respect the large-p memory limits and avoid extra cache locks. + return CalculateCycleLengthGpu(gpu, divisor, divisorData); + } + + public ulong GetCycleHybrid(PrimeOrderCalculatorAccelerator gpu, ulong divisor, in MontgomeryDivisorData divisorData) + { + // Fast-path: in-memory array for small divisors + if (divisor <= PerfectNumberConstants.MaxQForDivisorCycles) + { + var arr = _smallCycles; + if (arr is not null) + { + ulong cached = arr[(int)divisor]; + if (cached != 0UL) + { + return cached; + } + } + } + + // Binary search for divisor in sorted _table + int mid, right, left = 0; + ulong d; + List<(ulong divisor, ulong cycleLength)> cycles = _table; + + right = cycles.Count - 1; + while (left <= right) + { + mid = left + ((right - left) >> 1); + d = cycles[mid].divisor; + + if (d == divisor) + { + return cycles[mid].cycleLength; + } + + if (d < divisor) + { + left = mid + 1; + } + else + { + right = mid - 1; + } + } + + // TODO: Route this miss to an ephemeral single-cycle computation on the device selected by the current + // configuration (and skip persisting the result) when the divisor falls outside the in-memory snapshot + // so we respect the large-p memory limits and avoid extra cache locks. + return CalculateCycleLengthHybrid(gpu, divisor, divisorData); + } + + public static UInt128 GetCycleCpu(UInt128 divisor) { // For divisor = 2^k, cycle is 1 if ((divisor & (divisor - UInt128.One)) == UInt128.Zero) @@ -224,17 +396,111 @@ public class MersenneDivisorCycles ulong divisor64 = (ulong)divisor; Queue divisorPool = MontgomeryDivisorDataPool.Shared; MontgomeryDivisorData divisorData = divisorPool.FromModulus(divisor64); - ulong cycle = CalculateCycleLength(gpu, divisor64, divisorData); + ulong cycle = CalculateCycleLengthCpu(divisor64, divisorData); divisorPool.Return(divisorData); return (UInt128)cycle; } - UInt128 wideOrder = PrimeOrderCalculator.Calculate( + UInt128 wideOrder = PrimeOrderCalculator.CalculateCpu( + divisor, + previousOrder: null, + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); + if (wideOrder != UInt128.Zero) + { + return wideOrder; + } + + // Otherwise, find order of 2 mod divisor + // TODO: Port this UInt128 path to the unrolled-hex cycle calculator so wide + // divisors stop relying on the slow shift-and-subtract loop measured in the + // GPU benchmarks. + UInt128 order = UInt128.One; + UInt128 pow = UInt128Numbers.Two; + while (pow != UInt128.One) + { + pow <<= 1; + if (pow >= divisor) + { + pow -= divisor; + } + + order++; + } + + return order; + } + + public static UInt128 GetCycleHybrid(PrimeOrderCalculatorAccelerator gpu, UInt128 divisor) + { + // For divisor = 2^k, cycle is 1 + if ((divisor & (divisor - UInt128.One)) == UInt128.Zero) + { + return UInt128.One; + } + + if (divisor <= ulong.MaxValue) + { + ulong divisor64 = (ulong)divisor; + Queue divisorPool = MontgomeryDivisorDataPool.Shared; + MontgomeryDivisorData divisorData = divisorPool.FromModulus(divisor64); + ulong cycle = CalculateCycleLengthHybrid(gpu, divisor64, divisorData); + divisorPool.Return(divisorData); + return (UInt128)cycle; + } + + UInt128 wideOrder = PrimeOrderCalculator.CalculateHybrid( gpu, divisor, previousOrder: null, - PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault, - PrimeOrderCalculator.PrimeOrderHeuristicDevice.Cpu); + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); + if (wideOrder != UInt128.Zero) + { + return wideOrder; + } + + // Otherwise, find order of 2 mod divisor + // TODO: Port this UInt128 path to the unrolled-hex cycle calculator so wide + // divisors stop relying on the slow shift-and-subtract loop measured in the + // GPU benchmarks. + UInt128 order = UInt128.One; + UInt128 pow = UInt128Numbers.Two; + while (pow != UInt128.One) + { + pow <<= 1; + if (pow >= divisor) + { + pow -= divisor; + } + + order++; + } + + return order; + } + + public static UInt128 GetCycleGpu(PrimeOrderCalculatorAccelerator gpu, UInt128 divisor) + { + // For divisor = 2^k, cycle is 1 + if ((divisor & (divisor - UInt128.One)) == UInt128.Zero) + { + return UInt128.One; + } + + if (divisor <= ulong.MaxValue) + { + ulong divisor64 = (ulong)divisor; + Queue divisorPool = MontgomeryDivisorDataPool.Shared; + MontgomeryDivisorData divisorData = divisorPool.FromModulus(divisor64); + ulong cycle = CalculateCycleLengthGpu(gpu, divisor64, divisorData); + divisorPool.Return(divisorData); + return (UInt128)cycle; + } + + UInt128 wideOrder = PrimeOrderCalculator.CalculateGpu( + gpu, + divisor, + previousOrder: null, + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); if (wideOrder != UInt128.Zero) { return wideOrder; @@ -261,7 +527,6 @@ public class MersenneDivisorCycles } public static bool TryCalculateCycleLengthForExponentCpu( - PrimeOrderCalculatorAccelerator gpu, ulong divisor, ulong exponent, in MontgomeryDivisorData divisorData, @@ -285,13 +550,11 @@ public class MersenneDivisorCycles // return true; // } - ulong computedOrder = PrimeOrderCalculator.Calculate( - gpu, + ulong computedOrder = PrimeOrderCalculator.CalculateCpu( divisor, previousOrder: null, divisorData, - PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault, - PrimeOrderCalculator.PrimeOrderHeuristicDevice.Cpu); + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); if (computedOrder != 0UL) { cycleLength = computedOrder; @@ -331,8 +594,178 @@ public class MersenneDivisorCycles factorCounts[2UL] = twoCount; } - bool exponentAccumulated = AccumulateFactors(gpu, exponent, factorCounts); - bool factorsAccumulated = exponentAccumulated && AccumulateFactors(gpu, k, factorCounts); + bool exponentAccumulated = AccumulateFactors(exponent, factorCounts); + bool factorsAccumulated = exponentAccumulated && AccumulateFactors(k, factorCounts); + + if (factorsAccumulated) + { + cycleLength = ReduceOrder(divisorData, phi, factorCounts); + success = true; + primeOrderFailed = false; + } + + factorCounts.Clear(); + ThreadStaticPools.ReturnFactorCountDictionary(factorCounts); + return success; + } + + public static bool TryCalculateCycleLengthForExponentHybrid( + PrimeOrderCalculatorAccelerator gpu, + ulong divisor, + ulong exponent, + in MontgomeryDivisorData divisorData, + out ulong cycleLength, + out bool primeOrderFailed) + { + cycleLength = 0UL; + primeOrderFailed = false; + + // EvenPerfectBitScanner never feeds powers of two into this helper, so leave the branch commented out to avoid redundant work. + // if ((divisor & (divisor - 1UL)) == 0UL) + // { + // cycleLength = 1UL; + // return true; + // } + + // The scanner only calls this path with prime exponents greater than one and odd divisors above three, keeping the small-parameter guard unreachable. + // if (divisor <= 3UL || exponent <= 1UL) + // { + // cycleLength = CalculateCycleLength(divisor, divisorData); + // return true; + // } + + ulong computedOrder = PrimeOrderCalculator.CalculateHybrid( + gpu, + divisor, + previousOrder: null, + divisorData, + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); + if (computedOrder != 0UL) + { + cycleLength = computedOrder; + return true; + } + + primeOrderFailed = true; + + ulong phi = divisor - 1UL; + // A zero totient would imply divisor == 0, which never occurs on the tested path. Keep the guard documented without + // branching in the hot loop. + // if (phi == 0UL) + // { + // return false; + // } + + int twoCount = BitOperations.TrailingZeroCount(phi); + ulong reducedPhi = phi >> twoCount; + if (reducedPhi == 0UL || (reducedPhi % exponent) != 0UL) + { + return false; + } + + ulong k = reducedPhi / exponent; + // k == 0 would indicate reducedPhi < exponent, which the EvenPerfectBitScanner call flow prevents. Document the + // assumption instead of branching. + // if (k == 0UL) + // { + // return false; + // } + + Dictionary factorCounts = ThreadStaticPools.RentFactorCountDictionary(); + bool success = false; + + if (twoCount > 0) + { + factorCounts[2UL] = twoCount; + } + + bool exponentAccumulated = AccumulateFactors(exponent, factorCounts); + bool factorsAccumulated = exponentAccumulated && AccumulateFactors(k, factorCounts); + + if (factorsAccumulated) + { + cycleLength = ReduceOrder(divisorData, phi, factorCounts); + success = true; + primeOrderFailed = false; + } + + factorCounts.Clear(); + ThreadStaticPools.ReturnFactorCountDictionary(factorCounts); + return success; + } + + public static bool TryCalculateCycleLengthForExponentGpu( + PrimeOrderCalculatorAccelerator gpu, + ulong divisor, + ulong exponent, + in MontgomeryDivisorData divisorData, + out ulong cycleLength, + out bool primeOrderFailed) + { + cycleLength = 0UL; + primeOrderFailed = false; + + // EvenPerfectBitScanner never feeds powers of two into this helper, so leave the branch commented out to avoid redundant work. + // if ((divisor & (divisor - 1UL)) == 0UL) + // { + // cycleLength = 1UL; + // return true; + // } + + // The scanner only calls this path with prime exponents greater than one and odd divisors above three, keeping the small-parameter guard unreachable. + // if (divisor <= 3UL || exponent <= 1UL) + // { + // cycleLength = CalculateCycleLength(divisor, divisorData); + // return true; + // } + + ulong computedOrder = PrimeOrderCalculator.CalculateGpu( + gpu, + divisor, + previousOrder: null, + divisorData, + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); + if (computedOrder != 0UL) + { + cycleLength = computedOrder; + return true; + } + + primeOrderFailed = true; + + ulong phi = divisor - 1UL; + // A zero totient would imply divisor == 0, which never occurs on the tested path. Keep the guard documented without + // branching in the hot loop. + // if (phi == 0UL) + // { + // return false; + // } + + int twoCount = BitOperations.TrailingZeroCount(phi); + ulong reducedPhi = phi >> twoCount; + if (reducedPhi == 0UL || (reducedPhi % exponent) != 0UL) + { + return false; + } + + ulong k = reducedPhi / exponent; + // k == 0 would indicate reducedPhi < exponent, which the EvenPerfectBitScanner call flow prevents. Document the + // assumption instead of branching. + // if (k == 0UL) + // { + // return false; + // } + + Dictionary factorCounts = ThreadStaticPools.RentFactorCountDictionary(); + bool success = false; + + if (twoCount > 0) + { + factorCounts[2UL] = twoCount; + } + + bool exponentAccumulated = AccumulateFactors(exponent, factorCounts); + bool factorsAccumulated = exponentAccumulated && AccumulateFactors(k, factorCounts); if (factorsAccumulated) { @@ -347,7 +780,6 @@ public class MersenneDivisorCycles } private static bool AccumulateFactors( - PrimeOrderCalculatorAccelerator gpu, ulong value, Dictionary counts) { @@ -357,7 +789,7 @@ public class MersenneDivisorCycles } Dictionary scratch = ThreadStaticPools.RentFactorScratchDictionary(); - bool success = TryFactorIntoCountsInternal(gpu, value, scratch); + bool success = TryFactorIntoCountsInternal(value, scratch); if (!success) { scratch.Clear(); @@ -379,7 +811,7 @@ public class MersenneDivisorCycles // private static ulong _tryFactorIntoCountsInternalHits; // private static ulong _tryCalculateCycleLengthHeuristicHits; - private static bool TryFactorIntoCountsInternal(PrimeOrderCalculatorAccelerator gpu, ulong value, Dictionary counts) + private static bool TryFactorIntoCountsInternal(ulong value, Dictionary counts) { // The EvenPerfectBitScanner only requests factors for values >= 2 here. Leave the guard documented without executing it. // if (value <= 1UL) @@ -409,15 +841,7 @@ public class MersenneDivisorCycles } } - // if (HeuristicPrimeTester.Exclusive.IsPrime(remaining, CancellationToken.None)) - - // Atomic.Add(ref _tryFactorIntoCountsInternalHits, 1UL); - // Console.WriteLine($"MersenneDivisorCycles.TryFactorIntoCountsInternal hits {Volatile.Read(ref _tryFactorIntoCountsInternalHits)}"); - - // HeuristicPrimeTester primeTester = _primeTester ??= new(); if (HeuristicPrimeTester.IsPrimeCpu(remaining)) - // if (HeuristicCombinedPrimeTester.IsPrimeGpu(gpu, remaining)) - // if (PrimeTester.IsPrime(remaining)) { AddFactor(counts, remaining); return true; @@ -441,13 +865,13 @@ public class MersenneDivisorCycles // return false; // } - if (!TryFactorIntoCountsInternal(gpu, factor, counts)) + if (!TryFactorIntoCountsInternal(factor, counts)) { return false; } // Guard the recursive calls to terminate once Pollard-Rho reduces either branch to 1. - if (quotient > 1UL && !TryFactorIntoCountsInternal(gpu, quotient, counts)) + if (quotient > 1UL && !TryFactorIntoCountsInternal(quotient, counts)) { return false; } @@ -765,7 +1189,7 @@ public class MersenneDivisorCycles // unrolled-hex calculator so the CPU generator matches // the GPU benchmark leader for large divisors. MontgomeryDivisorData divisorData = divisorPool.FromModulus(divisor); - ulong cycle = CalculateCycleLength(gpu, divisor, divisorData); + ulong cycle = CalculateCycleLengthGpu(gpu, divisor, divisorData); divisorPool.Return(divisorData); localCycles[localCycleIndex++] = (divisor, cycle); } @@ -933,18 +1357,31 @@ public class MersenneDivisorCycles public static ulong CalculateCycleLengthGpu(ulong divisor) => DivisorCycleKernels.CalculateCycleLengthGpu(divisor); - public static ulong CalculateCycleLength(PrimeOrderCalculatorAccelerator gpu, ulong divisor, in MontgomeryDivisorData divisorData, bool skipPrimeOrderHeuristic = false) + public static ulong CalculateCycleLengthCpu(ulong divisor, in MontgomeryDivisorData divisorData, bool skipPrimeOrderHeuristic = false) { - if (TryCalculateCycleLengthHeuristic(gpu, divisor, divisorData, out ulong cycleLength, skipPrimeOrderHeuristic)) + if (TryCalculateCycleLengthHeuristicCpu(divisor, divisorData, out ulong cycleLength, skipPrimeOrderHeuristic)) return cycleLength; return CalculateCycleLengthFallback(divisor); } - // [ThreadStatic] - // private static HeuristicPrimeTester? _primeTester; + public static ulong CalculateCycleLengthGpu(PrimeOrderCalculatorAccelerator gpu, ulong divisor, in MontgomeryDivisorData divisorData, bool skipPrimeOrderHeuristic = false) + { + if (TryCalculateCycleLengthHeuristicGpu(gpu, divisor, divisorData, out ulong cycleLength, skipPrimeOrderHeuristic)) + return cycleLength; - internal static bool TryCalculateCycleLengthHeuristic(PrimeOrderCalculatorAccelerator gpu, ulong divisor, in MontgomeryDivisorData divisorData, out ulong cycleLength, bool skipPrimeOrderHeuristic = false) + return CalculateCycleLengthFallback(divisor); + } + + public static ulong CalculateCycleLengthHybrid(PrimeOrderCalculatorAccelerator gpu, ulong divisor, in MontgomeryDivisorData divisorData, bool skipPrimeOrderHeuristic = false) + { + if (TryCalculateCycleLengthHeuristicHybrid(gpu, divisor, divisorData, out ulong cycleLength, skipPrimeOrderHeuristic)) + return cycleLength; + + return CalculateCycleLengthFallback(divisor); + } + + internal static bool TryCalculateCycleLengthHeuristicCpu(ulong divisor, in MontgomeryDivisorData divisorData, out ulong cycleLength, bool skipPrimeOrderHeuristic = false) { if ((divisor & (divisor - 1UL)) == 0UL) { @@ -958,24 +1395,79 @@ public class MersenneDivisorCycles return true; } - // if (!skipPrimeOrderHeuristic) - // { - // Atomic.Add(ref _tryCalculateCycleLengthHeuristicHits, 1UL); - // Console.WriteLine($"MersenneDivisorCycles.CalculateCycleLengthFallback hits {Volatile.Read(ref _tryCalculateCycleLengthHeuristicHits)}"); - // } - - // HeuristicPrimeTester primeTester = _primeTester ??= new(); - // if (!skipPrimeOrderHeuristic && HeuristicPrimeTester.Exclusive.IsPrimeCpu(divisor, CancellationToken.None)) if (!skipPrimeOrderHeuristic && HeuristicPrimeTester.IsPrimeCpu(divisor)) - // if (!skipPrimeOrderHeuristic && HeuristicCombinedPrimeTester.IsPrimeGpu(divisor)) { - ulong computedOrder = PrimeOrderCalculator.Calculate( + ulong computedOrder = PrimeOrderCalculator.CalculateCpu( + divisor, + previousOrder: null, + divisorData, + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); + if (computedOrder != 0UL) + { + cycleLength = computedOrder; + return true; + } + } + + cycleLength = 0UL; + return false; + } + + internal static bool TryCalculateCycleLengthHeuristicHybrid(PrimeOrderCalculatorAccelerator gpu, ulong divisor, in MontgomeryDivisorData divisorData, out ulong cycleLength, bool skipPrimeOrderHeuristic = false) + { + if ((divisor & (divisor - 1UL)) == 0UL) + { + cycleLength = 1UL; + return true; + } + + if (divisor <= 3UL) + { + cycleLength = CalculateCycleLengthFallback(divisor); + return true; + } + + if (!skipPrimeOrderHeuristic && HeuristicPrimeTester.IsPrimeCpu(divisor)) + { + ulong computedOrder = PrimeOrderCalculator.CalculateHybrid( gpu, divisor, previousOrder: null, divisorData, - PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault, - PrimeOrderCalculator.PrimeOrderHeuristicDevice.Cpu); + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); + if (computedOrder != 0UL) + { + cycleLength = computedOrder; + return true; + } + } + + cycleLength = 0UL; + return false; + } + + internal static bool TryCalculateCycleLengthHeuristicGpu(PrimeOrderCalculatorAccelerator gpu, ulong divisor, in MontgomeryDivisorData divisorData, out ulong cycleLength, bool skipPrimeOrderHeuristic = false) + { + if ((divisor & (divisor - 1UL)) == 0UL) + { + cycleLength = 1UL; + return true; + } + + if (divisor <= 3UL) + { + cycleLength = CalculateCycleLengthFallback(divisor); + return true; + } + + if (!skipPrimeOrderHeuristic && HeuristicPrimeTester.IsPrimeGpu(gpu, divisor)) + { + ulong computedOrder = PrimeOrderCalculator.CalculateGpu( + gpu, + divisor, + previousOrder: null, + divisorData, + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); if (computedOrder != 0UL) { cycleLength = computedOrder; diff --git a/PerfectNumbers.Core/MersenneNumberTester.cs b/PerfectNumbers.Core/MersenneNumberTester.cs index 89a7258..d76ee01 100644 --- a/PerfectNumbers.Core/MersenneNumberTester.cs +++ b/PerfectNumbers.Core/MersenneNumberTester.cs @@ -25,15 +25,19 @@ public sealed class MersenneNumberTester( bool useGpuScan = true, bool useGpuOrder = true, bool useResidue = true, - ulong maxK = 5_000_000UL) + ulong maxK = 5_000_000UL, + ComputationDevice orderDevice = ComputationDevice.Gpu) { private readonly bool _useResidue = useResidue; private readonly bool _useIncremental = useIncremental && !useResidue; private readonly ulong _maxK = maxK; - private readonly GpuKernelType _kernelType = kernelType; - private readonly bool _useGpuLucas = useGpuLucas; - private readonly bool _useGpuScan = useGpuScan; // device for pow2mod/incremental scanning - private readonly bool _useGpuOrder = useGpuOrder; // device for order computations + private readonly Func _sharesFactorWithExponentMinusOne = orderDevice switch + { + ComputationDevice.Hybrid => static (gpu, exponent) => exponent.SharesFactorWithExponentMinusOneHybrid(gpu), + _ when useGpuScan => static (gpu, exponent) => exponent.SharesFactorWithExponentMinusOneGpu(gpu), + _ => static (_, exponent) => exponent.SharesFactorWithExponentMinusOneCpu(), + }; + private static readonly ConcurrentDictionary _orderCache = []; // TODO: Swap this ConcurrentDictionary for the pooled dictionary variant highlighted in // Pow2MontgomeryModBenchmarks once order warmups reuse deterministic divisor-cycle snapshots; the pooled approach removed @@ -52,7 +56,154 @@ public sealed class MersenneNumberTester( private static readonly Accelerator[] _accelerators = AcceleratorPool.Shared.Accelerators; - public void WarmUpOrders(ulong exponent, ulong limit = 5_000_000UL) + public void WarmUpOrdersHybrid(ulong exponent, ulong limit = 5_000_000UL) + { + bool cacheEnabled = useOrderCache; + if (!cacheEnabled) + { + return; + } + + UInt128 twoP = (UInt128)exponent << 1; // 2 * p + LastDigit lastDigit = (exponent & 3UL) == 3UL ? LastDigit.Seven : LastDigit.One; + var gpu = PrimeOrderCalculatorAccelerator.Rent(1); + if (!useGpuOrder) + { + UInt128 k = UInt128.One; + UInt128 q; + ulong remainder; + for (; k <= limit; k++) + { + q = twoP * k + UInt128.One; + remainder = q.Mod10(); + bool shouldCheck = lastDigit == LastDigit.Seven + ? remainder == 7UL || remainder == 9UL || remainder == 3UL + : remainder == 1UL || remainder == 3UL || remainder == 9UL; + + if (shouldCheck) + { + ulong mod8 = q.Mod8(); + if (mod8 != 1UL && mod8 != 7UL) + { + continue; + } + else if (q.Mod3() == 0UL || q.Mod5() == 0UL) + { + continue; + } + } + + if (!shouldCheck || !cacheEnabled || (cacheEnabled && _orderCache.ContainsKey(q))) + { + continue; + } + + ulong ord = q.CalculateOrderHybrid(gpu); + _orderCache[q] = ord; + } + + PrimeOrderCalculatorAccelerator.Return(gpu); + return; + } + + ArrayPool uInt128Pool = ThreadStaticPools.UInt128Pool; + UInt128[] qsBuffer = uInt128Pool.Rent((int)limit); + int idx = 0; + UInt128 k2 = 1UL; + UInt128 q2; + ulong remainder2; + for (; k2 <= limit; k2++) + { + q2 = twoP * k2 + 1UL; + remainder2 = q2.Mod10(); + bool shouldCheck2 = lastDigit == LastDigit.Seven + ? remainder2 == 7UL || remainder2 == 9UL || remainder2 == 3UL + : remainder2 == 1UL || remainder2 == 3UL || remainder2 == 9UL; + + if (shouldCheck2) + { + ulong mod8 = q2.Mod8(); + if (mod8 != 1UL && mod8 != 7UL) + { + shouldCheck2 = false; + } + else if (q2.Mod3() == 0UL || q2.Mod5() == 0UL) + { + shouldCheck2 = false; + } + } + + if (!shouldCheck2 || (cacheEnabled && _orderCache.ContainsKey(q2))) + { + continue; + } + + qsBuffer[idx++] = q2; + } + + if (idx == 0) + { + PrimeOrderCalculatorAccelerator.Return(gpu); + ThreadStaticPools.UInt128Pool.Return(qsBuffer); + return; + } + + UInt128[] qs = qsBuffer; + var acceleratorIndex = AcceleratorPool.Shared.Rent(); + var accelerator = _accelerators[acceleratorIndex]; + var stream = AcceleratorStreamPool.Rent(acceleratorIndex); + var orderKernel = GpuKernelPool.GetOrAddKernels(acceleratorIndex, stream, KernelType.OrderKernelScan).Order!; + + int batchSize = GpuConstants.ScanBatchSize; + ulong divMul = (ulong)((((UInt128)1 << 64) - 1UL) / exponent) + 1UL; + int offset = 0; + var gpuUInt128Pool = ThreadStaticPools.GpuUInt128Pool; + MemoryBuffer1D? qBuffer; + MemoryBuffer1D? orderBuffer; + + while (offset < idx) + { + int count = Math.Min(batchSize, idx - offset); + qBuffer = accelerator.Allocate1D(count); + orderBuffer = accelerator.Allocate1D(count); + + var tmp = gpuUInt128Pool.Rent(count); + for (int i = 0; i < count; i++) + { + tmp[i] = (GpuUInt128)qs[offset + i]; + } + + qBuffer.View.CopyFromCPU(stream, ref tmp[0], count); + gpuUInt128Pool.Return(tmp); + orderKernel(stream, count, exponent, divMul, qBuffer.View, orderBuffer.View); + stream.Synchronize(); + + ulong[] orders = orderBuffer.GetAsArray1D(); + for (int i = 0; i < count; i++) + { + ulong order = orders[i]; + if (order == 0UL) + { + order = qs[offset + i].CalculateOrderHybrid(gpu); + } + + if (useOrderCache) + { + _orderCache[qs[offset + i]] = order; + } + } + + offset += count; + orderBuffer.Dispose(); + qBuffer.Dispose(); + } + + AcceleratorStreamPool.Return(acceleratorIndex,stream); + PrimeOrderCalculatorAccelerator.Return(gpu); + ThreadStaticPools.UInt128Pool.Return(qs); + } + + public void WarmUpOrdersCpu(ulong exponent, ulong limit = 5_000_000UL) { bool cacheEnabled = useOrderCache; if (!cacheEnabled) @@ -63,8 +214,7 @@ public sealed class MersenneNumberTester( UInt128 twoP = (UInt128)exponent << 1; // 2 * p LastDigit lastDigit = (exponent & 3UL) == 3UL ? LastDigit.Seven : LastDigit.One; // CPU path: compute orders directly and cache - var gpu = PrimeOrderCalculatorAccelerator.Rent(1); - if (!_useGpuOrder) + if (!useGpuOrder) { UInt128 k = UInt128.One; UInt128 q; @@ -98,7 +248,164 @@ public sealed class MersenneNumberTester( continue; } - ulong ord = q.CalculateOrder(gpu); + ulong ord = q.CalculateOrderCpu(); + _orderCache[q] = ord; + } + + return; + } + + ArrayPool uInt128Pool = ThreadStaticPools.UInt128Pool; + UInt128[] qsBuffer = uInt128Pool.Rent((int)limit); + int idx = 0; + UInt128 k2 = 1UL; + UInt128 q2; + ulong remainder2; + for (; k2 <= limit; k2++) + { + q2 = twoP * k2 + 1UL; + remainder2 = q2.Mod10(); + // TODO: Reuse the same residue-automaton fast path here so the GPU warmup staging avoids `%` and + // branches the benchmarks showed slower than the tracked stepping helper. + bool shouldCheck2 = lastDigit == LastDigit.Seven + ? remainder2 == 7UL || remainder2 == 9UL || remainder2 == 3UL + : remainder2 == 1UL || remainder2 == 3UL || remainder2 == 9UL; + + // Early modular filters for odd order and small-prime rejections + if (shouldCheck2) + { + ulong mod8 = q2.Mod8(); + if (mod8 != 1UL && mod8 != 7UL) + { + shouldCheck2 = false; + } + else if (q2.Mod3() == 0UL || q2.Mod5() == 0UL) + { + shouldCheck2 = false; + } + } + + if (!shouldCheck2 || (cacheEnabled && _orderCache.ContainsKey(q2))) + { + continue; + } + + qsBuffer[idx++] = q2; + } + + if (idx == 0) + { + ThreadStaticPools.UInt128Pool.Return(qsBuffer); + return; + } + + UInt128[] qs = qsBuffer; + + var acceleratorIndex = AcceleratorPool.Shared.Rent(); + var accelerator = _accelerators[acceleratorIndex]; + var stream = AcceleratorStreamPool.Rent(acceleratorIndex); + var orderKernel = GpuKernelPool.GetOrAddKernels(acceleratorIndex, stream, KernelType.OrderKernelScan).Order!; + + // Guard long-running kernels by chunking the warm-up across batches. + // Reuse the same ScanBatchSize knob used for scanning. + int batchSize = GpuConstants.ScanBatchSize; + ulong divMul = (ulong)((((UInt128)1 << 64) - 1UL) / exponent) + 1UL; + int offset = 0; + var gpuUInt128Pool = ThreadStaticPools.GpuUInt128Pool; + MemoryBuffer1D? qBuffer; + MemoryBuffer1D? orderBuffer; + + while (offset < idx) + { + int count = Math.Min(batchSize, idx - offset); + qBuffer = accelerator.Allocate1D(count); + orderBuffer = accelerator.Allocate1D(count); + + // Convert to device-friendly GpuUInt128 on the fly + var tmp = gpuUInt128Pool.Rent(count); + for (int i = 0; i < count; i++) + { + tmp[i] = (GpuUInt128)qs[offset + i]; + } + + // Copy only the populated elements to the device buffer to avoid overrun on pooled arrays. + qBuffer.View.CopyFromCPU(stream, ref tmp[0], count); + gpuUInt128Pool.Return(tmp); + orderKernel(stream, count, exponent, divMul, qBuffer.View, orderBuffer.View); + stream.Synchronize(); + + // TODO: Change this to .CopyToCPU and use pre-allocated buffer with fixed, max capacity, using stream. Synchronize on the stream after copy. + ulong[] orders = orderBuffer.GetAsArray1D(); + for (int i = 0; i < count; i++) + { + ulong order = orders[i]; + if (order == 0UL) + { + order = qs[offset + i].CalculateOrderCpu(); + } + + if (useOrderCache) + { + _orderCache[qs[offset + i]] = order; + } + } + + offset += count; + orderBuffer.Dispose(); + qBuffer.Dispose(); + } + + AcceleratorStreamPool.Return(acceleratorIndex,stream); + uInt128Pool.Return(qs); + } + + public void WarmUpOrdersGpu(ulong exponent, ulong limit = 5_000_000UL) + { + bool cacheEnabled = useOrderCache; + if (!cacheEnabled) + { + return; + } + + UInt128 twoP = (UInt128)exponent << 1; // 2 * p + LastDigit lastDigit = (exponent & 3UL) == 3UL ? LastDigit.Seven : LastDigit.One; + // CPU path: compute orders directly and cache + var gpu = PrimeOrderCalculatorAccelerator.Rent(1); + if (!useGpuOrder) + { + UInt128 k = UInt128.One; + UInt128 q; + ulong remainder; + for (; k <= limit; k++) + { + q = twoP * k + UInt128.One; + remainder = q.Mod10(); + // TODO: Replace these repeated Mod10/Mod8/Mod3/Mod5 calls with a residue automaton walk so + // order warmups reuse the optimized cycle stepping validated in the residue benchmarks. + bool shouldCheck = lastDigit == LastDigit.Seven + ? remainder == 7UL || remainder == 9UL || remainder == 3UL + : remainder == 1UL || remainder == 3UL || remainder == 9UL; + + // Early modular filters for odd order and small-prime rejections + if (shouldCheck) + { + ulong mod8 = q.Mod8(); + if (mod8 != 1UL && mod8 != 7UL) + { + continue; + } + else if (q.Mod3() == 0UL || q.Mod5() == 0UL) + { + continue; + } + } + + if (!shouldCheck || !cacheEnabled || (cacheEnabled && _orderCache.ContainsKey(q))) + { + continue; + } + + ulong ord = q.CalculateOrderGpu(gpu); _orderCache[q] = ord; } @@ -194,7 +501,7 @@ public sealed class MersenneNumberTester( ulong order = orders[i]; if (order == 0UL) { - order = qs[offset + i].CalculateOrder(gpu); + order = qs[offset + i].CalculateOrderGpu(gpu); } if (useOrderCache) @@ -225,7 +532,7 @@ public sealed class MersenneNumberTester( return false; } - if ((exponent & 3UL) == 1UL && exponent.SharesFactorWithExponentMinusOne(gpu)) + if ((exponent & 3UL) == 1UL && _sharesFactorWithExponentMinusOne(gpu, exponent)) { return false; } @@ -250,13 +557,13 @@ public sealed class MersenneNumberTester( maxK = _maxK; } - if (_useGpuScan) + if (useGpuScan) { - _residueGpuTester!.Scan(exponent, twoP, lastDigit, maxK, ref prePrime); + _residueGpuTester!.Scan(gpu, exponent, twoP, lastDigit, maxK, ref prePrime); } else { - _residueCpuTester!.Scan(gpu, exponent, twoP, lastDigit, maxK, ref prePrime); + _residueCpuTester!.Scan(exponent, twoP, lastDigit, maxK, ref prePrime); } return prePrime; @@ -269,7 +576,7 @@ public sealed class MersenneNumberTester( UInt128 twoPPre = (UInt128)exponent << 1; UInt128 maxKPre = (UInt128)PreLucasPreScanK; LastDigit lastDigitPre = (exponent & 3UL) == 3UL ? LastDigit.Seven : LastDigit.One; - if (_useGpuScan) + if (useGpuScan) { _incrementalGpuTester.Scan(exponent, twoPPre, lastDigitPre, maxKPre, ref prePrime); } @@ -283,7 +590,7 @@ public sealed class MersenneNumberTester( return false; } - return _useGpuLucas + return useGpuLucas ? _lucasLehmerGpuTester.IsPrime(gpu, exponent, runOnGpu: true) : _lucasLehmerCpuTester.IsPrime(gpu, exponent); } @@ -292,7 +599,7 @@ public sealed class MersenneNumberTester( if (useOrder) { - if (_useGpuOrder) + if (useGpuOrder) { _orderGpuTester.Scan(exponent, twoP, lastDigit, maxK, ref isPrime); } @@ -303,7 +610,7 @@ public sealed class MersenneNumberTester( } else { - if (_useGpuScan) + if (useGpuScan) { _incrementalGpuTester.Scan(exponent, twoP, lastDigit, maxK, ref isPrime); } @@ -332,6 +639,4 @@ public sealed class MersenneNumberTester( UInt128 pow = exponent.PowMod(q); orders[index] = pow == 1UL ? exponent : 0UL; } - - } diff --git a/PerfectNumbers.Core/ModResidueTracker.cs b/PerfectNumbers.Core/ModResidueTracker.cs index 33bd5ba..c2876d8 100644 --- a/PerfectNumbers.Core/ModResidueTracker.cs +++ b/PerfectNumbers.Core/ModResidueTracker.cs @@ -39,12 +39,9 @@ public sealed class ModResidueTracker /// (number must be non-decreasing across calls). Adds and initializes new /// divisors on first use. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool IsDivisible(PrimeOrderCalculatorAccelerator gpu, UInt128 number, UInt128 divisor) + public bool IsDivisibleCpu(UInt128 number, UInt128 divisor) { - if (divisor == UInt128.Zero) - { - throw new ArgumentOutOfRangeException(nameof(divisor)); - } + ArgumentOutOfRangeException.ThrowIfEqual(divisor, UInt128.Zero); EnsureAt(number); @@ -52,7 +49,75 @@ public sealed class ModResidueTracker int idx = LowerBound(divisors, divisor); if (idx == divisors.Count || divisors[idx] != divisor) { - UInt128 residue = ComputeInitialResidue(gpu, number, divisor); + UInt128 residue = ComputeInitialResidueCpu(number, divisor); + divisors.Insert(idx, divisor); + _residues.Insert(idx, residue); + if (_hasLastAppended && idx == divisors.Count - 1 && divisor >= _lastAppended) + { + _lastAppended = divisor; + } + else if (!_hasLastAppended) + { + _hasLastAppended = true; + _lastAppended = divisor; + } + + return residue == UInt128.Zero; + } + + return _residues[idx] == UInt128.Zero; + } + + /// Returns true if the provided number (either p or M_p depending on model) + /// is divisible by the divisor. Advances internal state forward if needed + /// (number must be non-decreasing across calls). Adds and initializes new + /// divisors on first use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsDivisibleGpu(PrimeOrderCalculatorAccelerator gpu, UInt128 number, UInt128 divisor) + { + ArgumentOutOfRangeException.ThrowIfEqual(divisor, UInt128.Zero); + + EnsureAt(number); + + List divisors = _divisors; + int idx = LowerBound(divisors, divisor); + if (idx == divisors.Count || divisors[idx] != divisor) + { + UInt128 residue = ComputeInitialResidueGpu(gpu, number, divisor); + divisors.Insert(idx, divisor); + _residues.Insert(idx, residue); + if (_hasLastAppended && idx == divisors.Count - 1 && divisor >= _lastAppended) + { + _lastAppended = divisor; + } + else if (!_hasLastAppended) + { + _hasLastAppended = true; + _lastAppended = divisor; + } + + return residue == UInt128.Zero; + } + + return _residues[idx] == UInt128.Zero; + } + + /// Returns true if the provided number (either p or M_p depending on model) + /// is divisible by the divisor. Advances internal state forward if needed + /// (number must be non-decreasing across calls). Adds and initializes new + /// divisors on first use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsDivisibleHybrid(PrimeOrderCalculatorAccelerator gpu, UInt128 number, UInt128 divisor) + { + ArgumentOutOfRangeException.ThrowIfEqual(divisor, UInt128.Zero); + + EnsureAt(number); + + List divisors = _divisors; + int idx = LowerBound(divisors, divisor); + if (idx == divisors.Count || divisors[idx] != divisor) + { + UInt128 residue = ComputeInitialResidueHybrid(gpu, number, divisor); divisors.Insert(idx, divisor); _residues.Insert(idx, residue); if (_hasLastAppended && idx == divisors.Count - 1 && divisor >= _lastAppended) @@ -81,7 +146,7 @@ public sealed class ModResidueTracker // Merge or append current candidate divisor (ascending order required across calls). // Returns true and sets 'divisible' based on residue at 'number'. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool MergeOrAppend(PrimeOrderCalculatorAccelerator gpu, UInt128 number, UInt128 divisor, out bool divisible) + public bool MergeOrAppendCpu(UInt128 number, UInt128 divisor, out bool divisible) { // Assume number is non-decreasing; advance residues lazily when number changes. List divisors = _divisors; @@ -97,7 +162,7 @@ public sealed class ModResidueTracker List residues = _residues; if (divisors.Count == 0 || (_hasLastAppended && divisor >= _lastAppended && divisors[^1] <= divisor)) { - residue = ComputeInitialResidue(gpu, number, divisor); + residue = ComputeInitialResidueCpu(number, divisor); divisors.Add(divisor); residues.Add(residue); _hasLastAppended = true; @@ -123,7 +188,129 @@ public sealed class ModResidueTracker // Insert at cursor to keep order - residue = ComputeInitialResidue(gpu, number, divisor); + residue = ComputeInitialResidueCpu(number, divisor); + divisors.Insert(cursor, divisor); + residues.Insert(cursor, residue); + divisible = residue == UInt128.Zero; + _cursor = cursor + 1; + + // update last appended hint + if (!_hasLastAppended || divisor >= _lastAppended) + { + _hasLastAppended = true; + _lastAppended = divisor; + } + + return true; + } + + // Merge or append current candidate divisor (ascending order required across calls). + // Returns true and sets 'divisible' based on residue at 'number'. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MergeOrAppendGpu(PrimeOrderCalculatorAccelerator gpu, UInt128 number, UInt128 divisor, out bool divisible) + { + // Assume number is non-decreasing; advance residues lazily when number changes. + List divisors = _divisors; + if (number != _currentNumber) + { + EnsureAt(number); + // After EnsureAt we can keep _cursor (still valid monotonic pointer) + if (_cursor > divisors.Count) _cursor = divisors.Count; + } + + UInt128 residue; + // Fast path: append to tail when candidates are strictly increasing. + List residues = _residues; + if (divisors.Count == 0 || (_hasLastAppended && divisor >= _lastAppended && divisors[^1] <= divisor)) + { + residue = ComputeInitialResidueGpu(gpu, number, divisor); + divisors.Add(divisor); + residues.Add(residue); + _hasLastAppended = true; + _lastAppended = divisor; + _cursor = divisors.Count; // position after last + divisible = residue == UInt128.Zero; + return true; + } + + // Merge pointer: advance until current >= divisor + var cursor = _cursor; + while (cursor < divisors.Count && divisors[cursor] < divisor) + { + cursor++; + } + + if (cursor < divisors.Count && divisors[cursor] == divisor) + { + divisible = residues[cursor] == UInt128.Zero; + _cursor = cursor + 1; // position after matched + return true; + } + + + // Insert at cursor to keep order + residue = ComputeInitialResidueGpu(gpu, number, divisor); + divisors.Insert(cursor, divisor); + residues.Insert(cursor, residue); + divisible = residue == UInt128.Zero; + _cursor = cursor + 1; + + // update last appended hint + if (!_hasLastAppended || divisor >= _lastAppended) + { + _hasLastAppended = true; + _lastAppended = divisor; + } + + return true; + } + + // Merge or append current candidate divisor (ascending order required across calls). + // Returns true and sets 'divisible' based on residue at 'number'. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MergeOrAppendHybrid(PrimeOrderCalculatorAccelerator gpu, UInt128 number, UInt128 divisor, out bool divisible) + { + // Assume number is non-decreasing; advance residues lazily when number changes. + List divisors = _divisors; + if (number != _currentNumber) + { + EnsureAt(number); + // After EnsureAt we can keep _cursor (still valid monotonic pointer) + if (_cursor > divisors.Count) _cursor = divisors.Count; + } + + UInt128 residue; + // Fast path: append to tail when candidates are strictly increasing. + List residues = _residues; + if (divisors.Count == 0 || (_hasLastAppended && divisor >= _lastAppended && divisors[^1] <= divisor)) + { + residue = ComputeInitialResidueHybrid(gpu, number, divisor); + divisors.Add(divisor); + residues.Add(residue); + _hasLastAppended = true; + _lastAppended = divisor; + _cursor = divisors.Count; // position after last + divisible = residue == UInt128.Zero; + return true; + } + + // Merge pointer: advance until current >= divisor + var cursor = _cursor; + while (cursor < divisors.Count && divisors[cursor] < divisor) + { + cursor++; + } + + if (cursor < divisors.Count && divisors[cursor] == divisor) + { + divisible = residues[cursor] == UInt128.Zero; + _cursor = cursor + 1; // position after matched + return true; + } + + + // Insert at cursor to keep order + residue = ComputeInitialResidueHybrid(gpu, number, divisor); divisors.Insert(cursor, divisor); residues.Insert(cursor, residue); divisible = residue == UInt128.Zero; @@ -238,7 +425,7 @@ public sealed class ModResidueTracker } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private UInt128 ComputeInitialResidue(PrimeOrderCalculatorAccelerator gpu, UInt128 number, UInt128 divisor) + private UInt128 ComputeInitialResidueCpu(UInt128 number, UInt128 divisor) { if (_model == ResidueModel.Identity) { @@ -266,12 +453,82 @@ public sealed class ModResidueTracker if (pow != UInt128.One) return (pow - UInt128.One) % divisor; - ulong ord = divisor.CalculateOrder(gpu); + ulong ord = divisor.CalculateOrderCpu(); if (ord == 0UL || number % (UInt128)ord != UInt128.Zero) return UInt128.One; // does NOT divide return UInt128.Zero; // q really divides M_p } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private UInt128 ComputeInitialResidueGpu(PrimeOrderCalculatorAccelerator gpu, UInt128 number, UInt128 divisor) + { + if (_model == ResidueModel.Identity) + { + // Fast path for 64-bit inputs + if (number <= ulong.MaxValue && divisor <= ulong.MaxValue) + { + // TODO: Swap this modulo fallback for the shared divisor-cycle remainder helper so 64-bit + // residues reuse cached cycles instead of recomputing `%` on every lookup. + return (ulong)number % (ulong)divisor; + } + + // TODO: Use the Montgomery folding helper measured faster in the Pow2Montgomery benchmarks so this + // wide residue path avoids general `%` on UInt128 inputs. + return number % divisor; + } + + // residue of Mersenne number M_number = 2^number - 1 modulo divisor + // TODO: Consult MersenneDivisorCycles.Shared (or similar caches) here so we reuse precomputed cycle lengths + // instead of recomputing powmods for every divisor; the divisor-cycle benchmarks showed large wins once the + // cached orders were used across scans, and a miss should trigger the configured device to compute the cycle + // immediately without storing it back or requesting additional cache blocks. + UInt128 pow = number.Pow2MontgomeryModWindowed(divisor); + + // q divides M_p ⇔ 2^p ≡ 1 (mod q) AND ord_q(2) | p + if (pow != UInt128.One) + return (pow - UInt128.One) % divisor; + + ulong ord = divisor.CalculateOrderGpu(gpu); + if (ord == 0UL || number % (UInt128)ord != UInt128.Zero) + return UInt128.One; // does NOT divide + + return UInt128.Zero; // q really divides M_p + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private UInt128 ComputeInitialResidueHybrid(PrimeOrderCalculatorAccelerator gpu, UInt128 number, UInt128 divisor) + { + if (_model == ResidueModel.Identity) + { + // Fast path for 64-bit inputs + if (number <= ulong.MaxValue && divisor <= ulong.MaxValue) + { + // TODO: Swap this modulo fallback for the shared divisor-cycle remainder helper so 64-bit + // residues reuse cached cycles instead of recomputing `%` on every lookup. + return (ulong)number % (ulong)divisor; + } + + // TODO: Use the Montgomery folding helper measured faster in the Pow2Montgomery benchmarks so this + // wide residue path avoids general `%` on UInt128 inputs. + return number % divisor; + } + + // residue of Mersenne number M_number = 2^number - 1 modulo divisor + // TODO: Consult MersenneDivisorCycles.Shared (or similar caches) here so we reuse precomputed cycle lengths + // instead of recomputing powmods for every divisor; the divisor-cycle benchmarks showed large wins once the + // cached orders were used across scans, and a miss should trigger the configured device to compute the cycle + // immediately without storing it back or requesting additional cache blocks. + UInt128 pow = number.Pow2MontgomeryModWindowed(divisor); + + // q divides M_p ⇔ 2^p ≡ 1 (mod q) AND ord_q(2) | p + if (pow != UInt128.One) + return (pow - UInt128.One) % divisor; + + ulong ord = divisor.CalculateOrderHybrid(gpu); + if (ord == 0UL || number % (UInt128)ord != UInt128.Zero) + return UInt128.One; // does NOT divide + + return UInt128.Zero; // q really divides M_p + } } diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs index 2f91c55..4e8fe86 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs @@ -12,7 +12,6 @@ namespace PerfectNumbers.Core; internal static partial class PrimeOrderCalculator { - internal readonly struct PendingEntry { public readonly ulong Value; @@ -44,7 +43,11 @@ internal static partial class PrimeOrderCalculator } } - private static ulong CalculateInternal(PrimeOrderCalculatorAccelerator gpu, ulong prime, ulong? previousOrder, in MontgomeryDivisorData divisorData, in PrimeOrderSearchConfig config) + public static ulong CalculateCpu( + ulong prime, + ulong? previousOrder, + in MontgomeryDivisorData divisorData, + in PrimeOrderSearchConfig config) { // TODO: Is this condition ever met on EvenPerfectBitScanner's execution path? If not, we can add a clarification comment and comment out the entire block. We want to support p candidates at least greater or equal to 31. if (prime <= 3UL) @@ -54,36 +57,124 @@ internal static partial class PrimeOrderCalculator ulong phi = prime - 1UL; - // if (PrimeOrderGpuHeuristics.TryCalculateOrder(gpu, prime, previousOrder, config, divisorData, out ulong gpuOrder)) - // if (IsGpuHeuristicDevice && PrimeOrderGpuHeuristics.TryCalculateOrder(gpu, prime, previousOrder, config, divisorData, out ulong gpuOrder)) - // { - // return gpuOrder; - // } - - PartialFactorResult phiFactors = PartialFactor(gpu, phi, config); + PartialFactorResult phiFactors = PartialFactorCpu(phi, config); ulong result; if (phiFactors.Factors is null) { - // result = prime <= PerfectNumberConstants.MaxQForDivisorCycles - // ? CalculateByFactorizationGpu(gpu, prime, divisorData) - // : CalculateByFactorizationCpu(gpu, prime, divisorData); - // result = RunOnCpu() - // ? CalculateByFactorizationCpu(gpu, prime, divisorData) - // : CalculateByFactorizationGpu(gpu, prime, divisorData); - // result = CalculateByFactorizationGpu(gpu, prime, divisorData); - result = CalculateByFactorizationCpu(gpu, prime, divisorData); - + result = CalculateByFactorizationCpu(prime, divisorData); + phiFactors.Dispose(); return result; } - result = RunHeuristicPipelineCpu(gpu, prime, previousOrder, config, divisorData, phi, phiFactors); + result = RunHeuristicPipelineCpu(prime, previousOrder, config, divisorData, phi, phiFactors); phiFactors.Dispose(); return result; } + public static UInt128 CalculateCpu( + in UInt128 prime, + in UInt128? previousOrder, + in PrimeOrderSearchConfig config) + { + MontgomeryDivisorData divisorData; + UInt128 result; + if (prime <= ulong.MaxValue) + { + ulong? previous = null; + if (previousOrder.HasValue) + { + UInt128 previousValue = previousOrder.Value; + if (previousValue <= ulong.MaxValue) + { + previous = (ulong)previousValue; + } + else + { + previous = ulong.MaxValue; + } + } + + ulong prime64 = (ulong)prime; + Queue divisorPool = MontgomeryDivisorDataPool.Shared; + divisorData = divisorPool.FromModulus(prime64); + ulong order64 = CalculateCpu(prime64, previous, divisorData, config); + divisorPool.Return(divisorData); + result = order64 == 0UL ? UInt128.Zero : (UInt128)order64; + } + else + { + divisorData = MontgomeryDivisorData.Empty; + result = CalculateWideInternalCpu(prime, previousOrder, divisorData, config); + } + + return result; + } + + private static UInt128 CalculateWideInternalCpu(in UInt128 prime, in UInt128? previousOrder, in MontgomeryDivisorData? divisorData, in PrimeOrderSearchConfig config) + { + if (prime <= UInt128.One) + { + return UInt128.One; + } + + if (prime == (UInt128)3UL) + { + return (UInt128)2UL; + } + + UInt128 phi = prime - UInt128.One; + PartialFactorResult128 phiFactors = PartialFactorWide(phi, config); + UInt128 result; + if (phiFactors.Factors is null) + { + result = FinishStrictlyWideCpu(prime, divisorData); + } + else + { + result = RunHeuristicPipelineWideCpu(prime, previousOrder, divisorData, config, phi, phiFactors); + } + + return result; + } + private static ulong RunHeuristicPipelineCpu( + ulong prime, + ulong? previousOrder, + in PrimeOrderSearchConfig config, + in MontgomeryDivisorData divisorData, + ulong phi, + PartialFactorResult phiFactors) + { + if (phiFactors.FullyFactored && TrySpecialMaxCpu(phi, prime, phiFactors, divisorData)) + { + return phi; + } + + ulong candidateOrder = InitializeStartingOrderCpu(prime, phi, divisorData); + candidateOrder = ExponentLoweringCpu(candidateOrder, prime, phiFactors, divisorData); + + if (TryConfirmOrderCpu(prime, candidateOrder, divisorData, config, out PartialFactorResult? orderFactors)) + { + return candidateOrder; + } + + if (config.Mode == PrimeOrderMode.Strict) + { + orderFactors?.Dispose(); + return CalculateByFactorizationCpu(prime, divisorData); + } + + if (TryHeuristicFinishCpu(prime, candidateOrder, previousOrder, divisorData, config, phiFactors, orderFactors, out ulong order)) + { + return order; + } + + return candidateOrder; + } + + private static ulong RunHeuristicPipelineHybrid( PrimeOrderCalculatorAccelerator gpu, ulong prime, ulong? previousOrder, @@ -92,15 +183,15 @@ internal static partial class PrimeOrderCalculator ulong phi, PartialFactorResult phiFactors) { - if (phiFactors.FullyFactored && TrySpecialMaxCpu(gpu, phi, prime, phiFactors, divisorData)) + if (phiFactors.FullyFactored && TrySpecialMaxHybrid(gpu, phi, prime, phiFactors, divisorData)) { return phi; } - ulong candidateOrder = InitializeStartingOrderCpu(gpu, prime, phi, divisorData); + ulong candidateOrder = InitializeStartingOrderHybrid(gpu, prime, phi, divisorData); candidateOrder = ExponentLoweringCpu(candidateOrder, prime, phiFactors, divisorData); - if (TryConfirmOrderCpu(gpu, prime, candidateOrder, divisorData, config, out PartialFactorResult? orderFactors)) + if (TryConfirmOrderHybrid(gpu, prime, candidateOrder, divisorData, config, out PartialFactorResult? orderFactors)) { return candidateOrder; } @@ -108,13 +199,10 @@ internal static partial class PrimeOrderCalculator if (config.Mode == PrimeOrderMode.Strict) { orderFactors?.Dispose(); - return CalculateByFactorizationCpu(gpu, prime, divisorData); - // return RunOnCpu() - // ? CalculateByFactorizationCpu(gpu, prime, divisorData) - // : CalculateByFactorizationGpu(gpu, prime, divisorData); + return CalculateByFactorizationCpu(prime, divisorData); } - if (TryHeuristicFinishCpu(gpu, prime, candidateOrder, previousOrder, divisorData, config, phiFactors, orderFactors, out ulong order)) + if (TryHeuristicFinishHybrid(gpu, prime, candidateOrder, previousOrder, divisorData, config, phiFactors, orderFactors, out ulong order)) { return order; } @@ -122,8 +210,295 @@ internal static partial class PrimeOrderCalculator return candidateOrder; } + private static UInt128 RunHeuristicPipelineWideCpu( + in UInt128 prime, + in UInt128? previousOrder, + in MontgomeryDivisorData? divisorData, + in PrimeOrderSearchConfig config, + in UInt128 phi, + in PartialFactorResult128 phiFactors) + { + if (phiFactors.FullyFactored && TrySpecialMaxWideCpu(phi, prime, divisorData, phiFactors)) + { + return phi; + } + + UInt128 candidateOrder = InitializeStartingOrderWideCpu(prime, phi, divisorData); + candidateOrder = ExponentLoweringWideCpu(candidateOrder, prime, divisorData, phiFactors); + + if (TryConfirmOrderWideCpu(prime, candidateOrder, divisorData, config)) + { + return candidateOrder; + } + + if (config.Mode == PrimeOrderMode.Strict) + { + return FinishStrictlyWideCpu(prime, divisorData); + } + + if (TryHeuristicFinishWideCpu(prime, candidateOrder, previousOrder, divisorData, config, out UInt128 order)) + { + return order; + } + + return candidateOrder; + } + + private static UInt128 FinishStrictlyWideCpu(in UInt128 prime, in MontgomeryDivisorData? divisorData) + { + UInt128 phi = prime - UInt128.One; + Dictionary counts = new(capacity: 8); + FactorCompletelyWide(phi, counts); + if (counts.Count == 0) + { + return phi; + } + + List> entries = new(counts); + entries.Sort(static (a, b) => a.Key.CompareTo(b.Key)); + + UInt128 order = phi; + int entryCount = entries.Count; + for (int i = 0; i < entryCount; i++) + { + UInt128 primeFactor = entries[i].Key; + int exponent = entries[i].Value; + for (int iteration = 0; iteration < exponent; iteration++) + { + if ((order % primeFactor) != UInt128.Zero) + { + UInt128 candidate = order / primeFactor; + if (Pow2ModWideCpu(candidate, prime, divisorData) == UInt128.One) + { + order = candidate; + } + else + { + break; + } + } + } + } + + return order; + } + + private static bool TrySpecialMaxWideCpu(in UInt128 phi, in UInt128 prime, in MontgomeryDivisorData? divisorData, in PartialFactorResult128 factors) + { + ReadOnlySpan factorSpan = factors.Factors; + int length = factors.Count; + for (int i = 0; i < length; i++) + { + UInt128 factor = factorSpan[i].Value; + UInt128 reduced = phi / factor; + if (Pow2ModWideCpu(reduced, prime, divisorData) == UInt128.One) + { + return false; + } + } + + return true; + } + + private static UInt128 InitializeStartingOrderWideCpu(in UInt128 prime, in UInt128 phi, in MontgomeryDivisorData? divisorData) + { + UInt128 order = phi; + UInt128 mod8 = prime & (UInt128)7UL; + if (mod8 == UInt128.One || mod8 == (UInt128)7UL) + { + UInt128 half = phi >> 1; + if (Pow2ModWideCpu(half, prime, divisorData) == UInt128.One) + { + order = half; + } + } + + return order; + } + + private static UInt128 ExponentLoweringWideCpu(UInt128 order, in UInt128 prime, in MontgomeryDivisorData? divisorData, in PartialFactorResult128 factors) + { + ArrayPool pool = ThreadStaticPools.FactorEntry128Pool; + ReadOnlySpan factorSpan = factors.Factors; + int length = factors.Count; + // TODO: This should never trigger from production code - check + // if (length == 0) + // { + // return order; + // } + FactorEntry128[] tempArray = pool.Rent(length + 1); + Span buffer = tempArray.AsSpan(0, length); + + factorSpan.CopyTo(buffer); + if (!factors.FullyFactored && factors.Cofactor > UInt128.One && IsPrimeWide(factors.Cofactor)) + { + buffer[length] = new FactorEntry128(factors.Cofactor, 1); + length++; + } + + buffer[..length].Sort(static (a, b) => a.Value.CompareTo(b.Value)); + + // TODO: Investigating using stepper to replace modulo operation + for (int i = 0; i < length; i++) + { + UInt128 primeFactor = buffer[i].Value; + int exponent = buffer[i].Exponent; + for (int iteration = 0; iteration < exponent; iteration++) + { + if ((order % primeFactor) == UInt128.Zero) + { + UInt128 reduced = order / primeFactor; + if (Pow2ModWideCpu(reduced, prime, divisorData) == UInt128.One) + { + order = reduced; + continue; + } + } + + break; + } + } + + pool.Return(tempArray, clearArray: false); + return order; + } + + private static bool TryConfirmOrderWideCpu(in UInt128 prime, in UInt128 order, in MontgomeryDivisorData? divisorData, in PrimeOrderSearchConfig config) + { + if (order == UInt128.Zero) + { + return false; + } + + if (Pow2ModWideCpu(order, prime, divisorData) != UInt128.One) + { + return false; + } + + PartialFactorResult128 factorization = PartialFactorWide(order, config); + if (factorization.Factors is null) + { + return false; + } + + if (!factorization.FullyFactored) + { + if (factorization.Cofactor <= UInt128.One) + { + return false; + } + + if (!IsPrimeWide(factorization.Cofactor)) + { + return false; + } + + factorization = factorization.WithAdditionalPrime(factorization.Cofactor); + } + + ReadOnlySpan span = factorization.Factors; + int length = factorization.Count; + for (int i = 0; i < length; i++) + { + UInt128 primeFactor = span[i].Value; + UInt128 reduced = order; + for (int iteration = 0; iteration < span[i].Exponent; iteration++) + { + if ((reduced % primeFactor) != UInt128.Zero) + { + break; + } + + reduced /= primeFactor; + if (Pow2ModWideCpu(reduced, prime, divisorData) == UInt128.One) + { + return false; + } + } + } + + return true; + } + + private static bool TryHeuristicFinishWideCpu( + in UInt128 prime, + in UInt128 order, + in UInt128? previousOrder, + in MontgomeryDivisorData? divisorData, + in PrimeOrderSearchConfig config, + out UInt128 result) + { + result = UInt128.Zero; + if (order <= UInt128.One) + { + return false; + } + + PartialFactorResult128 orderFactors = PartialFactorWide(order, config); + if (orderFactors.Factors is null) + { + return false; + } + + if (!orderFactors.FullyFactored) + { + if (orderFactors.Cofactor <= UInt128.One) + { + return false; + } + + if (!IsPrimeWide(orderFactors.Cofactor)) + { + return false; + } + + orderFactors = orderFactors.WithAdditionalPrime(orderFactors.Cofactor); + } + + int capacity = config.MaxPowChecks <= 0 ? 64 : config.MaxPowChecks * 4; + List candidates = new(capacity); + FactorEntry128[] factorArray = orderFactors.Factors!; + BuildCandidatesWide(order, factorArray, orderFactors.Count, candidates, capacity); + if (candidates.Count == 0) + { + return false; + } + + SortCandidatesWide(prime, previousOrder, candidates); + + int powBudget = config.MaxPowChecks <= 0 ? candidates.Count : config.MaxPowChecks; + int powUsed = 0; + int candidateCount = candidates.Count; + + for (int i = 0; i < candidateCount; i++) + { + if (powUsed >= powBudget && powBudget > 0) + { + break; + } + + UInt128 candidate = candidates[i]; + powUsed++; + + if (Pow2ModWideCpu(candidate, prime, divisorData) != UInt128.One) + { + continue; + } + + if (!TryConfirmCandidateWideCpu(prime, candidate, config, ref powUsed, powBudget, divisorData)) + { + continue; + } + + result = candidate; + return true; + } + + return false; + } + // private static ulong specialMaxHits; - private static bool TrySpecialMaxCpu(PrimeOrderCalculatorAccelerator gpu, ulong phi, ulong prime, PartialFactorResult factors, in MontgomeryDivisorData divisorData) + private static bool TrySpecialMaxCpu(ulong phi, ulong prime, PartialFactorResult factors, in MontgomeryDivisorData divisorData) { int length = factors.Count; // The phi factorization on the scanner path always yields at least one entry for phi >= 2, @@ -134,7 +509,22 @@ internal static partial class PrimeOrderCalculator // } ReadOnlySpan factorSpan = new(factors.Factors, 0, length); + Span stackBuffer = stackalloc ulong[length]; + return EvaluateSpecialMaxCandidatesCpu(stackBuffer, factorSpan, phi, prime, divisorData); + } + + private static bool TrySpecialMaxHybrid(PrimeOrderCalculatorAccelerator gpu, ulong phi, ulong prime, PartialFactorResult factors, in MontgomeryDivisorData divisorData) + { + int length = factors.Count; + // The phi factorization on the scanner path always yields at least one entry for phi >= 2, + // preventing the zero-length case from occurring outside synthetic tests. + // if (length == 0) + // { + // return true; + // } + + ReadOnlySpan factorSpan = new(factors.Factors, 0, length); Span stackBuffer = stackalloc ulong[length]; if (length <= 8) @@ -169,7 +559,7 @@ internal static partial class PrimeOrderCalculator buffer.Sort(); ExponentRemainderStepperCpu stepper = ThreadStaticPools.RentExponentStepperCpu(divisorData); - + if (stepper.InitializeCpuIsUnity(buffer[0])) { ThreadStaticPools.ReturnExponentStepperCpu(stepper); @@ -189,13 +579,28 @@ internal static partial class PrimeOrderCalculator return true; } - private static ulong InitializeStartingOrderCpu(PrimeOrderCalculatorAccelerator gpu, ulong prime, ulong phi, in MontgomeryDivisorData divisorData) + private static ulong InitializeStartingOrderCpu(ulong prime, ulong phi, in MontgomeryDivisorData divisorData) { ulong order = phi; if ((prime & 7UL) == 1UL || (prime & 7UL) == 7UL) { ulong half = phi >> 1; - if (Pow2EqualsOneCpu(gpu, half, prime, divisorData)) + if (Pow2EqualsOneCpu(half, prime, divisorData)) + { + order = half; + } + } + + return order; + } + + private static ulong InitializeStartingOrderHybrid(PrimeOrderCalculatorAccelerator gpu, ulong prime, ulong phi, in MontgomeryDivisorData divisorData) + { + ulong order = phi; + if ((prime & 7UL) == 1UL || (prime & 7UL) == 7UL) + { + ulong half = phi >> 1; + if (Pow2EqualsOneHybrid(gpu, half, prime, divisorData)) { order = half; } @@ -330,6 +735,53 @@ internal static partial class PrimeOrderCalculator } private static bool TryConfirmOrderCpu( + ulong prime, + ulong order, + in MontgomeryDivisorData divisorData, + in PrimeOrderSearchConfig config, + out PartialFactorResult? reusableFactorization) + { + reusableFactorization = null; + + if (order == 0UL) + { + return false; + } + + if (!Pow2EqualsOneCpu(order, prime, divisorData)) + { + return false; + } + + PartialFactorResult factorization = PartialFactorCpu(order, config); + if (factorization.Factors is null) + { + factorization.Dispose(); + return false; + } + + if (!factorization.FullyFactored) + { + if (factorization.Cofactor <= 1UL || !factorization.CofactorIsPrime) + { + factorization.Dispose(); + return false; + } + + factorization.WithAdditionalPrime(factorization.Cofactor); + } + + if (!ValidateOrderAgainstFactors(prime, order, divisorData, factorization)) + { + reusableFactorization = factorization; + return false; + } + + factorization.Dispose(); + return true; + } + + private static bool TryConfirmOrderHybrid( PrimeOrderCalculatorAccelerator gpu, ulong prime, ulong order, @@ -344,12 +796,12 @@ internal static partial class PrimeOrderCalculator return false; } - if (!Pow2EqualsOneCpu(gpu, order, prime, divisorData)) + if (!Pow2EqualsOneHybrid(gpu, order, prime, divisorData)) { return false; } - PartialFactorResult factorization = PartialFactor(gpu, order, config); + PartialFactorResult factorization = PartialFactorHybrid(gpu, order, config); if (factorization.Factors is null) { factorization.Dispose(); @@ -491,7 +943,6 @@ internal static partial class PrimeOrderCalculator } private static bool TryHeuristicFinishCpu( - PrimeOrderCalculatorAccelerator gpu, ulong prime, ulong order, ulong? previousOrder, @@ -509,7 +960,7 @@ internal static partial class PrimeOrderCalculator } // Reuse the partial factorization from TryConfirmOrderCpu when available. - PartialFactorResult orderFactors = cachedOrderFactors ?? PartialFactor(gpu, order, config); + PartialFactorResult orderFactors = cachedOrderFactors ?? PartialFactorCpu(order, config); try { if (orderFactors.Factors is null) @@ -537,7 +988,269 @@ internal static partial class PrimeOrderCalculator candidates.Clear(); ulong[] factorArray = orderFactors.Factors!; int[] exponentsArray = orderFactors.Exponents!; - // DebugLog("Building candidates list"); + + BuildCandidates(order, factorArray, exponentsArray, orderFactors.Count, candidates, capacity); + if (candidates.Count == 0) + { + ThreadStaticPools.ReturnUlongList(candidates); + return false; + } + + SortCandidates(prime, previousOrder, candidates); + + int powBudget = config.MaxPowChecks <= 0 ? candidates.Count : config.MaxPowChecks; + int powUsed = 0; + int candidateCount = candidates.Count; + Span candidateSpan = CollectionsMarshal.AsSpan(candidates); + ExponentRemainderStepperCpu powStepper = ThreadStaticPools.RentExponentStepperCpu(divisorData); + bool powStepperInitialized = false; + + int index = 0; + while (index < candidateCount && powUsed < powBudget) + { + int remaining = candidateCount - index; + int budgetRemaining = powBudget - powUsed; + int batchSize = Math.Min(remaining, Math.Min(budgetRemaining, PrimeOrderConstants.MaxGpuBatchSize)); + if (batchSize <= 0) + { + break; + } + + ReadOnlySpan batch = candidateSpan.Slice(index, batchSize); + + int startIndex = 0; + if (!powStepperInitialized) + { + ulong candidate = batch[0]; + powUsed++; + + bool equalsOne = powStepper.InitializeCpuIsUnity(candidate); + powStepperInitialized = true; + if (equalsOne && TryConfirmCandidateCpu(prime, candidate, divisorData, config, ref powUsed, powBudget)) + { + candidates.Clear(); + ThreadStaticPools.ReturnUlongList(candidates); + ThreadStaticPools.ReturnExponentStepperCpu(powStepper); + result = candidate; + return true; + } + + startIndex = 1; + } + + for (int i = startIndex; i < batchSize && powUsed < powBudget; i++) + { + ulong candidate = batch[i]; + powUsed++; + + bool equalsOne = powStepper.ComputeNextIsUnity(candidate); + if (!equalsOne) + { + continue; + } + + if (!TryConfirmCandidateCpu(prime, candidate, divisorData, config, ref powUsed, powBudget)) + { + continue; + } + + candidates.Clear(); + ThreadStaticPools.ReturnUlongList(candidates); + ThreadStaticPools.ReturnExponentStepperCpu(powStepper); + result = candidate; + return true; + } + + index += batchSize; + } + + candidates.Clear(); + ThreadStaticPools.ReturnUlongList(candidates); + ThreadStaticPools.ReturnExponentStepperCpu(powStepper); + + return false; + } + finally + { + orderFactors.Dispose(); + } + } + + private static bool TryHeuristicFinishHybrid( + PrimeOrderCalculatorAccelerator gpu, + ulong prime, + ulong order, + ulong? previousOrder, + in MontgomeryDivisorData divisorData, + in PrimeOrderSearchConfig config, + PartialFactorResult phiFactors, + PartialFactorResult? cachedOrderFactors, + out ulong result) + { + result = 0UL; + if (order <= 1UL) + { + cachedOrderFactors?.Dispose(); + return false; + } + + // Reuse the partial factorization from TryConfirmOrderCpu when available. + PartialFactorResult orderFactors = cachedOrderFactors ?? PartialFactorHybrid(gpu, order, config); + try + { + if (orderFactors.Factors is null) + { + return false; + } + + if (!orderFactors.FullyFactored) + { + if (orderFactors.Cofactor <= 1UL || !orderFactors.CofactorIsPrime) + { + return false; + } + + orderFactors.WithAdditionalPrime(orderFactors.Cofactor); + } + + int capacity = config.MaxPowChecks <= 0 ? 64 : config.MaxPowChecks << 2; + List candidates = ThreadStaticPools.RentUlongList(capacity); + candidates.Clear(); + ulong[] factorArray = orderFactors.Factors!; + int[] exponentsArray = orderFactors.Exponents!; + + BuildCandidates(order, factorArray, exponentsArray, orderFactors.Count, candidates, capacity); + if (candidates.Count == 0) + { + ThreadStaticPools.ReturnUlongList(candidates); + return false; + } + + SortCandidates(prime, previousOrder, candidates); + + int powBudget = config.MaxPowChecks <= 0 ? candidates.Count : config.MaxPowChecks; + int powUsed = 0; + int candidateCount = candidates.Count; + Span candidateSpan = CollectionsMarshal.AsSpan(candidates); + ExponentRemainderStepperCpu powStepper = ThreadStaticPools.RentExponentStepperCpu(divisorData); + bool powStepperInitialized = false; + + int index = 0; + Span stackGpuRemainders = stackalloc ulong[PerfectNumberConstants.DefaultSmallPrimeFactorSlotCount]; + ArrayPool pool = ThreadStaticPools.UlongPool; + while (index < candidateCount && powUsed < powBudget) + { + int remaining = candidateCount - index; + int budgetRemaining = powBudget - powUsed; + int batchSize = Math.Min(remaining, Math.Min(budgetRemaining, PrimeOrderConstants.MaxGpuBatchSize)); + if (batchSize <= 0) + { + break; + } + + ReadOnlySpan batch = candidateSpan.Slice(index, batchSize); + int startIndex = 0; + + if (!powStepperInitialized) + { + ulong candidate = batch[0]; + powUsed++; + + bool equalsOne = powStepper.InitializeCpuIsUnity(candidate); + powStepperInitialized = true; + if (equalsOne && TryConfirmCandidateHybrid(gpu, prime, candidate, divisorData, config, ref powUsed, powBudget)) + { + candidates.Clear(); + ThreadStaticPools.ReturnUlongList(candidates); + ThreadStaticPools.ReturnExponentStepperCpu(powStepper); + result = candidate; + return true; + } + + startIndex = 1; + } + + for (int i = startIndex; i < batchSize && powUsed < powBudget; i++) + { + ulong candidate = batch[i]; + powUsed++; + + bool equalsOne = powStepper.ComputeNextIsUnity(candidate); + if (!equalsOne) + { + continue; + } + + if (!TryConfirmCandidateHybrid(gpu, prime, candidate, divisorData, config, ref powUsed, powBudget)) + { + continue; + } + + candidates.Clear(); + ThreadStaticPools.ReturnUlongList(candidates); + ThreadStaticPools.ReturnExponentStepperCpu(powStepper); + result = candidate; + return true; + } + + index += batchSize; + } + + candidates.Clear(); + ThreadStaticPools.ReturnUlongList(candidates); + ThreadStaticPools.ReturnExponentStepperCpu(powStepper); + + return false; + } + finally + { + orderFactors.Dispose(); + } + } + + private static bool TryHeuristicFinishGpu( + PrimeOrderCalculatorAccelerator gpu, + ulong prime, + ulong order, + ulong? previousOrder, + in MontgomeryDivisorData divisorData, + in PrimeOrderSearchConfig config, + PartialFactorResult phiFactors, + PartialFactorResult? cachedOrderFactors, + out ulong result) + { + result = 0UL; + if (order <= 1UL) + { + cachedOrderFactors?.Dispose(); + return false; + } + + // Reuse the partial factorization from TryConfirmOrderCpu when available. + PartialFactorResult orderFactors = cachedOrderFactors ?? PartialFactorGpu(gpu, order, config); + try + { + if (orderFactors.Factors is null) + { + return false; + } + + if (!orderFactors.FullyFactored) + { + if (orderFactors.Cofactor <= 1UL || !orderFactors.CofactorIsPrime) + { + return false; + } + + orderFactors.WithAdditionalPrime(orderFactors.Cofactor); + } + + int capacity = config.MaxPowChecks <= 0 ? 64 : config.MaxPowChecks << 2; + List candidates = ThreadStaticPools.RentUlongList(capacity); + candidates.Clear(); + ulong[] factorArray = orderFactors.Factors!; + int[] exponentsArray = orderFactors.Exponents!; + BuildCandidates(order, factorArray, exponentsArray, orderFactors.Count, candidates, capacity); if (candidates.Count == 0) { @@ -545,7 +1258,6 @@ internal static partial class PrimeOrderCalculator return false; } - // DebugLog("Sorting candidates"); SortCandidates(prime, previousOrder, candidates); int powBudget = config.MaxPowChecks <= 0 ? candidates.Count : config.MaxPowChecks; @@ -556,10 +1268,8 @@ internal static partial class PrimeOrderCalculator ExponentRemainderStepperCpu powStepper = ThreadStaticPools.RentExponentStepperCpu(divisorData); bool powStepperInitialized = false; - // DebugLog(() => $"Checking candidates ({candidateCount} candidates, {powBudget} pow budget)"); int index = 0; - const int StackGpuBatchSize = 64; - Span stackGpuRemainders = stackalloc ulong[StackGpuBatchSize]; + Span stackGpuRemainders = stackalloc ulong[PerfectNumberConstants.DefaultSmallPrimeFactorSlotCount]; ArrayPool pool = ThreadStaticPools.UlongPool; while (index < candidateCount && powUsed < powBudget) { @@ -578,9 +1288,9 @@ internal static partial class PrimeOrderCalculator bool gpuStackRemainders = false; GpuPow2ModStatus status = GpuPow2ModStatus.Unavailable; - if (allowGpuBatch && IsGpuPow2Allowed) + if (allowGpuBatch) { - if (batchSize <= StackGpuBatchSize) + if (batchSize <= PerfectNumberConstants.DefaultSmallPrimeFactorSlotCount) { Span localRemainders = stackGpuRemainders[..batchSize]; status = PrimeOrderGpuHeuristics.TryPow2ModBatch(gpu, batch, prime, localRemainders, divisorData); @@ -622,7 +1332,7 @@ internal static partial class PrimeOrderCalculator bool equalsOne = powStepper.InitializeCpuIsUnity(candidate); powStepperInitialized = true; - if (equalsOne && TryConfirmCandidateCpu(gpu, prime, candidate, divisorData, config, ref powUsed, powBudget)) + if (equalsOne && TryConfirmCandidateCpu(prime, candidate, divisorData, config, ref powUsed, powBudget)) { if (gpuPool is not null) { @@ -659,7 +1369,7 @@ internal static partial class PrimeOrderCalculator continue; } - if (!TryConfirmCandidateCpu(gpu, prime, candidate, divisorData, config, ref powUsed, powBudget)) + if (!TryConfirmCandidateCpu(prime, candidate, divisorData, config, ref powUsed, powBudget)) { continue; } @@ -695,6 +1405,7 @@ internal static partial class PrimeOrderCalculator orderFactors.Dispose(); } } + private static void SortCandidates(ulong prime, ulong? previousOrder, List candidates) { ulong previous = previousOrder ?? 0UL; @@ -870,94 +1581,161 @@ internal static partial class PrimeOrderCalculator } } - private static bool TryConfirmCandidateCpu(PrimeOrderCalculatorAccelerator gpu, ulong prime, ulong candidate, in MontgomeryDivisorData divisorData, in PrimeOrderSearchConfig config, ref int powUsed, int powBudget) + private static bool TryConfirmCandidateCpu(ulong prime, ulong candidate, in MontgomeryDivisorData divisorData, in PrimeOrderSearchConfig config, ref int powUsed, int powBudget) { - PartialFactorResult factorization = PartialFactor(gpu, candidate, config); - try + PartialFactorResult factorization = PartialFactorCpu(candidate, config); + + if (factorization.Factors is null) { - if (factorization.Factors is null) + factorization.Dispose(); + return false; + } + + if (!factorization.FullyFactored) + { + if (factorization.Cofactor <= 1UL || !factorization.CofactorIsPrime) { + factorization.Dispose(); return false; } - if (!factorization.FullyFactored) + factorization.WithAdditionalPrime(factorization.Cofactor); + } + + ReadOnlySpan factorsSpan = factorization.Factors; + ReadOnlySpan exponentsSpan = factorization.Exponents; + int length = factorization.Count; + + const int StackExponentCapacity = 32; + const int ExponentHardLimit = 256; + + Span stackBuffer = stackalloc ulong[StackExponentCapacity]; + ulong[]? heapCandidateArray = null; + ExponentRemainderStepperCpu stepper = ThreadStaticPools.RentExponentStepperCpu(divisorData); + bool violates = false; + + for (int i = 0; i < length; i++) + { + ulong primeFactor = factorsSpan[i]; + int exponent = exponentsSpan[i]; + + // Factorization entries for candidate confirmation are always positive here. + // if (exponent <= 0) + // { + // continue; + // } + + if (exponent > ExponentHardLimit) { - if (factorization.Cofactor <= 1UL) - { - return false; - } - - if (!factorization.CofactorIsPrime) - { - return false; - } - - factorization.WithAdditionalPrime(factorization.Cofactor); + throw new InvalidOperationException($"Candidate factor exponent {exponent} exceeds the supported limit of {ExponentHardLimit}."); } - // ReadOnlySpan span = factorization.Factors; - ReadOnlySpan factorsSpan = factorization.Factors; - ReadOnlySpan exponentsSpan = factorization.Exponents; - int length = factorization.Count; - - const int StackExponentCapacity = 32; - const int ExponentHardLimit = 256; - - Span stackBuffer = stackalloc ulong[StackExponentCapacity]; - ulong[]? heapCandidateArray = null; - ExponentRemainderStepperCpu stepper = ThreadStaticPools.RentExponentStepperCpu(divisorData); - bool violates = false; - - for (int i = 0; i < length; i++) + if (exponent <= StackExponentCapacity) { - // ulong primeFactor = factorsSpan[i].Value; - // int exponent = factorsSpan[i].Exponent; - ulong primeFactor = factorsSpan[i]; - int exponent = exponentsSpan[i]; - - // Factorization entries for candidate confirmation are always positive here. - // if (exponent <= 0) - // { - // continue; - // } - - if (exponent > ExponentHardLimit) - { - throw new InvalidOperationException($"Candidate factor exponent {exponent} exceeds the supported limit of {ExponentHardLimit}."); - } - - if (exponent <= StackExponentCapacity) - { - if (CheckCandidateViolation(stackBuffer[..exponent], primeFactor, exponent, candidate, prime, ref powUsed, powBudget, ref stepper)) - { - violates = true; - break; - } - - continue; - } - - heapCandidateArray ??= ThreadStaticPools.UlongPool.Rent(ExponentHardLimit); - if (CheckCandidateViolation(heapCandidateArray.AsSpan(0, exponent), primeFactor, exponent, candidate, prime, ref powUsed, powBudget, ref stepper)) + if (CheckCandidateViolation(stackBuffer[..exponent], primeFactor, exponent, candidate, prime, ref powUsed, powBudget, ref stepper)) { violates = true; break; } + + continue; } - ThreadStaticPools.ReturnExponentStepperCpu(stepper); - - if (heapCandidateArray is not null) + heapCandidateArray ??= ThreadStaticPools.UlongPool.Rent(ExponentHardLimit); + if (CheckCandidateViolation(heapCandidateArray.AsSpan(0, exponent), primeFactor, exponent, candidate, prime, ref powUsed, powBudget, ref stepper)) { - ThreadStaticPools.UlongPool.Return(heapCandidateArray); + violates = true; + break; } - - return !violates; } - finally + + ThreadStaticPools.ReturnExponentStepperCpu(stepper); + + if (heapCandidateArray is not null) + { + ThreadStaticPools.UlongPool.Return(heapCandidateArray); + } + + factorization.Dispose(); + return !violates; + } + + private static bool TryConfirmCandidateHybrid(PrimeOrderCalculatorAccelerator gpu, ulong prime, ulong candidate, in MontgomeryDivisorData divisorData, in PrimeOrderSearchConfig config, ref int powUsed, int powBudget) + { + PartialFactorResult factorization = PartialFactorHybrid(gpu, candidate, config); + + if (factorization.Factors is null) { factorization.Dispose(); + return false; } + + if (!factorization.FullyFactored) + { + if (factorization.Cofactor <= 1UL || !factorization.CofactorIsPrime) + { + factorization.Dispose(); + return false; + } + + factorization.WithAdditionalPrime(factorization.Cofactor); + } + + ReadOnlySpan factorsSpan = factorization.Factors; + ReadOnlySpan exponentsSpan = factorization.Exponents; + int length = factorization.Count; + + const int StackExponentCapacity = 32; + const int ExponentHardLimit = 256; + + Span stackBuffer = stackalloc ulong[StackExponentCapacity]; + ulong[]? heapCandidateArray = null; + ExponentRemainderStepperCpu stepper = ThreadStaticPools.RentExponentStepperCpu(divisorData); + bool violates = false; + + for (int i = 0; i < length; i++) + { + ulong primeFactor = factorsSpan[i]; + int exponent = exponentsSpan[i]; + + // Factorization entries for candidate confirmation are always positive here. + // if (exponent <= 0) + // { + // continue; + // } + + if (exponent > ExponentHardLimit) + { + throw new InvalidOperationException($"Candidate factor exponent {exponent} exceeds the supported limit of {ExponentHardLimit}."); + } + + if (exponent <= StackExponentCapacity) + { + if (CheckCandidateViolation(stackBuffer[..exponent], primeFactor, exponent, candidate, prime, ref powUsed, powBudget, ref stepper)) + { + violates = true; + break; + } + + continue; + } + + heapCandidateArray ??= ThreadStaticPools.UlongPool.Rent(ExponentHardLimit); + if (CheckCandidateViolation(heapCandidateArray.AsSpan(0, exponent), primeFactor, exponent, candidate, prime, ref powUsed, powBudget, ref stepper)) + { + violates = true; + break; + } + } + + ThreadStaticPools.ReturnExponentStepperCpu(stepper); + if (heapCandidateArray is not null) + { + ThreadStaticPools.UlongPool.Return(heapCandidateArray); + } + factorization.Dispose(); + + return !violates; } private static bool CheckCandidateViolation(Span buffer, ulong primeFactor, int exponent, ulong candidate, ulong prime, ref int powUsed, int powBudget, ref ExponentRemainderStepperCpu stepper) @@ -1041,9 +1819,11 @@ internal static partial class PrimeOrderCalculator return false; } - private static ulong _pow2GpuHits; - private static ulong _pow2CpuHits; - private static bool Pow2EqualsOneCpu(PrimeOrderCalculatorAccelerator gpu, ulong exponent, ulong prime, in MontgomeryDivisorData divisorData) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool Pow2EqualsOneCpu(ulong exponent, ulong prime, in MontgomeryDivisorData divisorData) + => exponent.Pow2MontgomeryModWindowedConvertToStandardCpu(divisorData) == 1UL; + + private static bool Pow2EqualsOneHybrid(PrimeOrderCalculatorAccelerator gpu, ulong exponent, ulong prime, in MontgomeryDivisorData divisorData) { // if (exponent >= PerfectNumberConstants.MaxQForDivisorCycles || prime >= PerfectNumberConstants.MaxQForDivisorCycles) // { @@ -1059,7 +1839,7 @@ internal static partial class PrimeOrderCalculator // Atomic.Add(ref _pow2CpuHits, 1); // Console.WriteLine($"pow2 CPU Hits {_pow2CpuHits}"); - return exponent.Pow2MontgomeryModWindowedCpu(divisorData, keepMontgomery: false) == 1UL; + return exponent.Pow2MontgomeryModWindowedConvertToStandardCpu(divisorData) == 1UL; } // private static ulong _partialFactorHits; @@ -1069,24 +1849,19 @@ internal static partial class PrimeOrderCalculator private static bool _goToGpu = true; private static int _cpuCount; - private static PartialFactorResult PartialFactor(PrimeOrderCalculatorAccelerator gpu, ulong value, in PrimeOrderSearchConfig config) + private static PartialFactorResult PartialFactorCpu(ulong value, in PrimeOrderSearchConfig config) { if (value <= 1UL) { return PartialFactorResult.Empty; } - const int FactorSlotCount = PrimeOrderConstants.GpuSmallPrimeFactorSlots; - // stackalloc is faster than pooling - // ulong[] primeSlotsArray = ThreadStaticPools.UlongPool.Rent(FactorSlotCount); - // int[] exponentSlotsArray = ThreadStaticPools.IntPool.Rent(FactorSlotCount); - // Span primeSlots = primeSlotsArray.AsSpan(0, FactorSlotCount); - // Span exponentSlots = exponentSlotsArray.AsSpan(0, FactorSlotCount); - Span primeSlots = stackalloc ulong[FactorSlotCount]; - Span exponentSlots = stackalloc int[FactorSlotCount]; + Span primeSlots = stackalloc ulong[PrimeOrderConstants.GpuSmallPrimeFactorSlots]; + Span exponentSlots = stackalloc int[PrimeOrderConstants.GpuSmallPrimeFactorSlots]; ulong[] primeSlotArray = []; int[] exponentSlotArray = []; + // We don't need to worry about leftovers, because we always use indexes within the calculated counts // primeSlots.Clear(); // exponentSlots.Clear(); @@ -1094,69 +1869,43 @@ internal static partial class PrimeOrderCalculator uint limit = config.SmallFactorLimit == 0 ? uint.MaxValue : config.SmallFactorLimit; ulong remaining = value; - // bool gpuFactored = false; - // if (IsGpuHeuristicDevice) - // { - // gpuFactored = PrimeOrderGpuHeuristics.TryPartialFactor(gpu, value, limit, primeSlots, exponentSlots, out factorCount, out remaining); - // } - List pending = ThreadStaticPools.RentPrimeOrderPendingEntryList(2); - Stack? compositeStack = null; - // PrimeTester primeTester = PrimeTester.Exclusive; - // HeuristicPrimeTester? tester = _tester ??= new(); - - // if (!gpuFactored) - // { - PopulateSmallPrimeFactorsCpu( - value, - limit, - ref primeSlots, - ref exponentSlots, - ref primeSlotArray, - ref exponentSlotArray, - out int factorCount, - out remaining); - // } + PopulateSmallPrimeFactorsCpu( + value, + limit, + ref primeSlots, + ref exponentSlots, + ref primeSlotArray, + ref exponentSlotArray, + out int factorCount, + out remaining); + bool limitReached; if (remaining > 1UL) { - pending.Add(new PendingEntry(remaining, knownComposite: false)); - } - - if (config.PollardRhoMilliseconds > 0 && pending.Count > 0) - { - long deadlineTimestamp = CreateDeadlineTimestamp(config.PollardRhoMilliseconds); - compositeStack = ThreadStaticPools.RentUlongStack(Math.Max(pending.Count << 1, 4)); - compositeStack.Push(remaining); - pending.Clear(); - - bool pollardRhoDeadlineReached = Stopwatch.GetTimestamp() > deadlineTimestamp; - if (!pollardRhoDeadlineReached) + if (config.PollardRhoMilliseconds > 0) { - CollectFactors(gpu, ref primeSlots, ref exponentSlots, ref primeSlotArray, ref exponentSlotArray, ref factorCount, pending, compositeStack, deadlineTimestamp, ref pollardRhoDeadlineReached); - // bool runOnCpu = !RunOnGpu(); - // if (runOnCpu) - // { - // CollectFactorsCpu(ref primeSlots, ref exponentSlots, ref primeSlotArray, ref exponentSlotArray, ref factorCount, pending, compositeStack, deadlineTimestamp, ref pollardRhoDeadlineReached); - // } - // else - // { - // CollectFactorsGpu(gpu, ref primeSlots, ref exponentSlots, ref primeSlotArray, ref exponentSlotArray, ref factorCount, pending, compositeStack, deadlineTimestamp, ref pollardRhoDeadlineReached); - // } + long deadlineTimestamp = CreateDeadlineTimestamp(config.PollardRhoMilliseconds); + Stack compositeStack = ThreadStaticPools.RentUlongStack(4); + compositeStack.Push(remaining); - if (!pollardRhoDeadlineReached) + CollectFactorsCpu(ref primeSlots, ref exponentSlots, ref primeSlotArray, ref exponentSlotArray, ref factorCount, pending, compositeStack, deadlineTimestamp, out limitReached); + + if (limitReached) { - pollardRhoDeadlineReached = Stopwatch.GetTimestamp() > deadlineTimestamp; + while (compositeStack.Count > 0) + { + pending.Add(new PendingEntry(compositeStack.Pop(), knownComposite: false)); + } } + + compositeStack.Clear(); + ThreadStaticPools.ReturnUlongStack(compositeStack); } - - if (pollardRhoDeadlineReached) + else { - while (compositeStack.Count > 0) - { - pending.Add(new PendingEntry(compositeStack.Pop(), knownComposite: false)); - } + pending.Add(new PendingEntry(remaining, knownComposite: false)); } } @@ -1178,21 +1927,11 @@ internal static partial class PrimeOrderCalculator if (!entry.HasKnownPrimality) { - // bool isPrime = HeuristicCombinedPrimeTester.IsPrimeCpu(composite); - // bool isPrime = PrimeTester.IsPrime(composite); - bool isPrime = RunOnCpu() ? PrimeTesterByLastDigit.IsPrimeCpu(composite) : HeuristicCombinedPrimeTester.IsPrimeGpu(gpu, composite); - // bool isPrime = PrimeTester.IsPrimeInternal(composite, CancellationToken.None); + bool isPrime = PrimeTesterByLastDigit.IsPrimeCpu(composite); // Atomic.Add(ref _partialFactorPendingHits, 1UL); // Console.WriteLine($"Partial factor pending hits {Volatile.Read(ref _partialFactorPendingHits)}"); - // bool isPrime = RunOnGpu() - // ? HeuristicCombinedPrimeTester.IsPrimeGpu(gpu, composite) - // : PrimeTester.IsPrime(composite); - // bool isPrime = PrimeTester.IsPrime(composite); - // bool isPrime = HeuristicCombinedPrimeTester.IsPrimeGpu(gpu, composite); - // bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(composite); - entry = entry.WithPrimality(isPrime); pending[index] = entry; } @@ -1209,7 +1948,7 @@ internal static partial class PrimeOrderCalculator cofactorContainsComposite = true; } } - + bool cofactorIsPrime; if (cofactor <= 1UL) { @@ -1221,28 +1960,12 @@ internal static partial class PrimeOrderCalculator } else { - // cofactorIsPrime = PrimeTester.IsPrime(cofactor); - // cofactorIsPrime = HeuristicCombinedPrimeTester.IsPrimeCpu(cofactor); - cofactorIsPrime = RunOnCpu() ? PrimeTesterByLastDigit.IsPrimeCpu(cofactor) : HeuristicCombinedPrimeTester.IsPrimeGpu(gpu, cofactor); - // cofactorIsPrime = HeuristicCombinedPrimeTester.IsPrimeGpu(gpu, cofactor); - // cofactorIsPrime = PrimeTester.IsPrimeInternal(cofactor, CancellationToken.None); + cofactorIsPrime = PrimeTesterByLastDigit.IsPrimeCpu(cofactor); // Atomic.Add(ref _partialFactorCofactorHits, 1UL); // Console.WriteLine($"Partial factor cofactor hits {Volatile.Read(ref _partialFactorCofactorHits)}"); - - // cofactorIsPrime = HeuristicCombinedPrimeTester.IsPrimeGpu(gpu, cofactor); - // cofactorIsPrime = PrimeTester.IsPrimeGpu(gpu, cofactor); - // cofactorIsPrime = RunOnGpu() - // ? HeuristicCombinedPrimeTester.IsPrimeGpu(gpu, cofactor) - // : PrimeTester.IsPrime(cofactor); - // cofactorIsPrime = PrimeTester.IsPrime(cofactor); - // cofactorIsPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(cofactor); } - // GpuPrimeWorkLimiter.Release(); - - // ArrayPool pool = ThreadStaticPools.FactorEntryPool; - bool temp; PartialFactorResult result; if (factorCount == 0) { @@ -1271,18 +1994,14 @@ internal static partial class PrimeOrderCalculator ArrayPool ulongPool = ThreadStaticPools.UlongPool; ArrayPool intPool = ThreadStaticPools.IntPool; - // ArrayPool boolPool = ThreadStaticPools.BoolPool; - // FactorEntry[] array = pool.Rent(factorCount); ulong[] factorsArray = ulongPool.Rent(factorCount); int[] exponentsArray = intPool.Rent(factorCount); - // bool[] cofactorIsPrimesArray = boolPool.Rent(factorCount); - int arrayIndex = 0; for (int i = 0; i < factorCount; i++) { - ulong primeValue = primeSlots[i]; - int exponentValue = exponentSlots[i]; + factorsArray[i] = primeSlots[i]; + exponentsArray[i] = exponentSlots[i]; // This will never happen on the execution path from production code // if (primeValue == 0UL || exponentValue == 0) @@ -1290,28 +2009,382 @@ internal static partial class PrimeOrderCalculator // throw new Exception("Prime value or exponent equals zero"); // continue; // } - - factorsArray[arrayIndex] = primeValue; - exponentsArray[arrayIndex] = exponentValue; - arrayIndex++; } - Array.Sort(factorsArray, exponentsArray, 0, arrayIndex); - temp = cofactor == 1UL; - - result = PartialFactorResult.Rent(factorsArray, exponentsArray, cofactor, temp, arrayIndex, cofactorIsPrime); + Array.Sort(factorsArray, exponentsArray, 0, factorCount); + + // Check if it was it factored completely + limitReached = cofactor == 1UL; + + result = PartialFactorResult.Rent(factorsArray, exponentsArray, cofactor, limitReached, factorCount, cofactorIsPrime); ReturnResult: - if (primeSlotArray is not null) + if (primeSlotArray.Length > 0) + { + ThreadStaticPools.UlongPool.Return(primeSlotArray, clearArray: false); + ThreadStaticPools.IntPool.Return(exponentSlotArray, clearArray: false); + } + + pending.Clear(); + ThreadStaticPools.ReturnPrimeOrderPendingEntryList(pending); + return result; + } + + private static PartialFactorResult PartialFactorHybrid(PrimeOrderCalculatorAccelerator gpu, ulong value, in PrimeOrderSearchConfig config) + { + if (value <= 1UL) + { + return PartialFactorResult.Empty; + } + + // stackalloc is faster than pooling + Span primeSlots = stackalloc ulong[PrimeOrderConstants.GpuSmallPrimeFactorSlots]; + Span exponentSlots = stackalloc int[PrimeOrderConstants.GpuSmallPrimeFactorSlots]; + ulong[] primeSlotArray = []; + int[] exponentSlotArray = []; + // We don't need to worry about leftovers, because we always use indexes within the calculated counts + // primeSlots.Clear(); + // exponentSlots.Clear(); + + uint limit = config.SmallFactorLimit == 0 ? uint.MaxValue : config.SmallFactorLimit; + ulong remaining = value; + + List pending = ThreadStaticPools.RentPrimeOrderPendingEntryList(2); + + PopulateSmallPrimeFactorsCpu( + value, + limit, + ref primeSlots, + ref exponentSlots, + ref primeSlotArray, + ref exponentSlotArray, + out int factorCount, + out remaining); + + bool limitReached; + if (remaining > 1UL) + { + if (config.PollardRhoMilliseconds > 0) + { + long deadlineTimestamp = CreateDeadlineTimestamp(config.PollardRhoMilliseconds); + Stack compositeStack = ThreadStaticPools.RentUlongStack(4); + compositeStack.Push(remaining); + + CollectFactorsHybrid(gpu, ref primeSlots, ref exponentSlots, primeSlotArray: ref primeSlotArray, ref exponentSlotArray, ref factorCount, pending, compositeStack, deadlineTimestamp, out limitReached); + + if (limitReached) + { + while (compositeStack.Count > 0) + { + pending.Add(new PendingEntry(compositeStack.Pop(), knownComposite: false)); + } + } + + compositeStack.Clear(); + ThreadStaticPools.ReturnUlongStack(compositeStack); + } + else + { + pending.Add(new PendingEntry(remaining, knownComposite: false)); + } + } + + ulong cofactor = 1UL; + bool cofactorContainsComposite = false; + int pendingCount = pending.Count; + int index = 0; + for (; index < pendingCount; index++) + { + PendingEntry entry = pending[index]; + ulong composite = entry.Value; + + if (entry.KnownComposite) + { + cofactor = checked(cofactor * composite); + cofactorContainsComposite = true; + continue; + } + + if (!entry.HasKnownPrimality) + { + bool isPrime = RunOnCpu() ? PrimeTesterByLastDigit.IsPrimeCpu(composite) : HeuristicCombinedPrimeTester.IsPrimeGpu(gpu, composite); + + // Atomic.Add(ref _partialFactorPendingHits, 1UL); + // Console.WriteLine($"Partial factor pending hits {Volatile.Read(ref _partialFactorPendingHits)}"); + + entry = entry.WithPrimality(isPrime); + pending[index] = entry; + } + + if (entry.IsPrime) + { + AddFactorToCollector(ref primeSlots, ref exponentSlots, ref primeSlotArray, ref exponentSlotArray, ref factorCount, composite, 1); + } + // composite is never smaller on the execution path + // else if (composite > 1UL) + else + { + cofactor = checked(cofactor * composite); + cofactorContainsComposite = true; + } + } + + bool cofactorIsPrime; + if (cofactor <= 1UL) + { + cofactorIsPrime = false; + } + else if (cofactorContainsComposite) + { + cofactorIsPrime = false; + } + else + { + cofactorIsPrime = RunOnCpu() ? PrimeTesterByLastDigit.IsPrimeCpu(cofactor) : HeuristicCombinedPrimeTester.IsPrimeGpu(gpu, cofactor); + } + + PartialFactorResult result; + if (factorCount == 0) + { + if (cofactor == value) + { + result = PartialFactorResult.Rent(cofactor, false, 0, cofactorIsPrime); + goto ReturnResult; + } + + result = PartialFactorResult.Rent(cofactor, cofactor == 1UL, 0, cofactorIsPrime); + goto ReturnResult; + } + + // This will never happen in production code. We'll always get at least 1 factor + // if (factorCount == 0) + // { + // if (cofactor == value) + // { + // result = PartialFactorResult.Rent(null, value, false, 0); + // goto ReturnResult; + // } + + // result = PartialFactorResult.Rent(null, cofactor, cofactor == 1UL, 0); + // goto ReturnResult; + // } + + ArrayPool ulongPool = ThreadStaticPools.UlongPool; + ArrayPool intPool = ThreadStaticPools.IntPool; + + ulong[] factorsArray = ulongPool.Rent(factorCount); + int[] exponentsArray = intPool.Rent(factorCount); + + for (int i = 0; i < factorCount; i++) + { + factorsArray[i] = primeSlots[i]; + exponentsArray[i] = exponentSlots[i]; + + // This will never happen on the execution path from production code + // if (primeValue == 0UL || exponentValue == 0) + // { + // throw new Exception("Prime value or exponent equals zero"); + // continue; + // } + } + + Array.Sort(factorsArray, exponentsArray, 0, factorCount); + + // Check if it was factored completely + limitReached = cofactor == 1UL; + + result = PartialFactorResult.Rent(factorsArray, exponentsArray, cofactor, limitReached, factorCount, cofactorIsPrime); + + ReturnResult: + if (primeSlotArray.Length > 0) { ThreadStaticPools.UlongPool.Return(primeSlotArray, clearArray: false); ThreadStaticPools.IntPool.Return(exponentSlotArray!, clearArray: false); } - if (compositeStack is not null) + pending.Clear(); + ThreadStaticPools.ReturnPrimeOrderPendingEntryList(pending); + return result; + } + + private static PartialFactorResult PartialFactorGpu(PrimeOrderCalculatorAccelerator gpu, ulong value, in PrimeOrderSearchConfig config) + { + if (value <= 1UL) { - compositeStack.Clear(); - ThreadStaticPools.ReturnUlongStack(compositeStack); + return PartialFactorResult.Empty; + } + + // stackalloc is faster than pooling + Span primeSlots = stackalloc ulong[PrimeOrderConstants.GpuSmallPrimeFactorSlots]; + Span exponentSlots = stackalloc int[PrimeOrderConstants.GpuSmallPrimeFactorSlots]; + ulong[] primeSlotArray = []; + int[] exponentSlotArray = []; + // We don't need to worry about leftovers, because we always use indexes within the calculated counts + // primeSlots.Clear(); + // exponentSlots.Clear(); + + uint limit = config.SmallFactorLimit == 0 ? uint.MaxValue : config.SmallFactorLimit; + ulong remaining = value; + + bool gpuFactored = PrimeOrderGpuHeuristics.TryPartialFactor(gpu, value, limit, primeSlots, exponentSlots, out int factorCount, out remaining); + + List pending = ThreadStaticPools.RentPrimeOrderPendingEntryList(2); + + if (!gpuFactored) + { + PopulateSmallPrimeFactorsCpu( + value, + limit, + ref primeSlots, + ref exponentSlots, + ref primeSlotArray, + ref exponentSlotArray, + out factorCount, + out remaining); + } + + bool limitReached; + if (remaining > 1UL) + { + if (config.PollardRhoMilliseconds > 0) + { + long deadlineTimestamp = CreateDeadlineTimestamp(config.PollardRhoMilliseconds); + Stack compositeStack = ThreadStaticPools.RentUlongStack(4); + compositeStack.Push(remaining); + + CollectFactorsCpu(ref primeSlots, ref exponentSlots, ref primeSlotArray, ref exponentSlotArray, ref factorCount, pending, compositeStack, deadlineTimestamp, out limitReached); + + if (limitReached) + { + while (compositeStack.Count > 0) + { + pending.Add(new PendingEntry(compositeStack.Pop(), knownComposite: false)); + } + } + + compositeStack.Clear(); + ThreadStaticPools.ReturnUlongStack(compositeStack); + } + else + { + pending.Add(new PendingEntry(remaining, knownComposite: false)); + } + } + + ulong cofactor = 1UL; + bool cofactorContainsComposite = false; + int pendingCount = pending.Count; + int index = 0; + for (; index < pendingCount; index++) + { + PendingEntry entry = pending[index]; + ulong composite = entry.Value; + + if (entry.KnownComposite) + { + cofactor = checked(cofactor * composite); + cofactorContainsComposite = true; + continue; + } + + if (!entry.HasKnownPrimality) + { + bool isPrime = PrimeTesterByLastDigit.IsPrimeCpu(composite); + + // Atomic.Add(ref _partialFactorPendingHits, 1UL); + // Console.WriteLine($"Partial factor pending hits {Volatile.Read(ref _partialFactorPendingHits)}"); + + entry = entry.WithPrimality(isPrime); + pending[index] = entry; + } + + if (entry.IsPrime) + { + AddFactorToCollector(ref primeSlots, ref exponentSlots, ref primeSlotArray, ref exponentSlotArray, ref factorCount, composite, 1); + } + // composite is never smaller on the execution path + // else if (composite > 1UL) + else + { + cofactor = checked(cofactor * composite); + cofactorContainsComposite = true; + } + } + + bool cofactorIsPrime; + if (cofactor <= 1UL) + { + cofactorIsPrime = false; + } + else if (cofactorContainsComposite) + { + cofactorIsPrime = false; + } + else + { + cofactorIsPrime = PrimeTesterByLastDigit.IsPrimeCpu(cofactor); + + // Atomic.Add(ref _partialFactorCofactorHits, 1UL); + // Console.WriteLine($"Partial factor cofactor hits {Volatile.Read(ref _partialFactorCofactorHits)}"); + } + + PartialFactorResult result; + if (factorCount == 0) + { + if (cofactor == value) + { + result = PartialFactorResult.Rent(cofactor, false, 0, cofactorIsPrime); + goto ReturnResult; + } + + result = PartialFactorResult.Rent(cofactor, cofactor == 1UL, 0, cofactorIsPrime); + goto ReturnResult; + } + + // This will never happen in production code. We'll always get at least 1 factor + // if (factorCount == 0) + // { + // if (cofactor == value) + // { + // result = PartialFactorResult.Rent(null, value, false, 0); + // goto ReturnResult; + // } + + // result = PartialFactorResult.Rent(null, cofactor, cofactor == 1UL, 0); + // goto ReturnResult; + // } + + ArrayPool ulongPool = ThreadStaticPools.UlongPool; + ArrayPool intPool = ThreadStaticPools.IntPool; + + ulong[] factorsArray = ulongPool.Rent(factorCount); + int[] exponentsArray = intPool.Rent(factorCount); + + for (int i = 0; i < factorCount; i++) + { + factorsArray[i] = primeSlots[i]; + exponentsArray[i] = exponentSlots[i]; + + // This will never happen on the execution path from production code + // if (primeValue == 0UL || exponentValue == 0) + // { + // throw new Exception("Prime value or exponent equals zero"); + // continue; + // } + } + + Array.Sort(factorsArray, exponentsArray, 0, factorCount); + + // Check if it was factored completely + limitReached = cofactor == 1UL; + + result = PartialFactorResult.Rent(factorsArray, exponentsArray, cofactor, limitReached, factorCount, cofactorIsPrime); + + ReturnResult: + if (primeSlotArray.Length > 0) + { + ThreadStaticPools.UlongPool.Return(primeSlotArray, clearArray: false); + ThreadStaticPools.IntPool.Return(exponentSlotArray!, clearArray: false); } pending.Clear(); @@ -1320,7 +2393,7 @@ internal static partial class PrimeOrderCalculator } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void CollectFactors(PrimeOrderCalculatorAccelerator gpu, ref Span primeSlots, ref Span exponentSlots, ref ulong[] primeSlotArray, ref int[] exponentSlotArray, ref int factorCount, List pending, Stack compositeStack, long deadlineTimestamp, ref bool pollardRhoDeadlineReached) + private static void CollectFactorsHybrid(PrimeOrderCalculatorAccelerator gpu, ref Span primeSlots, ref Span exponentSlots, ref ulong[] primeSlotArray, ref int[] exponentSlotArray, ref int factorCount, List pending, Stack compositeStack, long deadlineTimestamp, out bool pollardRhoDeadlineReached) { while (compositeStack.Count > 0) { @@ -1333,15 +2406,13 @@ internal static partial class PrimeOrderCalculator continue; } - long currentTimestamp = Stopwatch.GetTimestamp(); - if (currentTimestamp > deadlineTimestamp) + pollardRhoDeadlineReached = Stopwatch.GetTimestamp() > deadlineTimestamp; + if (pollardRhoDeadlineReached) { - pollardRhoDeadlineReached = true; pending.Add(new PendingEntry(composite, knownComposite: true)); continue; } - // if (!TryPollardRhoGpu(gpu, composite, out ulong factor)) if (!TryPollardRhoCpu(composite, deadlineTimestamp, out ulong factor)) { pending.Add(new PendingEntry(composite, knownComposite: true)); @@ -1352,10 +2423,12 @@ internal static partial class PrimeOrderCalculator compositeStack.Push(factor); compositeStack.Push(quotient); } + + pollardRhoDeadlineReached = false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void CollectFactorsCpu(ref Span primeSlots, ref Span exponentSlots, ref ulong[] primeSlotArray, ref int[] exponentSlotArray, ref int factorCount, List pending, Stack compositeStack, long deadlineTimestamp, ref bool pollardRhoDeadlineReached) + private static void CollectFactorsCpu(ref Span primeSlots, ref Span exponentSlots, ref ulong[] primeSlotArray, ref int[] exponentSlotArray, ref int factorCount, List pending, Stack compositeStack, long deadlineTimestamp, out bool pollardRhoDeadlineReached) { while (compositeStack.Count > 0) { @@ -1368,15 +2441,13 @@ internal static partial class PrimeOrderCalculator continue; } - long currentTimestamp = Stopwatch.GetTimestamp(); - if (currentTimestamp > deadlineTimestamp) + pollardRhoDeadlineReached = Stopwatch.GetTimestamp() > deadlineTimestamp; + if (pollardRhoDeadlineReached) { - pollardRhoDeadlineReached = true; pending.Add(new PendingEntry(composite, knownComposite: true)); continue; } - // if (!TryPollardRhoGpu(gpu, composite, out ulong factor)) if (!TryPollardRhoCpu(composite, deadlineTimestamp, out ulong factor)) { pending.Add(new PendingEntry(composite, knownComposite: true)); @@ -1387,6 +2458,8 @@ internal static partial class PrimeOrderCalculator compositeStack.Push(factor); compositeStack.Push(quotient); } + + pollardRhoDeadlineReached = false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -1426,25 +2499,7 @@ internal static partial class PrimeOrderCalculator private static bool RunOnCpu() { - // return false; - return true; - // int cpuCount; - - // lock(_sync) - // { - // cpuCount = _cpuCount; - // cpuCount++; - // if (cpuCount == PerfectNumberConstants.GpuFrequency) - // { - // cpuCount = 0; - // } - - // _cpuCount = cpuCount; - // } - - // return cpuCount == 0; int cpuCount = Atomic.Add(ref _cpuCount, 1); - // return (cpuCount % GpuFrequency) == 0; if (cpuCount == PerfectNumberConstants.GpuFrequency) { Atomic.Add(ref _cpuCount, -PerfectNumberConstants.GpuFrequency); @@ -1614,6 +2669,7 @@ internal static partial class PrimeOrderCalculator return deadline; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static long ConvertMillisecondsToStopwatchTicks(int milliseconds) { long frequency = Stopwatch.Frequency; @@ -1631,7 +2687,7 @@ internal static partial class PrimeOrderCalculator return true; } - long timestamp = 0L; // reused for deadline checks. + long timestamp; // reused for deadline checks. while (true) { ulong c = (DeterministicRandomCpu.NextUInt64() % (n - 1UL)) + 1UL; @@ -1795,11 +2851,11 @@ internal static partial class PrimeOrderCalculator exponents = newExponentSpan; } - private static ulong CalculateByFactorizationCpu(PrimeOrderCalculatorAccelerator gpu, ulong prime, in MontgomeryDivisorData divisorData) + private static ulong CalculateByFactorizationCpu(ulong prime, in MontgomeryDivisorData divisorData) { ulong phi = prime - 1UL; Dictionary counts = new(capacity: 8); - FactorCompletelyCpu(gpu, phi, counts); + FactorCompletelyCpu(phi, counts, false); if (counts.Count == 0) { return phi; @@ -1811,7 +2867,6 @@ internal static partial class PrimeOrderCalculator ulong order = phi; int entryCount = entries.Length; - // GpuPrimeWorkLimiter.Acquire(); for (int i = 0; i < entryCount; i++) { ulong primeFactor = entries[i].Key; @@ -1824,13 +2879,11 @@ internal static partial class PrimeOrderCalculator break; } + // TODO: Benchmark performance with the division vs reusing remainder ulong candidate = order - primeFactor * remainder; // ulong candidate = order / primeFactor; - // if (candidate.Pow2MontgomeryModWindowedCpu(divisorData, keepMontgomery: false) == 1UL) - if (candidate.Pow2MontgomeryModWindowedCpu(divisorData, keepMontgomery: false) == 1UL) - // if (candidate.Pow2MontgomeryModWindowedConvertGpu(gpu, divisorData) == 1UL) - // if (Pow2EqualsOneCpu(candidate, prime, divisorData)) + if (candidate.Pow2MontgomeryModWindowedConvertToStandardCpu(divisorData) == 1UL) { order = candidate; continue; @@ -1840,21 +2893,13 @@ internal static partial class PrimeOrderCalculator } } - // GpuPrimeWorkLimiter.Release(); return order; } - private static void FactorCompletelyCpu(PrimeOrderCalculatorAccelerator gpu, ulong value, Dictionary counts) - { - FactorCompletely(gpu, value, counts, knownComposite: false); - } - // private static ulong _factorCompletelyHits; // private static ulong _factorCompletelyPollardRhoHits; - // [ThreadStatic] - // private static HeuristicPrimeTester? _tester; - private static void FactorCompletely(PrimeOrderCalculatorAccelerator gpu, ulong value, Dictionary counts, bool knownComposite) + private static void FactorCompletelyCpu(ulong value, Dictionary counts, bool knownComposite) { if (value <= 1UL) { @@ -1863,16 +2908,8 @@ internal static partial class PrimeOrderCalculator // Atomic.Add(ref _factorCompletelyHits, 1UL); // Console.WriteLine($"Factor completely hits {Volatile.Read(ref _factorCompletelyHits)}"); - // HeuristicPrimeTester tester = _tester ??= new(); - // bool isPrime = primeTester.HeuristicIsPrimeGpu(value); - // bool isPrime = HeuristicCombinedPrimeTester.IsPrimeGpu(gpu, value); - // bool isPrime = PrimeTester.IsPrimeGpu(value); bool isPrime = PrimeTesterByLastDigit.IsPrimeCpu(value); - // bool isPrime = RunOnGpu() - // ? HeuristicCombinedPrimeTester.IsPrimeGpu(gpu, value) - // : PrimeTester.IsPrime(value); - // bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(value); if (!knownComposite && isPrime) { AddFactor(counts, value, 1); @@ -1885,14 +2922,7 @@ internal static partial class PrimeOrderCalculator // Atomic.Add(ref _factorCompletelyPollardRhoHits, 1UL); // Console.WriteLine($"Factor completely after PollardRho hits {Volatile.Read(ref _factorCompletelyPollardRhoHits)}"); - // isPrime = primeTester.HeuristicIsPrimeGpu(factor); - // isPrime = HeuristicCombinedPrimeTester.IsPrimeGpu(gpu, factor); - // isPrime = PrimeTester.IsPrimeGpu(factor); - // isPrime = RunOnGpu() - // ? HeuristicCombinedPrimeTester.IsPrimeGpu(gpu, factor) - // : PrimeTester.IsPrime(factor); isPrime = PrimeTesterByLastDigit.IsPrimeCpu(factor); - // isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(factor); if (isPrime) { int exponent = 1; @@ -1907,13 +2937,13 @@ internal static partial class PrimeOrderCalculator if (remaining > 1UL) { - FactorCompletely(gpu, remaining, counts, knownComposite: false); + FactorCompletelyCpu(remaining, counts, knownComposite: false); } return; } - FactorCompletely(gpu, factor, counts, knownComposite: true); + FactorCompletelyCpu(factor, counts, knownComposite: true); if (quotient == factor) { @@ -1921,16 +2951,10 @@ internal static partial class PrimeOrderCalculator } else { - // isPrime = primeTester.HeuristicIsPrimeGpu(quotient); - // isPrime = PrimeTester.IsPrimeInternal(quotient, CancellationToken.None); - // Atomic.Add(ref _partialFactorCofactorHits, 1UL); // Console.WriteLine($"Partial factor cofactor hits {Volatile.Read(ref _partialFactorCofactorHits)}"); - // isPrime = HeuristicCombinedPrimeTester.IsPrimeGpu(gpu, quotient); isPrime = PrimeTesterByLastDigit.IsPrimeCpu(quotient); - // isPrime = PrimeTester.IsPrimeGpu(quotient); - // isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(quotient); } if (isPrime) @@ -1939,7 +2963,7 @@ internal static partial class PrimeOrderCalculator } else { - FactorCompletely(gpu, quotient, counts, knownComposite: true); + FactorCompletelyCpu(quotient, counts, knownComposite: true); } } @@ -1973,6 +2997,69 @@ internal static partial class PrimeOrderCalculator } } + private static UInt128 Pow2ModWideCpu(in UInt128 exponent, in UInt128 modulus, in MontgomeryDivisorData? divisorData) + { + if (modulus == UInt128.One) + { + return UInt128.Zero; + } + + return exponent.Pow2MontgomeryModWindowed(modulus); + } + + private static bool TryConfirmCandidateWideCpu(in UInt128 prime, in UInt128 candidate, in PrimeOrderSearchConfig config, ref int powUsed, int powBudget, in MontgomeryDivisorData? divisorData) + { + PartialFactorResult128 factorization = PartialFactorWide(candidate, config); + if (factorization.Factors is null) + { + return false; + } + + if (!factorization.FullyFactored) + { + if (factorization.Cofactor <= UInt128.One) + { + return false; + } + + if (!IsPrimeWide(factorization.Cofactor)) + { + return false; + } + + factorization = factorization.WithAdditionalPrime(factorization.Cofactor); + } + + ReadOnlySpan span = factorization.Factors; + int length = factorization.Count; + for (int i = 0; i < length; i++) + { + UInt128 primeFactor = span[i].Value; + UInt128 reduced = candidate; + for (int iteration = 0; iteration < span[i].Exponent; iteration++) + { + if ((reduced % primeFactor) != UInt128.Zero) + { + break; + } + + reduced /= primeFactor; + if (powUsed >= powBudget && powBudget > 0) + { + return false; + } + + powUsed++; + if (Pow2ModWideCpu(reduced, prime, divisorData) == UInt128.One) + { + return false; + } + } + } + + return true; + } + private readonly struct CandidateKey(int primary, long secondary, long tertiary) { public readonly int Primary = primary; diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.Gpu.cs b/PerfectNumbers.Core/PrimeOrderCalculator.Gpu.cs index 753a6c6..e8de8c4 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.Gpu.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.Gpu.cs @@ -1,3 +1,4 @@ +using System.Buffers; using ILGPU; using ILGPU.Runtime; using PerfectNumbers.Core.Gpu; @@ -7,17 +8,408 @@ namespace PerfectNumbers.Core; internal static partial class PrimeOrderCalculator { - private static bool IsGpuPow2Allowed => s_pow2ModeInitialized && s_allowGpuPow2; - private const int GpuSmallPrimeFactorSlots = 64; + public static ulong CalculateGpu( + PrimeOrderCalculatorAccelerator gpu, + ulong prime, + ulong? previousOrder, + in MontgomeryDivisorData divisorData, + in PrimeOrderSearchConfig config) + { + // TODO: Is this condition ever met on EvenPerfectBitScanner's execution path? If not, we can add a clarification comment and comment out the entire block. We want to support p candidates at least greater or equal to 31. + if (prime <= 3UL) + { + return prime == 3UL ? 2UL : 1UL; + } + + ulong phi = prime - 1UL; + + if (PrimeOrderGpuHeuristics.TryCalculateOrder(gpu, prime, previousOrder, config, divisorData, out ulong gpuOrder)) + { + return gpuOrder; + } + + PartialFactorResult phiFactors = PartialFactorGpu(gpu, phi, config); + + ulong result; + if (phiFactors.Factors is null) + { + result = CalculateByFactorizationCpu(prime, divisorData); + + phiFactors.Dispose(); + return result; + } + + result = RunHeuristicPipelineCpu(prime, previousOrder, config, divisorData, phi, phiFactors); + phiFactors.Dispose(); + return result; + } + + public static UInt128 CalculateGpu( + PrimeOrderCalculatorAccelerator gpu, + in UInt128 prime, + in UInt128? previousOrder, + in PrimeOrderSearchConfig config) + { + MontgomeryDivisorData divisorData; + UInt128 result; + if (prime <= ulong.MaxValue) + { + ulong? previous = null; + if (previousOrder.HasValue) + { + UInt128 previousValue = previousOrder.Value; + if (previousValue <= ulong.MaxValue) + { + previous = (ulong)previousValue; + } + else + { + previous = ulong.MaxValue; + } + } + + ulong prime64 = (ulong)prime; + Queue divisorPool = MontgomeryDivisorDataPool.Shared; + divisorData = divisorPool.FromModulus(prime64); + ulong order64 = CalculateGpu(gpu, prime64, previous, divisorData, config); + divisorPool.Return(divisorData); + result = order64 == 0UL ? UInt128.Zero : (UInt128)order64; + } + else + { + divisorData = MontgomeryDivisorData.Empty; + result = CalculateWideInternalGpu(gpu, prime, previousOrder, divisorData, config); + } + + return result; + } + + private static UInt128 CalculateWideInternalGpu(PrimeOrderCalculatorAccelerator gpu, in UInt128 prime, in UInt128? previousOrder, in MontgomeryDivisorData? divisorData, in PrimeOrderSearchConfig config) + { + if (prime <= UInt128.One) + { + return UInt128.One; + } + + if (prime == (UInt128)3UL) + { + return (UInt128)2UL; + } + + UInt128 phi = prime - UInt128.One; + PartialFactorResult128 phiFactors = PartialFactorWide(phi, config); + UInt128 result; + if (phiFactors.Factors is null) + { + result = FinishStrictlyWideGpu(gpu, prime, divisorData); + } + else + { + result = RunHeuristicPipelineWideGpu(gpu, prime, previousOrder, divisorData, config, phi, phiFactors); + } + + return result; + } + + private static UInt128 RunHeuristicPipelineWideGpu( + PrimeOrderCalculatorAccelerator gpu, + in UInt128 prime, + in UInt128? previousOrder, + in MontgomeryDivisorData? divisorData, + in PrimeOrderSearchConfig config, + in UInt128 phi, + in PartialFactorResult128 phiFactors) + { + if (phiFactors.FullyFactored && TrySpecialMaxWideGpu(gpu, phi, prime, divisorData, phiFactors)) + { + return phi; + } + + UInt128 candidateOrder = InitializeStartingOrderWideGpu(gpu, prime, phi, divisorData); + candidateOrder = ExponentLoweringWideGpu(gpu, candidateOrder, prime, divisorData, phiFactors); + + if (TryConfirmOrderWideGpu(gpu, prime, candidateOrder, divisorData, config)) + { + return candidateOrder; + } + + if (config.Mode == PrimeOrderMode.Strict) + { + return FinishStrictlyWideGpu(gpu, prime, divisorData); + } + + if (TryHeuristicFinishWideGpu(gpu, prime, candidateOrder, previousOrder, divisorData, config, out UInt128 order)) + { + return order; + } + + return candidateOrder; + } + + private static UInt128 FinishStrictlyWideGpu(PrimeOrderCalculatorAccelerator gpu, in UInt128 prime, in MontgomeryDivisorData? divisorData) + { + UInt128 phi = prime - UInt128.One; + Dictionary counts = new(capacity: 8); + FactorCompletelyWide(phi, counts); + if (counts.Count == 0) + { + return phi; + } + + List> entries = new(counts); + entries.Sort(static (a, b) => a.Key.CompareTo(b.Key)); + + UInt128 order = phi; + int entryCount = entries.Count; + for (int i = 0; i < entryCount; i++) + { + UInt128 primeFactor = entries[i].Key; + int exponent = entries[i].Value; + for (int iteration = 0; iteration < exponent; iteration++) + { + if ((order % primeFactor) != UInt128.Zero) + { + UInt128 candidate = order / primeFactor; + if (Pow2ModWideGpu(gpu, candidate, prime, divisorData) == UInt128.One) + { + order = candidate; + } + else + { + break; + } + } + } + } + + return order; + } + + private static bool TrySpecialMaxWideGpu(PrimeOrderCalculatorAccelerator gpu, in UInt128 phi, in UInt128 prime, in MontgomeryDivisorData? divisorData, in PartialFactorResult128 factors) + { + ReadOnlySpan factorSpan = factors.Factors; + int length = factors.Count; + for (int i = 0; i < length; i++) + { + UInt128 factor = factorSpan[i].Value; + UInt128 reduced = phi / factor; + if (Pow2ModWideGpu(gpu, reduced, prime, divisorData) == UInt128.One) + { + return false; + } + } + + return true; + } + + private static UInt128 InitializeStartingOrderWideGpu(PrimeOrderCalculatorAccelerator gpu, in UInt128 prime, in UInt128 phi, in MontgomeryDivisorData? divisorData) + { + UInt128 order = phi; + UInt128 mod8 = prime & (UInt128)7UL; + if (mod8 == UInt128.One || mod8 == (UInt128)7UL) + { + UInt128 half = phi >> 1; + if (Pow2ModWideGpu(gpu, half, prime, divisorData) == UInt128.One) + { + order = half; + } + } + + return order; + } + + private static UInt128 ExponentLoweringWideGpu(PrimeOrderCalculatorAccelerator gpu, UInt128 order, in UInt128 prime, in MontgomeryDivisorData? divisorData, in PartialFactorResult128 factors) + { + ArrayPool pool = ThreadStaticPools.FactorEntry128Pool; + ReadOnlySpan factorSpan = factors.Factors; + int length = factors.Count; + // TODO: This should never trigger from production code - check + // if (length == 0) + // { + // return order; + // } + FactorEntry128[] tempArray = pool.Rent(length + 1); + + Span buffer = tempArray.AsSpan(0, length); + factorSpan.CopyTo(buffer); + + if (!factors.FullyFactored && factors.Cofactor > UInt128.One && IsPrimeWide(factors.Cofactor)) + { + buffer[length] = new FactorEntry128(factors.Cofactor, 1); + length++; + } + + buffer[..length].Sort(static (a, b) => a.Value.CompareTo(b.Value)); + + // TODO: Investigating using stepper to replace modulo operation + for (int i = 0; i < length; i++) + { + UInt128 primeFactor = buffer[i].Value; + int exponent = buffer[i].Exponent; + for (int iteration = 0; iteration < exponent; iteration++) + { + if ((order % primeFactor) == UInt128.Zero) + { + UInt128 reduced = order / primeFactor; + if (Pow2ModWideGpu(gpu, reduced, prime, divisorData) == UInt128.One) + { + order = reduced; + continue; + } + } + + break; + } + } + + pool.Return(tempArray, clearArray: false); + return order; + } + + private static bool TryConfirmOrderWideGpu(PrimeOrderCalculatorAccelerator gpu, in UInt128 prime, in UInt128 order, in MontgomeryDivisorData? divisorData, in PrimeOrderSearchConfig config) + { + if (order == UInt128.Zero) + { + return false; + } + + if (Pow2ModWideGpu(gpu, order, prime, divisorData) != UInt128.One) + { + return false; + } + + PartialFactorResult128 factorization = PartialFactorWide(order, config); + if (factorization.Factors is null) + { + return false; + } + + if (!factorization.FullyFactored) + { + if (factorization.Cofactor <= UInt128.One) + { + return false; + } + + if (!IsPrimeWide(factorization.Cofactor)) + { + return false; + } + + factorization = factorization.WithAdditionalPrime(factorization.Cofactor); + } + + ReadOnlySpan span = factorization.Factors; + int length = factorization.Count; + for (int i = 0; i < length; i++) + { + UInt128 primeFactor = span[i].Value; + UInt128 reduced = order; + for (int iteration = 0; iteration < span[i].Exponent; iteration++) + { + if ((reduced % primeFactor) != UInt128.Zero) + { + break; + } + + reduced /= primeFactor; + if (Pow2ModWideGpu(gpu, reduced, prime, divisorData) == UInt128.One) + { + return false; + } + } + } + + return true; + } + + private static bool TryHeuristicFinishWideGpu( + PrimeOrderCalculatorAccelerator gpu, + in UInt128 prime, + in UInt128 order, + in UInt128? previousOrder, + in MontgomeryDivisorData? divisorData, + in PrimeOrderSearchConfig config, + out UInt128 result) + { + result = UInt128.Zero; + if (order <= UInt128.One) + { + return false; + } + + PartialFactorResult128 orderFactors = PartialFactorWide(order, config); + if (orderFactors.Factors is null) + { + return false; + } + + if (!orderFactors.FullyFactored) + { + if (orderFactors.Cofactor <= UInt128.One) + { + return false; + } + + if (!IsPrimeWide(orderFactors.Cofactor)) + { + return false; + } + + orderFactors = orderFactors.WithAdditionalPrime(orderFactors.Cofactor); + } + + int capacity = config.MaxPowChecks <= 0 ? 64 : config.MaxPowChecks * 4; + List candidates = new(capacity); + FactorEntry128[] factorArray = orderFactors.Factors!; + BuildCandidatesWide(order, factorArray, orderFactors.Count, candidates, capacity); + if (candidates.Count == 0) + { + return false; + } + + SortCandidatesWide(prime, previousOrder, candidates); + + int powBudget = config.MaxPowChecks <= 0 ? candidates.Count : config.MaxPowChecks; + int powUsed = 0; + int candidateCount = candidates.Count; + + for (int i = 0; i < candidateCount; i++) + { + if (powUsed >= powBudget && powBudget > 0) + { + break; + } + + UInt128 candidate = candidates[i]; + powUsed++; + + if (Pow2ModWideGpu(gpu, candidate, prime, divisorData) != UInt128.One) + { + continue; + } + + if (!TryConfirmCandidateWideGpu(gpu, prime, candidate, config, ref powUsed, powBudget, divisorData)) + { + continue; + } + + result = candidate; + return true; + } + + return false; + } + private static ulong CalculateByFactorizationGpu(PrimeOrderCalculatorAccelerator gpu, ulong prime, in MontgomeryDivisorData divisorData) { ulong phi = prime - 1UL; Dictionary counts = gpu.Pow2ModEntriesToTestOnHost; counts.Clear(); - FactorCompletelyCpu(gpu, phi, counts); + FactorCompletelyCpu(phi, counts, false); if (counts.Count == 0) { return phi; @@ -156,6 +548,86 @@ internal static partial class PrimeOrderCalculator return result != 0; } + private static UInt128 Pow2ModWideGpu(PrimeOrderCalculatorAccelerator gpu, in UInt128 exponent, in UInt128 modulus, in MontgomeryDivisorData? divisorData) + { + if (modulus == UInt128.One) + { + return UInt128.Zero; + } + + if (modulus <= (UInt128)ulong.MaxValue && exponent <= (UInt128)ulong.MaxValue) + { + ulong prime64 = (ulong)modulus; + ulong exponent64 = (ulong)exponent; + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(gpu, exponent64, prime64, out ulong remainder, divisorData); + if (status == GpuPow2ModStatus.Success) + { + return remainder; + } + } + + GpuPow2ModStatus wideStatus = PrimeOrderGpuHeuristics.TryPow2Mod(gpu, exponent, modulus, out UInt128 wideRemainder); + if (wideStatus == GpuPow2ModStatus.Success) + { + return wideRemainder; + } + + return exponent.Pow2MontgomeryModWindowed(modulus); + } + + private static bool TryConfirmCandidateWideGpu(PrimeOrderCalculatorAccelerator gpu, in UInt128 prime, in UInt128 candidate, in PrimeOrderSearchConfig config, ref int powUsed, int powBudget, in MontgomeryDivisorData? divisorData) + { + PartialFactorResult128 factorization = PartialFactorWide(candidate, config); + if (factorization.Factors is null) + { + return false; + } + + if (!factorization.FullyFactored) + { + if (factorization.Cofactor <= UInt128.One) + { + return false; + } + + if (!IsPrimeWide(factorization.Cofactor)) + { + return false; + } + + factorization = factorization.WithAdditionalPrime(factorization.Cofactor); + } + + ReadOnlySpan span = factorization.Factors; + int length = factorization.Count; + for (int i = 0; i < length; i++) + { + UInt128 primeFactor = span[i].Value; + UInt128 reduced = candidate; + for (int iteration = 0; iteration < span[i].Exponent; iteration++) + { + if ((reduced % primeFactor) != UInt128.Zero) + { + break; + } + + reduced /= primeFactor; + if (powUsed >= powBudget && powBudget > 0) + { + return false; + } + + powUsed++; + if (Pow2ModWideGpu(gpu, reduced, prime, divisorData) == UInt128.One) + { + return false; + } + } + } + + return true; + } + private static bool TryPollardRhoGpu(PrimeOrderCalculatorAccelerator gpu, ulong n, out ulong factor) { Span randomStateSpan = stackalloc ulong[1]; @@ -184,6 +656,5 @@ internal static partial class PrimeOrderCalculator factor = factored ? factorSpan[0] : 0UL; return factored; } - } diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.Hybrid.cs b/PerfectNumbers.Core/PrimeOrderCalculator.Hybrid.cs new file mode 100644 index 0000000..d628aef --- /dev/null +++ b/PerfectNumbers.Core/PrimeOrderCalculator.Hybrid.cs @@ -0,0 +1,78 @@ +using PerfectNumbers.Core.Gpu.Accelerators; + +namespace PerfectNumbers.Core; + +internal static partial class PrimeOrderCalculator +{ + public static ulong CalculateHybrid( + PrimeOrderCalculatorAccelerator gpu, + ulong prime, + ulong? previousOrder, + in MontgomeryDivisorData divisorData, + in PrimeOrderSearchConfig config) + { + // TODO: Is this condition ever met on EvenPerfectBitScanner's execution path? If not, we can add a clarification comment and comment out the entire block. We want to support p candidates at least greater or equal to 31. + if (prime <= 3UL) + { + return prime == 3UL ? 2UL : 1UL; + } + + ulong phi = prime - 1UL; + + PartialFactorResult phiFactors = PartialFactorHybrid(gpu, phi, config); + + ulong result; + if (phiFactors.Factors is null) + { + result = CalculateByFactorizationCpu(prime, divisorData); + + phiFactors.Dispose(); + return result; + } + + result = RunHeuristicPipelineHybrid(gpu, prime, previousOrder, config, divisorData, phi, phiFactors); + + phiFactors.Dispose(); + return result; + } + + public static UInt128 CalculateHybrid( + PrimeOrderCalculatorAccelerator gpu, + in UInt128 prime, + in UInt128? previousOrder, + in PrimeOrderSearchConfig config) + { + MontgomeryDivisorData divisorData; + UInt128 result; + if (prime <= ulong.MaxValue) + { + ulong? previous = null; + if (previousOrder.HasValue) + { + UInt128 previousValue = previousOrder.Value; + if (previousValue <= ulong.MaxValue) + { + previous = (ulong)previousValue; + } + else + { + previous = ulong.MaxValue; + } + } + + ulong prime64 = (ulong)prime; + Queue divisorPool = MontgomeryDivisorDataPool.Shared; + divisorData = divisorPool.FromModulus(prime64); + ulong order64 = CalculateHybrid(gpu, prime64, previous, divisorData, config); + divisorPool.Return(divisorData); + result = order64 == 0UL ? UInt128.Zero : (UInt128)order64; + } + else + { + divisorData = MontgomeryDivisorData.Empty; + result = CalculateWideInternalCpu(prime, previousOrder, divisorData, config); + } + + return result; + } +} diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.Shared.cs b/PerfectNumbers.Core/PrimeOrderCalculator.Shared.cs index b6d90c9..b4e98e4 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.Shared.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.Shared.cs @@ -8,365 +8,6 @@ namespace PerfectNumbers.Core; internal static partial class PrimeOrderCalculator { - [ThreadStatic] - private static bool s_pow2ModeInitialized; - - [ThreadStatic] - private static bool s_allowGpuPow2; - - [ThreadStatic] - private static PrimeOrderHeuristicDevice s_deviceMode; - - [ThreadStatic] - private static bool s_debugLoggingEnabled; - - private static bool IsGpuHeuristicDevice => s_pow2ModeInitialized && s_deviceMode == PrimeOrderHeuristicDevice.Gpu; - - private readonly struct Pow2ModeScope - { - private readonly bool _previousInitialized; - private readonly bool _previousAllow; - private readonly PrimeOrderHeuristicDevice _previousDevice; - - public Pow2ModeScope(PrimeOrderHeuristicDevice device) - { - _previousInitialized = s_pow2ModeInitialized; - _previousAllow = s_allowGpuPow2; - _previousDevice = s_deviceMode; - s_pow2ModeInitialized = true; - s_allowGpuPow2 = device == PrimeOrderHeuristicDevice.Gpu; - s_deviceMode = device; - } - - public void Dispose() - { - s_allowGpuPow2 = _previousAllow; - s_pow2ModeInitialized = _previousInitialized; - s_deviceMode = _previousDevice; - } - } - - private readonly struct DebugLoggingScope - { - private readonly bool _previous; - - public DebugLoggingScope(bool enabled) - { - _previous = s_debugLoggingEnabled; - s_debugLoggingEnabled = enabled; - } - - public void Dispose() - { - s_debugLoggingEnabled = _previous; - } - } - - private static Pow2ModeScope UsePow2Mode(PrimeOrderHeuristicDevice device) => new(device); - - private static DebugLoggingScope UseDebugLogging(bool enabled) => new(enabled); - - [Conditional("DEBUG")] - private static void DebugLog(string message) - { - if (s_debugLoggingEnabled) - { - Console.WriteLine(message); - } - } - - [Conditional("DEBUG")] - private static void DebugLog(Func messageFactory) - { - if (s_debugLoggingEnabled) - { - Console.WriteLine(messageFactory()); - } - } - private static UInt128 CalculateWideInternal(PrimeOrderCalculatorAccelerator gpu, in UInt128 prime, in UInt128? previousOrder, in MontgomeryDivisorData? divisorData, in PrimeOrderSearchConfig config) - { - if (prime <= UInt128.One) - { - return UInt128.One; - } - - if (prime == (UInt128)3UL) - { - return (UInt128)2UL; - } - - UInt128 phi = prime - UInt128.One; - var debugScope = UseDebugLogging(!IsGpuHeuristicDevice); - PartialFactorResult128 phiFactors = PartialFactorWide(phi, config); - UInt128 result; - if (phiFactors.Factors is null) - { - result = FinishStrictlyWide(gpu, prime, divisorData); - } - else - { - result = RunHeuristicPipelineWide(gpu, prime, previousOrder, divisorData, config, phi, phiFactors); - } - - debugScope.Dispose(); - return result; - } - - private static UInt128 RunHeuristicPipelineWide( - PrimeOrderCalculatorAccelerator gpu, - in UInt128 prime, - in UInt128? previousOrder, - in MontgomeryDivisorData? divisorData, - in PrimeOrderSearchConfig config, - in UInt128 phi, - in PartialFactorResult128 phiFactors) - { - if (phiFactors.FullyFactored && TrySpecialMaxWide(gpu, phi, prime, divisorData, phiFactors)) - { - return phi; - } - - UInt128 candidateOrder = InitializeStartingOrderWide(gpu, prime, phi, divisorData); - candidateOrder = ExponentLoweringWide(gpu, candidateOrder, prime, divisorData, phiFactors); - - if (TryConfirmOrderWide(gpu, prime, candidateOrder, divisorData, config)) - { - return candidateOrder; - } - - if (config.Mode == PrimeOrderMode.Strict) - { - return FinishStrictlyWide(gpu, prime, divisorData); - } - - if (TryHeuristicFinishWide(gpu, prime, candidateOrder, previousOrder, divisorData, config, out UInt128 order)) - { - return order; - } - - return candidateOrder; - } - - private static UInt128 FinishStrictlyWide(PrimeOrderCalculatorAccelerator gpu, in UInt128 prime, in MontgomeryDivisorData? divisorData) - { - return CalculateByFactorizationWide(gpu, prime, divisorData); - } - - private static bool TrySpecialMaxWide(PrimeOrderCalculatorAccelerator gpu, in UInt128 phi, in UInt128 prime, in MontgomeryDivisorData? divisorData, in PartialFactorResult128 factors) - { - ReadOnlySpan factorSpan = factors.Factors; - int length = factors.Count; - for (int i = 0; i < length; i++) - { - UInt128 factor = factorSpan[i].Value; - UInt128 reduced = phi / factor; - if (Pow2ModWide(gpu, reduced, prime, divisorData) == UInt128.One) - { - return false; - } - } - - return true; - } - - private static UInt128 InitializeStartingOrderWide(PrimeOrderCalculatorAccelerator gpu, in UInt128 prime, in UInt128 phi, in MontgomeryDivisorData? divisorData) - { - UInt128 order = phi; - UInt128 mod8 = prime & (UInt128)7UL; - if (mod8 == UInt128.One || mod8 == (UInt128)7UL) - { - UInt128 half = phi >> 1; - if (Pow2ModWide(gpu, half, prime, divisorData) == UInt128.One) - { - order = half; - } - } - - return order; - } - - private static UInt128 ExponentLoweringWide(PrimeOrderCalculatorAccelerator gpu, UInt128 order, in UInt128 prime, in MontgomeryDivisorData divisorData, in PartialFactorResult128 factors) - { - ArrayPool pool = ThreadStaticPools.FactorEntry128Pool; - ReadOnlySpan factorSpan = factors.Factors; - int length = factors.Count; - // TODO: This should never trigger from production code - check - // if (length == 0) - // { - // return order; - // } - FactorEntry128[] tempArray = pool.Rent(length + 1); - - Span buffer = tempArray.AsSpan(0, length); - factorSpan.CopyTo(buffer); - - if (!factors.FullyFactored && factors.Cofactor > UInt128.One && IsPrimeWide(factors.Cofactor)) - { - buffer[length] = new FactorEntry128(factors.Cofactor, 1); - length++; - } - - buffer.Slice(0, length).Sort(static (a, b) => a.Value.CompareTo(b.Value)); - - for (int i = 0; i < length; i++) - { - UInt128 primeFactor = buffer[i].Value; - int exponent = buffer[i].Exponent; - for (int iteration = 0; iteration < exponent; iteration++) - { - if ((order % primeFactor) == UInt128.Zero) - { - UInt128 reduced = order / primeFactor; - if (Pow2ModWide(gpu, reduced, prime, divisorData) == UInt128.One) - { - order = reduced; - continue; - } - } - - break; - } - } - - pool.Return(tempArray, clearArray: false); - return order; - } - - private static bool TryConfirmOrderWide(PrimeOrderCalculatorAccelerator gpu, in UInt128 prime, in UInt128 order, in MontgomeryDivisorData divisorData, in PrimeOrderSearchConfig config) - { - if (order == UInt128.Zero) - { - return false; - } - - if (Pow2ModWide(gpu, order, prime, divisorData) != UInt128.One) - { - return false; - } - - PartialFactorResult128 factorization = PartialFactorWide(order, config); - if (factorization.Factors is null) - { - return false; - } - - if (!factorization.FullyFactored) - { - if (factorization.Cofactor <= UInt128.One) - { - return false; - } - - if (!IsPrimeWide(factorization.Cofactor)) - { - return false; - } - - factorization = factorization.WithAdditionalPrime(factorization.Cofactor); - } - - ReadOnlySpan span = factorization.Factors; - int length = factorization.Count; - for (int i = 0; i < length; i++) - { - UInt128 primeFactor = span[i].Value; - UInt128 reduced = order; - for (int iteration = 0; iteration < span[i].Exponent; iteration++) - { - if ((reduced % primeFactor) != UInt128.Zero) - { - break; - } - - reduced /= primeFactor; - if (Pow2ModWide(gpu, reduced, prime, divisorData) == UInt128.One) - { - return false; - } - } - } - - return true; - } - - private static bool TryHeuristicFinishWide( - PrimeOrderCalculatorAccelerator gpu, - in UInt128 prime, - in UInt128 order, - in UInt128? previousOrder, - in MontgomeryDivisorData divisorData, - in PrimeOrderSearchConfig config, - out UInt128 result) - { - result = UInt128.Zero; - if (order <= UInt128.One) - { - return false; - } - - PartialFactorResult128 orderFactors = PartialFactorWide(order, config); - if (orderFactors.Factors is null) - { - return false; - } - - if (!orderFactors.FullyFactored) - { - if (orderFactors.Cofactor <= UInt128.One) - { - return false; - } - - if (!IsPrimeWide(orderFactors.Cofactor)) - { - return false; - } - - orderFactors = orderFactors.WithAdditionalPrime(orderFactors.Cofactor); - } - - int capacity = config.MaxPowChecks <= 0 ? 64 : config.MaxPowChecks * 4; - List candidates = new(capacity); - FactorEntry128[] factorArray = orderFactors.Factors!; - BuildCandidatesWide(order, factorArray, orderFactors.Count, candidates, capacity); - if (candidates.Count == 0) - { - return false; - } - - SortCandidatesWide(prime, previousOrder, candidates); - - int powBudget = config.MaxPowChecks <= 0 ? candidates.Count : config.MaxPowChecks; - int powUsed = 0; - int candidateCount = candidates.Count; - - for (int i = 0; i < candidateCount; i++) - { - if (powUsed >= powBudget && powBudget > 0) - { - break; - } - - UInt128 candidate = candidates[i]; - powUsed++; - - if (Pow2ModWide(gpu, candidate, prime, divisorData) != UInt128.One) - { - continue; - } - - if (!TryConfirmCandidateWide(gpu, prime, candidate, config, ref powUsed, powBudget, divisorData)) - { - continue; - } - - result = candidate; - return true; - } - - return false; - } - private static void SortCandidatesWide(UInt128 prime, in UInt128? previousOrder, List candidates) { UInt128 previous = previousOrder ?? UInt128.Zero; @@ -539,59 +180,6 @@ internal static partial class PrimeOrderCalculator } } - private static bool TryConfirmCandidateWide(PrimeOrderCalculatorAccelerator gpu, in UInt128 prime, in UInt128 candidate, in PrimeOrderSearchConfig config, ref int powUsed, int powBudget, in MontgomeryDivisorData divisorData) - { - PartialFactorResult128 factorization = PartialFactorWide(candidate, config); - if (factorization.Factors is null) - { - return false; - } - - if (!factorization.FullyFactored) - { - if (factorization.Cofactor <= UInt128.One) - { - return false; - } - - if (!IsPrimeWide(factorization.Cofactor)) - { - return false; - } - - factorization = factorization.WithAdditionalPrime(factorization.Cofactor); - } - - ReadOnlySpan span = factorization.Factors; - int length = factorization.Count; - for (int i = 0; i < length; i++) - { - UInt128 primeFactor = span[i].Value; - UInt128 reduced = candidate; - for (int iteration = 0; iteration < span[i].Exponent; iteration++) - { - if ((reduced % primeFactor) != UInt128.Zero) - { - break; - } - - reduced /= primeFactor; - if (powUsed >= powBudget && powBudget > 0) - { - return false; - } - - powUsed++; - if (Pow2ModWide(gpu, reduced, prime, divisorData) == UInt128.One) - { - return false; - } - } - } - - return true; - } - private static PartialFactorResult128 PartialFactorWide(in UInt128 value, in PrimeOrderSearchConfig config) { if (value <= UInt128.One) @@ -858,45 +446,6 @@ internal static partial class PrimeOrderCalculator return 64 + BitOperations.TrailingZeroCount(high); } - private static UInt128 CalculateByFactorizationWide(PrimeOrderCalculatorAccelerator gpu, in UInt128 prime, in MontgomeryDivisorData? divisorData) - { - UInt128 phi = prime - UInt128.One; - Dictionary counts = new(capacity: 8); - FactorCompletelyWide(phi, counts); - if (counts.Count == 0) - { - return phi; - } - - List> entries = new(counts); - entries.Sort(static (a, b) => a.Key.CompareTo(b.Key)); - - UInt128 order = phi; - int entryCount = entries.Count; - for (int i = 0; i < entryCount; i++) - { - UInt128 primeFactor = entries[i].Key; - int exponent = entries[i].Value; - for (int iteration = 0; iteration < exponent; iteration++) - { - if ((order % primeFactor) != UInt128.Zero) - { - UInt128 candidate = order / primeFactor; - if (Pow2ModWide(gpu, candidate, prime, divisorData) == UInt128.One) - { - order = candidate; - } - else - { - break; - } - } - } - } - - return order; - } - private static void FactorCompletelyWide(in UInt128 value, Dictionary counts) { if (value <= UInt128.One) @@ -1029,36 +578,6 @@ internal static partial class PrimeOrderCalculator return true; } - private static UInt128 Pow2ModWide(PrimeOrderCalculatorAccelerator gpu, in UInt128 exponent, in UInt128 modulus, in MontgomeryDivisorData? divisorData) - { - if (modulus == UInt128.One) - { - return UInt128.Zero; - } - - if (IsGpuPow2Allowed) - { - if (modulus <= (UInt128)ulong.MaxValue && exponent <= (UInt128)ulong.MaxValue) - { - ulong prime64 = (ulong)modulus; - ulong exponent64 = (ulong)exponent; - GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(gpu, exponent64, prime64, out ulong remainder, divisorData); - if (status == GpuPow2ModStatus.Success) - { - return remainder; - } - } - - GpuPow2ModStatus wideStatus = PrimeOrderGpuHeuristics.TryPow2Mod(gpu, exponent, modulus, out UInt128 wideRemainder); - if (wideStatus == GpuPow2ModStatus.Success) - { - return wideRemainder; - } - } - - return exponent.Pow2MontgomeryModWindowed(modulus); - } - private static void AddFactor(Dictionary counts, in UInt128 prime, int exponent) { if (counts.TryGetValue(prime, out int existing)) diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.cs b/PerfectNumbers.Core/PrimeOrderCalculator.cs index 6198919..e8140ea 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.cs @@ -1,5 +1,4 @@ using System.Runtime.CompilerServices; -using PerfectNumbers.Core.Gpu.Accelerators; namespace PerfectNumbers.Core; @@ -17,21 +16,21 @@ internal static partial class PrimeOrderCalculator public static PrimeOrderSearchConfig HeuristicDefault { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _heuristicDefault; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _heuristicDefault; } public static readonly PrimeOrderSearchConfig StrictDefault = new(smallFactorLimit: 1_000_000, pollardRhoMilliseconds: 0, maxPowChecks: 0, PrimeOrderMode.Strict); public static void ConfigureHeuristicDefault(int threadCount) { - _heuristicDefault = CreateHeuristicDefault(threadCount); + _heuristicDefault = CreateHeuristicDefault(threadCount); } public static PrimeOrderSearchConfig CreateHeuristicDefault(int threadCount) { - int normalizedThreadCount = threadCount < 1 ? 1 : threadCount; - return new(smallFactorLimit: 100_000, pollardRhoMilliseconds: normalizedThreadCount * 24, maxPowChecks: 24, PrimeOrderMode.Heuristic); + int normalizedThreadCount = threadCount < 1 ? 1 : threadCount; + return new(smallFactorLimit: 100_000, pollardRhoMilliseconds: normalizedThreadCount * 24, maxPowChecks: 24, PrimeOrderMode.Heuristic); } public readonly uint SmallFactorLimit = smallFactorLimit; @@ -40,68 +39,6 @@ internal static partial class PrimeOrderCalculator public readonly PrimeOrderMode Mode = mode; } - internal enum PrimeOrderHeuristicDevice - { - Cpu, - Gpu, - } - // TODO: Remove branching on the CPU / GPU paths // TODO: Split big non-static functions into smaller / extract static code to limit JIT time - public static ulong Calculate( - PrimeOrderCalculatorAccelerator gpu, - ulong prime, - ulong? previousOrder, - in MontgomeryDivisorData divisorData, - in PrimeOrderSearchConfig config, - PrimeOrderHeuristicDevice device) - { - var scope = UsePow2Mode(device); - ulong order = CalculateInternal(gpu, prime, previousOrder, divisorData, config); - scope.Dispose(); - return order; - } - - public static UInt128 Calculate( - PrimeOrderCalculatorAccelerator gpu, - in UInt128 prime, - in UInt128? previousOrder, - in PrimeOrderSearchConfig config, - in PrimeOrderHeuristicDevice device) - { - var scope = UsePow2Mode(device); - MontgomeryDivisorData divisorData; - UInt128 result; - if (prime <= ulong.MaxValue) - { - ulong? previous = null; - if (previousOrder.HasValue) - { - UInt128 previousValue = previousOrder.Value; - if (previousValue <= ulong.MaxValue) - { - previous = (ulong)previousValue; - } - else - { - previous = ulong.MaxValue; - } - } - - ulong prime64 = (ulong)prime; - Queue divisorPool = MontgomeryDivisorDataPool.Shared; - divisorData = divisorPool.FromModulus(prime64); - ulong order64 = Calculate(gpu, prime64, previous, divisorData, config, device); - divisorPool.Return(divisorData); - result = order64 == 0UL ? UInt128.Zero : (UInt128)order64; - } - else - { - divisorData = MontgomeryDivisorData.Empty; - result = CalculateWideInternal(gpu, prime, previousOrder, divisorData, config); - } - - scope.Dispose(); - return result; - } } diff --git a/PerfectNumbers.Core/ThreadStaticPools.cs b/PerfectNumbers.Core/ThreadStaticPools.cs index 0c22a03..39b9f2a 100644 --- a/PerfectNumbers.Core/ThreadStaticPools.cs +++ b/PerfectNumbers.Core/ThreadStaticPools.cs @@ -69,7 +69,6 @@ namespace PerfectNumbers.Core } _mersenneCpuDivisorSession = null; - session.Reset(); return session; } diff --git a/PerfectNumbers.Core/UInt128Extensions.cs b/PerfectNumbers.Core/UInt128Extensions.cs index 63b609f..5152383 100644 --- a/PerfectNumbers.Core/UInt128Extensions.cs +++ b/PerfectNumbers.Core/UInt128Extensions.cs @@ -517,7 +517,7 @@ public static class UInt128Extensions return (b1, a1); } - public static ulong CalculateOrder(this UInt128 q, PrimeOrderCalculatorAccelerator gpu) + public static ulong CalculateOrderCpu(this UInt128 q) { if (q <= UInt128Numbers.Two) { @@ -536,7 +536,107 @@ public static class UInt128Extensions ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2; int i = 0, primesLength = smallPrimes.Length; - UInt128 cycle = MersenneDivisorCycles.GetCycle(gpu, q); + UInt128 cycle = MersenneDivisorCycles.GetCycleCpu(q); + // TODO: If the cache lacks this cycle, immediately schedule the configured device + // (GPU by default) to compute it on the fly and skip inserting it into the cache so + // wide-order factoring can still leverage cycle stepping without breaching the + // memory cap or introducing extra synchronization. + ulong prime, temp; + for (; i < primesLength; i++) + { + if (smallPrimesPow2[i] > order) + { + break; + } + + prime = smallPrimes[i]; + while (order % prime == 0UL) + { + temp = order / prime; + if (temp.PowModWithCycle(q, cycle) == one) + { + order = temp; + } + else + { + break; + } + } + } + + return order; + } + + public static ulong CalculateOrderGpu(this UInt128 q, PrimeOrderCalculatorAccelerator gpu) + { + if (q <= UInt128Numbers.Two) + { + return 0UL; + } + + UInt128 one = UInt128.One; + UInt128 phi = q - one; + if (phi > ulong.MaxValue) + { + throw new NotImplementedException("Such big values are not yet supported"); + } + + ulong order = (ulong)phi; + uint[] smallPrimes = PrimesGenerator.SmallPrimes; + ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2; + + int i = 0, primesLength = smallPrimes.Length; + UInt128 cycle = MersenneDivisorCycles.GetCycleGpu(gpu, q); + // TODO: If the cache lacks this cycle, immediately schedule the configured device + // (GPU by default) to compute it on the fly and skip inserting it into the cache so + // wide-order factoring can still leverage cycle stepping without breaching the + // memory cap or introducing extra synchronization. + ulong prime, temp; + for (; i < primesLength; i++) + { + if (smallPrimesPow2[i] > order) + { + break; + } + + prime = smallPrimes[i]; + while (order % prime == 0UL) + { + temp = order / prime; + if (temp.PowModWithCycle(q, cycle) == one) + { + order = temp; + } + else + { + break; + } + } + } + + return order; + } + + public static ulong CalculateOrderHybrid(this UInt128 q, PrimeOrderCalculatorAccelerator gpu) + { + if (q <= UInt128Numbers.Two) + { + return 0UL; + } + + UInt128 one = UInt128.One; + UInt128 phi = q - one; + if (phi > ulong.MaxValue) + { + throw new NotImplementedException("Such big values are not yet supported"); + } + + ulong order = (ulong)phi; + uint[] smallPrimes = PrimesGenerator.SmallPrimes; + ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2; + + int i = 0, primesLength = smallPrimes.Length; + UInt128 cycle = MersenneDivisorCycles.GetCycleHybrid(gpu, q); // TODO: If the cache lacks this cycle, immediately schedule the configured device // (GPU by default) to compute it on the fly and skip inserting it into the cache so // wide-order factoring can still leverage cycle stepping without breaching the diff --git a/PerfectNumbers.Core/ULongExtensions.Cpu.cs b/PerfectNumbers.Core/ULongExtensions.Cpu.cs index 0e14ffe..8d92b18 100644 --- a/PerfectNumbers.Core/ULongExtensions.Cpu.cs +++ b/PerfectNumbers.Core/ULongExtensions.Cpu.cs @@ -5,263 +5,329 @@ namespace PerfectNumbers.Core; public static partial class ULongExtensions { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ulong Pow2MontgomeryModWindowedCpu(this ulong exponent, in MontgomeryDivisorData divisor, bool keepMontgomery) - { - ulong modulus = divisor.Modulus; - // This should never happen in production code. - // if (exponent == 0UL) - // { - // return keepMontgomery ? divisor.MontgomeryOne : 1UL % modulus; - // } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Pow2MontgomeryModWindowedKeepMontgomeryCpu(this ulong exponent, in MontgomeryDivisorData divisor) + { + ulong modulus = divisor.Modulus; + // This should never happen in production code. + // if (exponent == 0UL) + // { + // return keepMontgomery ? divisor.MontgomeryOne : 1UL % modulus; + // } - // Small divisors come from the precomputed <= 4,000,000 snapshot and tiny rotation counts - // appear when cycle reductions trigger. Route both cases through the single-bit ladder so the - // CPU path avoids renting the window tables when a cheaper method is available. - if (exponent <= Pow2WindowFallbackThreshold || modulus <= (ulong)PerfectNumberConstants.MaxQForDivisorCycles) - { - return Pow2MontgomeryModSingleBit(exponent, divisor, keepMontgomery); - } + // Small divisors come from the precomputed <= 4,000,000 snapshot and tiny rotation counts + // appear when cycle reductions trigger. Route both cases through the single-bit ladder so the + // CPU path avoids renting the window tables when a cheaper method is available. + if (exponent <= Pow2WindowFallbackThreshold || modulus <= (ulong)PerfectNumberConstants.MaxQForDivisorCycles) + { + return Pow2MontgomeryModSingleBitKeepMontgomery(exponent, divisor); + } - int bitLength = GetPortableBitLengthCpu(exponent); - int windowSize = GetWindowSizeCpu(bitLength); - int oddPowerCount = 1 << (windowSize - 1); + int bitLength = GetPortableBitLengthCpu(exponent); + int windowSize = GetWindowSizeCpu(bitLength); + int oddPowerCount = 1 << (windowSize - 1); - ulong result = divisor.MontgomeryOne; - ulong nPrime = divisor.NPrime; + ulong result = divisor.MontgomeryOne; + ulong nPrime = divisor.NPrime; - ulong[] oddPowersArray = ThreadStaticPools.UlongPool.Rent(oddPowerCount); - Span oddPowers = oddPowersArray.AsSpan(0, oddPowerCount); - InitializeMontgomeryOddPowersCpu(divisor, modulus, nPrime, oddPowers); + ulong[] oddPowersArray = ThreadStaticPools.UlongPool.Rent(oddPowerCount); + Span oddPowers = oddPowersArray.AsSpan(0, oddPowerCount); + InitializeMontgomeryOddPowersCpu(divisor, modulus, nPrime, oddPowers); - int index = bitLength - 1; - while (index >= 0) - { - if (((exponent >> index) & 1UL) == 0UL) - { - result = result.MontgomeryMultiplyCpu(result, modulus, nPrime); - index--; - continue; - } + int index = bitLength - 1; + while (index >= 0) + { + if (((exponent >> index) & 1UL) == 0UL) + { + result = result.MontgomeryMultiplyCpu(result, modulus, nPrime); + index--; + continue; + } - int windowStart = index - windowSize + 1; - if (windowStart < 0) - { - windowStart = 0; - } + int windowStart = index - windowSize + 1; + if (windowStart < 0) + { + windowStart = 0; + } - while (((exponent >> windowStart) & 1UL) == 0UL) - { - windowStart++; - } + while (((exponent >> windowStart) & 1UL) == 0UL) + { + windowStart++; + } - int windowLength = index - windowStart + 1; - for (int square = 0; square < windowLength; square++) - { - result = result.MontgomeryMultiplyCpu(result, modulus, nPrime); - } + int windowLength = index - windowStart + 1; + for (int square = 0; square < windowLength; square++) + { + result = result.MontgomeryMultiplyCpu(result, modulus, nPrime); + } - ulong mask = (1UL << windowLength) - 1UL; - ulong windowValue = (exponent >> windowStart) & mask; - int tableIndex = (int)((windowValue - 1UL) >> 1); - ulong multiplier = oddPowers[tableIndex]; - result = result.MontgomeryMultiplyCpu(multiplier, modulus, nPrime); + ulong mask = (1UL << windowLength) - 1UL; + ulong windowValue = (exponent >> windowStart) & mask; + int tableIndex = (int)((windowValue - 1UL) >> 1); + ulong multiplier = oddPowers[tableIndex]; + result = result.MontgomeryMultiplyCpu(multiplier, modulus, nPrime); - index = windowStart - 1; - } + index = windowStart - 1; + } - ThreadStaticPools.UlongPool.Return(oddPowersArray); + ThreadStaticPools.UlongPool.Return(oddPowersArray); - if (!keepMontgomery) - { - result = result.MontgomeryMultiplyCpu(1UL, modulus, nPrime); - } + return result; + } - return result; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Pow2MontgomeryModWindowedConvertToStandardCpu(this ulong exponent, in MontgomeryDivisorData divisor) + { + ulong modulus = divisor.Modulus; + // This should never happen in production code. + // if (exponent == 0UL) + // { + // return keepMontgomery ? divisor.MontgomeryOne : 1UL % modulus; + // } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void InitializeMontgomeryOddPowersCpu(in MontgomeryDivisorData divisor, ulong modulus, ulong nPrime, Span oddPowers) - { - oddPowers[0] = divisor.MontgomeryTwo; - if (oddPowers.Length == 1) - { - return; - } + // Small divisors come from the precomputed <= 4,000,000 snapshot and tiny rotation counts + // appear when cycle reductions trigger. Route both cases through the single-bit ladder so the + // CPU path avoids renting the window tables when a cheaper method is available. + if (exponent <= Pow2WindowFallbackThreshold || modulus <= (ulong)PerfectNumberConstants.MaxQForDivisorCycles) + { + return Pow2MontgomeryModSingleBitConvertToStandard(exponent, divisor); + } - ulong previous; - ulong square = divisor.MontgomeryTwoSquared; + int bitLength = GetPortableBitLengthCpu(exponent); + int windowSize = GetWindowSizeCpu(bitLength); + int oddPowerCount = 1 << (windowSize - 1); - for (int i = 1; i < oddPowers.Length; i++) - { - previous = oddPowers[i - 1]; - oddPowers[i] = previous.MontgomeryMultiplyCpu(square, modulus, nPrime); - } - } + ulong result = divisor.MontgomeryOne; + ulong nPrime = divisor.NPrime; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ulong Pow2MontgomeryModWithCycleCpu(this ulong exponent, ulong cycleLength, in MontgomeryDivisorData divisor) - { - ulong rotationCount = exponent % cycleLength; - return Pow2MontgomeryModWindowedCpu(rotationCount, divisor, keepMontgomery: false); - } + ulong[] oddPowersArray = ThreadStaticPools.UlongPool.Rent(oddPowerCount); + Span oddPowers = oddPowersArray.AsSpan(0, oddPowerCount); + InitializeMontgomeryOddPowersCpu(divisor, modulus, nPrime, oddPowers); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ulong Pow2MontgomeryModFromCycleRemainderCpu(this ulong reducedExponent, in MontgomeryDivisorData divisor) - { - return Pow2MontgomeryModWindowedCpu(reducedExponent, divisor, keepMontgomery: false); - } + int index = bitLength - 1; + while (index >= 0) + { + if (((exponent >> index) & 1UL) == 0UL) + { + result = result.MontgomeryMultiplyCpu(result, modulus, nPrime); + index--; + continue; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ulong CalculateMersenneDivisorCycleLengthUnrolledHexCpu(this ulong divisor) - { - // EvenPerfectBitScanner only routes odd divisors here; keep the guard commented out for benchmarks. - // if ((divisor & (divisor - 1UL)) == 0UL) - // { - // return 1UL; - // } + int windowStart = index - windowSize + 1; + if (windowStart < 0) + { + windowStart = 0; + } - ulong order = 1UL; - ulong pow = 2UL; + while (((exponent >> windowStart) & 1UL) == 0UL) + { + windowStart++; + } - while (true) - { - if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) - { - return order; - } + int windowLength = index - windowStart + 1; + for (int square = 0; square < windowLength; square++) + { + result = result.MontgomeryMultiplyCpu(result, modulus, nPrime); + } - if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) - { - return order; - } + ulong mask = (1UL << windowLength) - 1UL; + ulong windowValue = (exponent >> windowStart) & mask; + int tableIndex = (int)((windowValue - 1UL) >> 1); + ulong multiplier = oddPowers[tableIndex]; + result = result.MontgomeryMultiplyCpu(multiplier, modulus, nPrime); - if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) - { - return order; - } + index = windowStart - 1; + } - if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) - { - return order; - } + ThreadStaticPools.UlongPool.Return(oddPowersArray); - if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) - { - return order; - } + result = result.MontgomeryMultiplyCpu(1UL, modulus, nPrime); + return result; + } - if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) - { - return order; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InitializeMontgomeryOddPowersCpu(in MontgomeryDivisorData divisor, ulong modulus, ulong nPrime, Span oddPowers) + { + oddPowers[0] = divisor.MontgomeryTwo; + if (oddPowers.Length == 1) + { + return; + } - if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) - { - return order; - } + ulong previous; + ulong square = divisor.MontgomeryTwoSquared; - if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) - { - return order; - } + for (int i = 1; i < oddPowers.Length; i++) + { + previous = oddPowers[i - 1]; + oddPowers[i] = previous.MontgomeryMultiplyCpu(square, modulus, nPrime); + } + } - if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) - { - return order; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Pow2MontgomeryModWithCycleConvertToStandardCpu(this ulong exponent, ulong cycleLength, in MontgomeryDivisorData divisor) + { + ulong rotationCount = exponent % cycleLength; + return Pow2MontgomeryModWindowedConvertToStandardCpu(rotationCount, divisor); + } - if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) - { - return order; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Pow2MontgomeryModFromCycleRemainderCpu(this ulong reducedExponent, in MontgomeryDivisorData divisor) + { + return Pow2MontgomeryModWindowedConvertToStandardCpu(reducedExponent, divisor); + } - if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) - { - return order; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong CalculateMersenneDivisorCycleLengthUnrolledHexCpu(this ulong divisor) + { + // EvenPerfectBitScanner only routes odd divisors here; keep the guard commented out for benchmarks. + // if ((divisor & (divisor - 1UL)) == 0UL) + // { + // return 1UL; + // } - if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) - { - return order; - } + ulong order = 1UL; + ulong pow = 2UL; - if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) - { - return order; - } + while (true) + { + if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) + { + return order; + } - if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) - { - return order; - } + if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) + { + return order; + } - if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) - { - return order; - } + if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) + { + return order; + } - if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) - { - return order; - } - } - } + if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) + { + return order; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool AdvanceMersenneDivisorCycleStepCpu(ref ulong pow, ulong divisor, ref ulong order) - { - pow += pow; - if (pow >= divisor) - { - pow -= divisor; - } + if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) + { + return order; + } - order++; - return pow == 1UL; - } + if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) + { + return order; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetPortableBitLengthCpu(ulong value) - { - // Keep this commented out. It will never happen in production code. - // if (value == 0UL) - // { - // return 0; - // } + if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) + { + return order; + } - return 64 - BitOperations.LeadingZeroCount(value); - } + if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) + { + return order; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetWindowSizeCpu(int bitLength) - { - if (bitLength <= 6) - { - return Math.Max(bitLength, 1); - } + if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) + { + return order; + } - if (bitLength <= 23) - { - return 4; - } + if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) + { + return order; + } - if (bitLength <= 79) - { - return 5; - } + if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) + { + return order; + } - if (bitLength <= 239) - { - return 6; - } + if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) + { + return order; + } - if (bitLength <= 671) - { - return 7; - } + if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) + { + return order; + } - return PerfectNumberConstants.Pow2WindowSize; - } + if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) + { + return order; + } + + if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) + { + return order; + } + + if (AdvanceMersenneDivisorCycleStepCpu(ref pow, divisor, ref order)) + { + return order; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool AdvanceMersenneDivisorCycleStepCpu(ref ulong pow, ulong divisor, ref ulong order) + { + pow += pow; + if (pow >= divisor) + { + pow -= divisor; + } + + order++; + return pow == 1UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetPortableBitLengthCpu(ulong value) + { + // Keep this commented out. It will never happen in production code. + // if (value == 0UL) + // { + // return 0; + // } + + return 64 - BitOperations.LeadingZeroCount(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetWindowSizeCpu(int bitLength) + { + if (bitLength <= 6) + { + return Math.Max(bitLength, 1); + } + + if (bitLength <= 23) + { + return 4; + } + + if (bitLength <= 79) + { + return 5; + } + + if (bitLength <= 239) + { + return 6; + } + + if (bitLength <= 671) + { + return 7; + } + + return PerfectNumberConstants.Pow2WindowSize; + } } diff --git a/PerfectNumbers.Core/ULongExtensions.cs b/PerfectNumbers.Core/ULongExtensions.cs index 85187bf..a759a27 100644 --- a/PerfectNumbers.Core/ULongExtensions.cs +++ b/PerfectNumbers.Core/ULongExtensions.cs @@ -99,7 +99,7 @@ public static partial class ULongExtensions } } - public static ulong CalculateOrder(this ulong q, PrimeOrderCalculatorAccelerator gpu) + public static ulong CalculateOrderCpu(this ulong q) { if (q <= 2UL) { @@ -112,7 +112,101 @@ public static partial class ULongExtensions int i = 0, primesLength = smallPrimes.Length; UInt128 q128 = q, - cycle = MersenneDivisorCycles.GetCycle(gpu, q128); + cycle = MersenneDivisorCycles.GetCycleCpu(q128); + // TODO: When the shared cycle snapshot cannot serve this divisor, trigger an on-demand + // GPU computation (respecting the configured device) without promoting the result into + // the cache so the order calculator still benefits from cycle stepping while keeping the + // single-block memory plan intact. + + for (; i < primesLength; i++) + { + if (smallPrimesPow2[i] > order) + { + break; + } + + prime = smallPrimes[i]; + // TODO: Replace this `%` driven factor peeling with the divisor-cycle aware + // factoring helper so large orders reuse the cached remainders highlighted in + // the latest divisor-cycle benchmarks instead of recomputing slow modulo checks. + while (order % prime == 0UL) + { + temp = order / prime; + if (temp.PowModWithCycle(q128, cycle) == UInt128.One) + { + order = temp; + } + else + { + break; + } + } + } + + return order; + } + + public static ulong CalculateOrderGpu(this ulong q, PrimeOrderCalculatorAccelerator gpu) + { + if (q <= 2UL) + { + return 0UL; + } + + ulong order = q - 1UL, prime, temp; + uint[] smallPrimes = PrimesGenerator.SmallPrimes; + ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2; + + int i = 0, primesLength = smallPrimes.Length; + UInt128 q128 = q, + cycle = MersenneDivisorCycles.GetCycleGpu(gpu, q128); + // TODO: When the shared cycle snapshot cannot serve this divisor, trigger an on-demand + // GPU computation (respecting the configured device) without promoting the result into + // the cache so the order calculator still benefits from cycle stepping while keeping the + // single-block memory plan intact. + + for (; i < primesLength; i++) + { + if (smallPrimesPow2[i] > order) + { + break; + } + + prime = smallPrimes[i]; + // TODO: Replace this `%` driven factor peeling with the divisor-cycle aware + // factoring helper so large orders reuse the cached remainders highlighted in + // the latest divisor-cycle benchmarks instead of recomputing slow modulo checks. + while (order % prime == 0UL) + { + temp = order / prime; + if (temp.PowModWithCycle(q128, cycle) == UInt128.One) + { + order = temp; + } + else + { + break; + } + } + } + + return order; + } + + public static ulong CalculateOrderHybrid(this ulong q, PrimeOrderCalculatorAccelerator gpu) + { + if (q <= 2UL) + { + return 0UL; + } + + ulong order = q - 1UL, prime, temp; + uint[] smallPrimes = PrimesGenerator.SmallPrimes; + ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2; + + int i = 0, primesLength = smallPrimes.Length; + UInt128 q128 = q, + cycle = MersenneDivisorCycles.GetCycleHybrid(gpu, q128); // TODO: When the shared cycle snapshot cannot serve this divisor, trigger an on-demand // GPU computation (respecting the configured device) without promoting the result into // the cache so the order calculator still benefits from cycle stepping while keeping the @@ -542,13 +636,13 @@ public static partial class ULongExtensions return result; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - // Reuse the direct UInt128 reduction path that dominated MulMod64Benchmarks to avoid - // the redundant operand modulo operations in the older fallback. - public static ulong MulMod64(this ulong a, ulong b, ulong modulus) - { - return (ulong)(((UInt128)a * b) % modulus); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + // Reuse the direct UInt128 reduction path that dominated MulMod64Benchmarks to avoid + // the redundant operand modulo operations in the older fallback. + public static ulong MulMod64(this ulong a, ulong b, ulong modulus) + { + return (ulong)(((UInt128)a * b) % modulus); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong MontgomeryMultiplyCpu(this ulong a, ulong b, ulong modulus, ulong nPrime) @@ -566,8 +660,8 @@ public static partial class ULongExtensions return result; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ulong Pow2MontgomeryModSingleBit(ulong exponent, in MontgomeryDivisorData divisor, bool keepMontgomery) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Pow2MontgomeryModSingleBitKeepMontgomery(ulong exponent, in MontgomeryDivisorData divisor) { ulong modulus = divisor.Modulus; ulong nPrime = divisor.NPrime; @@ -591,268 +685,295 @@ public static partial class ULongExtensions baseVal = baseVal.MontgomeryMultiplyCpu(baseVal, modulus, nPrime); } - return keepMontgomery ? result : result.MontgomeryMultiplyCpu(1UL, modulus, nPrime); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Pow2MontgomeryModSingleBitConvertToStandard(ulong exponent, in MontgomeryDivisorData divisor) + { + ulong modulus = divisor.Modulus; + ulong nPrime = divisor.NPrime; + ulong result = divisor.MontgomeryOne; + ulong baseVal = divisor.MontgomeryTwo; + ulong remainingExponent = exponent; + + while (remainingExponent != 0UL) + { + if ((remainingExponent & 1UL) != 0UL) + { + result = result.MontgomeryMultiplyCpu(baseVal, modulus, nPrime); + } + + remainingExponent >>= 1; + if (remainingExponent == 0UL) + { + break; + } + + baseVal = baseVal.MontgomeryMultiplyCpu(baseVal, modulus, nPrime); + } + + return result.MontgomeryMultiplyCpu(1UL, modulus, nPrime); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static UInt128 PowMod(this ulong exponent, UInt128 modulus) + { + // EvenPerfectBitScanner starts CPU scans at p >= 138,000,000 (Program.cs), + // so production never routes exponent == 0 here. Keep the guard commented out + // for targeted micro-benchmarks. + // if (exponent == 0UL) + // { + // return UInt128.One; + // } + + // MersenneResidueAutomaton seeds q = 2 * p * k + 1 (ModuloAutomata.cs), + // making modulus >= 2 * p + 1 > 276,000,000 and odd on the CPU path. The + // modulus == 1 guard is therefore unreachable during EvenPerfectBitScanner + // runs. + // if (modulus == UInt128.One) + // { + // return UInt128.Zero; + // } + + // Production exponents stay above 64 bits and modulus never drops below 4 + // for admissible divisors, so the classic doubling fallback does not + // trigger in scans. + // if (exponent < 64 || modulus < 4) + // { + // return Pow2ByDoubling(exponent, modulus); + // } + + // Every admissible divisor q is odd (2 * p * k + 1), preventing the + // power-of-two mask path from activating in production. + // if ((modulus & (modulus - 1)) == 0) + // { + // UInt128 masked = UInt128.One << (int)(exponent & 127); + // return masked & (modulus - 1); + // } + + if (!ShouldUseWindowedPow2(modulus)) + { + return Pow2ByDoubling(exponent, modulus); + } + + return exponent.Pow2MontgomeryModWindowed(modulus); } + /// + /// Computes 2^exponent mod modulus using a known cycle length. + /// + + public static UInt128 PowModWithCycle(this ulong exponent, UInt128 modulus, ulong cycleLength) + { + // EvenPerfectBitScanner keeps exponent >= 138,000,000 on CPU (Program.cs), + // so the zero-exponent guard stays disabled outside tests. + // if (exponent == 0UL) + // { + // return UInt128.One; + // } + + // The divisor generator produces odd q = 2 * p * k + 1 (ModuloAutomata.cs), + // so modulus never reaches 1 in production. + // if (modulus == UInt128.One) + // { + // return UInt128.Zero; + // } + + // Every admissible divisor is odd, so the power-of-two shortcut cannot + // trigger while scanning real candidates. + // if ((modulus & (modulus - 1)) == 0) + // { + // UInt128 masked = UInt128.One << (int)(exponent & 127); + // return masked & (modulus - 1); + // } + + bool hasCycle = cycleLength != 0UL; + // TODO: Replace this modulo with the cached cycle remainder produced by the divisor-cycle cache so PowModWithCycle avoids + // repeated `%` work, matching the ProcessEightBitWindows wins captured in Pow2MontgomeryModCycleComputationBenchmarks. + ulong rotationCount = hasCycle && exponent >= cycleLength ? exponent % cycleLength : exponent; + + // Keep this fast path: valid divisors satisfy cycle | exponent, producing + // a zero remainder that means 2^exponent ≡ 1 (mod q). + if (hasCycle && rotationCount == 0UL) + { + return UInt128.One; + } + + // FastDiv64 can return tiny divisors (see MersenneNumberIncrementalCpuTester), + // so the small-rotation fallback still fires on the CPU path. + if (modulus < 4 || rotationCount < 64) + { + return Pow2ByDoubling(rotationCount, modulus); + } + + if (ShouldUseWindowedPow2(modulus)) + { + UInt128 exponent128 = exponent; + UInt128 rotationCount128 = rotationCount; + return exponent128.Pow2MontgomeryModWindowedWithCycle(modulus, cycleLength, rotationCount128); + } + + return Pow2ByDoubling(rotationCount, modulus); + } + + + + + /// + /// Computes 2^exponent mod modulus using a known cycle length. + /// + + public static UInt128 PowModWithCycle(this ulong exponent, UInt128 modulus, UInt128 cycleLength) + { + // CPU scans only feed exponents >= 138,000,000 (Program.cs), so keep the + // zero-exponent shortcut commented out for tests. + // if (exponent == 0UL) + // { + // return UInt128.One; + // } + + // Modulus originates from q = 2 * p * k + 1 (ModuloAutomata.cs), therefore + // modulus == 1 never occurs outside dedicated benchmarks. + // if (modulus == UInt128.One) + // { + // return UInt128.Zero; + // } + + // Admissible q are odd, rendering the power-of-two shortcut unreachable. + // if ((modulus & (modulus - UInt128.One)) == UInt128.Zero) + // { + // UInt128 masked = UInt128.One << (int)(exponent & UInt128Numbers.OneHundredTwentySeven); + // return masked & (modulus - UInt128.One); + // } + + bool hasCycle = cycleLength != UInt128.Zero; + UInt128 exponent128 = exponent; + // TODO: Swap this modulo with the upcoming UInt128 cycle remainder helper so large-exponent scans reuse cached + // reductions instead of recomputing `%` for every lookup, as demonstrated in Pow2MontgomeryModCycleComputationBenchmarks. + UInt128 rotationCount = hasCycle && exponent128 >= cycleLength ? exponent128 % cycleLength : exponent128; + + if (hasCycle && rotationCount == UInt128.Zero) + { + return UInt128.One; + } + + if (modulus < UInt128Numbers.Four || rotationCount < UInt128Numbers.SixtyFour) + { + return Pow2ByDoubling(rotationCount, modulus); + } + + if (ShouldUseWindowedPow2(modulus)) + { + return exponent128.Pow2MontgomeryModWindowedWithCycle(modulus, cycleLength, rotationCount); + } + + return Pow2ByDoubling(rotationCount, modulus); + } + + + + + /// + /// Computes 2^exponent mod modulus using a known cycle length. + /// + + public static UInt128 PowModWithCycle(this UInt128 exponent, UInt128 modulus, UInt128 cycleLength) + { + UInt128 one = UInt128.One; + UInt128 zero = UInt128.Zero; + + // EvenPerfectBitScanner drives this path with exponents derived from p, phi, + // or divisor quotients, none of which are zero on the production scan. + // if (exponent == zero) + // { + // return one; + // } + + // Modulus again comes from q = 2 * p * k + 1, so modulus == 1 is unreachable. + // if (modulus == one) + // { + // return zero; + // } + + // Candidate divisors stay odd, therefore the power-of-two shortcut stays off. + // if ((modulus & (modulus - one)) == zero) + // { + // UInt128 masked = one << (int)(exponent & UInt128Numbers.OneHundredTwentySeven); + // return masked & (modulus - one); + // } + + bool hasCycle = cycleLength != zero; + // TODO: Swap this modulo with the shared UInt128 cycle remainder helper once available so CRT powmods reuse cached + // reductions in the windowed ladder, avoiding the `%` cost highlighted in Pow2MontgomeryModCycleComputationBenchmarks. + UInt128 rotationCount = hasCycle && exponent >= cycleLength ? exponent % cycleLength : exponent; + + if (hasCycle && rotationCount == zero) + { + return one; + } + + if (modulus < UInt128Numbers.Four || rotationCount < UInt128Numbers.SixtyFour) + { + return Pow2ByDoubling(rotationCount, modulus); + } + + if (ShouldUseWindowedPow2(modulus)) + { + return exponent.Pow2MontgomeryModWindowedWithCycle(modulus, cycleLength, rotationCount); + } + + return Pow2ByDoubling(rotationCount, modulus); + } + + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static UInt128 PowMod(this ulong exponent, UInt128 modulus) - { - // EvenPerfectBitScanner starts CPU scans at p >= 138,000,000 (Program.cs), - // so production never routes exponent == 0 here. Keep the guard commented out - // for targeted micro-benchmarks. - // if (exponent == 0UL) - // { - // return UInt128.One; - // } + private static bool ShouldUseWindowedPow2(UInt128 modulus) => modulus > Pow2WindowedModulusThreshold; - // MersenneResidueAutomaton seeds q = 2 * p * k + 1 (ModuloAutomata.cs), - // making modulus >= 2 * p + 1 > 276,000,000 and odd on the CPU path. The - // modulus == 1 guard is therefore unreachable during EvenPerfectBitScanner - // runs. - // if (modulus == UInt128.One) - // { - // return UInt128.Zero; - // } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static UInt128 Pow2ByDoubling(ulong exponent, UInt128 modulus) + { + UInt128 result = UInt128.One; + for (ulong i = 0; i < exponent; i++) + { + result <<= 1; + while (result >= modulus) + { + result -= modulus; + } + } - // Production exponents stay above 64 bits and modulus never drops below 4 - // for admissible divisors, so the classic doubling fallback does not - // trigger in scans. - // if (exponent < 64 || modulus < 4) - // { - // return Pow2ByDoubling(exponent, modulus); - // } + return result; + } - // Every admissible divisor q is odd (2 * p * k + 1), preventing the - // power-of-two mask path from activating in production. - // if ((modulus & (modulus - 1)) == 0) - // { - // UInt128 masked = UInt128.One << (int)(exponent & 127); - // return masked & (modulus - 1); - // } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static UInt128 Pow2ByDoubling(UInt128 exponent, UInt128 modulus) + { + UInt128 result = UInt128.One; + UInt128 index = UInt128.Zero; - if (!ShouldUseWindowedPow2(modulus)) - { - return Pow2ByDoubling(exponent, modulus); - } + while (index < exponent) + { + result <<= 1; + while (result >= modulus) + { + result -= modulus; + } - return exponent.Pow2MontgomeryModWindowed(modulus); - } + index += UInt128.One; + } - - /// - /// Computes 2^exponent mod modulus using a known cycle length. - /// - - public static UInt128 PowModWithCycle(this ulong exponent, UInt128 modulus, ulong cycleLength) - { - // EvenPerfectBitScanner keeps exponent >= 138,000,000 on CPU (Program.cs), - // so the zero-exponent guard stays disabled outside tests. - // if (exponent == 0UL) - // { - // return UInt128.One; - // } - - // The divisor generator produces odd q = 2 * p * k + 1 (ModuloAutomata.cs), - // so modulus never reaches 1 in production. - // if (modulus == UInt128.One) - // { - // return UInt128.Zero; - // } - - // Every admissible divisor is odd, so the power-of-two shortcut cannot - // trigger while scanning real candidates. - // if ((modulus & (modulus - 1)) == 0) - // { - // UInt128 masked = UInt128.One << (int)(exponent & 127); - // return masked & (modulus - 1); - // } - - bool hasCycle = cycleLength != 0UL; - // TODO: Replace this modulo with the cached cycle remainder produced by the divisor-cycle cache so PowModWithCycle avoids - // repeated `%` work, matching the ProcessEightBitWindows wins captured in Pow2MontgomeryModCycleComputationBenchmarks. - ulong rotationCount = hasCycle && exponent >= cycleLength ? exponent % cycleLength : exponent; - - // Keep this fast path: valid divisors satisfy cycle | exponent, producing - // a zero remainder that means 2^exponent ≡ 1 (mod q). - if (hasCycle && rotationCount == 0UL) - { - return UInt128.One; - } - - // FastDiv64 can return tiny divisors (see MersenneNumberIncrementalCpuTester), - // so the small-rotation fallback still fires on the CPU path. - if (modulus < 4 || rotationCount < 64) - { - return Pow2ByDoubling(rotationCount, modulus); - } - - if (ShouldUseWindowedPow2(modulus)) - { - UInt128 exponent128 = exponent; - UInt128 rotationCount128 = rotationCount; - return exponent128.Pow2MontgomeryModWindowedWithCycle(modulus, cycleLength, rotationCount128); - } - - return Pow2ByDoubling(rotationCount, modulus); - } - - - - - /// - /// Computes 2^exponent mod modulus using a known cycle length. - /// - - public static UInt128 PowModWithCycle(this ulong exponent, UInt128 modulus, UInt128 cycleLength) - { - // CPU scans only feed exponents >= 138,000,000 (Program.cs), so keep the - // zero-exponent shortcut commented out for tests. - // if (exponent == 0UL) - // { - // return UInt128.One; - // } - - // Modulus originates from q = 2 * p * k + 1 (ModuloAutomata.cs), therefore - // modulus == 1 never occurs outside dedicated benchmarks. - // if (modulus == UInt128.One) - // { - // return UInt128.Zero; - // } - - // Admissible q are odd, rendering the power-of-two shortcut unreachable. - // if ((modulus & (modulus - UInt128.One)) == UInt128.Zero) - // { - // UInt128 masked = UInt128.One << (int)(exponent & UInt128Numbers.OneHundredTwentySeven); - // return masked & (modulus - UInt128.One); - // } - - bool hasCycle = cycleLength != UInt128.Zero; - UInt128 exponent128 = exponent; - // TODO: Swap this modulo with the upcoming UInt128 cycle remainder helper so large-exponent scans reuse cached - // reductions instead of recomputing `%` for every lookup, as demonstrated in Pow2MontgomeryModCycleComputationBenchmarks. - UInt128 rotationCount = hasCycle && exponent128 >= cycleLength ? exponent128 % cycleLength : exponent128; - - if (hasCycle && rotationCount == UInt128.Zero) - { - return UInt128.One; - } - - if (modulus < UInt128Numbers.Four || rotationCount < UInt128Numbers.SixtyFour) - { - return Pow2ByDoubling(rotationCount, modulus); - } - - if (ShouldUseWindowedPow2(modulus)) - { - return exponent128.Pow2MontgomeryModWindowedWithCycle(modulus, cycleLength, rotationCount); - } - - return Pow2ByDoubling(rotationCount, modulus); - } - - - - - /// - /// Computes 2^exponent mod modulus using a known cycle length. - /// - - public static UInt128 PowModWithCycle(this UInt128 exponent, UInt128 modulus, UInt128 cycleLength) - { - UInt128 one = UInt128.One; - UInt128 zero = UInt128.Zero; - - // EvenPerfectBitScanner drives this path with exponents derived from p, phi, - // or divisor quotients, none of which are zero on the production scan. - // if (exponent == zero) - // { - // return one; - // } - - // Modulus again comes from q = 2 * p * k + 1, so modulus == 1 is unreachable. - // if (modulus == one) - // { - // return zero; - // } - - // Candidate divisors stay odd, therefore the power-of-two shortcut stays off. - // if ((modulus & (modulus - one)) == zero) - // { - // UInt128 masked = one << (int)(exponent & UInt128Numbers.OneHundredTwentySeven); - // return masked & (modulus - one); - // } - - bool hasCycle = cycleLength != zero; - // TODO: Swap this modulo with the shared UInt128 cycle remainder helper once available so CRT powmods reuse cached - // reductions in the windowed ladder, avoiding the `%` cost highlighted in Pow2MontgomeryModCycleComputationBenchmarks. - UInt128 rotationCount = hasCycle && exponent >= cycleLength ? exponent % cycleLength : exponent; - - if (hasCycle && rotationCount == zero) - { - return one; - } - - if (modulus < UInt128Numbers.Four || rotationCount < UInt128Numbers.SixtyFour) - { - return Pow2ByDoubling(rotationCount, modulus); - } - - if (ShouldUseWindowedPow2(modulus)) - { - return exponent.Pow2MontgomeryModWindowedWithCycle(modulus, cycleLength, rotationCount); - } - - return Pow2ByDoubling(rotationCount, modulus); - } - - - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool ShouldUseWindowedPow2(UInt128 modulus) => modulus > Pow2WindowedModulusThreshold; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static UInt128 Pow2ByDoubling(ulong exponent, UInt128 modulus) - { - UInt128 result = UInt128.One; - for (ulong i = 0; i < exponent; i++) - { - result <<= 1; - while (result >= modulus) - { - result -= modulus; - } - } - - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static UInt128 Pow2ByDoubling(UInt128 exponent, UInt128 modulus) - { - UInt128 result = UInt128.One; - UInt128 index = UInt128.Zero; - - while (index < exponent) - { - result <<= 1; - while (result >= modulus) - { - result -= modulus; - } - - index += UInt128.One; - } - - return result; - } + return result; + } /// /// Computes 2^exponent mod modulus using iterative CRT composition from mod 10 up to modulus. /// Only for modulus >= 10 and reasonable size. /// - public static UInt128 PowModCrt(this ulong exponent, PrimeOrderCalculatorAccelerator gpu, UInt128 modulus, MersenneDivisorCycles cycles) + public static UInt128 PowModCrtCpu(this ulong exponent, UInt128 modulus) { if (modulus < 10) return PowMod(exponent, modulus); // fallback to classic @@ -867,7 +988,95 @@ public static partial class ULongExtensions for (; modulusCandidate <= modulus; modulusCandidate++) { - cycle = MersenneDivisorCycles.GetCycle(gpu, modulusCandidate); + cycle = MersenneDivisorCycles.GetCycleCpu(modulusCandidate); + remainderForCandidate = cycle > zero + ? PowModWithCycle(exponent, modulusCandidate, cycle) + : PowMod(exponent, modulusCandidate); + + // Solve x ≡ result mod currentModulus + // x ≡ remM mod m + // Find x mod (currentModulus * m) + // Since currentModulus and m are coprime, use CRT: + // x = result + currentModulus * t, where t ≡ (remM - result) * inv(currentModulus, m) mod m + + // TODO: Replace this `% modulusCandidate` with the cached residue helper derived from Mod10_8_5_3Benchmarks so CRT + // composition avoids repeated modulo divisions when combining residues for large divisor sets. + result += currentModulus * ((remainderForCandidate + modulusCandidate - (result % modulusCandidate)) * ModInverse(currentModulus, modulusCandidate) % modulusCandidate); + currentModulus *= modulusCandidate; + + if (currentModulus >= modulus) + break; + } + + // TODO: Swap this final `% modulus` with the pooled remainder cache so the CRT result write-back avoids one more division, + // aligning with the optimizations captured in Mod10_8_5_3Benchmarks. + return result % modulus; + } + + /// + /// Computes 2^exponent mod modulus using iterative CRT composition from mod 10 up to modulus. + /// Only for modulus >= 10 and reasonable size. + /// + public static UInt128 PowModCrtGpu(this ulong exponent, PrimeOrderCalculatorAccelerator gpu, UInt128 modulus) + { + if (modulus < 10) + return PowMod(exponent, modulus); // fallback to classic + + // Use cycle length 4 for mod 10 + UInt128 currentModulus = 10, + cycle, + modulusCandidate = 11, + remainderForCandidate, + result = PowModWithCycle(exponent, 10, 4), + zero = UInt128.Zero; + + for (; modulusCandidate <= modulus; modulusCandidate++) + { + cycle = MersenneDivisorCycles.GetCycleGpu(gpu, modulusCandidate); + remainderForCandidate = cycle > zero + ? PowModWithCycle(exponent, modulusCandidate, cycle) + : PowMod(exponent, modulusCandidate); + + // Solve x ≡ result mod currentModulus + // x ≡ remM mod m + // Find x mod (currentModulus * m) + // Since currentModulus and m are coprime, use CRT: + // x = result + currentModulus * t, where t ≡ (remM - result) * inv(currentModulus, m) mod m + + // TODO: Replace this `% modulusCandidate` with the cached residue helper derived from Mod10_8_5_3Benchmarks so CRT + // composition avoids repeated modulo divisions when combining residues for large divisor sets. + result += currentModulus * ((remainderForCandidate + modulusCandidate - (result % modulusCandidate)) * ModInverse(currentModulus, modulusCandidate) % modulusCandidate); + currentModulus *= modulusCandidate; + + if (currentModulus >= modulus) + break; + } + + // TODO: Swap this final `% modulus` with the pooled remainder cache so the CRT result write-back avoids one more division, + // aligning with the optimizations captured in Mod10_8_5_3Benchmarks. + return result % modulus; + } + + /// + /// Computes 2^exponent mod modulus using iterative CRT composition from mod 10 up to modulus. + /// Only for modulus >= 10 and reasonable size. + /// + public static UInt128 PowModCrtHybrid(this ulong exponent, PrimeOrderCalculatorAccelerator gpu, UInt128 modulus) + { + if (modulus < 10) + return PowMod(exponent, modulus); // fallback to classic + + // Use cycle length 4 for mod 10 + UInt128 currentModulus = 10, + cycle, + modulusCandidate = 11, + remainderForCandidate, + result = PowModWithCycle(exponent, 10, 4), + zero = UInt128.Zero; + + for (; modulusCandidate <= modulus; modulusCandidate++) + { + cycle = MersenneDivisorCycles.GetCycleHybrid(gpu, modulusCandidate); remainderForCandidate = cycle > zero ? PowModWithCycle(exponent, modulusCandidate, cycle) : PowMod(exponent, modulusCandidate); @@ -927,7 +1136,7 @@ public static partial class ULongExtensions } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool SharesFactorWithExponentMinusOne(this ulong exponent, PrimeOrderCalculatorAccelerator gpu) + public static bool SharesFactorWithExponentMinusOneCpu(this ulong exponent) { ulong prime, value = exponent - 1UL; value >>= BitOperations.TrailingZeroCount(value); @@ -943,7 +1152,7 @@ public static partial class ULongExtensions continue; } - if (exponent % prime.CalculateOrder(gpu) == 0UL) + if (exponent % prime.CalculateOrderCpu() == 0UL) { return true; } @@ -955,7 +1164,81 @@ public static partial class ULongExtensions while (value % prime == 0UL); } - if (value > 1UL && exponent % value.CalculateOrder(gpu) == 0UL) + if (value > 1UL && exponent % value.CalculateOrderCpu() == 0UL) + { + return true; + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool SharesFactorWithExponentMinusOneGpu(this ulong exponent, PrimeOrderCalculatorAccelerator gpu) + { + ulong prime, value = exponent - 1UL; + value >>= BitOperations.TrailingZeroCount(value); + uint[] smallPrimes = PrimesGenerator.SmallPrimes; + ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2; + int i = 1, smallPrimesLength = smallPrimes.Length; + + for (; i < smallPrimesLength && smallPrimesPow2[i] <= value; i++) + { + prime = smallPrimes[i]; + if (value % prime != 0UL) + { + continue; + } + + if (exponent % prime.CalculateOrderGpu(gpu) == 0UL) + { + return true; + } + + do + { + value /= prime; + } + while (value % prime == 0UL); + } + + if (value > 1UL && exponent % value.CalculateOrderGpu(gpu) == 0UL) + { + return true; + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool SharesFactorWithExponentMinusOneHybrid(this ulong exponent, PrimeOrderCalculatorAccelerator gpu) + { + ulong prime, value = exponent - 1UL; + value >>= BitOperations.TrailingZeroCount(value); + uint[] smallPrimes = PrimesGenerator.SmallPrimes; + ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2; + int i = 1, smallPrimesLength = smallPrimes.Length; + + for (; i < smallPrimesLength && smallPrimesPow2[i] <= value; i++) + { + prime = smallPrimes[i]; + if (value % prime != 0UL) + { + continue; + } + + if (exponent % prime.CalculateOrderHybrid(gpu) == 0UL) + { + return true; + } + + do + { + value /= prime; + } + while (value % prime == 0UL); + } + + if (value > 1UL && exponent % value.CalculateOrderHybrid(gpu) == 0UL) { return true; }