Clarify cycle stepper reset inlining

This commit is contained in:
Marek Lemańczyk committed 2025-10-02 13:30:56 +02:00
1 parent 8b467c27f3
commit b8aa845c5c
53 files changed
+1736 -1131

No files matched your search

+9 -5
View File
@@ -2,8 +2,12 @@ namespace PerfectNumbers.Core.Cpu;
public static class CpuConstants
{
// Allowed q mod 10 sets depending on last digit of M_p
public const int LastSevenMask10 = (1 << 7) | (1 << 9);
// For M_p ending with 1, restrict to {1,3,9}; 7 considered only for q == 7 (not possible for large p)
public const int LastOneMask10 = (1 << 1) | (1 << 3) | (1 << 9);
}
// Allowed q mod 10 sets depending on last digit of M_p
public const int LastSevenMask10 = (1 << 7) | (1 << 9);
// For M_p ending with 1, restrict to {1,3,9}; 7 considered only for q == 7 (not possible for large p)
public const int LastOneMask10 = (1 << 1) | (1 << 3) | (1 << 9);
// TODO: Generate these masks at startup from the benchmarked Mod10 automaton tables so CPU filtering stays aligned with
// the optimized divisor-cycle residue steps.
// TODO: Fold in the Mod6ComparisonBenchmarks stride table here so callers can merge the mod 6 skips with these masks and
// avoid redundant residue work in the CPU hot loop.
}
@@ -200,14 +200,20 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
if (useCycles)
{
cycleBlock = DivisorCycleCache.Shared.Acquire(divisor);
// TODO: Swap this block lease for the direct single-cycle lookup once the cache exposes it
// so the CPU scanner mirrors the benchmarked single-block policy without retaining mutable
// blocks beyond the startup snapshot.
divisorCycle = cycleBlock.GetCycle(divisor);
// TODO: When divisorCycle is zero (cache miss), compute only that single cycle on the device
// selected by the current settings, skip inserting the result into the shared cache, and keep
// operating with the single on-disk block without requesting additional blocks.
hit = CheckDivisor(prime, divisorCycle != 0UL, divisorCycle, divisorData);
}
else
{
// TODO: Force this path to hydrate divisor cycles from disk once the cache guarantees
// coverage so we can retire the slower Montgomery-only fallback highlighted by the CPU
// divisor benchmarks.
// TODO: Remove this no-cycle branch and require on-demand cycle computation (without
// mutating the cache) so CPU scans never fall back to the slower Montgomery-only path
// identified in the benchmarks.
hit = CheckDivisor(prime, false, 0UL, divisorData);
}
@@ -347,6 +353,9 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
bool useDivisorCycles = _owner._useDivisorCycles;
if (useDivisorCycles && divisorCycle == 0UL)
{
// TODO: Replace this guard with an on-demand CPU/GPU cycle computation path that skips
// cache insertion and continues operating with the single cached block instead of
// requesting additional cycle batches when a lookup misses.
throw new InvalidOperationException($"Missing divisor cycle for divisor {divisor}.");
}
@@ -2,94 +2,101 @@ namespace PerfectNumbers.Core.Cpu;
public class MersenneNumberIncrementalCpuTester(GpuKernelType kernelType)
{
private static readonly MersenneDivisorCycles _cycles = MersenneDivisorCycles.Shared;
private readonly GpuKernelType _kernelType = kernelType;
private static readonly MersenneDivisorCycles _cycles = MersenneDivisorCycles.Shared;
private readonly GpuKernelType _kernelType = kernelType;
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
{
UInt128 k, one = k = UInt128.One;
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
// Use residue automaton always on CPU
var auto = new MersenneResidueAutomaton(exponent);
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
{
UInt128 k = UInt128.One;
UInt128 one = k;
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
// Use residue automaton always on CPU
var auto = new MersenneResidueAutomaton(exponent);
bool reject, shouldCheck;
UInt128 q = auto.CurrentQ(),
divPow, halfPow, phi, qCycle;
bool reject;
bool shouldCheck;
UInt128 q = auto.CurrentQ();
UInt128 divPow;
UInt128 halfPow;
UInt128 phi;
UInt128 qCycle;
ulong div, mod8, phi64;
ulong div;
ulong mod8;
ulong phi64;
while (k <= maxK && Volatile.Read(ref isPrime))
{
qCycle = MersenneDivisorCycles.GetCycle(q);
shouldCheck = lastIsSeven
? (auto.Mod10R != 5UL)
: (auto.Mod10R != 5UL);
if (shouldCheck)
{
mod8 = auto.Mod8R;
if (mod8 != 1UL && mod8 != 7UL)
{
shouldCheck = false;
}
else if (auto.Mod3R == 0UL || auto.Mod5R == 0UL)
{
shouldCheck = false;
}
}
while (k <= maxK && Volatile.Read(ref isPrime))
{
// 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(q);
shouldCheck = auto.Mod10R != 5UL;
if (shouldCheck)
{
mod8 = auto.Mod8R;
if (mod8 != 1UL && mod8 != 7UL)
{
shouldCheck = false;
}
else if (auto.Mod3R == 0UL || auto.Mod5R == 0UL)
{
// TODO: Swap these Mod3/Mod5 zero checks to the cached residue tables once the automaton exposes
// the benchmarked lookup-based helpers so CPU scans avoid runtime modulo instructions.
shouldCheck = false;
}
}
if (shouldCheck)
{
if (_kernelType == GpuKernelType.Pow2Mod)
if (shouldCheck)
{
if (_kernelType == GpuKernelType.Pow2Mod)
{
// TODO: Swap this PowModWithCycle call for the ProcessEightBitWindows helper once
// the scalar implementation lands so CPU pow2mod scans match the GPU speedups
// captured in GpuPow2ModBenchmarks.
reject = exponent.PowModWithCycle(q, qCycle) == UInt128.One;
}
else
{
phi = q - one;
reject = false;
if (phi <= ulong.MaxValue)
{
phi64 = (ulong)phi;
// TODO: Route the phi-based powmods through the upcoming windowed helper to
// avoid the current square-and-multiply cost highlighted in the Pow2 bench
// suite when scanning large divisors.
if (phi64.PowModWithCycle(q, qCycle) == one)
{
// TODO: Once the windowed pow2 helper is available expose a version
// that reuses cached windows here instead of recomputing via the slow
// bit ladder for every divisor.
halfPow = (phi64 >> 1).PowModWithCycle(q, qCycle) - one;
if (halfPow.BinaryGcd(q) == one)
{
div = phi64.FastDiv64(exponent, divMul);
// TODO: Replace this divisor powmod with the shared windowed
// implementation so each candidate uses the benchmarked fast
// path instead of the current per-bit multiply chain.
divPow = div.PowModWithCycle(q, qCycle) - one;
if (divPow.BinaryGcd(q) == one)
{
// TODO: Swap this PowModWithCycle call for the ProcessEightBitWindows helper once
// the scalar implementation lands so CPU pow2mod scans match the GPU speedups
// captured in GpuPow2ModBenchmarks.
reject = exponent.PowModWithCycle(q, qCycle) == UInt128.One;
reject = true;
}
else
{
phi = q - one;
reject = false;
if (phi <= ulong.MaxValue)
{
phi64 = (ulong)phi;
// TODO: Route the phi-based powmods through the upcoming windowed helper to
// avoid the current square-and-multiply cost highlighted in the Pow2 bench
// suite when scanning large divisors.
if (phi64.PowModWithCycle(q, qCycle) == one)
{
// TODO: Once the windowed pow2 helper is available expose a version
// that reuses cached windows here instead of recomputing via the slow
// bit ladder for every divisor.
halfPow = (phi64 >> 1).PowModWithCycle(q, qCycle) - one;
if (halfPow.BinaryGcd(q) == one)
{
div = phi64.FastDiv64(exponent, divMul);
// TODO: Replace this divisor powmod with the shared windowed
// implementation so each candidate uses the benchmarked fast
// path instead of the current per-bit multiply chain.
divPow = div.PowModWithCycle(q, qCycle) - one;
if (divPow.BinaryGcd(q) == one)
{
reject = true;
}
}
}
}
}
}
}
}
}
if (reject && q.IsPrimeCandidate())
{
Volatile.Write(ref isPrime, false);
break;
}
}
if (reject && q.IsPrimeCandidate())
{
Volatile.Write(ref isPrime, false);
break;
}
}
k += 1UL;
auto.Next();
q = auto.CurrentQ();
}
return;
}
}
k += 1UL;
auto.Next();
q = auto.CurrentQ();
}
}
}
@@ -13,7 +13,10 @@ public class MersenneNumberOrderCpuTester(GpuKernelType kernelType)
while (k <= maxK && Volatile.Read(ref isPrime))
{
qCycle = MersenneDivisorCycles.GetCycle(q);
qCycle = MersenneDivisorCycles.GetCycle(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.
int allowMask2 = lastIsSeven ? CpuConstants.LastSevenMask10 : CpuConstants.LastOneMask10;
bool shouldCheck = ((allowMask2 >> (int)auto.Mod10R) & 1) != 0;
if (shouldCheck)
@@ -10,9 +10,14 @@ public class MersenneNumberResidueCpuTester
// Initialize/update Mersenne residue tracker and start a merge walk for ascending q divisors
_mersenneResidueTracker ??= new(ResidueModel.Mersenne);
// For small divisors q <= 4,000,000 use cached cycle length to fast-path checks
// and avoid per-q powmods when possible when scanning lanes.
_mersenneResidueTracker.BeginMerge(exponent);
// For small divisors q <= 4,000,000 use cached cycle length to fast-path checks
// and avoid per-q powmods when possible when scanning lanes.
// TODO: Wire this residue scan into DivisorCycleCache so the mandatory cycle acceleration from the
// by-divisor benchmarks applies here instead of relying solely on ModResidueTracker powmods.
// TODO: When a required cycle is missing from the snapshot, compute only that single cycle on the device
// selected by the current settings, skip persisting the result, and avoid scheduling additional cache blocks so
// we keep operating with the single shared block.
_mersenneResidueTracker.BeginMerge(exponent);
// Start at k = 1: q0 = 2*p*1 + 1
UInt128 k = UInt128.One;
@@ -80,12 +85,15 @@ public class MersenneNumberResidueCpuTester
r5_3 = r5_2 + step5; if (r5_3 >= 10UL) r5_3 -= 10UL; if (r5_3 >= 5UL) r5_3 -= 5UL;
// lane 0
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()))
if (lanes >= 1 &&
(((allowMask >> (int)r10_0) & 1) != 0 && (r8_0 == 1UL || r8_0 == 7UL) && r3_0 != 0UL && r5_0 != 0UL))
{
// TODO: Once cycle data is exposed for residue scans, consult the cached divisor cycle here
// (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(exponent, q, out isDivider) && isDivider &&
(q <= ulong.MaxValue ? ((ulong)q).IsPrimeCandidate() : q.IsPrimeCandidate()))
{
localIsPrime = false;
}