Hybrid device mode implemented as the current --mersenne=bydivisor CPU execution path. CPU path is now limited to CPU calculations, only.

This commit is contained in:
mlemanczyk committed 2025-12-01 03:45:39 +01:00
1 parent 353a121d03
commit 941733905b
49 files changed
+4965 -1726

No files matched your search

+325 -20
View File
@@ -25,15 +25,19 @@ public sealed class MersenneNumberTester(
bool useGpuScan = true,
bool useGpuOrder = true,
bool useResidue = true,
ulong maxK = 5_000_000UL)
ulong maxK = 5_000_000UL,
ComputationDevice orderDevice = ComputationDevice.Gpu)
{
private readonly bool _useResidue = useResidue;
private readonly bool _useIncremental = useIncremental && !useResidue;
private readonly ulong _maxK = maxK;
private readonly GpuKernelType _kernelType = kernelType;
private readonly bool _useGpuLucas = useGpuLucas;
private readonly bool _useGpuScan = useGpuScan; // device for pow2mod/incremental scanning
private readonly bool _useGpuOrder = useGpuOrder; // device for order computations
private readonly Func<PrimeOrderCalculatorAccelerator, ulong, bool> _sharesFactorWithExponentMinusOne = orderDevice switch
{
ComputationDevice.Hybrid => static (gpu, exponent) => exponent.SharesFactorWithExponentMinusOneHybrid(gpu),
_ when useGpuScan => static (gpu, exponent) => exponent.SharesFactorWithExponentMinusOneGpu(gpu),
_ => static (_, exponent) => exponent.SharesFactorWithExponentMinusOneCpu(),
};
private static readonly ConcurrentDictionary<UInt128, ulong> _orderCache = [];
// TODO: Swap this ConcurrentDictionary for the pooled dictionary variant highlighted in
// Pow2MontgomeryModBenchmarks once order warmups reuse deterministic divisor-cycle snapshots; the pooled approach removed
@@ -52,7 +56,154 @@ public sealed class MersenneNumberTester(
private static readonly Accelerator[] _accelerators = AcceleratorPool.Shared.Accelerators;
public void WarmUpOrders(ulong exponent, ulong limit = 5_000_000UL)
public void WarmUpOrdersHybrid(ulong exponent, ulong limit = 5_000_000UL)
{
bool cacheEnabled = useOrderCache;
if (!cacheEnabled)
{
return;
}
UInt128 twoP = (UInt128)exponent << 1; // 2 * p
LastDigit lastDigit = (exponent & 3UL) == 3UL ? LastDigit.Seven : LastDigit.One;
var gpu = PrimeOrderCalculatorAccelerator.Rent(1);
if (!useGpuOrder)
{
UInt128 k = UInt128.One;
UInt128 q;
ulong remainder;
for (; k <= limit; k++)
{
q = twoP * k + UInt128.One;
remainder = q.Mod10();
bool shouldCheck = lastDigit == LastDigit.Seven
? remainder == 7UL || remainder == 9UL || remainder == 3UL
: remainder == 1UL || remainder == 3UL || remainder == 9UL;
if (shouldCheck)
{
ulong mod8 = q.Mod8();
if (mod8 != 1UL && mod8 != 7UL)
{
continue;
}
else if (q.Mod3() == 0UL || q.Mod5() == 0UL)
{
continue;
}
}
if (!shouldCheck || !cacheEnabled || (cacheEnabled && _orderCache.ContainsKey(q)))
{
continue;
}
ulong ord = q.CalculateOrderHybrid(gpu);
_orderCache[q] = ord;
}
PrimeOrderCalculatorAccelerator.Return(gpu);
return;
}
ArrayPool<UInt128> uInt128Pool = ThreadStaticPools.UInt128Pool;
UInt128[] qsBuffer = uInt128Pool.Rent((int)limit);
int idx = 0;
UInt128 k2 = 1UL;
UInt128 q2;
ulong remainder2;
for (; k2 <= limit; k2++)
{
q2 = twoP * k2 + 1UL;
remainder2 = q2.Mod10();
bool shouldCheck2 = lastDigit == LastDigit.Seven
? remainder2 == 7UL || remainder2 == 9UL || remainder2 == 3UL
: remainder2 == 1UL || remainder2 == 3UL || remainder2 == 9UL;
if (shouldCheck2)
{
ulong mod8 = q2.Mod8();
if (mod8 != 1UL && mod8 != 7UL)
{
shouldCheck2 = false;
}
else if (q2.Mod3() == 0UL || q2.Mod5() == 0UL)
{
shouldCheck2 = false;
}
}
if (!shouldCheck2 || (cacheEnabled && _orderCache.ContainsKey(q2)))
{
continue;
}
qsBuffer[idx++] = q2;
}
if (idx == 0)
{
PrimeOrderCalculatorAccelerator.Return(gpu);
ThreadStaticPools.UInt128Pool.Return(qsBuffer);
return;
}
UInt128[] qs = qsBuffer;
var acceleratorIndex = AcceleratorPool.Shared.Rent();
var accelerator = _accelerators[acceleratorIndex];
var stream = AcceleratorStreamPool.Rent(acceleratorIndex);
var orderKernel = GpuKernelPool.GetOrAddKernels(acceleratorIndex, stream, KernelType.OrderKernelScan).Order!;
int batchSize = GpuConstants.ScanBatchSize;
ulong divMul = (ulong)((((UInt128)1 << 64) - 1UL) / exponent) + 1UL;
int offset = 0;
var gpuUInt128Pool = ThreadStaticPools.GpuUInt128Pool;
MemoryBuffer1D<GpuUInt128, Stride1D.Dense>? qBuffer;
MemoryBuffer1D<ulong, Stride1D.Dense>? orderBuffer;
while (offset < idx)
{
int count = Math.Min(batchSize, idx - offset);
qBuffer = accelerator.Allocate1D<GpuUInt128>(count);
orderBuffer = accelerator.Allocate1D<ulong>(count);
var tmp = gpuUInt128Pool.Rent(count);
for (int i = 0; i < count; i++)
{
tmp[i] = (GpuUInt128)qs[offset + i];
}
qBuffer.View.CopyFromCPU(stream, ref tmp[0], count);
gpuUInt128Pool.Return(tmp);
orderKernel(stream, count, exponent, divMul, qBuffer.View, orderBuffer.View);
stream.Synchronize();
ulong[] orders = orderBuffer.GetAsArray1D();
for (int i = 0; i < count; i++)
{
ulong order = orders[i];
if (order == 0UL)
{
order = qs[offset + i].CalculateOrderHybrid(gpu);
}
if (useOrderCache)
{
_orderCache[qs[offset + i]] = order;
}
}
offset += count;
orderBuffer.Dispose();
qBuffer.Dispose();
}
AcceleratorStreamPool.Return(acceleratorIndex,stream);
PrimeOrderCalculatorAccelerator.Return(gpu);
ThreadStaticPools.UInt128Pool.Return(qs);
}
public void WarmUpOrdersCpu(ulong exponent, ulong limit = 5_000_000UL)
{
bool cacheEnabled = useOrderCache;
if (!cacheEnabled)
@@ -63,8 +214,7 @@ public sealed class MersenneNumberTester(
UInt128 twoP = (UInt128)exponent << 1; // 2 * p
LastDigit lastDigit = (exponent & 3UL) == 3UL ? LastDigit.Seven : LastDigit.One;
// CPU path: compute orders directly and cache
var gpu = PrimeOrderCalculatorAccelerator.Rent(1);
if (!_useGpuOrder)
if (!useGpuOrder)
{
UInt128 k = UInt128.One;
UInt128 q;
@@ -98,7 +248,164 @@ public sealed class MersenneNumberTester(
continue;
}
ulong ord = q.CalculateOrder(gpu);
ulong ord = q.CalculateOrderCpu();
_orderCache[q] = ord;
}
return;
}
ArrayPool<UInt128> uInt128Pool = ThreadStaticPools.UInt128Pool;
UInt128[] qsBuffer = uInt128Pool.Rent((int)limit);
int idx = 0;
UInt128 k2 = 1UL;
UInt128 q2;
ulong remainder2;
for (; k2 <= limit; k2++)
{
q2 = twoP * k2 + 1UL;
remainder2 = q2.Mod10();
// TODO: Reuse the same residue-automaton fast path here so the GPU warmup staging avoids `%` and
// branches the benchmarks showed slower than the tracked stepping helper.
bool shouldCheck2 = lastDigit == LastDigit.Seven
? remainder2 == 7UL || remainder2 == 9UL || remainder2 == 3UL
: remainder2 == 1UL || remainder2 == 3UL || remainder2 == 9UL;
// Early modular filters for odd order and small-prime rejections
if (shouldCheck2)
{
ulong mod8 = q2.Mod8();
if (mod8 != 1UL && mod8 != 7UL)
{
shouldCheck2 = false;
}
else if (q2.Mod3() == 0UL || q2.Mod5() == 0UL)
{
shouldCheck2 = false;
}
}
if (!shouldCheck2 || (cacheEnabled && _orderCache.ContainsKey(q2)))
{
continue;
}
qsBuffer[idx++] = q2;
}
if (idx == 0)
{
ThreadStaticPools.UInt128Pool.Return(qsBuffer);
return;
}
UInt128[] qs = qsBuffer;
var acceleratorIndex = AcceleratorPool.Shared.Rent();
var accelerator = _accelerators[acceleratorIndex];
var stream = AcceleratorStreamPool.Rent(acceleratorIndex);
var orderKernel = GpuKernelPool.GetOrAddKernels(acceleratorIndex, stream, KernelType.OrderKernelScan).Order!;
// Guard long-running kernels by chunking the warm-up across batches.
// Reuse the same ScanBatchSize knob used for scanning.
int batchSize = GpuConstants.ScanBatchSize;
ulong divMul = (ulong)((((UInt128)1 << 64) - 1UL) / exponent) + 1UL;
int offset = 0;
var gpuUInt128Pool = ThreadStaticPools.GpuUInt128Pool;
MemoryBuffer1D<GpuUInt128, Stride1D.Dense>? qBuffer;
MemoryBuffer1D<ulong, Stride1D.Dense>? orderBuffer;
while (offset < idx)
{
int count = Math.Min(batchSize, idx - offset);
qBuffer = accelerator.Allocate1D<GpuUInt128>(count);
orderBuffer = accelerator.Allocate1D<ulong>(count);
// Convert to device-friendly GpuUInt128 on the fly
var tmp = gpuUInt128Pool.Rent(count);
for (int i = 0; i < count; i++)
{
tmp[i] = (GpuUInt128)qs[offset + i];
}
// Copy only the populated elements to the device buffer to avoid overrun on pooled arrays.
qBuffer.View.CopyFromCPU(stream, ref tmp[0], count);
gpuUInt128Pool.Return(tmp);
orderKernel(stream, count, exponent, divMul, qBuffer.View, orderBuffer.View);
stream.Synchronize();
// TODO: Change this to .CopyToCPU and use pre-allocated buffer with fixed, max capacity, using stream. Synchronize on the stream after copy.
ulong[] orders = orderBuffer.GetAsArray1D();
for (int i = 0; i < count; i++)
{
ulong order = orders[i];
if (order == 0UL)
{
order = qs[offset + i].CalculateOrderCpu();
}
if (useOrderCache)
{
_orderCache[qs[offset + i]] = order;
}
}
offset += count;
orderBuffer.Dispose();
qBuffer.Dispose();
}
AcceleratorStreamPool.Return(acceleratorIndex,stream);
uInt128Pool.Return(qs);
}
public void WarmUpOrdersGpu(ulong exponent, ulong limit = 5_000_000UL)
{
bool cacheEnabled = useOrderCache;
if (!cacheEnabled)
{
return;
}
UInt128 twoP = (UInt128)exponent << 1; // 2 * p
LastDigit lastDigit = (exponent & 3UL) == 3UL ? LastDigit.Seven : LastDigit.One;
// CPU path: compute orders directly and cache
var gpu = PrimeOrderCalculatorAccelerator.Rent(1);
if (!useGpuOrder)
{
UInt128 k = UInt128.One;
UInt128 q;
ulong remainder;
for (; k <= limit; k++)
{
q = twoP * k + UInt128.One;
remainder = q.Mod10();
// TODO: Replace these repeated Mod10/Mod8/Mod3/Mod5 calls with a residue automaton walk so
// order warmups reuse the optimized cycle stepping validated in the residue benchmarks.
bool shouldCheck = lastDigit == LastDigit.Seven
? remainder == 7UL || remainder == 9UL || remainder == 3UL
: remainder == 1UL || remainder == 3UL || remainder == 9UL;
// Early modular filters for odd order and small-prime rejections
if (shouldCheck)
{
ulong mod8 = q.Mod8();
if (mod8 != 1UL && mod8 != 7UL)
{
continue;
}
else if (q.Mod3() == 0UL || q.Mod5() == 0UL)
{
continue;
}
}
if (!shouldCheck || !cacheEnabled || (cacheEnabled && _orderCache.ContainsKey(q)))
{
continue;
}
ulong ord = q.CalculateOrderGpu(gpu);
_orderCache[q] = ord;
}
@@ -194,7 +501,7 @@ public sealed class MersenneNumberTester(
ulong order = orders[i];
if (order == 0UL)
{
order = qs[offset + i].CalculateOrder(gpu);
order = qs[offset + i].CalculateOrderGpu(gpu);
}
if (useOrderCache)
@@ -225,7 +532,7 @@ public sealed class MersenneNumberTester(
return false;
}
if ((exponent & 3UL) == 1UL && exponent.SharesFactorWithExponentMinusOne(gpu))
if ((exponent & 3UL) == 1UL && _sharesFactorWithExponentMinusOne(gpu, exponent))
{
return false;
}
@@ -250,13 +557,13 @@ public sealed class MersenneNumberTester(
maxK = _maxK;
}
if (_useGpuScan)
if (useGpuScan)
{
_residueGpuTester!.Scan(exponent, twoP, lastDigit, maxK, ref prePrime);
_residueGpuTester!.Scan(gpu, exponent, twoP, lastDigit, maxK, ref prePrime);
}
else
{
_residueCpuTester!.Scan(gpu, exponent, twoP, lastDigit, maxK, ref prePrime);
_residueCpuTester!.Scan(exponent, twoP, lastDigit, maxK, ref prePrime);
}
return prePrime;
@@ -269,7 +576,7 @@ public sealed class MersenneNumberTester(
UInt128 twoPPre = (UInt128)exponent << 1;
UInt128 maxKPre = (UInt128)PreLucasPreScanK;
LastDigit lastDigitPre = (exponent & 3UL) == 3UL ? LastDigit.Seven : LastDigit.One;
if (_useGpuScan)
if (useGpuScan)
{
_incrementalGpuTester.Scan(exponent, twoPPre, lastDigitPre, maxKPre, ref prePrime);
}
@@ -283,7 +590,7 @@ public sealed class MersenneNumberTester(
return false;
}
return _useGpuLucas
return useGpuLucas
? _lucasLehmerGpuTester.IsPrime(gpu, exponent, runOnGpu: true)
: _lucasLehmerCpuTester.IsPrime(gpu, exponent);
}
@@ -292,7 +599,7 @@ public sealed class MersenneNumberTester(
if (useOrder)
{
if (_useGpuOrder)
if (useGpuOrder)
{
_orderGpuTester.Scan(exponent, twoP, lastDigit, maxK, ref isPrime);
}
@@ -303,7 +610,7 @@ public sealed class MersenneNumberTester(
}
else
{
if (_useGpuScan)
if (useGpuScan)
{
_incrementalGpuTester.Scan(exponent, twoP, lastDigit, maxK, ref isPrime);
}
@@ -332,6 +639,4 @@ public sealed class MersenneNumberTester(
UInt128 pow = exponent.PowMod(q);
orders[index] = pow == 1UL ? exponent : 0UL;
}
}