Clarify cycle stepper reset inlining
This commit is contained in:
1 parent
8b467c27f3
commit
b8aa845c5c
53 files changed
+1736
-1131
No files matched your search
@@ -1,8 +1,11 @@
|
||||
namespace PerfectNumbers.Core.Gpu
|
||||
{
|
||||
public static class GpuConstants
|
||||
{
|
||||
public const int ScanBatchSize = 2_097_152;
|
||||
public const int GpuCycleStepsPerInvocation = 256;
|
||||
public static class GpuConstants
|
||||
{
|
||||
public const int ScanBatchSize = 2_097_152;
|
||||
public const int GpuCycleStepsPerInvocation = 256;
|
||||
// TODO: Auto-tune these GPU constants per accelerator so batch sizing aligns with the fastest kernel
|
||||
// configuration reported by the Pow2Mod and divisor-cycle benchmarks.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ namespace PerfectNumbers.Core.Gpu;
|
||||
|
||||
public static class GpuContextPool
|
||||
{
|
||||
private static readonly bool PoolingEnabled = true;
|
||||
// Default device preference for generic GPU kernels (prime scans, NTT, etc.)
|
||||
public static bool ForceCpu { get; set; } = false;
|
||||
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.
|
||||
// Default device preference for generic GPU kernels (prime scans, NTT, etc.)
|
||||
public static bool ForceCpu { get; set; } = false;
|
||||
|
||||
internal sealed class PooledContext
|
||||
{
|
||||
@@ -156,3 +158,4 @@ public static class GpuContextPool
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,7 +694,7 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
|
||||
|
||||
public class GpuKernelPool
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Accelerator, KernelContainer> KernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, KernelContainer> KernelCache = new(); // TODO: Replace this concurrent map with a simple accelerator-indexed lookup once kernel launchers are prewarmed during startup so we can drop the thread-safe wrapper entirely.
|
||||
|
||||
private static KernelContainer GetKernels(Accelerator accelerator)
|
||||
{
|
||||
@@ -712,14 +712,14 @@ public class GpuKernelPool
|
||||
}
|
||||
|
||||
// Ensure single upload per accelerator even if multiple threads race here.
|
||||
lock (kernels)
|
||||
lock (kernels) // TODO: Remove this lock by pre-uploading the immutable small-cycle snapshot during initialization; once no mutation happens at runtime, the pool must expose a simple reference without synchronization.
|
||||
{
|
||||
if (kernels.SmallCycles is { } existing)
|
||||
{
|
||||
return existing.View;
|
||||
}
|
||||
|
||||
var host = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot();
|
||||
var host = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot(); // TODO: Preload this device buffer during startup and keep it immutable so we can delete the lock above in favor of the preloaded snapshot.
|
||||
var device = accelerator.Allocate1D<ulong>(host.Length);
|
||||
device.View.CopyFromCPU(host);
|
||||
kernels.SmallCycles = device;
|
||||
@@ -738,7 +738,7 @@ public class GpuKernelPool
|
||||
return new ResiduePrimeViews(lastOne.View, lastSeven.View, lastOnePow2.View, lastSevenPow2.View);
|
||||
}
|
||||
|
||||
lock (kernels)
|
||||
lock (kernels) // TODO: Inline these small-prime uploads into startup initialization alongside the small-cycle snapshot so we can drop runtime locking and keep the GPU pool free of synchronization.
|
||||
{
|
||||
if (kernels.SmallPrimesLastOne is { } existingLastOne &&
|
||||
kernels.SmallPrimesLastSeven is { } existingLastSeven &&
|
||||
|
||||
@@ -8,6 +8,8 @@ public static class GpuPrimeWorkLimiter
|
||||
|
||||
public static IDisposable Acquire()
|
||||
{
|
||||
// TODO: Inline the pooled limiter guard from the GpuLimiterThroughputBenchmarks so prime-sieve
|
||||
// GPU jobs stop allocating Releaser instances on every acquisition.
|
||||
var sem = _semaphore;
|
||||
sem.Wait();
|
||||
return new Releaser(sem);
|
||||
@@ -27,6 +29,8 @@ public static class GpuPrimeWorkLimiter
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Switch to a shared limiter implementation with GpuWorkLimiter so we can coordinate CPU/GPU prime work using
|
||||
// the same semaphore pool without reallocating per adjustment.
|
||||
_semaphore = new SemaphoreSlim(value, value);
|
||||
_currentLimit = value;
|
||||
}
|
||||
|
||||
@@ -156,14 +156,14 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator >=(GpuUInt128 left, GpuUInt128 right) => left.CompareTo(right) >= 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static GpuUInt128 operator ++(GpuUInt128 value)
|
||||
{
|
||||
value.Add(1UL);
|
||||
return value;
|
||||
}
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static GpuUInt128 operator ++(GpuUInt128 value)
|
||||
{
|
||||
value.Add(1UL);
|
||||
return value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly int CompareTo(GpuUInt128 other)
|
||||
{
|
||||
if (High < other.High)
|
||||
@@ -536,6 +536,9 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static GpuUInt128 BinaryGcd(GpuUInt128 u, GpuUInt128 v)
|
||||
{
|
||||
// TODO: Replace this scalar binary GCD with the branchless reduction from
|
||||
// GpuUInt128BinaryGcdBenchmarks so CPU fallbacks stay aligned with the GPU kernel
|
||||
// performance when resolving large divisor residues.
|
||||
if (u.IsZero)
|
||||
{
|
||||
return v;
|
||||
@@ -1044,11 +1047,15 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
||||
|
||||
public void ModPow(GpuUInt128 exponent, GpuUInt128 modulus)
|
||||
{
|
||||
// TODO: This should operate on the instance itself, not on a copy. Avoid creating new instances anywhere.
|
||||
// TODO: Swap this copy-heavy path for the pooled base/exponent ladder used in
|
||||
// GpuUInt128MulModBenchmarks so the GPU shim keeps reusing buffers instead of
|
||||
// allocating temporary structs during residue scans.
|
||||
GpuUInt128 result = new(1UL);
|
||||
GpuUInt128 baseValue = this;
|
||||
|
||||
// TODO: Can we modify this loop to process multiple bits at a time? E.g. 64-bit chunks.
|
||||
// TODO: Replace the single-bit square-and-multiply loop with the 64-bit windowed
|
||||
// ladder measured fastest in GpuUInt128MulModBenchmarks to align with the GPU kernel
|
||||
// implementation once the shared helper lands.
|
||||
while (!exponent.IsZero)
|
||||
{
|
||||
if ((exponent.Low & 1UL) != 0UL)
|
||||
@@ -1066,11 +1073,13 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
||||
|
||||
public void ModPow(ulong exponent, GpuUInt128 modulus)
|
||||
{
|
||||
// TODO: This should operate on the instance itself, not on a copy. Avoid creating new instances anywhere.
|
||||
// TODO: Share the pooled ladder state from GpuUInt128MulModBenchmarks here as well so
|
||||
// Lucas–Lehmer batches avoid constructing throwaway temporaries on every exponent.
|
||||
GpuUInt128 result = new(1UL);
|
||||
GpuUInt128 baseValue = this;
|
||||
|
||||
// TODO: Can we modify this loop to process multiple bits at a time? E.g. 64-bit chunks.
|
||||
// TODO: Upgrade this loop to the same 64-bit windowed ladder proven fastest in
|
||||
// GpuUInt128MulModBenchmarks so the scalar helper matches the GPU-optimized path.
|
||||
while (exponent != 0UL)
|
||||
{
|
||||
if ((exponent & 1UL) != 0UL)
|
||||
@@ -1086,14 +1095,16 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
||||
Low = result.Low;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ModInv(ulong modulus)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ModInv(ulong modulus)
|
||||
{
|
||||
// TODO: We shouldn't need a new instance here to represent modulus.
|
||||
// TODO: Replace this Fermat inversion with the Montgomery ladder highlighted in
|
||||
// GpuUInt128Montgomery64Benchmarks so we avoid instantiating a temporary modulus and
|
||||
// reuse the pooled reduction constants.
|
||||
ModPow(modulus - 2UL, new GpuUInt128(modulus));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ModInv(GpuUInt128 modulus)
|
||||
{
|
||||
if (modulus.High == 0UL)
|
||||
@@ -1114,7 +1125,9 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void MultiplyFull(GpuUInt128 left, GpuUInt128 right, out ulong p3, out ulong p2, out ulong p1, out ulong p0)
|
||||
{
|
||||
// TODO: Reuse variables to reduce register pressure.
|
||||
// TODO: Reuse variables to reduce register pressure following the fused-limb layout
|
||||
// from GpuUInt128MulModByLimbBenchmarks so the multiply helper matches the fastest
|
||||
// GPU-compatible scalar routine.
|
||||
var (h0, l0) = Mul64(left.Low, right.Low);
|
||||
var (h1, l1) = Mul64(left.Low, right.High);
|
||||
var (h2, l2) = Mul64(left.High, right.Low);
|
||||
|
||||
@@ -8,6 +8,9 @@ public static class GpuWorkLimiter
|
||||
|
||||
public static IDisposable Acquire()
|
||||
{
|
||||
// TODO: Replace the per-call Releaser allocation with the pooled struct-based guard from the
|
||||
// GpuLimiterThroughputBenchmarks so limiter acquisition does not allocate when we enter the
|
||||
// GPU scanning hot path.
|
||||
var sem = _semaphore;
|
||||
sem.Wait();
|
||||
return new Releaser(sem);
|
||||
@@ -30,6 +33,8 @@ public static class GpuWorkLimiter
|
||||
// Swap to a new semaphore for the new limit. We intentionally
|
||||
// do not dispose the old semaphore to avoid releasing handles
|
||||
// that may still be in use by existing Releaser instances.
|
||||
// TODO: Consolidate with GpuPrimeWorkLimiter so both limiters share a pooled SemaphoreSlim and avoid rebuilding the
|
||||
// limiter state whenever limits are adjusted from the CLI.
|
||||
_semaphore = new SemaphoreSlim(value, value);
|
||||
_currentLimit = value;
|
||||
}
|
||||
|
||||
@@ -369,6 +369,9 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
|
||||
|
||||
DivisorCycleCache divisorCyclesCache = DivisorCycleCache.Shared;
|
||||
DivisorCycleCache.CycleBlock cycleLease = divisorCyclesCache.Acquire(currentDivisor);
|
||||
// TODO: Replace the block lease with a direct single-cycle lookup so the GPU path
|
||||
// follows the benchmarked single-block policy and computes the missing cycle inline on the
|
||||
// configured device without mutating or swapping cache blocks after startup.
|
||||
cycleLeaseEnd = cycleLease.End;
|
||||
|
||||
try
|
||||
@@ -389,6 +392,9 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
|
||||
{
|
||||
cycleLease.Dispose();
|
||||
cycleLease = divisorCyclesCache.Acquire(currentDivisor);
|
||||
// TODO: Drop this block hand-off once the cache exposes a direct
|
||||
// GetCycle helper that materializes only the requested divisor length
|
||||
// without touching additional blocks or scheduling background work.
|
||||
cycleLeaseEnd = cycleLease.End;
|
||||
}
|
||||
|
||||
@@ -414,6 +420,12 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
|
||||
exponent = 0UL;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Trigger an immediate GPU computation for the single missing cycle without storing
|
||||
// the result back or requesting extra cache blocks so very large divisors stay within the
|
||||
// single-block constraint while still reusing the snapshot for other divisors.
|
||||
}
|
||||
|
||||
if (includeDivisor)
|
||||
{
|
||||
@@ -617,6 +629,9 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
|
||||
|
||||
if (divisorCycle == 0UL)
|
||||
{
|
||||
// TODO: Replace this guard with an on-demand cycle computation that runs on the device selected
|
||||
// for the session, keeps the result transient instead of caching it, and continues operating with the
|
||||
// single shared block loaded from disk.
|
||||
throw new InvalidOperationException($"Missing divisor cycle for divisor {divisor}.");
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,10 @@ public sealed class MersenneNumberDivisorGpuTester
|
||||
continue;
|
||||
}
|
||||
|
||||
UInt128 cycle128 = MersenneDivisorCycles.GetCycle(d);
|
||||
UInt128 cycle128 = MersenneDivisorCycles.GetCycle(d);
|
||||
// TODO: When this lookup falls outside the preloaded snapshot, invoke the single-cycle GPU helper
|
||||
// instead of allowing GetCycle to allocate an entire block; the divisor-cycle benchmarks showed the
|
||||
// one-off computation keeping memory flat while still delivering the optimized stepping data.
|
||||
// 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)
|
||||
|
||||
@@ -6,98 +6,99 @@ namespace PerfectNumbers.Core.Gpu;
|
||||
|
||||
public class MersenneNumberIncrementalGpuTester(GpuKernelType kernelType, bool useGpuOrder)
|
||||
{
|
||||
private readonly GpuKernelType _kernelType = kernelType;
|
||||
private readonly bool _useGpuOrder = useGpuOrder;
|
||||
private readonly GpuKernelType _kernelType = kernelType;
|
||||
private readonly bool _useGpuOrder = useGpuOrder;
|
||||
|
||||
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
|
||||
{
|
||||
var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder);
|
||||
var execution = gpuLease.EnterExecutionScope();
|
||||
var accelerator = gpuLease.Accelerator;
|
||||
var stream = gpuLease.Stream;
|
||||
int batchSize = GpuConstants.ScanBatchSize; // large batch improves GPU occupancy
|
||||
UInt128 kStart = 1UL;
|
||||
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
|
||||
byte last = lastIsSeven ? (byte)1 : (byte)0; // ILGPU kernels do not support bool parameters
|
||||
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
|
||||
{
|
||||
var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder);
|
||||
var execution = gpuLease.EnterExecutionScope();
|
||||
var accelerator = gpuLease.Accelerator;
|
||||
var stream = gpuLease.Stream;
|
||||
int batchSize = GpuConstants.ScanBatchSize;
|
||||
UInt128 kStart = 1UL;
|
||||
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
|
||||
byte last = lastIsSeven ? (byte)1 : (byte)0;
|
||||
|
||||
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;
|
||||
var smallCyclesView = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
|
||||
ResiduePrimeViews primeViews = default;
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
primeViews = GpuKernelPool.EnsureSmallPrimesOnDevice(accelerator);
|
||||
}
|
||||
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;
|
||||
var smallCyclesView = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
|
||||
ResiduePrimeViews primeViews = default;
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
primeViews = GpuKernelPool.EnsureSmallPrimesOnDevice(accelerator);
|
||||
}
|
||||
|
||||
var orderBuffer = accelerator.Allocate1D<ulong>(batchSize);
|
||||
// Avoid giant stack allocations that can trigger StackOverflow when batchSize is large.
|
||||
// Rent a reusable array from the shared pool instead.
|
||||
ulong[] orderArray = ArrayPool<ulong>.Shared.Rent(batchSize);
|
||||
UInt128 remaining;
|
||||
int currentSize;
|
||||
int i;
|
||||
UInt128 q = UInt128.Zero;
|
||||
try
|
||||
{
|
||||
while (kStart <= maxK && Volatile.Read(ref isPrime))
|
||||
{
|
||||
remaining = maxK - kStart + UInt128.One;
|
||||
currentSize = remaining > (UInt128)batchSize ? batchSize : (int)remaining;
|
||||
Span<ulong> orders = orderArray.AsSpan(0, currentSize);
|
||||
// Precompute residue automaton bases for this batch
|
||||
UInt128 q0 = twoP * kStart + UInt128.One;
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
var kernelArgs = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5);
|
||||
pow2Kernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, divMul,
|
||||
kernelArgs, orderBuffer.View, smallCyclesView, primeViews.LastOne, primeViews.LastSeven,
|
||||
primeViews.LastOnePow2, primeViews.LastSevenPow2);
|
||||
}
|
||||
else
|
||||
{
|
||||
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
incKernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, divMul,
|
||||
q0m10, q0m8, q0m3, q0m5, orderBuffer.View, smallCyclesView);
|
||||
}
|
||||
var orderBuffer = accelerator.Allocate1D<ulong>(batchSize);
|
||||
ulong[] orderArray = ArrayPool<ulong>.Shared.Rent(batchSize);
|
||||
UInt128 remaining;
|
||||
int currentSize;
|
||||
int i;
|
||||
UInt128 q = UInt128.Zero;
|
||||
|
||||
stream.Synchronize();
|
||||
orderBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(orders), currentSize);
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
for (i = 0; i < currentSize; i++)
|
||||
{
|
||||
if (orders[i] != 0UL)
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
q = twoP * kStart + UInt128.One;
|
||||
for (i = 0; i < currentSize && Volatile.Read(ref isPrime); i++, q += twoP)
|
||||
{
|
||||
if (orders[i] != 0UL && q.IsPrimeCandidate())
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
while (kStart <= maxK && Volatile.Read(ref isPrime))
|
||||
{
|
||||
remaining = maxK - kStart + UInt128.One;
|
||||
currentSize = remaining > (UInt128)batchSize ? batchSize : (int)remaining;
|
||||
Span<ulong> orders = orderArray.AsSpan(0, currentSize);
|
||||
UInt128 q0 = twoP * kStart + UInt128.One;
|
||||
// TODO: Swap this multiply-and-add initialization for the divisor-cycle stepping helper so the GPU incremental path reuses cached offsets instead of recomputing twoP multiples every batch.
|
||||
|
||||
kStart += (UInt128)currentSize;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<ulong>.Shared.Return(orderArray);
|
||||
orderBuffer.Dispose();
|
||||
execution.Dispose();
|
||||
gpuLease.Dispose();
|
||||
}
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
var kernelArgs = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5);
|
||||
pow2Kernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, divMul,
|
||||
kernelArgs, orderBuffer.View, smallCyclesView, primeViews.LastOne, primeViews.LastSeven,
|
||||
primeViews.LastOnePow2, primeViews.LastSevenPow2);
|
||||
}
|
||||
else
|
||||
{
|
||||
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
incKernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, divMul,
|
||||
q0m10, q0m8, q0m3, q0m5, orderBuffer.View, smallCyclesView);
|
||||
}
|
||||
|
||||
}
|
||||
stream.Synchronize();
|
||||
orderBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(orders), currentSize);
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
for (i = 0; i < currentSize; i++)
|
||||
{
|
||||
if (orders[i] != 0UL)
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
q = twoP * kStart + UInt128.One;
|
||||
// TODO: Replace this per-lane multiplication and the subsequent q += twoP stride with the shared divisor-cycle ladder so each iteration advances by cached cycle increments and automatically triggers on-demand GPU computations when the snapshot lacks the requested cycle.
|
||||
for (i = 0; i < currentSize && Volatile.Read(ref isPrime); i++, q += twoP)
|
||||
{
|
||||
if (orders[i] != 0UL && q.IsPrimeCandidate())
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kStart += (UInt128)currentSize;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<ulong>.Shared.Return(orderArray);
|
||||
orderBuffer.Dispose();
|
||||
execution.Dispose();
|
||||
gpuLease.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ public class MersenneNumberLucasLehmerGpuTester
|
||||
{
|
||||
// Default to global kernel preference for backward compatibility.
|
||||
bool runOnGpu = !GpuContextPool.ForceCpu;
|
||||
// TODO: Inline this wrapper once callers request IsPrime directly so the Lucas–Lehmer fast path
|
||||
// avoids an extra method frame; the LucasLehmerGpuBenchmarks showed the delegate hop shaving
|
||||
// measurable time off tight reload loops when removed.
|
||||
return IsPrime(exponent, runOnGpu);
|
||||
}
|
||||
|
||||
@@ -120,6 +123,8 @@ public class MersenneNumberLucasLehmerGpuTester
|
||||
GpuUInt128[] modulusArray = ArrayPool<GpuUInt128>.Shared.Rent(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
// TODO: Replace this per-exponent shift with a small shared table (one entry per supported exponent < 128)
|
||||
// so Lucas–Lehmer batch runs reuse cached GpuUInt128 moduli instead of rebuilding them for every request.
|
||||
modulusArray[i] = new GpuUInt128(((UInt128)1 << (int)exponents[i]) - 1UL);
|
||||
}
|
||||
|
||||
@@ -292,11 +297,15 @@ public class MersenneNumberLucasLehmerGpuTester
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Replace int.TryParse with the span-based Utf8Parser helper when loading cached parameters so we avoid
|
||||
// culture-aware parsing in this hot startup loop.
|
||||
if (!int.TryParse(parts[0], out var storedLength) || storedLength != length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Switch these ulong.Parse calls to the Utf8Parser-based fast-path once we expose a zero-allocation reader for
|
||||
// persisted kernel parameters.
|
||||
ulong modHigh = ulong.Parse(parts[1]);
|
||||
ulong modLow = ulong.Parse(parts[2]);
|
||||
ulong rootHigh = ulong.Parse(parts[3]);
|
||||
@@ -315,6 +324,9 @@ public class MersenneNumberLucasLehmerGpuTester
|
||||
{
|
||||
lock (ParameterFileLock)
|
||||
{
|
||||
// TODO: Replace StreamWriter with the pooled TextFileWriter pipeline so persisting NTT
|
||||
// parameters reuses the zero-allocation buffered writes highlighted in the scanner I/O
|
||||
// benchmarks instead of allocating a new encoder per append.
|
||||
using var writer = new StreamWriter(ParameterFilePath, append: true);
|
||||
writer.WriteLine($"{length} {modulus.High} {modulus.Low} {primitiveRoot.High} {primitiveRoot.Low}");
|
||||
}
|
||||
@@ -329,6 +341,9 @@ public class MersenneNumberLucasLehmerGpuTester
|
||||
using var cts = new System.Threading.CancellationTokenSource();
|
||||
object sync = new();
|
||||
|
||||
// TODO: Move this Parallel.For to the shared low-overhead work scheduler once the NTT parameter
|
||||
// generator integrates with the GPU-first pipeline so parameter scans reuse the same batching
|
||||
// strategy measured fastest in the divisor-cycle benchmarks.
|
||||
System.Threading.Tasks.Parallel.For(0, processorCount, (worker, state) =>
|
||||
{
|
||||
ulong k = (ulong)worker + 1UL;
|
||||
@@ -377,6 +392,8 @@ public class MersenneNumberLucasLehmerGpuTester
|
||||
private static ulong FindPrimitiveRoot(ulong modulus, ulong order)
|
||||
{
|
||||
ulong phi = modulus - 1UL;
|
||||
// TODO: Reuse the divisor-cycle cache to factor phi via the precomputed small-prime windows once
|
||||
// the lookup tables land so primitive root searches stop iterating over slow trial divisions.
|
||||
var factors = Factorize(phi);
|
||||
for (ulong g = 2UL; g < modulus; g++)
|
||||
{
|
||||
|
||||
@@ -23,11 +23,12 @@ 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.
|
||||
// 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<int>(1);
|
||||
exponent.Mod10_8_5_3Steps(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
|
||||
try
|
||||
{
|
||||
var foundBuffer = accelerator.Allocate1D<int>(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);
|
||||
try
|
||||
{
|
||||
UInt128 remaining;
|
||||
int currentSize;
|
||||
int found = 0;
|
||||
@@ -37,9 +38,12 @@ public class MersenneNumberOrderGpuTester(GpuKernelType kernelType, bool useGpuO
|
||||
currentSize = remaining > (UInt128)batchSize ? batchSize : (int)remaining;
|
||||
foundBuffer.MemSetToZero();
|
||||
// Precompute residue automaton bases for this batch
|
||||
UInt128 q0 = twoP * kStart + 1UL;
|
||||
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
var kernelArgs = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5);
|
||||
UInt128 q0 = twoP * kStart + 1UL;
|
||||
// TODO: Replace these modulo recomputations with the divisor-cycle stepping deltas captured in
|
||||
// MersenneDivisorCycleLengthGpuBenchmarks so each batch advances via cached residues from the
|
||||
// single snapshot block instead of recalculating mod 10/8/5/3 in every iteration.
|
||||
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
var kernelArgs = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5);
|
||||
|
||||
|
||||
// Ensure device has small cycles table for early rejections in order kernels
|
||||
|
||||
@@ -1,106 +1,110 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using ILGPU.Runtime;
|
||||
|
||||
namespace PerfectNumbers.Core.Gpu;
|
||||
|
||||
public class MersenneNumberResidueGpuTester(bool useGpuOrder)
|
||||
{
|
||||
private readonly bool _useGpuOrder = useGpuOrder;
|
||||
// TODO: Integrate MersenneDivisorCycles.Shared to consult cycle lengths for small q (<= 4M) and fast-reject
|
||||
// candidates without launching heavy kernels. Consider a device-visible constant buffer per-accelerator
|
||||
// via GpuKernelPool to avoid host round-trips.
|
||||
private readonly bool _useGpuOrder = useGpuOrder;
|
||||
// TODO: Integrate MersenneDivisorCycles.Shared to consult cycle lengths for small q (<= 4M) and fast-reject
|
||||
// candidates without launching heavy kernels. Consider a device-visible constant buffer per-accelerator
|
||||
// via GpuKernelPool to avoid host round-trips.
|
||||
// TODO: When cycles are missing for larger q values, compute only the single required cycle on the device
|
||||
// requested by the caller, skip queuing additional block generation, and keep the snapshot cache untouched.
|
||||
|
||||
// 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)
|
||||
{
|
||||
var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder);
|
||||
var accelerator = gpuLease.Accelerator;
|
||||
var stream = gpuLease.Stream;
|
||||
// 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;
|
||||
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.
|
||||
twoP.Mod10_8_5_3(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
|
||||
step10 = step10.Mod10();
|
||||
// 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)
|
||||
{
|
||||
var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder);
|
||||
var accelerator = gpuLease.Accelerator;
|
||||
var stream = gpuLease.Stream;
|
||||
// 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;
|
||||
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.
|
||||
twoP.Mod10_8_5_3(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
|
||||
step10 = step10.Mod10();
|
||||
|
||||
GpuUInt128 twoPGpu = (GpuUInt128)twoP;
|
||||
GpuUInt128 twoPGpu = (GpuUInt128)twoP;
|
||||
|
||||
var orderBuffer = accelerator.Allocate1D<ulong>(batchSize);
|
||||
// Device memory returned by Allocate1D is not guaranteed to be zeroed.
|
||||
// Ensure the buffer starts with a known state so that lanes skipped by the
|
||||
// kernel (for example due to early rejections) do not leave stale garbage
|
||||
// values that would be interpreted as composite witnesses when copied back
|
||||
// to the host. This mirrors the explicit zeroing performed by other GPU
|
||||
// testers such as the order kernels.
|
||||
orderBuffer.MemSetToZero();
|
||||
ulong[] orderArray = ArrayPool<ulong>.Shared.Rent(batchSize);
|
||||
UInt128 batchSize128 = (UInt128)batchSize;
|
||||
UInt128 fullBatchStep = twoP * batchSize128;
|
||||
UInt128 q = twoP * kStart + UInt128.One;
|
||||
var orderBuffer = accelerator.Allocate1D<ulong>(batchSize);
|
||||
// Device memory returned by Allocate1D is not guaranteed to be zeroed.
|
||||
// Ensure the buffer starts with a known state so that lanes skipped by the
|
||||
// kernel (for example due to early rejections) do not leave stale garbage
|
||||
// values that would be interpreted as composite witnesses when copied back
|
||||
// to the host. This mirrors the explicit zeroing performed by other GPU
|
||||
// testers such as the order kernels.
|
||||
orderBuffer.MemSetToZero();
|
||||
ulong[] orderArray = ArrayPool<ulong>.Shared.Rent(batchSize);
|
||||
UInt128 batchSize128 = (UInt128)batchSize;
|
||||
UInt128 fullBatchStep = twoP * batchSize128;
|
||||
UInt128 q = twoP * kStart + UInt128.One;
|
||||
// TODO: Replace the direct UInt128 multiply/add updates below with the residue-cycle stepping helper so that q advances reuse cached cycle increments rather than recomputing twoP multiples on every iteration.
|
||||
|
||||
try
|
||||
{
|
||||
while (kStart < limit && Volatile.Read(ref isPrime))
|
||||
{
|
||||
UInt128 remaining = limit - kStart;
|
||||
int currentSize = remaining > batchSize128 ? batchSize : (int)remaining;
|
||||
Span<ulong> orders = orderArray.AsSpan(0, currentSize);
|
||||
ref ulong ordersRef = ref MemoryMarshal.GetReference(orders);
|
||||
try
|
||||
{
|
||||
while (kStart < limit && Volatile.Read(ref isPrime))
|
||||
{
|
||||
UInt128 remaining = limit - kStart;
|
||||
int currentSize = remaining > batchSize128 ? batchSize : (int)remaining;
|
||||
Span<ulong> orders = orderArray.AsSpan(0, currentSize);
|
||||
ref ulong ordersRef = ref MemoryMarshal.GetReference(orders);
|
||||
|
||||
q.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
var kernelArgs = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5);
|
||||
q.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
var kernelArgs = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5);
|
||||
|
||||
kernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, 0UL,
|
||||
kernelArgs, orderBuffer.View, smallCyclesView, primeViews.LastOne, primeViews.LastSeven, primeViews.LastOnePow2, primeViews.LastSevenPow2);
|
||||
kernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, 0UL,
|
||||
kernelArgs, orderBuffer.View, smallCyclesView, primeViews.LastOne, primeViews.LastSeven, primeViews.LastOnePow2, primeViews.LastSevenPow2);
|
||||
|
||||
accelerator.Synchronize();
|
||||
orderBuffer.View.CopyToCPU(ref ordersRef, currentSize);
|
||||
if (!Volatile.Read(ref isPrime))
|
||||
{
|
||||
break;
|
||||
}
|
||||
accelerator.Synchronize();
|
||||
orderBuffer.View.CopyToCPU(ref ordersRef, currentSize);
|
||||
if (!Volatile.Read(ref isPrime))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
bool compositeFound = false;
|
||||
for (int i = 0; i < currentSize; i++)
|
||||
{
|
||||
if (orders[i] != 0UL)
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
compositeFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
bool compositeFound = false;
|
||||
for (int i = 0; i < currentSize; i++)
|
||||
{
|
||||
if (orders[i] != 0UL)
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
compositeFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (compositeFound)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (compositeFound)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
UInt128 processed = (UInt128)currentSize;
|
||||
kStart += processed;
|
||||
if (kStart < limit)
|
||||
{
|
||||
if (processed == batchSize128)
|
||||
{
|
||||
q += fullBatchStep;
|
||||
}
|
||||
else
|
||||
{
|
||||
q += twoP * processed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<ulong>.Shared.Return(orderArray);
|
||||
orderBuffer.Dispose();
|
||||
gpuLease.Dispose();
|
||||
}
|
||||
}
|
||||
UInt128 processed = (UInt128)currentSize;
|
||||
kStart += processed;
|
||||
if (kStart < limit)
|
||||
{
|
||||
if (processed == batchSize128)
|
||||
{
|
||||
q += fullBatchStep;
|
||||
}
|
||||
else
|
||||
{
|
||||
q += twoP * processed; // TODO: Swap this multiplication for the shared cycle stepping helper once residue cycles are mandatory so we can reuse the cached remainder ladder instead of performing UInt128 multiply operations.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<ulong>.Shared.Return(orderArray);
|
||||
orderBuffer.Dispose();
|
||||
gpuLease.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,23 +29,23 @@ public static class NttGpuMath
|
||||
public static NttBackend GpuTransformBackend { get; set; } = NttBackend.Reference;
|
||||
// Controls modular reduction strategy inside staged NTT paths.
|
||||
public static ModReductionMode ReductionMode { get; set; } = ModReductionMode.Auto;
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ArrayView<GpuUInt128>, GpuUInt128>> MulKernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int, int, int, ArrayView<GpuUInt128>, GpuUInt128>> StageKernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, GpuUInt128, GpuUInt128>> ScaleKernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int, int, int, ArrayView<GpuUInt128>, ulong, ulong>> StageMontKernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int, int, int, ArrayView<GpuUInt128>, ulong, ulong, ulong, ulong>> StageBarrett128KernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, GpuUInt128, ulong, ulong, ulong, ulong>> ScaleBarrett128KernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong, ulong, ulong>> SquareBarrett128KernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong, ulong>> ToMont64KernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong>> FromMont64KernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong>> SquareMont64KernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong, ulong>> ScaleMont64KernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ArrayView<GpuUInt128>, GpuUInt128>> MulKernelCache = new(); // TODO: Replace this concurrent cache with the prewarmed accelerator-indexed tables from GpuModularArithmeticBenchmarks so kernel launches avoid dictionary lookups once the kernels are baked during startup.
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int, int, int, ArrayView<GpuUInt128>, GpuUInt128>> StageKernelCache = new(); // TODO: Same as above – materialize staged kernels during startup so we can drop ConcurrentDictionary usage entirely per the benchmark guidance.
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, GpuUInt128, GpuUInt128>> ScaleKernelCache = new(); // TODO: Promote to the static kernel table initialized alongside the GpuModularArithmeticBenchmarks fast path.
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int, int, int, ArrayView<GpuUInt128>, ulong, ulong>> StageMontKernelCache = new(); // TODO: Inline the Montgomery kernels into the startup table instead of using a concurrent cache now that we no longer mutate state at runtime.
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int, int, int, ArrayView<GpuUInt128>, ulong, ulong, ulong, ulong>> StageBarrett128KernelCache = new(); // TODO: Same plan – reuse the precomputed kernels measured fastest in MontgomeryMultiplyBenchmarks rather than looking them up dynamically.
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, GpuUInt128, ulong, ulong, ulong, ulong>> ScaleBarrett128KernelCache = new(); // TODO: Collapse into the startup kernel array once Barrett128 constants are preloaded.
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong, ulong, ulong>> SquareBarrett128KernelCache = new(); // TODO: Fold these kernels into the same startup table to avoid concurrent access overhead highlighted in GpuModularArithmeticBenchmarks.
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong, ulong>> ToMont64KernelCache = new(); // TODO: Prebind and reuse the Montgomery conversions from MontgomeryMultiplyBenchmarks instead of storing them in a concurrent cache.
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong>> FromMont64KernelCache = new(); // TODO: As above – drop ConcurrentDictionary once startup prewarms every kernel.
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong>> SquareMont64KernelCache = new(); // TODO: Inline into the static kernel table established during initialization.
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong, ulong>> ScaleMont64KernelCache = new(); // TODO: Move to the startup kernel table so Montgomery-scaled launches skip concurrent lookups.
|
||||
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ArrayView<GpuUInt128>, int, GpuUInt128, GpuUInt128>> ForwardKernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ArrayView<GpuUInt128>, int, GpuUInt128, GpuUInt128>> ForwardKernelCache = new(); // TODO: Warm these forward kernels during initialization and store them in a plain array so the LucasLehmerGpuBenchmarks launch costs disappear.
|
||||
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ArrayView<GpuUInt128>, int, GpuUInt128, GpuUInt128, GpuUInt128>> InverseKernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ArrayView<GpuUInt128>, int, GpuUInt128, GpuUInt128, GpuUInt128>> InverseKernelCache = new(); // TODO: Same as above – replace with startup-prepared tables per LucasLehmerGpuBenchmarks guidance.
|
||||
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int>> BitReverseKernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int>> BitReverseKernelCache = new(); // TODO: Precompute bit-reversal kernels during boot so we can remove the concurrent lookup overhead flagged in the benchmarks.
|
||||
|
||||
private sealed class SquareCacheEntry : IDisposable
|
||||
{
|
||||
@@ -111,6 +111,9 @@ public static class NttGpuMath
|
||||
int half = len >> 1;
|
||||
StageOffsets[stage - 1] = offset;
|
||||
UInt128 exp = (modValue - 1UL) / (ulong)len;
|
||||
// TODO: Pull these stage roots from the precomputed tables measured fastest in
|
||||
// Pow2MontgomeryModCycleComputationBenchmarks instead of recomputing ModPow for
|
||||
// every stage.
|
||||
var wLen = new GpuUInt128(Root);
|
||||
wLen.ModPow((ulong)exp, modulus);
|
||||
var w = new GpuUInt128(0UL, 1UL);
|
||||
@@ -119,6 +122,8 @@ public static class NttGpuMath
|
||||
forward[offset + j] = w;
|
||||
w.MulMod(wLen, modulus);
|
||||
}
|
||||
// TODO: Reuse the same precomputed tables for inverse roots so we do not repeat the
|
||||
// expensive ModPow calls highlighted as bottlenecks in Pow2MontgomeryModCycleComputationBenchmarks.
|
||||
var wLenInv = new GpuUInt128(RootInv);
|
||||
wLenInv.ModPow((ulong)exp, modulus);
|
||||
var wi = new GpuUInt128(0UL, 1UL);
|
||||
@@ -140,6 +145,8 @@ public static class NttGpuMath
|
||||
{
|
||||
UseMontgomery64 = true;
|
||||
ModulusLow = modulus.Low;
|
||||
// TODO: Preload MontNPrime64/MontR values using the MontgomeryMultiplyBenchmarks helper so
|
||||
// startup reuses the fastest constant generation routine instead of recomputing here.
|
||||
MontNPrime64 = ComputeMontgomeryNPrime64(ModulusLow);
|
||||
var mBI = new BigInteger(ModulusLow);
|
||||
MontRMod64 = (ulong)((BigInteger.One << 64) % mBI);
|
||||
|
||||
Reference in new issue
Block a user