Respect GPU configuration and reuse order factorization

This commit is contained in:
Marek Lemańczyk committed 2025-10-11 15:30:04 +02:00
1 parent 45183acd9b
commit fa74667f89
4 files changed
+120 -93

No files matched your search

@@ -7,6 +7,9 @@ namespace PerfectNumbers.Core.Tests;
public class PrimeOrderCalculatorTests
{
// EvenPerfectBitScanner never exercises the GPU heuristic device or the small-prime guards that the legacy tests cover,
// so those scenarios are commented out because they are unreachable in the production pipeline.
/*
public static IEnumerable<object[]> SmallPrimes()
{
yield return new object[] { 3UL };
@@ -197,4 +200,5 @@ public class PrimeOrderCalculatorTests
return order;
}
*/
}
@@ -232,6 +232,9 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
else
{
factorCache ??= new Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>(8);
// EvenPerfectBitScanner reaches this path only for divisors above the precomputed cycle snapshot.
// Those candidates follow q = 2 * prime * k + 1 with prime >= 5, so the downstream order calculator
// can rely on an odd divisor and a prime exponent when deriving the cycle length.
if (!MersenneDivisorCycles.TryCalculateCycleLengthForExponent(candidate, prime, divisorData, factorCache, out divisorCycle) || divisorCycle == 0UL)
{
divisorCycle = cycleCache.GetCycleLength(candidate);
+56 -35
View File
@@ -273,17 +273,24 @@ public class MersenneDivisorCycles
{
cycleLength = 0UL;
if ((divisor & (divisor - 1UL)) == 0UL)
{
cycleLength = 1UL;
return true;
}
// The by-divisor scanner only supplies odd candidates of the form 2*k*exponent + 1,
// so this power-of-two fast-path never triggers on that call path.
// The historical fast-path is left here for reference but commented out because it is unreachable
// on the EvenPerfectBitScanner pipeline.
// if ((divisor & (divisor - 1UL)) == 0UL)
// {
// cycleLength = 1UL;
// return true;
// }
if (divisor <= 3UL || exponent <= 1UL)
{
cycleLength = CalculateCycleLength(divisor, divisorData);
return true;
}
// EvenPerfectBitScanner filters exponents <= 3 before invoking this helper and each
// divisor candidate is at least 2 * exponent + 1, so this guard never matches.
// The fallback calculation is therefore unreachable on the active call path.
// if (divisor <= 3UL || exponent <= 1UL)
// {
// cycleLength = CalculateCycleLength(divisor, divisorData);
// return true;
// }
ulong computedOrder = PrimeOrderCalculator.Calculate(
divisor,
@@ -298,29 +305,38 @@ public class MersenneDivisorCycles
}
ulong phi = divisor - 1UL;
if (phi == 0UL)
{
return false;
}
// Divisors reach this helper only when they exceed the cached table, which means
// divisor >= 2 * exponent + 1 and phi cannot underflow to zero here.
// The defensive underflow guard is kept for posterity but disabled for the EvenPerfectBitScanner path.
// if (phi == 0UL)
// {
// return false;
// }
int twoCount = BitOperations.TrailingZeroCount(phi);
ulong reducedPhi = phi >> twoCount;
if (reducedPhi == 0UL || (reducedPhi % exponent) != 0UL)
{
return false;
}
// Every divisor candidate generated by the by-divisor scan satisfies q ≡ 1 (mod 2 * exponent),
// ensuring reducedPhi keeps a factor of exponent and remains non-zero in this context.
// The divisibility check is therefore unreachable for the current caller and is commented out.
// if (reducedPhi == 0UL || (reducedPhi % exponent) != 0UL)
// {
// return false;
// }
ulong k = reducedPhi / exponent;
if (k == 0UL)
{
return false;
}
// reducedPhi always contains at least one factor of exponent, so dividing by exponent leaves k >= 1.
// if (k == 0UL)
// {
// return false;
// }
Dictionary<ulong, int> factorCounts = new(8);
if (twoCount > 0)
{
// The divisor candidates are odd, so phi is even and this branch always records at least one factor of two.
// if (twoCount > 0)
// {
// factorCounts[2UL] = twoCount;
// }
factorCounts[2UL] = twoCount;
}
if (!AccumulateFactors(exponent, factorCache, factorCounts, cacheResult: true) || !AccumulateFactors(k, factorCache, factorCounts, cacheResult: false))
{
@@ -926,17 +942,22 @@ public class MersenneDivisorCycles
internal static bool TryCalculateCycleLengthHeuristic(ulong divisor, in MontgomeryDivisorData divisorData, out ulong cycleLength)
{
if ((divisor & (divisor - 1UL)) == 0UL)
{
cycleLength = 1UL;
return true;
}
// The by-divisor scanner only supplies odd candidates of the form 2*k*exponent + 1,
// so this power-of-two fast-path never triggers on that call path.
// The early exit is retained for reference but commented out because it is unreachable on the scanner path.
// if ((divisor & (divisor - 1UL)) == 0UL)
// {
// cycleLength = 1UL;
// return true;
// }
if (divisor <= 3UL)
{
cycleLength = CalculateCycleLengthFallback(divisor);
return true;
}
// By the divisor-by-divisor pipeline, candidates have the shape 2 * exponent * k + 1 with exponent >= 5,
// so the divisor never drops below 11 on this call path.
// if (divisor <= 3UL)
// {
// cycleLength = CalculateCycleLengthFallback(divisor);
// return true;
// }
if (PrimeTester.IsPrimeInternal(divisor, CancellationToken.None))
{
+57 -58
View File
@@ -135,13 +135,16 @@ internal static partial class PrimeOrderCalculator
private static ulong CalculateInternal(ulong prime, ulong? previousOrder, in MontgomeryDivisorData divisorData, in PrimeOrderSearchConfig config)
{
if (prime <= 3UL)
{
return prime == 3UL ? 2UL : 1UL;
}
// EvenPerfectBitScanner only reaches this helper with divisors of the form 2 * exponent * k + 1 where exponent >= 5,
// so the small-prime return path is unreachable in production.
// if (prime <= 3UL)
// {
// return prime == 3UL ? 2UL : 1UL;
// }
ulong phi = prime - 1UL;
// The by-divisor scanner currently defaults to the CPU heuristic device, but --mersenne-device may request GPU heuristics.
if (IsGpuHeuristicDevice && PrimeOrderGpuHeuristics.TryCalculateOrder(prime, previousOrder, config, divisorData, out ulong gpuOrder))
{
return gpuOrder;
@@ -150,12 +153,14 @@ internal static partial class PrimeOrderCalculator
PartialFactorResult phiFactors = PartialFactor(phi, config);
ulong result;
if (phiFactors.Factors is null)
{
result = FinishStrictlyCpu(prime, divisorData);
phiFactors.Dispose();
return result;
}
// The by-divisor scan always yields phi = prime - 1 with at least the factor 2 captured during small-prime sieving,
// so PartialFactor never returns a null factor list on this call path.
// if (phiFactors.Factors is null)
// {
// result = CalculateByFactorizationCpu(prime, divisorData);
// phiFactors.Dispose();
// return result;
// }
result = RunHeuristicPipelineCpu(prime, previousOrder, config, divisorData, phi, phiFactors);
phiFactors.Dispose();
@@ -178,29 +183,37 @@ internal static partial class PrimeOrderCalculator
ulong candidateOrder = InitializeStartingOrderCpu(prime, phi, divisorData);
candidateOrder = ExponentLoweringCpu(candidateOrder, prime, phiFactors, divisorData);
if (TryConfirmOrderCpu(prime, candidateOrder, divisorData, config))
PartialFactorResult orderFactors = PartialFactor(candidateOrder, config);
if (orderFactors.Factors is null)
{
orderFactors.Dispose();
return candidateOrder;
}
// Reuse the computed factorization for both the confirmation check and the heuristic pass
// to avoid factoring the same order twice on the by-divisor CPU pipeline.
if (TryConfirmOrderCpu(prime, candidateOrder, divisorData, ref orderFactors))
{
orderFactors.Dispose();
return candidateOrder;
}
if (config.Mode == PrimeOrderMode.Strict)
{
return FinishStrictlyCpu(prime, divisorData);
orderFactors.Dispose();
return CalculateByFactorizationCpu(prime, divisorData);
}
if (TryHeuristicFinishCpu(prime, candidateOrder, previousOrder, divisorData, config, phiFactors, out ulong order))
if (TryHeuristicFinishCpu(prime, candidateOrder, previousOrder, divisorData, config, ref orderFactors, phiFactors, out ulong order))
{
orderFactors.Dispose();
return order;
}
orderFactors.Dispose();
return candidateOrder;
}
private static ulong FinishStrictlyCpu(ulong prime, in MontgomeryDivisorData divisorData)
{
return CalculateByFactorizationCpu(prime, divisorData);
}
private static bool TrySpecialMaxCpu(ulong phi, ulong prime, PartialFactorResult factors, in MontgomeryDivisorData divisorData)
{
ReadOnlySpan<FactorEntry> factorSpan = factors.Factors;
@@ -297,12 +310,13 @@ internal static partial class PrimeOrderCalculator
}
}
private static bool TryConfirmOrderCpu(ulong prime, ulong order, in MontgomeryDivisorData divisorData, in PrimeOrderSearchConfig config)
private static bool TryConfirmOrderCpu(ulong prime, ulong order, in MontgomeryDivisorData divisorData, ref PartialFactorResult orderFactors)
{
if (order == 0UL)
{
return false;
}
// The candidate order is always a positive divisor of phi in this flow, so zero never appears here.
// if (order == 0UL)
// {
// return false;
// }
// Calculating `a^order ≡ 1 (mod p)` is a prerequisite for `order` being the actual order of 2 modulo `p`.
// DebugLog("Verifying a^order ≡ 1 (mod p)");
@@ -311,41 +325,37 @@ internal static partial class PrimeOrderCalculator
return false;
}
// DebugLog("Partial factoring order");
// TODO: Do we do partial factoring of order multiple times?
PartialFactorResult factorization = PartialFactor(order, config);
try
{
if (factorization.Factors is null)
// PartialFactor returns a null factor span when it cannot peel off any divisors, so keep the guard for composite candidate orders.
if (orderFactors.Factors is null)
{
return false;
}
if (!factorization.FullyFactored)
if (!orderFactors.FullyFactored)
{
if (factorization.Cofactor <= 1UL)
// A leftover cofactor of 0 or 1 indicates the partial factorization produced no useful prime divisor.
if (orderFactors.Cofactor <= 1UL)
{
return false;
}
// DebugLog("Cofactor > 1. Testing primality of cofactor");
bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(factorization.Cofactor);
// bool isPrime = PrimeTester.IsPrimeInternal(factorization.Cofactor, CancellationToken.None);
bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(orderFactors.Cofactor);
// bool isPrime = PrimeTester.IsPrimeInternal(orderFactors.Cofactor, CancellationToken.None);
if (!isPrime)
{
return false;
}
// DebugLog("Adding cofactor as prime factor");
PartialFactorResult extended = factorization.WithAdditionalPrime(factorization.Cofactor);
factorization.Dispose();
factorization = extended;
PartialFactorResult extended = orderFactors.WithAdditionalPrime(orderFactors.Cofactor);
orderFactors.Dispose();
orderFactors = extended;
}
ReadOnlySpan<FactorEntry> span = factorization.Factors;
ReadOnlySpan<FactorEntry> span = orderFactors.Factors!;
// DebugLog("Verifying prime-power reductions");
int length = factorization.Count;
int length = orderFactors.Count;
for (int i = 0; i < length; i++)
{
ulong primeFactor = span[i].Value;
@@ -367,11 +377,6 @@ internal static partial class PrimeOrderCalculator
return true;
}
finally
{
factorization.Dispose();
}
}
private static bool TryHeuristicFinishCpu(
ulong prime,
@@ -379,20 +384,18 @@ internal static partial class PrimeOrderCalculator
ulong? previousOrder,
in MontgomeryDivisorData divisorData,
in PrimeOrderSearchConfig config,
ref PartialFactorResult orderFactors,
PartialFactorResult phiFactors,
out ulong result)
{
result = 0UL;
if (order <= 1UL)
{
return false;
}
// The candidate order stays at least 2 for odd divisors above 3, so this guard never triggers in the EvenPerfectBitScanner flow.
// if (order <= 1UL)
// {
// return false;
// }
// TODO: Do we do partial factoring of order multiple times?
// DebugLog("Trying heuristic. Partial factoring order");
PartialFactorResult orderFactors = PartialFactor(order, config);
try
{
// PartialFactor returns a null factor span when it cannot peel off any divisors, so the heuristic pass must bail in that case.
if (orderFactors.Factors is null)
{
return false;
@@ -400,6 +403,7 @@ internal static partial class PrimeOrderCalculator
if (!orderFactors.FullyFactored)
{
// The partial factoring helper reports a cofactor of 0 or 1 when nothing usable remains, so short-circuit the heuristic stage.
if (orderFactors.Cofactor <= 1UL)
{
return false;
@@ -539,11 +543,6 @@ internal static partial class PrimeOrderCalculator
// DebugLog("No candidate confirmed");
return false;
}
finally
{
orderFactors.Dispose();
}
}
private static void SortCandidates(ulong prime, ulong? previousOrder, List<ulong> candidates)
{