Merge pull request #24 from mlemanczyk/codex/modify-evenperfectbitscanner-for-residue-mode

Report residue scan exhaustion through detailed checks
This commit is contained in:
Marek Lemańczyk authored and GitHub committed 2025-09-17 19:02:45 +02:00
commit 2d024cc708
7 files changed
+286 -45

No files matched your search

+91 -2
View File
@@ -142,6 +142,95 @@ public class ProgramTests
}
}
[Fact]
public void Divisor_mode_sets_detailedCheck_when_divisor_is_found()
{
var useDivisorField = typeof(Program).GetField("_useDivisor", BindingFlags.NonPublic | BindingFlags.Static)!;
var divisorField = typeof(Program).GetField("_divisor", BindingFlags.NonPublic | BindingFlags.Static)!;
var testerField = typeof(Program).GetField("_divisorTester", BindingFlags.NonPublic | BindingFlags.Static)!;
var candidatesField = typeof(MersenneNumberDivisorGpuTester).GetField("_divisorCandidates", BindingFlags.NonPublic | BindingFlags.Static)!;
var primeField = typeof(Program).GetField("PrimeTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var residueField = typeof(Program).GetField("PResidue", BindingFlags.NonPublic | BindingFlags.Static)!;
var forceCpuProp = typeof(GpuContextPool).GetProperty("ForceCpu");
useDivisorField.SetValue(null, true);
divisorField.SetValue(null, UInt128.Zero);
testerField.SetValue(null, new MersenneNumberDivisorGpuTester());
primeField.SetValue(null, new ThreadLocal<PrimeTester>(() => new PrimeTester(), trackAllValues: true));
residueField.SetValue(null, new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true));
candidatesField.SetValue(null, new (ulong, uint)[] { (23UL, 11U) });
forceCpuProp!.SetValue(null, true);
try
{
Program.IsEvenPerfectCandidate(11UL, 32UL, out bool searched, out bool detailedCheck).Should().BeFalse();
searched.Should().BeTrue();
detailedCheck.Should().BeTrue();
}
finally
{
useDivisorField.SetValue(null, false);
testerField.SetValue(null, null);
candidatesField.SetValue(null, null);
primeField.SetValue(null, null);
residueField.SetValue(null, null);
forceCpuProp!.SetValue(null, false);
}
}
[Fact]
public void Residue_mode_sets_detailedCheck_based_on_exhaustion()
{
var useDivisorField = typeof(Program).GetField("_useDivisor", BindingFlags.NonPublic | BindingFlags.Static)!;
var residueModeField = typeof(Program).GetField("_useResidueMode", BindingFlags.NonPublic | BindingFlags.Static)!;
var mersenneField = typeof(Program).GetField("MersenneTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var primeField = typeof(Program).GetField("PrimeTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var residueField = typeof(Program).GetField("PResidue", BindingFlags.NonPublic | BindingFlags.Static)!;
var forceCpuProp = typeof(GpuContextPool).GetProperty("ForceCpu");
const ulong exponent = 7UL;
useDivisorField.SetValue(null, false);
residueModeField.SetValue(null, true);
primeField.SetValue(null, new ThreadLocal<PrimeTester>(() => new PrimeTester(), trackAllValues: true));
residueField.SetValue(null, new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, exponent, true), trackAllValues: true));
forceCpuProp!.SetValue(null, true);
try
{
mersenneField.SetValue(null, new ThreadLocal<MersenneNumberTester>(() => new MersenneNumberTester(
useGpuScan: false,
useGpuOrder: false,
useResidue: true,
maxK: 0UL,
residueDivisorSets: 0UL), trackAllValues: true));
Program.IsEvenPerfectCandidate(exponent, 0UL, out bool searched, out bool detailedCheck).Should().BeTrue();
searched.Should().BeTrue();
detailedCheck.Should().BeFalse();
mersenneField.SetValue(null, new ThreadLocal<MersenneNumberTester>(() => new MersenneNumberTester(
useGpuScan: false,
useGpuOrder: false,
useResidue: true,
maxK: 4UL,
residueDivisorSets: 2UL), trackAllValues: true));
Program.IsEvenPerfectCandidate(exponent, 0UL, out searched, out detailedCheck).Should().BeTrue();
searched.Should().BeTrue();
detailedCheck.Should().BeTrue();
}
finally
{
mersenneField.SetValue(null, null);
primeField.SetValue(null, null);
residueField.SetValue(null, null);
residueModeField.SetValue(null, false);
forceCpuProp!.SetValue(null, false);
useDivisorField.SetValue(null, false);
}
}
[Fact]
public void Divisor_mode_accepts_large_search_limit()
{
@@ -223,7 +312,7 @@ public class ProgramTests
var forceCpuProp = typeof(GpuContextPool).GetProperty("ForceCpu");
useDivisorField.SetValue(null, false);
mersenneField.SetValue(null, new ThreadLocal<MersenneNumberTester>(() => new MersenneNumberTester(useIncremental: true, useResidue: true, maxK: 1_000UL), trackAllValues: true));
mersenneField.SetValue(null, new ThreadLocal<MersenneNumberTester>(() => new MersenneNumberTester(useIncremental: true, useResidue: true, maxK: 1_000UL, residueDivisorSets: 1UL), trackAllValues: true));
primeField.SetValue(null, new ThreadLocal<PrimeTester>(() => new PrimeTester(), trackAllValues: true));
residueField.SetValue(null, new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true));
forceCpuProp!.SetValue(null, false);
@@ -232,7 +321,7 @@ public class ProgramTests
{
Program.IsEvenPerfectCandidate(29UL, 0UL, out bool searched, out bool detailed).Should().BeFalse();
searched.Should().BeTrue();
detailed.Should().BeFalse();
detailed.Should().BeTrue();
Program.IsEvenPerfectCandidate(31UL, 0UL, out searched, out detailed).Should().BeTrue();
searched.Should().BeTrue();
+6 -4
View File
@@ -447,7 +447,8 @@ internal static class Program
useGpuScan: mersenneOnGpu,
useGpuOrder: orderOnGpu,
useResidue: useResidue,
maxK: residueKMax);
maxK: residueKMax,
residueDivisorSets: divisorCyclesSearchLimit);
if (!useLucas)
{
tester.WarmUpOrders(currentP, _orderWarmupLimitOverride ?? 5_000_000UL);
@@ -731,7 +732,7 @@ internal static class Program
Console.WriteLine(" --divisor-cycles-device=cpu|gpu device for cycles generation (default gpu)");
Console.WriteLine(" --divisor-cycles-batch-size=<value> batch size for cycles generation (default 512)");
Console.WriteLine(" --divisor-cycles-continue continue divisor cycles generation");
Console.WriteLine(" --divisor-cycles-limit=<value> cycle search iterations when --mersenne=divisor");
Console.WriteLine(" --divisor-cycles-limit=<value> cycle search iterations when --mersenne=divisor or residue");
Console.WriteLine(" --use-order test primality via q order");
Console.WriteLine(" --workaround-mod avoid '%' operator on the GPU");
@@ -1083,8 +1084,9 @@ internal static class Program
return _divisorTester!.IsPrime(p, _divisor, divisorCyclesSearchLimit, out detailedCheck);
}
detailedCheck = MersenneTesters.Value!.IsMersennePrime(p);
return detailedCheck;
bool mersennePrime = MersenneTesters.Value!.IsMersennePrime(p, out bool residueExhausted);
detailedCheck = residueExhausted;
return mersennePrime;
}
// Use ModResidueTracker with a small set of primes to pre-filter composite p.
@@ -24,8 +24,18 @@ public class MersenneNumberResidueCpuTesterTests
private static void RunCase(MersenneNumberResidueCpuTester tester, ulong exponent, ulong maxK, bool expectedPrime)
{
bool isPrime = true;
tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime);
bool exhausted = false;
tester.Scan(
exponent,
(UInt128)exponent << 1,
LastDigitIsSeven(exponent),
(UInt128)maxK,
1UL,
(UInt128)maxK,
ref isPrime,
ref exhausted);
isPrime.Should().Be(expectedPrime);
exhausted.Should().BeTrue();
}
}
@@ -27,8 +27,18 @@ public class MersenneNumberResidueGpuTesterTests
private static void RunCase(MersenneNumberResidueGpuTester tester, ulong exponent, ulong maxK, bool expectedPrime)
{
bool isPrime = true;
tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime);
bool exhausted = false;
tester.Scan(
exponent,
(UInt128)exponent << 1,
LastDigitIsSeven(exponent),
(UInt128)maxK,
1UL,
(UInt128)maxK,
ref isPrime,
ref exhausted);
isPrime.Should().Be(expectedPrime);
exhausted.Should().BeTrue();
}
}
@@ -5,8 +5,21 @@ public class MersenneNumberResidueCpuTester
private ModResidueTracker? _mersenneResidueTracker;
// CPU residue variant using tracker + unrolled residue updates (no method calls in the loop).
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
public void Scan(
ulong exponent,
UInt128 twoP,
bool lastIsSeven,
UInt128 perSetLimit,
ulong setCount,
UInt128 overallLimit,
ref bool isPrime,
ref bool divisorsExhausted)
{
if (setCount == 0UL || perSetLimit == UInt128.Zero || overallLimit == UInt128.Zero)
{
return;
}
// Initialize/update Mersenne residue tracker and start a merge walk for ascending q divisors
_mersenneResidueTracker ??= new(ResidueModel.Mersenne);
@@ -14,12 +27,6 @@ public class MersenneNumberResidueCpuTester
// and avoid per-q powmods when possible when scanning lanes.
_mersenneResidueTracker.BeginMerge(exponent);
// Start at k = 1: q0 = 2*p*1 + 1
UInt128 k = UInt128.One;
UInt128 q = twoP + UInt128.One;
// Current residues
q.Mod10_8_5_3(out ulong r10, out ulong r8, out ulong r5, out ulong r3);
// Step residues for q += 2*p
ulong step10 = (ulong)(twoP % 10UL) % 10UL;
ulong step8 = (ulong)(twoP % 8UL);
ulong step3 = (ulong)(twoP % 3UL);
@@ -43,18 +50,39 @@ public class MersenneNumberResidueCpuTester
bool localIsPrime, isDivider;
maxK++;
ModResidueTracker tracker = _mersenneResidueTracker!;
while (k < maxK && (localIsPrime = Volatile.Read(ref isPrime)))
UInt128 limitInclusive = overallLimit + UInt128.One;
for (ulong setIndex = 0; setIndex < setCount && Volatile.Read(ref isPrime); setIndex++)
{
remaining = maxK - k;
UInt128 setOffset = perSetLimit * (UInt128)setIndex;
UInt128 k = setOffset + UInt128.One;
if (k >= limitInclusive)
{
break;
}
UInt128 setLimitExclusive = setOffset + perSetLimit + UInt128.One;
if (setLimitExclusive > limitInclusive)
{
setLimitExclusive = limitInclusive;
}
UInt128 q = twoP * k + UInt128.One;
q.Mod10_8_5_3(out ulong r10, out ulong r8, out ulong r5, out ulong r3);
while (k < setLimitExclusive && (localIsPrime = Volatile.Read(ref isPrime)))
{
remaining = setLimitExclusive - k;
if (remaining == UInt128.Zero)
{
break;
}
lanes = remaining > 4UL ? 4 : (int)remaining;
if (lanes <= 0)
{
break;
}
// Compute lane residues incrementally (avoid multiplications, minimize reductions)
// r10
@@ -85,7 +113,6 @@ public class MersenneNumberResidueCpuTester
if (lanes >= 1 &&
(((allowMask >> (int)r10_0) & 1) != 0 && (r8_0 == 1UL || r8_0 == 7UL) && r3_0 != 0UL && r5_0 != 0UL))
{
// cycle-based quick check for small q
if (localIsPrime && tracker.MergeOrAppend(exponent, q, out isDivider) && isDivider &&
(q <= ulong.MaxValue ? ((ulong)q).IsPrimeCandidate() : q.IsPrimeCandidate()))
{
@@ -135,10 +162,12 @@ public class MersenneNumberResidueCpuTester
if (!localIsPrime)
{
Volatile.Write(ref isPrime, false);
divisorsExhausted = true;
return;
}
else if (!Volatile.Read(ref isPrime))
{
divisorsExhausted = true;
return;
}
@@ -162,8 +191,9 @@ public class MersenneNumberResidueCpuTester
r3 += step3; if (r3 >= 3UL) r3 -= 3UL;
r5 += step5; if (r5 >= 10UL) r5 -= 10UL; if (r5 >= 5UL) r5 -= 5UL;
}
}
return;
divisorsExhausted = true;
}
}
}
}
@@ -12,15 +12,27 @@ public class MersenneNumberResidueGpuTester(bool useGpuOrder)
// via GpuKernelPool to avoid host round-trips.
// GPU residue variant: check 2^p % q == 1 for q = 2*p*k + 1.
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
public void Scan(
ulong exponent,
UInt128 twoP,
bool lastIsSeven,
UInt128 perSetLimit,
ulong setCount,
UInt128 overallLimit,
ref bool isPrime,
ref bool divisorsExhausted)
{
if (setCount == 0UL || perSetLimit == UInt128.Zero || overallLimit == UInt128.Zero)
{
return;
}
var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder);
var accelerator = gpuLease.Accelerator;
// Ensure device has small cycles and primes tables for in-kernel lookup
var smallCyclesView = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
ResiduePrimeViews primeViews = GpuKernelPool.EnsureSmallPrimesOnDevice(accelerator);
int batchSize = GpuConstants.ScanBatchSize;
UInt128 kStart = 1UL;
byte last = lastIsSeven ? (byte)1 : (byte)0;
var kernel = gpuLease.Pow2ModKernel;
ulong step10 = ((exponent % 10UL) << 1) % 10UL;
@@ -31,27 +43,38 @@ public class MersenneNumberResidueGpuTester(bool useGpuOrder)
var orderBuffer = accelerator.Allocate1D<ulong>(batchSize);
ulong[] orderArray = ArrayPool<ulong>.Shared.Rent(batchSize);
UInt128 remaining;
int currentSize;
int i;
UInt128 batchSize128 = (UInt128)batchSize, q;
UInt128 batchSize128 = (UInt128)batchSize;
UInt128 limitInclusive = overallLimit + UInt128.One;
Span<ulong> orders = orderArray.AsSpan(0, batchSize);
try
{
while (kStart < maxK && Volatile.Read(ref isPrime))
for (ulong setIndex = 0; setIndex < setCount && Volatile.Read(ref isPrime); setIndex++)
{
remaining = maxK - kStart;
if (remaining > batchSize128)
UInt128 setOffset = perSetLimit * (UInt128)setIndex;
UInt128 setStart = setOffset + UInt128.One;
if (setStart >= limitInclusive)
{
currentSize = batchSize;
}
else
{
currentSize = (int)remaining;
orders = orderArray.AsSpan(0, currentSize);
break;
}
q = twoP * kStart + UInt128.One;
UInt128 setLimitExclusive = setOffset + perSetLimit + UInt128.One;
if (setLimitExclusive > limitInclusive)
{
setLimitExclusive = limitInclusive;
}
UInt128 kStart = setStart;
while (kStart < setLimitExclusive && Volatile.Read(ref isPrime))
{
UInt128 remaining = setLimitExclusive - kStart;
int currentSize = remaining > batchSize128 ? batchSize : (int)remaining;
if (currentSize <= 0)
{
break;
}
orders = orderArray.AsSpan(0, currentSize);
UInt128 q = twoP * kStart + UInt128.One;
q.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
var ra = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5);
@@ -65,7 +88,7 @@ public class MersenneNumberResidueGpuTester(bool useGpuOrder)
break;
}
for (i = 0; i < currentSize; i++)
for (int i = 0; i < currentSize; i++)
{
if (orders[i] != 0UL)
{
@@ -77,11 +100,14 @@ public class MersenneNumberResidueGpuTester(bool useGpuOrder)
kStart += batchSize128;
}
}
}
finally
{
ArrayPool<ulong>.Shared.Return(orderArray);
orderBuffer.Dispose();
gpuLease.Dispose();
}
divisorsExhausted = true;
}
}
+80 -6
View File
@@ -25,11 +25,13 @@ public sealed class MersenneNumberTester(
bool useGpuScan = true,
bool useGpuOrder = true,
bool useResidue = true,
ulong maxK = 5_000_000UL)
ulong maxK = 5_000_000UL,
ulong residueDivisorSets = PerfectNumberConstants.ExtraDivisorCycleSearchLimit)
{
private readonly bool _useResidue = useResidue;
private readonly bool _useIncremental = useIncremental && !useResidue;
private readonly ulong _maxK = maxK;
private readonly ulong _residueDivisorSetCount = residueDivisorSets;
private readonly GpuKernelType _kernelType = kernelType;
private readonly bool _useModuloWorkaround = useModuloWorkaround;
private readonly bool _useGpuLucas = useGpuLucas;
@@ -48,6 +50,23 @@ public sealed class MersenneNumberTester(
// TODO: Ensure Program passes useResidue correctly and that residue-vs-incremental-vs-LL selection respects CLI flags.
// Also consider injecting a shared cycles cache (MersenneDivisorCycles.Shared) to both CPU and GPU testers.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static UInt128 DivideRoundUp(UInt128 value, UInt128 divisor)
{
if (divisor == UInt128.Zero)
{
return UInt128.Zero;
}
UInt128 result = value / divisor;
if (result * divisor == value)
{
return result;
}
return result + UInt128.One;
}
public void WarmUpOrders(ulong exponent, ulong limit = 5_000_000UL)
{
bool cacheEnabled = useOrderCache;
@@ -191,23 +210,30 @@ public sealed class MersenneNumberTester(
ArrayPool<UInt128>.Shared.Return(qs);
}
public bool IsMersennePrime(ulong exponent)
public bool IsMersennePrime(ulong exponent) => IsMersennePrime(exponent, out _);
public bool IsMersennePrime(ulong exponent, out bool divisorsExhausted)
{
divisorsExhausted = false;
// Safe early rejections using known orders for tiny primes:
// 7 | M_p iff 3 | p. Avoid rejecting p == 3 where M_3 == 7 is prime.
if ((exponent % 3UL) == 0UL && exponent != 3UL)
{
divisorsExhausted = true;
return false;
}
if ((exponent & 3UL) == 1UL && exponent.SharesFactorWithExponentMinusOne())
{
divisorsExhausted = true;
return false;
}
// Skip even exponents: for even p > 2, M_p is composite; here p is large.
if ((exponent & 1UL) == 0UL)
{
divisorsExhausted = true;
return false;
}
@@ -225,16 +251,60 @@ public sealed class MersenneNumberTester(
maxK = _maxK;
}
if (_maxK == 0UL || _residueDivisorSetCount == 0UL)
{
return prePrime;
}
UInt128 totalLimit = (UInt128)_maxK;
if (totalLimit > maxK)
{
totalLimit = maxK;
}
if (totalLimit == UInt128.Zero)
{
return true;
}
ulong requestedSets = _residueDivisorSetCount;
if (requestedSets == 0UL)
{
requestedSets = 1UL;
}
UInt128 perSetLimit = DivideRoundUp(totalLimit, (UInt128)requestedSets);
if (perSetLimit == UInt128.Zero)
{
perSetLimit = UInt128.One;
}
UInt128 setsNeeded = DivideRoundUp(totalLimit, perSetLimit);
ulong effectiveSets = requestedSets;
if (setsNeeded < (UInt128)effectiveSets)
{
effectiveSets = (ulong)setsNeeded;
}
bool scanCompleted = false;
if (_useGpuScan)
{
_residueGpuTester!.Scan(exponent, twoP, lastIsSeven, maxK, ref prePrime);
_residueGpuTester!.Scan(exponent, twoP, lastIsSeven, perSetLimit, effectiveSets, totalLimit, ref prePrime, ref scanCompleted);
}
else
{
_residueCpuTester!.Scan(exponent, twoP, lastIsSeven, maxK, ref prePrime);
_residueCpuTester!.Scan(exponent, twoP, lastIsSeven, perSetLimit, effectiveSets, totalLimit, ref prePrime, ref scanCompleted);
}
return prePrime;
if (!prePrime)
{
divisorsExhausted = true;
return false;
}
divisorsExhausted = scanCompleted;
return true;
}
if (!_useIncremental)
@@ -255,12 +325,15 @@ public sealed class MersenneNumberTester(
if (!prePrime)
{
divisorsExhausted = true;
return false;
}
return _useGpuLucas
bool lucasPrime = _useGpuLucas
? _lucasLehmerGpuTester.IsPrime(exponent, runOnGpu: true)
: _lucasLehmerCpuTester.IsPrime(exponent);
divisorsExhausted = true;
return lucasPrime;
}
bool isPrime = true;
@@ -288,6 +361,7 @@ public sealed class MersenneNumberTester(
}
}
divisorsExhausted = true;
return isPrime;
}