Restore cycle-aware divisor scans and background generation

This commit is contained in:
Marek Lemańczyk committed 2025-09-25 02:12:40 +02:00
1 parent bc4e526946
commit b798970ce9
6 files changed
+245 -491

No files matched your search

+3 -1
View File
@@ -56,7 +56,9 @@ Never ever run tests marked with trait Category = Slow. Only run tests with cate
There is only one agents file - this one. Don't search for another one.
Always use indentation with 4 spaces and / or 1 tab in source code files.
Always use indentation with 4 spaces in source code files. Do not use 8-space indentation or tabs.
Keep the divisor cycle cache limited to three blocks in memory (the base snapshot plus at most two additional blocks) and always start background computation of the next block immediately after loading the first block from disk and whenever work begins on the most recently generated block.
All code, comments, commit messages, branch names and PR descriptions must be written in American English.
+1
View File
@@ -457,6 +457,7 @@ internal static class Program
MersenneDivisorCycles.Shared.LoadFrom(cyclesPath);
}
DivisorCycleCache.Shared.ConfigureGeneratorDevice(orderOnGpu);
DivisorCycleCache.Shared.ReloadFromCurrentSnapshot();
Console.WriteLine("Divisor cycles are ready");
@@ -1,5 +1,4 @@
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using PerfectNumbers.Core;
@@ -8,7 +7,6 @@ using MontgomeryDivisorData = PerfectNumbers.Core.Gpu.MersenneNumberDivisorByDiv
namespace PerfectNumbers.Core.Cpu;
// TODO: The current implementation is over-complicated for CPU. We shouldn't need all the hassle with intermediate buffers, checking the status there etc.
public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDivisorByDivisorTester
{
private readonly object _sync = new();
@@ -74,7 +72,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
{
ulong allowedMax;
bool useCycles;
int batchCapacity;
lock (_sync)
{
@@ -85,7 +82,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
allowedMax = ComputeAllowedMaxDivisor(prime, _divisorLimit);
useCycles = _useDivisorCycles;
batchCapacity = _batchSize;
}
if (allowedMax < 3UL)
@@ -94,34 +90,17 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
return true;
}
ulong[] divisors = ArrayPool<ulong>.Shared.Rent(batchCapacity);
byte[] hits = ArrayPool<byte>.Shared.Rent(batchCapacity);
MontgomeryDivisorData[] divisorData = ArrayPool<MontgomeryDivisorData>.Shared.Rent(batchCapacity);
ulong processedCount = 0UL;
ulong lastProcessed = 0UL;
bool composite = false;
bool processedAll = false;
try
{
composite = CheckDivisors(
bool composite = CheckDivisors(
prime,
allowedMax,
useCycles,
divisors,
hits,
divisorData,
out lastProcessed,
out processedAll,
out processedCount);
}
finally
{
ArrayPool<ulong>.Shared.Return(divisors, clearArray: false);
ArrayPool<byte>.Shared.Return(hits, clearArray: false);
ArrayPool<MontgomeryDivisorData>.Shared.Return(divisorData, clearArray: false);
}
if (processedCount > 0UL)
{
@@ -169,9 +148,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
ulong prime,
ulong allowedMax,
bool useCycles,
ulong[] divisors,
byte[] hits,
MontgomeryDivisorData[] divisorData,
out ulong lastProcessed,
out bool processedAll,
out ulong processedCount)
@@ -185,92 +161,53 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
return false;
}
int batchCapacity = _batchSize;
ulong divisor = 3UL;
bool composite = false;
while (divisor <= allowedMax)
{
int batchSize = 0;
bool reachedEndInBatch = false;
while (batchSize < batchCapacity && divisor <= allowedMax)
{
ulong currentDivisor = divisor;
ulong nextDivisor = currentDivisor + 2UL;
divisors[batchSize++] = currentDivisor;
processedCount++;
lastProcessed = currentDivisor;
lastProcessed = divisor;
if (nextDivisor <= currentDivisor)
MontgomeryDivisorData divisorData = CreateMontgomeryDivisorData(divisor);
byte hit;
if (useCycles)
{
reachedEndInBatch = true;
break;
using DivisorCycleCache.Lease cycleLease = DivisorCycleCache.Shared.Acquire(divisor);
ulong divisorCycle = cycleLease.GetCycle(divisor);
hit = CheckDivisor(prime, divisorCycle != 0UL, divisorCycle, divisorData);
}
else
{
hit = CheckDivisor(prime, false, 0UL, divisorData);
}
if (nextDivisor > allowedMax)
{
divisor = nextDivisor;
reachedEndInBatch = true;
break;
}
divisor = nextDivisor;
}
if (batchSize == 0)
{
if (reachedEndInBatch)
if (hit != 0)
{
processedAll = true;
return true;
}
break;
}
Span<MontgomeryDivisorData> divisorDataSpan = divisorData.AsSpan(0, batchSize);
Span<ulong> divisorSpan = divisors.AsSpan(0, batchSize);
Span<byte> hitsSpan = hits.AsSpan(0, batchSize);
for (int i = 0; i < batchSize; i++)
{
divisorDataSpan[i] = CreateMontgomeryDivisorData(divisorSpan[i]);
}
for (int i = 0; i < batchSize; i++)
{
hitsSpan[i] = CheckDivisor(prime, useCycles, divisorSpan[i], divisorDataSpan[i]);
if (hitsSpan[i] != 0)
{
composite = true;
lastProcessed = divisorSpan[i];
break;
}
}
if (!composite)
{
lastProcessed = divisorSpan[batchSize - 1];
}
if (composite)
{
break;
}
if (reachedEndInBatch)
if (divisor >= allowedMax - 1UL)
{
processedAll = true;
break;
}
if (divisor > ulong.MaxValue - 2UL)
{
break;
}
processedAll = processedAll || divisor > allowedMax;
return composite;
divisor += 2UL;
}
private static byte CheckDivisor(ulong prime, bool useCycles, ulong divisor, in MontgomeryDivisorData divisorData)
if (!processedAll)
{
processedAll = divisor > allowedMax;
}
return false;
}
private static byte CheckDivisor(ulong prime, bool useCycles, ulong divisorCycle, in MontgomeryDivisorData divisorData)
{
ulong modulus = divisorData.Modulus;
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
@@ -278,26 +215,11 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
return 0;
}
ulong exponent = prime;
ulong residue = useCycles && divisorCycle != 0UL
? prime.Pow2MontgomeryModWithCycle(divisorCycle, divisorData)
: prime.Pow2MontgomeryMod(divisorData);
if (useCycles)
{
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(divisor);
if (cycle == 0UL)
{
return 0;
}
ulong remainder = prime % cycle;
if (remainder != 0UL)
{
return 0;
}
exponent = remainder;
}
return exponent.Pow2MontgomeryMod(divisorData) == 1UL ? (byte)1 : (byte)0;
return residue == 1UL ? (byte)1 : (byte)0;
}
private void UpdateStatusUnsafe(ulong lastProcessed, ulong processedCount)
@@ -380,19 +302,11 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
private sealed class DivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession
{
private readonly MersenneNumberDivisorByDivisorCpuTester _owner;
private ulong[] _primeBuffer;
private int[] _positionBuffer;
private ulong[] _exponentBuffer;
private int _capacity;
private bool _disposed;
internal DivisorScanSession(MersenneNumberDivisorByDivisorCpuTester owner)
{
_owner = owner;
_capacity = Math.Max(1, owner._batchSize);
_primeBuffer = ArrayPool<ulong>.Shared.Rent(_capacity);
_positionBuffer = ArrayPool<int>.Shared.Rent(_capacity);
_exponentBuffer = ArrayPool<ulong>.Shared.Rent(_capacity);
}
internal void Reset()
@@ -407,116 +321,41 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
throw new ObjectDisposedException(nameof(DivisorScanSession));
}
int primesLength = primes.Length;
if (primesLength == 0)
int length = primes.Length;
if (length == 0)
{
return;
}
MersenneNumberDivisorByDivisorCpuTester owner = _owner;
int batchSize = owner._batchSize;
EnsureCapacity(batchSize);
ulong modulus = divisor;
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
hits.Clear();
return;
}
bool cycleEnabled = owner._useDivisorCycles && divisorCycle != 0UL;
ulong cycle = divisorCycle;
bool cycleEnabled = _owner._useDivisorCycles && divisorCycle != 0UL;
MontgomeryDivisorData divisorData = CreateMontgomeryDivisorData(divisor);
int offset = 0;
Span<ulong> primeSpan = _primeBuffer.AsSpan();
Span<int> positionSpan = _positionBuffer.AsSpan();
Span<ulong> exponentSpan = _exponentBuffer.AsSpan();
ulong previousResidue = 0UL;
ulong previousPrime = 0UL;
bool hasPreviousResidue = false;
while (offset < primesLength)
for (int i = 0; i < length; i++)
{
int sliceLength = Math.Min(batchSize, primesLength - offset);
ReadOnlySpan<ulong> primesSlice = primes.Slice(offset, sliceLength);
Span<byte> hitsSlice = hits.Slice(offset, sliceLength);
hitsSlice.Clear();
int computeCount = 0;
ulong prime = primes[i];
if (cycleEnabled)
{
for (int i = 0; i < sliceLength; i++)
{
ulong prime = primesSlice[i];
if (prime % cycle != 0UL)
ulong remainder = prime % divisorCycle;
if (remainder != 0UL)
{
hits[i] = 0;
continue;
}
primeSpan[computeCount] = prime;
positionSpan[computeCount] = i;
computeCount++;
}
}
else
{
for (int i = 0; i < sliceLength; i++)
{
primeSpan[computeCount] = primesSlice[i];
positionSpan[computeCount] = i;
computeCount++;
}
}
if (computeCount > 0)
{
Span<ulong> exponentSlice = exponentSpan[..computeCount];
ulong currentPrime = hasPreviousResidue ? previousPrime : 0UL;
bool hasDeltaSource = hasPreviousResidue;
ulong residue = cycleEnabled
? prime.Pow2MontgomeryModWithCycle(divisorCycle, divisorData)
: prime.Pow2MontgomeryMod(divisorData);
for (int i = 0; i < computeCount; i++)
{
ulong prime = primeSpan[i];
ulong exponentValue;
if (hasDeltaSource)
{
exponentValue = prime - currentPrime;
}
else
{
exponentValue = prime;
hasDeltaSource = true;
}
if (cycleEnabled)
{
exponentValue %= cycle;
}
exponentSlice[i] = exponentValue;
currentPrime = prime;
}
for (int i = 0; i < computeCount; i++)
{
exponentSlice[i] = exponentSlice[i].Pow2MontgomeryMod(divisorData);
}
for (int i = 0; i < computeCount; i++)
{
int position = positionSpan[i];
ulong stepResidue = exponentSlice[i];
ulong prime = primeSpan[i];
ulong residue = hasPreviousResidue ? MultiplyMod(previousResidue, stepResidue, modulus) : stepResidue;
hitsSlice[position] = residue == 1UL ? (byte)1 : (byte)0;
previousResidue = residue;
previousPrime = prime;
hasPreviousResidue = true;
}
}
offset += sliceLength;
hits[i] = residue == 1UL ? (byte)1 : (byte)0;
}
}
@@ -530,34 +369,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
_disposed = true;
_owner.ReturnSession(this);
}
private void EnsureCapacity(int requiredCapacity)
{
if (requiredCapacity <= _capacity)
{
return;
}
ArrayPool<ulong>.Shared.Return(_primeBuffer, clearArray: false);
ArrayPool<int>.Shared.Return(_positionBuffer, clearArray: false);
ArrayPool<ulong>.Shared.Return(_exponentBuffer, clearArray: false);
_capacity = requiredCapacity;
_primeBuffer = ArrayPool<ulong>.Shared.Rent(_capacity);
_positionBuffer = ArrayPool<int>.Shared.Rent(_capacity);
_exponentBuffer = ArrayPool<ulong>.Shared.Rent(_capacity);
}
private static ulong MultiplyMod(ulong left, ulong right, ulong modulus)
{
if (modulus == 0UL)
{
return 0UL;
}
UInt128 product = (UInt128)left * right;
return (ulong)(product % modulus);
}
}
}
+137 -187
View File
@@ -1,5 +1,11 @@
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using ILGPU;
using ILGPU.Runtime;
using PerfectNumbers.Core.Gpu;
namespace PerfectNumbers.Core;
@@ -7,9 +13,6 @@ public sealed class DivisorCycleCache
{
internal sealed class CycleBlock
{
// TODO: This can be removed. See other comments.
private int _referenceCount;
internal CycleBlock(int index, ulong start, ulong[] cycles)
{
Index = index;
@@ -18,7 +21,6 @@ public sealed class DivisorCycleCache
End = start + (ulong)cycles.Length - 1UL;
}
// TODO: Let's use fields instead of the properties below for better performance
internal int Index { get; }
internal ulong Start { get; }
@@ -27,27 +29,9 @@ public sealed class DivisorCycleCache
internal ulong[] Cycles { get; }
// TODO: We don't need full reference tracking. It's enough to assign divisor block to a task, instead. We can remove excessive blocks as required.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void AddRef()
{
Interlocked.Increment(ref _referenceCount);
}
// TODO: We don't need full reference tracking. It's enough to assign divisor block to a task, instead. We can remove excessive blocks as required.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Release()
{
Interlocked.Decrement(ref _referenceCount);
}
// TODO: We don't need full reference tracking. It's enough to assign divisor block to a task, instead. This can be removed
internal bool IsInUse => Volatile.Read(ref _referenceCount) > 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ulong GetCycle(ulong divisor)
{
// TODO: The production code should guarantee that this will never occur and we'll use divisors with their corresponding blocks, only. This can be removed and we can simplify this method to =>
if (divisor < Start || divisor > End)
{
return 0UL;
@@ -59,21 +43,15 @@ public sealed class DivisorCycleCache
public struct Lease : IDisposable
{
// TODO: We can probably remove _owner, if we don't need full cycle management which will help GC to clear these out
private readonly DivisorCycleCache? _owner;
private CycleBlock? _block;
internal Lease(DivisorCycleCache owner, CycleBlock block)
internal Lease(CycleBlock block)
{
_owner = owner;
_block = block;
_block.AddRef();
}
// TODO: We're highly over-complicating this. _block is always assigned by the constructor. We don't need to make it nullable. This can be removed.
public readonly bool IsValid => _block is not null;
// TODO: Once we make _block non-nullable we can simplify these to just fields assigned during the creation for the best performance.
public readonly ulong Start => _block?.Start ?? 0UL;
public readonly ulong End => _block?.End ?? 0UL;
@@ -85,32 +63,19 @@ public sealed class DivisorCycleCache
public void Dispose()
{
// TODO: We don't need full cycle block management. We can rely on GC to do its job. Thanks to that we could possibly make the struct read-only.
CycleBlock? block = _block;
if (block is null)
{
return;
}
block.Release();
_owner?.Release(block);
_block = null;
}
}
private readonly object _sync = new();
private readonly ConcurrentDictionary<int, Task<CycleBlock>> _pending = new();
// TODO: _baseBlock should be like no other block. Why do we need a separate property for it? It should become current upon initialization
private readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>>> _gpuKernelCache = new(AcceleratorReferenceComparer.Instance);
private CycleBlock _baseBlock = null!;
private CycleBlock? _previous;
private CycleBlock? _current;
private CycleBlock? _next;
private bool _initialized;
private CycleBlock? _activeBlock;
private CycleBlock? _prefetchedBlock;
private bool _useGpuGeneration = true;
// TODO: Let's not use lazy but directly create the instance, instead for better performance. Then we can remove _lazy and just assign the instance to Shared.
private static readonly Lazy<DivisorCycleCache> _lazy = new(() => new DivisorCycleCache());
public static DivisorCycleCache Shared => _lazy.Value;
public static DivisorCycleCache Shared { get; } = new DivisorCycleCache();
private DivisorCycleCache()
{
@@ -124,44 +89,44 @@ public sealed class DivisorCycleCache
Initialize(snapshot);
}
public void ConfigureGeneratorDevice(bool useGpu)
{
lock (_sync)
{
_useGpuGeneration = useGpu;
}
}
private void Initialize(ulong[] snapshot)
{
lock (_sync)
{
_pending.Clear();
_baseBlock = new CycleBlock(index: 0, start: 0UL, snapshot);
_previous = _baseBlock;
_current = null;
_next = null;
_initialized = true;
_activeBlock = null;
_prefetchedBlock = null;
StartPrefetchLocked(1);
}
}
public Lease Acquire(ulong divisor)
{
// TODO: We don't need _initialized. The constructor calls Initialize and makes sure it's always initialized, before use. This check can be removed, too.
if (!_initialized)
{
throw new InvalidOperationException("DivisorCycleCache must be initialized before use.");
}
// TODO: Do we need Lease instances, if we don't care who is using our blocks once they were given and just remove them internally from future use?
if (divisor <= _baseBlock.End)
{
return new Lease(this, _baseBlock);
return new Lease(_baseBlock);
}
CycleBlock block = AcquireDynamicBlock(divisor);
return new Lease(this, block);
return new Lease(block);
}
private CycleBlock AcquireDynamicBlock(ulong divisor)
{
int blockIndex = GetBlockIndex(divisor);
while (true)
{
Task<CycleBlock>? taskToAwait = null;
Task<CycleBlock>? pendingTask = null;
lock (_sync)
{
if (TryGetCachedBlockLocked(blockIndex, out CycleBlock? cached))
@@ -169,14 +134,14 @@ public sealed class DivisorCycleCache
return cached!;
}
if (!_pending.TryGetValue(blockIndex, out taskToAwait))
if (!_pending.TryGetValue(blockIndex, out pendingTask))
{
taskToAwait = Task.Run(() => GenerateBlock(blockIndex));
_pending[blockIndex] = taskToAwait;
pendingTask = Task.Run(() => GenerateBlock(blockIndex));
_pending[blockIndex] = pendingTask;
}
}
CycleBlock block = taskToAwait.GetAwaiter().GetResult();
CycleBlock block = pendingTask.GetAwaiter().GetResult();
lock (_sync)
{
_pending.TryRemove(blockIndex, out _);
@@ -186,7 +151,7 @@ public sealed class DivisorCycleCache
return cachedAfterAwait!;
}
PromoteBlockLocked(block);
SetActiveBlockLocked(block);
return block;
}
}
@@ -194,22 +159,18 @@ public sealed class DivisorCycleCache
private bool TryGetCachedBlockLocked(int blockIndex, out CycleBlock? block)
{
if (_current is { Index: var currentIndex } current && currentIndex == blockIndex)
CycleBlock? active = _activeBlock;
if (active is not null && active.Index == blockIndex)
{
block = current;
block = active;
return true;
}
if (_previous is { Index: var previousIndex } previous && previousIndex == blockIndex)
CycleBlock? prefetched = _prefetchedBlock;
if (prefetched is not null && prefetched.Index == blockIndex)
{
block = previous;
return true;
}
if (_next is { Index: var nextIndex } next && nextIndex == blockIndex)
{
PromoteNextLocked();
block = _current!;
PromotePrefetchedLocked();
block = _activeBlock!;
return true;
}
@@ -217,59 +178,41 @@ public sealed class DivisorCycleCache
return false;
}
private void PromoteNextLocked()
private void PromotePrefetchedLocked()
{
if (_next is null)
CycleBlock? prefetched = _prefetchedBlock;
if (prefetched is null)
{
return;
}
CycleBlock? oldPrevious = _previous;
_previous = _current ?? _baseBlock;
_current = _next;
_next = null;
StartPrefetchLocked(_current!.Index + 1);
TryReleaseBlockLocked(oldPrevious);
_activeBlock = prefetched;
_prefetchedBlock = null;
StartPrefetchLocked(prefetched.Index + 1);
}
private void PromoteBlockLocked(CycleBlock block)
private void SetActiveBlockLocked(CycleBlock block)
{
if (_current is null)
_activeBlock = block;
CycleBlock? prefetched = _prefetchedBlock;
if (prefetched is not null && prefetched.Index != block.Index + 1)
{
_previous = _baseBlock;
_current = block;
_prefetchedBlock = null;
}
StartPrefetchLocked(block.Index + 1);
return;
}
if (block.Index > _current.Index)
{
CycleBlock? oldPrevious = _previous;
_previous = _current;
_current = block;
StartPrefetchLocked(block.Index + 1);
TryReleaseBlockLocked(oldPrevious);
return;
}
if (_previous is null || block.Index >= _previous.Index)
{
CycleBlock? oldPrevious = _previous;
_previous = block;
TryReleaseBlockLocked(oldPrevious);
}
}
private void StartPrefetchLocked(int blockIndex)
{
// TODO: Why do we need this check? This should never happen in production code.
if (blockIndex <= 0)
{
return;
}
// TODO: Why do we need this check? This should never happen in production code.
if (_next is { Index: var nextIndex } next && nextIndex == blockIndex)
CycleBlock? prefetched = _prefetchedBlock;
if (prefetched is not null && prefetched.Index == blockIndex)
{
return;
}
@@ -278,12 +221,8 @@ public sealed class DivisorCycleCache
{
if (existing.IsCompletedSuccessfully)
{
if (_next is null || _next.Index < blockIndex)
{
_next = existing.Result;
}
_pending.TryRemove(blockIndex, out _);
AssignPrefetchedBlockLocked(blockIndex, existing.Result);
}
return;
@@ -304,70 +243,22 @@ public sealed class DivisorCycleCache
lock (_sync)
{
if (_next is null || _next.Index < blockIndex)
{
_next = task.Result;
}
_pending.TryRemove(blockIndex, out _);
TrimCacheLocked();
AssignPrefetchedBlockLocked(blockIndex, task.Result);
}
}
private void TrimCacheLocked()
private void AssignPrefetchedBlockLocked(int blockIndex, CycleBlock block)
{
if (_previous is null)
CycleBlock? active = _activeBlock;
if (active is not null && blockIndex != active.Index + 1)
{
return;
}
if (ReferenceEquals(_previous, _baseBlock))
if (_prefetchedBlock is null || _prefetchedBlock.Index < blockIndex)
{
return;
}
if (_previous.IsInUse)
{
return;
}
if (_next is not null && _current is not null)
{
_previous = null;
}
}
private void TryReleaseBlockLocked(CycleBlock? block)
{
if (block is null || ReferenceEquals(block, _baseBlock))
{
return;
}
if (ReferenceEquals(block, _current) || ReferenceEquals(block, _next))
{
return;
}
if (block.IsInUse)
{
return;
}
if (ReferenceEquals(block, _previous))
{
_previous = null;
}
}
private void Release(CycleBlock block)
{
if (!block.IsInUse)
{
lock (_sync)
{
TryReleaseBlockLocked(block);
}
_prefetchedBlock = block;
}
}
@@ -379,32 +270,34 @@ public sealed class DivisorCycleCache
}
ulong start = GetBlockStart(blockIndex);
int length = GetBlockLength(blockIndex);
int length = _baseBlock.Cycles.Length;
ulong[] cycles = new ulong[length];
ulong divisor = start;
for (int i = 0; i < length; i++)
if (_useGpuGeneration)
{
cycles[i] = MersenneDivisorCycles.CalculateCycleLength(divisor);
divisor++;
ComputeCyclesGpu(start, cycles);
}
else
{
ComputeCyclesCpu(start, cycles);
}
return new CycleBlock(blockIndex, start, cycles);
}
private int GetBlockLength(int blockIndex)
private int GetBlockIndex(ulong divisor)
{
// TODO: We're over-complicating this again. We should just remember and use the _baseBlock.Cycles.Length. Remember it in a field for better performance during initialization / creation
if (blockIndex == 0)
if (divisor <= _baseBlock.End)
{
return _baseBlock.Cycles.Length;
return 0;
}
return PerfectNumberConstants.MaxQForDivisorCycles;
ulong offset = divisor - (_baseBlock.End + 1UL);
return (int)(offset / (ulong)PerfectNumberConstants.MaxQForDivisorCycles) + 1;
}
private ulong GetBlockStart(int blockIndex)
{
// TODO: We're over-complicating this. There should be only 3 blocks kept. We should simply check all 3 to return the correct one, instead of expensive multiplications.
if (blockIndex == 0)
{
return 0UL;
@@ -414,15 +307,72 @@ public sealed class DivisorCycleCache
return baseEnd + 1UL + (ulong)(blockIndex - 1) * (ulong)PerfectNumberConstants.MaxQForDivisorCycles;
}
private int GetBlockIndex(ulong divisor)
private void ComputeCyclesCpu(ulong start, ulong[] destination)
{
// TODO: We're over-complicating this. There should be only 3 blocks kept. We should simply check all 3 to return the correct one, instead of expensive multiplications.
if (divisor <= _baseBlock.End)
ulong divisor = start;
for (int i = 0; i < destination.Length; i++)
{
return 0;
destination[i] = MersenneDivisorCycles.CalculateCycleLength(divisor);
divisor++;
}
}
ulong offset = divisor - (_baseBlock.End + 1UL);
return (int)(offset / (ulong)PerfectNumberConstants.MaxQForDivisorCycles) + 1;
private void ComputeCyclesGpu(ulong start, ulong[] destination)
{
var gpuLease = GpuKernelPool.GetKernel(_useGpuGeneration);
var execution = gpuLease.EnterExecutionScope();
try
{
Accelerator accelerator = gpuLease.Accelerator;
Action<Index1D, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>> kernel = _gpuKernelCache.GetOrAdd(accelerator, LoadKernel);
int length = destination.Length;
ulong[] divisors = ArrayPool<ulong>.Shared.Rent(length);
try
{
for (int i = 0; i < length; i++)
{
divisors[i] = start + (ulong)i;
}
using MemoryBuffer1D<ulong, Stride1D.Dense> divisorBuffer = accelerator.Allocate1D<ulong>(length);
using MemoryBuffer1D<ulong, Stride1D.Dense> resultBuffer = accelerator.Allocate1D<ulong>(length);
divisorBuffer.View.CopyFromCPU(ref divisors[0], length);
kernel(length, divisorBuffer.View, resultBuffer.View);
accelerator.Synchronize();
resultBuffer.View.CopyToCPU(ref destination[0], length);
}
finally
{
ArrayPool<ulong>.Shared.Return(divisors, clearArray: false);
}
}
finally
{
execution.Dispose();
gpuLease.Dispose();
}
}
private static Action<Index1D, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>> LoadKernel(Accelerator accelerator)
{
return accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>>(GpuDivisorCycleKernel);
}
private static void GpuDivisorCycleKernel(Index1D index, ArrayView1D<ulong, Stride1D.Dense> divisors, ArrayView1D<ulong, Stride1D.Dense> cycles)
{
cycles[index] = MersenneDivisorCycles.CalculateCycleLengthGpu(divisors[index]);
}
private sealed class AcceleratorReferenceComparer : IEqualityComparer<Accelerator>
{
internal static AcceleratorReferenceComparer Instance { get; } = new();
public bool Equals(Accelerator? x, Accelerator? y) => ReferenceEquals(x, y);
public int GetHashCode(Accelerator obj) => RuntimeHelpers.GetHashCode(obj);
}
}
@@ -129,7 +129,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
{
lock (_sync)
{
UpdateStatusUnsafe(lastProcessed, processedCount);
UpdateStatusUnsafe(processedCount);
}
}
@@ -174,6 +174,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
ulong currentDivisor, divisor = 3UL, next, nextDivisor;
int batchSize, i;
bool reachedEndInBatch;
bool requiresCycleKernelCheck;
Span<MontgomeryDivisorData> divisorDataSpan;
Span<ulong> divisorSpan;
@@ -185,6 +186,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
{
batchSize = 0;
reachedEndInBatch = false;
requiresCycleKernelCheck = false;
while (batchSize < batchCapacity && divisor <= allowedMax)
{
@@ -192,11 +194,29 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
nextDivisor = currentDivisor + 2UL;
processedCount++;
bool includeDivisor = true;
if (useCycles)
{
using DivisorCycleCache.Lease cycleLease = DivisorCycleCache.Shared.Acquire(currentDivisor);
ulong cycle = cycleLease.GetCycle(currentDivisor);
if (cycle == 0UL)
{
requiresCycleKernelCheck = true;
}
else if (prime % cycle != 0UL)
{
includeDivisor = false;
}
}
if (includeDivisor)
{
divisors[batchSize++] = currentDivisor;
}
lastProcessed = currentDivisor;
next = nextDivisor;
// This will be only true when we exceed ulong.MaxValue
if (next <= divisor)
{
reachedEndInBatch = true;
@@ -218,9 +238,10 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
if (reachedEndInBatch)
{
processedAll = true;
break;
}
break;
continue;
}
divisorDataSpan = divisorData.AsSpan(0, batchSize);
@@ -236,7 +257,8 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
}
divisorView.CopyFromCPU(ref MemoryMarshal.GetReference(divisorDataSpan), batchSize);
kernel(batchSize, prime, useCycles ? (byte)1 : (byte)0, divisorView, hitsView);
byte useCycleChecks = useCycles && requiresCycleKernelCheck ? (byte)1 : (byte)0;
kernel(batchSize, prime, useCycleChecks, divisorView, hitsView);
accelerator.Synchronize();
hitsView.CopyToCPU(ref MemoryMarshal.GetReference(hitsSpan), batchSize);
@@ -301,8 +323,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
return Math.Min((1UL << (int)(prime - 1UL)) - 1UL, divisorLimit);
}
// TODO: Since lastProcessed is unused, let's remove it.
private void UpdateStatusUnsafe(ulong lastProcessed, ulong processedCount)
private void UpdateStatusUnsafe(ulong processedCount)
{
if (processedCount == 0UL)
{
@@ -340,7 +361,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
hits[index] = 0;
return;
}
// TODO: Can we move this back before the GPU call and let CPU handle it? We significantly slowed down after this was moved to GPU.
ulong remainder = prime % cycle;
if (remainder != 0UL)
{
@@ -456,9 +476,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
Span<ulong> hostSpan = _hostBuffer.AsSpan();
Span<ulong> primeSpan = _primeBuffer.AsSpan();
Span<int> positionSpan = _positionBuffer.AsSpan();
bool hasPreviousResidue = false;
ulong previousResidue = 0UL;
ulong previousPrime = 0UL;
while (offset < primesLength)
{
@@ -467,63 +484,47 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
Span<byte> hitsSlice = hits.Slice(offset, batchSize);
int computeCount = 0;
Span<byte> cycleMaskSpan = default;
if (cycleEnabled)
{
// TODO: Can we move this back before the GPU call and let CPU handle it? We significantly slowed down after this was moved to GPU.
cycleMaskSpan = _cycleMaskHost.AsSpan(0, batchSize);
Span<byte> cycleMaskSpan = _cycleMaskHost.AsSpan(0, batchSize);
ArrayView1D<ulong, Stride1D.Dense> primeInputView = _resultsBuffer.View.SubView(0, batchSize);
primeInputView.CopyFromCPU(ref MemoryMarshal.GetReference(primesSlice), batchSize);
ArrayView1D<byte, Stride1D.Dense> cycleMaskView = cycleMaskViewFull.SubView(0, batchSize);
cycleKernel(batchSize, cycle, primeInputView, cycleMaskView);
accelerator.Synchronize();
cycleMaskView.CopyToCPU(ref MemoryMarshal.GetReference(cycleMaskSpan), batchSize);
}
for (int i = 0; i < batchSize; i++)
{
if (cycleEnabled && cycleMaskSpan[i] == 0)
if (cycleMaskSpan[i] == 0)
{
hitsSlice[i] = 0;
continue;
}
// TODO: Avoid one-time use variables like prime here.
ulong prime = primesSlice[i];
primeSpan[computeCount] = prime;
primeSpan[computeCount] = primesSlice[i];
positionSpan[computeCount] = i;
computeCount++;
}
}
else
{
for (int i = 0; i < batchSize; i++)
{
primeSpan[computeCount] = primesSlice[i];
positionSpan[computeCount] = i;
computeCount++;
}
}
if (computeCount > 0)
{
Span<ulong> exponentSlice = hostSpan[..computeCount];
ulong currentPrime = hasPreviousResidue ? previousPrime : 0UL;
bool hasDeltaSource = hasPreviousResidue;
for (int i = 0; i < computeCount; i++)
{
ulong prime = primeSpan[i];
ulong exponentValue;
if (hasDeltaSource)
{
exponentValue = prime - currentPrime;
}
else
{
exponentValue = prime;
hasDeltaSource = true;
}
if (cycleEnabled)
{
exponentValue %= cycle;
}
exponentSlice[i] = exponentValue;
currentPrime = prime;
exponentSlice[i] = cycleEnabled ? prime % cycle : prime;
}
ArrayView1D<ulong, Stride1D.Dense> exponentView = exponentsView.SubView(0, computeCount);
@@ -533,29 +534,11 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
accelerator.Synchronize();
resultView.CopyToCPU(ref MemoryMarshal.GetReference(exponentSlice), computeCount);
ulong residueAccumulator = previousResidue;
ulong lastPrime = previousPrime;
bool hasAccumulator = hasPreviousResidue;
for (int i = 0; i < computeCount; i++)
{
// TODO: Avoid one-time use variables like prime and position here.
int position = positionSpan[i];
ulong stepResidue = exponentSlice[i];
ulong prime = primeSpan[i];
ulong residue = hasAccumulator ? MultiplyMod(residueAccumulator, stepResidue, modulus) : stepResidue;
ulong residue = exponentSlice[i];
hitsSlice[position] = residue == 1UL ? (byte)1 : (byte)0;
exponentSlice[i] = residue;
residueAccumulator = residue;
lastPrime = prime;
hasAccumulator = true;
}
if (hasAccumulator)
{
previousResidue = residueAccumulator;
previousPrime = lastPrime;
hasPreviousResidue = true;
}
}
@@ -574,16 +557,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
_owner.ReturnSession(this);
}
private static ulong MultiplyMod(ulong left, ulong right, ulong modulus)
{
if (modulus == 0UL)
{
return 0UL;
}
UInt128 product = (UInt128)left * right;
return (ulong)(product % modulus);
}
}
+17
View File
@@ -246,6 +246,23 @@ public static class ULongExtensions
return result.MontgomeryMultiply(1UL, modulus, nPrime);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Pow2MontgomeryModWithCycle(this ulong exponent, ulong cycleLength, in MersenneNumberDivisorByDivisorGpuTester.MontgomeryDivisorData divisor)
{
if (cycleLength == 0UL)
{
return exponent.Pow2MontgomeryMod(divisor);
}
ulong rotationCount = exponent % cycleLength;
if (rotationCount == 0UL)
{
return 1UL;
}
return rotationCount.Pow2MontgomeryMod(divisor);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static UInt128 PowMod(this ulong exponent, UInt128 modulus)
{