Hybrid device mode implemented as the current --mersenne=bydivisor CPU execution path. CPU path is now limited to CPU calculations, only.

This commit is contained in:
mlemanczyk committed 2025-12-01 03:45:39 +01:00
1 parent 353a121d03
commit 941733905b
49 files changed
+4965 -1726

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
namespace PerfectNumbers.Core;
public enum ComputationDevice
{
Cpu,
Hybrid,
Gpu,
}
@@ -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<ulong, ulong> _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.
@@ -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)
{
@@ -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)
{
@@ -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;
@@ -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.
@@ -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;
+186 -15
View File
@@ -15,7 +15,6 @@ public sealed class DivisorCycleCache
private readonly int _acceleratorIndex;
private readonly Action<AcceleratorStream, Index1D, int, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<byte, Stride1D.Dense>> _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<ulong> result = stackalloc ulong[1];
ReadOnlySpan<ulong> singleDivisor = stackalloc ulong[1] { divisor };
GetCycleLengths(gpu, singleDivisor, result);
GetCycleLengthsCpu(singleDivisor, result);
return result[0];
}
@@ -90,12 +84,88 @@ public sealed class DivisorCycleCache
Queue<MontgomeryDivisorData> 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<ulong> divisors, Span<ulong> cycles)
public ulong GetCycleLengthHybrid(PrimeOrderCalculatorAccelerator gpu, ulong divisor, bool skipPrimeOrderHeuristic = false)
{
if (!skipPrimeOrderHeuristic)
{
Span<ulong> result = stackalloc ulong[1];
ReadOnlySpan<ulong> 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<MontgomeryDivisorData> 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<ulong> result = stackalloc ulong[1];
ReadOnlySpan<ulong> 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<MontgomeryDivisorData> 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<ulong> divisors, Span<ulong> cycles)
{
ulong[] snapshot = _snapshot;
int length = divisors.Length;
@@ -133,16 +203,103 @@ public sealed class DivisorCycleCache
if (missingCount != 0)
{
ReadOnlySpan<int> 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<ulong> divisors, Span<ulong> cycles)
{
ulong[] snapshot = _snapshot;
int length = divisors.Length;
int[]? rentedMissing = null;
Span<int> missingBuffer = length <= StackBufferThreshold
? stackalloc int[length]
: new Span<int>(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<int> missingIndices = missingBuffer[..missingCount];
ComputeCyclesHybrid(gpu, divisors, cycles, missingIndices);
}
if (rentedMissing is not null)
{
ThreadStaticPools.IntPool.Return(rentedMissing, clearArray: false);
}
}
public void GetCycleLengthsGpu(ReadOnlySpan<ulong> divisors, Span<ulong> cycles)
{
ulong[] snapshot = _snapshot;
int length = divisors.Length;
int[]? rentedMissing = null;
Span<int> missingBuffer = length <= StackBufferThreshold
? stackalloc int[length]
: new Span<int>(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<int> 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<ulong> divisors, Span<ulong> cycles, ReadOnlySpan<int> indices)
private static void ComputeCyclesCpu(ReadOnlySpan<ulong> divisors, Span<ulong> cycles, ReadOnlySpan<int> indices)
{
Queue<MontgomeryDivisorData> 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<ulong> divisors, Span<ulong> cycles, ReadOnlySpan<int> indices)
{
Queue<MontgomeryDivisorData> 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);
}
}
@@ -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;
/// <summary>
/// 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.
/// </summary>
public void Reset()
/// <summary>
/// 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.
/// </summary>
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);
}
@@ -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
{
@@ -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;
@@ -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;
}
@@ -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();
@@ -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)
@@ -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;
+43 -3
View File
@@ -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;
+541 -49
View File
@@ -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<MontgomeryDivisorData> 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<MontgomeryDivisorData> 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<MontgomeryDivisorData> 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<ulong, int> 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<ulong, int> 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<ulong, int> counts)
{
@@ -357,7 +789,7 @@ public class MersenneDivisorCycles
}
Dictionary<ulong, int> 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<ulong, int> counts)
private static bool TryFactorIntoCountsInternal(ulong value, Dictionary<ulong, int> 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;
+325 -20
View File
@@ -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<PrimeOrderCalculatorAccelerator, ulong, bool> _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<UInt128, ulong> _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<UInt128> 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<GpuUInt128, Stride1D.Dense>? qBuffer;
MemoryBuffer1D<ulong, Stride1D.Dense>? orderBuffer;
while (offset < idx)
{
int count = Math.Min(batchSize, idx - offset);
qBuffer = accelerator.Allocate1D<GpuUInt128>(count);
orderBuffer = accelerator.Allocate1D<ulong>(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<UInt128> 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<GpuUInt128, Stride1D.Dense>? qBuffer;
MemoryBuffer1D<ulong, Stride1D.Dense>? orderBuffer;
while (offset < idx)
{
int count = Math.Min(batchSize, idx - offset);
qBuffer = accelerator.Allocate1D<GpuUInt128>(count);
orderBuffer = accelerator.Allocate1D<ulong>(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;
}
}
+269 -12
View File
@@ -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<UInt128> 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<UInt128> 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<UInt128> divisors = _divisors;
@@ -97,7 +162,7 @@ public sealed class ModResidueTracker
List<UInt128> 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<UInt128> 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<UInt128> 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<UInt128> 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<UInt128> 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
}
}
File diff suppressed because it is too large. Load diff
+475 -4
View File
@@ -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<MontgomeryDivisorData> 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<UInt128, int> counts = new(capacity: 8);
FactorCompletelyWide(phi, counts);
if (counts.Count == 0)
{
return phi;
}
List<KeyValuePair<UInt128, int>> 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<FactorEntry128> 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<FactorEntry128> pool = ThreadStaticPools.FactorEntry128Pool;
ReadOnlySpan<FactorEntry128> 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<FactorEntry128> 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<FactorEntry128> 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<UInt128> 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<ulong, int> 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<FactorEntry128> 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<ulong> randomStateSpan = stackalloc ulong[1];
@@ -184,6 +656,5 @@ internal static partial class PrimeOrderCalculator
factor = factored ? factorSpan[0] : 0UL;
return factored;
}
}
@@ -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<MontgomeryDivisorData> 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;
}
}
@@ -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<string> 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<FactorEntry128> 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<FactorEntry128> pool = ThreadStaticPools.FactorEntry128Pool;
ReadOnlySpan<FactorEntry128> 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<FactorEntry128> 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<FactorEntry128> 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<UInt128> 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<UInt128> 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<FactorEntry128> 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<UInt128, int> counts = new(capacity: 8);
FactorCompletelyWide(phi, counts);
if (counts.Count == 0)
{
return phi;
}
List<KeyValuePair<UInt128, int>> 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<UInt128, int> 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<UInt128, int> counts, in UInt128 prime, int exponent)
{
if (counts.TryGetValue(prime, out int existing))
+5 -68
View File
@@ -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<MontgomeryDivisorData> 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;
}
}
-1
View File
@@ -69,7 +69,6 @@ namespace PerfectNumbers.Core
}
_mersenneCpuDivisorSession = null;
session.Reset();
return session;
}
+102 -2
View File
@@ -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
+278 -212
View File
@@ -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<ulong> oddPowers = oddPowersArray.AsSpan(0, oddPowerCount);
InitializeMontgomeryOddPowersCpu(divisor, modulus, nPrime, oddPowers);
ulong[] oddPowersArray = ThreadStaticPools.UlongPool.Rent(oddPowerCount);
Span<ulong> 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<ulong> 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<ulong> 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<ulong> 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;
}
}
+545 -262
View File
@@ -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);
}
/// <summary>
/// Computes 2^exponent mod modulus using a known cycle length.
/// </summary>
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);
}
/// <summary>
/// Computes 2^exponent mod modulus using a known cycle length.
/// </summary>
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);
}
/// <summary>
/// Computes 2^exponent mod modulus using a known cycle length.
/// </summary>
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;
}
/// <summary>
/// Computes 2^exponent mod modulus using a known cycle length.
/// </summary>
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);
}
/// <summary>
/// Computes 2^exponent mod modulus using a known cycle length.
/// </summary>
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);
}
/// <summary>
/// Computes 2^exponent mod modulus using a known cycle length.
/// </summary>
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;
}
/// <summary>
/// Computes 2^exponent mod modulus using iterative CRT composition from mod 10 up to modulus.
/// Only for modulus >= 10 and reasonable size.
/// </summary>
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;
}
/// <summary>
/// Computes 2^exponent mod modulus using iterative CRT composition from mod 10 up to modulus.
/// Only for modulus >= 10 and reasonable size.
/// </summary>
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;
}
/// <summary>
/// Computes 2^exponent mod modulus using iterative CRT composition from mod 10 up to modulus.
/// Only for modulus >= 10 and reasonable size.
/// </summary>
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;
}