Divisor-Cycle Cache Rework

This commit is contained in:
Marek Lemańczyk committed 2025-10-02 19:53:09 +02:00
1 parent f9e548279c
commit ebe48d5f2f
11 files changed
+1836 -2660

No files matched your search

@@ -1,6 +1,7 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
File diff suppressed because it is too large. Load diff
@@ -1,22 +0,0 @@
using FluentAssertions;
using Xunit;
namespace PerfectNumbers.Core.Tests;
[Trait("Category", "Fast")]
public class EulerPrimeTesterTests
{
private readonly EulerPrimeTester _tester = new(new PrimeTester());
[Theory]
[InlineData(13UL, true)]
[InlineData(29UL, true)]
[InlineData(3UL, false)]
[InlineData(21UL, false)]
[InlineData(1UL, false)]
public void IsEulerPrime_returns_expected(ulong n, bool expected)
{
_tester.IsEulerPrime(n).Should().Be(expected);
}
}
@@ -110,7 +110,6 @@ public class MersenneNumberDivisorGpuTesterTests
{
var tester = new MersenneNumberDivisorByDivisorGpuTester
{
UseDivisorCycles = true,
GpuBatchSize = 8,
};
@@ -147,7 +146,6 @@ public class MersenneNumberDivisorGpuTesterTests
{
var tester = new MersenneNumberDivisorByDivisorGpuTester
{
UseDivisorCycles = true,
GpuBatchSize = 2,
};
@@ -169,7 +167,6 @@ public class MersenneNumberDivisorGpuTesterTests
{
var tester = new MersenneNumberDivisorByDivisorCpuTester
{
UseDivisorCycles = true,
BatchSize = 2,
};
@@ -12,14 +12,8 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
private ulong _divisorLimit;
private ulong _lastStatusDivisor;
private bool _isConfigured;
private bool _useDivisorCycles;
private int _batchSize = 1_024;
public bool UseDivisorCycles
{
get => _useDivisorCycles;
set => _useDivisorCycles = value; // TODO: Delete the toggle once divisor cycle data is always consulted so CPU scans never fall back to the slower pure-Montgomery path.
}
public int BatchSize
{
@@ -69,8 +63,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
public bool IsPrime(ulong prime, out bool divisorsExhausted)
{
ulong allowedMax;
bool useCycles;
lock (_sync)
{
if (!_isConfigured)
@@ -79,7 +71,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
}
allowedMax = ComputeAllowedMaxDivisor(prime, _divisorLimit);
useCycles = _useDivisorCycles;
}
if (allowedMax < 3UL)
@@ -93,12 +84,11 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
bool processedAll = false;
bool composite = CheckDivisors(
prime,
allowedMax,
useCycles,
out lastProcessed,
out processedAll,
out processedCount);
prime,
allowedMax,
out lastProcessed,
out processedAll,
out processedCount);
if (processedCount > 0UL)
{
@@ -168,12 +158,11 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
}
private bool CheckDivisors(
ulong prime,
ulong allowedMax,
bool useCycles,
out ulong lastProcessed,
out bool processedAll,
out ulong processedCount)
ulong prime,
ulong allowedMax,
out ulong lastProcessed,
out bool processedAll,
out ulong processedCount)
{
lastProcessed = 0UL;
processedCount = 0UL;
@@ -194,35 +183,15 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
// TODO: Hoist MontgomeryDivisorData acquisition into the divisor-cycle cache so we reuse the
// staged ProcessEightBitWindows-ready metadata instead of reloading the slower Montgomery
// structs for every divisor.
byte hit;
ulong divisorCycle = 0UL;
DivisorCycleCache.CycleBlock? cycleBlock = null;
if (useCycles)
ulong divisorCycle = DivisorCycleCache.Shared.GetCycleLength(divisor);
if (divisorCycle != 0UL)
{
cycleBlock = DivisorCycleCache.Shared.Acquire(divisor);
// TODO: Swap this block lease for the direct single-cycle lookup once the cache exposes it
// so the CPU scanner mirrors the benchmarked single-block policy without retaining mutable
// blocks beyond the startup snapshot.
divisorCycle = cycleBlock.GetCycle(divisor);
// TODO: When divisorCycle is zero (cache miss), compute only that single cycle on the device
// selected by the current settings, skip inserting the result into the shared cache, and keep
// operating with the single on-disk block without requesting additional blocks.
hit = CheckDivisor(prime, divisorCycle != 0UL, divisorCycle, divisorData);
}
else
{
// TODO: Remove this no-cycle branch and require on-demand cycle computation (without
// mutating the cache) so CPU scans never fall back to the slower Montgomery-only path
// identified in the benchmarks.
hit = CheckDivisor(prime, false, 0UL, divisorData);
}
cycleBlock?.Dispose();
if (hit != 0)
{
processedAll = true;
return true;
byte hit = CheckDivisor(prime, divisorCycle, divisorData);
if (hit != 0)
{
processedAll = true;
return true;
}
}
if (divisor >= allowedMax - 1UL)
@@ -249,22 +218,18 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
return false;
}
private static byte CheckDivisor(ulong prime, bool useCycles, ulong divisorCycle, in MontgomeryDivisorData divisorData)
private static byte CheckDivisor(ulong prime, ulong divisorCycle, in MontgomeryDivisorData divisorData)
{
ulong modulus = divisorData.Modulus;
if (modulus <= 1UL || (modulus & 1UL) == 0UL || divisorCycle == 0UL)
{
ulong modulus = divisorData.Modulus;
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
return 0;
}
ulong residue = useCycles && divisorCycle != 0UL
? prime.Pow2MontgomeryModWithCycle(divisorCycle, divisorData)
: prime.Pow2MontgomeryMod(divisorData); // TODO: Switch to the upcoming ProcessEightBitWindows helper once the
// scalar Pow2Minus1Mod adopts it so CPU scans match the GPU speedups.
return residue == 1UL ? (byte)1 : (byte)0;
return 0;
}
ulong residue = prime.Pow2MontgomeryModWithCycle(divisorCycle, divisorData);
return residue == 1UL ? (byte)1 : (byte)0;
}
private void UpdateStatusUnsafe(ulong lastProcessed, ulong processedCount)
{
if (processedCount == 0UL)
@@ -343,23 +308,22 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
return;
}
ulong modulus = divisor;
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
if (divisor <= 1UL || (divisor & 1UL) == 0UL)
{
hits.Clear();
return;
}
bool useDivisorCycles = _owner._useDivisorCycles;
if (useDivisorCycles && divisorCycle == 0UL)
if (divisorCycle == 0UL)
{
// TODO: Replace this guard with an on-demand CPU/GPU cycle computation path that skips
// cache insertion and continues operating with the single cached block instead of
// requesting additional cycle batches when a lookup misses.
throw new InvalidOperationException($"Missing divisor cycle for divisor {divisor}.");
divisorCycle = DivisorCycleCache.Shared.GetCycleLength(divisor);
if (divisorCycle == 0UL)
{
hits.Clear();
return;
}
}
bool cycleEnabled = useDivisorCycles;
MontgomeryDivisorData divisorData = MontgomeryDivisorDataCache.Get(divisor);
var exponentStepper = new ExponentRemainderStepper(divisorData);
@@ -369,39 +333,25 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
return;
}
if (cycleEnabled)
var stepper = new CycleRemainderStepper(divisorCycle);
ulong remainder = stepper.Initialize(primes[0]);
hits[0] = remainder == 0UL
? (exponentStepper.ComputeNextIsUnity(primes[0]) ? (byte)1 : (byte)0)
: (byte)0;
for (int i = 1; i < length; i++)
{
var stepper = new CycleRemainderStepper(divisorCycle);
ulong remainder = stepper.Initialize(primes[0]);
hits[0] = remainder == 0UL
? (exponentStepper.ComputeNextIsUnity(primes[0]) ? (byte)1 : (byte)0)
: (byte)0;
for (int i = 1; i < length; i++)
remainder = stepper.ComputeNext(primes[i]);
if (remainder != 0UL)
{
remainder = stepper.ComputeNext(primes[i]);
if (remainder != 0UL)
{
hits[i] = 0;
continue;
}
hits[i] = exponentStepper.ComputeNextIsUnity(primes[i]) ? (byte)1 : (byte)0;
hits[i] = 0;
continue;
}
return;
}
// TODO: Remove this no-cycle fallback once divisor cycles are mandatory; the per-prime pow2 ladder here
// keeps the slower Montgomery stepping alive even though the benchmarks showed the cached cycle path is far
// faster for large divisor sets.
for (int i = 0; i < length; i++)
{
hits[i] = exponentStepper.ComputeNextIsUnity(primes[i]) ? (byte)1 : (byte)0;
}
}
public void Dispose()
{
if (_disposed)
@@ -415,3 +365,4 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
}
}
+166 -466
View File
@@ -1,10 +1,10 @@
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using ILGPU;
using ILGPU.Runtime;
using PerfectNumbers.Core.Gpu;
@@ -13,535 +13,232 @@ namespace PerfectNumbers.Core;
public sealed class DivisorCycleCache
{
public sealed class CycleBlock : IDisposable
{
private readonly DivisorCycleCache _owner;
private int _referenceCount;
internal CycleBlock(DivisorCycleCache owner, int index, ulong start, in ulong[] cycles)
{
_owner = owner;
Index = index;
Start = start;
End = start + (ulong)cycles.Length - 1UL;
Cycles = cycles;
}
internal int Index { get; }
internal ulong Start { get; }
internal ulong End { get; }
internal ulong[] Cycles { get; }
internal int ReferenceCount => Volatile.Read(ref _referenceCount);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ulong GetCycle(ulong divisor) => Cycles[(int)(divisor - Start)];
internal void Retain()
{
Interlocked.Increment(ref _referenceCount);
}
public void Dispose()
{
int remaining = Interlocked.Decrement(ref _referenceCount);
if (remaining < 0)
{
throw new InvalidOperationException("Cycle block released more times than it was acquired.");
}
if (remaining == 0)
{
_owner.OnCycleBlockReleased(this);
}
}
}
private const int CycleGenerationBatchSize = 262_144; // TODO: Remove the block-sized batch once the single-cycle generator
// replaces block hydration so we stop materializing large buffers
// just to satisfy one missing divisor lookup.
private const byte ByteZero = 0;
private const byte ByteOne = 1;
private const int StackBufferThreshold = 128;
private readonly object _sync = new(); // TODO: Delete the synchronization object once the cache exposes only snapshot reads and single-cycle computation so callers no longer contend on locks.
private readonly ConcurrentDictionary<int, Task<CycleBlock>> _pending = new(); // TODO: Drop the concurrent dictionary entirely when the block pipeline is replaced with single-cycle generation; the new plan never queues background work.
private readonly ConcurrentDictionary<Accelerator, Action<Index1D, int, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<byte, Stride1D.Dense>>> _gpuKernelCache = new(AcceleratorReferenceComparer.Instance); // TODO: Swap this ConcurrentDictionary for a simple accelerator-indexed array once the single-cycle helper precompiles kernels during startup, eliminating the need for thread-safe lookups.
private CycleBlock _baseBlock = null!;
private CycleBlock? _activeBlock; // TODO: Remove dynamic blocks altogether once lookups route to the snapshot or compute an individual cycle without mutating shared state.
private CycleBlock? _prefetchedBlock; // TODO: Delete prefetch support so the cache never materializes extra blocks and callers always compute the missing cycle inline.
private bool _useGpuGeneration = true;
private readonly int _divisorCyclesBatchSize;
private static int _sharedDivisorCyclesBatchSize = GpuConstants.GpuCycleStepsPerInvocation;
private readonly ConcurrentDictionary<Accelerator, Action<Index1D, int, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<byte, Stride1D.Dense>>> _gpuKernelCache = new(AcceleratorReferenceComparer.Instance);
private volatile ulong[] _snapshot;
private volatile bool _useGpuGeneration = true;
private readonly int _divisorCyclesBatchSize;
private static int _sharedDivisorCyclesBatchSize = GpuConstants.GpuCycleStepsPerInvocation;
public static void SetDivisorCyclesBatchSize(int divisorCyclesBatchSize)
{
_sharedDivisorCyclesBatchSize = Math.Max(1, divisorCyclesBatchSize);
}
public static void SetDivisorCyclesBatchSize(int divisorCyclesBatchSize) => _sharedDivisorCyclesBatchSize = divisorCyclesBatchSize;
public static DivisorCycleCache Shared { get; } = new DivisorCycleCache(_sharedDivisorCyclesBatchSize);
public int PreferredBatchSize => _divisorCyclesBatchSize;
private DivisorCycleCache(int divisorCyclesBatchSize)
{
_divisorCyclesBatchSize = divisorCyclesBatchSize;
ulong[] snapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot();
Initialize(snapshot);
}
// TODO: Remove the ability to reload at runtime once the cache becomes a read-only snapshot plus
// transient single-cycle generator; future callers should load the snapshot during startup only.
public void ReloadFromCurrentSnapshot()
{
ulong[] snapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot();
Initialize(snapshot);
_divisorCyclesBatchSize = Math.Max(1, divisorCyclesBatchSize);
_snapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot();
}
public void ConfigureGeneratorDevice(bool useGpu)
{
lock (_sync)
{
_useGpuGeneration = useGpu;
}
// TODO: Remove the lock once single-cycle generation eliminates shared mutable state; a simple
// volatile write will be enough when the cache becomes read-only.
_useGpuGeneration = useGpu;
}
private void Initialize(ulong[] snapshot)
public void RefreshSnapshot()
{
CycleBlock? activeToDispose = null;
CycleBlock? prefetchedToDispose = null;
_snapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot();
}
lock (_sync)
public ulong GetCycleLength(ulong divisor)
{
Span<ulong> result = stackalloc ulong[1];
ReadOnlySpan<ulong> singleDivisor = stackalloc ulong[1] { divisor };
GetCycleLengths(singleDivisor, result);
return result[0];
}
public void GetCycleLengths(ReadOnlySpan<ulong> divisors, Span<ulong> cycles)
{
ulong[] snapshot = _snapshot;
int length = divisors.Length;
int[]? rentedMissing = null;
Span<int> missingBuffer = length <= StackBufferThreshold
? stackalloc int[length]
: new Span<int>(rentedMissing = ArrayPool<int>.Shared.Rent(length), 0, length);
int missingCount = 0;
for (int i = 0; i < length; i++)
{
if (_activeBlock is not null && _activeBlock.Index != 0)
ulong divisor = divisors[i];
if (divisor <= 1UL)
{
activeToDispose = _activeBlock;
throw new InvalidDataException("Divisor must be > 1");
}
if (_prefetchedBlock is not null && _prefetchedBlock.Index != 0)
if (divisor < (ulong)snapshot.Length)
{
prefetchedToDispose = _prefetchedBlock;
}
_pending.Clear();
// TODO: Remove the dictionary once prefetching and background block tracking disappear; with single-cycle generation we will keep only the snapshot and skip additional bookkeeping entirely.
_baseBlock = new CycleBlock(this, index: 0, start: 0UL, snapshot);
_activeBlock = null;
_prefetchedBlock = null;
StartPrefetchLocked(1);
}
activeToDispose?.Dispose();
prefetchedToDispose?.Dispose();
}
public CycleBlock Acquire(ulong divisor)
{
CycleBlock block = divisor <= _baseBlock.End ? _baseBlock : AcquireDynamicBlock(divisor);
// TODO: Replace this block-based acquisition with a direct GetCycle API that computes the missing cycle synchronously on the configured device so the cache never hands out mutable blocks or requires disposal.
block.Retain();
return block;
}
private CycleBlock AcquireDynamicBlock(ulong divisor)
{
int blockIndex = GetBlockIndex(divisor);
while (true)
{
Task<CycleBlock>? pendingTask = null;
lock (_sync)
{
if (TryGetCachedBlockLocked(blockIndex, out CycleBlock? cached))
ulong cached = snapshot[divisor];
if (cached == 0UL)
{
return cached!;
throw new InvalidDataException($"Divisor cycle is missing for {divisor}");
}
if (!_pending.TryGetValue(blockIndex, out pendingTask))
{
// TODO: Replace Task.Run with an inline single-cycle computation that runs on the configured
// device so we materialize exactly one missing cycle and stop scheduling thread-pool work for
// entire blocks.
pendingTask = Task.Run(() => GenerateBlock(blockIndex));
_pending[blockIndex] = pendingTask;
}
cycles[i] = cached;
}
CycleBlock block = pendingTask.GetAwaiter().GetResult();
lock (_sync)
else
{
_pending.TryRemove(blockIndex, out _);
if (TryGetCachedBlockLocked(blockIndex, out CycleBlock? cachedAfterAwait))
{
return cachedAfterAwait!;
}
SetActiveBlockLocked(block);
// TODO: Stop promoting freshly generated blocks into the shared cache and, instead of
// returning a block, surface the computed cycle directly to the caller so no extra cache
// state survives beyond the single snapshot block.
return block;
missingBuffer[missingCount++] = i;
}
}
}
private bool TryGetCachedBlockLocked(int blockIndex, out CycleBlock? block)
{
CycleBlock? active = _activeBlock;
if (active is not null && active.Index == blockIndex)
if (missingCount != 0)
{
block = active;
return true;
}
CycleBlock? prefetched = _prefetchedBlock;
if (prefetched is not null && prefetched.Index == blockIndex)
{
PromotePrefetchedLocked();
block = _activeBlock!;
return true;
}
block = null;
return false;
}
private void PromotePrefetchedLocked()
{
CycleBlock? prefetched = _prefetchedBlock;
if (prefetched is null)
{
return;
}
CycleBlock? previousActive = _activeBlock;
_activeBlock = prefetched;
_prefetchedBlock = null;
prefetched.Retain();
prefetched.Dispose();
if (previousActive is not null && previousActive.Index != 0 && previousActive.Index != prefetched.Index)
{
previousActive.Dispose();
}
StartPrefetchLocked(prefetched.Index + 1); // TODO: Remove this call once prefetching is dropped
// so we strictly keep a single active block in memory.
}
private void SetActiveBlockLocked(CycleBlock block)
{
CycleBlock? previousActive = _activeBlock;
_activeBlock = block;
block.Retain();
CycleBlock? prefetched = _prefetchedBlock;
if (prefetched is not null && prefetched.Index != block.Index + 1)
{
if (prefetched.Index != 0)
ReadOnlySpan<int> missingIndices = missingBuffer[..missingCount];
if (_useGpuGeneration)
{
prefetched.Dispose();
ComputeCyclesGpu(divisors, cycles, missingIndices);
}
_prefetchedBlock = null;
}
if (previousActive is not null && previousActive.Index != 0 && previousActive.Index != block.Index)
{
previousActive.Dispose();
}
StartPrefetchLocked(block.Index + 1); // TODO: Remove once prefetching is removed and the cache
// no longer speculatively requests the next block after a miss.
}
private void StartPrefetchLocked(int blockIndex)
{
// TODO: Remove prefetching entirely so the cache maintains a single snapshot block and computes
// individual cycles on demand instead of triggering background generation for future indices.
if (blockIndex <= 0)
{
return;
}
CycleBlock? prefetched = _prefetchedBlock;
if (prefetched is not null && prefetched.Index == blockIndex)
{
return;
}
if (_pending.TryGetValue(blockIndex, out Task<CycleBlock>? existing))
{
if (existing.IsCompletedSuccessfully)
else
{
_pending.TryRemove(blockIndex, out _);
AssignPrefetchedBlockLocked(blockIndex, existing.Result);
ComputeCyclesCpu(divisors, cycles, missingIndices);
}
return;
}
// TODO: Delete this once prefetching is removed; single-cycle generation will happen inline and
// no asynchronous work should be scheduled.
Task<CycleBlock> task = Task.Run(() => GenerateBlock(blockIndex));
_pending[blockIndex] = task;
task.ContinueWith(t => OnPrefetchCompleted(blockIndex, t), TaskScheduler.Default);
if (rentedMissing is not null)
{
ArrayPool<int>.Shared.Return(rentedMissing, clearArray: false);
}
}
private void OnPrefetchCompleted(int blockIndex, Task<CycleBlock> task)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ComputeCyclesCpu(ReadOnlySpan<ulong> divisors, Span<ulong> cycles, ReadOnlySpan<int> indices)
{
// TODO: Delete this callback when we remove prefetching. Single-cycle generation will run inline,
// eliminating the need for asynchronous continuation handlers and concurrent dictionaries.
if (!task.IsCompletedSuccessfully)
for (int i = 0; i < indices.Length; i++)
{
_pending.TryRemove(blockIndex, out _);
return;
}
lock (_sync)
{
_pending.TryRemove(blockIndex, out _);
AssignPrefetchedBlockLocked(blockIndex, task.Result);
int targetIndex = indices[i];
cycles[targetIndex] = MersenneDivisorCycles.CalculateCycleLength(divisors[targetIndex]);
}
}
private void AssignPrefetchedBlockLocked(int blockIndex, CycleBlock block)
private void ComputeCyclesGpu(ReadOnlySpan<ulong> divisors, Span<ulong> cycles, ReadOnlySpan<int> indices)
{
// TODO: Remove this entire method when prefetching goes away. The single-cycle flow should never
// allocate or retain extra blocks beyond the startup snapshot, so prefetched block bookkeeping
// becomes unnecessary.
CycleBlock? active = _activeBlock;
if (active is not null && blockIndex != active.Index + 1)
int count = indices.Length;
ulong[]? rentedDivisors = null;
Span<ulong> workDivisors = count <= StackBufferThreshold
? stackalloc ulong[count]
: new Span<ulong>(rentedDivisors = ArrayPool<ulong>.Shared.Rent(count), 0, count);
ulong[]? rentedResults = null;
Span<ulong> workResults = count <= StackBufferThreshold
? stackalloc ulong[count]
: new Span<ulong>(rentedResults = ArrayPool<ulong>.Shared.Rent(count), 0, count);
for (int i = 0; i < count; i++)
{
MontgomeryDivisorDataCache.ReleaseBlock(block);
return;
workDivisors[i] = divisors[indices[i]];
}
CycleBlock? previousPrefetched = _prefetchedBlock;
if (ReferenceEquals(previousPrefetched, block))
ComputeCyclesGpuCore(workDivisors, workResults);
for (int i = 0; i < count; i++)
{
return;
cycles[indices[i]] = workResults[i];
}
if (_prefetchedBlock is null || _prefetchedBlock.Index <= blockIndex)
if (rentedDivisors is not null)
{
if (previousPrefetched is not null && previousPrefetched.Index != 0)
{
previousPrefetched.Dispose();
}
_prefetchedBlock = block;
block.Retain();
}
else
{
MontgomeryDivisorDataCache.ReleaseBlock(block);
ArrayPool<ulong>.Shared.Return(rentedDivisors, clearArray: false);
ArrayPool<ulong>.Shared.Return(rentedResults!, clearArray: false);
}
}
private void OnCycleBlockReleased(CycleBlock block)
private void ComputeCyclesGpuCore(ReadOnlySpan<ulong> divisors, Span<ulong> destination)
{
if (block.Index == 0)
{
return;
}
for (int attempt = 0; attempt < 8; attempt++)
{
if (block.ReferenceCount != 0)
{
return;
}
Thread.SpinWait(1 << attempt);
}
bool stillTracked;
lock (_sync)
{
stillTracked = ReferenceEquals(_activeBlock, block) || ReferenceEquals(_prefetchedBlock, block);
}
if (!stillTracked && block.ReferenceCount == 0)
{
MontgomeryDivisorDataCache.ReleaseBlock(block);
}
}
private CycleBlock GenerateBlock(int blockIndex)
{
if (blockIndex == 0)
{
return _baseBlock;
}
ulong start = GetBlockStart(blockIndex);
int length = _baseBlock.Cycles.Length;
ulong[] cycles = new ulong[length];
if (_useGpuGeneration)
{
ComputeCyclesGpu(start, cycles, _divisorCyclesBatchSize);
}
else
{
ComputeCyclesCpu(start, cycles);
}
// TODO: Replace this block generator with a single-cycle helper that reuses shared buffers,
// returns just the requested divisor's cycle, and avoids mutating cache state beyond the
// preloaded snapshot.
return new CycleBlock(this, blockIndex, start, cycles);
}
private int GetBlockIndex(ulong divisor)
{
if (divisor <= _baseBlock.End)
{
return 0;
}
ulong offset = divisor - (_baseBlock.End + 1UL);
return (int)(offset / (ulong)PerfectNumberConstants.MaxQForDivisorCycles) + 1;
}
private ulong GetBlockStart(int blockIndex)
{
if (blockIndex == 0)
{
return 0UL;
}
ulong baseEnd = _baseBlock.End;
return baseEnd + 1UL + (ulong)(blockIndex - 1) * (ulong)PerfectNumberConstants.MaxQForDivisorCycles;
}
private void ComputeCyclesCpu(ulong start, ulong[] destination)
{
ulong divisor = start;
for (int i = 0; i < destination.Length; i++)
{
// TODO: Port this CPU fallback to the unrolled-hex cycle generator once it is shared so misses outside the snapshot
// stop looping through CalculateCycleLength, which benchmarks showed is far slower than the GPU-assisted pipeline.
destination[i] = MersenneDivisorCycles.CalculateCycleLength(divisor);
divisor++;
}
}
private void ComputeCyclesGpu(ulong start, ulong[] destination, int divisorCyclesBatchSize)
{
var gpuLease = GpuKernelPool.GetKernel(_useGpuGeneration);
int length = divisors.Length;
var gpuLease = GpuKernelPool.GetKernel(useGpuOrder: true);
var execution = gpuLease.EnterExecutionScope();
try
Accelerator accelerator = gpuLease.Accelerator;
var kernel = _gpuKernelCache.GetOrAdd(accelerator, LoadKernel);
using MemoryBuffer1D<ulong, Stride1D.Dense> divisorBuffer = accelerator.Allocate1D<ulong>(length);
using MemoryBuffer1D<ulong, Stride1D.Dense> powBuffer = accelerator.Allocate1D<ulong>(length);
using MemoryBuffer1D<ulong, Stride1D.Dense> orderBuffer = accelerator.Allocate1D<ulong>(length);
using MemoryBuffer1D<ulong, Stride1D.Dense> resultBuffer = accelerator.Allocate1D<ulong>(length);
using MemoryBuffer1D<byte, Stride1D.Dense> statusBuffer = accelerator.Allocate1D<byte>(length);
ulong[]? rentedPow = null;
Span<ulong> powSpan = length <= StackBufferThreshold
? stackalloc ulong[length]
: new Span<ulong>(rentedPow = ArrayPool<ulong>.Shared.Rent(length), 0, length);
ulong[]? rentedOrder = null;
Span<ulong> orderSpan = length <= StackBufferThreshold
? stackalloc ulong[length]
: new Span<ulong>(rentedOrder = ArrayPool<ulong>.Shared.Rent(length), 0, length);
ulong[]? rentedResult = null;
Span<ulong> resultSpan = length <= StackBufferThreshold
? stackalloc ulong[length]
: new Span<ulong>(rentedResult = ArrayPool<ulong>.Shared.Rent(length), 0, length);
byte[]? rentedStatus = null;
Span<byte> statusSpan = length <= StackBufferThreshold
? stackalloc byte[length]
: new Span<byte>(rentedStatus = ArrayPool<byte>.Shared.Rent(length), 0, length);
ref ulong divisorRef = ref MemoryMarshal.GetReference(divisors);
divisorBuffer.View.CopyFromCPU(ref divisorRef, length);
for (int i = 0; i < length; i++)
{
Accelerator accelerator = gpuLease.Accelerator;
Action<Index1D, int, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<byte, Stride1D.Dense>> kernel = _gpuKernelCache.GetOrAdd(accelerator, LoadKernel);
// TODO: Once single-cycle generation is in place, bypass this block-sized kernel entirely and
// call the ProcessEightBitWindows-based GPU helper that materializes only the requested
// divisor, reusing a pinned upload buffer instead of allocating per block.
int length = destination.Length;
int chunkCapacity = Math.Min(length, CycleGenerationBatchSize);
ulong[] divisors = ArrayPool<ulong>.Shared.Rent(chunkCapacity);
ulong[] powScratch = ArrayPool<ulong>.Shared.Rent(chunkCapacity);
ulong[] orderScratch = ArrayPool<ulong>.Shared.Rent(chunkCapacity);
byte[] statusScratch = ArrayPool<byte>.Shared.Rent(chunkCapacity);
try
ulong divisor = divisors[i];
ulong initialPow = 2UL;
if (initialPow >= divisor)
{
using MemoryBuffer1D<ulong, Stride1D.Dense> divisorBuffer = accelerator.Allocate1D<ulong>(chunkCapacity);
using MemoryBuffer1D<ulong, Stride1D.Dense> powBuffer = accelerator.Allocate1D<ulong>(chunkCapacity);
using MemoryBuffer1D<ulong, Stride1D.Dense> orderBuffer = accelerator.Allocate1D<ulong>(chunkCapacity);
using MemoryBuffer1D<ulong, Stride1D.Dense> resultBuffer = accelerator.Allocate1D<ulong>(chunkCapacity);
using MemoryBuffer1D<byte, Stride1D.Dense> statusBuffer = accelerator.Allocate1D<byte>(chunkCapacity);
initialPow -= divisor;
}
int offset = 0;
while (offset < length)
powSpan[i] = initialPow;
orderSpan[i] = 1UL;
resultSpan[i] = 0UL;
statusSpan[i] = ByteZero;
}
powBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(powSpan), length);
orderBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(orderSpan), length);
resultBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(resultSpan), length);
statusBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(statusSpan), length);
int pending;
do
{
kernel(length, _divisorCyclesBatchSize, divisorBuffer.View, powBuffer.View, orderBuffer.View, resultBuffer.View, statusBuffer.View);
accelerator.Synchronize();
statusBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(statusSpan), length);
pending = 0;
for (int i = 0; i < length; i++)
{
if (statusSpan[i] == ByteZero)
{
int batchSize = Math.Min(chunkCapacity, length - offset);
Span<ulong> divisorsSpan = divisors.AsSpan(0, batchSize);
Span<ulong> destinationSpan = destination.AsSpan(offset, batchSize);
Span<ulong> powSpan = powScratch.AsSpan(0, batchSize);
Span<ulong> orderSpan = orderScratch.AsSpan(0, batchSize);
Span<byte> statusSpan = statusScratch.AsSpan(0, batchSize);
int pending = 0;
for (int i = 0; i < batchSize; i++)
{
ulong divisorValue = start + (ulong)(offset + i);
divisorsSpan[i] = divisorValue;
if ((divisorValue & (divisorValue - 1UL)) == 0UL)
{
destinationSpan[i] = 1UL;
powSpan[i] = 0UL;
orderSpan[i] = 1UL;
statusSpan[i] = ByteOne;
}
else
{
ulong initialPow = 2UL;
if (initialPow >= divisorValue)
{
initialPow -= divisorValue;
}
destinationSpan[i] = 0UL;
powSpan[i] = initialPow;
orderSpan[i] = 1UL;
statusSpan[i] = ByteZero;
pending++;
}
}
ArrayView1D<ulong, Stride1D.Dense> divisorView = divisorBuffer.View.SubView(0, batchSize);
ArrayView1D<ulong, Stride1D.Dense> powView = powBuffer.View.SubView(0, batchSize);
ArrayView1D<ulong, Stride1D.Dense> orderView = orderBuffer.View.SubView(0, batchSize);
ArrayView1D<ulong, Stride1D.Dense> resultView = resultBuffer.View.SubView(0, batchSize);
ArrayView1D<byte, Stride1D.Dense> statusView = statusBuffer.View.SubView(0, batchSize);
divisorView.CopyFromCPU(ref MemoryMarshal.GetReference(divisorsSpan), batchSize);
powView.CopyFromCPU(ref MemoryMarshal.GetReference(powSpan), batchSize);
orderView.CopyFromCPU(ref MemoryMarshal.GetReference(orderSpan), batchSize);
resultView.CopyFromCPU(ref MemoryMarshal.GetReference(destinationSpan), batchSize);
statusView.CopyFromCPU(ref MemoryMarshal.GetReference(statusSpan), batchSize);
while (pending > 0)
{
kernel(batchSize, divisorCyclesBatchSize, divisorView, powView, orderView, resultView, statusView);
accelerator.Synchronize();
statusView.CopyToCPU(ref MemoryMarshal.GetReference(statusSpan), batchSize);
pending = 0;
for (int i = 0; i < batchSize; i++)
{
if (statusSpan[i] == ByteZero)
{
pending++;
}
}
}
resultView.CopyToCPU(ref MemoryMarshal.GetReference(destinationSpan), batchSize);
offset += batchSize;
pending++;
}
}
finally
{
ArrayPool<ulong>.Shared.Return(divisors, clearArray: false);
ArrayPool<ulong>.Shared.Return(powScratch, clearArray: false);
ArrayPool<ulong>.Shared.Return(orderScratch, clearArray: false);
ArrayPool<byte>.Shared.Return(statusScratch, clearArray: false);
}
}
finally
while (pending > 0);
resultBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(resultSpan), length);
resultSpan.CopyTo(destination);
if (rentedPow is not null)
{
execution.Dispose();
gpuLease.Dispose();
ArrayPool<ulong>.Shared.Return(rentedPow, clearArray: false);
ArrayPool<ulong>.Shared.Return(rentedOrder!, clearArray: false);
ArrayPool<ulong>.Shared.Return(rentedResult!, clearArray: false);
ArrayPool<byte>.Shared.Return(rentedStatus!, clearArray: false);
}
execution.Dispose();
gpuLease.Dispose();
}
private static Action<Index1D, int, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<byte, Stride1D.Dense>> LoadKernel(Accelerator accelerator)
@@ -551,7 +248,7 @@ public sealed class DivisorCycleCache
private static void GpuAdvanceDivisorCyclesKernel(
Index1D index,
int steps,
int steps,
ArrayView1D<ulong, Stride1D.Dense> divisors,
ArrayView1D<ulong, Stride1D.Dense> pow,
ArrayView1D<ulong, Stride1D.Dense> order,
@@ -602,3 +299,6 @@ public sealed class DivisorCycleCache
}
}
-22
View File
@@ -1,22 +0,0 @@
using System.Runtime.CompilerServices;
namespace PerfectNumbers.Core;
public sealed class EulerPrimeTester
{
private readonly PrimeTester _primeTester;
public EulerPrimeTester(PrimeTester primeTester)
{
_primeTester = primeTester;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsEulerPrime(ulong n)
{
// TODO: Inline this helper into callers so we skip the wrapper and pass the divisibility check directly to the optimized
// prime tester path highlighted in the CPU benchmarks.
return (n & 3UL) == 1UL && _primeTester.IsPrime(n, CancellationToken.None);
}
}
@@ -18,7 +18,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
private readonly object _sync = new();
private ulong _divisorLimit;
private bool _isConfigured;
private bool _useDivisorCycles;
private readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<MontgomeryDivisorData>, ArrayView<ulong>, ArrayView<byte>>> _kernelCache = new();
private readonly ConcurrentDictionary<Accelerator, Action<Index1D, MontgomeryDivisorData, ArrayView<ulong>, ArrayView<ulong>>> _kernelExponentCache = new();
@@ -43,12 +42,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
set => GpuBatchSize = value;
}
public bool UseDivisorCycles
{
get => _useDivisorCycles;
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)
{
lock (_sync)
@@ -61,7 +54,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
public bool IsPrime(ulong prime, out bool divisorsExhausted)
{
ulong allowedMax;
bool useCycles;
int batchCapacity;
lock (_sync)
@@ -72,7 +64,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
}
allowedMax = ComputeAllowedMaxDivisor(prime, _divisorLimit);
useCycles = _useDivisorCycles;
batchCapacity = _gpuBatchSize;
}
@@ -97,7 +88,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
composite = CheckDivisors(
prime,
allowedMax,
useCycles,
accelerator,
kernel,
resources.DivisorsBuffer,
@@ -156,7 +146,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
private static bool CheckDivisors(
ulong prime,
ulong allowedMax,
bool useCycles,
Accelerator accelerator,
Action<Index1D, ArrayView<MontgomeryDivisorData>, ArrayView<ulong>, ArrayView<byte>> kernel,
MemoryBuffer1D<MontgomeryDivisorData, Stride1D.Dense> divisorsBuffer,
@@ -170,68 +159,12 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
out bool coveredRange,
out ulong processedCount)
{
lastProcessed = 0UL;
int batchCapacity = (int)divisorsBuffer.Length;
bool composite = false;
bool processedAll = false;
ulong currentDivisor = 3UL;
processedCount = 0UL;
if (allowedMax < 3UL)
{
coveredRange = true;
return false;
}
return useCycles
? CheckDivisorsWithCycles(
prime,
allowedMax,
accelerator,
kernel,
divisorsBuffer,
exponentBuffer,
hitsBuffer,
divisors,
exponents,
hits,
divisorData,
ref lastProcessed,
ref processedCount,
out coveredRange)
: CheckDivisorsWithoutCycles(
prime,
allowedMax,
accelerator,
kernel,
divisorsBuffer,
exponentBuffer,
hitsBuffer,
divisors,
exponents,
hits,
divisorData,
ref lastProcessed,
ref processedCount,
out coveredRange);
}
private static bool CheckDivisorsWithoutCycles(
ulong prime,
ulong allowedMax,
Accelerator accelerator,
Action<Index1D, ArrayView<MontgomeryDivisorData>, ArrayView<ulong>, ArrayView<byte>> kernel,
MemoryBuffer1D<MontgomeryDivisorData, Stride1D.Dense> divisorsBuffer,
MemoryBuffer1D<ulong, Stride1D.Dense> exponentBuffer,
MemoryBuffer1D<byte, Stride1D.Dense> hitsBuffer,
in ulong[] divisors,
ulong[] exponents,
byte[] hits,
MontgomeryDivisorData[] divisorData,
ref ulong lastProcessed,
ref ulong processedCount,
out bool coveredRange)
{
int batchCapacity = (int)divisorsBuffer.Length;
bool composite = false;
bool processedAll = false;
ulong currentDivisor = 3UL;
lastProcessed = 0UL;
Span<MontgomeryDivisorData> divisorDataSpan;
Span<ulong> divisorSpan;
@@ -241,138 +174,15 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
ArrayView1D<ulong, Stride1D.Dense> exponentView;
ArrayView1D<byte, Stride1D.Dense> hitsView;
while (currentDivisor <= allowedMax)
DivisorCycleCache cycleCache = DivisorCycleCache.Shared;
int cycleBatchCapacity = Math.Min(batchCapacity, cycleCache.PreferredBatchSize);
if (cycleBatchCapacity <= 0)
{
int batchSize = 0;
bool reachedEndInBatch = false;
// The GPU buffers are sized exactly to the configured batch capacity, so we
// must stop once the current batch is full to avoid writing past the spans.
while (batchSize < batchCapacity && currentDivisor <= allowedMax)
{
ulong nextDivisor = currentDivisor + 2UL;
processedCount++;
divisors[batchSize] = currentDivisor;
exponents[batchSize] = prime;
batchSize++;
lastProcessed = currentDivisor;
if (nextDivisor <= currentDivisor)
{
reachedEndInBatch = true;
break;
}
if (nextDivisor > allowedMax)
{
currentDivisor = nextDivisor;
reachedEndInBatch = true;
break;
}
currentDivisor = nextDivisor;
}
if (batchSize == 0)
{
if (reachedEndInBatch)
{
processedAll = true;
break;
}
continue;
}
divisorDataSpan = divisorData.AsSpan(0, batchSize);
divisorSpan = divisors.AsSpan(0, batchSize);
exponentSpan = exponents.AsSpan(0, batchSize);
hitsSpan = hits.AsSpan(0, batchSize);
divisorView = divisorsBuffer.View.SubView(0, batchSize);
exponentView = exponentBuffer.View.SubView(0, batchSize);
hitsView = hitsBuffer.View.SubView(0, batchSize);
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]);
}
divisorView.CopyFromCPU(ref MemoryMarshal.GetReference(divisorDataSpan), batchSize);
exponentView.CopyFromCPU(ref MemoryMarshal.GetReference(exponentSpan), batchSize);
kernel(batchSize, divisorView, exponentView, hitsView);
hitsView.CopyToCPU(ref MemoryMarshal.GetReference(hitsSpan), batchSize);
for (int i = 0; i < batchSize; i++)
{
if (hitsSpan[i] != 0)
{
composite = true;
lastProcessed = divisorSpan[i];
break;
}
}
if (!composite)
{
lastProcessed = divisorSpan[batchSize - 1];
}
if (composite)
{
break;
}
if (reachedEndInBatch)
{
processedAll = true;
break;
}
cycleBatchCapacity = batchCapacity;
}
coveredRange = composite || processedAll || currentDivisor > allowedMax;
return composite;
}
private static bool CheckDivisorsWithCycles(
ulong prime,
ulong allowedMax,
Accelerator accelerator,
Action<Index1D, ArrayView<MontgomeryDivisorData>, ArrayView<ulong>, ArrayView<byte>> kernel,
MemoryBuffer1D<MontgomeryDivisorData, Stride1D.Dense> divisorsBuffer,
MemoryBuffer1D<ulong, Stride1D.Dense> exponentBuffer,
MemoryBuffer1D<byte, Stride1D.Dense> hitsBuffer,
in ulong[] divisors,
ulong[] exponents,
byte[] hits,
MontgomeryDivisorData[] divisorData,
ref ulong lastProcessed,
ref ulong processedCount,
out bool coveredRange)
{
int batchCapacity = (int)divisorsBuffer.Length;
bool composite = false;
bool processedAll = false;
ulong currentDivisor = 3UL;
ulong cycleLeaseEnd;
Span<MontgomeryDivisorData> divisorDataSpan;
Span<ulong> divisorSpan;
Span<ulong> exponentSpan;
Span<byte> hitsSpan;
ArrayView1D<MontgomeryDivisorData, Stride1D.Dense> divisorView;
ArrayView1D<ulong, Stride1D.Dense> exponentView;
ArrayView1D<byte, Stride1D.Dense> hitsView;
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;
ulong[] cycleDivisors = ArrayPool<ulong>.Shared.Rent(cycleBatchCapacity);
ulong[] cycleLengths = ArrayPool<ulong>.Shared.Rent(cycleBatchCapacity);
try
{
@@ -381,75 +191,77 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
int batchSize = 0;
bool reachedEndInBatch = false;
// Batch writes share the same buffers as the kernel launch, so we guard
// against exceeding the rented capacity on each iteration.
while (batchSize < batchCapacity && currentDivisor <= allowedMax)
{
ulong nextDivisor = currentDivisor + 2UL;
processedCount++;
if (currentDivisor > cycleLeaseEnd)
int chunkCount = Math.Min(cycleBatchCapacity, batchCapacity - batchSize);
ulong remainingWidth = allowedMax - currentDivisor;
ulong maxOddCount = (remainingWidth >> 1) + 1UL;
if (maxOddCount < (ulong)chunkCount)
{
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;
chunkCount = (int)maxOddCount;
}
bool includeDivisor = true;
ulong exponent = prime;
ulong cycleLength = cycleLease.GetCycle(currentDivisor);
if (cycleLength > 0UL)
{
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;
}
if (remainder != 0UL)
{
includeDivisor = false;
}
else
{
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)
{
divisors[batchSize] = currentDivisor;
exponents[batchSize] = exponent;
batchSize++;
}
lastProcessed = currentDivisor;
if (nextDivisor <= currentDivisor)
if (chunkCount <= 0)
{
reachedEndInBatch = true;
break;
}
if (nextDivisor > allowedMax)
Span<ulong> chunkDivisors = cycleDivisors.AsSpan(0, chunkCount);
Span<ulong> chunkCycles = cycleLengths.AsSpan(0, chunkCount);
ulong fillDivisor = currentDivisor;
for (int i = 0; i < chunkCount; i++)
{
currentDivisor = nextDivisor;
reachedEndInBatch = true;
break;
chunkDivisors[i] = fillDivisor;
fillDivisor += 2UL;
}
currentDivisor = nextDivisor;
cycleCache.GetCycleLengths(chunkDivisors, chunkCycles);
for (int i = 0; i < chunkCount && batchSize < batchCapacity; i++)
{
ulong divisorValue = chunkDivisors[i];
ulong cycleLength = chunkCycles[i];
processedCount++;
bool includeDivisor = true;
ulong exponent = prime;
if (cycleLength > 0UL)
{
ulong remainder = prime;
if (remainder >= cycleLength)
{
remainder %= cycleLength;
}
if (remainder != 0UL)
{
includeDivisor = false;
}
else
{
exponent = 0UL;
}
}
if (includeDivisor)
{
divisors[batchSize] = divisorValue;
exponents[batchSize] = exponent;
batchSize++;
}
lastProcessed = divisorValue;
currentDivisor = divisorValue + 2UL;
if (currentDivisor > allowedMax)
{
reachedEndInBatch = true;
break;
}
}
}
if (batchSize == 0)
@@ -473,8 +285,6 @@ 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]);
}
@@ -512,7 +322,8 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
}
finally
{
cycleLease.Dispose();
ArrayPool<ulong>.Shared.Return(cycleDivisors, clearArray: false);
ArrayPool<ulong>.Shared.Return(cycleLengths, clearArray: false);
}
coveredRange = composite || processedAll || currentDivisor > allowedMax;
@@ -601,17 +412,12 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
throw new ObjectDisposedException(nameof(DivisorScanSession));
}
MersenneNumberDivisorByDivisorGpuTester owner = _owner;
int primesLength = primes.Length;
if (primesLength == 0)
{
return;
}
// The production pipeline never adjusts the GPU batch size after a session
// is initialized, so the buffers allocated in the constructor remain
// large enough for every launch.
MontgomeryDivisorData divisorData = MontgomeryDivisorDataCache.Get(divisor);
var exponentStepper = new ExponentRemainderStepper(divisorData);
if (!exponentStepper.IsValidModulus)
@@ -620,19 +426,14 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
return;
}
bool useDivisorCycles = owner._useDivisorCycles;
if (!useDivisorCycles)
{
ProcessWithoutCycle(ref exponentStepper, divisorData, primes, hits);
return;
}
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}.");
divisorCycle = DivisorCycleCache.Shared.GetCycleLength(divisor);
if (divisorCycle == 0UL)
{
hits.Clear();
return;
}
}
var cycleStepper = new CycleRemainderStepper(divisorCycle);
@@ -640,78 +441,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
ProcessWithCycle(ref exponentStepper, ref cycleStepper, initialCycleRemainder, divisorCycle, divisorData, primes, hits);
}
private void ProcessWithoutCycle(
ref ExponentRemainderStepper exponentStepper,
in MontgomeryDivisorData divisorData,
ReadOnlySpan<ulong> primes,
Span<byte> hits)
{
Action<Index1D, MontgomeryDivisorData, ArrayView<ulong>, ArrayView<ulong>> kernel = _kernel;
ArrayView1D<ulong, Stride1D.Dense> exponentsView = _exponentsBuffer.View;
ArrayView1D<ulong, Stride1D.Dense> resultsView = _resultsBuffer.View;
Span<ulong> hostSpan = _hostBuffer.AsSpan(0, _capacity);
int primesLength = primes.Length;
int gpuBatchSize = _owner._gpuBatchSize;
int offset = 0;
while (offset < primesLength)
{
int batchSize = Math.Min(gpuBatchSize, primesLength - offset);
ReadOnlySpan<ulong> primesSlice = primes.Slice(offset, batchSize);
Span<byte> hitsSlice = hits.Slice(offset, batchSize);
Span<ulong> exponentSlice = hostSpan[..batchSize];
bool hasState = exponentStepper.HasState;
ulong previousExponent = exponentStepper.PreviousExponent;
int startIndex = 0;
if (!hasState)
{
ulong primeValue = primesSlice[0];
exponentSlice[0] = primeValue;
previousExponent = primeValue;
hasState = true;
startIndex = 1;
}
for (int i = startIndex; i < batchSize; i++)
{
ulong primeValue = primesSlice[i];
exponentSlice[i] = primeValue - previousExponent;
previousExponent = primeValue;
}
ArrayView1D<ulong, Stride1D.Dense> exponentView = exponentsView.SubView(0, batchSize);
exponentView.CopyFromCPU(ref MemoryMarshal.GetReference(exponentSlice), batchSize);
ArrayView1D<ulong, Stride1D.Dense> resultView = resultsView.SubView(0, batchSize);
kernel(batchSize, divisorData, exponentView, resultView);
resultView.CopyToCPU(ref MemoryMarshal.GetReference(exponentSlice), batchSize);
bool requiresInitialization = !exponentStepper.HasState;
for (int i = 0; i < batchSize; i++)
{
ulong primeValue = primesSlice[i];
ulong montgomeryResult = exponentSlice[i];
bool isUnity;
if (requiresInitialization)
{
exponentStepper.TryInitializeFromMontgomeryResult(primeValue, montgomeryResult, out isUnity);
requiresInitialization = false;
}
else
{
exponentStepper.TryAdvanceWithMontgomeryDelta(primeValue, montgomeryResult, out isUnity);
}
hitsSlice[i] = isUnity ? (byte)1 : (byte)0;
}
offset += batchSize;
}
}
private void ProcessWithCycle(
ref ExponentRemainderStepper exponentStepper,
@@ -973,3 +702,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
IMersenneNumberDivisorByDivisorTester.IDivisorScanSession IMersenneNumberDivisorByDivisorTester.CreateDivisorSession() => CreateDivisorSession();
}
@@ -4,8 +4,6 @@ namespace PerfectNumbers.Core;
public interface IMersenneNumberDivisorByDivisorTester
{
bool UseDivisorCycles { get; set; } // TODO: Remove the setter once divisor cycle usage becomes mandatory so all implementations always leverage the cached cycles.
int BatchSize { get; set; }
void ConfigureFromMaxPrime(ulong maxPrime);
@@ -25,4 +23,3 @@ public interface IMersenneNumberDivisorByDivisorTester
void CheckDivisor(ulong divisor, ulong divisorCycle, ReadOnlySpan<ulong> primes, Span<byte> hits);
}
}
@@ -237,7 +237,6 @@ public static class MersenneNumberDivisorByDivisorTester
Span<byte> hitsSpan = default;
int hitIndex = 0;
int index;
bool useDivisorCycles = tester.UseDivisorCycles; // TODO: Remove the conditional path once all testers always enable divisor cycles to keep every divisor scan on the faster cached-length track.
ulong divisorCycle = 0UL;
try
@@ -255,23 +254,7 @@ public static class MersenneNumberDivisorByDivisorTester
divisor = AcquireNextDivisor(ref nextDivisor, divisorLimit, ref divisorsExhaustedFlag, ref finalDivisorBits, out exhausted, ref localDivisorCursor, ref localDivisorsRemaining);
if (divisor != 0UL)
{
DivisorCycleCache.CycleBlock? cycleBlock = null;
if (useDivisorCycles)
{
cycleBlock = DivisorCycleCache.Shared.Acquire(divisor);
// TODO: Remove this block leasing once the cache exposes a direct
// single-cycle retrieval API so coordinator threads never mutate the
// cache state beyond the snapshot loaded at startup.
divisorCycle = cycleBlock.GetCycle(divisor);
// TODO: When divisorCycle returns zero, compute only that single cycle on the
// device selected by the current configuration, keep the result transient, and
// continue operating with the single cached block instead of scheduling additional
// batches.
}
else
{
divisorCycle = 0UL;
}
divisorCycle = DivisorCycleCache.Shared.GetCycleLength(divisor);
activeCount = BuildPrimeBuffer(
divisor,
@@ -296,7 +279,6 @@ public static class MersenneNumberDivisorByDivisorTester
if (activeCount == 0)
{
cycleBlock?.Dispose();
continue;
}
@@ -330,11 +312,8 @@ public static class MersenneNumberDivisorByDivisorTester
FlushPendingResults(compositesBuffer, ref compositesCount, markComposite, clearComposite, printResult);
}
cycleBlock?.Dispose();
continue;
}
if (!exhausted)
{
if (Volatile.Read(ref remainingStates) == 0)
@@ -738,3 +717,6 @@ public static class MersenneNumberDivisorByDivisorTester
internal bool PassedAllTests { get; }
}
}
+3 -113
View File
@@ -17,13 +17,7 @@ public readonly struct MontgomeryDivisorData(ulong modulus, ulong nPrime, ulong
internal static class MontgomeryDivisorDataCache
{
private const ulong BaseBlockEnd = PerfectNumberConstants.MaxQForDivisorCycles;
private static readonly ConcurrentDictionary<ulong, CacheEntry> Cache = new(); // TODO: Replace this with a plain Dictionary that never mutates after startup once the divisor-cycle snapshot becomes read-only so Montgomery lookups avoid any concurrent collections.
// TODO: Delete the block-membership tracking entirely when the divisor-cycle cache stops producing
// dynamic blocks. The single-cycle plan keeps the snapshot immutable, so we should not retain block
// indices or concurrent maps at all.
private static readonly ConcurrentDictionary<int, ConcurrentDictionary<ulong, byte>> BlockMembership = new();
private static readonly ConcurrentDictionary<ulong, MontgomeryDivisorData> Cache = new();
public static MontgomeryDivisorData Get(ulong modulus)
{
@@ -32,97 +26,7 @@ internal static class MontgomeryDivisorDataCache
return new MontgomeryDivisorData(modulus, 0UL, 0UL, 0UL);
}
int blockIndex = ComputeBlockIndex(modulus);
// TODO: Short-circuit block indices greater than zero once the cache stops prefetching future
// blocks; when only the startup snapshot remains we should bypass block arithmetic entirely and
// compute single cycles inline on cache misses without updating shared state.
return GetManaged(modulus, blockIndex);
}
internal static void ReleaseBlock(DivisorCycleCache.CycleBlock block)
{
// TODO: Remove this entire release hook once the cycle cache no longer hands out dynamic blocks;
// the single-cycle pipeline will never transfer ownership back to Montgomery metadata.
if (block.Index <= 0)
{
return;
}
if (BlockMembership.TryRemove(block.Index, out ConcurrentDictionary<ulong, byte>? members))
{
foreach (ulong modulus in members.Keys)
{
Cache.TryRemove(modulus, out _);
}
}
}
private static MontgomeryDivisorData GetManaged(ulong modulus, int blockIndex)
{
while (true)
{
if (Cache.TryGetValue(modulus, out CacheEntry entry))
{
if (entry.BlockIndex != blockIndex)
{
if (blockIndex > 0 && entry.BlockIndex <= 0)
{
CacheEntry updated = new(entry.Data, blockIndex);
if (Cache.TryUpdate(modulus, updated, entry))
{
RegisterBlockMembership(modulus, blockIndex);
return entry.Data;
}
continue;
}
if (blockIndex > 0)
{
RegisterBlockMembership(modulus, blockIndex);
}
return entry.Data;
}
return entry.Data;
}
MontgomeryDivisorData created = Create(modulus);
if (Cache.TryAdd(modulus, new CacheEntry(created, blockIndex)))
{
RegisterBlockMembership(modulus, blockIndex);
return created;
}
}
// TODO: Collapse this retry loop once the cache becomes a simple snapshot dictionary. With the
// single-cycle policy we can compute the requested cycle, populate the dictionary exactly once,
// and return without spinning through concurrent retries.
}
private static void RegisterBlockMembership(ulong modulus, int blockIndex)
{
// TODO: Delete this method once block membership goes away; the single snapshot model must avoid
// tracking additional indices or allocating concurrent dictionaries for Montgomery lookups.
if (blockIndex <= 0)
{
return;
}
ConcurrentDictionary<ulong, byte> set = BlockMembership.GetOrAdd(blockIndex, static _ => new ConcurrentDictionary<ulong, byte>());
set.TryAdd(modulus, 0);
}
private static int ComputeBlockIndex(ulong modulus)
{
if (modulus <= BaseBlockEnd)
{
return 0;
}
// TODO: Return 0 here once the divisor-cycle cache no longer tracks additional blocks so
// MontgomeryDivisorData lookups never attempt to follow prefetched ranges or trigger new cycle
// computations for entire blocks.
return (int)((modulus - (BaseBlockEnd + 1UL)) / (ulong)PerfectNumberConstants.MaxQForDivisorCycles) + 1;
return Cache.GetOrAdd(modulus, static m => Create(m));
}
private static MontgomeryDivisorData Create(ulong modulus)
@@ -135,8 +39,6 @@ internal static class MontgomeryDivisorDataCache
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
// TODO: Replace this generic `%` reduction with the UInt128 Montgomery folding helper measured faster in
// MontgomeryMultiplyBenchmarks so CPU paths avoid the slow BigInteger-style remainder in hot divisor cache lookups.
private static ulong ComputeMontgomeryResidue(ulong value, ulong modulus) => (ulong)((UInt128)value * (UInt128.One << 64) % modulus);
private static ulong ComputeMontgomeryNPrime(ulong modulus)
@@ -148,19 +50,7 @@ internal static class MontgomeryDivisorDataCache
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
return unchecked(0UL - inv);
}
private readonly struct CacheEntry
{
internal CacheEntry(MontgomeryDivisorData data, int blockIndex)
{
Data = data;
BlockIndex = blockIndex;
}
internal MontgomeryDivisorData Data { get; }
internal int BlockIndex { get; }
}
}