Refine heuristic prime tester integration

This commit is contained in:
Marek Lemańczyk committed 2025-10-26 18:15:27 +01:00
1 parent 21d645fcf0
commit 1e5eadac34
10 files changed
+186 -263

No files matched your search

+128 -203
View File
@@ -71,7 +71,6 @@ public sealed class HeuristicPrimeTester
public readonly bool RequiresCycleComputation = !hasCycleLengthHint;
}
public static bool EnableHeuristicPrimeTesting { get; set; } = true;
public static int HeuristicGpuDivisorBatchSize { get; set; } = 4_096;
@@ -82,30 +81,25 @@ public sealed class HeuristicPrimeTester
public static HeuristicPrimeTester Exclusive => _tester ??= new();
public bool IsPrime(ulong n, CancellationToken ct)
public bool IsPrimeCpu(ulong n, CancellationToken ct)
{
if (!EnableHeuristicPrimeTesting)
ct.ThrowIfCancellationRequested();
byte nMod10 = (byte)n.Mod10();
ulong sqrtLimit = ComputeHeuristicSqrt(n);
if (sqrtLimit < 3UL)
{
return PrimeTester.IsPrimeInternal(n, ct);
return EvaluateWithOpenNumericFallback(n);
}
return HeuristicIsPrimeCpu(n);
bool includeGroupB = UseHeuristicGroupBTrialDivision;
return HeuristicTrialDivisionCpu(n, sqrtLimit, nMod10, includeGroupB);
}
public bool IsPrime(ulong n)
public bool IsPrimeCpu(ulong n)
{
return IsPrime(n, CancellationToken.None);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsPrimeGpu(ulong n, CancellationToken ct)
{
if (!EnableHeuristicPrimeTesting)
{
return PrimeTester.Exclusive.IsPrimeGpu(n, ct);
}
return IsPrimeGpu(n);
return IsPrimeCpu(n, CancellationToken.None);
}
public bool IsPrimeGpu(ulong n)
@@ -117,45 +111,9 @@ public sealed class HeuristicPrimeTester
public bool IsPrimeGpu(ulong n, ulong sqrtLimit, byte nMod10)
{
if (!EnableHeuristicPrimeTesting)
{
return PrimeTester.Exclusive.IsPrimeGpu(n, CancellationToken.None);
}
return HeuristicIsPrimeGpuCore(n, sqrtLimit, nMod10);
}
public bool HeuristicIsPrimeGpu(ulong n)
{
return IsPrimeGpu(n);
}
public static bool HeuristicIsPrimeCpu(ulong n)
{
return HeuristicIsPrimeCpu(n, 0UL, (byte)n.Mod10());
}
public static bool HeuristicIsPrimeCpu(ulong n, ulong sqrtLimit, byte nMod10)
{
if (!EnableHeuristicPrimeTesting)
{
return PrimeTester.IsPrimeInternal(n, CancellationToken.None);
}
if (sqrtLimit == 0UL)
{
sqrtLimit = ComputeHeuristicSqrt(n);
}
if (sqrtLimit < 3UL)
{
return EvaluateWithOpenNumericFallback(n);
}
bool includeGroupB = UseHeuristicGroupBTrialDivision;
return HeuristicTrialDivisionCpu(n, sqrtLimit, nMod10, includeGroupB);
}
private bool HeuristicIsPrimeGpuCore(ulong n, ulong sqrtLimit, byte nMod10)
{
// TODO: Is this condition ever met on the execution path in EvenPerfectBitScanner?
@@ -169,11 +127,6 @@ public sealed class HeuristicPrimeTester
return HeuristicTrialDivisionCpu(n, sqrtLimit, nMod10, includeGroupB: false);
}
if (GpuContextPool.ForceCpu)
{
return HeuristicIsPrimeCpu(n, sqrtLimit, nMod10);
}
bool compositeDetected = HeuristicTrialDivisionGpuDetectsDivisor(n, sqrtLimit, nMod10);
return !compositeDetected;
}
@@ -304,6 +257,7 @@ public sealed class HeuristicPrimeTester
private bool HeuristicTrialDivisionGpuDetectsDivisor(ulong n, ulong sqrtLimit, byte nMod10)
{
int batchCapacity = Math.Max(1, HeuristicGpuDivisorBatchSize);
@@ -311,194 +265,165 @@ public sealed class HeuristicPrimeTester
var divisorPool = ThreadStaticPools.UlongPool;
var hitPool = ThreadStaticPools.BytePool;
ulong[]? divisorArray = null;
byte[]? hitFlags = null;
ulong[] divisorArray = divisorPool.Rent(batchCapacity);
byte[] hitFlags = hitPool.Rent(batchCapacity);
var limiter = GpuPrimeWorkLimiter.Acquire();
var gpu = PrimeTester.PrimeTesterGpuContextPool.Rent();
var accelerator = gpu.Accelerator;
var state = PrimeTester.GpuKernelState.GetOrCreate(accelerator);
var scratch = state.RentScratch(batchCapacity, accelerator);
bool compositeDetected = false;
int count = 0;
try
lock (gpu.ExecutionLock)
{
divisorArray = divisorPool.Rent(batchCapacity);
hitFlags = hitPool.Rent(batchCapacity);
var input = scratch.Input;
var output = scratch.Output;
var accelerator = gpu.Accelerator;
var state = PrimeTester.GpuKernelState.GetOrCreate(accelerator);
lock (gpu.ExecutionLock)
bool ProcessBatch(int length)
{
var scratch = state.RentScratch(batchCapacity, accelerator);
try
if (length <= 0)
{
int count = 0;
return false;
}
bool ProcessBatch(int length)
input.View.CopyFromCPU(ref divisorArray[0], length);
state.HeuristicTrialDivisionKernel(length, input.View, n, output.View);
accelerator.Synchronize();
output.View.CopyToCPU(ref hitFlags[0], length);
for (int i = 0; i < length; i++)
{
if (hitFlags[i] == 0)
{
if (length <= 0)
{
return false;
}
scratch.Input.View.CopyFromCPU(ref divisorArray![0], length);
state.HeuristicTrialDivisionKernel(length, scratch.Input.View, n, scratch.Output.View);
accelerator.Synchronize();
scratch.Output.View.CopyToCPU(ref hitFlags![0], length);
for (int i = 0; i < length; i++)
{
if (hitFlags![i] == 0)
{
continue;
}
ulong divisor = divisorArray![i];
if (divisor > 1UL && n % divisor == 0UL)
{
return true;
}
}
return false;
continue;
}
bool QueueDivisor(ulong divisor)
ulong divisor = divisorArray[i];
if (divisor > 1UL && n % divisor == 0UL)
{
divisorArray![count] = divisor;
return true;
}
}
return false;
}
ReadOnlySpan<int> groupADivisors = HeuristicPrimeSieves.GroupADivisors;
int groupAIndex = 0;
ReadOnlySpan<byte> endingOrder = GetGroupBEndingOrder(nMod10);
Span<int> indices = endingOrder.Length <= 8
? stackalloc int[endingOrder.Length]
: new int[endingOrder.Length];
bool groupAHasMore = groupAIndex < groupADivisors.Length && (ulong)groupADivisors[groupAIndex] <= sqrtLimit;
bool groupBHasMore = !endingOrder.IsEmpty && HasGroupBCandidates(endingOrder, indices, sqrtLimit);
while (!compositeDetected && (groupAHasMore || groupBHasMore))
{
if (groupAHasMore)
{
int processed = 0;
while (processed < interleaveBatchSize && groupAIndex < groupADivisors.Length)
{
ulong divisor = (ulong)groupADivisors[groupAIndex];
if (divisor > sqrtLimit)
{
groupAIndex = groupADivisors.Length;
groupAHasMore = false;
break;
}
divisorArray[count] = divisor;
count++;
if (count == batchCapacity)
{
if (ProcessBatch(count))
{
return true;
compositeDetected = true;
goto Cleanup;
}
count = 0;
}
return false;
groupAIndex++;
processed++;
}
ReadOnlySpan<int> groupADivisors = HeuristicPrimeSieves.GroupADivisors;
int groupAIndex = 0;
ReadOnlySpan<byte> endingOrder = GetGroupBEndingOrder(nMod10);
Span<int> indices = endingOrder.Length <= 8
? stackalloc int[endingOrder.Length]
: new int[endingOrder.Length];
bool groupAHasMore = groupAIndex < groupADivisors.Length && (ulong)groupADivisors[groupAIndex] <= sqrtLimit;
bool groupBHasMore = !endingOrder.IsEmpty && HasGroupBCandidates(endingOrder, indices, sqrtLimit);
while (!compositeDetected && (groupAHasMore || groupBHasMore))
if (groupAIndex >= groupADivisors.Length)
{
if (groupAHasMore)
{
int processed = 0;
while (processed < interleaveBatchSize && groupAIndex < groupADivisors.Length)
{
ulong divisor = (ulong)groupADivisors[groupAIndex];
if (divisor > sqrtLimit)
{
groupAIndex = groupADivisors.Length;
groupAHasMore = false;
break;
}
compositeDetected = QueueDivisor(divisor);
if (compositeDetected)
{
break;
}
groupAIndex++;
processed++;
}
if (compositeDetected)
{
break;
}
if (groupAIndex >= groupADivisors.Length)
{
groupAHasMore = false;
}
else
{
groupAHasMore = (ulong)groupADivisors[groupAIndex] <= sqrtLimit;
if (!groupAHasMore)
{
groupAIndex = groupADivisors.Length;
}
}
}
if (groupBHasMore)
{
int processed = 0;
while (processed < interleaveBatchSize)
{
if (!TrySelectNextGroupBDivisor(endingOrder, indices, sqrtLimit, out ulong divisor))
{
groupBHasMore = false;
break;
}
compositeDetected = QueueDivisor(divisor);
if (compositeDetected)
{
break;
}
processed++;
}
if (compositeDetected)
{
break;
}
if (groupBHasMore)
{
groupBHasMore = HasGroupBCandidates(endingOrder, indices, sqrtLimit);
}
}
groupAHasMore = false;
}
if (!compositeDetected && count > 0)
else
{
compositeDetected = ProcessBatch(count);
groupAHasMore = (ulong)groupADivisors[groupAIndex] <= sqrtLimit;
if (!groupAHasMore)
{
groupAIndex = groupADivisors.Length;
}
}
}
finally
if (!compositeDetected && groupBHasMore)
{
state.ReturnScratch(scratch);
int processed = 0;
while (processed < interleaveBatchSize)
{
if (!TrySelectNextGroupBDivisor(endingOrder, indices, sqrtLimit, out ulong divisor))
{
groupBHasMore = false;
break;
}
divisorArray[count] = divisor;
count++;
if (count == batchCapacity)
{
if (ProcessBatch(count))
{
compositeDetected = true;
goto Cleanup;
}
count = 0;
}
processed++;
}
if (groupBHasMore)
{
groupBHasMore = HasGroupBCandidates(endingOrder, indices, sqrtLimit);
}
}
}
}
finally
{
if (divisorArray is not null)
{
divisorPool.Return(divisorArray);
}
if (hitFlags is not null)
if (!compositeDetected && count > 0)
{
hitPool.Return(hitFlags);
}
if (ProcessBatch(count))
{
compositeDetected = true;
goto Cleanup;
}
gpu.Dispose();
limiter.Dispose();
count = 0;
}
}
Cleanup:
state.ReturnScratch(scratch);
divisorPool.Return(divisorArray);
hitPool.Return(hitFlags);
gpu.Dispose();
limiter.Dispose();
return compositeDetected;
}
private static bool EvaluateWithOpenNumericFallback(ulong n)
{
return Prime.Numbers.IsPrime(n);
+5 -5
View File
@@ -394,9 +394,9 @@ public class MersenneDivisorCycles
return true;
}
// TODO: Benchmark PrimeTester.IsPrimeInternal against Open.Numeric.Primes on the EvenPerfectBitScanner workloads to confirm the faster option for wide composites.
// if (HeuristicPrimeTester.Exclusive.IsPrime(remaining, CancellationToken.None))
if (PrimeTester.IsPrimeInternal(remaining, CancellationToken.None))
// TODO: Benchmark PrimeTester.IsPrimeCp against Open.Numeric.Primes on the EvenPerfectBitScanner workloads to confirm the faster option for wide composites.
// if (HeuristicPrimeTester.Exclusive.IsPrimeCpu(remaining, CancellationToken.None))
if (PrimeTester.IsPrimeCp(remaining, CancellationToken.None))
{
AddFactor(counts, remaining);
return true;
@@ -928,8 +928,8 @@ public class MersenneDivisorCycles
return true;
}
// if (!skipPrimeOrderHeuristic && HeuristicPrimeTester.Exclusive.IsPrime(divisor, CancellationToken.None))
if (!skipPrimeOrderHeuristic && PrimeTester.IsPrimeInternal(divisor, CancellationToken.None))
// if (!skipPrimeOrderHeuristic && HeuristicPrimeTester.Exclusive.IsPrimeCpu(divisor, CancellationToken.None))
if (!skipPrimeOrderHeuristic && PrimeTester.IsPrimeCp(divisor, CancellationToken.None))
{
ulong computedOrder = PrimeOrderCalculator.Calculate(
divisor,
@@ -25,8 +25,8 @@ public static class MersennePrimeFactorTester
return false;
}
// if (!HeuristicPrimeTester.Exclusive.IsPrime(q, ct))
if (!PrimeTester.IsPrimeInternal(q, ct))
// if (!HeuristicPrimeTester.Exclusive.IsPrimeCpu(q, ct))
if (!PrimeTester.IsPrimeCp(q, ct))
{
return false;
}
@@ -234,8 +234,8 @@ public static class MersennePrimeFactorTester
continue;
}
// if (HeuristicPrimeTester.Exclusive.IsPrime(m, ct))
if (PrimeTester.IsPrimeInternal(m, ct))
// if (HeuristicPrimeTester.Exclusive.IsPrimeCpu(m, ct))
if (PrimeTester.IsPrimeCp(m, ct))
{
dict[m] = 1;
continue;
+13 -13
View File
@@ -516,7 +516,7 @@ internal static partial class PrimeOrderCalculator
return false;
}
// if (!PrimeTester.IsPrimeInternal(orderFactors.Cofactor, CancellationToken.None))
// if (!PrimeTester.IsPrimeCp(orderFactors.Cofactor, CancellationToken.None))
if (!orderFactors.CofactorIsPrime)
{
return false;
@@ -1123,8 +1123,8 @@ internal static partial class PrimeOrderCalculator
// }
// bool isPrime = primeTester.HeuristicIsPrimeGpu(composite);
// bool isPrime = PrimeTester.IsPrimeInternal(composite, CancellationToken.None);
bool isPrime = PrimeTester.IsPrimeInternal(composite, CancellationToken.None);
// bool isPrime = PrimeTester.IsPrimeCp(composite, CancellationToken.None);
bool isPrime = PrimeTester.IsPrimeCp(composite, CancellationToken.None);
// bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(composite);
if (isPrime)
@@ -1186,8 +1186,8 @@ internal static partial class PrimeOrderCalculator
if (!entry.HasKnownPrimality)
{
// bool isPrime = primeTester.HeuristicIsPrimeGpu(composite);
// bool isPrime = PrimeTester.IsPrimeInternal(composite, CancellationToken.None);
bool isPrime = PrimeTester.IsPrimeInternal(composite, CancellationToken.None);
// bool isPrime = PrimeTester.IsPrimeCp(composite, CancellationToken.None);
bool isPrime = PrimeTester.IsPrimeCp(composite, CancellationToken.None);
// bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(composite);
entry = entry.WithPrimality(isPrime);
@@ -1219,8 +1219,8 @@ internal static partial class PrimeOrderCalculator
else
{
// cofactorIsPrime = primeTester.HeuristicIsPrimeGpu(cofactor);
// cofactorIsPrime = PrimeTester.IsPrimeInternal(cofactor, CancellationToken.None);
cofactorIsPrime = PrimeTester.IsPrimeInternal(cofactor, CancellationToken.None);
// cofactorIsPrime = PrimeTester.IsPrimeCp(cofactor, CancellationToken.None);
cofactorIsPrime = PrimeTester.IsPrimeCp(cofactor, CancellationToken.None);
// cofactorIsPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(cofactor);
}
@@ -1752,8 +1752,8 @@ internal static partial class PrimeOrderCalculator
PrimeTester primeTester = PrimeTester.Exclusive;
// bool isPrime = primeTester.HeuristicIsPrimeGpu(value);
// bool isPrime = PrimeTester.IsPrimeInternal(value, CancellationToken.None);
bool isPrime = PrimeTester.IsPrimeInternal(value, CancellationToken.None);
// bool isPrime = PrimeTester.IsPrimeCp(value, CancellationToken.None);
bool isPrime = PrimeTester.IsPrimeCp(value, CancellationToken.None);
// bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(value);
if (!knownComposite && isPrime)
{
@@ -1765,8 +1765,8 @@ internal static partial class PrimeOrderCalculator
ulong quotient = value / factor;
// isPrime = primeTester.HeuristicIsPrimeGpu(factor);
// isPrime = PrimeTester.IsPrimeInternal(factor, CancellationToken.None);
isPrime = PrimeTester.IsPrimeInternal(factor, CancellationToken.None);
// isPrime = PrimeTester.IsPrimeCp(factor, CancellationToken.None);
isPrime = PrimeTester.IsPrimeCp(factor, CancellationToken.None);
// isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(factor);
if (isPrime)
{
@@ -1797,8 +1797,8 @@ internal static partial class PrimeOrderCalculator
else
{
// isPrime = primeTester.HeuristicIsPrimeGpu(quotient);
// isPrime = PrimeTester.IsPrimeInternal(quotient, CancellationToken.None);
isPrime = PrimeTester.IsPrimeInternal(quotient, CancellationToken.None);
// isPrime = PrimeTester.IsPrimeCp(quotient, CancellationToken.None);
isPrime = PrimeTester.IsPrimeCp(quotient, CancellationToken.None);
// isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(quotient);
}
@@ -955,8 +955,8 @@ internal static partial class PrimeOrderCalculator
{
if (value <= ulong.MaxValue)
{
// return HeuristicPrimeTester.Exclusive.IsPrime((ulong)value, CancellationToken.None);
return PrimeTester.IsPrimeInternal((ulong)value, CancellationToken.None);
// return HeuristicPrimeTester.Exclusive.IsPrimeCpu((ulong)value, CancellationToken.None);
return PrimeTester.IsPrimeCp((ulong)value, CancellationToken.None);
}
if ((value & UInt128.One) == UInt128.Zero)
+13 -15
View File
@@ -24,29 +24,27 @@ public sealed class PrimeTester
public bool IsPrime(ulong n, CancellationToken ct)
{
// EvenPerfectBitScanner feeds monotonically increasing odd exponents that start well above the
// small-prime cutoffs and already exclude multiples of five. These guards remain for targeted
// tests and diagnostics so ad-hoc callers can still probe small or even values when needed.
if (n <= 1UL)
{
return false;
}
// bool isTwo = n == 2UL;
if (n == 2UL)
{
throw new InvalidOperationException("PrimeTester.IsPrime encountered the sentinel input 2.");
}
bool isOdd = (n & 1UL) != 0UL;
// ulong mod5 = n % 5UL;
// bool divisibleByFive = n > 5UL && mod5 == 0UL;
bool result = isOdd;
bool result = /*n >= 2UL && (isTwo || */ (n == 2UL || isOdd) /*) && !divisibleByFive*/;
// Production scans always fall through to trial division because n is already > 5 by the time this
// path executes. Keep the guard so synthetic callers that hammer tiny inputs can skip the loop.
bool requiresTrialDivision = result /*&& n >= 7UL && !isTwo*/ && n >= 7UL;
bool requiresTrialDivision = result && n >= 7UL;
if (requiresTrialDivision)
{
bool sharesMaxExponentFactor = n.Mod10() == 1UL && SharesFactorWithMaxExponent(n);
result &= !sharesMaxExponentFactor;
// EvenPerfectBitScanner streams exponents starting at 136,279,841, so the Mod10/GCD guard never fires on the
// production path. Leave the logic commented out as instrumentation for diagnostic builds.
// bool sharesMaxExponentFactor = n.Mod10() == 1UL && SharesFactorWithMaxExponent(n);
// result &= !sharesMaxExponentFactor;
if (result)
{
@@ -95,10 +93,10 @@ public sealed class PrimeTester
bool gpuReportedPrime = !forceCpu && !belowGpuRange && outFlags[0] != 0;
bool requiresCpuFallback = forceCpu || belowGpuRange || !gpuReportedPrime;
return requiresCpuFallback ? IsPrimeInternal(n, ct) : true;
return requiresCpuFallback ? IsPrimeCp(n, ct) : true;
}
public static bool IsPrimeInternal(ulong n, CancellationToken ct)
public static bool IsPrimeCp(ulong n, CancellationToken ct)
{
// Preserve the legacy entry point for callers that bypass the thread-local caches.
return Exclusive.IsPrime(n, ct);