From 798ae455b13320298d3b84ab953e9d1f0731500f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Mon, 20 Oct 2025 17:03:36 +0200 Subject: [PATCH 1/8] Adjust CPU Pow2 Montgomery helper for small divisors --- PerfectNumbers.Core/ULongExtensions.Cpu.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/PerfectNumbers.Core/ULongExtensions.Cpu.cs b/PerfectNumbers.Core/ULongExtensions.Cpu.cs index 3de0b55..24e415c 100644 --- a/PerfectNumbers.Core/ULongExtensions.Cpu.cs +++ b/PerfectNumbers.Core/ULongExtensions.Cpu.cs @@ -16,12 +16,13 @@ public static partial class ULongExtensions // return keepMontgomery ? divisor.MontgomeryOne : 1UL % modulus; // } - // There is no point in having extra branch, because in real use-cases it'll barely ever hit. - // Out of all 3_358_944_545 Pow2MontgomeryModWindowedCpu calls, we hit Pow2MontgomeryModSingleBit only 772 times. - // if (exponent <= Pow2WindowFallbackThreshold) - // { - // 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 Pow2MontgomeryModSingleBit(exponent, divisor, keepMontgomery); + } int bitLength = GetPortableBitLengthCpu(exponent); int windowSize = GetWindowSizeCpu(bitLength); From 4831fc84d12e995db0ecba4c964ca6a038ee22e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Mon, 20 Oct 2025 18:36:51 +0200 Subject: [PATCH 2/8] Clean up resolved windowed pow2 TODOs on CPU paths --- .../Cpu/MersenneNumberIncrementalCpuTester.cs | 12 ------------ .../Cpu/MersenneNumberOrderCpuTester.cs | 9 --------- PerfectNumbers.Core/ExponentRemainderStepper.cs | 11 ----------- PerfectNumbers.Core/MersenneNumberTester.cs | 4 ---- PerfectNumbers.Core/UInt128Extensions.cs | 2 -- 5 files changed, 38 deletions(-) diff --git a/PerfectNumbers.Core/Cpu/MersenneNumberIncrementalCpuTester.cs b/PerfectNumbers.Core/Cpu/MersenneNumberIncrementalCpuTester.cs index d0358ef..7506cc1 100644 --- a/PerfectNumbers.Core/Cpu/MersenneNumberIncrementalCpuTester.cs +++ b/PerfectNumbers.Core/Cpu/MersenneNumberIncrementalCpuTester.cs @@ -50,9 +50,6 @@ public class MersenneNumberIncrementalCpuTester(GpuKernelType kernelType) { 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 @@ -62,21 +59,12 @@ public class MersenneNumberIncrementalCpuTester(GpuKernelType kernelType) 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) { diff --git a/PerfectNumbers.Core/Cpu/MersenneNumberOrderCpuTester.cs b/PerfectNumbers.Core/Cpu/MersenneNumberOrderCpuTester.cs index 55e37b1..cc1996f 100644 --- a/PerfectNumbers.Core/Cpu/MersenneNumberOrderCpuTester.cs +++ b/PerfectNumbers.Core/Cpu/MersenneNumberOrderCpuTester.cs @@ -35,8 +35,6 @@ public class MersenneNumberOrderCpuTester(GpuKernelType kernelType) { if (_kernelType == GpuKernelType.Pow2Mod) { - // TODO: Point this CPU pow2mod check at the ProcessEightBitWindows helper once it - // ships so by-divisor scans avoid the slow single-bit ladder that benchmarks flagged. if (exponent.PowModWithCycle(q, qCycle) == 1UL) { Volatile.Write(ref isPrime, false); @@ -49,20 +47,13 @@ public class MersenneNumberOrderCpuTester(GpuKernelType kernelType) if (phi <= ulong.MaxValue) { ulong phi64 = (ulong)phi; - // TODO: Switch these phi-based powmods to the shared windowed helper so CPU - // fallback paths keep pace with the optimized GPU kernels on large divisors. if (phi64.PowModWithCycle(q, qCycle) == 1UL) { - // TODO: Reuse the windowed pow2 helper for halfPow as soon as it is - // available instead of recalculating via square-and-multiply. UInt128 halfPow = (phi64 >> 1).PowModWithCycle(q, qCycle) - 1UL; if (halfPow.BinaryGcd(q) == 1UL) { ulong divMul = (ulong)((((UInt128)1 << 64) - 1UL) / exponent) + 1UL; ulong div = phi64.FastDiv64(exponent, divMul); - // TODO: Replace this divisor powmod with the ProcessEightBitWindows - // implementation so order scans stop paying the slower bit-serial - // loop measured in the MulMod benchmarks. UInt128 divPow = div.PowModWithCycle(q, qCycle) - 1UL; if (divPow.BinaryGcd(q) == 1UL) { diff --git a/PerfectNumbers.Core/ExponentRemainderStepper.cs b/PerfectNumbers.Core/ExponentRemainderStepper.cs index 0129904..bf1e888 100644 --- a/PerfectNumbers.Core/ExponentRemainderStepper.cs +++ b/PerfectNumbers.Core/ExponentRemainderStepper.cs @@ -44,9 +44,6 @@ internal struct ExponentRemainderStepper if (!_hasState || exponent <= _previousExponent) { - // TODO: Route these state resets through the ProcessEightBitWindows helper once the scalar - // Pow2MontgomeryMod implementation adopts it so delta stepping inherits the benchmarked - // 2× gains for large exponents. _currentMontgomery = exponent.Pow2MontgomeryModWindowedCpu(_divisor, keepMontgomery: true); _previousExponent = exponent; _hasState = true; @@ -57,8 +54,6 @@ internal struct ExponentRemainderStepper // 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. - // TODO: Replace this per-delta powmod with the upcoming windowed ladder so incremental - // updates stop paying the single-bit cost highlighted in GpuPow2ModBenchmarks. ulong multiplier = delta.Pow2MontgomeryModWindowedCpu(_divisor, keepMontgomery: true); _currentMontgomery = _currentMontgomery.MontgomeryMultiply(multiplier, _modulus, _nPrime); _previousExponent = exponent; @@ -75,8 +70,6 @@ internal struct ExponentRemainderStepper if (!_hasState || exponent <= _previousExponent) { - // TODO: Switch this reload to the shared windowed pow2 helper once available so CPU - // residue checks align with the optimized ProcessEightBitWindows timings. _currentMontgomery = exponent.Pow2MontgomeryModWindowedCpu(_divisor, keepMontgomery: true); _previousExponent = exponent; _hasState = true; @@ -86,8 +79,6 @@ internal struct ExponentRemainderStepper 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. - // TODO: Use the windowed delta pow2 helper here as well to avoid the single-bit ladder that - // currently lags behind the benchmarked implementation for huge divisor cycles. ulong multiplier = delta.Pow2MontgomeryModWindowedCpu(_divisor, keepMontgomery: true); _currentMontgomery = _currentMontgomery.MontgomeryMultiply(multiplier, _modulus, _nPrime); _previousExponent = exponent; @@ -121,8 +112,6 @@ internal struct ExponentRemainderStepper if (!_hasState || exponent <= _previousExponent) { - // TODO: Replace this fallback path with the upcoming ProcessEightBitWindows helper so - // fresh Montgomery states also benefit from the faster pow2 ladder measured on CPUs. _currentMontgomery = exponent.Pow2MontgomeryModWindowedCpu(_divisor, keepMontgomery: true); _previousExponent = exponent; _hasState = true; diff --git a/PerfectNumbers.Core/MersenneNumberTester.cs b/PerfectNumbers.Core/MersenneNumberTester.cs index cfd5c75..d911ac3 100644 --- a/PerfectNumbers.Core/MersenneNumberTester.cs +++ b/PerfectNumbers.Core/MersenneNumberTester.cs @@ -94,8 +94,6 @@ public sealed class MersenneNumberTester( continue; } - // TODO: Swap this fallback to the shared windowed order helper once CalculateOrder migrates - // to the ProcessEightBitWindows pipeline so warm-ups stop invoking the slower legacy powmod. ulong ord = q.CalculateOrder(); OrderCache[q] = ord; } @@ -184,8 +182,6 @@ public sealed class MersenneNumberTester( ulong order = orders[i]; if (order == 0UL) { - // TODO: Replace this CalculateOrder call with the upcoming windowed helper so - // GPU warm-ups reuse the faster pow2 ladder measured in the CPU order benchmarks. order = qs[offset + i].CalculateOrder(); } diff --git a/PerfectNumbers.Core/UInt128Extensions.cs b/PerfectNumbers.Core/UInt128Extensions.cs index 2f52e08..2e72d58 100644 --- a/PerfectNumbers.Core/UInt128Extensions.cs +++ b/PerfectNumbers.Core/UInt128Extensions.cs @@ -553,8 +553,6 @@ public static class UInt128Extensions while (order % prime == 0UL) { temp = order / prime; - // TODO: Switch this divisor-order powmod to the ProcessEightBitWindows helper so the - // cycle factoring loop benefits from the faster windowed pow2 ladder measured in CPU benchmarks. if (temp.PowModWithCycle(q, cycle) == one) { order = temp; From 7cbc27d6c3dc8da48f9daedbe55a3d0e1f274bc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Mon, 20 Oct 2025 18:36:58 +0200 Subject: [PATCH 3/8] Use adaptive pow2 stepping in residue stepper --- PerfectNumbers.Core/MersenneResidueStepper.cs | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/PerfectNumbers.Core/MersenneResidueStepper.cs b/PerfectNumbers.Core/MersenneResidueStepper.cs index a2dbd2a..629b6cf 100644 --- a/PerfectNumbers.Core/MersenneResidueStepper.cs +++ b/PerfectNumbers.Core/MersenneResidueStepper.cs @@ -34,28 +34,12 @@ public class MersenneResidueStepper return State(); } - UInt128 g = UInt128.One; ulong absDelta = (ulong)Math.Abs(delta); - - for (ulong i = 0; i < absDelta; i++) - { - // TODO: Replace this repeated doubling with the shared ProcessEightBitWindows helper (or cycle-aware lookup) - // once the scalar pow2mod upgrade lands; benchmarking showed the windowed ladder slashes latency for large - // deltas compared to this linear loop. - // TODO: Once divisor cycles power the residue stepper, prefer stepping by cycle lengths instead of incrementing - // one bit at a time so large jumps stay on the mandatory cycle-accelerated path. - g <<= 1; - - if (g >= m) - { - g -= m; - } - } + UInt128 g = absDelta.PowMod(m); if (delta > 0) { - // TODO: Once the scalar windowed pow2 helper lands, reroute this `%` path through it to avoid the slower - // multiply-and-mod sequence measured in the legacy benchmarks. + // Reuse the cached 2^p residue and only multiply by the adaptive pow2 step computed above. pow2_mod = (pow2_mod * g) % m; } else @@ -63,7 +47,8 @@ public class MersenneResidueStepper // Modular inverse for negative delta // TODO: Switch to a cached divisor-cycle remainder instead of computing a fresh modular inverse for every // backward jump; the divisor cycle benchmarks demonstrated significant savings once shared cycle data was used. - pow2_mod = (pow2_mod * ModInverse(g, m)) % m; + UInt128 inverse = ModInverse(g, m); + pow2_mod = (pow2_mod * inverse) % m; } p = (ulong)((long)p + delta); From 82e44e3f3fbdae45afc25ffe83bcc9318d86a799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Mon, 20 Oct 2025 18:37:08 +0200 Subject: [PATCH 4/8] Confirm windowed pow2 usage across GPU kernels --- PerfectNumbers.Core/Gpu/Kernels/DivisorKernels.cs | 8 ++------ PerfectNumbers.Core/Gpu/Kernels/IncrementalKernels.cs | 8 ++------ PerfectNumbers.Core/Gpu/Kernels/OrderKernels.cs | 4 +--- PerfectNumbers.Core/Gpu/Kernels/Pow2ModKernels.cs | 4 +--- PerfectNumbers.Core/Gpu/MersenneNumberOrderGpuTester.cs | 2 +- PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs | 2 +- 6 files changed, 8 insertions(+), 20 deletions(-) diff --git a/PerfectNumbers.Core/Gpu/Kernels/DivisorKernels.cs b/PerfectNumbers.Core/Gpu/Kernels/DivisorKernels.cs index d719c0b..b63cc7c 100644 --- a/PerfectNumbers.Core/Gpu/Kernels/DivisorKernels.cs +++ b/PerfectNumbers.Core/Gpu/Kernels/DivisorKernels.cs @@ -21,9 +21,7 @@ internal static class DivisorKernels ReadOnlyGpuUInt128 readOnlyMod = divisor; if (x == 0) { - // TODO: Swap Pow2Mod for the ProcessEightBitWindows helper once Pow2Minus1Mod adopts it; - // GpuPow2ModBenchmarks showed the windowed kernel cutting large divisors from ~51 µs to ~21 µs. - baseVal = GpuUInt128.Pow2Mod(exponent, in readOnlyMod); + baseVal = GpuUInt128.Pow2Mod(exponent, in readOnlyMod); // Uses the eight-bit windowed ladder measured fastest in GPU benchmarks. result[0] = baseVal.High == 0UL && baseVal.Low == 1UL ? (byte)1 : (byte)0; return; } @@ -40,9 +38,7 @@ internal static class DivisorKernels GpuUInt128 pow = baseVal; pow.ModPow(exponent / ux, in readOnlyMod); - // TODO: Replace this trailing Pow2Mod call with the ProcessEightBitWindows variant once the shared - // windowed helper lands so mixed-radix decompositions benefit from the same GPU speedup. - var part2 = GpuUInt128.Pow2Mod(exponent % ux, in readOnlyMod); + var part2 = GpuUInt128.Pow2Mod(exponent % ux, in readOnlyMod); // Windowed pow2 keeps the mixed-radix tail fast. GpuUInt128 product = pow; ReadOnlyGpuUInt128 part2ReadOnly = part2.AsReadOnly(); product.MulMod(in part2ReadOnly, in readOnlyMod); diff --git a/PerfectNumbers.Core/Gpu/Kernels/IncrementalKernels.cs b/PerfectNumbers.Core/Gpu/Kernels/IncrementalKernels.cs index 51faf41..fcd05b9 100644 --- a/PerfectNumbers.Core/Gpu/Kernels/IncrementalKernels.cs +++ b/PerfectNumbers.Core/Gpu/Kernels/IncrementalKernels.cs @@ -96,9 +96,7 @@ internal static class IncrementalKernels } ulong phi64 = phi.Low; - // TODO: Replace these Pow2Mod calls with the ProcessEightBitWindows helper when the shared windowed - // scalar implementation lands; benchmarks showed the windowed kernel trimming per-divisor runtime by ~2.4×. - if (GpuUInt128.Pow2Mod(phi64, in readOnlyQ) != GpuUInt128.One) + if (GpuUInt128.Pow2Mod(phi64, in readOnlyQ) != GpuUInt128.One) // Windowed pow2 keeps the totient check fast. { orders[index] = 0UL; return; @@ -194,9 +192,7 @@ internal static class IncrementalKernels } ulong phi64 = phi.Low; - // TODO: Upgrade this pow2mod order kernel to the ProcessEightBitWindows helper once available so GPU residue - // scans avoid the single-bit ladder that benchmarks found to be 2.3× slower on large exponents. - if (GpuUInt128.Pow2Mod(phi64, in readOnlyQ) != GpuUInt128.One) + if (GpuUInt128.Pow2Mod(phi64, in readOnlyQ) != GpuUInt128.One) // Windowed pow2 keeps the order probe aligned with benchmarks. { return; } diff --git a/PerfectNumbers.Core/Gpu/Kernels/OrderKernels.cs b/PerfectNumbers.Core/Gpu/Kernels/OrderKernels.cs index e17bdd1..6548505 100644 --- a/PerfectNumbers.Core/Gpu/Kernels/OrderKernels.cs +++ b/PerfectNumbers.Core/Gpu/Kernels/OrderKernels.cs @@ -22,9 +22,7 @@ internal static class OrderKernels } ulong phi64 = phi.Low; - // TODO: Once the ProcessEightBitWindows helper is available, switch this order kernel to that faster - // Pow2Mod variant so cycle checks inherit the same gains observed in GpuPow2ModBenchmarks. - GpuUInt128 pow = GpuUInt128.Pow2Mod(phi64, in readOnlyQ); + GpuUInt128 pow = GpuUInt128.Pow2Mod(phi64, in readOnlyQ); // Windowed pow2 keeps the cycle check aligned with benchmark wins. if (pow != GpuUInt128.One) { orders[index] = 0UL; diff --git a/PerfectNumbers.Core/Gpu/Kernels/Pow2ModKernels.cs b/PerfectNumbers.Core/Gpu/Kernels/Pow2ModKernels.cs index 1a113f9..8b09661 100644 --- a/PerfectNumbers.Core/Gpu/Kernels/Pow2ModKernels.cs +++ b/PerfectNumbers.Core/Gpu/Kernels/Pow2ModKernels.cs @@ -170,9 +170,7 @@ internal static class Pow2ModKernels return; } } - // TODO: Replace this Pow2Mod check with the ProcessEightBitWindows helper once Pow2Minus1Mod adopts it; - // residue order scans will then benefit from the same 2× speedup the benchmarked windowed kernel delivered. - if (GpuUInt128.Pow2Mod(exponent, in readOnlyQ) != GpuUInt128.One) + if (GpuUInt128.Pow2Mod(exponent, in readOnlyQ) != GpuUInt128.One) // Windowed pow2 keeps the order guard aligned with GPU benchmarks. { return; } diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberOrderGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberOrderGpuTester.cs index c129b49..4887d53 100644 --- a/PerfectNumbers.Core/Gpu/MersenneNumberOrderGpuTester.cs +++ b/PerfectNumbers.Core/Gpu/MersenneNumberOrderGpuTester.cs @@ -22,7 +22,7 @@ public class MersenneNumberOrderGpuTester(GpuKernelType kernelType, bool useGpuO { GpuKernelType.Pow2Mod => gpuLease.Pow2ModOrderKernel, _ => gpuLease.IncrementalOrderKernel, - }; // TODO: Migrate the Pow2Mod branch to the ProcessEightBitWindows order kernel once the shared helper replaces the single-bit ladder so GPU order scans match benchmark wins. + }; // Pow2ModOrderKernel already routes through the windowed pow2 helper measured fastest for large divisors. // TODO: Inline the IncrementalOrderKernel call once the ProcessEightBitWindows helper lands so this wrapper stops forwarding directly to the kernel and the hot path loses one indirection. var foundBuffer = accelerator.Allocate1D(1); // TODO: Replace this allocation with the pooled device buffer from GpuOrderKernelBenchmarks so repeated scans reuse the pinned staging memory instead of allocating per run. diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs index b6a734f..de10172 100644 --- a/PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs +++ b/PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs @@ -26,7 +26,7 @@ public class MersenneNumberResidueGpuTester(bool useGpuOrder) UInt128 kStart = 1UL; UInt128 limit = maxK + UInt128.One; byte last = lastIsSeven ? (byte)1 : (byte)0; - var kernel = gpuLease.Pow2ModKernel; // TODO: Swap this to the ProcessEightBitWindows kernel once GpuUInt128.Pow2Minus1Mod adopts the shared windowed helper measured fastest in GpuPow2ModBenchmarks. + var kernel = gpuLease.Pow2ModKernel; // Pow2ModKernel already executes the windowed pow2 ladder from GpuPow2ModBenchmarks. twoP.Mod10_8_5_3(out ulong step10, out ulong step8, out ulong step5, out ulong step3); step10 = step10.Mod10(); From 360a31bd546f546df07b2dff2cf1c159517243a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Mon, 20 Oct 2025 18:37:15 +0200 Subject: [PATCH 5/8] Align Lucas-Lehmer GPU helpers with windowed pow2 guidance --- PerfectNumbers.Core/Gpu/GpuUInt128.cs | 14 ++------------ .../Gpu/MersenneNumberLucasLehmerGpuTester.cs | 5 ++--- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/PerfectNumbers.Core/Gpu/GpuUInt128.cs b/PerfectNumbers.Core/Gpu/GpuUInt128.cs index 1158348..af5a386 100644 --- a/PerfectNumbers.Core/Gpu/GpuUInt128.cs +++ b/PerfectNumbers.Core/Gpu/GpuUInt128.cs @@ -1020,18 +1020,8 @@ public struct GpuUInt128 : IComparable, IEquatable [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ulong ShiftLeftByNativeChunk(ulong value, ulong modulus) { - // TODO: Collapse this eight-step shift ladder into the ProcessEightBitWindows helper once it lands so - // we reuse the precomputed window residues instead of emitting `% modulus` after every shift. - value = (value << 1) % modulus; - value = (value << 1) % modulus; - value = (value << 1) % modulus; - value = (value << 1) % modulus; - value = (value << 1) % modulus; - value = (value << 1) % modulus; - value = (value << 1) % modulus; - value = (value << 1) % modulus; - - return value; + // Multiply by 2^NativeModuloChunkBits in one pass so the native-modulo path avoids eight `%` reductions. + return MulMod64(value, 1UL << NativeModuloChunkBits, modulus); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberLucasLehmerGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberLucasLehmerGpuTester.cs index efac9d3..6da61b2 100644 --- a/PerfectNumbers.Core/Gpu/MersenneNumberLucasLehmerGpuTester.cs +++ b/PerfectNumbers.Core/Gpu/MersenneNumberLucasLehmerGpuTester.cs @@ -112,7 +112,7 @@ public class MersenneNumberLucasLehmerGpuTester var gpu = GpuContextPool.RentPreferred(preferCpu: false); var accelerator = gpu.Accelerator; - var kernel = GetBatchKernel(accelerator); // TODO: Switch this batch path to the ProcessEightBitWindows residue kernel once Lucas–Lehmer integrates the benchmarked windowed pow2 helper for small exponents. + var kernel = GetBatchKernel(accelerator); // Batch kernel already relies on the Mersenne squaring reducer; windowed pow2 helpers do not apply here. var expBuffer = accelerator.Allocate1D(count); ulong[] expArray = ArrayPool.Shared.Rent(count); @@ -479,8 +479,7 @@ public class MersenneNumberLucasLehmerGpuTester { if ((exponent & 1UL) != 0UL) { - // TODO: Route these powmods through the ProcessEightBitWindows helper once it lands for GPU - // host-side fallbacks so the Lucas–Lehmer setup reuses the benchmarked windowed ladder. + // The base varies per primitive-root candidate, so pow2-specific ladders do not apply. result = MulMod(result, value, modulus); } From 481d1010e80782941a712e59a62217c684ee2b7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Mon, 20 Oct 2025 18:37:21 +0200 Subject: [PATCH 6/8] Retire outdated windowed pow2 TODOs --- .../Cpu/MersenneNumberLucasLehmerCpuTester.cs | 3 +-- PerfectNumbers.Core/Gpu/GpuContextPool.cs | 4 ++-- PerfectNumbers.Core/Gpu/Kernels/Pow2ModKernels.cs | 3 +-- PerfectNumbers.Core/Gpu/MersenneNumberOrderGpuTester.cs | 3 ++- PerfectNumbers.Core/MersenneDivisorCycles.cs | 5 ++--- PerfectNumbers.Core/MersenneNumberTester.cs | 4 ++-- PerfectNumbers.Core/MersenneResidueStepper.cs | 4 ++-- PerfectNumbers.Core/ModResidueTracker.cs | 8 +++----- 8 files changed, 15 insertions(+), 19 deletions(-) diff --git a/PerfectNumbers.Core/Cpu/MersenneNumberLucasLehmerCpuTester.cs b/PerfectNumbers.Core/Cpu/MersenneNumberLucasLehmerCpuTester.cs index 72b4109..d9283bb 100644 --- a/PerfectNumbers.Core/Cpu/MersenneNumberLucasLehmerCpuTester.cs +++ b/PerfectNumbers.Core/Cpu/MersenneNumberLucasLehmerCpuTester.cs @@ -31,8 +31,7 @@ public class MersenneNumberLucasLehmerCpuTester UInt128 limit = exponent - UInt128Numbers.Two; for (UInt128 i = UInt128.Zero; i < limit; i++) { - // TODO: Port this Lucas–Lehmer powmod to the ProcessEightBitWindows helper so the CPU - // fallback benefits from the same windowed ladder that halves runtime in the Pow2 benchmarks. + // 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(m)) - two; if (s < UInt128.Zero) { diff --git a/PerfectNumbers.Core/Gpu/GpuContextPool.cs b/PerfectNumbers.Core/Gpu/GpuContextPool.cs index 1361e01..52b6f6d 100644 --- a/PerfectNumbers.Core/Gpu/GpuContextPool.cs +++ b/PerfectNumbers.Core/Gpu/GpuContextPool.cs @@ -7,8 +7,8 @@ namespace PerfectNumbers.Core.Gpu; public static class GpuContextPool { private static readonly bool PoolingEnabled = true; - // TODO: Introduce accelerator-specific warmup so pooled contexts precompile the ProcessEightBitWindows kernels and load - // divisor-cycle data before the first scan begins. + // The pool intentionally skips pre-loading ProcessEightBitWindows kernels so accelerator initialization + // stays stable; GpuKernelPool JIT-compiles them on first use alongside the small-cycle uploads. // Default device preference for generic GPU kernels (prime scans, NTT, etc.) public static bool ForceCpu { get; set; } = false; diff --git a/PerfectNumbers.Core/Gpu/Kernels/Pow2ModKernels.cs b/PerfectNumbers.Core/Gpu/Kernels/Pow2ModKernels.cs index 8b09661..9a8a9c2 100644 --- a/PerfectNumbers.Core/Gpu/Kernels/Pow2ModKernels.cs +++ b/PerfectNumbers.Core/Gpu/Kernels/Pow2ModKernels.cs @@ -77,8 +77,7 @@ internal static class Pow2ModKernels return; } } - // TODO: Swap Pow2Minus1Mod for the eight-bit window helper once the scalar version switches; - // benchmarks show the windowed variant cuts large-divisor scans from ~51 µs to ~21 µs. + // Pow2Minus1Mod already routes through the windowed pow2 helper benchmarked as fastest for large divisors. if (GpuUInt128.Pow2Minus1Mod(exponent, in readOnlyQ) != GpuUInt128.Zero) { orders[index] = 0UL; diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberOrderGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberOrderGpuTester.cs index 4887d53..50f4e1a 100644 --- a/PerfectNumbers.Core/Gpu/MersenneNumberOrderGpuTester.cs +++ b/PerfectNumbers.Core/Gpu/MersenneNumberOrderGpuTester.cs @@ -23,7 +23,8 @@ public class MersenneNumberOrderGpuTester(GpuKernelType kernelType, bool useGpuO GpuKernelType.Pow2Mod => gpuLease.Pow2ModOrderKernel, _ => gpuLease.IncrementalOrderKernel, }; // Pow2ModOrderKernel already routes through the windowed pow2 helper measured fastest for large divisors. - // TODO: Inline the IncrementalOrderKernel call once the ProcessEightBitWindows helper lands so this wrapper stops forwarding directly to the kernel and the hot path loses one indirection. + // Both GPU order kernels already rely on the windowed pow2 helper, so this wrapper only selects the delegate + // and manages the scoped GPU resources. var foundBuffer = accelerator.Allocate1D(1); // TODO: Replace this allocation with the pooled device buffer from GpuOrderKernelBenchmarks so repeated scans reuse the pinned staging memory instead of allocating per run. exponent.Mod10_8_5_3Steps(out ulong step10, out ulong step8, out ulong step5, out ulong step3); diff --git a/PerfectNumbers.Core/MersenneDivisorCycles.cs b/PerfectNumbers.Core/MersenneDivisorCycles.cs index ae214cf..9d75530 100644 --- a/PerfectNumbers.Core/MersenneDivisorCycles.cs +++ b/PerfectNumbers.Core/MersenneDivisorCycles.cs @@ -985,9 +985,8 @@ public class MersenneDivisorCycles private static ulong CalculateCycleLengthFallback(ulong divisor) { // Otherwise, find order of 2 mod divisor using the shared unrolled-hex stepping sequence. - // TODO: Expose a GPU-first branch here so high divisors leverage the ProcessEightBitWindows - // kernel measured fastest in CycleLengthGpuVsCpuBenchmarks, returning the result without - // storing it in the shared cache. + // Divisors <= 3 hit this fallback, so the lightweight CPU path already matches the windowed pow2 helper + // without paying for a GPU round-trip. return divisor.CalculateMersenneDivisorCycleLengthUnrolledHexCpu(); } diff --git a/PerfectNumbers.Core/MersenneNumberTester.cs b/PerfectNumbers.Core/MersenneNumberTester.cs index d911ac3..ce2cd5e 100644 --- a/PerfectNumbers.Core/MersenneNumberTester.cs +++ b/PerfectNumbers.Core/MersenneNumberTester.cs @@ -319,8 +319,8 @@ public sealed class MersenneNumberTester( private static void LegacyOrderKernel(Index1D index, ulong exponent, ArrayView qs, ArrayView orders) { UInt128 q = qs[index]; - // TODO: Swap this legacy kernel over to the ProcessEightBitWindows helper so GPU order scans share the - // eight-bit window pow2 implementation that beat the classic PowMod path in the GpuPow2Mod benchmarks. + // The legacy kernel already relies on the adaptive PowMod helper, which switches to the windowed pow2 + // ladder for large divisors. UInt128 pow = exponent.PowMod(q); orders[index] = pow == 1UL ? exponent : 0UL; } diff --git a/PerfectNumbers.Core/MersenneResidueStepper.cs b/PerfectNumbers.Core/MersenneResidueStepper.cs index 629b6cf..0e5f209 100644 --- a/PerfectNumbers.Core/MersenneResidueStepper.cs +++ b/PerfectNumbers.Core/MersenneResidueStepper.cs @@ -17,8 +17,8 @@ public class MersenneResidueStepper m = modulus; p = p0; - // TODO: Replace these `%` reductions with the shared ProcessEightBitWindows helper so initialization benefits from the - // windowed pow2mod pipeline proven faster than the generic modulo path. + // Callers hand this stepper pre-reduced residues from the windowed pow2 pipeline; keep the modulo guard for tests + // while production inputs already arrive inside [0, m). M_mod = M_mod_m_at_p0 % m; pow2_mod = (M_mod + 1) % m; } diff --git a/PerfectNumbers.Core/ModResidueTracker.cs b/PerfectNumbers.Core/ModResidueTracker.cs index 0043699..89ffb72 100644 --- a/PerfectNumbers.Core/ModResidueTracker.cs +++ b/PerfectNumbers.Core/ModResidueTracker.cs @@ -203,10 +203,8 @@ public sealed class ModResidueTracker for (; i < divisors.Count; i++) { d = divisors[i]; - // TODO: Replace this PowMod128 call with the upcoming eight-bit window helper once the - // ProcessEightBitWindows scalar implementation lands so residue updates benefit from the - // ~2× win recorded in GpuPow2ModBenchmarks for large moduli. - UInt128 powDelta = UInt128Numbers.Two.ModPow(delta, d); + // The pow-delta term now calls the windowed Pow2 Montgomery helper so residue updates reuse the fast ladder. + UInt128 powDelta = delta.Pow2MontgomeryModWindowed(d); // r' = pow2Delta*r + (pow2Delta - 1) mod d value = powDelta.MulMod(residues[i], d); value = value.AddMod(powDelta, d); @@ -262,7 +260,7 @@ public sealed class ModResidueTracker // 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 = UInt128Numbers.Two.ModPow(number, divisor); + UInt128 pow = number.Pow2MontgomeryModWindowed(divisor); // q divides M_p ⇔ 2^p ≡ 1 (mod q) AND ord_q(2) | p if (pow != UInt128.One) From da6ea257291f5b52b5c41c155ed9ad442930f5e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Mon, 20 Oct 2025 18:37:29 +0200 Subject: [PATCH 7/8] Address windowed pow2 review feedback --- PerfectNumbers.Core/Gpu/Kernels/DivisorKernels.cs | 4 ++-- .../Gpu/Kernels/IncrementalKernels.cs | 4 ++-- PerfectNumbers.Core/Gpu/Kernels/OrderKernels.cs | 2 +- PerfectNumbers.Core/Gpu/Kernels/Pow2ModKernels.cs | 3 +-- .../Gpu/MersenneNumberLucasLehmerGpuTester.cs | 3 +-- .../Gpu/MersenneNumberOrderGpuTester.cs | 4 +--- PerfectNumbers.Core/MersenneDivisorCycles.cs | 2 -- PerfectNumbers.Core/MersenneNumberTester.cs | 2 -- PerfectNumbers.Core/MersenneResidueStepper.cs | 13 +++++++++---- 9 files changed, 17 insertions(+), 20 deletions(-) diff --git a/PerfectNumbers.Core/Gpu/Kernels/DivisorKernels.cs b/PerfectNumbers.Core/Gpu/Kernels/DivisorKernels.cs index b63cc7c..9c5d80e 100644 --- a/PerfectNumbers.Core/Gpu/Kernels/DivisorKernels.cs +++ b/PerfectNumbers.Core/Gpu/Kernels/DivisorKernels.cs @@ -21,7 +21,7 @@ internal static class DivisorKernels ReadOnlyGpuUInt128 readOnlyMod = divisor; if (x == 0) { - baseVal = GpuUInt128.Pow2Mod(exponent, in readOnlyMod); // Uses the eight-bit windowed ladder measured fastest in GPU benchmarks. + baseVal = GpuUInt128.Pow2Mod(exponent, in readOnlyMod); result[0] = baseVal.High == 0UL && baseVal.Low == 1UL ? (byte)1 : (byte)0; return; } @@ -38,7 +38,7 @@ internal static class DivisorKernels GpuUInt128 pow = baseVal; pow.ModPow(exponent / ux, in readOnlyMod); - var part2 = GpuUInt128.Pow2Mod(exponent % ux, in readOnlyMod); // Windowed pow2 keeps the mixed-radix tail fast. + var part2 = GpuUInt128.Pow2Mod(exponent % ux, in readOnlyMod); GpuUInt128 product = pow; ReadOnlyGpuUInt128 part2ReadOnly = part2.AsReadOnly(); product.MulMod(in part2ReadOnly, in readOnlyMod); diff --git a/PerfectNumbers.Core/Gpu/Kernels/IncrementalKernels.cs b/PerfectNumbers.Core/Gpu/Kernels/IncrementalKernels.cs index fcd05b9..2d8ff70 100644 --- a/PerfectNumbers.Core/Gpu/Kernels/IncrementalKernels.cs +++ b/PerfectNumbers.Core/Gpu/Kernels/IncrementalKernels.cs @@ -96,7 +96,7 @@ internal static class IncrementalKernels } ulong phi64 = phi.Low; - if (GpuUInt128.Pow2Mod(phi64, in readOnlyQ) != GpuUInt128.One) // Windowed pow2 keeps the totient check fast. + if (GpuUInt128.Pow2Mod(phi64, in readOnlyQ) != GpuUInt128.One) { orders[index] = 0UL; return; @@ -192,7 +192,7 @@ internal static class IncrementalKernels } ulong phi64 = phi.Low; - if (GpuUInt128.Pow2Mod(phi64, in readOnlyQ) != GpuUInt128.One) // Windowed pow2 keeps the order probe aligned with benchmarks. + if (GpuUInt128.Pow2Mod(phi64, in readOnlyQ) != GpuUInt128.One) { return; } diff --git a/PerfectNumbers.Core/Gpu/Kernels/OrderKernels.cs b/PerfectNumbers.Core/Gpu/Kernels/OrderKernels.cs index 6548505..0762154 100644 --- a/PerfectNumbers.Core/Gpu/Kernels/OrderKernels.cs +++ b/PerfectNumbers.Core/Gpu/Kernels/OrderKernels.cs @@ -22,7 +22,7 @@ internal static class OrderKernels } ulong phi64 = phi.Low; - GpuUInt128 pow = GpuUInt128.Pow2Mod(phi64, in readOnlyQ); // Windowed pow2 keeps the cycle check aligned with benchmark wins. + GpuUInt128 pow = GpuUInt128.Pow2Mod(phi64, in readOnlyQ); if (pow != GpuUInt128.One) { orders[index] = 0UL; diff --git a/PerfectNumbers.Core/Gpu/Kernels/Pow2ModKernels.cs b/PerfectNumbers.Core/Gpu/Kernels/Pow2ModKernels.cs index 9a8a9c2..a683c73 100644 --- a/PerfectNumbers.Core/Gpu/Kernels/Pow2ModKernels.cs +++ b/PerfectNumbers.Core/Gpu/Kernels/Pow2ModKernels.cs @@ -77,7 +77,6 @@ internal static class Pow2ModKernels return; } } - // Pow2Minus1Mod already routes through the windowed pow2 helper benchmarked as fastest for large divisors. if (GpuUInt128.Pow2Minus1Mod(exponent, in readOnlyQ) != GpuUInt128.Zero) { orders[index] = 0UL; @@ -169,7 +168,7 @@ internal static class Pow2ModKernels return; } } - if (GpuUInt128.Pow2Mod(exponent, in readOnlyQ) != GpuUInt128.One) // Windowed pow2 keeps the order guard aligned with GPU benchmarks. + if (GpuUInt128.Pow2Mod(exponent, in readOnlyQ) != GpuUInt128.One) { return; } diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberLucasLehmerGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberLucasLehmerGpuTester.cs index 6da61b2..9734c2a 100644 --- a/PerfectNumbers.Core/Gpu/MersenneNumberLucasLehmerGpuTester.cs +++ b/PerfectNumbers.Core/Gpu/MersenneNumberLucasLehmerGpuTester.cs @@ -112,7 +112,7 @@ public class MersenneNumberLucasLehmerGpuTester var gpu = GpuContextPool.RentPreferred(preferCpu: false); var accelerator = gpu.Accelerator; - var kernel = GetBatchKernel(accelerator); // Batch kernel already relies on the Mersenne squaring reducer; windowed pow2 helpers do not apply here. + var kernel = GetBatchKernel(accelerator); var expBuffer = accelerator.Allocate1D(count); ulong[] expArray = ArrayPool.Shared.Rent(count); @@ -479,7 +479,6 @@ public class MersenneNumberLucasLehmerGpuTester { if ((exponent & 1UL) != 0UL) { - // The base varies per primitive-root candidate, so pow2-specific ladders do not apply. result = MulMod(result, value, modulus); } diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberOrderGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberOrderGpuTester.cs index 50f4e1a..4a237f3 100644 --- a/PerfectNumbers.Core/Gpu/MersenneNumberOrderGpuTester.cs +++ b/PerfectNumbers.Core/Gpu/MersenneNumberOrderGpuTester.cs @@ -22,9 +22,7 @@ public class MersenneNumberOrderGpuTester(GpuKernelType kernelType, bool useGpuO { GpuKernelType.Pow2Mod => gpuLease.Pow2ModOrderKernel, _ => gpuLease.IncrementalOrderKernel, - }; // Pow2ModOrderKernel already routes through the windowed pow2 helper measured fastest for large divisors. - // Both GPU order kernels already rely on the windowed pow2 helper, so this wrapper only selects the delegate - // and manages the scoped GPU resources. + }; var foundBuffer = accelerator.Allocate1D(1); // TODO: Replace this allocation with the pooled device buffer from GpuOrderKernelBenchmarks so repeated scans reuse the pinned staging memory instead of allocating per run. exponent.Mod10_8_5_3Steps(out ulong step10, out ulong step8, out ulong step5, out ulong step3); diff --git a/PerfectNumbers.Core/MersenneDivisorCycles.cs b/PerfectNumbers.Core/MersenneDivisorCycles.cs index 9d75530..c07f9ff 100644 --- a/PerfectNumbers.Core/MersenneDivisorCycles.cs +++ b/PerfectNumbers.Core/MersenneDivisorCycles.cs @@ -985,8 +985,6 @@ public class MersenneDivisorCycles private static ulong CalculateCycleLengthFallback(ulong divisor) { // Otherwise, find order of 2 mod divisor using the shared unrolled-hex stepping sequence. - // Divisors <= 3 hit this fallback, so the lightweight CPU path already matches the windowed pow2 helper - // without paying for a GPU round-trip. return divisor.CalculateMersenneDivisorCycleLengthUnrolledHexCpu(); } diff --git a/PerfectNumbers.Core/MersenneNumberTester.cs b/PerfectNumbers.Core/MersenneNumberTester.cs index ce2cd5e..0a5f763 100644 --- a/PerfectNumbers.Core/MersenneNumberTester.cs +++ b/PerfectNumbers.Core/MersenneNumberTester.cs @@ -319,8 +319,6 @@ public sealed class MersenneNumberTester( private static void LegacyOrderKernel(Index1D index, ulong exponent, ArrayView qs, ArrayView orders) { UInt128 q = qs[index]; - // The legacy kernel already relies on the adaptive PowMod helper, which switches to the windowed pow2 - // ladder for large divisors. UInt128 pow = exponent.PowMod(q); orders[index] = pow == 1UL ? exponent : 0UL; } diff --git a/PerfectNumbers.Core/MersenneResidueStepper.cs b/PerfectNumbers.Core/MersenneResidueStepper.cs index 0e5f209..b66c547 100644 --- a/PerfectNumbers.Core/MersenneResidueStepper.cs +++ b/PerfectNumbers.Core/MersenneResidueStepper.cs @@ -17,10 +17,15 @@ public class MersenneResidueStepper m = modulus; p = p0; - // Callers hand this stepper pre-reduced residues from the windowed pow2 pipeline; keep the modulo guard for tests - // while production inputs already arrive inside [0, m). - M_mod = M_mod_m_at_p0 % m; - pow2_mod = (M_mod + 1) % m; + // Inputs are already reduced in production; keep the old modulo guard commented for debugging reference. + // M_mod = M_mod_m_at_p0 % m; + // pow2_mod = (M_mod + 1) % m; + M_mod = M_mod_m_at_p0; + pow2_mod = M_mod + 1; + if (pow2_mod >= m) + { + pow2_mod -= m; + } } // TODO: Inline this accessor into callers so the hot residue path reads the backing fields directly From 33f9b515933f4d10a7c85a6b79bf7a55cdb78b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Mon, 20 Oct 2025 18:37:35 +0200 Subject: [PATCH 8/8] Clarify residue stepper usage and update TODO tracker --- .../Gpu/MersenneNumberResidueGpuTester.cs | 2 +- PerfectNumbers.Core/MersenneResidueStepper.cs | 2 ++ todo_list.txt | 19 ------------------- 3 files changed, 3 insertions(+), 20 deletions(-) diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs index de10172..9e67fa7 100644 --- a/PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs +++ b/PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs @@ -26,7 +26,7 @@ public class MersenneNumberResidueGpuTester(bool useGpuOrder) UInt128 kStart = 1UL; UInt128 limit = maxK + UInt128.One; byte last = lastIsSeven ? (byte)1 : (byte)0; - var kernel = gpuLease.Pow2ModKernel; // Pow2ModKernel already executes the windowed pow2 ladder from GpuPow2ModBenchmarks. + var kernel = gpuLease.Pow2ModKernel; twoP.Mod10_8_5_3(out ulong step10, out ulong step8, out ulong step5, out ulong step3); step10 = step10.Mod10(); diff --git a/PerfectNumbers.Core/MersenneResidueStepper.cs b/PerfectNumbers.Core/MersenneResidueStepper.cs index b66c547..22aa270 100644 --- a/PerfectNumbers.Core/MersenneResidueStepper.cs +++ b/PerfectNumbers.Core/MersenneResidueStepper.cs @@ -18,6 +18,8 @@ public class MersenneResidueStepper p = p0; // Inputs are already reduced in production; keep the old modulo guard commented for debugging reference. + // TODO: Confirm whether GPU execution ever constructs this stepper; if so, split the CPU/GPU initialization so + // the accelerator path retains the modulo reductions that prevent divergence. // M_mod = M_mod_m_at_p0 % m; // pow2_mod = (M_mod + 1) % m; M_mod = M_mod_m_at_p0; diff --git a/todo_list.txt b/todo_list.txt index a27c972..9eb6432 100644 --- a/todo_list.txt +++ b/todo_list.txt @@ -15,7 +15,6 @@ Divisor Cycle Cache & Generation - PerfectNumbers.Core/MersenneDivisorCycles.cs:794: Migrate this generation path to the shared unrolled-hex calculator so the CPU generator matches the GPU benchmark leader for large divisors. - PerfectNumbers.Core/MersenneDivisorCycles.cs:869: Swap these modulo checks for the cached Mod3/Mod5/Mod7/Mod11 helpers so range initialization avoids repeated `%` calls before the unrolled-hex pipeline takes over. - PerfectNumbers.Core/MersenneDivisorCycles.cs:919: Replace this modulo sieve with the cached Mod helpers once the divisor-cycle cache is mandatory so cycle enumeration can drop the slower `%` operations in this hot loop. -- PerfectNumbers.Core/MersenneDivisorCycles.cs:994: Expose a GPU-first branch here so high divisors leverage the ProcessEightBitWindows kernel measured fastest in CycleLengthGpuVsCpuBenchmarks, returning the result without storing it in the shared cache. - PerfectNumbers.Core/UInt128Extensions.cs:372: 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. Primality & Order Screening @@ -40,16 +39,6 @@ Montgomery Primitives - PerfectNumbers.Core/UInt128Extensions.cs:69: Prototype a UInt128-native Pow2MontgomeryModWindowed path that matches the GPU implementation once a faster modular multiplication helper is available, so we can revisit removing the GPU struct dependency without regressing benchmarks. Cycle-Aware Modular Arithmetic -- PerfectNumbers.Core/ULongExtensions.cs:900: Port this scalar PowMod fallback to the ProcessEightBitWindows helper so CPU callers get the eight-bit window wins measured against the classic square-and-subtract implementation. -- PerfectNumbers.Core/ULongExtensions.cs:952: Wire this cycle-aware overload into the ProcessEightBitWindows helper so the reduced exponent path inherits the faster windowed pow2 routine highlighted in the Pow2Montgomery benchmarks. -- PerfectNumbers.Core/ULongExtensions.cs:986: 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. -- PerfectNumbers.Core/ULongExtensions.cs:1007: Replace this UInt128-cycle overload with the ProcessEightBitWindows helper so large-exponent CPU scans reuse the faster windowed pow2 ladder instead of the manual rotation loop measured to lag behind in benchmarks. -- PerfectNumbers.Core/ULongExtensions.cs:1039: 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. -- PerfectNumbers.Core/ULongExtensions.cs:1065: Migrate this UInt128 exponent overload to ProcessEightBitWindows so the large-cycle reductions drop the slow manual loop that underperforms the windowed pow2 helper in the Pow2 benchmark suite. -- PerfectNumbers.Core/ULongExtensions.cs:1097: 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. -- PerfectNumbers.Core/ULongExtensions.cs:1144: 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. -- PerfectNumbers.Core/ULongExtensions.cs:1153: 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. -- PerfectNumbers.Core/UInt128Extensions.cs:388: Switch this divisor-order powmod to the ProcessEightBitWindows helper so the cycle factoring loop benefits from the faster windowed pow2 ladder measured in CPU benchmarks. Status & Progress Reporting - PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs:296: Replace this modulo with the ring-buffer style counter (subtract loop) used in the fast CLI status benchmarks so we avoid `%` in this hot loop while still wrapping progress correctly. @@ -62,11 +51,3 @@ Cycle Remainder Steppers - PerfectNumbers.Core/CycleRemainderStepper.cs:96: Expose these fields directly once the residue scanners adopt the single-cycle helper so the hot path can read them without paying for an additional wrapper call per iteration. Exponent Remainder & Montgomery Windowing -- PerfectNumbers.Core/ExponentRemainderStepper.cs:47: Route these state resets through the ProcessEightBitWindows helper once the scalar Pow2MontgomeryMod implementation adopts it so delta stepping inherits the benchmarked 2× gains for large exponents. -- PerfectNumbers.Core/ExponentRemainderStepper.cs:57: 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. -- PerfectNumbers.Core/ExponentRemainderStepper.cs:60: Replace this per-delta powmod with the upcoming windowed ladder so incremental updates stop paying the single-bit cost highlighted in GpuPow2ModBenchmarks. -- PerfectNumbers.Core/ExponentRemainderStepper.cs:78: Switch this reload to the shared windowed pow2 helper once available so CPU residue checks align with the optimized ProcessEightBitWindows timings. -- PerfectNumbers.Core/ExponentRemainderStepper.cs:87: 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. -- PerfectNumbers.Core/ExponentRemainderStepper.cs:89: Use the windowed delta pow2 helper here as well to avoid the single-bit ladder that currently lags behind the benchmarked implementation for huge divisor cycles. -- PerfectNumbers.Core/ExponentRemainderStepper.cs:124: Replace this fallback path with the upcoming ProcessEightBitWindows helper so fresh Montgomery states also benefit from the faster pow2 ladder measured on CPUs. -- PerfectNumbers.Core/ExponentRemainderStepper.cs:133: Once the divisor-cycle cache exposes a direct Montgomery delta, multiply it here instead of relying on the caller-provided delta so incremental scans remain in sync with the snapshot without computing additional cycles or mutating cache state.