Add TODOs for divisor metadata caching

This commit is contained in:
Marek Lemańczyk committed 2025-10-01 22:52:21 +02:00
1 parent 513fc7acb0
commit 36d316d19c
53 files changed
+929 -393

No files matched your search

+15 -1
View File
@@ -199,6 +199,8 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
ArrayView1D<ulong, Stride1D.Dense> smallCycles)
{
ulong idx = (ulong)index.X;
// TODO: Replace these `%` computations with the precomputed Mod3/Mod5 tables so GPU kernels reuse cached residues
// instead of performing modulo operations that the benchmarks showed slower on wide sweeps.
ulong idxMod3 = idx % 3UL;
ulong idxMod5 = idx % 5UL;
// residue automaton
@@ -216,7 +218,9 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
}
else
{
ulong step3 = ((exponent % 3UL) << 1) % 3UL;
// TODO: Swap these `%` filters to the shared Mod helpers so the residue automaton matches the benchmarked
// bitmask-based implementation for GPU workloads.
ulong step3 = ((exponent % 3UL) << 1) % 3UL;
ulong r3 = q0m3 + (step3 * idxMod3); r3 -= (r3 >= 3UL) ? 3UL : 0UL;
if (r3 == 0UL)
{
@@ -264,6 +268,8 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
}
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, q) != GpuUInt128.One)
{
orders[index] = 0UL;
@@ -299,6 +305,8 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
}
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, q);
if (pow != GpuUInt128.One)
{
@@ -409,6 +417,8 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
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.
if (GpuUInt128.Pow2Minus1Mod(exponent, q) != GpuUInt128.Zero)
{
orders[index] = 0UL;
@@ -563,6 +573,8 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
}
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, q) != GpuUInt128.One)
{
return;
@@ -639,6 +651,8 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
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, q) != GpuUInt128.One)
{
return;
+20
View File
@@ -256,6 +256,9 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
return Zero;
}
// TODO: Replace this single-bit ladder with the ProcessEightBitWindows strategy measured
// in GpuPow2ModBenchmarks once the GPU kernels can share the windowed helper. The windowed
// version held 21.5 µs for 33-bit moduli versus 51.0 µs here when scanning large divisors.
GpuUInt128 result = Zero;
ulong i = 0UL;
while (i < exponent)
@@ -287,6 +290,8 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
ulong e0 = 0UL, e1 = 0UL, e2 = 0UL, e3 = 0UL, e4 = 0UL, e5 = 0UL, e6 = 0UL, e7 = 0UL;
bool a0 = false, a1 = false, a2 = false, a3 = false, a4 = false, a5 = false, a6 = false, a7 = false;
// TODO: Mirror the eight-bit window batching once the scalar helper above switches; the
// current bit-serial loop lags by ~3× on 8k–33-bit moduli compared to the windowed path.
int len = exponents.Length;
if (len > 0) { e0 = exponents[0]; a0 = true; }
if (len > 1) { e1 = exponents[1]; a1 = true; }
@@ -372,6 +377,9 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddMod(GpuUInt128 value, ulong modulus)
{
// TODO: Pre-reduce the operands via the Montgomery ladder used in MulMod64Benchmarks so the GPU
// compatible shim stops paying for `%` on every call; the InlineUInt128 helper ran 6–82× faster on
// large 64-bit workloads while preserving compatibility with the CPU scanner.
ulong a = Low % modulus;
ulong b = value.Low % modulus;
ulong sum = a + b;
@@ -387,6 +395,9 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddMod(ulong value, ulong modulus)
{
// TODO: Fold these operands with the ImmediateModulo helper once the GPU shim exposes it, avoiding
// repeated `%` reductions that the benchmarks showed are far slower than the Montgomery-based path
// for dense operands.
ulong a = Low % modulus;
ulong b = value % modulus;
ulong sum = a + b;
@@ -718,6 +729,8 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
/// </summary>
internal void MulModByLimb(GpuUInt128 other, GpuUInt128 modulus)
{
// TODO: Relocate this limb-based reducer to the benchmark project once the production
// pipeline switches to the faster allocating legacy path demonstrated in the benchmarks.
MultiplyFull(this, other, out var p3, out var p2, out var p1, out var p0);
High = p3;
@@ -808,6 +821,8 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly ulong MulModWithNativeModulo(ulong value, ulong modulus)
{
// TODO: Drop this native-modulo path from production after migrating callers to MulMod,
// which benchmarked 4-8× faster on dense operands and still wins on mixed workloads.
ulong multiplicand = Low % modulus;
var remainder = value % modulus;
@@ -891,6 +906,8 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
[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;
@@ -929,6 +946,8 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly ulong MulModMontgomery64(GpuUInt128 other, ulong modulus, ulong nPrime, ulong r2)
{
// TODO: Retire this struct-based Montgomery path from production after adopting the extension
// helper, which benchmarks 6-7× faster across dense and near-modulus operands.
ulong modulusLocal = modulus;
ulong a = Low % modulusLocal;
ulong b = other.Low % modulusLocal;
@@ -947,6 +966,7 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly ulong MulModMontgomery64(ulong other, ulong modulus, ulong nPrime, ulong r2)
{
// TODO: Same as above—migrate callers to the scalar extension to avoid this 6-7× slowdown.
ulong modulusLocal = modulus;
ulong a = Low % modulusLocal;
ulong b = other % modulusLocal;
@@ -46,7 +46,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
public bool UseDivisorCycles
{
get => _useDivisorCycles;
set => _useDivisorCycles = value;
set => _useDivisorCycles = value; // TODO: Remove this flag once GPU runs unconditionally fetch cycle lengths so every divisor check benefits from the measured speedups.
}
public void ConfigureFromMaxPrime(ulong maxPrime)
@@ -295,6 +295,9 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
for (int i = 0; i < batchSize; i++)
{
// TODO: Stage MontgomeryDivisorData in a GPU-ready cache keyed by divisor so we can copy
// precomputed ProcessEightBitWindows metadata directly instead of rehydrating the slower
// Montgomery structs for each batch item.
divisorDataSpan[i] = MontgomeryDivisorDataCache.Get(divisorSpan[i]);
}
@@ -397,6 +400,8 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
ulong remainder = prime;
if (remainder >= cycleLength)
{
// TODO: Replace this `%` with the cycle-cache helper that subtracts precomputed
// remainders (or uses a lookup table) so GPU batching avoids modulo in this inner loop.
remainder %= cycleLength;
}
@@ -456,6 +461,8 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
for (int i = 0; i < batchSize; i++)
{
// TODO: Preload ProcessEightBitWindows-friendly divisor metadata for GPU batches so this
// hot loop stops rebuilding MontgomeryDivisorData structures for every scan chunk.
divisorDataSpan[i] = MontgomeryDivisorDataCache.Get(divisorSpan[i]);
}
@@ -541,7 +548,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
}
ulong exponent = exponents[index];
hits[index] = exponent.Pow2MontgomeryMod(divisor) == 1UL ? (byte)1 : (byte)0;
hits[index] = exponent.Pow2MontgomeryMod(divisor) == 1UL ? (byte)1 : (byte)0; // TODO: Replace Pow2MontgomeryMod with the ProcessEightBitWindows helper once available so GPU by-divisor scans reuse the benchmarked fast pow2 ladder.
}
public sealed class DivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession
@@ -762,6 +769,8 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
if (workValue >= divisorCycle)
{
// TODO: Replace this `%` with the divisor-cycle remainder helper so we reuse cached
// modulo results instead of recomputing them in the GPU staging loop.
workValue %= divisorCycle;
}
@@ -831,7 +840,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
ulong exponent = exponents[index];
results[index] = exponent.Pow2MontgomeryModMontgomery(divisor);
results[index] = exponent.Pow2MontgomeryModMontgomery(divisor); // TODO: Swap this Montgomery-domain pow2 with the windowed helper slated to replace the slower ladder in Pow2Minus1Mod benchmarks.
}
public ulong DivisorLimit
@@ -75,6 +75,8 @@ public sealed class MersenneNumberDivisorGpuTester
for (int k = 0; k < len; k++)
{
(ulong dSmall, cycle) = candidates[k];
// TODO: Replace this `%` check with the shared divisor-cycle divisibility helper so GPU scans
// reuse the cached cycle remainders instead of recomputing modulo for every candidate.
if (p % cycle != 0UL)
{
continue;
@@ -109,10 +111,12 @@ public sealed class MersenneNumberDivisorGpuTester
}
UInt128 cycle128 = MersenneDivisorCycles.GetCycle(d);
if ((UInt128)p % cycle128 != UInt128.Zero)
{
continue;
}
// TODO: Feed this check through the ProcessEightBitWindows-based residue tracker so we avoid the slow
// UInt128 modulo on the CPU fallback highlighted by the Pow2Montgomery benchmarks.
if ((UInt128)p % cycle128 != UInt128.Zero)
{
continue;
}
if (IsDivisible(p, d))
{
@@ -138,9 +142,11 @@ public sealed class MersenneNumberDivisorGpuTester
? 63 - XMath.LeadingZeroCount(divisor.Low)
: 127 - XMath.LeadingZeroCount(divisor.High);
mod = divisor;
if (x == 0)
{
baseVal = GpuUInt128.Pow2Mod(exponent, mod);
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, mod);
result[0] = baseVal.High == 0UL && baseVal.Low == 1UL ? (byte)1 : (byte)0;
return;
}
@@ -155,9 +161,11 @@ public sealed class MersenneNumberDivisorGpuTester
baseVal = new GpuUInt128(0UL, 1UL << x);
}
GpuUInt128 pow = baseVal;
pow.ModPow(exponent / ux, mod);
var part2 = GpuUInt128.Pow2Mod(exponent % ux, mod);
GpuUInt128 pow = baseVal;
pow.ModPow(exponent / ux, mod);
// 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, mod);
GpuUInt128 product = pow;
product.MulMod(part2, mod);
result[0] = product.High == 0UL && product.Low == 1UL ? (byte)1 : (byte)0;
@@ -20,7 +20,7 @@ public class MersenneNumberIncrementalGpuTester(GpuKernelType kernelType, bool u
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
byte last = lastIsSeven ? (byte)1 : (byte)0; // ILGPU kernels do not support bool parameters
var pow2Kernel = gpuLease.Pow2ModKernel;
var pow2Kernel = gpuLease.Pow2ModKernel; // TODO: Route this binding to the ProcessEightBitWindows kernel once the scalar helper lands so GPU incremental scans inherit the windowed speedup from GpuPow2ModBenchmarks.
var incKernel = gpuLease.IncrementalKernel;
exponent.Mod10_8_5_3Steps(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
GpuUInt128 twoPGpu = (GpuUInt128)twoP;
@@ -51,6 +51,7 @@ public class MersenneNumberLucasLehmerGpuTester
// Early rejections aligned with incremental/order sieves, but safe for small p:
// - If 3 | p and p != 3, then 7 | M_p -> composite.
// - If p ≡ 1 (mod 4) and p shares a factor with (p-1), reject fast.
// TODO: Replace this `% 3` guard with ULongExtensions.Mod3 once GPU LL filtering reuses the benchmarked bitmask helper.
if ((exponent % 3UL) == 0UL && exponent != 3UL)
{
return false;
@@ -78,7 +79,7 @@ public class MersenneNumberLucasLehmerGpuTester
var gpu = GpuContextPool.RentPreferred(preferCpu: !runOnGpu);
var accelerator = gpu.Accelerator;
var kernel = GetKernel(accelerator);
var modulus = new GpuUInt128(((UInt128)1 << (int)exponent) - 1UL);
var modulus = new GpuUInt128(((UInt128)1 << (int)exponent) - 1UL); // TODO: Cache these Mersenne moduli per exponent so LL GPU runs skip rebuilding them every launch.
var buffer = accelerator.Allocate1D<GpuUInt128>(1);
kernel(1, exponent, modulus, buffer.View);
accelerator.Synchronize();
@@ -109,7 +110,7 @@ public class MersenneNumberLucasLehmerGpuTester
var gpu = GpuContextPool.RentPreferred(preferCpu: false);
var accelerator = gpu.Accelerator;
var kernel = GetBatchKernel(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 expBuffer = accelerator.Allocate1D<ulong>(count);
ulong[] expArray = ArrayPool<ulong>.Shared.Rent(count);
@@ -403,6 +404,8 @@ public class MersenneNumberLucasLehmerGpuTester
var factors = new List<ulong>();
for (ulong p = 2UL; p * p <= n; p += p == 2UL ? 1UL : 2UL)
{
// TODO: Replace these `%` factor checks with the shared Mod helpers (Mod3/Mod5/etc.) once the GPU
// pre-filter adopts the benchmarked bitmask operations to avoid slow modulo instructions.
if (n % p == 0UL)
{
factors.Add(p);
@@ -435,6 +438,8 @@ public class MersenneNumberLucasLehmerGpuTester
for (ulong divisor = 3UL; divisor * divisor <= n; divisor += 2UL)
{
// TODO: Swap this `%` for the divisor-cycle aware Mod helper once the residue pre-checks expose it
// so primality filtering avoids slow modulo instructions in tight loops.
if (n % divisor == 0UL)
{
return false;
@@ -451,6 +456,8 @@ 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.
result = MulMod(result, value, modulus);
}
@@ -463,6 +470,8 @@ public class MersenneNumberLucasLehmerGpuTester
private static ulong MulMod(ulong a, ulong b, ulong modulus)
{
// TODO: Swap this UInt128 `%` reduction for the GPU-compatible MulMod helper once it adopts the faster
// inline UInt128 path benchmarked in MulMod64Benchmarks so host/GPU parity avoids BigInteger-style fallbacks.
return (ulong)(((UInt128)a * b) % modulus);
}
@@ -18,11 +18,11 @@ public class MersenneNumberOrderGpuTester(GpuKernelType kernelType, bool useGpuO
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
byte last = lastIsSeven ? (byte)1 : (byte)0; // ILGPU kernels do not support bool parameters
var kernel = _kernelType switch
{
GpuKernelType.Pow2Mod => gpuLease.Pow2ModOrderKernel,
_ => gpuLease.IncrementalOrderKernel,
};
var kernel = _kernelType switch
{
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.
var foundBuffer = accelerator.Allocate1D<int>(1);
exponent.Mod10_8_5_3Steps(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
@@ -24,7 +24,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;
var kernel = gpuLease.Pow2ModKernel; // TODO: Swap this to the ProcessEightBitWindows kernel once GpuUInt128.Pow2Minus1Mod adopts the shared windowed helper measured fastest in GpuPow2ModBenchmarks.
twoP.Mod10_8_5_3(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
step10 = step10.Mod10();
+5
View File
@@ -414,6 +414,8 @@ public static class NttGpuMath
// load a block of size `len` into shared memory, do butterflies, write back.
// Requires explicit grouped kernels and chosen group size.
int t = index.X;
// TODO: Replace this `%` with a bitmask when `half` is a power of two so the stage index math matches the faster
// residue strategy from the GPU residue benchmarks.
int j = t % half;
int block = t / half;
int k = block * len;
@@ -488,6 +490,8 @@ public static class NttGpuMath
private static void StageBarrett128Kernel(Index1D index, ArrayView<GpuUInt128> data, int len, int half, int stageOffset, ArrayView<GpuUInt128> twiddles, ulong modHigh, ulong modLow, ulong muHigh, ulong muLow)
{
int t = index.X;
// TODO: Use the bitmask-based remainder helper here as well to remove `%` from the butterfly stage and align with the
// optimized kernels highlighted in the GPU pow2mod benchmarks.
int j = t % half;
int block = t / half;
int k = block * len;
@@ -537,6 +541,7 @@ public static class NttGpuMath
private static void StageMontKernel(Index1D index, ArrayView<GpuUInt128> data, int len, int half, int stageOffset, ArrayView<GpuUInt128> twiddlesMont, ulong modulus, ulong nPrime)
{
int t = index.X;
// TODO: Apply the bitmask remainder helper to this stage too so every butterfly path drops the slower `%` operation.
int j = t % half;
int block = t / half;
int k = block * len;