using System.Runtime.CompilerServices; using ILGPU; using ILGPU.Runtime; using PerfectNumbers.Core.Gpu; using PerfectNumbers.Core.Gpu.Accelerators; namespace PerfectNumbers.Core; public sealed class DivisorCycleCache { private const byte ByteZero = 0; private const byte ByteOne = 1; private const int StackBufferThreshold = 128; private readonly Accelerator _accelerator; private readonly int _acceleratorIndex; private readonly Action, ArrayView1D, ArrayView1D, ArrayView1D, ArrayView1D> _gpuKernel; private volatile ulong[] _snapshot; private volatile bool _useGpuGeneration = true; private readonly int _divisorCyclesBatchSize; private static int _sharedDivisorCyclesBatchSize = GpuConstants.GpuCycleStepsPerInvocation; private const int CycleCacheTrackingLimit = 500_000; private static readonly HashSet CycleCacheTrackedDivisors = new(); private static readonly object CycleCacheTrackingLock = new(); private static ulong CycleCacheHitCount; private static bool CycleCacheTrackingDisabled; public static void SetDivisorCyclesBatchSize(int divisorCyclesBatchSize) { _sharedDivisorCyclesBatchSize = Math.Max(1, divisorCyclesBatchSize); } public static DivisorCycleCache Shared { get; } = new DivisorCycleCache(_sharedDivisorCyclesBatchSize); public int PreferredBatchSize => _divisorCyclesBatchSize; // private static readonly Accelerator[] _accelerators; private DivisorCycleCache(int divisorCyclesBatchSize) { _divisorCyclesBatchSize = Math.Max(1, divisorCyclesBatchSize); var acceleratorIndex = AcceleratorPool.Shared.Rent(); _acceleratorIndex = acceleratorIndex; Accelerator accelerator = AcceleratorPool.Shared.Accelerators[acceleratorIndex]; _accelerator = accelerator; _gpuKernel = LoadKernel(accelerator); _snapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot(); } public void ConfigureGeneratorDevice(bool useGpu) { _useGpuGeneration = useGpu; } public void RefreshSnapshot() { _snapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot(); } public ulong GetCycleLength(PrimeOrderCalculatorAccelerator gpu, ulong divisor, bool skipPrimeOrderHeuristic = false) { if (!skipPrimeOrderHeuristic) { Span result = stackalloc ulong[1]; ReadOnlySpan singleDivisor = stackalloc ulong[1] { divisor }; GetCycleLengths(gpu, singleDivisor, result); return result[0]; } // The skipPrimeOrderHeuristic path is only used for divisors produced by the CPU by-divisor scanner, // which always generates values greater than one. Keep this guard disabled here to avoid repeating the // validation already performed by the general GetCycleLengths path. // if (divisor <= 1UL) // { // throw new InvalidDataException("Divisor must be > 1"); // } ulong[] snapshot = _snapshot; ulong cycleLength; if (divisor < (ulong)snapshot.Length) { cycleLength = snapshot[divisor]; if (cycleLength == 0UL) { throw new InvalidDataException($"Divisor cycle is missing for {divisor}"); } return cycleLength; } Queue divisorPool = MontgomeryDivisorDataPool.Shared; MontgomeryDivisorData divisorData = divisorPool.FromModulus(divisor); cycleLength = MersenneDivisorCycles.CalculateCycleLength(gpu, divisor, divisorData, skipPrimeOrderHeuristic: true); divisorPool.Return(divisorData); return cycleLength; } public void GetCycleLengths(PrimeOrderCalculatorAccelerator gpu,ReadOnlySpan divisors, Span cycles) { ulong[] snapshot = _snapshot; int length = divisors.Length; int[]? rentedMissing = null; Span missingBuffer = length <= StackBufferThreshold ? stackalloc int[length] : new Span(rentedMissing = ThreadStaticPools.IntPool.Rent(length), 0, length); int missingCount = 0; for (int i = 0; i < length; i++) { ulong divisor = divisors[i]; if (divisor <= 1UL) { throw new InvalidDataException("Divisor must be > 1"); } if (divisor < (ulong)snapshot.Length) { ulong cached = snapshot[divisor]; if (cached == 0UL) { throw new InvalidDataException($"Divisor cycle is missing for {divisor}"); } cycles[i] = cached; } else { missingBuffer[missingCount++] = i; } } if (missingCount != 0) { ReadOnlySpan missingIndices = missingBuffer[..missingCount]; if (_useGpuGeneration) { ComputeCyclesGpu(divisors, cycles, missingIndices); } else { ComputeCyclesCpu(gpu, divisors, cycles, missingIndices); } } if (rentedMissing is not null) { ThreadStaticPools.IntPool.Return(rentedMissing, clearArray: false); } } private static void ObserveCycleCacheDivisor(ulong divisor) { bool logHit = false; ulong hitNumber = 0UL; bool trackingDisabledNow = false; lock (CycleCacheTrackingLock) { if (CycleCacheTrackingDisabled) { return; } if (!CycleCacheTrackedDivisors.Add(divisor)) { CycleCacheHitCount++; hitNumber = CycleCacheHitCount; logHit = true; } else if (CycleCacheTrackedDivisors.Count >= CycleCacheTrackingLimit) { CycleCacheTrackingDisabled = true; CycleCacheTrackedDivisors.Clear(); trackingDisabledNow = true; } } if (logHit) { Console.WriteLine($"Cycle cache hit for divisor {divisor} ({hitNumber})"); } else if (trackingDisabledNow) { Console.WriteLine("Cycle cache hit tracking disabled after exceeding the tracking limit."); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ComputeCyclesCpu(PrimeOrderCalculatorAccelerator gpu, ReadOnlySpan divisors, Span cycles, ReadOnlySpan indices) { Queue divisorPool = MontgomeryDivisorDataPool.Shared; for (int i = 0; i < indices.Length; i++) { int targetIndex = indices[i]; ulong divisor = divisors[targetIndex]; MontgomeryDivisorData divisorData = divisorPool.FromModulus(divisor); cycles[targetIndex] = MersenneDivisorCycles.CalculateCycleLength(gpu, divisor, divisorData); divisorPool.Return(divisorData); } } private void ComputeCyclesGpu(ReadOnlySpan divisors, Span cycles, ReadOnlySpan indices) { int count = indices.Length; ulong[]? rentedDivisors = null; Span workDivisors = count <= StackBufferThreshold ? stackalloc ulong[count] : new Span(rentedDivisors = ThreadStaticPools.UlongPool.Rent(count), 0, count); ulong[]? rentedResults = null; Span workResults = count <= StackBufferThreshold ? stackalloc ulong[count] : new Span(rentedResults = ThreadStaticPools.UlongPool.Rent(count), 0, count); for (int i = 0; i < count; i++) { workDivisors[i] = divisors[indices[i]]; } ComputeCyclesGpuCore(workDivisors, workResults); for (int i = 0; i < count; i++) { cycles[indices[i]] = workResults[i]; } if (rentedDivisors is not null) { ThreadStaticPools.UlongPool.Return(rentedDivisors, clearArray: false); ThreadStaticPools.UlongPool.Return(rentedResults!, clearArray: false); } } private const int DefaultCapacity = 128; [ThreadStatic] private static MemoryBuffer1D? divisorBuffer; [ThreadStatic] private static MemoryBuffer1D? powBuffer; [ThreadStatic] private static MemoryBuffer1D? orderBuffer; [ThreadStatic] private static MemoryBuffer1D? resultBuffer; [ThreadStatic] private static MemoryBuffer1D? statusBuffer; private void EnsureCapacity(int requiredCapacity) { if (requiredCapacity <= (divisorBuffer?.Length ?? 0)) { return; } if (divisorBuffer != null) { divisorBuffer.Dispose(); powBuffer!.Dispose(); orderBuffer!.Dispose(); resultBuffer!.Dispose(); statusBuffer!.Dispose(); } Accelerator accelerator = _accelerator; // lock (accelerator) { divisorBuffer = accelerator.Allocate1D(requiredCapacity); powBuffer = accelerator.Allocate1D(requiredCapacity); orderBuffer = accelerator.Allocate1D(requiredCapacity); resultBuffer = accelerator.Allocate1D(requiredCapacity); statusBuffer = accelerator.Allocate1D(requiredCapacity); } } private void ComputeCyclesGpuCore(ReadOnlySpan divisors, Span destination) { int length = divisors.Length; EnsureCapacity(length); ulong[]? rentedPow = null; Span powSpan = length <= StackBufferThreshold ? stackalloc ulong[length] : new Span(rentedPow = ThreadStaticPools.UlongPool.Rent(length), 0, length); ulong[]? rentedOrder = null; Span orderSpan = length <= StackBufferThreshold ? stackalloc ulong[length] : new Span(rentedOrder = ThreadStaticPools.UlongPool.Rent(length), 0, length); ulong[]? rentedResult = null; Span resultSpan = length <= StackBufferThreshold ? stackalloc ulong[length] : new Span(rentedResult = ThreadStaticPools.UlongPool.Rent(length), 0, length); byte[]? rentedStatus = null; Span statusSpan = length <= StackBufferThreshold ? stackalloc byte[length] : new Span(rentedStatus = ThreadStaticPools.BytePool.Rent(length), 0, length); for (int i = 0; i < length; i++) { ulong divisor = divisors[i]; ulong initialPow = 2UL; if (initialPow >= divisor) { initialPow -= divisor; } powSpan[i] = initialPow; orderSpan[i] = 1UL; resultSpan[i] = 0UL; statusSpan[i] = ByteZero; } // GpuPrimeWorkLimiter.Acquire(); int pending; var kernel = _gpuKernel; var acceleratorIndex = _acceleratorIndex; var stream = AcceleratorStreamPool.Rent(acceleratorIndex); divisorBuffer!.View.CopyFromCPU(stream, divisors); powBuffer!.View.CopyFromCPU(stream, powSpan); orderBuffer!.View.CopyFromCPU(stream, orderSpan); resultBuffer!.View.CopyFromCPU(stream, resultSpan); statusBuffer!.View.CopyFromCPU(stream, statusSpan); do { kernel(stream, length, _divisorCyclesBatchSize, divisorBuffer.View, powBuffer.View, orderBuffer.View, resultBuffer.View, statusBuffer.View); statusBuffer.View.CopyToCPU(stream, statusSpan); stream.Synchronize(); pending = 0; for (int i = 0; i < length; i++) { if (statusSpan[i] == ByteZero) { pending++; } } } while (pending > 0); resultBuffer.View.CopyToCPU(stream, resultSpan); stream.Synchronize(); AcceleratorStreamPool.Return(acceleratorIndex, stream); resultSpan.CopyTo(destination); if (rentedPow is not null) { ThreadStaticPools.UlongPool.Return(rentedPow, clearArray: false); ThreadStaticPools.UlongPool.Return(rentedOrder!, clearArray: false); ThreadStaticPools.UlongPool.Return(rentedResult!, clearArray: false); ThreadStaticPools.BytePool.Return(rentedStatus!, clearArray: false); } // GpuPrimeWorkLimiter.Release(); } private static Action, ArrayView1D, ArrayView1D, ArrayView1D, ArrayView1D> LoadKernel(Accelerator accelerator) { var loaded = accelerator.LoadAutoGroupedStreamKernel, ArrayView1D, ArrayView1D, ArrayView1D, ArrayView1D>(DivisorCycleKernels.GpuAdvanceDivisorCyclesKernel); var kernel = KernelUtil.GetKernel(loaded); return kernel.CreateLauncherDelegate, ArrayView1D, ArrayView1D, ArrayView1D, ArrayView1D>>(); } private sealed class AcceleratorReferenceComparer : IEqualityComparer { internal static AcceleratorReferenceComparer Instance { get; } = new(); public bool Equals(Accelerator? x, Accelerator? y) => ReferenceEquals(x, y); public int GetHashCode(Accelerator obj) => RuntimeHelpers.GetHashCode(obj); } }