Compare commits

...
Author SHA1 Message Date
Marek Lemańczyk 7ea7cf527e Merge pull request #168 from mlemanczyk/codex/remove-redundant-wrapper-methods-in-mersenne-path
Eliminate IDisposable dependencies
2025-10-13 22:16:12 +02:00
Marek Lemańczyk c6008a65b4 Cache GPU remainder configs and fold constant modulos 2025-10-13 22:15:58 +02:00
Marek Lemańczyk 0b43880959 Remove IDisposable usage and using declarations 2025-10-13 21:55:16 +02:00
Marek Lemańczyk 305a489d6c Merge pull request #167 from mlemanczyk/codex/analyze-if-conditions-in-evenperfectbitscanner
Document unreachable guards in GPU by-divisor pipeline
2025-10-13 20:42:48 +02:00
Marek Lemańczyk f2eb6580c4 Comment unreachable configuration guards in by-divisor testers 2025-10-13 20:41:14 +02:00
Marek Lemańczyk b0ba6401ca Remove configuration locks from by-divisor testers 2025-10-13 20:41:07 +02:00
Marek Lemańczyk 6e89700f80 Clarify invariants across GPU divisor helpers 2025-10-13 20:41:00 +02:00
Marek Lemańczyk bccb2f43c9 Document unreachable guards in GPU by-divisor pipeline 2025-10-13 19:56:47 +02:00
Marek Lemańczyk 65e010ded5 Merge pull request #166 from mlemanczyk/codex/locate-montgomery-arithmetic-usage-in-gpu-kernels-ir9oyy
Align prime-order GPU pow2 kernel with plain modular helper
2025-10-13 18:21:54 +02:00
Marek Lemańczyk a255dc718d Restore snapshot missing-cycle guard 2025-10-13 18:21:22 +02:00
Marek Lemańczyk c81579865c Compute missing small divisor cycles on demand 2025-10-13 18:18:57 +02:00
Marek Lemańczyk c8d6143ab7 Simplify by-divisor buffer guards and document prime ordering 2025-10-13 18:12:06 +02:00
Marek Lemańczyk 594a38b9d7 Move by-divisor tests to large-cycle divisors 2025-10-13 18:05:58 +02:00
Marek Lemańczyk c0b6b29837 Document divisor invariants and refresh pow2 tests 2025-10-13 17:42:56 +02:00
Marek Lemańczyk af909e9e2d Simplify prime guards and adopt unified mulmod 2025-10-13 16:56:50 +02:00
Marek Lemańczyk 3f5904c340 Clarify windowed pow2 invariants and tidy Montgomery pooling 2025-10-13 14:30:06 +02:00
Marek Lemańczyk 79695b0c56 Switch GPU pow2mod helpers to plain windowed arithmetic 2025-10-13 14:21:35 +02:00
Marek Lemańczyk 1916196e89 Introduce windowed plain pow2mod helper for CPU checks 2025-10-13 14:21:28 +02:00
Marek Lemańczyk 73e27326a2 Use plain modular math for single-shot CPU checks 2025-10-13 14:21:22 +02:00
Marek Lemańczyk 092947e216 Remove GPU Montgomery usage tracker note 2025-10-13 12:19:01 +02:00
Marek Lemańczyk 716ffcb243 Route prime-order pow2 kernel through plain modular helper 2025-10-13 05:08:34 +02:00
Marek Lemańczyk e10394afa3 Merge pull request #165 from mlemanczyk/revert-164-codex/locate-montgomery-arithmetic-usage-in-gpu-kernels
Revert "Use standard modular math for single-use GPU exponent checks"
2025-10-13 05:00:42 +02:00
Marek Lemańczyk 8b02993bac Revert "Use standard modular math for single-use GPU exponent checks" 2025-10-13 04:59:42 +02:00
Marek Lemańczyk ad6a5c6e61 Merge pull request #164 from mlemanczyk/codex/locate-montgomery-arithmetic-usage-in-gpu-kernels
Use standard modular math for single-use GPU exponent checks
2025-10-13 04:59:36 +02:00
Marek Lemańczyk b07bf769c9 Switch single-use GPU exponent checks to standard modular math 2025-10-13 04:58:20 +02:00
Marek Lemańczyk 7ef0906a9b Merge pull request #163 from mlemanczyk/codex/analyze-caching-strategy-for-mersenne-path
Clarify per-divisor Montgomery recomputation
2025-10-13 03:43:36 +02:00
Marek Lemańczyk f9d51efe94 Clarify per-divisor Montgomery recomputation 2025-10-13 03:43:23 +02:00
Marek Lemańczyk b2b71f705a Merge pull request #162 from mlemanczyk/codex/analyze-execution-path-for-gpu-optimization
Batch CPU divisor scanning in reusable batches
2025-10-13 01:42:26 +02:00
Marek Lemańczyk 9e06a21718 Streamline GPU remainder batching tables 2025-10-13 01:35:50 +02:00
Marek Lemańczyk 483584b5d4 Add GPU options for by-divisor Montgomery data 2025-10-13 01:35:46 +02:00
Marek Lemańczyk ea29035c55 Use GpuUInt128 in by-divisor GPU scan 2025-10-13 01:35:40 +02:00
Marek Lemańczyk cd47b122a5 Batch GPU remainder filtering for by-divisor scan 2025-10-13 01:35:36 +02:00
Marek Lemańczyk cfedfd12c8 Restore CPU by-divisor flow when GPU deltas disabled 2025-10-13 01:35:32 +02:00
Marek Lemańczyk 41e6703926 Add dedicated GPU residue evaluator for by-divisor deltas 2025-10-13 01:35:26 +02:00
Marek Lemańczyk 0902b18721 Add GPU delta routing option for by-divisor mode 2025-10-13 01:35:21 +02:00
mlemanczyk 67dcf3ac7b Local variables optimized for reuse and lowered registry pressure. 2025-10-12 23:57:58 +02:00
Marek Lemańczyk 681e9a4a5e Batch CPU divisor scanning across reusable buffers 2025-10-12 22:46:58 +02:00
mlemanczyk 77ef93eb91 Standardized on neutral naming for easier reuse 2025-10-12 21:19:54 +02:00
mlemanczyk fcfb6f6434 New benchmarks added. Tiny optimization 2025-10-12 21:17:21 +02:00
mlemanczyk 8c0320d837 Add optimized 2025-10-12 20:25:43 +02:00
mlemanczyk 0ff93e1d09 MulAdd optimized 2025-10-12 20:18:35 +02:00
mlemanczyk 6c4fa7fcf6 Benchmark selection restored 2025-10-12 20:15:38 +02:00
mlemanczyk 9e5ee36e6e Tiny optimization 2025-10-12 19:00:56 +02:00
Marek Lemańczyk e5486af49d Merge pull request #161 from mlemanczyk/codex/add-benchmarks-for-mutableuint128.multiplyadd
Restore original MultiplyAddInline implementation
2025-10-12 18:54:22 +02:00
Marek Lemańczyk 67d9f4be55 Restore original MultiplyAddInline logic 2025-10-12 18:54:05 +02:00
mlemanczyk d555e157f6 Calculation switched to typical O(N) 2025-10-12 18:23:06 +02:00
Marek Lemańczyk 7562f14b1c Merge pull request #160 from mlemanczyk/codex/remove-uint128-from-mutableuint128
Restore MutableUInt128 UInt128-based initialization
2025-10-12 16:03:26 +02:00
Marek Lemańczyk cc50e7376f Restore UInt128 initialization helpers 2025-10-12 16:03:10 +02:00
Marek Lemańczyk 808bf64fab Merge pull request #159 from mlemanczyk/codex/update-pow2mod-to-use-montgomery
Switch Pow2Mod kernel to non-windowed Montgomery exponentiation
2025-10-12 05:44:53 +02:00
Marek Lemańczyk 86989d0502 Replace Pow2Mod kernel with Montgomery binary ladder 2025-10-12 05:44:39 +02:00
mlemanczyk e5dc688865 Locking temporarily disabled for testing 2025-10-12 05:38:33 +02:00
mlemanczyk c64e672552 Heuristic parameters adjusted to test match thread counts in runs 2025-10-12 05:38:05 +02:00
mlemanczyk a1848e4f0b Compilation errors fixed. 2025-10-12 00:39:12 +02:00
mlemanczyk be4ed3db34 Merge branch 'odd-powers-on-gpu' of https://github.com/mlemanczyk/even-perfect-numbers-scanner into odd-powers-on-gpu 2025-10-12 00:38:00 +02:00
mlemanczyk dde01cf8b9 Compilation errors fixed. 2025-10-12 00:37:57 +02:00
Marek Lemańczyk 4838abc6c0 Merge pull request #158 from mlemanczyk/codex/revert-to-parallel.foreach-for-candidate-processing
Restore Parallel.ForEach scheduling for by-divisor CPU path
2025-10-12 00:36:31 +02:00
Marek Lemańczyk 9c378db18f Use Parallel.ForEach for grouped by-divisor work items 2025-10-12 00:36:20 +02:00
Marek Lemańczyk 4f73d89716 Merge pull request #157 from mlemanczyk/codex/ensure-proper-threading-for-candidate-groups
Group by-divisor CPU workloads per worker
2025-10-12 00:16:31 +02:00
Marek Lemańczyk 421cb8ae7d Group by-divisor CPU workloads per worker 2025-10-12 00:00:47 +02:00
Marek Lemańczyk 17f1ea73a7 Merge pull request #156 from mlemanczyk/codex/analyze-gc-pressure-in-evenperfectbitscanner
Reduce GC churn in CPU Montgomery pow2 ladder
2025-10-11 23:13:31 +02:00
Marek Lemańczyk 60c8dbf6d6 Make MutableUInt128 a struct and reuse it in Pollard Rho 2025-10-11 23:13:17 +02:00
Marek Lemańczyk 9d62278bb3 Use stack buffers for CPU exponent lowering and add MutableUInt128 2025-10-11 23:13:12 +02:00
Marek Lemańczyk 2da549fe8c Reduce GC in CPU odd power ladder 2025-10-11 22:45:46 +02:00
Marek Lemańczyk b33158390f Merge pull request #155 from mlemanczyk/codex/add-time-limit-for-prime-testing
Refine by-divisor prime testing time limits
2025-10-11 22:29:24 +02:00
Marek Lemańczyk 6a1925dd47 Refine by-divisor prime test time limits 2025-10-11 22:28:52 +02:00
Marek Lemańczyk b47e758235 Merge pull request #154 from mlemanczyk/codex/analyze-evenperfectbitscanner-for-early-termination
Short-circuit order reduction in by-divisor CPU path
2025-10-11 21:47:56 +02:00
Marek Lemańczyk 2801832af9 Short-circuit divisor order reduction once the exponent is reached 2025-10-11 21:47:39 +02:00
Marek Lemańczyk ec265bde8b Merge pull request #153 from mlemanczyk/codex/analyze-caching-efficiency-in-evenperfectbitscanner
Remove Montgomery divisor cache
2025-10-11 21:17:45 +02:00
Marek Lemańczyk ad22329814 Avoid pooling small buffers in CPU divisor pipeline 2025-10-11 21:17:18 +02:00
Marek Lemańczyk 8e9390bc5e Isolate CPU divisor pools per thread 2025-10-11 21:17:12 +02:00
Marek Lemańczyk cd69c08fb4 Remove Montgomery divisor cache 2025-10-11 20:50:13 +02:00
Marek Lemańczyk fca650b285 Merge pull request #152 from mlemanczyk/codex/optimize-accelerator-allocation-for-mersennedivisorcycles
Remove GPU fallback from context pool
2025-10-11 18:37:48 +02:00
Marek Lemańczyk c4858d2829 Dispose GPU execution scope with the kernel lease 2025-10-11 18:37:38 +02:00
Marek Lemańczyk d476748216 Remove CPU fallback from GPU context pool 2025-10-11 18:31:46 +02:00
Marek Lemańczyk 3e2d2b05b1 Merge pull request #151 from mlemanczyk/codex/modify-primeordercalculator-for-divisor-cycle
Reuse cached divisor cycle suppression scopes
2025-10-11 17:35:13 +02:00
Marek Lemańczyk 3c926e4909 Cache divisor cycle suppression scopes 2025-10-11 17:35:00 +02:00
Marek Lemańczyk e1be80048f Merge pull request #150 from mlemanczyk/codex/analyze-garbage-collector-cost-issue
Pool prime factor partial results for divisor factoring
2025-10-11 16:39:24 +02:00
Marek Lemańczyk 99c98cd017 Pool prime factor partial results for divisor factoring 2025-10-11 16:39:07 +02:00
Marek Lemańczyk 8e7df60dbf Merge pull request #149 from mlemanczyk/codex/modify-primeordercalculator-for-gpu-execution
Enable GPU fallbacks for CPU order calculations
2025-10-11 15:58:54 +02:00
Marek Lemańczyk 3c1519874c Enable GPU fallbacks for CPU order calculations 2025-10-11 15:58:34 +02:00
Marek Lemańczyk 8fb7f937c0 Merge pull request #148 from mlemanczyk/codex/analyze-and-comment-mersennedivisorcycles-method-calls
Restore GPU heuristics and reuse cached order factorization
2025-10-11 15:30:31 +02:00
Marek Lemańczyk fa74667f89 Respect GPU configuration and reuse order factorization 2025-10-11 15:30:04 +02:00
Marek Lemańczyk 45183acd9b Merge pull request #147 from mlemanczyk/codex/update-.csproj-to-exclude-.temp.cs-files
Exclude VS Code temp files from project builds
2025-10-11 14:24:55 +02:00
Marek Lemańczyk 4f5ae7b674 Exclude VS Code temp files from compilation 2025-10-11 14:22:59 +02:00
mlemanczyk 12140bca0a Rented pools made thread static 2025-10-11 14:14:28 +02:00
mlemanczyk 1cbf49c8b5 Small optimizations 2025-10-11 13:37:23 +02:00
Marek Lemańczyk 74fb7f1535 Merge pull request #146 from mlemanczyk/codex/optimize-ulongextensions-for-gpu-calculations
Accelerate Montgomery odd power initialization with GPU batches
2025-10-10 23:16:29 +02:00
Marek Lemańczyk 9181959390 Accelerate Montgomery odd power initialization with GPU batches 2025-10-10 23:12:49 +02:00
58 changed files with 4646 additions and 1304 deletions

No files matched your search

@@ -15,4 +15,7 @@
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" /> <PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project> </Project>
@@ -158,36 +158,16 @@ public class MulMod64Benchmarks
} }
/// <summary> /// <summary>
/// Uses the GPU-friendly <see cref="ULongExtensions.MulMod64GpuCompatible(ulong, ulong, ulong)"/> helper; dense inputs now /// Uses the production <see cref="ULongExtensions.MulMod64(ulong, ulong, ulong)"/> helper shared by CPU and GPU callers.
/// cost 24.4–61.9 ns, while NearFullRange and SparseOperands finish near 7.27 ns and ZeroOperands stay baseline-fast at
/// 3.59 ns.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Observed means: CrossWordBlend 24.422 ns (6.86×), MixedBitPattern 61.850 ns, NearFullRange 7.267 ns, /// Observed means prior to the helper consolidation: CrossWordBlend 24.422 ns, MixedBitPattern 61.850 ns, NearFullRange 7.267 ns,
/// PrimeSizedModulus 55.114 ns, SparseOperands 7.261 ns, ZeroOperands 3.587 ns. /// PrimeSizedModulus 55.114 ns, SparseOperands 7.261 ns, ZeroOperands 3.587 ns.
/// </remarks> /// </remarks>
[Benchmark] [Benchmark]
public ulong GpuCompatibleBaseline() public ulong UnifiedMulMod()
{ {
// TODO: Callers in production should migrate to ULongExtensions.MulMod64 where GPU parity return Input.Left.MulMod64(Input.Right, Input.Modulus);
// is not required; that baseline stayed 6–17× faster for dense 64-bit operands.
return Input.Left.MulMod64GpuCompatible(Input.Right, Input.Modulus);
}
/// <summary>
/// Uses the deferred native-modulo helper <see cref="ULongExtensions.MulMod64GpuCompatibleDeferred(ulong, ulong, ulong)"/>;
/// excels when operands are tiny (2.01 ns on ZeroOperands) yet remains the slowest choice on dense data at 146–295 ns.
/// </summary>
/// <remarks>
/// Observed means: CrossWordBlend 146.559 ns (40.63×), MixedBitPattern 255.555 ns, NearFullRange 21.390 ns,
/// PrimeSizedModulus 294.486 ns, SparseOperands 18.182 ns, ZeroOperands 2.009 ns.
/// </remarks>
[Benchmark]
public ulong GpuCompatibleDeferred()
{
// TODO: Retire the deferred GPU shim from runtime paths and keep it only for benchmarks;
// ULongExtensions.MulMod64 avoids the 6×–82× slowdown on real-world operand mixes.
return Input.Left.MulMod64GpuCompatibleDeferred(Input.Right, Input.Modulus);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -0,0 +1,75 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[ShortRunJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class MutableUInt128AddBenchmarks
{
private static readonly BenchmarkInput[] Inputs =
[
new(Create(0UL, 0x0000_0000_0000_0007UL), Create(0, 0x3UL), "TinyOperands"),
new(Create(0x1234_5678_9ABC_DEF0UL, 0x0FED_CBA9_8765_4321UL), Create(0xFFFF_FFFF, 0x0000_0001UL), "MixedOperands"),
new(Create(0xFFFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFFFUL), Create(0xFFFF_FFFF, 0xFFFF_FFFBUL), "NearMaxOperands"),
];
[ParamsSource(nameof(GetInputs))]
public BenchmarkInput Input { get; set; }
public static IEnumerable<BenchmarkInput> GetInputs() => Inputs;
private static MutableUInt128 Create(ulong high, ulong low) => new(high, low);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void StandardInternal(ref MutableUInt128 source, MutableUInt128 value)
{
ulong temp = source.Low + value.Low;
source.Low = temp;
temp = temp < source.Low ? 1UL : 0UL;
temp += source.High;
temp += value.High;
source.High = temp;
}
/// <summary>
/// Baseline MultiplyAdd implementation that chains Multiply and Add; measured 3.370 ns with mixed operands,
/// 3.361 ns when both limbs and inputs are near their maxima, and 3.368 ns with tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.370 ns, NearMaxOperands 3.361 ns, TinyOperands 3.368 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public MutableUInt128 Standard()
{
MutableUInt128 value = Input.InitialValue;
StandardInternal(ref value, Input.Addend);
return value;
}
/// <summary>
/// Fully inlined MultiplyAdd implementation that kept all arithmetic inside the method; landed at 3.175 ns on mixed operands,
/// 3.177 ns on near-max inputs, and 3.178 ns on tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.175 ns, NearMaxOperands 3.177 ns, TinyOperands 3.178 ns.
/// Status: Implemented
/// </remarks>
[Benchmark]
public MutableUInt128 Optimized()
{
MutableUInt128 value = Input.InitialValue;
value.Add(Input.Addend);
return value;
}
public readonly record struct BenchmarkInput(MutableUInt128 InitialValue, MutableUInt128 Addend, string Name)
{
public override string ToString()
{
return Name;
}
}
}
@@ -0,0 +1,100 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[ShortRunJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class MutableUInt128Mul64Benchmarks
{
private static readonly BenchmarkInput[] Inputs =
[
new(Create(0UL, 0x0000_0000_0000_0007UL), 0x3UL, "TinyOperands"),
new(Create(0x1234_5678_9ABC_DEF0UL, 0x0FED_CBA9_8765_4321UL), 0xFFFF_FFFF_0000_0001UL, "MixedOperands"),
new(Create(0xFFFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFFFUL), 0xFFFF_FFFF_FFFF_FFFBUL, "NearMaxOperands"),
];
[ParamsSource(nameof(GetInputs))]
public BenchmarkInput Input { get; set; }
public static IEnumerable<BenchmarkInput> GetInputs() => Inputs;
private static MutableUInt128 Create(ulong high, ulong low) => new(high, low);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mul64(ulong left, ulong right, out ulong low)
{
const ulong Mask32 = 0xFFFF_FFFFUL;
ulong leftLow = left & Mask32;
ulong leftHigh = left >> 32;
ulong rightLow = right & Mask32;
ulong rightHigh = right >> 32;
ulong lowLow = leftLow * rightLow;
ulong lowHigh = leftLow * rightHigh;
ulong highLow = leftHigh * rightLow;
ulong highHigh = leftHigh * rightHigh;
ulong carry = (lowLow >> 32) + (lowHigh & Mask32) + (highLow & Mask32);
ulong high = highHigh + (lowHigh >> 32) + (highLow >> 32) + (carry >> 32);
low = ((carry & Mask32) << 32) | (lowLow & Mask32);
return high;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void StandardInternal(ref MutableUInt128 source, ulong value)
{
ulong currentHigh = source.High;
ulong currentLow = source.Low;
ulong carry = Mul64(currentLow, value, out ulong newLow);
Mul64(currentHigh, value, out ulong shiftedHigh);
ulong newHigh = shiftedHigh + carry;
source.Low = newLow;
source.High = newHigh;
}
/// <summary>
/// Baseline MultiplyAdd implementation that chains Multiply and Add; measured 3.113 ns with mixed operands,
/// 3.128 ns when both limbs and inputs are near their maxima, and 3.131 ns with tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.113 ns, NearMaxOperands 3.128 ns, TinyOperands 3.131 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public MutableUInt128 Standard()
{
MutableUInt128 value = Input.InitialValue;
StandardInternal(ref value, Input.Addend);
return value;
}
/// <summary>
/// Fully inlined MultiplyAdd implementation that kept all arithmetic inside the method; landed at 2.969 ns on mixed operands,
/// 2.912 ns on near-max inputs, and 2.935 ns on tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 2.969 ns, NearMaxOperands 2.912 ns, TinyOperands 2.935 ns.
/// Status: Implemented
/// </remarks>
[Benchmark]
public MutableUInt128 Optimized()
{
MutableUInt128 value = Input.InitialValue;
value.Mul(Input.Addend);
return value;
}
public readonly record struct BenchmarkInput(MutableUInt128 InitialValue, ulong Addend, string Name)
{
public override string ToString()
{
return Name;
}
}
}
@@ -0,0 +1,76 @@
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[ShortRunJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class MutableUInt128MultiplyAddBenchmarks
{
private static readonly MultiplyAddInput[] Inputs =
[
new(Create(0UL, 0x0000_0000_0000_0007UL), 0x3UL, 0x5UL, "TinyOperands"),
new(Create(0x1234_5678_9ABC_DEF0UL, 0x0FED_CBA9_8765_4321UL), 0xFFFF_FFFF_0000_0001UL, 0x1357_9BDF_2468_ACEEUL, "MixedOperands"),
new(Create(0xFFFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFFFUL), 0xFFFF_FFFF_FFFF_FFFBUL, 0xFFFF_FFFF_FFFF_FFF7UL, "NearMaxOperands"),
];
[ParamsSource(nameof(GetInputs))]
public MultiplyAddInput Input { get; set; }
public static IEnumerable<MultiplyAddInput> GetInputs() => Inputs;
private static UInt128 Create(ulong high, ulong low)
{
return ((UInt128)high << 64) | low;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void MultiplyAdd(ref MutableUInt128 source, ulong multiplier, ulong addend)
{
source.Mul(multiplier);
source.Add(addend);
}
/// <summary>
/// Baseline MultiplyAdd implementation that chains Multiply and Add; measured 3.370 ns with mixed operands,
/// 3.361 ns when both limbs and inputs are near their maxima, and 3.368 ns with tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.370 ns, NearMaxOperands 3.361 ns, TinyOperands 3.368 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public MutableUInt128 MultiplyAddChained()
{
MutableUInt128 value = new(Input.InitialValue);
MultiplyAdd(ref value, Input.Multiplier, Input.Addend);
return value;
}
/// <summary>
/// Fully inlined MultiplyAdd implementation that kept all arithmetic inside the method; landed at 3.175 ns on mixed operands,
/// 3.177 ns on near-max inputs, and 3.178 ns on tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.175 ns, NearMaxOperands 3.177 ns, TinyOperands 3.178 ns.
/// Status: Implemented
/// </remarks>
[Benchmark]
public MutableUInt128 MultiplyAddFullyInlined()
{
MutableUInt128 value = new(Input.InitialValue);
value.MulAdd(Input.Multiplier, Input.Addend);
return value;
}
public readonly record struct MultiplyAddInput(UInt128 InitialValue, ulong Multiplier, ulong Addend, string Name)
{
public override string ToString()
{
return Name;
}
}
}
@@ -0,0 +1,75 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[ShortRunJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class MutableUInt128SubMutableBenchmarks
{
private static readonly BenchmarkInput[] Inputs =
[
new(Create(0UL, 0x0000_0000_0000_0007UL), Create(0, 0x3UL), "TinyOperands"),
new(Create(0x1234_5678_9ABC_DEF0UL, 0x0FED_CBA9_8765_4321UL), Create(0xFFFF_FFFF, 0x0000_0001UL), "MixedOperands"),
new(Create(0xFFFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFFFUL), Create(0xFFFF_FFFF, 0xFFFF_FFFBUL), "NearMaxOperands"),
];
[ParamsSource(nameof(GetInputs))]
public BenchmarkInput Input { get; set; }
public static IEnumerable<BenchmarkInput> GetInputs() => Inputs;
private static MutableUInt128 Create(ulong high, ulong low) => new(high, low);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void StandardInternal(ref MutableUInt128 source, MutableUInt128 value)
{
ulong low = source.Low;
ulong result = low - value.Low;
ulong borrow = low < value.Low ? 1UL : 0UL;
source.Low = result;
source.High = source.High - value.High - borrow;
}
/// <summary>
/// Baseline MultiplyAdd implementation that chains Multiply and Add; measured 3.370 ns with mixed operands,
/// 3.361 ns when both limbs and inputs are near their maxima, and 3.368 ns with tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.370 ns, NearMaxOperands 3.361 ns, TinyOperands 3.368 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public MutableUInt128 Standard()
{
MutableUInt128 value = Input.InitialValue;
StandardInternal(ref value, Input.Addend);
return value;
}
/// <summary>
/// Fully inlined MultiplyAdd implementation that kept all arithmetic inside the method; landed at 3.175 ns on mixed operands,
/// 3.177 ns on near-max inputs, and 3.178 ns on tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.175 ns, NearMaxOperands 3.177 ns, TinyOperands 3.178 ns.
/// Status: Implemented
/// </remarks>
[Benchmark]
public MutableUInt128 Optimized()
{
MutableUInt128 value = Input.InitialValue;
value.Sub(Input.Addend);
return value;
}
public readonly record struct BenchmarkInput(MutableUInt128 InitialValue, MutableUInt128 Addend, string Name)
{
public override string ToString()
{
return Name;
}
}
}
+14 -8
View File
@@ -8,16 +8,22 @@ public static class Program
{ {
public static void Main(string[] args) public static void Main(string[] args)
{ {
// var config = DefaultConfig.Instance.WithOptions(ConfigOptions.JoinSummary | ConfigOptions.DisableOptimizationsValidator); if (args.Length > 0)
// BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config); {
IConfig config = DefaultConfig.Instance.WithOptions(ConfigOptions.JoinSummary | ConfigOptions.DisableOptimizationsValidator);
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config);
return;
}
Pow2MontgomeryModCycleComputationBenchmarks benchmarks = new() BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run();
{
Scale = Pow2MontgomeryModCycleComputationBenchmarks.InputScale.Large
};
GpuPrimeWorkLimiter.SetLimit(1); // Pow2MontgomeryModCycleComputationBenchmarks benchmarks = new()
benchmarks.Setup(); // {
// Scale = Pow2MontgomeryModCycleComputationBenchmarks.InputScale.Large
// };
// GpuPrimeWorkLimiter.SetLimit(1);
// benchmarks.Setup();
// Console.WriteLine("Press ENTER to run"); // Console.WriteLine("Press ENTER to run");
// Console.ReadLine(); // Console.ReadLine();
@@ -11,4 +11,7 @@
<PackageReference Include="Open.Numeric.Primes" Version="4.0.4" /> <PackageReference Include="Open.Numeric.Primes" Version="4.0.4" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project> </Project>
@@ -22,4 +22,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="../EvenPerfectBitScanner/EvenPerfectBitScanner.csproj" /> <ProjectReference Include="../EvenPerfectBitScanner/EvenPerfectBitScanner.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project> </Project>
+2 -1
View File
@@ -540,7 +540,7 @@ public class ProgramTests
public void Main_displays_help_when_requested() public void Main_displays_help_when_requested()
{ {
var main = typeof(Program).GetMethod("Main", BindingFlags.NonPublic | BindingFlags.Static)!; var main = typeof(Program).GetMethod("Main", BindingFlags.NonPublic | BindingFlags.Static)!;
using var writer = new StringWriter(); var writer = new StringWriter();
TextWriter original = Console.Out; TextWriter original = Console.Out;
Console.SetOut(writer); Console.SetOut(writer);
@@ -554,6 +554,7 @@ public class ProgramTests
} }
string output = writer.ToString(); string output = writer.ToString();
writer.Dispose();
output.Should().Contain("Usage:"); output.Should().Contain("Usage:");
output.Should().Contain("--mersenne-device=cpu|gpu"); output.Should().Contain("--mersenne-device=cpu|gpu");
output.Should().Contain("--primes-device=cpu|gpu"); output.Should().Contain("--primes-device=cpu|gpu");
@@ -16,4 +16,7 @@
<PackageReference Include="Open.Numeric" Version="3.0.1" /> <PackageReference Include="Open.Numeric" Version="3.0.1" />
<PackageReference Include="Open.Numeric.Primes" Version="4.0.4" /> <PackageReference Include="Open.Numeric.Primes" Version="4.0.4" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project> </Project>
+131 -34
View File
@@ -44,6 +44,7 @@ internal static class Program
private static string? _resultsPrefix; private static string? _resultsPrefix;
private static readonly Optimized PrimeIterator = new(); private static readonly Optimized PrimeIterator = new();
private static ulong _byDivisorStartPrime; private static ulong _byDivisorStartPrime;
private static TimeSpan? _primeTestLimit;
[ThreadStatic] [ThreadStatic]
private static bool _lastCompositeP; private static bool _lastCompositeP;
@@ -74,6 +75,8 @@ internal static class Program
bool useResidue = false; // M_p test via residue divisors (replaces LL/incremental) bool useResidue = false; // M_p test via residue divisors (replaces LL/incremental)
bool useDivisor = false; // M_p divisibility by specific divisor bool useDivisor = false; // M_p divisibility by specific divisor
bool useByDivisor = false; // Iterative divisor scan across primes UInt128 divisor = UInt128.Zero; bool useByDivisor = false; // Iterative divisor scan across primes UInt128 divisor = UInt128.Zero;
ByDivisorDeltasDevice byDivisorDeltasDevice = ByDivisorDeltasDevice.Cpu;
ByDivisorMontgomeryDevice byDivisorMontgomeryDevice = ByDivisorMontgomeryDevice.Cpu;
// Device routing // Device routing
bool useGpuCycles = true; bool useGpuCycles = true;
bool mersenneOnGpu = true; // controls Lucas/incremental/pow2mod device bool mersenneOnGpu = true; // controls Lucas/incremental/pow2mod device
@@ -181,6 +184,51 @@ internal static class Program
{ {
mersenneOnGpu = !arg.AsSpan(arg.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase); mersenneOnGpu = !arg.AsSpan(arg.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
} }
else if (arg.StartsWith("--bydivisor-deltas-device=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> deviceValue = arg.AsSpan(arg.IndexOf('=') + 1);
if (deviceValue.Equals("gpu", StringComparison.OrdinalIgnoreCase))
{
byDivisorDeltasDevice = ByDivisorDeltasDevice.Gpu;
}
else if (deviceValue.Equals("cpu", StringComparison.OrdinalIgnoreCase))
{
byDivisorDeltasDevice = ByDivisorDeltasDevice.Cpu;
}
else
{
Console.WriteLine("Invalid --bydivisor-deltas-device value. Expected cpu or gpu.");
return;
}
}
else if (arg.StartsWith("--bydivisor-montgomery-device=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> deviceValue = arg.AsSpan(arg.IndexOf('=') + 1);
if (deviceValue.Equals("gpu", StringComparison.OrdinalIgnoreCase))
{
byDivisorMontgomeryDevice = ByDivisorMontgomeryDevice.Gpu;
}
else if (deviceValue.Equals("cpu", StringComparison.OrdinalIgnoreCase))
{
byDivisorMontgomeryDevice = ByDivisorMontgomeryDevice.Cpu;
}
else
{
Console.WriteLine("Invalid --bydivisor-montgomery-device value. Expected cpu or gpu.");
return;
}
}
else if (arg.StartsWith("--prime-test-limit=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> value = arg.AsSpan(arg.IndexOf('=') + 1);
if (!TryParsePrimeTestLimit(value, out TimeSpan parsedLimitValue))
{
Console.WriteLine("Failed to parse --prime-test-limit value.");
return;
}
_primeTestLimit = parsedLimitValue;
}
else if (arg.Equals("--workaround-mod", StringComparison.OrdinalIgnoreCase)) else if (arg.Equals("--workaround-mod", StringComparison.OrdinalIgnoreCase))
{ {
useModuloWorkaround = true; useModuloWorkaround = true;
@@ -377,6 +425,8 @@ internal static class Program
} }
} }
GpuKernelPool.CpuCycleUsesGpu = orderOnGpu;
if (useByDivisor) if (useByDivisor)
{ {
_byDivisorStartPrime = startPrimeProvided ? currentP : 0UL; _byDivisorStartPrime = startPrimeProvided ? currentP : 0UL;
@@ -517,6 +567,11 @@ internal static class Program
? new MersenneNumberDivisorByDivisorGpuTester() ? new MersenneNumberDivisorByDivisorGpuTester()
: new MersenneNumberDivisorByDivisorCpuTester(); : new MersenneNumberDivisorByDivisorCpuTester();
_byDivisorTester.BatchSize = scanBatchSize; _byDivisorTester.BatchSize = scanBatchSize;
if (_byDivisorTester is MersenneNumberDivisorByDivisorCpuTester cpuByDivisorTester)
{
cpuByDivisorTester.DeltasDevice = byDivisorDeltasDevice;
cpuByDivisorTester.MontgomeryDevice = byDivisorMontgomeryDevice;
}
} }
// Load RLE blacklist (optional) // Load RLE blacklist (optional)
@@ -552,18 +607,18 @@ internal static class Program
useDivisor, useDivisor,
useByDivisor, useByDivisor,
mersenneOnGpu, mersenneOnGpu,
useOrder, useOrder,
useModuloWorkaround, useModuloWorkaround,
_useGcdFilter, _useGcdFilter,
NttGpuMath.GpuTransformBackend, NttGpuMath.GpuTransformBackend,
gpuPrimeThreads, gpuPrimeThreads,
sliceSize, sliceSize,
scanBatchSize, scanBatchSize,
_orderWarmupLimitOverride ?? 5_000_000UL, _orderWarmupLimitOverride ?? 5_000_000UL,
NttGpuMath.ReductionMode, NttGpuMath.ReductionMode,
mersenneOnGpu ? "gpu" : "cpu", mersenneOnGpu ? "gpu" : "cpu",
(GpuContextPool.ForceCpu ? "cpu" : "gpu"), (GpuContextPool.ForceCpu ? "cpu" : "gpu"),
orderOnGpu ? "gpu" : "cpu"); orderOnGpu ? "gpu" : "cpu");
if (!string.IsNullOrEmpty(_resultsPrefix)) if (!string.IsNullOrEmpty(_resultsPrefix))
{ {
@@ -665,7 +720,21 @@ internal static class Program
if (_useByDivisorMode) if (_useByDivisorMode)
{ {
RunByDivisorMode(byDivisorCandidates, threadCount); if (_byDivisorTester is null)
{
throw new InvalidOperationException("By-divisor tester has not been initialized.");
}
MersenneNumberDivisorByDivisorTester.Run(
byDivisorCandidates,
_byDivisorTester,
_byDivisorPreviousResults,
_byDivisorStartPrime,
static () => _lastCompositeP = true,
static () => _lastCompositeP = false,
PrintResult,
threadCount,
_primeTestLimit);
FlushBuffer(); FlushBuffer();
StringBuilderPool.Return(_outputBuilder!); StringBuilderPool.Return(_outputBuilder!);
return; return;
@@ -806,26 +875,6 @@ internal static class Program
return candidates; return candidates;
} }
private static void RunByDivisorMode(List<ulong> candidates, int threadCount)
{
if (_byDivisorTester is null)
{
throw new InvalidOperationException("By-divisor tester has not been initialized.");
}
// TODO: Inline this helper by invoking MersenneNumberDivisorByDivisorTester.Run directly at call sites so
// the CLI avoids this extra frame during the hot composite-reporting path.
MersenneNumberDivisorByDivisorTester.Run(
candidates,
_byDivisorTester,
_byDivisorPreviousResults,
_byDivisorStartPrime,
static () => _lastCompositeP = true,
static () => _lastCompositeP = false,
PrintResult,
threadCount);
}
private static unsafe void LoadResultsFile(string resultsFileName, Action<ulong, bool, bool> lineProcessorAction) private static unsafe void LoadResultsFile(string resultsFileName, Action<ulong, bool, bool> lineProcessorAction)
{ {
using FileStream readStream = new(resultsFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); using FileStream readStream = new(resultsFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
@@ -890,6 +939,51 @@ internal static class Program
} }
} }
private static bool TryParsePrimeTestLimit(ReadOnlySpan<char> value, out TimeSpan result)
{
result = TimeSpan.Zero;
if (value.IsEmpty)
{
return false;
}
ReadOnlySpan<char> magnitudeSpan = value;
double magnitude;
if (value.Length >= 2 && value.EndsWith("ms", StringComparison.OrdinalIgnoreCase))
{
magnitudeSpan = value[..^2];
if (!double.TryParse(magnitudeSpan, NumberStyles.Float, CultureInfo.InvariantCulture, out magnitude))
{
return false;
}
result = TimeSpan.FromMilliseconds(magnitude);
return true;
}
char suffix = char.ToLowerInvariant(value[^1]);
if (suffix == 's' || suffix == 'm')
{
magnitudeSpan = value[..^1];
if (!double.TryParse(magnitudeSpan, NumberStyles.Float, CultureInfo.InvariantCulture, out magnitude))
{
return false;
}
result = suffix == 's' ? TimeSpan.FromSeconds(magnitude) : TimeSpan.FromMinutes(magnitude);
return true;
}
if (!double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out magnitude))
{
return false;
}
result = TimeSpan.FromMilliseconds(magnitude);
return true;
}
private static void PrintHelp() private static void PrintHelp()
{ {
Console.WriteLine("Usage: EvenPerfectBitScanner [options]"); Console.WriteLine("Usage: EvenPerfectBitScanner [options]");
@@ -923,10 +1017,13 @@ internal static class Program
Console.WriteLine(" --divisor-cycles-batch=<value> batch size for cycles generation (default 512)"); Console.WriteLine(" --divisor-cycles-batch=<value> batch size for cycles generation (default 512)");
Console.WriteLine(" --divisor-cycles-continue continue divisor cycles generation"); Console.WriteLine(" --divisor-cycles-continue continue divisor cycles generation");
Console.WriteLine(" --divisor-cycles-limit=<value> cycle search iterations when --mersenne=divisor"); Console.WriteLine(" --divisor-cycles-limit=<value> cycle search iterations when --mersenne=divisor");
Console.WriteLine(" --prime-test-limit=<value> Time limit for --mersenne=bydivisor prime checks (e.g. 5s, 500ms)");
Console.WriteLine(" --bydivisor-deltas-device=cpu|gpu Device for --mersenne=bydivisor residue batches (default: cpu)");
Console.WriteLine(" --bydivisor-montgomery-device=cpu|gpu Device for Montgomery data when using --mersenne=bydivisor (default: cpu)");
Console.WriteLine(" --use-order test primality via q order"); Console.WriteLine(" --use-order test primality via q order");
Console.WriteLine(" --workaround-mod avoid '%' operator on the GPU"); Console.WriteLine(" --workaround-mod avoid '%' operator on the GPU");
// mod-automaton removed // mod-automaton removed
Console.WriteLine(" --filter-p=<path> process only p from previous run results (required for --mersenne=bydivisor)"); Console.WriteLine(" --filter-p=<path> process only p from previous run results (required for --mersenne=bydivisor; primes must be strictly increasing)");
Console.WriteLine(" --write-batch-size=<value> overwrite frequency of disk writes (default 100 lines)"); Console.WriteLine(" --write-batch-size=<value> overwrite frequency of disk writes (default 100 lines)");
Console.WriteLine(" --gcd-filter enable early sieve based on GCD"); Console.WriteLine(" --gcd-filter enable early sieve based on GCD");
Console.WriteLine(" --help, -help, --?, -?, /? show this help message"); Console.WriteLine(" --help, -help, --?, -?, /? show this help message");
@@ -18,13 +18,14 @@ public class GpuPrimeLimiterTests
var startedSecond = new TaskCompletionSource<bool>(); var startedSecond = new TaskCompletionSource<bool>();
var sw = new Stopwatch(); var sw = new Stopwatch();
using var first = GpuPrimeWorkLimiter.Acquire(); var first = GpuPrimeWorkLimiter.Acquire();
sw.Start(); sw.Start();
var secondTask = Task.Run(() => var secondTask = Task.Run(() =>
{ {
using var second = GpuPrimeWorkLimiter.Acquire(); var second = GpuPrimeWorkLimiter.Acquire();
startedSecond.TrySetResult(true); startedSecond.TrySetResult(true);
second.Dispose();
}); });
// Give secondTask a moment to attempt acquire (it should block). // Give secondTask a moment to attempt acquire (it should block).
@@ -34,23 +34,19 @@ public class NttMontgomeryTests
// Device execution // Device execution
GpuContextPool.GpuContextLease context = GpuContextPool.Rent(); GpuContextPool.GpuContextLease context = GpuContextPool.Rent();
try var accelerator = context.Accelerator;
{ var buffer = accelerator.Allocate1D<GpuUInt128>(gpu.Length);
var accelerator = context.Accelerator; buffer.View.CopyFromCPU(ref gpu[0], gpu.Length);
using var buffer = accelerator.Allocate1D<GpuUInt128>(gpu.Length); NttGpuMath.SquareDevice(accelerator, buffer.View, modulus, primitiveRoot);
buffer.View.CopyFromCPU(ref gpu[0], gpu.Length); buffer.View.CopyToCPU(ref gpu[0], gpu.Length);
NttGpuMath.SquareDevice(accelerator, buffer.View, modulus, primitiveRoot);
buffer.View.CopyToCPU(ref gpu[0], gpu.Length);
for (int i = 0; i < cpu.Length; i++) for (int i = 0; i < cpu.Length; i++)
{
gpu[i].Should().Be(cpu[i]);
}
}
finally
{ {
context.Dispose(); gpu[i].Should().Be(cpu[i]);
} }
buffer.Dispose();
context.Dispose();
} }
[Fact] [Fact]
@@ -17,13 +17,14 @@ public class GpuWorkLimiterTests
var startedSecond = new TaskCompletionSource<bool>(); var startedSecond = new TaskCompletionSource<bool>();
var sw = new Stopwatch(); var sw = new Stopwatch();
using var first = GpuWorkLimiter.Acquire(); var first = GpuWorkLimiter.Acquire();
sw.Start(); sw.Start();
var secondTask = Task.Run(() => var secondTask = Task.Run(() =>
{ {
using var second = GpuWorkLimiter.Acquire(); var second = GpuWorkLimiter.Acquire();
startedSecond.TrySetResult(true); startedSecond.TrySetResult(true);
second.Dispose();
}); });
await Task.Delay(50); await Task.Delay(50);
@@ -14,8 +14,8 @@ public class MersenneDivisorCyclesGenerateGpuTests
{ {
MersenneDivisorCycles.GenerateGpu(path, maxDivisor: 50UL, batchSize: 16, skipCount: 0, nextPosition: 0); MersenneDivisorCycles.GenerateGpu(path, maxDivisor: 50UL, batchSize: 16, skipCount: 0, nextPosition: 0);
var pairs = new List<(ulong divisor, ulong cycle)>(); var pairs = new List<(ulong divisor, ulong cycle)>();
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read); var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
using var reader = new BinaryReader(stream); var reader = new BinaryReader(stream);
while (stream.Position < stream.Length) while (stream.Position < stream.Length)
{ {
ulong d = reader.ReadUInt64(); ulong d = reader.ReadUInt64();
@@ -41,6 +41,9 @@ public class MersenneDivisorCyclesGenerateGpuTests
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(pair.divisor); MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(pair.divisor);
pair.cycle.Should().Be(MersenneDivisorCycles.CalculateCycleLength(pair.divisor, divisorData)); pair.cycle.Should().Be(MersenneDivisorCycles.CalculateCycleLength(pair.divisor, divisorData));
} }
reader.Dispose();
stream.Dispose();
} }
finally finally
{ {
@@ -61,8 +64,8 @@ public class MersenneDivisorCyclesGenerateGpuTests
MersenneDivisorCycles.Generate(path, maxDivisor: 50UL, threads: 4); MersenneDivisorCycles.Generate(path, maxDivisor: 50UL, threads: 4);
var pairs = new List<(ulong divisor, ulong cycle)>(); var pairs = new List<(ulong divisor, ulong cycle)>();
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read); var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
using var reader = new BinaryReader(stream); var reader = new BinaryReader(stream);
while (stream.Position < stream.Length) while (stream.Position < stream.Length)
{ {
ulong divisor = reader.ReadUInt64(); ulong divisor = reader.ReadUInt64();
@@ -95,6 +98,9 @@ public class MersenneDivisorCyclesGenerateGpuTests
var (_, completeCount) = MersenneDivisorCycles.FindLast(path); var (_, completeCount) = MersenneDivisorCycles.FindLast(path);
completeCount.Should().Be(expected.Count); completeCount.Should().Be(expected.Count);
reader.Dispose();
stream.Dispose();
} }
finally finally
{ {
@@ -26,7 +26,8 @@ public class MersenneNumberDivisorByDivisorTesterTests
markComposite: () => compositeMarks++, markComposite: () => compositeMarks++,
clearComposite: () => clearedMarks++, clearComposite: () => clearedMarks++,
printResult: (prime, searched, detailed, passed) => results.Add((prime, searched, detailed, passed)), printResult: (prime, searched, detailed, passed) => results.Add((prime, searched, detailed, passed)),
threadCount: 1); threadCount: 1,
primeTestLimit: null);
tester.ConfiguredMaxPrime.Should().Be(13UL); tester.ConfiguredMaxPrime.Should().Be(13UL);
compositeMarks.Should().Be(2); compositeMarks.Should().Be(2);
@@ -59,7 +60,7 @@ public class MersenneNumberDivisorByDivisorTesterTests
return DivisorLimit; return DivisorLimit;
} }
public bool IsPrime(ulong prime, out bool divisorsExhausted) public bool IsPrime(ulong prime, out bool divisorsExhausted, TimeSpan? timeLimit = null)
{ {
divisorsExhausted = true; divisorsExhausted = true;
return prime % 7UL != 0UL && prime % 11UL != 0UL; return prime % 7UL != 0UL && prime % 11UL != 0UL;
@@ -1,5 +1,6 @@
using System; using System;
using FluentAssertions; using FluentAssertions;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Cpu; using PerfectNumbers.Core.Cpu;
using Xunit; using Xunit;
@@ -7,78 +8,125 @@ namespace PerfectNumbers.Core.Tests;
public class MersenneNumberDivisorCpuTesterTests public class MersenneNumberDivisorCpuTesterTests
{ {
[Fact] private const ulong LargeDivisor1 = 4_000_169UL;
[Trait("Category", "Fast")] private const ulong LargeDivisor2 = 4_000_561UL;
public void ByDivisor_tester_tracks_divisors_across_primes()
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_tester_tracks_divisors_across_primes()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(83UL);
tester.IsPrime(59UL, out bool divisorsExhausted).Should().BeTrue();
divisorsExhausted.Should().BeTrue();
tester.IsPrime(61UL, out divisorsExhausted).Should().BeTrue();
divisorsExhausted.Should().BeTrue();
tester.IsPrime(67UL, out divisorsExhausted).Should().BeTrue();
divisorsExhausted.Should().BeTrue();
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_prime_test_limit_disables_divisor_exhaustion()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(83UL);
tester.IsPrime(59UL, out bool divisorsExhausted, TimeSpan.Zero).Should().BeTrue();
divisorsExhausted.Should().BeFalse();
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_checks_divisors_across_exponents()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(197UL);
var session = tester.CreateDivisorSession();
ulong[] exponents = { 59UL, 61UL, 67UL, 71UL, 73UL, 79UL, 117_652UL, 333_380UL };
byte[] hits = new byte[exponents.Length];
MontgomeryDivisorData divisor1 = MontgomeryDivisorData.FromModulus(LargeDivisor1);
ulong cycle1 = MersenneDivisorCycles.CalculateCycleLength(LargeDivisor1, divisor1);
session.CheckDivisor(LargeDivisor1, divisor1, cycle1, exponents, hits);
byte[] expected1 = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
{ {
var tester = new MersenneNumberDivisorByDivisorCpuTester(); expected1[i] = exponents[i].Pow2ModWindowedCpu(divisor1.Modulus) == 1UL ? (byte)1 : (byte)0;
tester.ConfigureFromMaxPrime(11UL);
tester.IsPrime(5UL, out bool divisorsExhausted).Should().BeTrue();
divisorsExhausted.Should().BeTrue();
tester.IsPrime(7UL, out divisorsExhausted).Should().BeTrue();
divisorsExhausted.Should().BeTrue();
tester.IsPrime(11UL, out divisorsExhausted).Should().BeFalse();
divisorsExhausted.Should().BeTrue();
} }
[Fact] hits.Should().Equal(expected1);
[Trait("Category", "Fast")]
public void ByDivisor_session_checks_divisors_across_primes() Array.Fill(hits, (byte)0);
MontgomeryDivisorData divisor2 = MontgomeryDivisorData.FromModulus(LargeDivisor2);
ulong cycle2 = MersenneDivisorCycles.CalculateCycleLength(LargeDivisor2, divisor2);
session.CheckDivisor(LargeDivisor2, divisor2, cycle2, exponents, hits);
byte[] expected2 = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
{ {
var tester = new MersenneNumberDivisorByDivisorCpuTester(); expected2[i] = exponents[i].Pow2ModWindowedCpu(divisor2.Modulus) == 1UL ? (byte)1 : (byte)0;
tester.ConfigureFromMaxPrime(13UL);
using var session = tester.CreateDivisorSession();
ulong[] primes = { 5UL, 7UL, 11UL, 13UL };
byte[] hits = new byte[primes.Length];
ulong cycle23 = MersenneDivisorCycles.CalculateCycleLength(23UL, MontgomeryDivisorData.FromModulus(23UL));
session.CheckDivisor(23UL, MontgomeryDivisorData.FromModulus(23UL), cycle23, primes, hits);
hits.Should().ContainInOrder(new byte[] { 0, 0, 1, 0 });
Array.Fill(hits, (byte)0);
ulong cycle31 = MersenneDivisorCycles.CalculateCycleLength(31UL, MontgomeryDivisorData.FromModulus(31UL));
session.CheckDivisor(31UL, MontgomeryDivisorData.FromModulus(31UL), cycle31, primes, hits);
hits.Should().ContainInOrder(new byte[] { 1, 0, 0, 0 });
} }
[Fact] hits.Should().Equal(expected2);
[Trait("Category", "Fast")]
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_seven_as_composite() session.Dispose();
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_large_divisor1_as_composite()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(197UL);
var session = tester.CreateDivisorSession();
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(LargeDivisor1);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(LargeDivisor1, divisorData);
ulong[] exponents = { 59UL, 67UL, cycle, cycle * 2UL, cycle + 1UL };
byte[] hits = new byte[exponents.Length];
session.CheckDivisor(LargeDivisor1, divisorData, cycle, exponents, hits);
byte[] expected = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
{ {
var tester = new MersenneNumberDivisorByDivisorCpuTester(); expected[i] = exponents[i].Pow2ModWindowedCpu(divisorData.Modulus) == 1UL ? (byte)1 : (byte)0;
tester.ConfigureFromMaxPrime(43UL);
using var session = tester.CreateDivisorSession();
ulong[] exponents = { 6UL, 7UL, 9UL, 10UL, 12UL };
byte[] hits = new byte[exponents.Length];
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(7UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(7UL, divisorData);
session.CheckDivisor(7UL, divisorData, cycle, exponents, hits);
hits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 });
} }
[Fact] hits.Should().Equal(expected);
[Trait("Category", "Fast")]
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_eleven_as_composite() session.Dispose();
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_large_divisor2_as_composite()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(197UL);
var session = tester.CreateDivisorSession();
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(LargeDivisor2);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(LargeDivisor2, divisorData);
ulong[] exponents = { 61UL, 73UL, cycle, cycle * 2UL, cycle - 1UL };
byte[] hits = new byte[exponents.Length];
session.CheckDivisor(LargeDivisor2, divisorData, cycle, exponents, hits);
byte[] expected = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
{ {
var tester = new MersenneNumberDivisorByDivisorCpuTester(); expected[i] = exponents[i].Pow2ModWindowedCpu(divisorData.Modulus) == 1UL ? (byte)1 : (byte)0;
tester.ConfigureFromMaxPrime(61UL);
using var session = tester.CreateDivisorSession();
ulong[] exponents = { 10UL, 11UL, 20UL, 21UL, 30UL };
byte[] hits = new byte[exponents.Length];
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(11UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(11UL, divisorData);
session.CheckDivisor(11UL, divisorData, cycle, exponents, hits);
hits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 });
} }
hits.Should().Equal(expected);
session.Dispose();
}
} }
@@ -12,12 +12,12 @@ namespace PerfectNumbers.Core.Tests;
public class MersenneNumberDivisorGpuTesterTests public class MersenneNumberDivisorGpuTesterTests
{ {
[Theory] [Theory]
[InlineData(33UL, 7UL, true)] [InlineData(117_652UL, 4_000_169UL, true)]
[InlineData(35UL, 31UL, true)] [InlineData(333_380UL, 4_000_561UL, true)]
[InlineData(37UL, 223UL, true)] [InlineData(59UL, 4_000_169UL, false)]
[InlineData(33UL, 13UL, false)] [InlineData(61UL, 4_000_561UL, false)]
[InlineData(35UL, 73UL, false)] [InlineData(333_379UL, 4_000_561UL, false)]
[InlineData(37UL, 227UL, false)] [InlineData(117_653UL, 4_000_169UL, false)]
[Trait("Category", "Fast")] [Trait("Category", "Fast")]
public void IsDivisible_returns_expected(ulong exponent, ulong divisor, bool expected) public void IsDivisible_returns_expected(ulong exponent, ulong divisor, bool expected)
{ {
@@ -86,6 +86,17 @@ public class MersenneNumberDivisorGpuTesterTests
exhausted.Should().BeTrue(); exhausted.Should().BeTrue();
} }
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_gpu_prime_test_limit_stops_scan_immediately()
{
var tester = new MersenneNumberDivisorByDivisorGpuTester();
tester.ConfigureFromMaxPrime(43UL);
tester.IsPrime(31UL, out bool divisorsExhausted, TimeSpan.Zero).Should().BeTrue();
divisorsExhausted.Should().BeFalse();
}
[Fact] [Fact]
[Trait("Category", "Fast")] [Trait("Category", "Fast")]
public void ByDivisor_tester_requires_configuration() public void ByDivisor_tester_requires_configuration()
@@ -141,23 +152,41 @@ public class MersenneNumberDivisorGpuTesterTests
[Fact] [Fact]
[Trait("Category", "Fast")] [Trait("Category", "Fast")]
public void ByDivisor_session_checks_divisors_across_primes() public void ByDivisor_session_checks_divisors_across_exponents()
{ {
var tester = new MersenneNumberDivisorByDivisorGpuTester(); var tester = new MersenneNumberDivisorByDivisorGpuTester();
tester.ConfigureFromMaxPrime(43UL); tester.ConfigureFromMaxPrime(197UL);
using var session = tester.CreateDivisorSession(); var session = tester.CreateDivisorSession();
ulong[] primes = { 31UL, 37UL, 41UL, 43UL }; ulong[] exponents = { 59UL, 61UL, 67UL, 71UL, 73UL, 79UL, 117_652UL, 333_380UL };
byte[] hits = new byte[primes.Length]; byte[] hits = new byte[exponents.Length];
ulong cycle223 = MersenneDivisorCycles.CalculateCycleLength(223UL, MontgomeryDivisorData.FromModulus(223UL)); MontgomeryDivisorData divisor1 = MontgomeryDivisorData.FromModulus(4_000_169UL);
session.CheckDivisor(223UL, MontgomeryDivisorData.FromModulus(223UL), cycle223, primes, hits); ulong cycle1 = MersenneDivisorCycles.CalculateCycleLength(4_000_169UL, divisor1);
hits.Should().ContainInOrder(new byte[] { 0, 1, 0, 0 }); session.CheckDivisor(4_000_169UL, divisor1, cycle1, exponents, hits);
byte[] expected1 = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
{
expected1[i] = exponents[i].Pow2ModWindowedCpu(divisor1.Modulus) == 1UL ? (byte)1 : (byte)0;
}
hits.Should().Equal(expected1);
Array.Fill(hits, (byte)0); Array.Fill(hits, (byte)0);
ulong cycle13367 = MersenneDivisorCycles.CalculateCycleLength(13367UL, MontgomeryDivisorData.FromModulus(13367UL)); MontgomeryDivisorData divisor2 = MontgomeryDivisorData.FromModulus(4_000_561UL);
session.CheckDivisor(13367UL, MontgomeryDivisorData.FromModulus(13367UL), cycle13367, primes, hits); ulong cycle2 = MersenneDivisorCycles.CalculateCycleLength(4_000_561UL, divisor2);
hits.Should().ContainInOrder(new byte[] { 0, 0, 1, 0 }); session.CheckDivisor(4_000_561UL, divisor2, cycle2, exponents, hits);
byte[] expected2 = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
{
expected2[i] = exponents[i].Pow2ModWindowedCpu(divisor2.Modulus) == 1UL ? (byte)1 : (byte)0;
}
hits.Should().Equal(expected2);
session.Dispose();
} }
[Fact] [Fact]
@@ -169,122 +198,134 @@ public class MersenneNumberDivisorGpuTesterTests
GpuBatchSize = 2, GpuBatchSize = 2,
}; };
tester.ConfigureFromMaxPrime(47UL); tester.ConfigureFromMaxPrime(197UL);
using var session = tester.CreateDivisorSession(); var session = tester.CreateDivisorSession();
ulong[] primes = { 31UL, 37UL, 41UL, 43UL, 47UL }; ulong[] exponents = { 59UL, 61UL, 67UL, 71UL, 73UL, 79UL, 117_652UL };
byte[] hits = new byte[primes.Length]; byte[] hits = new byte[exponents.Length];
ulong cycle223 = MersenneDivisorCycles.CalculateCycleLength(223UL, MontgomeryDivisorData.FromModulus(223UL)); MontgomeryDivisorData divisor = MontgomeryDivisorData.FromModulus(4_000_169UL);
session.CheckDivisor(223UL, MontgomeryDivisorData.FromModulus(223UL), cycle223, primes, hits); ulong cycle = MersenneDivisorCycles.CalculateCycleLength(4_000_169UL, divisor);
session.CheckDivisor(4_000_169UL, divisor, cycle, exponents, hits);
hits.Should().ContainInOrder(new byte[] { 0, 1, 0, 0, 0 }); byte[] expected = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
{
expected[i] = exponents[i].Pow2ModWindowedCpu(divisor.Modulus) == 1UL ? (byte)1 : (byte)0;
}
hits.Should().Equal(expected);
session.Dispose();
} }
[Fact] [Fact]
[Trait("Category", "Fast")] [Trait("Category", "Fast")]
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_seven_as_composite() public void ByDivisor_session_marks_mersenne_numbers_divisible_by_large_divisor1_as_composite()
{ {
var tester = new MersenneNumberDivisorByDivisorGpuTester var tester = new MersenneNumberDivisorByDivisorGpuTester
{ {
GpuBatchSize = 5, GpuBatchSize = 5,
}; };
tester.ConfigureFromMaxPrime(43UL); tester.ConfigureFromMaxPrime(197UL);
using var session = tester.CreateDivisorSession(); var session = tester.CreateDivisorSession();
ulong[] exponents = { 6UL, 7UL, 9UL, 10UL, 12UL }; MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(4_000_169UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(4_000_169UL, divisorData);
ulong[] exponents = { 59UL, 67UL, cycle, cycle * 2UL, cycle + 1UL };
byte[] hits = new byte[exponents.Length]; byte[] hits = new byte[exponents.Length];
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(7UL); session.CheckDivisor(4_000_169UL, divisorData, cycle, exponents, hits);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(7UL, divisorData);
session.CheckDivisor(7UL, divisorData, cycle, exponents, hits);
var hostBufferField = typeof(MersenneNumberDivisorByDivisorGpuTester.DivisorScanSession) var hostBufferField = typeof(MersenneNumberDivisorByDivisorGpuTester.DivisorScanSession)
.GetField("_hostBuffer", BindingFlags.NonPublic | BindingFlags.Instance)!; .GetField("_hostBuffer", BindingFlags.NonPublic | BindingFlags.Instance)!;
ulong[] hostBuffer = (ulong[])hostBufferField.GetValue(session)!; ulong[] hostBuffer = (ulong[])hostBufferField.GetValue(session)!;
Span<ulong> residues = hostBuffer.AsSpan(0, exponents.Length); Span<ulong> residues = hostBuffer.AsSpan(0, exponents.Length);
byte[] computedHits = new byte[exponents.Length]; byte[] expected = new byte[exponents.Length];
for (int i = 0; i < residues.Length; i++) for (int i = 0; i < residues.Length; i++)
{ {
computedHits[i] = residues[i] == divisorData.MontgomeryOne ? (byte)1 : (byte)0; expected[i] = residues[i] == 1UL ? (byte)1 : (byte)0;
} }
computedHits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 }); byte[] direct = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
{
direct[i] = exponents[i].Pow2ModWindowedCpu(divisorData.Modulus) == 1UL ? (byte)1 : (byte)0;
}
expected.Should().Equal(direct);
hits.Should().Equal(direct);
session.Dispose();
} }
[Fact] [Fact]
[Trait("Category", "Fast")] [Trait("Category", "Fast")]
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_eleven_as_composite() public void ByDivisor_session_marks_mersenne_numbers_divisible_by_large_divisor2_as_composite()
{ {
var tester = new MersenneNumberDivisorByDivisorGpuTester var tester = new MersenneNumberDivisorByDivisorGpuTester
{ {
GpuBatchSize = 5, GpuBatchSize = 5,
}; };
tester.ConfigureFromMaxPrime(61UL); tester.ConfigureFromMaxPrime(197UL);
using var session = tester.CreateDivisorSession(); var session = tester.CreateDivisorSession();
ulong[] exponents = { 10UL, 11UL, 20UL, 21UL, 30UL }; MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(4_000_561UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(4_000_561UL, divisorData);
ulong[] exponents = { 61UL, 73UL, cycle, cycle * 2UL, cycle - 1UL };
byte[] hits = new byte[exponents.Length]; byte[] hits = new byte[exponents.Length];
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(11UL); session.CheckDivisor(4_000_561UL, divisorData, cycle, exponents, hits);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(11UL, divisorData);
session.CheckDivisor(11UL, divisorData, cycle, exponents, hits);
var hostBufferField = typeof(MersenneNumberDivisorByDivisorGpuTester.DivisorScanSession) var hostBufferField = typeof(MersenneNumberDivisorByDivisorGpuTester.DivisorScanSession)
.GetField("_hostBuffer", BindingFlags.NonPublic | BindingFlags.Instance)!; .GetField("_hostBuffer", BindingFlags.NonPublic | BindingFlags.Instance)!;
ulong[] hostBuffer = (ulong[])hostBufferField.GetValue(session)!; ulong[] hostBuffer = (ulong[])hostBufferField.GetValue(session)!;
Span<ulong> residues = hostBuffer.AsSpan(0, exponents.Length); Span<ulong> residues = hostBuffer.AsSpan(0, exponents.Length);
byte[] computedHits = new byte[exponents.Length]; byte[] expected = new byte[exponents.Length];
for (int i = 0; i < residues.Length; i++) for (int i = 0; i < residues.Length; i++)
{ {
computedHits[i] = residues[i] == divisorData.MontgomeryOne ? (byte)1 : (byte)0; expected[i] = residues[i] == 1UL ? (byte)1 : (byte)0;
} }
computedHits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 }); byte[] direct = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
{
direct[i] = exponents[i].Pow2ModWindowedCpu(divisorData.Modulus) == 1UL ? (byte)1 : (byte)0;
}
expected.Should().Equal(direct);
hits.Should().Equal(direct);
session.Dispose();
} }
[Fact] [Fact]
[Trait("Category", "Fast")] [Trait("Category", "Fast")]
public void ByDivisor_gpu_tester_skips_divisors_excluded_by_small_cycle_generation() public void ByDivisor_gpu_tester_generates_cycles_for_large_divisors()
{ {
var cycles = MersenneDivisorCycles.Shared; var tester = new MersenneNumberDivisorByDivisorGpuTester();
var tableField = typeof(MersenneDivisorCycles).GetField("_table", BindingFlags.NonPublic | BindingFlags.Instance)!; tester.ConfigureFromMaxPrime(197UL);
var smallCyclesField = typeof(MersenneDivisorCycles).GetField("_smallCycles", BindingFlags.NonPublic | BindingFlags.Instance)!;
var originalTable = (List<(ulong Divisor, ulong Cycle)>)tableField.GetValue(cycles)!; var session = tester.CreateDivisorSession();
var originalSmall = (ulong[]?)smallCyclesField.GetValue(cycles); ulong[] exponents = { 59UL, 67UL, 117_652UL };
byte[] hits = new byte[exponents.Length];
try MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(4_000_169UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(4_000_169UL, divisorData);
session.CheckDivisor(4_000_169UL, divisorData, cycle, exponents, hits);
byte[] expected = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
{ {
var patchedTable = new List<(ulong Divisor, ulong Cycle)>(); expected[i] = exponents[i].Pow2ModWindowedCpu(divisorData.Modulus) == 1UL ? (byte)1 : (byte)0;
ulong[] patchedSmall = new ulong[PerfectNumberConstants.MaxQForDivisorCycles + 1];
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(191UL);
patchedSmall[191] = MersenneDivisorCycles.CalculateCycleLength(191UL, divisorData);
tableField.SetValue(cycles, patchedTable);
smallCyclesField.SetValue(cycles, patchedSmall);
DivisorCycleCache.Shared.RefreshSnapshot();
var tester = new MersenneNumberDivisorByDivisorGpuTester();
tester.ConfigureFromMaxPrime(19UL);
typeof(MersenneNumberDivisorByDivisorGpuTester)
.GetField("_divisorLimit", BindingFlags.NonPublic | BindingFlags.Instance)!
.SetValue(tester, 200UL);
tester.IsPrime(19UL, out bool divisorsExhausted).Should().BeTrue();
divisorsExhausted.Should().BeTrue();
}
finally
{
tableField.SetValue(cycles, originalTable);
smallCyclesField.SetValue(cycles, originalSmall);
DivisorCycleCache.Shared.RefreshSnapshot();
GpuContextPool.DisposeAll();
} }
hits.Should().Equal(expected);
session.Dispose();
} }
[Fact] [Fact]
@@ -296,16 +337,25 @@ public class MersenneNumberDivisorGpuTesterTests
BatchSize = 2, BatchSize = 2,
}; };
tester.ConfigureFromMaxPrime(47UL); tester.ConfigureFromMaxPrime(197UL);
using var session = tester.CreateDivisorSession(); var session = tester.CreateDivisorSession();
ulong[] primes = { 31UL, 37UL, 41UL, 43UL, 47UL }; ulong[] exponents = { 59UL, 61UL, 67UL, 71UL, 73UL, 79UL, 117_652UL };
byte[] hits = new byte[primes.Length]; byte[] hits = new byte[exponents.Length];
ulong cycle223 = MersenneDivisorCycles.CalculateCycleLength(223UL, MontgomeryDivisorData.FromModulus(223UL)); MontgomeryDivisorData divisor = MontgomeryDivisorData.FromModulus(4_000_169UL);
session.CheckDivisor(223UL, MontgomeryDivisorData.FromModulus(223UL), cycle223, primes, hits); ulong cycle = MersenneDivisorCycles.CalculateCycleLength(4_000_169UL, divisor);
session.CheckDivisor(4_000_169UL, divisor, cycle, exponents, hits);
hits.Should().ContainInOrder(new byte[] { 0, 1, 0, 0, 0 }); byte[] expected = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
{
expected[i] = exponents[i].Pow2ModWindowedCpu(divisor.Modulus) == 1UL ? (byte)1 : (byte)0;
}
hits.Should().Equal(expected);
session.Dispose();
} }
} }
@@ -22,4 +22,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="../PerfectNumbers.Core/PerfectNumbers.Core.csproj" /> <ProjectReference Include="../PerfectNumbers.Core/PerfectNumbers.Core.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project> </Project>
@@ -7,6 +7,9 @@ namespace PerfectNumbers.Core.Tests;
public class PrimeOrderCalculatorTests public class PrimeOrderCalculatorTests
{ {
// EvenPerfectBitScanner never exercises the GPU heuristic device or the small-prime guards that the legacy tests cover,
// so those scenarios are commented out because they are unreachable in the production pipeline.
/*
public static IEnumerable<object[]> SmallPrimes() public static IEnumerable<object[]> SmallPrimes()
{ {
yield return new object[] { 3UL }; yield return new object[] { 3UL };
@@ -197,4 +200,5 @@ public class PrimeOrderCalculatorTests
return order; return order;
} }
*/
} }
@@ -0,0 +1,7 @@
namespace PerfectNumbers.Core;
public enum ByDivisorDeltasDevice
{
Cpu,
Gpu
}
@@ -0,0 +1,7 @@
namespace PerfectNumbers.Core;
public enum ByDivisorMontgomeryDevice
{
Cpu = 0,
Gpu = 1,
}
@@ -1,78 +1,91 @@
using System; using System;
using System.Buffers;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using System.Threading;
using PerfectNumbers.Core; using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
namespace PerfectNumbers.Core.Cpu; namespace PerfectNumbers.Core.Cpu;
public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDivisorByDivisorTester public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDivisorByDivisorTester
{ {
private readonly object _sync = new();
private readonly ConcurrentBag<DivisorScanSession> _sessionPool = new(); private readonly ConcurrentBag<DivisorScanSession> _sessionPool = new();
private ulong _divisorLimit; private ulong _divisorLimit;
private ulong _lastStatusDivisor; private int _lastStatusDivisor;
private bool _isConfigured; private bool _isConfigured;
private int _batchSize = 1_024; private int _batchSize = 1_024;
private ByDivisorDeltasDevice _deltasDevice = ByDivisorDeltasDevice.Cpu;
private ByDivisorMontgomeryDevice _montgomeryDevice = ByDivisorMontgomeryDevice.Cpu;
public int BatchSize public int BatchSize
{ {
get => _batchSize; get => _batchSize;
set => _batchSize = Math.Max(1, value); set
{
int sanitized = Math.Max(1, value);
_batchSize = sanitized;
}
}
public ByDivisorDeltasDevice DeltasDevice
{
get => _deltasDevice;
set => _deltasDevice = value;
}
public ByDivisorMontgomeryDevice MontgomeryDevice
{
get => _montgomeryDevice;
set => _montgomeryDevice = value;
} }
public void ConfigureFromMaxPrime(ulong maxPrime) public void ConfigureFromMaxPrime(ulong maxPrime)
{ {
lock (_sync) _divisorLimit = ComputeDivisorLimitFromMaxPrime(maxPrime);
{ _lastStatusDivisor = 0;
_divisorLimit = ComputeDivisorLimitFromMaxPrime(maxPrime); _isConfigured = true;
_lastStatusDivisor = 0UL;
_isConfigured = true;
}
} }
public ulong DivisorLimit public ulong DivisorLimit
{ {
get get
{ {
lock (_sync) // The by-divisor driver configures the tester during startup, so exposing the divisor limit never
{ // re-enters this method before initialization completes.
if (!_isConfigured) // if (!_isConfigured)
{ // {
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester."); // throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
} // }
return _divisorLimit; return _divisorLimit;
}
} }
} }
public ulong GetAllowedMaxDivisor(ulong prime) public ulong GetAllowedMaxDivisor(ulong prime)
{ {
lock (_sync) // The CLI wires the tester through ConfigureFromMaxPrime before invoking query helpers, so the guard
{ // would never trigger in production runs.
if (!_isConfigured) // if (!_isConfigured)
{ // {
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester."); // throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
} // }
return ComputeAllowedMaxDivisor(prime, _divisorLimit); return ComputeAllowedMaxDivisor(prime, _divisorLimit);
}
} }
public bool IsPrime(ulong prime, out bool divisorsExhausted) public bool IsPrime(ulong prime, out bool divisorsExhausted, TimeSpan? timeLimit = null)
{ {
ulong allowedMax; // The tester is configured once during startup, so runtime IsPrime calls always observe a ready
lock (_sync) // configuration and the legacy guard can remain commented out.
{ // if (!_isConfigured)
if (!_isConfigured) // {
{ // throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester."); // }
}
allowedMax = ComputeAllowedMaxDivisor(prime, _divisorLimit); ulong divisorLimit = _divisorLimit;
} ulong allowedMax = ComputeAllowedMaxDivisor(prime, divisorLimit);
if (allowedMax < 3UL) if (allowedMax < 3UL)
{ {
@@ -89,14 +102,12 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
allowedMax, allowedMax,
out lastProcessed, out lastProcessed,
out processedAll, out processedAll,
out processedCount); out processedCount,
timeLimit);
if (processedCount > 0UL) if (processedCount > 0UL)
{ {
lock (_sync) UpdateStatusUnsafe(processedCount);
{
UpdateStatusUnsafe(processedCount);
}
} }
if (composite) if (composite)
@@ -111,22 +122,21 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
public void PrepareCandidates(ReadOnlySpan<ulong> primes, Span<ulong> allowedMaxValues) public void PrepareCandidates(ReadOnlySpan<ulong> primes, Span<ulong> allowedMaxValues)
{ {
if (allowedMaxValues.Length < primes.Length) // EvenPerfectBitScanner always provides equally sized spans here, so the historical guard never triggers.
{ // if (allowedMaxValues.Length < primes.Length)
throw new ArgumentException("allowedMaxValues span must be at least as long as primes span.", nameof(allowedMaxValues)); // {
} // throw new ArgumentException("allowedMaxValues span must be at least as long as primes span.", nameof(allowedMaxValues));
// }
ulong divisorLimit; // PrepareCandidates is scheduled only after ConfigureFromMaxPrime succeeds, so the tester state is
// stable here and the defensive guard is no longer needed.
// if (!_isConfigured)
// {
// throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
// }
lock (_sync) // Configuration is immutable after startup, so reading the cached limit does not require locking.
{ ulong divisorLimit = _divisorLimit;
if (!_isConfigured)
{
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
}
divisorLimit = _divisorLimit;
}
for (int index = 0; index < primes.Length; index++) for (int index = 0; index < primes.Length; index++)
{ {
@@ -136,21 +146,20 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
public IMersenneNumberDivisorByDivisorTester.IDivisorScanSession CreateDivisorSession() public IMersenneNumberDivisorByDivisorTester.IDivisorScanSession CreateDivisorSession()
{ {
lock (_sync) // The session pool is populated only after configuration, so consumers never request a session before
// ConfigureFromMaxPrime completes.
// if (!_isConfigured)
// {
// throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
// }
if (_sessionPool.TryTake(out DivisorScanSession? session))
{ {
if (!_isConfigured) session.Reset();
{ return session;
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
}
if (_sessionPool.TryTake(out DivisorScanSession? session))
{
session.Reset();
return session;
}
return new DivisorScanSession(this);
} }
return new DivisorScanSession(this);
} }
private void ReturnSession(DivisorScanSession session) private void ReturnSession(DivisorScanSession session)
@@ -163,23 +172,50 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
ulong allowedMax, ulong allowedMax,
out ulong lastProcessed, out ulong lastProcessed,
out bool processedAll, out bool processedAll,
out ulong processedCount) out ulong processedCount,
TimeSpan? timeLimit)
{
if (_deltasDevice == ByDivisorDeltasDevice.Cpu)
{
return CheckDivisorsCpu(prime, allowedMax, out lastProcessed, out processedAll, out processedCount, timeLimit);
}
return CheckDivisorsGpu(prime, allowedMax, out lastProcessed, out processedAll, out processedCount, timeLimit);
}
private bool CheckDivisorsCpu(
ulong prime,
ulong allowedMax,
out ulong lastProcessed,
out bool processedAll,
out ulong processedCount,
TimeSpan? timeLimit)
{ {
lastProcessed = 0UL; lastProcessed = 0UL;
processedCount = 0UL; processedCount = 0UL;
processedAll = false; processedAll = false;
PrimeTestTimeLimit limitGuard;
if (!PrimeTestTimeLimit.TryCreate(timeLimit, out limitGuard))
{
return false;
}
bool enforceLimit = limitGuard.IsActive;
ulong iterationCounter = 0UL;
if (allowedMax < 3UL) if (allowedMax < 3UL)
{ {
return false; return false;
} }
UInt128 step = (UInt128)prime << 1; UInt128 step = (UInt128)prime << 1;
if (step == UInt128.Zero) // EvenPerfectBitScanner only supplies primes greater than one here, so the shifted step never becomes zero.
{ // if (step == UInt128.Zero)
processedAll = true; // {
return false; // processedAll = true;
} // return false;
// }
UInt128 limit = allowedMax; UInt128 limit = allowedMax;
UInt128 divisor = step + UInt128.One; UInt128 divisor = step + UInt128.One;
@@ -212,6 +248,13 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
while (divisor <= limit) while (divisor <= limit)
{ {
if (enforceLimit && (iterationCounter == 0UL || (iterationCounter & 63UL) == 0UL) && limitGuard.HasExpired())
{
processedAll = false;
return false;
}
iterationCounter++;
ulong candidate = (ulong)divisor; ulong candidate = (ulong)divisor;
processedCount++; processedCount++;
lastProcessed = candidate; lastProcessed = candidate;
@@ -222,7 +265,10 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
if (admissible && (remainder8 == 1 || remainder8 == 7) && remainder3 != 0 && remainder5 != 0 && remainder7 != 0 && remainder11 != 0) if (admissible && (remainder8 == 1 || remainder8 == 7) && remainder3 != 0 && remainder5 != 0 && remainder7 != 0 && remainder11 != 0)
{ {
MontgomeryDivisorData divisorData = MontgomeryDivisorDataCache.Get(candidate); // Each divisor appears only once in this monotonically increasing sequence, so
// rebuilding the Montgomery data per candidate avoids caching entries that we
// would never hit again.
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(candidate);
ulong divisorCycle; ulong divisorCycle;
if (candidate <= PerfectNumberConstants.MaxQForDivisorCycles) if (candidate <= PerfectNumberConstants.MaxQForDivisorCycles)
@@ -232,6 +278,9 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
else else
{ {
factorCache ??= new Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>(8); factorCache ??= new Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>(8);
// EvenPerfectBitScanner reaches this path only for divisors above the precomputed cycle snapshot.
// Those candidates follow q = 2 * prime * k + 1 with prime >= 5, so the downstream order calculator
// can rely on an odd divisor and a prime exponent when deriving the cycle length.
if (!MersenneDivisorCycles.TryCalculateCycleLengthForExponent(candidate, prime, divisorData, factorCache, out divisorCycle) || divisorCycle == 0UL) if (!MersenneDivisorCycles.TryCalculateCycleLengthForExponent(candidate, prime, divisorData, factorCache, out divisorCycle) || divisorCycle == 0UL)
{ {
divisorCycle = cycleCache.GetCycleLength(candidate); divisorCycle = cycleCache.GetCycleLength(candidate);
@@ -249,6 +298,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
if (divisorCycle == 0UL) if (divisorCycle == 0UL)
{ {
// Keep this guard active to surface mismatches between generated candidates and the snapshot cache.
Console.WriteLine($"Divisor cycle was not calculated for {prime}"); Console.WriteLine($"Divisor cycle was not calculated for {prime}");
} }
} }
@@ -266,6 +316,249 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
return false; return false;
} }
private bool CheckDivisorsGpu(
ulong prime,
ulong allowedMax,
out ulong lastProcessed,
out bool processedAll,
out ulong processedCount,
TimeSpan? timeLimit)
{
lastProcessed = 0UL;
processedCount = 0UL;
processedAll = false;
PrimeTestTimeLimit limitGuard;
if (!PrimeTestTimeLimit.TryCreate(timeLimit, out limitGuard))
{
return false;
}
bool enforceLimit = limitGuard.IsActive;
ulong iterationCounter = 0UL;
if (allowedMax < 3UL)
{
return false;
}
GpuUInt128 step128 = new GpuUInt128(prime);
step128.ShiftLeft(1);
// Primes above one yield a non-zero 2p value even after widening to GpuUInt128, so the defensive
// zero guard remains commented out to document the invariant without branching.
// if (step128.IsZero)
// {
// processedAll = true;
// return false;
// }
GpuUInt128 limit128 = new GpuUInt128(allowedMax);
GpuUInt128 divisor128 = step128;
divisor128.Add(1UL);
if (divisor128.CompareTo(limit128) > 0)
{
processedAll = true;
return false;
}
ulong step = step128.Low;
ulong limit64 = allowedMax;
int batchCapacity = Math.Max(1, _batchSize);
DivisorCycleCache cycleCache = DivisorCycleCache.Shared;
int preferredCycleBatch = cycleCache.PreferredBatchSize;
if (preferredCycleBatch > 0 && preferredCycleBatch < batchCapacity)
{
batchCapacity = preferredCycleBatch;
}
var candidateEvaluator = new MersenneNumberDivisorCandidateGpuEvaluator(batchCapacity);
ulong[] candidateBuffer = ArrayPool<ulong>.Shared.Rent(batchCapacity);
byte[] maskBuffer = ArrayPool<byte>.Shared.Rent(batchCapacity);
Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? factorCache = null;
const int RemainderTableSize = 6;
byte[] remainderBuffer = ArrayPool<byte>.Shared.Rent(RemainderTableSize);
byte[] stepBuffer = ArrayPool<byte>.Shared.Rent(RemainderTableSize);
Span<byte> remainderSpan = remainderBuffer.AsSpan(0, RemainderTableSize);
Span<byte> stepSpan = stepBuffer.AsSpan(0, RemainderTableSize);
// The remainder and step tables follow the fixed modulus order: 10, 8, 5, 3, 7, 11.
ulong startDivisor64 = divisor128.Low;
remainderSpan[0] = (byte)(startDivisor64 % 10UL);
remainderSpan[1] = (byte)(startDivisor64 % 8UL);
remainderSpan[2] = (byte)(startDivisor64 % 5UL);
remainderSpan[3] = (byte)(startDivisor64 % 3UL);
remainderSpan[4] = (byte)(startDivisor64 % 7UL);
remainderSpan[5] = (byte)(startDivisor64 % 11UL);
stepSpan[0] = (byte)(step % 10UL);
stepSpan[1] = (byte)(step % 8UL);
stepSpan[2] = (byte)(step % 5UL);
stepSpan[3] = (byte)(step % 3UL);
stepSpan[4] = (byte)(step % 7UL);
stepSpan[5] = (byte)(step % 11UL);
bool lastIsSeven = (prime & 3UL) == 3UL;
MersenneNumberDivisorRemainderGpuStepper remainderStepper = new(stepSpan);
MersenneNumberDivisorMontgomeryGpuBuilder? montgomeryBuilder = _montgomeryDevice == ByDivisorMontgomeryDevice.Gpu
? new MersenneNumberDivisorMontgomeryGpuBuilder(batchCapacity)
: null;
MontgomeryDivisorData[]? montgomeryBuffer = montgomeryBuilder != null
? ArrayPool<MontgomeryDivisorData>.Shared.Rent(batchCapacity)
: null;
ulong remainingCandidates = ComputeRemainingCandidateCount(startDivisor64, limit64, step);
try
{
while (remainingCandidates > 0UL && divisor128.CompareTo(limit128) <= 0)
{
if (divisor128.High != 0UL)
{
break;
}
ulong currentDivisor = divisor128.Low;
int chunkCount = remainingCandidates > (ulong)batchCapacity
? batchCapacity
: (int)remainingCandidates;
// batchCapacity and remainingCandidates are strictly positive in this path, so the zero-count
// shortcut never executes on production scans.
// if (chunkCount <= 0)
// {
// break;
// }
Span<ulong> candidateSpan = candidateBuffer.AsSpan(0, chunkCount);
Span<byte> maskSpan = maskBuffer.AsSpan(0, chunkCount);
candidateEvaluator.EvaluateCandidates(
currentDivisor,
step,
limit64,
remainderSpan,
stepSpan,
lastIsSeven,
candidateSpan,
maskSpan);
Span<MontgomeryDivisorData> montgomerySpan = default;
if (montgomeryBuilder != null && montgomeryBuffer != null)
{
montgomerySpan = montgomeryBuffer.AsSpan(0, chunkCount);
// Every candidate divisor is unique across the scan, so build Montgomery data
// for the current batch and drop it immediately instead of maintaining a cache.
montgomeryBuilder.Build(candidateSpan, montgomerySpan);
}
for (int i = 0; i < chunkCount; i++)
{
if (enforceLimit && (iterationCounter == 0UL || (iterationCounter & 63UL) == 0UL) && limitGuard.HasExpired())
{
processedAll = false;
return false;
}
iterationCounter++;
ulong candidate = candidateSpan[i];
processedCount++;
lastProcessed = candidate;
if (maskSpan[i] == 0)
{
continue;
}
MontgomeryDivisorData divisorData;
if (!montgomerySpan.IsEmpty)
{
divisorData = montgomerySpan[i];
if (divisorData.Modulus != candidate)
{
// The GPU builder might skip entries when masks zero them out; recompute on the CPU
// without caching because divisors never repeat within a session.
divisorData = MontgomeryDivisorData.FromModulus(candidate);
}
}
else
{
// Candidates remain unique even when GPU Montgomery support is disabled, so rebuild
// the divisor data inline instead of persisting unused cache entries.
divisorData = MontgomeryDivisorData.FromModulus(candidate);
}
ulong divisorCycle;
if (candidate <= PerfectNumberConstants.MaxQForDivisorCycles)
{
divisorCycle = cycleCache.GetCycleLength(candidate);
}
else
{
factorCache ??= new Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>(8);
if (!MersenneDivisorCycles.TryCalculateCycleLengthForExponent(candidate, prime, divisorData, factorCache, out divisorCycle) || divisorCycle == 0UL)
{
divisorCycle = cycleCache.GetCycleLength(candidate);
}
}
if (divisorCycle == prime)
{
processedAll = true;
return true;
}
if (divisorCycle == 0UL)
{
Console.WriteLine($"Divisor cycle was not calculated for {prime}");
}
}
GpuUInt128 increment = step128;
increment.Mul((ulong)chunkCount);
divisor128.Add(increment);
remainderStepper.Advance(chunkCount, remainderSpan);
remainingCandidates -= (ulong)chunkCount;
}
}
finally
{
montgomeryBuilder?.Dispose();
if (montgomeryBuffer != null)
{
ArrayPool<MontgomeryDivisorData>.Shared.Return(montgomeryBuffer, clearArray: false);
}
remainderStepper.Dispose();
ArrayPool<byte>.Shared.Return(remainderBuffer, clearArray: false);
ArrayPool<byte>.Shared.Return(stepBuffer, clearArray: false);
ArrayPool<ulong>.Shared.Return(candidateBuffer, clearArray: false);
ArrayPool<byte>.Shared.Return(maskBuffer, clearArray: false);
candidateEvaluator.Dispose();
}
processedAll = remainingCandidates == 0UL && divisor128.CompareTo(limit128) > 0;
return false;
}
private static ulong ComputeRemainingCandidateCount(ulong currentDivisor, ulong limit, ulong step)
{
if (step == 0UL || currentDivisor > limit)
{
return 0UL;
}
ulong distance = limit - currentDivisor;
ulong stepsAvailable = distance / step;
return stepsAvailable + 1UL;
}
private static byte CheckDivisor(ulong prime, ulong divisorCycle, in MontgomeryDivisorData divisorData) private static byte CheckDivisor(ulong prime, ulong divisorCycle, in MontgomeryDivisorData divisorData)
{ {
ulong residue = prime.Pow2MontgomeryModWithCycleCpu(divisorCycle, divisorData); ulong residue = prime.Pow2MontgomeryModWithCycleCpu(divisorCycle, divisorData);
@@ -290,17 +583,34 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
return; return;
} }
ulong interval = PerfectNumberConstants.ConsoleInterval; int interval = PerfectNumberConstants.ConsoleInterval;
if (interval == 0UL) // PerfectNumberConstants.ConsoleInterval is a positive constant, so this guard never triggers on EvenPerfectBitScanner.
// if (interval <= 0)
// {
// Volatile.Write(ref _lastStatusDivisor, 0);
// return;
// }
int processedDelta = (int)(processedCount % (uint)interval);
if (processedDelta == 0)
{ {
_lastStatusDivisor = 0UL;
return; return;
} }
ulong total = _lastStatusDivisor + processedCount; while (true)
// TODO: Replace this modulo with the ring-buffer style counter (subtract loop) used in the fast CLI {
// status benchmarks so we avoid `%` in this hot loop while still wrapping progress correctly. int previous = Volatile.Read(ref _lastStatusDivisor);
_lastStatusDivisor = total % interval; int total = previous + processedDelta;
if (total >= interval)
{
total -= interval;
}
if (previous == Interlocked.CompareExchange(ref _lastStatusDivisor, total, previous))
{
return;
}
}
} }
private static ulong ComputeDivisorLimitFromMaxPrime(ulong maxPrime) private static ulong ComputeDivisorLimitFromMaxPrime(ulong maxPrime)
@@ -320,23 +630,34 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
private static ulong ComputeAllowedMaxDivisor(ulong prime, ulong divisorLimit) private static ulong ComputeAllowedMaxDivisor(ulong prime, ulong divisorLimit)
{ {
if (prime <= 1UL) // The by-divisor scanner never emits exponents below the first odd prime, so the defensive guard stays commented out to
{ // avoid branching on production workloads.
return 0UL; // if (prime <= 1UL)
} // {
// return 0UL;
// }
if (prime - 1UL >= 64UL) // Production exponents always satisfy prime - 1 >= 64, so the legacy shift-based clamp would overflow on the hot path.
{ // Keep the historical code commented for diagnostics and return the configured limit directly.
return divisorLimit; // if (prime - 1UL >= 64UL)
} // {
// return divisorLimit;
// }
//
// return Math.Min((1UL << (int)(prime - 1UL)) - 1UL, divisorLimit);
return Math.Min((1UL << (int)(prime - 1UL)) - 1UL, divisorLimit); return divisorLimit;
} }
private sealed class DivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession private sealed class DivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession
{ {
private readonly MersenneNumberDivisorByDivisorCpuTester _owner; private readonly MersenneNumberDivisorByDivisorCpuTester _owner;
private bool _disposed; private bool _disposed;
private ulong[]? _primeDeltas;
private ulong[]? _cycleRemainders;
private ulong[]? _residues;
private int _bufferCapacity;
private MersenneNumberDivisorResidueGpuEvaluator? _gpuResidueEvaluator;
internal DivisorScanSession(MersenneNumberDivisorByDivisorCpuTester owner) internal DivisorScanSession(MersenneNumberDivisorByDivisorCpuTester owner)
{ {
@@ -356,16 +677,30 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
} }
int length = primes.Length; int length = primes.Length;
if (length == 0) // The production scheduler batches at least one exponent per divisor, so the legacy empty-span shortcut remains
// commented to avoid branching on the hot path.
// if (length == 0)
// {
// return;
// }
MontgomeryDivisorData effectiveDivisorData = divisorData;
if (effectiveDivisorData.Modulus != divisor)
{ {
return; // Each divisor surfaces only once across the monotonic scan, so rebuild the Montgomery data
// instead of retaining a cache entry that would never be reused within future checks.
effectiveDivisorData = MontgomeryDivisorData.FromModulus(divisor);
} }
MontgomeryDivisorData cachedData = divisorData; ulong modulus = effectiveDivisorData.Modulus;
if (cachedData.Modulus != divisor) // EvenPerfectBitScanner only schedules odd divisors greater than one, and the fallback
{ // above rebuilds Montgomery data when the modulus mismatches, so this guard never
cachedData = MontgomeryDivisorDataCache.Get(divisor); // triggers for production scans.
} // if (modulus <= 1UL || (modulus & 1UL) == 0UL)
// {
// hits.Clear();
// return;
// }
if (divisorCycle == 0UL) if (divisorCycle == 0UL)
{ {
@@ -377,9 +712,24 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
} }
} }
if (_owner._deltasDevice == ByDivisorDeltasDevice.Cpu)
{
CheckDivisorCpu(effectiveDivisorData, divisorCycle, primes, hits);
return;
}
CheckDivisorGpu(effectiveDivisorData, divisorCycle, primes, hits, length);
}
private static void CheckDivisorCpu(
in MontgomeryDivisorData divisorData,
ulong divisorCycle,
ReadOnlySpan<ulong> primes,
Span<byte> hits)
{
// Keep these remainder steppers in place so future updates continue reusing the previously computed residues. // Keep these remainder steppers in place so future updates continue reusing the previously computed residues.
// They are critical for avoiding repeated full Montgomery exponentiation work when scanning divisors. // They are critical for avoiding repeated full Montgomery exponentiation work when scanning divisors.
var exponentStepper = new ExponentRemainderStepper(cachedData); var exponentStepper = new ExponentRemainderStepper(divisorData);
if (!exponentStepper.IsValidModulus) if (!exponentStepper.IsValidModulus)
{ {
hits.Clear(); hits.Clear();
@@ -393,6 +743,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
? (exponentStepper.ComputeNextIsUnity(primes[0]) ? (byte)1 : (byte)0) ? (exponentStepper.ComputeNextIsUnity(primes[0]) ? (byte)1 : (byte)0)
: (byte)0; : (byte)0;
int length = primes.Length;
for (int i = 1; i < length; i++) for (int i = 1; i < length; i++)
{ {
remainder = cycleStepper.ComputeNext(primes[i]); remainder = cycleStepper.ComputeNext(primes[i]);
@@ -406,6 +757,169 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
} }
} }
private void CheckDivisorGpu(
in MontgomeryDivisorData divisorData,
ulong divisorCycle,
ReadOnlySpan<ulong> primes,
Span<byte> hits,
int length)
{
EnsureCapacity(length);
Span<ulong> deltaSpan = _primeDeltas!.AsSpan(0, length);
ComputePrimeDeltas(primes, deltaSpan);
Span<ulong> remainderSpan = _cycleRemainders!.AsSpan(0, length);
ComputeCycleRemainders(divisorCycle, primes, deltaSpan, remainderSpan);
EnsureGpuEvaluator();
Span<ulong> residueSpan = _residues!.AsSpan(0, length);
_gpuResidueEvaluator!.ComputeResidues(primes, divisorData, residueSpan);
for (int i = 0; i < length; i++)
{
hits[i] = remainderSpan[i] == 0UL && residueSpan[i] == 1UL ? (byte)1 : (byte)0;
}
}
private void EnsureGpuEvaluator()
{
if (_gpuResidueEvaluator is null)
{
_gpuResidueEvaluator = new MersenneNumberDivisorResidueGpuEvaluator(_owner._batchSize);
}
}
private void ReleaseGpuEvaluator()
{
if (_gpuResidueEvaluator is null)
{
return;
}
_gpuResidueEvaluator.Dispose();
_gpuResidueEvaluator = null;
}
private void EnsureCapacity(int requiredLength)
{
// Prime batches are never empty on the by-divisor scanner path, so the zero-length guard stays commented
// out to remove the redundant branch.
// if (requiredLength <= 0)
// {
// return;
// }
// Sessions release pooled buffers between scans; a single null check covers every array because buffers are rented and returned together.
if (requiredLength <= _bufferCapacity && _primeDeltas is not null)
{
return;
}
int newCapacity = _bufferCapacity;
if (newCapacity == 0)
{
// EvenPerfectBitScanner hits this branch on the first batch of each session, so keep the seed allocation.
newCapacity = 1;
}
while (newCapacity < requiredLength)
{
newCapacity <<= 1;
}
ReturnBuffers();
_primeDeltas = ArrayPool<ulong>.Shared.Rent(newCapacity);
_cycleRemainders = ArrayPool<ulong>.Shared.Rent(newCapacity);
_residues = ArrayPool<ulong>.Shared.Rent(newCapacity);
_bufferCapacity = newCapacity;
}
private void ReturnBuffers()
{
// Buffers are lazily allocated per session; the _primeDeltas guard applies to every pooled array because sessions rent and release them together.
if (_primeDeltas is null)
{
return;
}
ArrayPool<ulong>.Shared.Return(_primeDeltas, clearArray: false);
ArrayPool<ulong>.Shared.Return(_cycleRemainders!, clearArray: false);
ArrayPool<ulong>.Shared.Return(_residues!, clearArray: false);
_primeDeltas = null;
_cycleRemainders = null;
_residues = null;
_bufferCapacity = 0;
}
private static void ComputePrimeDeltas(ReadOnlySpan<ulong> primes, Span<ulong> deltas)
{
// Prime batches on the scanning path are never empty, so the legacy guard stays commented to remove the extra branch.
// if (primes.Length == 0)
// {
// return;
// }
ulong previous = primes[0];
deltas[0] = previous;
// Production batches feed strictly increasing primes; callers outside the scanning path must uphold the contract.
for (int i = 1; i < primes.Length; i++)
{
ulong current = primes[i];
deltas[i] = current - previous;
previous = current;
}
}
private static void ComputeCycleRemainders(ulong cycleLength, ReadOnlySpan<ulong> primes, ReadOnlySpan<ulong> deltas, Span<ulong> remainders)
{
// Divisor cycles are validated before reaching the GPU helper, and prime batches are non-empty, so
// the defensive zero checks remain commented out here.
// if (cycleLength == 0UL || primes.Length == 0)
// {
// remainders.Clear();
// return;
// }
ulong remainder = primes[0] % cycleLength;
remainders[0] = remainder;
for (int i = 1; i < primes.Length; i++)
{
remainder = AddMod(remainder, deltas[i], cycleLength);
remainders[i] = remainder;
}
}
private static ulong AddMod(ulong value, ulong delta, ulong modulus)
{
if (modulus == 0UL)
{
// A zero modulus indicates a missing cycle entry; preserve the fallback so snapshot regressions remain visible.
return 0UL;
}
if (ulong.MaxValue - value < delta)
{
UInt128 extended = (UInt128)value + delta;
return (ulong)(extended % modulus);
}
ulong sum = value + delta;
if (sum >= modulus)
{
sum -= modulus;
if (sum >= modulus)
{
sum %= modulus;
}
}
return sum;
}
public void Dispose() public void Dispose()
{ {
if (_disposed) if (_disposed)
@@ -414,6 +928,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
} }
_disposed = true; _disposed = true;
ReleaseGpuEvaluator();
_owner.ReturnSession(this); _owner.ReturnSession(this);
} }
} }
+48 -18
View File
@@ -63,7 +63,9 @@ public sealed class DivisorCycleCache
int[]? rentedMissing = null; int[]? rentedMissing = null;
Span<int> missingBuffer = length <= StackBufferThreshold Span<int> missingBuffer = length <= StackBufferThreshold
? stackalloc int[length] ? stackalloc int[length]
: new Span<int>(rentedMissing = ArrayPool<int>.Shared.Rent(length), 0, length); : length < PerfectNumberConstants.PooledArrayThreshold
? new Span<int>(new int[length])
: new Span<int>(rentedMissing = ThreadLocalArrayPool<int>.Shared.Rent(length), 0, length);
int missingCount = 0; int missingCount = 0;
@@ -84,11 +86,10 @@ public sealed class DivisorCycleCache
} }
cycles[i] = cached; cycles[i] = cached;
continue;
} }
else
{ missingBuffer[missingCount++] = i;
missingBuffer[missingCount++] = i;
}
} }
if (missingCount != 0) if (missingCount != 0)
@@ -102,11 +103,12 @@ public sealed class DivisorCycleCache
{ {
ComputeCyclesCpu(divisors, cycles, missingIndices); ComputeCyclesCpu(divisors, cycles, missingIndices);
} }
} }
if (rentedMissing is not null) if (rentedMissing is not null)
{ {
ArrayPool<int>.Shared.Return(rentedMissing, clearArray: false); ThreadLocalArrayPool<int>.Shared.Return(rentedMissing, clearArray: false);
} }
} }
@@ -128,12 +130,16 @@ public sealed class DivisorCycleCache
ulong[]? rentedDivisors = null; ulong[]? rentedDivisors = null;
Span<ulong> workDivisors = count <= StackBufferThreshold Span<ulong> workDivisors = count <= StackBufferThreshold
? stackalloc ulong[count] ? stackalloc ulong[count]
: new Span<ulong>(rentedDivisors = ArrayPool<ulong>.Shared.Rent(count), 0, count); : count < PerfectNumberConstants.PooledArrayThreshold
? new Span<ulong>(new ulong[count])
: new Span<ulong>(rentedDivisors = ThreadLocalArrayPool<ulong>.Shared.Rent(count), 0, count);
ulong[]? rentedResults = null; ulong[]? rentedResults = null;
Span<ulong> workResults = count <= StackBufferThreshold Span<ulong> workResults = count <= StackBufferThreshold
? stackalloc ulong[count] ? stackalloc ulong[count]
: new Span<ulong>(rentedResults = ArrayPool<ulong>.Shared.Rent(count), 0, count); : count < PerfectNumberConstants.PooledArrayThreshold
? new Span<ulong>(new ulong[count])
: new Span<ulong>(rentedResults = ThreadLocalArrayPool<ulong>.Shared.Rent(count), 0, count);
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
@@ -149,8 +155,12 @@ public sealed class DivisorCycleCache
if (rentedDivisors is not null) if (rentedDivisors is not null)
{ {
ArrayPool<ulong>.Shared.Return(rentedDivisors, clearArray: false); ThreadLocalArrayPool<ulong>.Shared.Return(rentedDivisors, clearArray: false);
ArrayPool<ulong>.Shared.Return(rentedResults!, clearArray: false); }
if (rentedResults is not null)
{
ThreadLocalArrayPool<ulong>.Shared.Return(rentedResults, clearArray: false);
} }
} }
@@ -172,22 +182,30 @@ public sealed class DivisorCycleCache
ulong[]? rentedPow = null; ulong[]? rentedPow = null;
Span<ulong> powSpan = length <= StackBufferThreshold Span<ulong> powSpan = length <= StackBufferThreshold
? stackalloc ulong[length] ? stackalloc ulong[length]
: new Span<ulong>(rentedPow = ArrayPool<ulong>.Shared.Rent(length), 0, length); : length < PerfectNumberConstants.PooledArrayThreshold
? new Span<ulong>(new ulong[length])
: new Span<ulong>(rentedPow = ThreadLocalArrayPool<ulong>.Shared.Rent(length), 0, length);
ulong[]? rentedOrder = null; ulong[]? rentedOrder = null;
Span<ulong> orderSpan = length <= StackBufferThreshold Span<ulong> orderSpan = length <= StackBufferThreshold
? stackalloc ulong[length] ? stackalloc ulong[length]
: new Span<ulong>(rentedOrder = ArrayPool<ulong>.Shared.Rent(length), 0, length); : length < PerfectNumberConstants.PooledArrayThreshold
? new Span<ulong>(new ulong[length])
: new Span<ulong>(rentedOrder = ThreadLocalArrayPool<ulong>.Shared.Rent(length), 0, length);
ulong[]? rentedResult = null; ulong[]? rentedResult = null;
Span<ulong> resultSpan = length <= StackBufferThreshold Span<ulong> resultSpan = length <= StackBufferThreshold
? stackalloc ulong[length] ? stackalloc ulong[length]
: new Span<ulong>(rentedResult = ArrayPool<ulong>.Shared.Rent(length), 0, length); : length < PerfectNumberConstants.PooledArrayThreshold
? new Span<ulong>(new ulong[length])
: new Span<ulong>(rentedResult = ThreadLocalArrayPool<ulong>.Shared.Rent(length), 0, length);
byte[]? rentedStatus = null; byte[]? rentedStatus = null;
Span<byte> statusSpan = length <= StackBufferThreshold Span<byte> statusSpan = length <= StackBufferThreshold
? stackalloc byte[length] ? stackalloc byte[length]
: new Span<byte>(rentedStatus = ArrayPool<byte>.Shared.Rent(length), 0, length); : length < PerfectNumberConstants.PooledArrayThreshold
? new Span<byte>(new byte[length])
: new Span<byte>(rentedStatus = ThreadLocalArrayPool<byte>.Shared.Rent(length), 0, length);
ref ulong divisorRef = ref MemoryMarshal.GetReference(divisors); ref ulong divisorRef = ref MemoryMarshal.GetReference(divisors);
divisorBuffer.View.CopyFromCPU(ref divisorRef, length); divisorBuffer.View.CopyFromCPU(ref divisorRef, length);
@@ -234,10 +252,22 @@ public sealed class DivisorCycleCache
resultSpan.CopyTo(destination); resultSpan.CopyTo(destination);
if (rentedPow is not null) if (rentedPow is not null)
{ {
ArrayPool<ulong>.Shared.Return(rentedPow, clearArray: false); ThreadLocalArrayPool<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); if (rentedOrder is not null)
{
ThreadLocalArrayPool<ulong>.Shared.Return(rentedOrder, clearArray: false);
}
if (rentedResult is not null)
{
ThreadLocalArrayPool<ulong>.Shared.Return(rentedResult, clearArray: false);
}
if (rentedStatus is not null)
{
ThreadLocalArrayPool<byte>.Shared.Return(rentedStatus, clearArray: false);
} }
execution.Dispose(); execution.Dispose();
+16 -5
View File
@@ -1,8 +1,19 @@
namespace PerfectNumbers.Core namespace PerfectNumbers.Core
{ {
public readonly struct FactorEntry(ulong value, int exponent) public readonly struct FactorEntry
{ {
public readonly ulong Value = value; public readonly ulong Value;
public readonly int Exponent = exponent; public readonly int Exponent = 1;
}
public FactorEntry(ulong value, int exponent)
{
Value = value;
Exponent = exponent;
}
public FactorEntry(ulong value)
{
Value = value;
}
}
} }
+134 -131
View File
@@ -6,156 +6,159 @@ namespace PerfectNumbers.Core.Gpu;
public static class GpuContextPool public static class GpuContextPool
{ {
private static readonly bool PoolingEnabled = true; private static readonly bool PoolingEnabled = true;
// TODO: Introduce accelerator-specific warmup so pooled contexts precompile the ProcessEightBitWindows kernels and load // TODO: Introduce accelerator-specific warmup so pooled contexts precompile the ProcessEightBitWindows kernels and load
// divisor-cycle data before the first scan begins. // divisor-cycle data before the first scan begins.
// Default device preference for generic GPU kernels (prime scans, NTT, etc.) // Default device preference for generic GPU kernels (prime scans, NTT, etc.)
public static bool ForceCpu { get; set; } = false; public static bool ForceCpu { get; set; } = false;
internal sealed class PooledContext internal sealed class PooledContext
{
private bool disposed;
public Context Context { get; }
public Accelerator Accelerator { get; }
public bool IsCpu { get; }
public object ExecutionLock { get; } = new();
public PooledContext(bool preferCpu)
{ {
private bool disposed; Context = Context.CreateDefault();
Accelerator = Context.GetPreferredDevice(preferCpu).CreateAccelerator(Context);
public Context Context { get; } IsCpu = Accelerator.AcceleratorType == AcceleratorType.CPU;
public Accelerator Accelerator { get; } // NOTE: Avoid loading/compiling any kernel here to prevent implicit
public bool IsCpu { get; } // CL stream/queue creation during accelerator construction.
public object ExecutionLock { get; } = new(); // Some OpenCL drivers are fragile when a queue is created immediately
// after device init. Real kernels will be JIT-loaded at first use.
public PooledContext(bool preferCpu)
{
Context = Context.CreateDefault();
Accelerator = Context.GetPreferredDevice(preferCpu).CreateAccelerator(Context);
IsCpu = Accelerator.AcceleratorType == AcceleratorType.CPU;
// NOTE: Avoid loading/compiling any kernel here to prevent implicit
// CL stream/queue creation during accelerator construction.
// Some OpenCL drivers are fragile when a queue is created immediately
// after device init. Real kernels will be JIT-loaded at first use.
}
public void Dispose()
{
if (disposed)
{
return;
}
// Ensure all cached GPU buffers for this accelerator are released.
NttGpuMath.ClearCaches(Accelerator);
// Release PrimeTester GPU state for this accelerator (kernel/device primes)
// Clear any per-accelerator cached resources to avoid releasing
// after the accelerator is destroyed.
PrimeTester.ClearGpuCaches(Accelerator);
Accelerator.Dispose();
Context.Dispose();
disposed = true;
}
}
private static readonly ConcurrentQueue<PooledContext> CpuPool = new();
private static readonly ConcurrentQueue<PooledContext> GpuPool = new();
private static readonly object CreationLock = new();
public static GpuContextLease Rent()
{
return RentPreferred(ForceCpu);
}
// Allows callers to choose CPU/GPU per use-case, decoupled from ForceCpu.
public static GpuContextLease RentPreferred(bool preferCpu)
{
if (PoolingEnabled)
{
if (preferCpu && CpuPool.TryDequeue(out var cpu))
{
return new GpuContextLease(cpu, CreationLock);
}
if (!preferCpu && GpuPool.TryDequeue(out var gpu))
{
return new GpuContextLease(gpu, CreationLock);
}
}
// Serialize accelerator creation to avoid concurrent CL device/stream setup across threads.
lock (CreationLock)
{
return new GpuContextLease(new PooledContext(preferCpu), CreationLock);
}
}
public static void DisposeAll()
{
if (!PoolingEnabled)
{
return;
}
while (CpuPool.TryDequeue(out var cpu))
{
cpu.Dispose();
}
while (GpuPool.TryDequeue(out var gpu))
{
gpu.Dispose();
}
}
private static void Return(PooledContext ctx)
{
if (PoolingEnabled)
{
ctx.Accelerator.Synchronize();
if (ctx.IsCpu)
{
CpuPool.Enqueue(ctx);
}
else
{
GpuPool.Enqueue(ctx);
}
}
else
{
lock (CreationLock)
{
ctx.Dispose();
}
}
} }
public void Dispose()
{
if (disposed)
{
return;
}
// Ensure all cached GPU buffers for this accelerator are released.
NttGpuMath.ClearCaches(Accelerator);
// Release PrimeTester GPU state for this accelerator (kernel/device primes)
// Clear any per-accelerator cached resources to avoid releasing
// after the accelerator is destroyed.
PrimeTester.ClearGpuCaches(Accelerator);
Accelerator.Dispose();
Context.Dispose();
disposed = true;
}
}
private static readonly ConcurrentQueue<PooledContext> CpuPool = new();
private static readonly ConcurrentQueue<PooledContext> GpuPool = new();
private static readonly object CreationLock = new();
public static GpuContextLease Rent()
{
return RentPreferred(ForceCpu);
}
// Allows callers to choose CPU/GPU per use-case, decoupled from ForceCpu.
public static GpuContextLease RentPreferred(bool preferCpu)
{
if (PoolingEnabled)
{
if (preferCpu && CpuPool.TryDequeue(out var cpu))
{
return new GpuContextLease(cpu, CreationLock);
}
if (!preferCpu && GpuPool.TryDequeue(out var gpu))
{
return new GpuContextLease(gpu, CreationLock);
}
}
// Serialize accelerator creation to avoid concurrent CL device/stream setup across threads.
lock (CreationLock)
{
return new GpuContextLease(new PooledContext(preferCpu), CreationLock);
}
}
public static void DisposeAll()
{
if (!PoolingEnabled)
{
return;
}
while (CpuPool.TryDequeue(out var cpu))
{
cpu.Dispose();
}
while (GpuPool.TryDequeue(out var gpu))
{
gpu.Dispose();
}
}
private static void Return(PooledContext ctx)
{
if (PoolingEnabled)
{
ctx.Accelerator.Synchronize();
if (ctx.IsCpu)
{
CpuPool.Enqueue(ctx);
}
else
{
GpuPool.Enqueue(ctx);
}
}
else
{
lock (CreationLock)
{
ctx.Dispose();
}
}
}
public struct GpuContextLease public struct GpuContextLease
{ {
private readonly PooledContext _ctx; private readonly PooledContext _ctx;
private bool disposed; private bool disposed;
internal GpuContextLease(PooledContext ctx, object kernelInitLock) internal GpuContextLease(PooledContext ctx, object kernelInitLock)
{ {
_ctx = ctx; _ctx = ctx;
KernelInitLock = kernelInitLock; KernelInitLock = kernelInitLock;
} }
public Context Context => _ctx.Context; public Context Context => _ctx.Context;
public Accelerator Accelerator => _ctx.Accelerator; public Accelerator Accelerator => _ctx.Accelerator;
public object KernelInitLock { get; } public object KernelInitLock { get; }
public object ExecutionLock => _ctx.ExecutionLock; public object ExecutionLock => _ctx.ExecutionLock;
internal void ReturnToPool()
{
if (disposed)
{
return;
}
public void Dispose() Return(_ctx);
{ disposed = true;
if (disposed) }
{
return;
}
Return(_ctx); public void Dispose()
disposed = true; {
ReturnToPool();
} }
} }
} }
+89 -32
View File
@@ -37,6 +37,7 @@ public sealed class KernelContainer
ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>? IncrementalOrder; ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>? IncrementalOrder;
public Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, public Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong,
ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>? Pow2ModOrder; ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>? Pow2ModOrder;
public Action<AcceleratorStream, Index1D, ArrayView1D<MontgomeryDivisorData, Stride1D.Dense>, ArrayView1D<int, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, int>? MontgomeryOddPowers;
// Optional device buffer with small divisor cycles (<= 4M). Index = divisor, value = cycle length. // Optional device buffer with small divisor cycles (<= 4M). Index = divisor, value = cycle length.
public MemoryBuffer1D<ulong, Stride1D.Dense>? SmallCycles; public MemoryBuffer1D<ulong, Stride1D.Dense>? SmallCycles;
@@ -75,10 +76,10 @@ public readonly struct ResiduePrimeViews(
public readonly ArrayView1D<ulong, Stride1D.Dense> LastSevenPow2 = lastSevenPow2; public readonly ArrayView1D<ulong, Stride1D.Dense> LastSevenPow2 = lastSevenPow2;
} }
public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, KernelContainer kernels) public ref struct GpuKernelLease(GpuPrimeWorkLimiter.Lease limiter, GpuContextLease gpu, KernelContainer kernels)
{ {
private bool disposedValue; private bool disposedValue;
private IDisposable? _limiter = limiter; private GpuPrimeWorkLimiter.Lease? _limiter = limiter;
private GpuContextLease? _gpu = gpu; private GpuContextLease? _gpu = gpu;
private KernelContainer _kernels = kernels; private KernelContainer _kernels = kernels;
private AcceleratorStream? _stream; private AcceleratorStream? _stream;
@@ -109,15 +110,15 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
get get
{ {
var accel = Accelerator; // avoid capturing 'this' in lambda var accel = Accelerator; // avoid capturing 'this' in lambda
lock (_gpu!.Value.KernelInitLock) // lock (_gpu!.Value.KernelInitLock)
{ // {
return KernelContainer.InitOnce(ref _kernels.Order, () => return KernelContainer.InitOnce(ref _kernels.Order, () =>
{ {
var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, ulong, ArrayView<GpuUInt128>, ArrayView<ulong>>(OrderKernelScan); var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, ulong, ArrayView<GpuUInt128>, ArrayView<ulong>>(OrderKernelScan);
var kernel = KernelUtil.GetKernel(loaded); var kernel = KernelUtil.GetKernel(loaded);
return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ulong, ulong, ArrayView<GpuUInt128>, ArrayView<ulong>>>(); return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ulong, ulong, ArrayView<GpuUInt128>, ArrayView<ulong>>>();
}); });
} // }
} }
} }
@@ -126,15 +127,15 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
get get
{ {
var accel = Accelerator; var accel = Accelerator;
lock (_gpu!.Value.KernelInitLock) // lock (_gpu!.Value.KernelInitLock)
{ // {
return KernelContainer.InitOnce(ref _kernels.Pow2Mod, () => return KernelContainer.InitOnce(ref _kernels.Pow2Mod, () =>
{ {
var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<ulong>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<uint, Stride1D.Dense>, ArrayView1D<uint, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>>(Pow2ModKernelScan); var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<ulong>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<uint, Stride1D.Dense>, ArrayView1D<uint, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>>(Pow2ModKernelScan);
var kernel = KernelUtil.GetKernel(loaded); var kernel = KernelUtil.GetKernel(loaded);
return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<ulong>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<uint, Stride1D.Dense>, ArrayView1D<uint, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>>>(); return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<ulong>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<uint, Stride1D.Dense>, ArrayView1D<uint, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>>>();
}); });
} // }
} }
} }
@@ -143,15 +144,15 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
get get
{ {
var accel = Accelerator; var accel = Accelerator;
lock (_gpu!.Value.KernelInitLock) // lock (_gpu!.Value.KernelInitLock)
{ // {
return KernelContainer.InitOnce(ref _kernels.Incremental, () => return KernelContainer.InitOnce(ref _kernels.Incremental, () =>
{ {
var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ulong, ulong, ulong, ulong, ArrayView<ulong>, ArrayView1D<ulong, Stride1D.Dense>>(IncrementalKernelScan); var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ulong, ulong, ulong, ulong, ArrayView<ulong>, ArrayView1D<ulong, Stride1D.Dense>>(IncrementalKernelScan);
var kernel = KernelUtil.GetKernel(loaded); var kernel = KernelUtil.GetKernel(loaded);
return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ulong, ulong, ulong, ulong, ArrayView<ulong>, ArrayView1D<ulong, Stride1D.Dense>>>(); return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ulong, ulong, ulong, ulong, ArrayView<ulong>, ArrayView1D<ulong, Stride1D.Dense>>>();
}); });
} // }
} }
} }
@@ -160,15 +161,15 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
get get
{ {
var accel = Accelerator; var accel = Accelerator;
lock (_gpu!.Value.KernelInitLock) // lock (_gpu!.Value.KernelInitLock)
{ // {
return KernelContainer.InitOnce(ref _kernels.IncrementalOrder, () => return KernelContainer.InitOnce(ref _kernels.IncrementalOrder, () =>
{ {
var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>(IncrementalOrderKernelScan); var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>(IncrementalOrderKernelScan);
var kernel = KernelUtil.GetKernel(loaded); var kernel = KernelUtil.GetKernel(loaded);
return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>>(); return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>>();
}); });
} // }
} }
} }
@@ -177,15 +178,66 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
get get
{ {
var accel = Accelerator; var accel = Accelerator;
lock (_gpu!.Value.KernelInitLock) // lock (_gpu!.Value.KernelInitLock)
{ // {
return KernelContainer.InitOnce(ref _kernels.Pow2ModOrder, () => return KernelContainer.InitOnce(ref _kernels.Pow2ModOrder, () =>
{ {
var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>(Pow2ModOrderKernelScan); var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>(Pow2ModOrderKernelScan);
var kernel = KernelUtil.GetKernel(loaded); var kernel = KernelUtil.GetKernel(loaded);
return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>>(); return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>>();
}); });
} // }
}
}
public readonly Action<AcceleratorStream, Index1D, ArrayView1D<MontgomeryDivisorData, Stride1D.Dense>, ArrayView1D<int, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, int> MontgomeryOddPowerKernel
{
get
{
var accel = Accelerator;
// lock (_gpu!.Value.KernelInitLock)
// {
return KernelContainer.InitOnce(ref _kernels.MontgomeryOddPowers, () =>
{
var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<MontgomeryDivisorData, Stride1D.Dense>, ArrayView1D<int, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, int>(MontgomeryOddPowerKernelScan);
var kernel = KernelUtil.GetKernel(loaded);
return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ArrayView1D<MontgomeryDivisorData, Stride1D.Dense>, ArrayView1D<int, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, int>>();
});
// }
}
}
private static void MontgomeryOddPowerKernelScan(
Index1D index,
ArrayView1D<MontgomeryDivisorData, Stride1D.Dense> divisors,
ArrayView1D<int, Stride1D.Dense> counts,
ArrayView1D<ulong, Stride1D.Dense> results,
int stride)
{
int tableIndex = index.X;
int baseOffset = tableIndex * stride;
MontgomeryDivisorData divisor = divisors[tableIndex];
int oddPowerCount = counts[tableIndex];
if (oddPowerCount <= 0)
{
return;
}
ulong montgomeryTwo = divisor.MontgomeryTwo;
results[baseOffset] = montgomeryTwo;
if (oddPowerCount == 1)
{
return;
}
ulong modulus = divisor.Modulus;
ulong nPrime = divisor.NPrime;
ulong square = divisor.MontgomeryTwoSquared;
ulong current = montgomeryTwo;
for (int i = 1; i < oddPowerCount; i++)
{
current = ULongExtensions.MontgomeryMultiply(current, square, modulus, nPrime);
results[baseOffset + i] = current;
} }
} }
@@ -344,7 +396,10 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
_stream?.Dispose(); _stream?.Dispose();
_stream = null; _stream = null;
_gpu?.Dispose(); if (_gpu.HasValue)
{
_gpu.Value.ReturnToPool();
}
_gpu = null; _gpu = null;
_executionLock = null; _executionLock = null;
@@ -684,18 +739,18 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
public ExecutionScope(object? sync) public ExecutionScope(object? sync)
{ {
_lock = sync; _lock = sync;
if (_lock is not null) // if (_lock is not null)
{ // {
Monitor.Enter(_lock); // Monitor.Enter(_lock);
} // }
} }
public void Dispose() public void Dispose()
{ {
if (_lock is not null) // if (_lock is not null)
{ // {
Monitor.Exit(_lock); // Monitor.Exit(_lock);
} // }
} }
} }
} }
@@ -704,6 +759,8 @@ public class GpuKernelPool
{ {
private static readonly ConcurrentDictionary<Accelerator, KernelContainer> KernelCache = new(); // TODO: Replace this concurrent map with a simple accelerator-indexed lookup once kernel launchers are prewarmed during startup so we can drop the thread-safe wrapper entirely. private static readonly ConcurrentDictionary<Accelerator, KernelContainer> KernelCache = new(); // TODO: Replace this concurrent map with a simple accelerator-indexed lookup once kernel launchers are prewarmed during startup so we can drop the thread-safe wrapper entirely.
public static bool CpuCycleUsesGpu { get; set; }
private static KernelContainer GetKernels(Accelerator accelerator) private static KernelContainer GetKernels(Accelerator accelerator)
{ {
return KernelCache.GetOrAdd(accelerator, _ => new KernelContainer()); return KernelCache.GetOrAdd(accelerator, _ => new KernelContainer());
@@ -720,8 +777,8 @@ public class GpuKernelPool
} }
// Ensure single upload per accelerator even if multiple threads race here. // Ensure single upload per accelerator even if multiple threads race here.
lock (kernels) // TODO: Remove this lock by pre-uploading the immutable small-cycle snapshot during initialization; once no mutation happens at runtime, the pool must expose a simple reference without synchronization. // lock (kernels) // TODO: Remove this lock by pre-uploading the immutable small-cycle snapshot during initialization; once no mutation happens at runtime, the pool must expose a simple reference without synchronization.
{ // {
if (kernels.SmallCycles is { } existing) if (kernels.SmallCycles is { } existing)
{ {
return existing.View; return existing.View;
@@ -732,7 +789,7 @@ public class GpuKernelPool
device.View.CopyFromCPU(host); device.View.CopyFromCPU(host);
kernels.SmallCycles = device; kernels.SmallCycles = device;
return device.View; return device.View;
} // }
} }
public static ResiduePrimeViews EnsureSmallPrimesOnDevice(Accelerator accelerator) public static ResiduePrimeViews EnsureSmallPrimesOnDevice(Accelerator accelerator)
@@ -746,8 +803,8 @@ public class GpuKernelPool
return new ResiduePrimeViews(lastOne.View, lastSeven.View, lastOnePow2.View, lastSevenPow2.View); return new ResiduePrimeViews(lastOne.View, lastSeven.View, lastOnePow2.View, lastSevenPow2.View);
} }
lock (kernels) // TODO: Inline these small-prime uploads into startup initialization alongside the small-cycle snapshot so we can drop runtime locking and keep the GPU pool free of synchronization. // lock (kernels) // TODO: Inline these small-prime uploads into startup initialization alongside the small-cycle snapshot so we can drop runtime locking and keep the GPU pool free of synchronization.
{ // {
if (kernels.SmallPrimesLastOne is { } existingLastOne && if (kernels.SmallPrimesLastOne is { } existingLastOne &&
kernels.SmallPrimesLastSeven is { } existingLastSeven && kernels.SmallPrimesLastSeven is { } existingLastSeven &&
kernels.SmallPrimesPow2LastOne is { } existingLastOnePow2 && kernels.SmallPrimesPow2LastOne is { } existingLastOnePow2 &&
@@ -776,7 +833,7 @@ public class GpuKernelPool
kernels.SmallPrimesPow2LastSeven = deviceLastSevenPow2; kernels.SmallPrimesPow2LastSeven = deviceLastSevenPow2;
return new ResiduePrimeViews(deviceLastOne.View, deviceLastSeven.View, deviceLastOnePow2.View, deviceLastSevenPow2.View); return new ResiduePrimeViews(deviceLastOne.View, deviceLastSeven.View, deviceLastOnePow2.View, deviceLastSevenPow2.View);
} // }
} }
public static GpuKernelLease GetKernel(bool useGpuOrder) public static GpuKernelLease GetKernel(bool useGpuOrder)
@@ -6,13 +6,13 @@ public static class GpuPrimeWorkLimiter
private static readonly object _lock = new(); private static readonly object _lock = new();
private static int _currentLimit = 1; private static int _currentLimit = 1;
public static IDisposable Acquire() public static Lease Acquire()
{ {
// TODO: Inline the pooled limiter guard from the GpuLimiterThroughputBenchmarks so prime-sieve // TODO: Inline the pooled limiter guard from the GpuLimiterThroughputBenchmarks so prime-sieve
// GPU jobs stop allocating Releaser instances on every acquisition. // GPU jobs stop allocating Releaser instances on every acquisition.
var sem = _semaphore; var sem = _semaphore;
sem.Wait(); sem.Wait();
return new Releaser(sem); return new Lease(sem);
} }
public static void SetLimit(int value) public static void SetLimit(int value)
@@ -36,12 +36,12 @@ public static class GpuPrimeWorkLimiter
} }
} }
private sealed class Releaser : IDisposable public sealed class Lease
{ {
private readonly SemaphoreSlim _sem; private readonly SemaphoreSlim _sem;
private bool _disposed; private bool _disposed;
public Releaser(SemaphoreSlim sem) public Lease(SemaphoreSlim sem)
{ {
_sem = sem; _sem = sem;
} }
+4 -4
View File
@@ -6,14 +6,14 @@ public static class GpuWorkLimiter
private static readonly object _lock = new(); private static readonly object _lock = new();
private static int _currentLimit = 1; private static int _currentLimit = 1;
public static IDisposable Acquire() public static Lease Acquire()
{ {
// TODO: Replace the per-call Releaser allocation with the pooled struct-based guard from the // TODO: Replace the per-call Releaser allocation with the pooled struct-based guard from the
// GpuLimiterThroughputBenchmarks so limiter acquisition does not allocate when we enter the // GpuLimiterThroughputBenchmarks so limiter acquisition does not allocate when we enter the
// GPU scanning hot path. // GPU scanning hot path.
var sem = _semaphore; var sem = _semaphore;
sem.Wait(); sem.Wait();
return new Releaser(sem); return new Lease(sem);
} }
public static void SetLimit(int value) public static void SetLimit(int value)
@@ -40,12 +40,12 @@ public static class GpuWorkLimiter
} }
} }
private sealed class Releaser : IDisposable public sealed class Lease
{ {
private readonly SemaphoreSlim _sem; private readonly SemaphoreSlim _sem;
private bool _disposed; private bool _disposed;
public Releaser(SemaphoreSlim sem) public Lease(SemaphoreSlim sem)
{ {
_sem = sem; _sem = sem;
} }
@@ -1,13 +1,13 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Numerics; using System.Numerics;
using ILGPU; using ILGPU;
using ILGPU.Runtime; using ILGPU.Runtime;
using MontgomeryDivisorData = PerfectNumbers.Core.MontgomeryDivisorData; using MontgomeryDivisorData = PerfectNumbers.Core.MontgomeryDivisorData;
using MontgomeryDivisorDataCache = PerfectNumbers.Core.MontgomeryDivisorDataCache;
using CycleRemainderStepper = PerfectNumbers.Core.CycleRemainderStepper; using CycleRemainderStepper = PerfectNumbers.Core.CycleRemainderStepper;
namespace PerfectNumbers.Core.Gpu; namespace PerfectNumbers.Core.Gpu;
@@ -20,7 +20,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
private const int RemainderSlotCount = 4; private const int RemainderSlotCount = 4;
private int _gpuBatchSize = GpuConstants.ScanBatchSize; private int _gpuBatchSize = GpuConstants.ScanBatchSize;
private readonly object _sync = new();
private ulong _divisorLimit; private ulong _divisorLimit;
private bool _isConfigured; private bool _isConfigured;
@@ -36,7 +35,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
_kernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel<Index1D, ArrayView<MontgomeryDivisorData>, ArrayView<ulong>, ArrayView<byte>>(CheckKernel)); _kernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel<Index1D, ArrayView<MontgomeryDivisorData>, ArrayView<ulong>, ArrayView<byte>>(CheckKernel));
private Action<Index1D, MontgomeryDivisorData, ArrayView<ulong>, ArrayView<ulong>> GetKernelByPrimeExponent(Accelerator accelerator) => private Action<Index1D, MontgomeryDivisorData, ArrayView<ulong>, ArrayView<ulong>> GetKernelByPrimeExponent(Accelerator accelerator) =>
_kernelExponentCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel<Index1D, MontgomeryDivisorData, ArrayView<ulong>, ArrayView<ulong>>(ComputeMontgomeryExponentKernel)); _kernelExponentCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel<Index1D, MontgomeryDivisorData, ArrayView<ulong>, ArrayView<ulong>>(ComputeExponentKernel));
private Action<Index1D, ArrayView<ulong>, ArrayView<byte>, byte> GetRemainderDeltaKernel(Accelerator accelerator) => private Action<Index1D, ArrayView<ulong>, ArrayView<byte>, byte> GetRemainderDeltaKernel(Accelerator accelerator) =>
_remainderDeltaKernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ArrayView<byte>, byte>(ComputeRemainderDeltasKernel)); _remainderDeltaKernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ArrayView<byte>, byte>(ComputeRemainderDeltasKernel));
@@ -65,34 +64,30 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
public void ConfigureFromMaxPrime(ulong maxPrime) public void ConfigureFromMaxPrime(ulong maxPrime)
{ {
lock (_sync) // Configuration happens once during startup, so no synchronization is required to cache the divisor limit.
{ _divisorLimit = ComputeDivisorLimitFromMaxPrime(maxPrime);
_divisorLimit = ComputeDivisorLimitFromMaxPrime(maxPrime); _isConfigured = true;
_isConfigured = true;
}
} }
public bool IsPrime(ulong prime, out bool divisorsExhausted) public bool IsPrime(ulong prime, out bool divisorsExhausted, TimeSpan? timeLimit = null)
{ {
ulong allowedMax; // The CLI always configures the tester during startup, so the historical guard is commented out to avoid branching.
int batchCapacity; // if (!_isConfigured)
// {
// throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
// }
lock (_sync) // Configuration is immutable, so the cached values can be read without locking.
{ ulong allowedMax = ComputeAllowedMaxDivisor(prime, _divisorLimit);
if (!_isConfigured) int batchCapacity = _gpuBatchSize;
{
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
}
allowedMax = ComputeAllowedMaxDivisor(prime, _divisorLimit); // The GPU path always receives an allowed maximum well above the minimal divisor, so the legacy
batchCapacity = _gpuBatchSize; // guard stays commented out to document the invariant without branching.
} // if (allowedMax < 3UL)
// {
if (allowedMax < 3UL) // divisorsExhausted = true;
{ // return true;
divisorsExhausted = true; // }
return true;
}
bool composite; bool composite;
bool coveredRange; bool coveredRange;
@@ -109,6 +104,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
composite = CheckDivisors( composite = CheckDivisors(
prime, prime,
allowedMax, allowedMax,
timeLimit,
accelerator, accelerator,
kernel, kernel,
resources.DivisorsBuffer, resources.DivisorsBuffer,
@@ -127,6 +123,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
resources.CycleCandidates, resources.CycleCandidates,
resources.CycleLengths, resources.CycleLengths,
resources.CycleCapacity, resources.CycleCapacity,
resources,
out lastProcessed, out lastProcessed,
out coveredRange, out coveredRange,
out processedCount out processedCount
@@ -155,18 +152,14 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
throw new ArgumentException("allowedMaxValues span must be at least as long as primes span.", nameof(allowedMaxValues)); throw new ArgumentException("allowedMaxValues span must be at least as long as primes span.", nameof(allowedMaxValues));
} }
ulong divisorLimit; // The GPU pipeline calls ConfigureFromMaxPrime before preparing candidates, so the legacy guard
// remains commented out to document the invariant without branching.
lock (_sync) // if (!_isConfigured)
{ // {
if (!_isConfigured) // throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
{ // }
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
}
divisorLimit = _divisorLimit;
}
ulong divisorLimit = _divisorLimit;
for (int index = 0; index < primes.Length; index++) for (int index = 0; index < primes.Length; index++)
{ {
allowedMaxValues[index] = ComputeAllowedMaxDivisor(primes[index], divisorLimit); allowedMaxValues[index] = ComputeAllowedMaxDivisor(primes[index], divisorLimit);
@@ -176,6 +169,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
private static bool CheckDivisors( private static bool CheckDivisors(
ulong prime, ulong prime,
ulong allowedMax, ulong allowedMax,
TimeSpan? timeLimit,
Accelerator accelerator, Accelerator accelerator,
Action<Index1D, ArrayView<MontgomeryDivisorData>, ArrayView<ulong>, ArrayView<byte>> kernel, Action<Index1D, ArrayView<MontgomeryDivisorData>, ArrayView<ulong>, ArrayView<byte>> kernel,
MemoryBuffer1D<MontgomeryDivisorData, Stride1D.Dense> divisorsBuffer, MemoryBuffer1D<MontgomeryDivisorData, Stride1D.Dense> divisorsBuffer,
@@ -194,6 +188,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
ulong[] cycleCandidates, ulong[] cycleCandidates,
ulong[] cycleLengths, ulong[] cycleLengths,
int cycleCapacity, int cycleCapacity,
BatchResources resources,
out ulong lastProcessed, out ulong lastProcessed,
out bool coveredRange, out bool coveredRange,
out ulong processedCount) out ulong processedCount)
@@ -203,6 +198,15 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
bool processedAll = false; bool processedAll = false;
processedCount = 0UL; processedCount = 0UL;
lastProcessed = 0UL; lastProcessed = 0UL;
coveredRange = false;
PrimeTestTimeLimit limitGuard;
if (!PrimeTestTimeLimit.TryCreate(timeLimit, out limitGuard))
{
return false;
}
bool enforceLimit = limitGuard.IsActive;
Span<MontgomeryDivisorData> divisorDataSpan; Span<MontgomeryDivisorData> divisorDataSpan;
Span<ulong> divisorSpan; Span<ulong> divisorSpan;
@@ -212,7 +216,8 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
ArrayView1D<ulong, Stride1D.Dense> exponentView; ArrayView1D<ulong, Stride1D.Dense> exponentView;
ArrayView1D<byte, Stride1D.Dense> hitsView; ArrayView1D<byte, Stride1D.Dense> hitsView;
cycleCapacity = Math.Max(1, cycleCapacity); // Batch resources expose a positive cycle capacity, so skip the redundant clamp here.
// cycleCapacity = Math.Max(1, cycleCapacity);
int chunkCapacity = Math.Min(batchCapacity, cycleCapacity); int chunkCapacity = Math.Min(batchCapacity, cycleCapacity);
int maxThreadsPerGroup = (int)accelerator.MaxNumThreadsPerGroup; int maxThreadsPerGroup = (int)accelerator.MaxNumThreadsPerGroup;
@@ -221,20 +226,22 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
chunkCapacity = Math.Min(chunkCapacity, maxThreadsPerGroup); chunkCapacity = Math.Min(chunkCapacity, maxThreadsPerGroup);
} }
if (chunkCapacity <= 0) // cycleCapacity already bounds chunkCapacity above zero, so the fallback would never execute.
{ // if (chunkCapacity <= 0)
chunkCapacity = cycleCapacity; // {
} // chunkCapacity = cycleCapacity;
// }
ulong[] filteredDivisors = ArrayPool<ulong>.Shared.Rent(chunkCapacity); ulong[] filteredDivisors = ArrayPool<ulong>.Shared.Rent(chunkCapacity);
ulong[] divisorGaps = ArrayPool<ulong>.Shared.Rent(chunkCapacity); ulong[] divisorGaps = ArrayPool<ulong>.Shared.Rent(chunkCapacity);
UInt128 twoP128 = (UInt128)prime << 1; UInt128 twoP128 = (UInt128)prime << 1;
if (twoP128 == UInt128.Zero) // Primes above one yield a non-zero 2p value, so the legacy zero guard remains commented out.
{ // if (twoP128 == UInt128.Zero)
coveredRange = true; // {
return false; // coveredRange = true;
} // return false;
// }
UInt128 allowedMax128 = allowedMax; UInt128 allowedMax128 = allowedMax;
UInt128 firstDivisor128 = twoP128 + UInt128.One; UInt128 firstDivisor128 = twoP128 + UInt128.One;
@@ -245,11 +252,13 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
} }
UInt128 maxK128 = (allowedMax128 - UInt128.One) / twoP128; UInt128 maxK128 = (allowedMax128 - UInt128.One) / twoP128;
if (maxK128 == UInt128.Zero) // Once the first divisor falls within the allowed range, at least one multiplier k is admissible,
{ // so keep the defensive zero check commented out.
coveredRange = true; // if (maxK128 == UInt128.Zero)
return false; // {
} // coveredRange = true;
// return false;
// }
ulong maxK = maxK128 > ulong.MaxValue ? ulong.MaxValue : (ulong)maxK128; ulong maxK = maxK128 > ulong.MaxValue ? ulong.MaxValue : (ulong)maxK128;
ulong currentK = 1UL; ulong currentK = 1UL;
@@ -278,6 +287,12 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
{ {
while (currentK <= maxK) while (currentK <= maxK)
{ {
if (enforceLimit && limitGuard.HasExpired())
{
processedAll = false;
return false;
}
int chunkCount = Math.Min(chunkCapacity, batchCapacity); int chunkCount = Math.Min(chunkCapacity, batchCapacity);
ulong remainingK = maxK - currentK + 1UL; ulong remainingK = maxK - currentK + 1UL;
if ((ulong)chunkCount > remainingK) if ((ulong)chunkCount > remainingK)
@@ -285,10 +300,12 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
chunkCount = (int)remainingK; chunkCount = (int)remainingK;
} }
if (chunkCount <= 0) // chunkCapacity and the remaining multiplier range are strictly positive here, so the zero
{ // chunk fallback never triggers on production scans.
break; // if (chunkCount <= 0)
} // {
// break;
// }
Span<ulong> candidateSpan = cycleCandidates.AsSpan(0, chunkCount); Span<ulong> candidateSpan = cycleCandidates.AsSpan(0, chunkCount);
Span<ulong> gapSpan = divisorGaps.AsSpan(0, chunkCount); Span<ulong> gapSpan = divisorGaps.AsSpan(0, chunkCount);
@@ -326,10 +343,10 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
gapView.CopyFromCPU(ref MemoryMarshal.GetReference(gapSpan), chunkCount); gapView.CopyFromCPU(ref MemoryMarshal.GetReference(gapSpan), chunkCount);
ComputeRemaindersOnGpu(chunkCount, remainder10, 10, gapView, deltaView, remainder10View, remainderDeltaKernel, remainderScanKernel); ComputeRemaindersOnGpu(chunkCount, remainder10, 10, gapView, deltaView, remainder10View, remainderDeltaKernel, remainderScanKernel, resources.GetRemainderScanConfig);
ComputeRemaindersOnGpu(chunkCount, remainder8, 8, gapView, deltaView, remainder8View, remainderDeltaKernel, remainderScanKernel); ComputeRemaindersOnGpu(chunkCount, remainder8, 8, gapView, deltaView, remainder8View, remainderDeltaKernel, remainderScanKernel, resources.GetRemainderScanConfig);
ComputeRemaindersOnGpu(chunkCount, remainder5, 5, gapView, deltaView, remainder5View, remainderDeltaKernel, remainderScanKernel); ComputeRemaindersOnGpu(chunkCount, remainder5, 5, gapView, deltaView, remainder5View, remainderDeltaKernel, remainderScanKernel, resources.GetRemainderScanConfig);
ComputeRemaindersOnGpu(chunkCount, remainder3, 3, gapView, deltaView, remainder3View, remainderDeltaKernel, remainderScanKernel); ComputeRemaindersOnGpu(chunkCount, remainder3, 3, gapView, deltaView, remainder3View, remainderDeltaKernel, remainderScanKernel, resources.GetRemainderScanConfig);
BuildCandidateMaskOnGpu(chunkCount, remainder10View, remainder8View, remainder5View, remainder3View, lastIsSevenFlag, maskView, maskSpan, candidateMaskKernel); BuildCandidateMaskOnGpu(chunkCount, remainder10View, remainder8View, remainder5View, remainder3View, lastIsSevenFlag, maskView, maskSpan, candidateMaskKernel);
@@ -396,6 +413,8 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
ulong divisorValue = filteredDivisorsSpan[i]; ulong divisorValue = filteredDivisorsSpan[i];
divisorSpan[i] = divisorValue; divisorSpan[i] = divisorValue;
exponentSpan[i] = prime; exponentSpan[i] = prime;
// Divisors never repeat while scanning the monotonically increasing sequence, so regenerate
// the Montgomery data for each entry rather than storing cache lines that no future batch would hit.
divisorDataSpan[i] = MontgomeryDivisorData.FromModulus(divisorValue); divisorDataSpan[i] = MontgomeryDivisorData.FromModulus(divisorValue);
} }
@@ -445,18 +464,18 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
ArrayView1D<byte, Stride1D.Dense> deltaView, ArrayView1D<byte, Stride1D.Dense> deltaView,
ArrayView1D<byte, Stride1D.Dense> remainderView, ArrayView1D<byte, Stride1D.Dense> remainderView,
Action<Index1D, ArrayView<ulong>, ArrayView<byte>, byte> deltaKernel, Action<Index1D, ArrayView<ulong>, ArrayView<byte>, byte> deltaKernel,
Action<KernelConfig, Index1D, ArrayView<byte>, ArrayView<byte>, byte, byte> scanKernel) Action<KernelConfig, Index1D, ArrayView<byte>, ArrayView<byte>, byte, byte> scanKernel,
Func<int, KernelConfig> configProvider)
{ {
if (count <= 0) // GPU batches always contain at least one divisor gap, so the empty-count guard remains commented
{ // to eliminate redundant branching.
return; // if (count <= 0)
} // {
// return;
// }
deltaKernel(count, gapView, deltaView, modulus); deltaKernel(count, gapView, deltaView, modulus);
SharedMemoryConfig sharedMemoryConfig = SharedMemoryConfig.RequestDynamic<int>(count); KernelConfig kernelConfig = configProvider(count);
Index1D gridDim = new Index1D(1);
Index1D groupDim = new Index1D(count);
KernelConfig kernelConfig = new KernelConfig(gridDim, groupDim, sharedMemoryConfig);
scanKernel(kernelConfig, new Index1D(count), deltaView, remainderView, baseRemainder, modulus); scanKernel(kernelConfig, new Index1D(count), deltaView, remainderView, baseRemainder, modulus);
} }
@@ -473,11 +492,13 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
Span<byte> destination, Span<byte> destination,
Action<Index1D, ArrayView<byte>, ArrayView<byte>, ArrayView<byte>, ArrayView<byte>, byte, ArrayView<byte>> maskKernel) Action<Index1D, ArrayView<byte>, ArrayView<byte>, ArrayView<byte>, ArrayView<byte>, byte, ArrayView<byte>> maskKernel)
{ {
if (count <= 0) // Candidate batches always provide at least one entry, so the zero-count guard stays commented out
{ // to avoid clearing spans unnecessarily.
destination.Clear(); // if (count <= 0)
return; // {
} // destination.Clear();
// return;
// }
maskKernel(count, remainder10View, remainder8View, remainder5View, remainder3View, lastIsSevenFlag, maskView); maskKernel(count, remainder10View, remainder8View, remainder5View, remainder3View, lastIsSevenFlag, maskView);
maskView.CopyToCPU(ref MemoryMarshal.GetReference(destination), count); maskView.CopyToCPU(ref MemoryMarshal.GetReference(destination), count);
@@ -485,10 +506,12 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
private static byte FetchLastRemainder(ArrayView1D<byte, Stride1D.Dense> remainderView, int count) private static byte FetchLastRemainder(ArrayView1D<byte, Stride1D.Dense> remainderView, int count)
{ {
if (count <= 0) // Kernels always process at least one remainder in this path, so the empty-span guard remains
{ // commented to document the invariant.
return 0; // if (count <= 0)
} // {
// return 0;
// }
byte value = 0; byte value = 0;
remainderView.SubView(count - 1, 1).CopyToCPU(ref value, 1); remainderView.SubView(count - 1, 1).CopyToCPU(ref value, 1);
@@ -501,10 +524,11 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
if (result >= modulus) if (result >= modulus)
{ {
result -= modulus; result -= modulus;
if (result >= modulus) // The step deltas remain below the modulus, so a single subtraction suffices on this path.
{ // if (result >= modulus)
result %= modulus; // {
} // result %= modulus;
// }
} }
return (byte)result; return (byte)result;
@@ -512,46 +536,60 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
private static ulong ComputeDivisorLimitFromMaxPrime(ulong maxPrime) private static ulong ComputeDivisorLimitFromMaxPrime(ulong maxPrime)
{ {
if (maxPrime <= 1UL) // The production configuration seeds the scan with exponents above the first odd prime, so the defensive guard remains
{ // commented to document the invariant without branching.
return 0UL; // if (maxPrime <= 1UL)
} // {
// return 0UL;
// }
if (maxPrime - 1UL >= 64UL) // Real workloads push maxPrime well past the 64-bit shift boundary, so keep the historical implementation commented out
{ // and return ulong.MaxValue directly.
return ulong.MaxValue; // if (maxPrime - 1UL >= 64UL)
} // {
// return ulong.MaxValue;
// }
//
// return (1UL << (int)(maxPrime - 1UL)) - 1UL;
return (1UL << (int)(maxPrime - 1UL)) - 1UL; return ulong.MaxValue;
} }
private static ulong ComputeAllowedMaxDivisor(ulong prime, ulong divisorLimit) private static ulong ComputeAllowedMaxDivisor(ulong prime, ulong divisorLimit)
{ {
if (prime <= 1UL) // The by-divisor search emits candidate exponents starting at the first odd prime, so the defensive guard remains
{ // commented to document that invariant.
return 0UL; // if (prime <= 1UL)
} // {
// return 0UL;
// }
if (prime - 1UL >= 64UL) // Production exponents are always large enough that the shift-based clamp would overflow, so skip the legacy branch and
{ // return the configured divisor limit directly.
return divisorLimit; // if (prime - 1UL >= 64UL)
} // {
// return divisorLimit;
// }
//
// return Math.Min((1UL << (int)(prime - 1UL)) - 1UL, divisorLimit);
return Math.Min((1UL << (int)(prime - 1UL)) - 1UL, divisorLimit); return divisorLimit;
} }
private static void CheckKernel(Index1D index, ArrayView<MontgomeryDivisorData> divisors, ArrayView<ulong> exponents, ArrayView<byte> hits) private static void CheckKernel(Index1D index, ArrayView<MontgomeryDivisorData> divisors, ArrayView<ulong> exponents, ArrayView<byte> hits)
{ {
MontgomeryDivisorData divisor = divisors[index]; MontgomeryDivisorData divisor = divisors[index];
ulong modulus = divisor.Modulus; ulong modulus = divisor.Modulus;
if (modulus <= 1UL || (modulus & 1UL) == 0UL) // The divisor staging path filters out non-prime and even moduli before they reach the device, so this guard
{ // would never fire for EvenPerfectBitScanner workloads.
hits[index] = 0; // if (modulus <= 1UL || (modulus & 1UL) == 0UL)
return; // {
} // hits[index] = 0;
// return;
// }
ulong exponent = exponents[index]; ulong exponent = exponents[index];
hits[index] = exponent.Pow2MontgomeryModWindowedGpu(divisor, keepMontgomery: false) == 1UL ? (byte)1 : (byte)0; hits[index] = exponent.Pow2ModWindowedGpu(modulus) == 1UL ? (byte)1 : (byte)0;
} }
public sealed class DivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession public sealed class DivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession
@@ -591,16 +629,20 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
} }
int length = primes.Length; int length = primes.Length;
if (length == 0) // Production batches always contain at least one exponent, so the empty-span guard remains commented out to remove
{ // the redundant branch.
return; // if (length == 0)
} // {
// return;
// }
if (divisorData.Modulus <= 1UL || (divisorData.Modulus & 1UL) == 0UL) // The divisor staging path filters non-prime and even moduli before kernels launch, so keep the defensive
{ // modulus guard commented out to avoid branching in the hot loop.
hits.Clear(); // if (divisorData.Modulus <= 1UL || (divisorData.Modulus & 1UL) == 0UL)
return; // {
} // hits.Clear();
// return;
// }
ArrayView1D<ulong, Stride1D.Dense> exponentsView = _exponentsBuffer.View; ArrayView1D<ulong, Stride1D.Dense> exponentsView = _exponentsBuffer.View;
ArrayView1D<ulong, Stride1D.Dense> resultsView = _resultsBuffer.View; ArrayView1D<ulong, Stride1D.Dense> resultsView = _resultsBuffer.View;
@@ -759,33 +801,34 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
mask[globalIndex] = 1; mask[globalIndex] = 1;
} }
private static void ComputeMontgomeryExponentKernel(Index1D index, MontgomeryDivisorData divisor, ArrayView<ulong> exponents, ArrayView<ulong> results) private static void ComputeExponentKernel(Index1D index, MontgomeryDivisorData divisor, ArrayView<ulong> exponents, ArrayView<ulong> results)
{ {
ulong modulus = divisor.Modulus; ulong modulus = divisor.Modulus;
if (modulus <= 1UL || (modulus & 1UL) == 0UL) // Kernel inputs are constrained to odd prime moduli, so this defensive branch would never execute on the configured
{ // scanning path.
results[index] = 0UL; // if (modulus <= 1UL || (modulus & 1UL) == 0UL)
return; // {
} // results[index] = 0UL;
// return;
// }
ulong exponent = exponents[index]; ulong exponent = exponents[index];
results[index] = exponent.Pow2MontgomeryModWindowedGpu(divisor, keepMontgomery: true); results[index] = exponent.Pow2ModWindowedGpu(modulus);
} }
public ulong DivisorLimit public ulong DivisorLimit
{ {
get get
{ {
lock (_sync) // The by-divisor scanner always configures the tester before exposing the limit, so the guard remains
{ // commented out to document the invariant without forcing a runtime branch.
if (!_isConfigured) // if (!_isConfigured)
{ // {
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester."); // throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
} // }
return _divisorLimit; return _divisorLimit;
}
} }
} }
@@ -802,15 +845,17 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
private void ReturnBatchResources(Accelerator accelerator, BatchResources resources) => _resourcePools.GetOrAdd(accelerator, static _ => []).Add(resources); private void ReturnBatchResources(Accelerator accelerator, BatchResources resources) => _resourcePools.GetOrAdd(accelerator, static _ => []).Add(resources);
private sealed class BatchResources : IDisposable private sealed class BatchResources
{ {
internal BatchResources(MersenneNumberDivisorByDivisorGpuTester owner, Accelerator accelerator, int capacity) internal BatchResources(MersenneNumberDivisorByDivisorGpuTester owner, Accelerator accelerator, int capacity)
{ {
int cycleCapacity = DivisorCycleCache.Shared.PreferredBatchSize; int cycleCapacity = DivisorCycleCache.Shared.PreferredBatchSize;
if (cycleCapacity <= 0) // The shared cache always reports a positive preferred size on production workloads, so the fallback remains
{ // commented out to avoid branching during resource initialization.
cycleCapacity = 1; // if (cycleCapacity <= 0)
} // {
// cycleCapacity = 1;
// }
int actualCapacity = Math.Max(capacity, cycleCapacity); int actualCapacity = Math.Max(capacity, cycleCapacity);
@@ -862,10 +907,24 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
internal readonly Action<KernelConfig, Index1D, ArrayView<byte>, ArrayView<byte>, byte, byte> RemainderScanKernel; internal readonly Action<KernelConfig, Index1D, ArrayView<byte>, ArrayView<byte>, byte, byte> RemainderScanKernel;
internal readonly Action<Index1D, ArrayView<byte>, ArrayView<byte>, ArrayView<byte>, ArrayView<byte>, byte, ArrayView<byte>> CandidateMaskKernel; internal readonly Action<Index1D, ArrayView<byte>, ArrayView<byte>, ArrayView<byte>, ArrayView<byte>, byte, ArrayView<byte>> CandidateMaskKernel;
private readonly Dictionary<int, KernelConfig> _remainderScanConfigs = new();
internal readonly int Capacity; internal readonly int Capacity;
internal readonly int CycleCapacity; internal readonly int CycleCapacity;
internal KernelConfig GetRemainderScanConfig(int count)
{
if (!_remainderScanConfigs.TryGetValue(count, out KernelConfig config))
{
SharedMemoryConfig sharedMemoryConfig = SharedMemoryConfig.RequestDynamic<int>(count);
config = new KernelConfig(new Index1D(1), new Index1D(count), sharedMemoryConfig);
_remainderScanConfigs[count] = config;
}
return config;
}
public void Dispose() public void Dispose()
{ {
DivisorsBuffer.Dispose(); DivisorsBuffer.Dispose();
@@ -894,37 +953,33 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
public ulong GetAllowedMaxDivisor(ulong prime) public ulong GetAllowedMaxDivisor(ulong prime)
{ {
lock (_sync) // The CLI configures the GPU tester once at startup, so callers never request divisor limits before
{ // initialization completes.
if (!_isConfigured) // if (!_isConfigured)
{ // {
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester."); // throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
} // }
return ComputeAllowedMaxDivisor(prime, _divisorLimit); return ComputeAllowedMaxDivisor(prime, _divisorLimit);
}
} }
public DivisorScanSession CreateDivisorSession() public IMersenneNumberDivisorByDivisorTester.IDivisorScanSession CreateDivisorSession()
{ {
lock (_sync) // Divisor sessions are handed out only after configuration, so this guard would never trigger on the
// production path.
// if (!_isConfigured)
// {
// throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
// }
if (_sessionPool.TryTake(out DivisorScanSession? session))
{ {
if (!_isConfigured) session.Reset();
{ return session;
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
}
if (_sessionPool.TryTake(out DivisorScanSession? session))
{
session.Reset();
return session;
}
return new DivisorScanSession(this);
} }
}
IMersenneNumberDivisorByDivisorTester.IDivisorScanSession IMersenneNumberDivisorByDivisorTester.CreateDivisorSession() => CreateDivisorSession(); return new DivisorScanSession(this);
}
} }
@@ -0,0 +1,273 @@
using System;
using System.Buffers;
using System.Runtime.InteropServices;
using ILGPU;
using ILGPU.Runtime;
namespace PerfectNumbers.Core.Gpu;
internal sealed class MersenneNumberDivisorCandidateGpuEvaluator
{
private const int RemainderCount = 6;
private readonly GpuContextPool.GpuContextLease _lease;
private readonly Accelerator _accelerator;
private readonly Action<Index1D, CandidateFilterArgs, ArrayView<ulong>, ArrayView<byte>> _kernel;
private MemoryBuffer1D<ulong, Stride1D.Dense> _candidateBuffer;
private MemoryBuffer1D<byte, Stride1D.Dense> _maskBuffer;
private ulong[] _hostCandidates;
private byte[] _hostMask;
private int _capacity;
private bool _disposed;
internal MersenneNumberDivisorCandidateGpuEvaluator(int batchSize)
{
_lease = GpuContextPool.RentPreferred(preferCpu: false);
_accelerator = _lease.Accelerator;
_kernel = _accelerator.LoadAutoGroupedStreamKernel<Index1D, CandidateFilterArgs, ArrayView<ulong>, ArrayView<byte>>(EvaluateCandidatesKernel);
_capacity = Math.Max(1, batchSize);
_candidateBuffer = _accelerator.Allocate1D<ulong>(_capacity);
_maskBuffer = _accelerator.Allocate1D<byte>(_capacity);
_hostCandidates = ArrayPool<ulong>.Shared.Rent(_capacity);
_hostMask = ArrayPool<byte>.Shared.Rent(_capacity);
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_candidateBuffer.Dispose();
_maskBuffer.Dispose();
ArrayPool<ulong>.Shared.Return(_hostCandidates, clearArray: false);
ArrayPool<byte>.Shared.Return(_hostMask, clearArray: false);
_lease.Dispose();
}
internal void EvaluateCandidates(
ulong startDivisor,
ulong step,
ulong limit,
ReadOnlySpan<byte> remainders,
ReadOnlySpan<byte> steps,
bool lastIsSeven,
Span<ulong> candidates,
Span<byte> mask)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(MersenneNumberDivisorCandidateGpuEvaluator));
}
if (remainders.Length != RemainderCount || steps.Length != RemainderCount)
{
throw new ArgumentException("Remainder and step tables must contain exactly six entries.");
}
int count = candidates.Length;
// Candidate batches always carry at least one divisor on the by-divisor path, so the empty-count
// guard remains commented out to eliminate the branch.
// if (count == 0)
// {
// return;
// }
if (count > _capacity)
{
EnsureCapacity(count);
}
Span<ulong> hostCandidateSpan = _hostCandidates.AsSpan(0, count);
Span<byte> hostMaskSpan = _hostMask.AsSpan(0, count);
ArrayView1D<ulong, Stride1D.Dense> candidateView = _candidateBuffer.View.SubView(0, count);
ArrayView1D<byte, Stride1D.Dense> maskView = _maskBuffer.View.SubView(0, count);
var args = new CandidateFilterArgs(
startDivisor,
step,
limit,
remainders[0],
remainders[1],
remainders[2],
remainders[3],
remainders[4],
remainders[5],
steps[0],
steps[1],
steps[2],
steps[3],
steps[4],
steps[5],
lastIsSeven ? (byte)1 : (byte)0);
_kernel(count, args, candidateView, maskView);
candidateView.CopyToCPU(ref MemoryMarshal.GetReference(hostCandidateSpan), count);
maskView.CopyToCPU(ref MemoryMarshal.GetReference(hostMaskSpan), count);
hostCandidateSpan.CopyTo(candidates);
hostMaskSpan.CopyTo(mask);
}
private void EnsureCapacity(int required)
{
if (required <= _capacity)
{
return;
}
int newCapacity = _capacity;
// The evaluator always starts with a positive capacity, so the zero-capacity fallback remains commented.
// if (newCapacity == 0)
// {
// newCapacity = 1;
// }
while (newCapacity < required)
{
newCapacity <<= 1;
}
_candidateBuffer.Dispose();
_maskBuffer.Dispose();
ArrayPool<ulong>.Shared.Return(_hostCandidates, clearArray: false);
ArrayPool<byte>.Shared.Return(_hostMask, clearArray: false);
_capacity = newCapacity;
_candidateBuffer = _accelerator.Allocate1D<ulong>(_capacity);
_maskBuffer = _accelerator.Allocate1D<byte>(_capacity);
_hostCandidates = ArrayPool<ulong>.Shared.Rent(_capacity);
_hostMask = ArrayPool<byte>.Shared.Rent(_capacity);
}
private static void EvaluateCandidatesKernel(
Index1D index,
CandidateFilterArgs args,
ArrayView<ulong> candidates,
ArrayView<byte> mask)
{
int idx = index;
if (idx >= mask.Length)
{
return;
}
ulong offset = (ulong)idx;
ulong candidate = args.StartDivisor + args.Step * offset;
candidates[idx] = candidate;
if (candidate > args.Limit || (args.Step != 0UL && candidate < args.StartDivisor))
{
mask[idx] = 0;
return;
}
int remainderValue10 = ComputeRemainder(args.Remainder10, args.Step10, idx, 10);
int remainderValue8 = ComputeRemainder(args.Remainder8, args.Step8, idx, 8);
int remainderValue5 = ComputeRemainder(args.Remainder5, args.Step5, idx, 5);
int remainderValue3 = ComputeRemainder(args.Remainder3, args.Step3, idx, 3);
int remainderValue7 = ComputeRemainder(args.Remainder7, args.Step7, idx, 7);
int remainderValue11 = ComputeRemainder(args.Remainder11, args.Step11, idx, 11);
bool lastIsSeven = args.LastIsSevenFlag != 0;
bool admissible = lastIsSeven
? (remainderValue10 == 3 || remainderValue10 == 7 || remainderValue10 == 9)
: (remainderValue10 == 1 || remainderValue10 == 3 || remainderValue10 == 9);
if (!admissible)
{
mask[idx] = 0;
return;
}
if ((remainderValue8 != 1 && remainderValue8 != 7)
|| remainderValue3 == 0
|| remainderValue5 == 0
|| remainderValue7 == 0
|| remainderValue11 == 0)
{
mask[idx] = 0;
return;
}
mask[idx] = 1;
}
private static int ComputeRemainder(byte remainder, byte step, int index, int modulus)
{
// Modulus lookups only cover {10, 8, 5, 3, 7, 11}, so the zero-modulus guard stays commented out.
// if (modulus == 0)
// {
// return 0;
// }
long value = remainder;
value += (long)step * index;
int result = (int)(value % modulus);
if (result < 0)
{
result += modulus;
}
return result;
}
private readonly struct CandidateFilterArgs
{
public CandidateFilterArgs(
ulong startDivisor,
ulong step,
ulong limit,
byte remainder10,
byte remainder8,
byte remainder5,
byte remainder3,
byte remainder7,
byte remainder11,
byte step10,
byte step8,
byte step5,
byte step3,
byte step7,
byte step11,
byte lastIsSevenFlag)
{
StartDivisor = startDivisor;
Step = step;
Limit = limit;
Remainder10 = remainder10;
Remainder8 = remainder8;
Remainder5 = remainder5;
Remainder3 = remainder3;
Remainder7 = remainder7;
Remainder11 = remainder11;
Step10 = step10;
Step8 = step8;
Step5 = step5;
Step3 = step3;
Step7 = step7;
Step11 = step11;
LastIsSevenFlag = lastIsSevenFlag;
}
public ulong StartDivisor { get; }
public ulong Step { get; }
public ulong Limit { get; }
public byte Remainder10 { get; }
public byte Remainder8 { get; }
public byte Remainder5 { get; }
public byte Remainder3 { get; }
public byte Remainder7 { get; }
public byte Remainder11 { get; }
public byte Step10 { get; }
public byte Step8 { get; }
public byte Step5 { get; }
public byte Step3 { get; }
public byte Step7 { get; }
public byte Step11 { get; }
public byte LastIsSevenFlag { get; }
}
}
@@ -0,0 +1,190 @@
using System;
using System.Buffers;
using System.Runtime.InteropServices;
using PerfectNumbers.Core;
using ILGPU;
using ILGPU.Runtime;
namespace PerfectNumbers.Core.Gpu;
internal sealed class MersenneNumberDivisorMontgomeryGpuBuilder
{
private readonly GpuContextPool.GpuContextLease _lease;
private readonly Accelerator _accelerator;
private readonly Action<Index1D, ArrayView<ulong>, ArrayView<MontgomeryDivisorData>> _kernel;
private MemoryBuffer1D<ulong, Stride1D.Dense> _modulusBuffer;
private MemoryBuffer1D<MontgomeryDivisorData, Stride1D.Dense> _resultBuffer;
private ulong[] _hostModulusBuffer;
private MontgomeryDivisorData[] _hostResultBuffer;
private int _capacity;
private bool _disposed;
internal MersenneNumberDivisorMontgomeryGpuBuilder(int batchSize)
{
_lease = GpuContextPool.RentPreferred(preferCpu: false);
_accelerator = _lease.Accelerator;
_kernel = _accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ArrayView<MontgomeryDivisorData>>(BuildKernel);
_capacity = Math.Max(1, batchSize);
_modulusBuffer = _accelerator.Allocate1D<ulong>(_capacity);
_resultBuffer = _accelerator.Allocate1D<MontgomeryDivisorData>(_capacity);
_hostModulusBuffer = ArrayPool<ulong>.Shared.Rent(_capacity);
_hostResultBuffer = ArrayPool<MontgomeryDivisorData>.Shared.Rent(_capacity);
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_modulusBuffer.Dispose();
_resultBuffer.Dispose();
ArrayPool<ulong>.Shared.Return(_hostModulusBuffer, clearArray: false);
ArrayPool<MontgomeryDivisorData>.Shared.Return(_hostResultBuffer, clearArray: false);
_lease.Dispose();
}
internal void Build(ReadOnlySpan<ulong> moduli, Span<MontgomeryDivisorData> destination)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(MersenneNumberDivisorMontgomeryGpuBuilder));
}
int length = moduli.Length;
// Montgomery batches always include at least one modulus on the by-divisor path, so the empty-length
// guard stays commented out to avoid branching.
// if (length == 0)
// {
// return;
// }
EnsureCapacity(length);
Span<ulong> hostModulusSpan = _hostModulusBuffer.AsSpan(0, length);
moduli.CopyTo(hostModulusSpan);
ArrayView1D<ulong, Stride1D.Dense> modulusView = _modulusBuffer.View.SubView(0, length);
modulusView.CopyFromCPU(ref MemoryMarshal.GetReference(hostModulusSpan), length);
ArrayView1D<MontgomeryDivisorData, Stride1D.Dense> resultView = _resultBuffer.View.SubView(0, length);
_kernel(length, modulusView, resultView);
Span<MontgomeryDivisorData> hostResultSpan = _hostResultBuffer.AsSpan(0, length);
resultView.CopyToCPU(ref MemoryMarshal.GetReference(hostResultSpan), length);
hostResultSpan.CopyTo(destination);
}
private void EnsureCapacity(int required)
{
if (required <= _capacity)
{
return;
}
int newCapacity = _capacity;
// The builder always starts with a positive capacity, so the zero-capacity fallback remains commented.
// if (newCapacity == 0)
// {
// newCapacity = 1;
// }
while (newCapacity < required)
{
newCapacity <<= 1;
}
_modulusBuffer.Dispose();
_resultBuffer.Dispose();
ArrayPool<ulong>.Shared.Return(_hostModulusBuffer, clearArray: false);
ArrayPool<MontgomeryDivisorData>.Shared.Return(_hostResultBuffer, clearArray: false);
_capacity = newCapacity;
_modulusBuffer = _accelerator.Allocate1D<ulong>(_capacity);
_resultBuffer = _accelerator.Allocate1D<MontgomeryDivisorData>(_capacity);
_hostModulusBuffer = ArrayPool<ulong>.Shared.Rent(_capacity);
_hostResultBuffer = ArrayPool<MontgomeryDivisorData>.Shared.Rent(_capacity);
}
private static void BuildKernel(Index1D index, ArrayView<ulong> moduli, ArrayView<MontgomeryDivisorData> destination)
{
int idx = index;
// The builder launches kernels with an extent equal to the modulus count, so the bounds guard remains commented
// out to avoid redundant branching on every thread.
// if (idx >= moduli.Length)
// {
// return;
// }
ulong modulus = moduli[idx];
// All staged divisors are odd primes on the scanner path, so keep the defensive modulus guard commented out to
// remove the branch from the kernel.
// if (modulus <= 1UL || (modulus & 1UL) == 0UL)
// {
// destination[idx] = default;
// return;
// }
ulong nPrime = ComputeMontgomeryNPrime(modulus);
ulong montgomeryOne = ComputeMontgomeryResidue64(modulus);
ulong montgomeryTwo = montgomeryOne.MulMod64(2UL, modulus);
ulong montgomeryTwoSquared = MontgomeryMultiplyGpu(montgomeryTwo, montgomeryTwo, modulus, nPrime);
destination[idx] = new MontgomeryDivisorData(
modulus,
nPrime,
montgomeryOne,
montgomeryTwo,
montgomeryTwoSquared);
}
private static ulong ComputeMontgomeryResidue64(ulong modulus)
{
ulong remainder = ulong.MaxValue % modulus;
remainder++;
if (remainder >= modulus)
{
remainder -= modulus;
}
return remainder;
}
private static ulong MontgomeryMultiplyGpu(ulong a, ulong b, ulong modulus, ulong nPrime)
{
GpuUInt128 product = new(a);
product.Mul64(new GpuUInt128(b));
ulong tLow = product.Low;
ulong tHigh = product.High;
ulong m = unchecked(tLow * nPrime);
GpuUInt128 mProduct = new(m);
mProduct.Mul64(new GpuUInt128(modulus));
ulong mLow = mProduct.Low;
ulong mHigh = mProduct.High;
ulong carry = unchecked((tLow + mLow) < tLow ? 1UL : 0UL);
ulong result = tHigh + mHigh + carry;
if (result >= modulus)
{
result -= modulus;
}
return result;
}
private static ulong ComputeMontgomeryNPrime(ulong modulus)
{
ulong inv = modulus;
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
return unchecked(0UL - inv);
}
}
@@ -0,0 +1,146 @@
using System;
using System.Buffers;
using System.Runtime.InteropServices;
using ILGPU;
using ILGPU.Runtime;
namespace PerfectNumbers.Core.Gpu;
internal sealed class MersenneNumberDivisorRemainderGpuStepper
{
private const int RemainderCount = 6;
private readonly GpuContextPool.GpuContextLease _lease;
private readonly Accelerator _accelerator;
private readonly Action<Index1D, int, ArrayView<byte>, ArrayView<byte>, ArrayView<byte>> _kernel;
private MemoryBuffer1D<byte, Stride1D.Dense> _remainderBuffer;
private MemoryBuffer1D<byte, Stride1D.Dense> _stepBuffer;
private MemoryBuffer1D<byte, Stride1D.Dense> _resultBuffer;
private byte[] _hostRemainders;
private byte[] _hostSteps;
private readonly int _length;
private int _capacity;
private bool _disposed;
internal MersenneNumberDivisorRemainderGpuStepper(ReadOnlySpan<byte> steps)
{
if (steps.Length != RemainderCount)
{
throw new ArgumentException("Step table must contain exactly six entries.");
}
_lease = GpuContextPool.RentPreferred(preferCpu: false);
_accelerator = _lease.Accelerator;
_kernel = _accelerator.LoadAutoGroupedStreamKernel<Index1D, int, ArrayView<byte>, ArrayView<byte>, ArrayView<byte>>(AdvanceRemaindersKernel);
_length = steps.Length;
_capacity = Math.Max(1, _length);
_remainderBuffer = _accelerator.Allocate1D<byte>(_capacity);
_stepBuffer = _accelerator.Allocate1D<byte>(_capacity);
_resultBuffer = _accelerator.Allocate1D<byte>(_capacity);
_hostRemainders = ArrayPool<byte>.Shared.Rent(_capacity);
_hostSteps = ArrayPool<byte>.Shared.Rent(_capacity);
steps.CopyTo(_hostSteps.AsSpan(0, _length));
ArrayView1D<byte, Stride1D.Dense> stepView = _stepBuffer.View.SubView(0, _length);
stepView.CopyFromCPU(ref MemoryMarshal.GetReference(_hostSteps.AsSpan(0, _length)), _length);
}
public void Dispose()
{
if (_disposed)
{
throw new InvalidOperationException("MersenneNumberDivisorRemainderGpuStepper was disposed more than once.");
}
_disposed = true;
_remainderBuffer.Dispose();
_stepBuffer.Dispose();
_resultBuffer.Dispose();
ArrayPool<byte>.Shared.Return(_hostRemainders, clearArray: false);
ArrayPool<byte>.Shared.Return(_hostSteps, clearArray: false);
_lease.Dispose();
}
internal void Advance(int advanceCount, Span<byte> remainders)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(MersenneNumberDivisorRemainderGpuStepper));
}
// GPU remainder batches always advance by a positive chunk count and operate on fixed-size tables, so
// the defensive early-return remains commented out.
// if (advanceCount <= 0 || remainders.Length == 0)
// {
// return;
// }
// Production code always calls the stepper with a fixed-length table, so the historical length guard stays commented.
// if (remainders.Length != _length)
// {
// throw new ArgumentException("Remainder buffer length must match the configured length.");
// }
Span<byte> hostRemainderSpan = _hostRemainders.AsSpan(0, _length);
remainders.CopyTo(hostRemainderSpan);
ArrayView1D<byte, Stride1D.Dense> remainderView = _remainderBuffer.View.SubView(0, _length);
remainderView.CopyFromCPU(ref MemoryMarshal.GetReference(hostRemainderSpan), _length);
ArrayView1D<byte, Stride1D.Dense> stepView = _stepBuffer.View.SubView(0, _length);
ArrayView1D<byte, Stride1D.Dense> resultView = _resultBuffer.View.SubView(0, _length);
_kernel(_length, advanceCount, remainderView, stepView, resultView);
resultView.CopyToCPU(ref MemoryMarshal.GetReference(hostRemainderSpan), _length);
hostRemainderSpan.CopyTo(remainders);
}
private static void AdvanceRemaindersKernel(Index1D index, int advanceCount, ArrayView<byte> remainders, ArrayView<byte> steps, ArrayView<byte> results)
{
int idx = index;
// Kernel launches match the remainder table length, so the bounds guard stays commented to avoid redundant branching.
// if (idx >= remainders.Length)
// {
// return;
// }
int modulus = GetModulus(idx);
// The modulus selector only emits positive bases (10, 8, 5, 3, 7, 11) for the configured indices, so the zero-modulus
// fallback remains commented out.
// if (modulus == 0)
// {
// results[idx] = remainders[idx];
// return;
// }
int remainder = remainders[idx] % modulus;
int step = steps[idx] % modulus;
long delta = (long)step * advanceCount;
int adjusted = (int)((remainder + delta) % modulus);
if (adjusted < 0)
{
adjusted += modulus;
}
results[idx] = (byte)adjusted;
}
private static int GetModulus(int index)
{
return index switch
{
0 => 10,
1 => 8,
2 => 5,
3 => 3,
4 => 7,
5 => 11,
_ => 0,
};
}
}
@@ -0,0 +1,128 @@
using System;
using System.Buffers;
using System.Runtime.InteropServices;
using PerfectNumbers.Core;
using ILGPU;
using ILGPU.Runtime;
namespace PerfectNumbers.Core.Gpu;
internal sealed class MersenneNumberDivisorResidueGpuEvaluator
{
private readonly GpuContextPool.GpuContextLease _lease;
private readonly Accelerator _accelerator;
private readonly Action<Index1D, MontgomeryDivisorData, ArrayView<ulong>, ArrayView<ulong>> _kernel;
private MemoryBuffer1D<ulong, Stride1D.Dense> _exponentBuffer;
private MemoryBuffer1D<ulong, Stride1D.Dense> _resultBuffer;
private ulong[] _hostBuffer;
private int _capacity;
private bool _disposed;
internal MersenneNumberDivisorResidueGpuEvaluator(int batchSize)
{
_lease = GpuContextPool.RentPreferred(preferCpu: false);
_accelerator = _lease.Accelerator;
_kernel = _accelerator.LoadAutoGroupedStreamKernel<Index1D, MontgomeryDivisorData, ArrayView<ulong>, ArrayView<ulong>>(ComputeResiduesKernel);
_capacity = Math.Max(1, batchSize);
_exponentBuffer = _accelerator.Allocate1D<ulong>(_capacity);
_resultBuffer = _accelerator.Allocate1D<ulong>(_capacity);
_hostBuffer = ArrayPool<ulong>.Shared.Rent(_capacity);
}
public void Dispose()
{
if (_disposed)
{
throw new InvalidOperationException("MersenneNumberDivisorResidueGpuEvaluator was disposed more than once.");
}
_disposed = true;
_exponentBuffer.Dispose();
_resultBuffer.Dispose();
ArrayPool<ulong>.Shared.Return(_hostBuffer, clearArray: false);
_lease.Dispose();
}
internal void ComputeResidues(ReadOnlySpan<ulong> exponents, in MontgomeryDivisorData divisorData, Span<ulong> destination)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(MersenneNumberDivisorResidueGpuEvaluator));
}
int length = exponents.Length;
// Residue batches always include at least one exponent when scheduled from the by-divisor scanner, so
// the empty-length guard remains commented out to avoid branching.
// if (length == 0)
// {
// return;
// }
EnsureCapacity(length);
Span<ulong> hostSpan = _hostBuffer.AsSpan(0, length);
exponents.CopyTo(hostSpan);
ArrayView1D<ulong, Stride1D.Dense> exponentView = _exponentBuffer.View.SubView(0, length);
exponentView.CopyFromCPU(ref MemoryMarshal.GetReference(hostSpan), length);
ArrayView1D<ulong, Stride1D.Dense> resultView = _resultBuffer.View.SubView(0, length);
_kernel(length, divisorData, exponentView, resultView);
resultView.CopyToCPU(ref MemoryMarshal.GetReference(hostSpan), length);
hostSpan.CopyTo(destination);
}
private void EnsureCapacity(int required)
{
if (required <= _capacity)
{
return;
}
int newCapacity = _capacity;
// The evaluator always starts with a positive capacity, so the zero-capacity fallback stays commented.
// if (newCapacity == 0)
// {
// newCapacity = 1;
// }
while (newCapacity < required)
{
newCapacity <<= 1;
}
_exponentBuffer.Dispose();
_resultBuffer.Dispose();
ArrayPool<ulong>.Shared.Return(_hostBuffer, clearArray: false);
_capacity = newCapacity;
_exponentBuffer = _accelerator.Allocate1D<ulong>(_capacity);
_resultBuffer = _accelerator.Allocate1D<ulong>(_capacity);
_hostBuffer = ArrayPool<ulong>.Shared.Rent(_capacity);
}
private static void ComputeResiduesKernel(Index1D index, MontgomeryDivisorData divisorData, ArrayView<ulong> exponents, ArrayView<ulong> results)
{
int idx = index;
int length = (int)exponents.Length;
// Batched launches match the buffer length on production workloads, so the bounds guard stays commented out to remove
// the redundant comparison.
// if (idx >= length)
// {
// return;
// }
ulong exponent = exponents[idx];
ulong modulus = divisorData.Modulus;
// Residue batches only include valid odd moduli, so the defensive check stays commented to avoid re-validating
// the invariant on every iteration.
// if (modulus <= 1UL || (modulus & 1UL) == 0UL)
// {
// results[idx] = 0UL;
// return;
// }
results[idx] = exponent.Pow2ModWindowedGpu(modulus);
}
}
@@ -327,8 +327,9 @@ public class MersenneNumberLucasLehmerGpuTester
// TODO: Replace StreamWriter with the pooled TextFileWriter pipeline so persisting NTT // TODO: Replace StreamWriter with the pooled TextFileWriter pipeline so persisting NTT
// parameters reuses the zero-allocation buffered writes highlighted in the scanner I/O // parameters reuses the zero-allocation buffered writes highlighted in the scanner I/O
// benchmarks instead of allocating a new encoder per append. // benchmarks instead of allocating a new encoder per append.
using var writer = new StreamWriter(ParameterFilePath, append: true); var writer = new StreamWriter(ParameterFilePath, append: true);
writer.WriteLine($"{length} {modulus.High} {modulus.Low} {primitiveRoot.High} {primitiveRoot.Low}"); writer.WriteLine($"{length} {modulus.High} {modulus.Low} {primitiveRoot.High} {primitiveRoot.Low}");
writer.Dispose();
} }
} }
@@ -338,7 +339,7 @@ public class MersenneNumberLucasLehmerGpuTester
ulong foundRoot = 0UL; ulong foundRoot = 0UL;
bool success = false; bool success = false;
var processorCount = Environment.ProcessorCount; var processorCount = Environment.ProcessorCount;
using var cts = new System.Threading.CancellationTokenSource(); var cts = new System.Threading.CancellationTokenSource();
object sync = new(); object sync = new();
// TODO: Move this Parallel.For to the shared low-overhead work scheduler once the NTT parameter // TODO: Move this Parallel.For to the shared low-overhead work scheduler once the NTT parameter
@@ -381,11 +382,13 @@ public class MersenneNumberLucasLehmerGpuTester
{ {
modulus = new GpuUInt128(0UL, foundCandidate); modulus = new GpuUInt128(0UL, foundCandidate);
primitiveRoot = new GpuUInt128(0UL, foundRoot); primitiveRoot = new GpuUInt128(0UL, foundRoot);
cts.Dispose();
return true; return true;
} }
modulus = new GpuUInt128(0UL, 18446744069414584321UL); modulus = new GpuUInt128(0UL, 18446744069414584321UL);
primitiveRoot = new GpuUInt128(0UL, 7UL); primitiveRoot = new GpuUInt128(0UL, 7UL);
cts.Dispose();
return false; return false;
} }
@@ -0,0 +1,88 @@
using System;
using System.Runtime.InteropServices;
using ILGPU.Runtime;
namespace PerfectNumbers.Core.Gpu;
internal static class MontgomeryOddPowerGpu
{
public static bool TryCompute(in MontgomeryDivisorData divisor, int oddPowerCount, Span<ulong> destination)
{
if (oddPowerCount <= 0)
{
return true;
}
Span<MontgomeryDivisorData> divisorSpan = stackalloc MontgomeryDivisorData[1];
divisorSpan[0] = divisor;
Span<int> countSpan = stackalloc int[1];
countSpan[0] = oddPowerCount;
Span<ulong> resultSpan = stackalloc ulong[PerfectNumberConstants.MaxOddPowersCount];
if (!TryComputeBatch(divisorSpan, countSpan, resultSpan, PerfectNumberConstants.MaxOddPowersCount))
{
return false;
}
resultSpan[..oddPowerCount].CopyTo(destination);
return true;
}
public static bool TryComputeBatch(
ReadOnlySpan<MontgomeryDivisorData> divisors,
ReadOnlySpan<int> oddPowerCounts,
Span<ulong> results,
int stride)
{
int divisorCount = divisors.Length;
if (divisorCount == 0)
{
return true;
}
if (oddPowerCounts.Length < divisorCount)
{
throw new ArgumentException(
"oddPowerCounts span must contain at least as many elements as divisors span.",
nameof(oddPowerCounts));
}
if (results.Length < divisorCount * stride)
{
throw new ArgumentException(
"The result span is shorter than the required batched odd power storage.",
nameof(results));
}
if (GpuContextPool.ForceCpu)
{
return false;
}
var lease = GpuKernelPool.GetKernel(useGpuOrder: true);
var execution = lease.EnterExecutionScope();
var accelerator = lease.Accelerator;
var stream = lease.Stream;
var divisorBuffer = accelerator.Allocate1D<MontgomeryDivisorData>(divisorCount);
var countBuffer = accelerator.Allocate1D<int>(divisorCount);
var resultBuffer = accelerator.Allocate1D<ulong>(divisorCount * stride);
divisorBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(divisors), divisorCount);
countBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(oddPowerCounts), divisorCount);
resultBuffer.MemSetToZero();
var kernel = lease.MontgomeryOddPowerKernel;
kernel(stream, divisorCount, divisorBuffer.View, countBuffer.View, resultBuffer.View, stride);
stream.Synchronize();
resultBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(results), divisorCount * stride);
resultBuffer.Dispose();
countBuffer.Dispose();
divisorBuffer.Dispose();
execution.Dispose();
lease.Dispose();
return true;
}
}
+1 -1
View File
@@ -47,7 +47,7 @@ public static class NttGpuMath
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int>> BitReverseKernelCache = new(); // TODO: Precompute bit-reversal kernels during boot so we can remove the concurrent lookup overhead flagged in the benchmarks. private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int>> BitReverseKernelCache = new(); // TODO: Precompute bit-reversal kernels during boot so we can remove the concurrent lookup overhead flagged in the benchmarks.
private sealed class SquareCacheEntry : IDisposable private sealed class SquareCacheEntry
{ {
public MemoryBuffer1D<GpuUInt128, Stride1D.Dense> Buffer { get; } public MemoryBuffer1D<GpuUInt128, Stride1D.Dense> Buffer { get; }
public GpuUInt128 Root { get; } public GpuUInt128 Root { get; }
@@ -805,13 +805,15 @@ internal static partial class PrimeOrderGpuHeuristics
for (int i = 0; i < factorCount; i++) for (int i = 0; i < factorCount; i++)
{ {
ulong factor = factors[i]; ulong factor = factors[i];
if (factor <= 1UL) // The factorization routine emits strictly greater-than-one factors for phi on this path, so the
{ // defensive continue would never execute.
continue; // if (factor <= 1UL)
} // {
// continue;
// }
ulong reduced = phi / factor; ulong reduced = phi / factor;
if (reduced.Pow2MontgomeryModWindowedGpu(divisor, keepMontgomery: false) == 1UL) if (reduced.Pow2ModWindowedGpu(divisor.Modulus) == 1UL)
{ {
return false; return false;
} }
@@ -827,7 +829,7 @@ internal static partial class PrimeOrderGpuHeuristics
if (residue == 1UL || residue == 7UL) if (residue == 1UL || residue == 7UL)
{ {
ulong half = phi >> 1; ulong half = phi >> 1;
if (Pow2EqualsOneKernel(half, divisor)) if ((half).Pow2ModWindowedGpu(divisor.Modulus) == 1UL)
{ {
order = half; order = half;
} }
@@ -848,10 +850,11 @@ internal static partial class PrimeOrderGpuHeuristics
{ {
ulong primeFactor = factors[i]; ulong primeFactor = factors[i];
int exponent = exponents[i]; int exponent = exponents[i];
if (primeFactor <= 1UL) // Factorization never yields unity on the scanning path, so skip the redundant guard.
{ // if (primeFactor <= 1UL)
continue; // {
} // continue;
// }
for (int iteration = 0; iteration < exponent; iteration++) for (int iteration = 0; iteration < exponent; iteration++)
{ {
@@ -861,7 +864,7 @@ internal static partial class PrimeOrderGpuHeuristics
} }
ulong reduced = order / primeFactor; ulong reduced = order / primeFactor;
if (Pow2EqualsOneKernel(reduced, divisor)) if ((reduced).Pow2ModWindowedGpu(divisor.Modulus) == 1UL)
{ {
order = reduced; order = reduced;
continue; continue;
@@ -887,12 +890,14 @@ internal static partial class PrimeOrderGpuHeuristics
ArrayView1D<ulong, Stride1D.Dense> compositeStack, ArrayView1D<ulong, Stride1D.Dense> compositeStack,
ArrayView1D<byte, Stride1D.Dense> statusOut) ArrayView1D<byte, Stride1D.Dense> statusOut)
{ {
if (order == 0UL) // Order values handed to the kernel are strictly positive on the production path, so the zero guard stays commented
{ // out to remove the redundant branch.
return false; // if (order == 0UL)
} // {
// return false;
// }
if (!Pow2EqualsOneKernel(order, divisor)) if ((order).Pow2ModWindowedGpu(divisor.Modulus) != 1UL)
{ {
return false; return false;
} }
@@ -922,10 +927,11 @@ internal static partial class PrimeOrderGpuHeuristics
{ {
ulong primeFactor = factors[i]; ulong primeFactor = factors[i];
int exponent = exponents[i]; int exponent = exponents[i];
if (primeFactor <= 1UL) // Factorization never yields one on the by-divisor path, so the defensive continue stays commented out.
{ // if (primeFactor <= 1UL)
continue; // {
} // continue;
// }
ulong reduced = order; ulong reduced = order;
for (int iteration = 0; iteration < exponent; iteration++) for (int iteration = 0; iteration < exponent; iteration++)
@@ -936,7 +942,7 @@ internal static partial class PrimeOrderGpuHeuristics
} }
reduced /= primeFactor; reduced /= primeFactor;
if (Pow2EqualsOneKernel(reduced, divisor)) if ((reduced).Pow2ModWindowedGpu(divisor.Modulus) == 1UL)
{ {
return false; return false;
} }
@@ -998,28 +1004,32 @@ internal static partial class PrimeOrderGpuHeuristics
long candidateCapacity = candidates.Length; long candidateCapacity = candidates.Length;
int candidateLimit = candidateCapacity < HeuristicCandidateLimit ? (int)candidateCapacity : HeuristicCandidateLimit; int candidateLimit = candidateCapacity < HeuristicCandidateLimit ? (int)candidateCapacity : HeuristicCandidateLimit;
int candidateCount = BuildCandidatesKernel(order, workFactors, workExponents, factorCount, candidates, stackIndex, stackExponent, stackProduct, candidateLimit); int candidateCount = BuildCandidatesKernel(order, workFactors, workExponents, factorCount, candidates, stackIndex, stackExponent, stackProduct, candidateLimit);
if (candidateCount == 0) // Candidate enumeration always yields at least one order on the configured workloads, so the zero-count guard remains
{ // commented out to eliminate the redundant branch.
return false; // if (candidateCount == 0)
} // {
// return false;
// }
SortCandidatesKernel(prime, previousOrder, hasPreviousOrder != 0, candidates, candidateCount); SortCandidatesKernel(prime, previousOrder, hasPreviousOrder != 0, candidates, candidateCount);
int powBudget = maxPowChecks <= 0 ? candidateCount : maxPowChecks; int powBudget = maxPowChecks <= 0 ? candidateCount : maxPowChecks;
if (powBudget <= 0) // The heuristics always reserve at least one check, so the zero-budget fallback remains commented out.
{ // if (powBudget <= 0)
powBudget = candidateCount; // {
} // powBudget = candidateCount;
// }
int powUsed = 0; int powUsed = 0;
for (int i = 0; i < candidateCount && powUsed < powBudget; i++) for (int i = 0; i < candidateCount && powUsed < powBudget; i++)
{ {
ulong candidate = candidates[i]; ulong candidate = candidates[i];
if (candidate <= 1UL) // Candidate generation skips 1 by construction, so the defensive continue stays commented out.
{ // if (candidate <= 1UL)
continue; // {
} // continue;
// }
if (powUsed >= powBudget) if (powUsed >= powBudget)
{ {
@@ -1027,7 +1037,7 @@ internal static partial class PrimeOrderGpuHeuristics
} }
powUsed++; powUsed++;
if (!Pow2EqualsOneKernel(candidate, divisor)) if ((candidate).Pow2ModWindowedGpu(divisor.Modulus) != 1UL)
{ {
continue; continue;
} }
@@ -1307,10 +1317,11 @@ internal static partial class PrimeOrderGpuHeuristics
{ {
ulong primeFactor = factors[i]; ulong primeFactor = factors[i];
int exponent = exponents[i]; int exponent = exponents[i];
if (primeFactor <= 1UL) // Factorization never yields one for candidate orders, so keep the guard commented out to avoid re-checking inputs.
{ // if (primeFactor <= 1UL)
continue; // {
} // continue;
// }
ulong reduced = candidate; ulong reduced = candidate;
for (int iteration = 0; iteration < exponent; iteration++) for (int iteration = 0; iteration < exponent; iteration++)
@@ -1327,7 +1338,7 @@ internal static partial class PrimeOrderGpuHeuristics
} }
powUsed++; powUsed++;
if (Pow2EqualsOneKernel(reduced, divisor)) if ((reduced).Pow2ModWindowedGpu(divisor.Modulus) == 1UL)
{ {
return false; return false;
} }
@@ -1349,17 +1360,21 @@ internal static partial class PrimeOrderGpuHeuristics
ArrayView1D<ulong, Stride1D.Dense> compositeStack, ArrayView1D<ulong, Stride1D.Dense> compositeStack,
ArrayView1D<byte, Stride1D.Dense> statusOut) ArrayView1D<byte, Stride1D.Dense> statusOut)
{ {
if (initial <= 1UL) // Composite candidates sent here are always greater than one during production scans, so the trivial-return branch stays
{ // commented out.
return true; // if (initial <= 1UL)
} // {
// return true;
// }
int stackCapacity = (int)compositeStack.Length; int stackCapacity = (int)compositeStack.Length;
if (stackCapacity <= 0) // Pollard-Rho always reserves a positive stack on production workloads, so the overflow guard remains commented out
{ // to avoid branching here.
statusOut[0] = (byte)PrimeOrderKernelStatus.PollardOverflow; // if (stackCapacity <= 0)
return false; // {
} // statusOut[0] = (byte)PrimeOrderKernelStatus.PollardOverflow;
// return false;
// }
int stackTop = 0; int stackTop = 0;
compositeStack[stackTop] = initial; compositeStack[stackTop] = initial;
@@ -1369,10 +1384,12 @@ internal static partial class PrimeOrderGpuHeuristics
{ {
stackTop--; stackTop--;
ulong composite = compositeStack[stackTop]; ulong composite = compositeStack[stackTop];
if (composite <= 1UL) // Composite values enqueued for factoring are always greater than one on production workloads, so the guard stays
{ // commented out to avoid extra branching.
continue; // if (composite <= 1UL)
} // {
// continue;
// }
if (!PeelSmallPrimesKernel( if (!PeelSmallPrimesKernel(
composite, composite,
@@ -1580,9 +1597,7 @@ internal static partial class PrimeOrderGpuHeuristics
bLocal >>= BitOperations.TrailingZeroCount(bLocal); bLocal >>= BitOperations.TrailingZeroCount(bLocal);
if (aLocal > bLocal) if (aLocal > bLocal)
{ {
ulong temp = aLocal; (aLocal, bLocal) = (bLocal, aLocal);
aLocal = bLocal;
bLocal = temp;
} }
bLocal -= aLocal; bLocal -= aLocal;
@@ -1593,15 +1608,10 @@ internal static partial class PrimeOrderGpuHeuristics
} }
} }
private static bool Pow2EqualsOneKernel(ulong exponent, in MontgomeryDivisorData divisor)
{
return exponent.Pow2MontgomeryModWindowedGpu(divisor, keepMontgomery: false) == 1UL;
}
private static ulong CalculateByDoublingKernel(ulong prime) private static ulong CalculateByDoublingKernel(ulong prime)
{ {
ulong order = 1UL; ulong order = 1UL;
ulong value = 2UL % prime; ulong value = 2UL; // Candidate primes exceed two, so the modulo would always return two.
while (value != 1UL) while (value != 1UL)
{ {
@@ -1619,10 +1629,11 @@ internal static partial class PrimeOrderGpuHeuristics
private static GpuPow2ModStatus TryPow2ModBatchInternal(ReadOnlySpan<UInt128> exponents, UInt128 prime, Span<UInt128> remainders) private static GpuPow2ModStatus TryPow2ModBatchInternal(ReadOnlySpan<UInt128> exponents, UInt128 prime, Span<UInt128> remainders)
{ {
if (exponents.Length == 0) // Residue batches are never empty on production workloads, so skip the zero-length success shortcut.
{ // if (exponents.Length == 0)
return GpuPow2ModStatus.Success; // {
} // return GpuPow2ModStatus.Success;
// }
if (remainders.Length < exponents.Length) if (remainders.Length < exponents.Length)
{ {
@@ -1698,27 +1709,30 @@ internal static partial class PrimeOrderGpuHeuristics
private static void ComputePow2ModCpu(ReadOnlySpan<ulong> exponents, ulong prime, in MontgomeryDivisorData divisorData, Span<ulong> results) private static void ComputePow2ModCpu(ReadOnlySpan<ulong> exponents, ulong prime, in MontgomeryDivisorData divisorData, Span<ulong> results)
{ {
int length = exponents.Length; int length = exponents.Length;
ulong modulus = divisorData.Modulus;
_ = prime; // modulus and divisorData both describe the same q; retain the parameter to preserve the public signature.
for (int i = 0; i < length; i++) for (int i = 0; i < length; i++)
{ {
results[i] = Pow2ModCpu(exponents[i], prime, divisorData); results[i] = exponents[i].Pow2ModWindowedCpu(modulus);
} }
} }
private static bool TryComputeOnGpuWide(ReadOnlySpan<UInt128> exponents, UInt128 prime, Span<UInt128> results) private static bool TryComputeOnGpuWide(ReadOnlySpan<UInt128> exponents, UInt128 prime, Span<UInt128> results)
{ {
int length = exponents.Length; int length = exponents.Length;
if (length == 0) // Production launches always carry work, so keep the empty-batch shortcut commented out.
{ // if (length == 0)
return true; // {
} // return true;
// }
GpuUInt128[]? rentedExponents = null; GpuUInt128[]? rentedExponents = null;
GpuUInt128[]? rentedResults = null; GpuUInt128[]? rentedResults = null;
var lease = GpuKernelPool.GetKernel(useGpuOrder: true); var lease = GpuKernelPool.GetKernel(useGpuOrder: true);
var execution = lease.EnterExecutionScope();
try try
{ {
var execution = lease.EnterExecutionScope();
Accelerator accelerator = lease.Accelerator; Accelerator accelerator = lease.Accelerator;
AcceleratorStream stream = lease.Stream; AcceleratorStream stream = lease.Stream;
var kernel = GetPow2ModWideKernel(accelerator); var kernel = GetPow2ModWideKernel(accelerator);
@@ -1755,7 +1769,6 @@ internal static partial class PrimeOrderGpuHeuristics
exponentBuffer.Dispose(); exponentBuffer.Dispose();
remainderBuffer.Dispose(); remainderBuffer.Dispose();
execution.Dispose();
return true; return true;
} }
catch (Exception) catch (Exception)
@@ -1765,6 +1778,8 @@ internal static partial class PrimeOrderGpuHeuristics
} }
finally finally
{ {
// Small batches use the stack-allocated fast path, leaving the rented arrays null. Retain the guards so pooled
// buffers are only returned when they were actually leased.
if (rentedExponents is not null) if (rentedExponents is not null)
{ {
ArrayPool<GpuUInt128>.Shared.Return(rentedExponents, clearArray: false); ArrayPool<GpuUInt128>.Shared.Return(rentedExponents, clearArray: false);
@@ -1775,6 +1790,7 @@ internal static partial class PrimeOrderGpuHeuristics
ArrayPool<GpuUInt128>.Shared.Return(rentedResults, clearArray: false); ArrayPool<GpuUInt128>.Shared.Return(rentedResults, clearArray: false);
} }
execution.Dispose();
lease.Dispose(); lease.Dispose();
} }
} }
@@ -1788,16 +1804,6 @@ internal static partial class PrimeOrderGpuHeuristics
} }
} }
private static ulong Pow2ModCpu(ulong exponent, ulong modulus, in MontgomeryDivisorData divisorData)
{
if (modulus <= 1UL)
{
return 0UL;
}
return exponent.Pow2MontgomeryModWindowedGpu(divisorData, keepMontgomery: false);
}
private static Action<AcceleratorStream, Index1D, ArrayView1D<ulong, Stride1D.Dense>, MontgomeryDivisorData, ArrayView1D<ulong, Stride1D.Dense>> GetPow2ModKernel(Accelerator accelerator) private static Action<AcceleratorStream, Index1D, ArrayView1D<ulong, Stride1D.Dense>, MontgomeryDivisorData, ArrayView1D<ulong, Stride1D.Dense>> GetPow2ModKernel(Accelerator accelerator)
{ {
return Pow2ModKernelCache.GetOrAdd(accelerator, static accel => return Pow2ModKernelCache.GetOrAdd(accelerator, static accel =>
@@ -1810,8 +1816,17 @@ internal static partial class PrimeOrderGpuHeuristics
private static void Pow2ModKernel(Index1D index, ArrayView1D<ulong, Stride1D.Dense> exponents, MontgomeryDivisorData divisor, ArrayView1D<ulong, Stride1D.Dense> remainders) private static void Pow2ModKernel(Index1D index, ArrayView1D<ulong, Stride1D.Dense> exponents, MontgomeryDivisorData divisor, ArrayView1D<ulong, Stride1D.Dense> remainders)
{ {
ulong modulus = divisor.Modulus;
// The by-divisor heuristics call this kernel only with odd prime moduli, so the defensive branch stays commented to
// document the invariant without spending a branch in the kernel.
// if (modulus <= 1UL || (modulus & 1UL) == 0UL)
// {
// remainders[index] = 0UL;
// return;
// }
ulong exponent = exponents[index]; ulong exponent = exponents[index];
remainders[index] = exponent.Pow2MontgomeryModWindowedGpu(divisor, keepMontgomery: false); remainders[index] = exponent.Pow2ModWindowedGpu(modulus);
} }
private static Action<AcceleratorStream, Index1D, ArrayView1D<GpuUInt128, Stride1D.Dense>, GpuUInt128, ArrayView1D<GpuUInt128, Stride1D.Dense>> GetPow2ModWideKernel(Accelerator accelerator) private static Action<AcceleratorStream, Index1D, ArrayView1D<GpuUInt128, Stride1D.Dense>, GpuUInt128, ArrayView1D<GpuUInt128, Stride1D.Dense>> GetPow2ModWideKernel(Accelerator accelerator)
@@ -12,14 +12,16 @@ public interface IMersenneNumberDivisorByDivisorTester
ulong GetAllowedMaxDivisor(ulong prime); ulong GetAllowedMaxDivisor(ulong prime);
bool IsPrime(ulong prime, out bool divisorsExhausted); bool IsPrime(ulong prime, out bool divisorsExhausted, TimeSpan? timeLimit = null);
IDivisorScanSession CreateDivisorSession(); IDivisorScanSession CreateDivisorSession();
void PrepareCandidates(ReadOnlySpan<ulong> primes, Span<ulong> allowedMaxValues); void PrepareCandidates(ReadOnlySpan<ulong> primes, Span<ulong> allowedMaxValues);
public interface IDivisorScanSession : IDisposable public interface IDivisorScanSession
{ {
void CheckDivisor(ulong divisor, in MontgomeryDivisorData divisorData, ulong divisorCycle, ReadOnlySpan<ulong> primes, Span<byte> hits); void CheckDivisor(ulong divisor, in MontgomeryDivisorData divisorData, ulong divisorCycle, ReadOnlySpan<ulong> primes, Span<byte> hits);
void Dispose();
} }
} }
+327 -120
View File
@@ -1,3 +1,4 @@
using System;
using System.Buffers; using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Generic; using System.Collections.Generic;
@@ -18,6 +19,12 @@ public class MersenneDivisorCycles
// Lightweight read-mostly cache for small divisors (<= 4,000,000). 0 => unknown // Lightweight read-mostly cache for small divisors (<= 4,000,000). 0 => unknown
private ulong[]? _smallCycles; private ulong[]? _smallCycles;
[ThreadStatic]
private static Dictionary<ulong, int>? s_factorCountsPool;
[ThreadStatic]
private static Dictionary<ulong, int>? s_factorScratchPool;
public static MersenneDivisorCycles Shared { get; } = new MersenneDivisorCycles(); public static MersenneDivisorCycles Shared { get; } = new MersenneDivisorCycles();
public MersenneDivisorCycles() public MersenneDivisorCycles()
@@ -67,23 +74,27 @@ public class MersenneDivisorCycles
return true; return true;
} }
if (exponent <= 1UL || divisor <= 1UL || (divisor & 1UL) == 0UL) // Exponent and divisor validation happens during scheduling, so the defensive guard remains commented out to keep the
{ // hot path branch-free.
return false; // if (exponent <= 1UL || divisor <= 1UL || (divisor & 1UL) == 0UL)
} // {
// return false;
// }
if (divisor <= PerfectNumberConstants.MaxQForDivisorCycles) // Production scans never revisit divisors in the small-cycle snapshot, so skip the legacy lookup here. Factoring and
{ // diagnostics can query TryMatchSmallCycleForFactoring when they need those cached values.
var small = Shared._smallCycles; // if (divisor <= PerfectNumberConstants.MaxQForDivisorCycles)
if (small is not null) // {
{ // var small = Shared._smallCycles;
ulong cached = small[(int)divisor]; // if (small is not null)
if (cached != 0UL) // {
{ // ulong cached = small[(int)divisor];
return cached == exponent; // if (cached != 0UL)
} // {
} // return cached == exponent;
} // }
// }
// }
if (TryCalculateCycleLengthHeuristic(divisor, divisorData, out ulong heuristicCycle) && heuristicCycle != 0UL) if (TryCalculateCycleLengthHeuristic(divisor, divisorData, out ulong heuristicCycle) && heuristicCycle != 0UL)
{ {
@@ -91,6 +102,7 @@ public class MersenneDivisorCycles
} }
Dictionary<ulong, FactorCacheEntry>? cache = factorCache; Dictionary<ulong, FactorCacheEntry>? cache = factorCache;
// Each by-divisor scan session starts with a null cache and fills it lazily, so keep the allocation guard in place.
if (cache is null) if (cache is null)
{ {
cache = new Dictionary<ulong, FactorCacheEntry>(8); cache = new Dictionary<ulong, FactorCacheEntry>(8);
@@ -102,7 +114,7 @@ public class MersenneDivisorCycles
return cycleLength == exponent; return cycleLength == exponent;
} }
return exponent.Pow2MontgomeryModWindowedCpu(divisorData, keepMontgomery: false) == 1UL; return exponent.Pow2ModWindowedCpu(divisorData.Modulus) == 1UL;
} }
public static bool CycleEqualsExponentForMersenneCandidate(ulong divisor, in MontgomeryDivisorData divisorData, ulong exponent) public static bool CycleEqualsExponentForMersenneCandidate(ulong divisor, in MontgomeryDivisorData divisorData, ulong exponent)
@@ -112,35 +124,70 @@ public class MersenneDivisorCycles
return false; return false;
} }
if (divisor <= PerfectNumberConstants.MaxQForDivisorCycles) // Candidate divisors on the production path sit well above the precomputed snapshot, so skip the small-cycle lookup
// here and leave factoring scenarios to call TryMatchSmallCycleForFactoring when necessary.
// if (divisor <= PerfectNumberConstants.MaxQForDivisorCycles)
// {
// MersenneDivisorCycles shared = Shared;
// ulong[]? small = shared._smallCycles;
// if (small is not null)
// {
// ulong cached = small[(int)divisor];
// if (cached != 0UL)
// {
// return cached == exponent;
// }
// }
// }
return exponent.Pow2ModWindowedCpu(divisorData.Modulus) == 1UL;
}
public static bool TryMatchSmallCycleForFactoring(ulong divisor, ulong exponent, out ulong cycleLength)
{
if (divisor > PerfectNumberConstants.MaxQForDivisorCycles)
{ {
MersenneDivisorCycles shared = Shared; // Factoring helpers probe arbitrary divisors, so anything above the snapshot limit legitimately misses here.
ulong[]? small = shared._smallCycles; cycleLength = 0UL;
if (small is not null) return false;
{
ulong cached = small[(int)divisor];
if (cached != 0UL)
{
return cached == exponent;
}
}
} }
return exponent.Pow2MontgomeryModWindowedCpu(divisorData, keepMontgomery: false) == 1UL; ulong[]? small = Shared._smallCycles;
if (small is null)
{
// Snapshot recycling during diagnostics can temporarily clear the table; surface the miss so callers recompute.
cycleLength = 0UL;
return false;
}
ulong cached = small[(int)divisor];
if (cached == 0UL)
{
// Only a subset of small divisors carry cached cycles; signal the miss so factoring falls back to the GPU helper.
cycleLength = 0UL;
return false;
}
cycleLength = cached;
return cached == exponent;
} }
private static bool IsValidMersenneDivisorCandidate(ulong divisor, ulong exponent) private static bool IsValidMersenneDivisorCandidate(ulong divisor, ulong exponent)
{ {
if (exponent <= 1UL || divisor <= 1UL || (divisor & 1UL) == 0UL) // Production workloads pre-filter invalid inputs, so skip the defensive guards here. Factoring and diagnostics should
{ // validate inputs before calling into the candidate path.
return false; // if (exponent <= 1UL || divisor <= 1UL || (divisor & 1UL) == 0UL)
} // {
// return false;
// }
UInt128 step = (UInt128)exponent << 1; UInt128 step = (UInt128)exponent << 1;
if (step == UInt128.Zero) // The overflow bail-out is handled by factoring-specific call sites; keep the historical branch commented to document the
{ // legacy behavior.
return false; // if (step == UInt128.Zero)
} // {
// return false;
// }
UInt128 adjusted = (UInt128)divisor - UInt128.One; UInt128 adjusted = (UInt128)divisor - UInt128.One;
return adjusted % step == UInt128.Zero; return adjusted % step == UInt128.Zero;
@@ -149,7 +196,7 @@ public class MersenneDivisorCycles
public static IEnumerable<(ulong divisor, ulong cycleLength)> EnumerateStream(Stream compressor) public static IEnumerable<(ulong divisor, ulong cycleLength)> EnumerateStream(Stream compressor)
{ {
// Binary pairs: (ulong divisor, ulong cycle) // Binary pairs: (ulong divisor, ulong cycle)
using var reader = new BinaryReader(compressor, Encoding.UTF8, leaveOpen: true); var reader = new BinaryReader(compressor, Encoding.UTF8, leaveOpen: true);
while (true) while (true)
{ {
ulong d, c; ulong d, c;
@@ -160,6 +207,7 @@ public class MersenneDivisorCycles
} }
catch (EndOfStreamException) catch (EndOfStreamException)
{ {
reader.Dispose();
yield break; yield break;
} }
@@ -273,17 +321,24 @@ public class MersenneDivisorCycles
{ {
cycleLength = 0UL; cycleLength = 0UL;
if ((divisor & (divisor - 1UL)) == 0UL) // The by-divisor scanner only supplies odd candidates of the form 2*k*exponent + 1,
{ // so this power-of-two fast-path never triggers on that call path.
cycleLength = 1UL; // The historical fast-path is left here for reference but commented out because it is unreachable
return true; // on the EvenPerfectBitScanner pipeline.
} // if ((divisor & (divisor - 1UL)) == 0UL)
// {
// cycleLength = 1UL;
// return true;
// }
if (divisor <= 3UL || exponent <= 1UL) // EvenPerfectBitScanner filters exponents <= 3 before invoking this helper and each
{ // divisor candidate is at least 2 * exponent + 1, so this guard never matches.
cycleLength = CalculateCycleLength(divisor, divisorData); // The fallback calculation is therefore unreachable on the active call path.
return true; // if (divisor <= 3UL || exponent <= 1UL)
} // {
// cycleLength = CalculateCycleLength(divisor, divisorData);
// return true;
// }
ulong computedOrder = PrimeOrderCalculator.Calculate( ulong computedOrder = PrimeOrderCalculator.Calculate(
divisor, divisor,
@@ -298,37 +353,53 @@ public class MersenneDivisorCycles
} }
ulong phi = divisor - 1UL; ulong phi = divisor - 1UL;
if (phi == 0UL) // Divisors reach this helper only when they exceed the cached table, which means
{ // divisor >= 2 * exponent + 1 and phi cannot underflow to zero here.
return false; // The defensive underflow guard is kept for posterity but disabled for the EvenPerfectBitScanner path.
} // if (phi == 0UL)
// {
// return false;
// }
int twoCount = BitOperations.TrailingZeroCount(phi); int twoCount = BitOperations.TrailingZeroCount(phi);
ulong reducedPhi = phi >> twoCount; ulong reducedPhi = phi >> twoCount;
if (reducedPhi == 0UL || (reducedPhi % exponent) != 0UL) // Every divisor candidate generated by the by-divisor scan satisfies q ≡ 1 (mod 2 * exponent),
{ // ensuring reducedPhi keeps a factor of exponent and remains non-zero in this context.
return false; // The divisibility check is therefore unreachable for the current caller and is commented out.
} // if (reducedPhi == 0UL || (reducedPhi % exponent) != 0UL)
// {
// return false;
// }
ulong k = reducedPhi / exponent; ulong k = reducedPhi / exponent;
if (k == 0UL) // reducedPhi always contains at least one factor of exponent, so dividing by exponent leaves k >= 1.
{ // if (k == 0UL)
return false; // {
} // return false;
// }
Dictionary<ulong, int> factorCounts = new(8); Dictionary<ulong, int> factorCounts = AcquireFactorCountDictionary(8);
if (twoCount > 0) try
{ {
// The divisor candidates are odd, so phi is even and this branch always records at least one factor of two.
// if (twoCount > 0)
// {
// factorCounts[2UL] = twoCount;
// }
factorCounts[2UL] = twoCount; factorCounts[2UL] = twoCount;
}
if (!AccumulateFactors(exponent, factorCache, factorCounts, cacheResult: true) || !AccumulateFactors(k, factorCache, factorCounts, cacheResult: false)) if (!AccumulateFactors(exponent, factorCache, factorCounts, cacheResult: true) || !AccumulateFactors(k, factorCache, factorCounts, cacheResult: false))
{
return false;
}
cycleLength = ReduceOrder(divisorData, phi, factorCounts, exponent);
return true;
}
finally
{ {
return false; ReleaseFactorCountDictionary(factorCounts);
} }
cycleLength = ReduceOrder(divisorData, phi, factorCounts);
return true;
} }
private static bool AccumulateFactors( private static bool AccumulateFactors(
@@ -342,61 +413,119 @@ public class MersenneDivisorCycles
return true; return true;
} }
if (!TryGetFactorization(value, cache, cacheResult, out FactorCacheEntry factorization)) FactorCacheEntry cachedFactorization;
PrimeFactorPartialResult? lease = null;
bool fromCache;
try
{ {
return false; if (!TryGetFactorization(value, cache, cacheResult, out cachedFactorization, out lease, out fromCache))
{
return false;
}
if (fromCache)
{
ulong[] primes = cachedFactorization.Primes;
byte[] exponents = cachedFactorization.Exponents;
int length = cachedFactorization.Count;
for (int i = 0; i < length; i++)
{
AddFactor(counts, primes[i], exponents[i]);
}
return true;
}
if (lease is null || lease.Count == 0)
{
return true;
}
ReadOnlySpan<ulong> primesSpan = lease.Primes;
ReadOnlySpan<byte> exponentsSpan = lease.Exponents;
int spanLength = lease.Count;
for (int i = 0; i < spanLength; i++)
{
AddFactor(counts, primesSpan[i], exponentsSpan[i]);
}
return true;
} }
finally
ulong[] primes = factorization.Primes;
byte[] exponents = factorization.Exponents;
int length = factorization.Count;
for (int i = 0; i < length; i++)
{ {
AddFactor(counts, primes[i], exponents[i]); lease?.Dispose();
} }
return true;
} }
private static bool TryGetFactorization( private static bool TryGetFactorization(
ulong value, ulong value,
Dictionary<ulong, FactorCacheEntry>? cache, Dictionary<ulong, FactorCacheEntry>? cache,
bool cacheResult, bool cacheResult,
out FactorCacheEntry factorization) out FactorCacheEntry factorization,
out PrimeFactorPartialResult? lease,
out bool fromCache)
{ {
if (cache is not null && cache.TryGetValue(value, out factorization)) factorization = default;
lease = null;
fromCache = false;
if (value <= 1UL)
{ {
return true; return true;
} }
Dictionary<ulong, int> scratch = new(8); if (cache is not null && cache.TryGetValue(value, out factorization))
if (!TryFactorIntoCountsInternal(value, scratch))
{ {
factorization = default!; fromCache = true;
return false; return true;
} }
int count = scratch.Count; Dictionary<ulong, int> scratch = AcquireFactorScratchDictionary(8);
ulong[] primes = new ulong[count]; try
byte[] exponents = new byte[count];
int index = 0;
foreach (KeyValuePair<ulong, int> entry in scratch)
{ {
primes[index] = entry.Key; if (!TryFactorIntoCountsInternal(value, scratch))
exponents[index] = checked((byte)entry.Value); {
index++; factorization = default;
return false;
}
int count = scratch.Count;
if (count == 0)
{
return true;
}
PrimeFactorPartialResult result = PrimeFactorPartialResult.Rent(count);
lease = result;
Span<ulong> primes = result.PrimeWriteSpan;
Span<byte> exponents = result.ExponentWriteSpan;
int index = 0;
foreach (KeyValuePair<ulong, int> entry in scratch)
{
primes[index] = entry.Key;
exponents[index] = checked((byte)entry.Value);
index++;
}
result.CommitCount(index);
result.Sort();
if (cache is not null && cacheResult)
{
FactorCacheEntry cacheEntry = result.ToCacheEntry();
cache[value] = cacheEntry;
}
return true;
} }
finally
Array.Sort(primes, exponents);
factorization = new FactorCacheEntry(primes, exponents, count);
if (cache is not null && cacheResult)
{ {
cache[value] = factorization; ReleaseFactorScratchDictionary(scratch);
} }
return true;
} }
private static bool TryFactorIntoCountsInternal(ulong value, Dictionary<ulong, int> counts) private static bool TryFactorIntoCountsInternal(ulong value, Dictionary<ulong, int> counts)
@@ -471,15 +600,22 @@ public class MersenneDivisorCycles
} }
} }
private static ulong ReduceOrder(in MontgomeryDivisorData divisorData, ulong initialOrder, Dictionary<ulong, int> factorCounts) private static ulong ReduceOrder(
in MontgomeryDivisorData divisorData,
ulong initialOrder,
Dictionary<ulong, int> factorCounts,
ulong targetOrder)
{ {
if (factorCounts.Count == 0) if (factorCounts.Count == 0 || initialOrder == targetOrder)
{ {
return initialOrder; return initialOrder;
} }
int count = factorCounts.Count; int count = factorCounts.Count;
ulong[] primes = ArrayPool<ulong>.Shared.Rent(count); bool rentFromPool = count >= PerfectNumberConstants.PooledArrayThreshold;
ulong[] primes = rentFromPool
? ThreadLocalArrayPool<ulong>.Shared.Rent(count)
: new ulong[count];
try try
{ {
int index = 0; int index = 0;
@@ -508,6 +644,13 @@ public class MersenneDivisorCycles
if (candidate.Pow2MontgomeryModWindowedCpu(divisorData, keepMontgomery: false) == 1UL) if (candidate.Pow2MontgomeryModWindowedCpu(divisorData, keepMontgomery: false) == 1UL)
{ {
order = candidate; order = candidate;
if (order == targetOrder)
{
// Early-exit once the order matches the tested exponent;
// further factor processing would not change the outcome for the by-divisor scan.
return order;
}
continue; continue;
} }
@@ -519,7 +662,10 @@ public class MersenneDivisorCycles
} }
finally finally
{ {
ArrayPool<ulong>.Shared.Return(primes, clearArray: false); if (rentFromPool)
{
ThreadLocalArrayPool<ulong>.Shared.Return(primes, clearArray: false);
}
} }
} }
@@ -592,6 +738,53 @@ public class MersenneDivisorCycles
} }
} }
// Thread-local dictionary pools keep the factoring helpers reusable so the GC stays out of the by-divisor CPU loop.
private static Dictionary<ulong, int> AcquireFactorCountDictionary(int capacityHint)
{
Dictionary<ulong, int>? dictionary = s_factorCountsPool;
if (dictionary is null)
{
return new Dictionary<ulong, int>(capacityHint);
}
s_factorCountsPool = null;
dictionary.Clear();
dictionary.EnsureCapacity(capacityHint);
return dictionary;
}
private static void ReleaseFactorCountDictionary(Dictionary<ulong, int> dictionary)
{
dictionary.Clear();
if (s_factorCountsPool is null)
{
s_factorCountsPool = dictionary;
}
}
private static Dictionary<ulong, int> AcquireFactorScratchDictionary(int capacityHint)
{
Dictionary<ulong, int>? dictionary = s_factorScratchPool;
if (dictionary is null)
{
return new Dictionary<ulong, int>(capacityHint);
}
s_factorScratchPool = null;
dictionary.Clear();
dictionary.EnsureCapacity(capacityHint);
return dictionary;
}
private static void ReleaseFactorScratchDictionary(Dictionary<ulong, int> dictionary)
{
dictionary.Clear();
if (s_factorScratchPool is null)
{
s_factorScratchPool = dictionary;
}
}
private static ulong NextRandomUInt64() private static ulong NextRandomUInt64()
{ {
Span<byte> buffer = stackalloc byte[8]; Span<byte> buffer = stackalloc byte[8];
@@ -637,7 +830,7 @@ public class MersenneDivisorCycles
// Start fresh to avoid mixing formats // Start fresh to avoid mixing formats
outputStream.SetLength(0L); outputStream.SetLength(0L);
outputStream.Position = 0L; outputStream.Position = 0L;
using var writer = new BinaryWriter(outputStream, Encoding.UTF8, leaveOpen: false); var writer = new BinaryWriter(outputStream, Encoding.UTF8, leaveOpen: false);
for (var taskIndex = 0; taskIndex < threads; taskIndex++) for (var taskIndex = 0; taskIndex < threads; taskIndex++)
{ {
ulong threadStart = start + (ulong)taskIndex * blockSize; ulong threadStart = start + (ulong)taskIndex * blockSize;
@@ -658,7 +851,11 @@ public class MersenneDivisorCycles
tasks[taskIndex] = Task.Run(() => tasks[taskIndex] = Task.Run(() =>
{ {
ulong rangeLength = localEnd - localStart + 1UL; ulong rangeLength = localEnd - localStart + 1UL;
(ulong divisor, ulong cycle)[] localCycles = ArrayPool<(ulong, ulong)>.Shared.Rent(checked((int)rangeLength)); int localLength = checked((int)rangeLength);
bool rentLocalCycles = localLength >= PerfectNumberConstants.PooledArrayThreshold;
(ulong divisor, ulong cycle)[] localCycles = rentLocalCycles
? ThreadLocalArrayPool<(ulong, ulong)>.Shared.Rent(localLength)
: new (ulong divisor, ulong cycle)[localLength];
int localCycleIndex = 0; int localCycleIndex = 0;
try try
@@ -706,12 +903,16 @@ public class MersenneDivisorCycles
} }
finally finally
{ {
ArrayPool<(ulong, ulong)>.Shared.Return(localCycles, clearArray: false); if (rentLocalCycles)
{
ThreadLocalArrayPool<(ulong, ulong)>.Shared.Return(localCycles, clearArray: false);
}
} }
}); });
} }
Task.WaitAll(tasks); Task.WaitAll(tasks);
writer.Dispose();
} }
public static void GenerateGpu(string path, ulong maxDivisor, int batchSize = 1_000_000, long skipCount = 0L, long nextPosition = 0L) public static void GenerateGpu(string path, ulong maxDivisor, int batchSize = 1_000_000, long skipCount = 0L, long nextPosition = 0L)
@@ -721,7 +922,7 @@ public class MersenneDivisorCycles
ulong start = 3UL; ulong start = 3UL;
var locker = new object(); var locker = new object();
var pool = ArrayPool<ulong>.Shared; var pool = ThreadLocalArrayPool<ulong>.Shared;
ulong batchSizeUL = (ulong)batchSize, d, end; ulong batchSizeUL = (ulong)batchSize, d, end;
int count = (int)Math.Min(batchSizeUL, maxDivisor), i, idx; int count = (int)Math.Min(batchSizeUL, maxDivisor), i, idx;
ulong[] divisors, outCycles, validDivisors; ulong[] divisors, outCycles, validDivisors;
@@ -748,7 +949,7 @@ public class MersenneDivisorCycles
outputStream.Position = 0L; outputStream.Position = 0L;
} }
using var writer = new BinaryWriter(outputStream, Encoding.UTF8, leaveOpen: false); var writer = new BinaryWriter(outputStream, Encoding.UTF8, leaveOpen: false);
try try
{ {
while (skipCount > 0L && start <= maxDivisor) while (skipCount > 0L && start <= maxDivisor)
@@ -823,6 +1024,7 @@ public class MersenneDivisorCycles
pool.Return(outCycles, clearArray: false); pool.Return(outCycles, clearArray: false);
} }
writer.Dispose();
stream.Dispose(); stream.Dispose();
lease.Dispose(); lease.Dispose();
@@ -926,17 +1128,22 @@ public class MersenneDivisorCycles
internal static bool TryCalculateCycleLengthHeuristic(ulong divisor, in MontgomeryDivisorData divisorData, out ulong cycleLength) internal static bool TryCalculateCycleLengthHeuristic(ulong divisor, in MontgomeryDivisorData divisorData, out ulong cycleLength)
{ {
if ((divisor & (divisor - 1UL)) == 0UL) // The by-divisor scanner only supplies odd candidates of the form 2*k*exponent + 1,
{ // so this power-of-two fast-path never triggers on that call path.
cycleLength = 1UL; // The early exit is retained for reference but commented out because it is unreachable on the scanner path.
return true; // if ((divisor & (divisor - 1UL)) == 0UL)
} // {
// cycleLength = 1UL;
// return true;
// }
if (divisor <= 3UL) // By the divisor-by-divisor pipeline, candidates have the shape 2 * exponent * k + 1 with exponent >= 5,
{ // so the divisor never drops below 11 on this call path.
cycleLength = CalculateCycleLengthFallback(divisor); // if (divisor <= 3UL)
return true; // {
} // cycleLength = CalculateCycleLengthFallback(divisor);
// return true;
// }
if (PrimeTester.IsPrimeInternal(divisor, CancellationToken.None)) if (PrimeTester.IsPrimeInternal(divisor, CancellationToken.None))
{ {
@@ -18,7 +18,8 @@ public static class MersenneNumberDivisorByDivisorTester
Action markComposite, Action markComposite,
Action clearComposite, Action clearComposite,
Action<ulong, bool, bool, bool> printResult, Action<ulong, bool, bool, bool> printResult,
int threadCount) int threadCount,
TimeSpan? primeTestLimit)
{ {
if (candidates.Count == 0) if (candidates.Count == 0)
{ {
@@ -179,11 +180,13 @@ public static class MersenneNumberDivisorByDivisorTester
return; return;
} }
if (maxPrime <= 1UL) // EvenPerfectBitScanner removes candidates <= 1 before reaching this point,
{ // so this guard never triggers during production runs.
Console.WriteLine("The filter specified by --filter-p must contain at least one prime exponent greater than 1 for --mersenne=bydivisor."); // if (maxPrime <= 1UL)
return; // {
} // Console.WriteLine("The filter specified by --filter-p must contain at least one prime exponent greater than 1 for --mersenne=bydivisor.");
// return;
// }
tester.ConfigureFromMaxPrime(maxPrime); tester.ConfigureFromMaxPrime(maxPrime);
@@ -243,7 +246,7 @@ public static class MersenneNumberDivisorByDivisorTester
void ProcessPrime(ulong prime) void ProcessPrime(ulong prime)
{ {
bool isPrime = tester.IsPrime(prime, out bool divisorsExhausted); bool isPrime = tester.IsPrime(prime, out bool divisorsExhausted, primeTestLimit);
if (!isPrime) if (!isPrime)
{ {
@@ -256,21 +259,52 @@ public static class MersenneNumberDivisorByDivisorTester
printResult(prime, true, divisorsExhausted, true); printResult(prime, true, divisorsExhausted, true);
} }
if (workerCount == 1) if (workerCount > filteredPrimes.Count)
{
workerCount = filteredPrimes.Count;
}
if (workerCount <= 1)
{ {
foreach (ulong prime in filteredPrimes) foreach (ulong prime in filteredPrimes)
{ {
ProcessPrime(prime); ProcessPrime(prime);
} }
}
else
{
ParallelOptions options = new()
{
MaxDegreeOfParallelism = workerCount
};
Parallel.ForEach(filteredPrimes, options, ProcessPrime); return;
} }
(int Start, int End)[] workerRanges = new (int, int)[workerCount];
int totalPrimeCount = filteredPrimes.Count;
int baseGroupSize = totalPrimeCount / workerCount;
int remaining = totalPrimeCount % workerCount;
int groupStartIndex = 0;
for (int workerIndex = 0; workerIndex < workerCount; workerIndex++)
{
int groupSize = baseGroupSize;
if (workerIndex < remaining)
{
groupSize++;
}
startIndex = groupStartIndex;
int endIndex = startIndex + groupSize;
workerRanges[workerIndex] = (startIndex, endIndex);
groupStartIndex = endIndex;
}
ParallelOptions parallelOptions = new()
{
MaxDegreeOfParallelism = workerCount
};
Parallel.ForEach(workerRanges, parallelOptions, range =>
{
for (int candidateIndex = range.Start; candidateIndex < range.End; candidateIndex++)
{
ProcessPrime(filteredPrimes[candidateIndex]);
}
});
} }
} }
@@ -45,32 +45,4 @@ public readonly struct MontgomeryDivisorData(ulong modulus, ulong nPrime, ulong
montgomeryTwo, montgomeryTwo,
montgomeryTwoSquared); montgomeryTwoSquared);
} }
}
internal static class MontgomeryDivisorDataCache
{
[ThreadStatic]
private static bool s_hasCachedValue;
[ThreadStatic]
private static ulong s_cachedModulus;
[ThreadStatic]
private static MontgomeryDivisorData s_cachedData;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static MontgomeryDivisorData Get(ulong modulus)
{
if (s_hasCachedValue && s_cachedModulus == modulus)
{
return s_cachedData;
}
MontgomeryDivisorData computed = MontgomeryDivisorData.FromModulus(modulus);
s_cachedModulus = modulus;
s_cachedData = computed;
s_hasCachedValue = true;
return computed;
}
} }
+243
View File
@@ -0,0 +1,243 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
namespace PerfectNumbers.Core;
/// <summary>
/// Mutable helper for 128-bit arithmetic derived from the .NET runtime UInt128 implementation.
/// Portions adapted from https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/System.Private.CoreLib/src/System/UInt128.cs.
/// </summary>
public struct MutableUInt128
{
public ulong High;
public ulong Low;
public MutableUInt128(ulong value)
{
Low = value;
}
public MutableUInt128(ulong high, ulong low)
{
High = high;
Low = low;
}
public MutableUInt128(UInt128 value)
{
High = (ulong)(value >> 64);
Low = (ulong)value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Set(ulong value)
{
High = 0UL;
Low = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Set(UInt128 value)
{
High = (ulong)(value >> 64);
Low = (ulong)value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Set(ulong high, ulong low)
{
High = high;
Low = low;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly UInt128 ToUInt128() => ((UInt128)High << 64) | Low;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(ulong value)
{
ulong low = Low + value;
if (low < value)
{
High++;
}
Low = low;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(MutableUInt128 value)
{
ulong temp = Low + value.Low;
Low = temp;
temp = temp < Low ? 1UL : 0UL;
temp += High;
temp += value.High;
High = temp;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Sub(ulong value)
{
ulong temp = Low;
ulong result = temp - value;
temp = result > temp ? 1UL : 0UL;
Low = result;
High -= temp;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Sub(MutableUInt128 value)
{
ulong temp = Low;
ulong result = temp - value.Low;
temp = temp < value.Low ? 1UL : 0UL;
Low = result;
High = High - value.High - temp;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Mul(ulong value)
{
ulong currentHigh = High;
ulong currentLow = Low;
ulong temp = Mul64(currentLow, value, out currentLow);
Mul64(currentHigh, value, out currentHigh);
temp += currentHigh;
Low = currentLow;
High = temp;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Mul64(ulong left, ulong right, out ulong low)
{
const ulong Mask32 = 0xFFFF_FFFFUL;
ulong leftHigh = left >> 32;
ulong rightLow = right & Mask32;
ulong leftLow = left & Mask32;
ulong lowLow = leftLow * rightLow;
ulong temp = right >> 32;
leftLow *= temp;
rightLow *= leftHigh;
leftHigh *= temp;
temp = (lowLow >> 32) + (leftLow & Mask32) + (rightLow & Mask32);
leftHigh += (leftLow >> 32) + (rightLow >> 32) + (temp >> 32);
low = ((temp & Mask32) << 32) | (lowLow & Mask32);
return leftHigh;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MulAdd(ulong multiplier, ulong addend)
{
const ulong Mask32 = 0xFFFF_FFFFUL;
ulong multiplierLow = multiplier & Mask32;
ulong multiplierHigh = multiplier >> 32;
ulong product11 = Low;
ulong product01 = product11 & Mask32;
product11 >>= 32;
ulong product00 = product01 * multiplierLow;
product01 *= multiplierHigh;
ulong product10 = product11 * multiplierLow;
product11 *= multiplierHigh;
ulong high = (product00 >> 32) + (product01 & Mask32) + (product10 & Mask32);
product11 = product11 + (product01 >> 32) + (product10 >> 32) + (high >> 32);
product10 = ((high & Mask32) << 32) | (product00 & Mask32);
high = High;
product00 = high & Mask32;
high >>= 32;
product01 = product00 * multiplierLow;
product00 *= multiplierHigh;
high *= multiplierLow;
high = (product01 >> 32) + (product00 & Mask32) + (high & Mask32);
product01 = ((high & Mask32) << 32) | (product01 & Mask32);
// Calculate low. We're assigning addend to local variable for a tiny bit better performance.
product00 = addend;
high = product10 + product00;
Low = high;
// Calculate high including carry
high = high < product00 ? 1UL : 0UL;
high += product11 + product01;
// Discard overflow above 128 bits to match Multiply's truncation semantics.
High = high;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly ulong Mod(ulong modulus)
{
// This case will never happen in production code.
// if (modulus == 0UL)
// {
// throw new DivideByZeroException();
// }
ulong high = High;
if (high == 0UL)
{
return Low % modulus;
}
MutableUInt128 remainder = default;
int highBitLength = 64 - BitOperations.LeadingZeroCount(high);
if (highBitLength > 0)
{
ProcessWord(ref remainder, high, highBitLength, modulus);
}
ProcessWord(ref remainder, Low, 64, modulus);
return remainder.Low;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ProcessWord(ref MutableUInt128 remainder, ulong word, int bitCount, ulong modulus)
{
for (int bit = bitCount - 1; bit >= 0; bit--)
{
remainder.ShiftLeft();
if (((word >> bit) & 1UL) != 0)
{
remainder.Add(1UL);
}
remainder.Reduce(modulus);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ShiftLeft()
{
High = (High << 1) | (Low >> 63);
Low <<= 1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Reduce(ulong modulus)
{
if (High != 0UL || Low >= modulus)
{
Sub(modulus);
if (High != 0UL || Low >= modulus)
{
Sub(modulus);
}
}
}
}
+39 -26
View File
@@ -1,5 +1,4 @@
using System; using System;
using System.Buffers;
namespace PerfectNumbers.Core namespace PerfectNumbers.Core
{ {
@@ -8,12 +7,20 @@ namespace PerfectNumbers.Core
[ThreadStatic] [ThreadStatic]
private static PartialFactorResult? s_poolHead; private static PartialFactorResult? s_poolHead;
public static readonly PartialFactorResult Empty = new()
{
Cofactor = 1UL,
FullyFactored = true,
};
public FactorEntry[]? Factors; public FactorEntry[]? Factors;
private PartialFactorResult? _next;
public ulong Cofactor; public ulong Cofactor;
public bool FullyFactored; public bool FullyFactored;
public int Count; public int Count;
private PartialFactorResult? _next;
private bool _factorsFromPool;
private PartialFactorResult() private PartialFactorResult()
{ {
} }
@@ -30,52 +37,58 @@ namespace PerfectNumbers.Core
s_poolHead = instance._next; s_poolHead = instance._next;
} }
FactorEntry[]? factorArray = factors;
instance._next = null; instance._next = null;
instance.Factors = factors; instance.Factors = factorArray;
instance.Cofactor = cofactor; instance.Cofactor = cofactor;
instance.FullyFactored = fullyFactored; instance.FullyFactored = fullyFactored;
instance.Count = count; instance.Count = count;
instance._factorsFromPool = factorArray is not null && factorArray.Length >= PerfectNumberConstants.PooledArrayThreshold;
return instance; return instance;
} }
public static readonly PartialFactorResult Empty = new PartialFactorResult
{
Cofactor = 1UL,
FullyFactored = true,
};
public PartialFactorResult WithAdditionalPrime(ulong prime) public PartialFactorResult WithAdditionalPrime(ulong prime)
{ {
FactorEntry[]? source = Factors; FactorEntry[]? source = Factors;
if (source is null || Count == 0) if (source is null || Count == 0)
{ {
FactorEntry[] local = ArrayPool<FactorEntry>.Shared.Rent(1); FactorEntry[] single = new FactorEntry[1];
local[0] = new FactorEntry(prime, 1); single[0] = new FactorEntry(prime);
return Rent(local, 1UL, true, 1); return Rent(single, 1UL, true, 1);
} }
FactorEntry[] extended = ArrayPool<FactorEntry>.Shared.Rent(Count + 1); int newCount = Count + 1;
FactorEntry[] extended = newCount >= PerfectNumberConstants.PooledArrayThreshold
? ThreadLocalArrayPool<FactorEntry>.Shared.Rent(newCount)
: new FactorEntry[newCount];
Array.Copy(source, 0, extended, 0, Count); Array.Copy(source, 0, extended, 0, Count);
extended[Count] = new FactorEntry(prime, 1); extended[Count] = new FactorEntry(prime);
Span<FactorEntry> span = extended.AsSpan(0, Count + 1); Span<FactorEntry> span = extended.AsSpan(0, newCount);
span.Sort(static (a, b) => a.Value.CompareTo(b.Value)); span.Sort(static (a, b) => a.Value.CompareTo(b.Value));
return Rent(extended, 1UL, true, Count + 1); return Rent(extended, 1UL, true, newCount);
} }
public void Dispose() public void Dispose()
{ {
if (this != Empty) if (this == Empty)
{ {
FactorEntry[]? factors = Factors; return;
if (factors is not null)
{
Factors = null;
ArrayPool<FactorEntry>.Shared.Return(factors, clearArray: false);
}
_next = s_poolHead;
s_poolHead = this;
} }
FactorEntry[]? factors = Factors;
if (factors is not null)
{
Factors = null;
if (_factorsFromPool)
{
ThreadLocalArrayPool<FactorEntry>.Shared.Return(factors, clearArray: false);
}
}
_factorsFromPool = false;
_next = s_poolHead;
s_poolHead = this;
} }
} }
} }
@@ -22,4 +22,7 @@
<InternalsVisibleTo Include="EvenPerfectBitScanner.Benchmarks" /> <InternalsVisibleTo Include="EvenPerfectBitScanner.Benchmarks" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project> </Project>
@@ -0,0 +1,150 @@
using System;
using System.Buffers;
namespace PerfectNumbers.Core
{
internal sealed class PrimeFactorPartialResult
{
[ThreadStatic]
private static PrimeFactorPartialResult? s_poolHead;
private PrimeFactorPartialResult? _next;
private ulong[]? _primes;
private byte[]? _exponents;
private bool _arraysFromPool;
private int _capacity;
private PrimeFactorPartialResult()
{
}
public static PrimeFactorPartialResult Rent(int capacity)
{
PrimeFactorPartialResult? instance = s_poolHead;
if (instance is null)
{
instance = new PrimeFactorPartialResult();
}
else
{
s_poolHead = instance._next;
}
instance._next = null;
instance.EnsureCapacity(capacity);
instance.Count = 0;
return instance;
}
public int Count { get; private set; }
internal ReadOnlySpan<ulong> Primes => _primes is null ? ReadOnlySpan<ulong>.Empty : _primes.AsSpan(0, Count);
internal ReadOnlySpan<byte> Exponents => _exponents is null ? ReadOnlySpan<byte>.Empty : _exponents.AsSpan(0, Count);
internal Span<ulong> PrimeWriteSpan => _primes is null ? Span<ulong>.Empty : _primes.AsSpan(0, _capacity);
internal Span<byte> ExponentWriteSpan => _exponents is null ? Span<byte>.Empty : _exponents.AsSpan(0, _capacity);
internal void CommitCount(int count)
{
Count = count;
}
internal void Sort()
{
if (Count == 0 || _primes is null || _exponents is null)
{
return;
}
Array.Sort(_primes, _exponents, 0, Count);
}
internal MersenneDivisorCycles.FactorCacheEntry ToCacheEntry()
{
if (Count == 0 || _primes is null || _exponents is null)
{
return default;
}
ulong[] primes = new ulong[Count];
byte[] exponents = new byte[Count];
Array.Copy(_primes, 0, primes, 0, Count);
Array.Copy(_exponents, 0, exponents, 0, Count);
return new MersenneDivisorCycles.FactorCacheEntry(primes, exponents, Count);
}
public void Dispose()
{
if (_arraysFromPool)
{
if (_primes is not null)
{
ThreadLocalArrayPool<ulong>.Shared.Return(_primes, clearArray: false);
}
if (_exponents is not null)
{
ThreadLocalArrayPool<byte>.Shared.Return(_exponents, clearArray: false);
}
_arraysFromPool = false;
_primes = null;
_exponents = null;
}
_capacity = 0;
Count = 0;
_next = s_poolHead;
s_poolHead = this;
}
private void EnsureCapacity(int capacity)
{
if (capacity <= 0)
{
_capacity = 0;
return;
}
if (_primes is not null && _primes.Length >= capacity)
{
_capacity = capacity;
return;
}
if (_arraysFromPool)
{
if (_primes is not null)
{
ThreadLocalArrayPool<ulong>.Shared.Return(_primes, clearArray: false);
}
if (_exponents is not null)
{
ThreadLocalArrayPool<byte>.Shared.Return(_exponents, clearArray: false);
}
_arraysFromPool = false;
_primes = null;
_exponents = null;
}
if (capacity >= PerfectNumberConstants.PooledArrayThreshold)
{
_primes = ThreadLocalArrayPool<ulong>.Shared.Rent(capacity);
_exponents = ThreadLocalArrayPool<byte>.Shared.Rent(capacity);
_arraysFromPool = true;
}
else
{
_primes = new ulong[capacity];
_exponents = new byte[capacity];
}
_capacity = capacity;
}
}
}
@@ -162,7 +162,7 @@ internal static partial class PrimeOrderCalculator
return order; return order;
} }
tempArray = ArrayPool<FactorEntry128>.Shared.Rent(length + 1); tempArray = ThreadLocalArrayPool<FactorEntry128>.Shared.Rent(length + 1);
Span<FactorEntry128> buffer = tempArray.AsSpan(0, length); Span<FactorEntry128> buffer = tempArray.AsSpan(0, length);
factorSpan.CopyTo(buffer); factorSpan.CopyTo(buffer);
@@ -200,7 +200,7 @@ internal static partial class PrimeOrderCalculator
{ {
if (tempArray is not null) if (tempArray is not null)
{ {
ArrayPool<FactorEntry128>.Shared.Return(tempArray, clearArray: false); ThreadLocalArrayPool<FactorEntry128>.Shared.Return(tempArray, clearArray: false);
} }
} }
} }
@@ -664,7 +664,7 @@ internal static partial class PrimeOrderCalculator
return new PartialFactorResult128(null, value, false, 0); return new PartialFactorResult128(null, value, false, 0);
} }
FactorEntry128[]? rented = counts.Count > 0 ? ArrayPool<FactorEntry128>.Shared.Rent(counts.Count) : null; FactorEntry128[]? rented = counts.Count > 0 ? ThreadLocalArrayPool<FactorEntry128>.Shared.Rent(counts.Count) : null;
int index = 0; int index = 0;
if (rented is not null) if (rented is not null)
{ {
@@ -682,7 +682,7 @@ internal static partial class PrimeOrderCalculator
span.Sort(static (a, b) => a.Value.CompareTo(b.Value)); span.Sort(static (a, b) => a.Value.CompareTo(b.Value));
resultArray = new FactorEntry128[index]; resultArray = new FactorEntry128[index];
span.CopyTo(resultArray); span.CopyTo(resultArray);
ArrayPool<FactorEntry128>.Shared.Return(rented, clearArray: false); ThreadLocalArrayPool<FactorEntry128>.Shared.Return(rented, clearArray: false);
} }
bool fullyFactored = cofactor == UInt128.One; bool fullyFactored = cofactor == UInt128.One;
@@ -1062,15 +1062,20 @@ internal static partial class PrimeOrderCalculator
private readonly struct FactorEntry128 private readonly struct FactorEntry128
{ {
public FactorEntry128(UInt128 value, int exponent) public FactorEntry128(UInt128 value)
{ {
Value = value; Value = value;
Exponent = exponent; }
}
public UInt128 Value { get; } public FactorEntry128(UInt128 value, int exponent)
{
Value = value;
Exponent = exponent;
}
public int Exponent { get; } public readonly UInt128 Value;
public readonly int Exponent = 1;
} }
private readonly struct PartialFactorResult128 private readonly struct PartialFactorResult128
@@ -1083,13 +1088,13 @@ internal static partial class PrimeOrderCalculator
Count = count; Count = count;
} }
public FactorEntry128[]? Factors { get; } public readonly FactorEntry128[]? Factors;
public UInt128 Cofactor { get; } public readonly UInt128 Cofactor;
public bool FullyFactored { get; } public readonly bool FullyFactored;
public int Count { get; } public readonly int Count;
public static PartialFactorResult128 Empty => new(null, UInt128.One, true, 0); public static PartialFactorResult128 Empty => new(null, UInt128.One, true, 0);
@@ -1097,14 +1102,13 @@ internal static partial class PrimeOrderCalculator
{ {
if (Factors is null) if (Factors is null)
{ {
FactorEntry128[] local = new FactorEntry128[1]; FactorEntry128[] local = [new FactorEntry128(prime)];
local[0] = new FactorEntry128(prime, 1); return new PartialFactorResult128(local, UInt128.One, true, 1);
return new PartialFactorResult128(local, UInt128.One, true, 1);
} }
FactorEntry128[] extended = new FactorEntry128[Count + 1]; FactorEntry128[] extended = new FactorEntry128[Count + 1];
Array.Copy(Factors, extended, Count); Array.Copy(Factors, extended, Count);
extended[Count] = new FactorEntry128(prime, 1); extended[Count] = new FactorEntry128(prime);
return new PartialFactorResult128(extended, UInt128.One, true, Count + 1); return new PartialFactorResult128(extended, UInt128.One, true, Count + 1);
} }
} }
+372 -226
View File
@@ -18,7 +18,7 @@ internal static partial class PrimeOrderCalculator
internal readonly struct PrimeOrderSearchConfig(uint smallFactorLimit, int pollardRhoMilliseconds, int maxPowChecks, PrimeOrderMode mode) internal readonly struct PrimeOrderSearchConfig(uint smallFactorLimit, int pollardRhoMilliseconds, int maxPowChecks, PrimeOrderMode mode)
{ {
public static PrimeOrderSearchConfig HeuristicDefault => new(smallFactorLimit: 100_000, pollardRhoMilliseconds: 24, maxPowChecks: 24, PrimeOrderMode.Heuristic); public static PrimeOrderSearchConfig HeuristicDefault => new(smallFactorLimit: 4_000_000, pollardRhoMilliseconds: 24 * 256, maxPowChecks: 24, PrimeOrderMode.Heuristic);
public static PrimeOrderSearchConfig StrictDefault => new(smallFactorLimit: 1_000_000, pollardRhoMilliseconds: 0, maxPowChecks: 0, PrimeOrderMode.Strict); public static PrimeOrderSearchConfig StrictDefault => new(smallFactorLimit: 1_000_000, pollardRhoMilliseconds: 0, maxPowChecks: 0, PrimeOrderMode.Strict);
public readonly uint SmallFactorLimit = smallFactorLimit; public readonly uint SmallFactorLimit = smallFactorLimit;
@@ -39,8 +39,13 @@ internal static partial class PrimeOrderCalculator
[ThreadStatic] [ThreadStatic]
private static bool s_allowGpuPow2; private static bool s_allowGpuPow2;
[ThreadStatic]
private static bool s_allowGpuCycle;
private static bool IsGpuPow2Allowed => s_pow2ModeInitialized && s_allowGpuPow2; private static bool IsGpuPow2Allowed => s_pow2ModeInitialized && s_allowGpuPow2;
private static bool IsGpuCycleAllowed => s_pow2ModeInitialized && s_allowGpuCycle;
[ThreadStatic] [ThreadStatic]
private static PrimeOrderHeuristicDevice s_deviceMode; private static PrimeOrderHeuristicDevice s_deviceMode;
@@ -56,27 +61,49 @@ internal static partial class PrimeOrderCalculator
[ThreadStatic] [ThreadStatic]
private static Dictionary<ulong, int>? s_partialFactorCounts; private static Dictionary<ulong, int>? s_partialFactorCounts;
[ThreadStatic]
private static ulong s_cachedDivisorCycleModulus;
[ThreadStatic]
private static ulong s_cachedDivisorCycleLength;
[ThreadStatic]
private static bool s_hasCachedDivisorCycle;
[ThreadStatic]
private static int s_divisorCycleSuppressionDepth;
[ThreadStatic]
private static MutableUInt128 s_pollardRhoPolynomialBuffer;
private static bool IsGpuHeuristicDevice => s_pow2ModeInitialized && s_deviceMode == PrimeOrderHeuristicDevice.Gpu; private static bool IsGpuHeuristicDevice => s_pow2ModeInitialized && s_deviceMode == PrimeOrderHeuristicDevice.Gpu;
private readonly struct Pow2ModeScope private readonly struct Pow2ModeScope
{ {
private readonly bool _previousInitialized; private readonly bool _previousInitialized;
private readonly bool _previousAllow; private readonly bool _previousAllowPow2;
private readonly bool _previousAllowCycle;
private readonly PrimeOrderHeuristicDevice _previousDevice; private readonly PrimeOrderHeuristicDevice _previousDevice;
public Pow2ModeScope(PrimeOrderHeuristicDevice device) public Pow2ModeScope(PrimeOrderHeuristicDevice device)
{ {
_previousInitialized = s_pow2ModeInitialized; _previousInitialized = s_pow2ModeInitialized;
_previousAllow = s_allowGpuPow2; _previousAllowPow2 = s_allowGpuPow2;
_previousAllowCycle = s_allowGpuCycle;
_previousDevice = s_deviceMode; _previousDevice = s_deviceMode;
s_pow2ModeInitialized = true; s_pow2ModeInitialized = true;
s_allowGpuPow2 = device == PrimeOrderHeuristicDevice.Gpu; bool gpuAvailable = !GpuContextPool.ForceCpu;
bool allowCpuPow2 = gpuAvailable && device == PrimeOrderHeuristicDevice.Cpu;
bool allowCpuCycle = gpuAvailable && device == PrimeOrderHeuristicDevice.Cpu && GpuKernelPool.CpuCycleUsesGpu;
s_allowGpuPow2 = device == PrimeOrderHeuristicDevice.Gpu || allowCpuPow2;
s_allowGpuCycle = device == PrimeOrderHeuristicDevice.Gpu || allowCpuCycle;
s_deviceMode = device; s_deviceMode = device;
} }
public void Dispose() public void Dispose()
{ {
s_allowGpuPow2 = _previousAllow; s_allowGpuPow2 = _previousAllowPow2;
s_allowGpuCycle = _previousAllowCycle;
s_pow2ModeInitialized = _previousInitialized; s_pow2ModeInitialized = _previousInitialized;
s_deviceMode = _previousDevice; s_deviceMode = _previousDevice;
} }
@@ -98,10 +125,45 @@ internal static partial class PrimeOrderCalculator
} }
} }
private static bool IsDivisorCycleSuppressed => s_divisorCycleSuppressionDepth > 0;
private readonly struct DivisorCycleSuppressionScope
{
private readonly bool _suppress;
private DivisorCycleSuppressionScope(bool suppress)
{
_suppress = suppress;
}
public static readonly DivisorCycleSuppressionScope Suppressed = new(true);
public static readonly DivisorCycleSuppressionScope Unsuppressed = new(false);
public void Enter()
{
if (_suppress)
{
s_divisorCycleSuppressionDepth++;
}
}
public void Exit()
{
if (_suppress)
{
s_divisorCycleSuppressionDepth--;
}
}
}
private static Pow2ModeScope UsePow2Mode(PrimeOrderHeuristicDevice device) => new(device); private static Pow2ModeScope UsePow2Mode(PrimeOrderHeuristicDevice device) => new(device);
private static DebugLoggingScope UseDebugLogging(bool enabled) => new(enabled); private static DebugLoggingScope UseDebugLogging(bool enabled) => new(enabled);
private static DivisorCycleSuppressionScope SuppressDivisorCycleUsage(bool suppress) =>
suppress ? DivisorCycleSuppressionScope.Suppressed : DivisorCycleSuppressionScope.Unsuppressed;
[Conditional("DEBUG")] [Conditional("DEBUG")]
private static void DebugLog(string message) private static void DebugLog(string message)
{ {
@@ -135,13 +197,16 @@ internal static partial class PrimeOrderCalculator
private static ulong CalculateInternal(ulong prime, ulong? previousOrder, in MontgomeryDivisorData divisorData, in PrimeOrderSearchConfig config) private static ulong CalculateInternal(ulong prime, ulong? previousOrder, in MontgomeryDivisorData divisorData, in PrimeOrderSearchConfig config)
{ {
if (prime <= 3UL) // EvenPerfectBitScanner only reaches this helper with divisors of the form 2 * exponent * k + 1 where exponent >= 5,
{ // so the small-prime return path is unreachable in production.
return prime == 3UL ? 2UL : 1UL; // if (prime <= 3UL)
} // {
// return prime == 3UL ? 2UL : 1UL;
// }
ulong phi = prime - 1UL; ulong phi = prime - 1UL;
// The by-divisor scanner currently defaults to the CPU heuristic device, but --mersenne-device may request GPU heuristics.
if (IsGpuHeuristicDevice && PrimeOrderGpuHeuristics.TryCalculateOrder(prime, previousOrder, config, divisorData, out ulong gpuOrder)) if (IsGpuHeuristicDevice && PrimeOrderGpuHeuristics.TryCalculateOrder(prime, previousOrder, config, divisorData, out ulong gpuOrder))
{ {
return gpuOrder; return gpuOrder;
@@ -150,12 +215,14 @@ internal static partial class PrimeOrderCalculator
PartialFactorResult phiFactors = PartialFactor(phi, config); PartialFactorResult phiFactors = PartialFactor(phi, config);
ulong result; ulong result;
if (phiFactors.Factors is null) // The by-divisor scan always yields phi = prime - 1 with at least the factor 2 captured during small-prime sieving,
{ // so PartialFactor never returns a null factor list on this call path.
result = FinishStrictlyCpu(prime, divisorData); // if (phiFactors.Factors is null)
phiFactors.Dispose(); // {
return result; // result = CalculateByFactorizationCpu(prime, divisorData, config);
} // phiFactors.Dispose();
// return result;
// }
result = RunHeuristicPipelineCpu(prime, previousOrder, config, divisorData, phi, phiFactors); result = RunHeuristicPipelineCpu(prime, previousOrder, config, divisorData, phi, phiFactors);
phiFactors.Dispose(); phiFactors.Dispose();
@@ -170,35 +237,52 @@ internal static partial class PrimeOrderCalculator
ulong phi, ulong phi,
PartialFactorResult phiFactors) PartialFactorResult phiFactors)
{ {
if (phiFactors.FullyFactored && TrySpecialMaxCpu(phi, prime, phiFactors, divisorData)) var cycleScope = SuppressDivisorCycleUsage(config.Mode == PrimeOrderMode.Heuristic && !IsGpuHeuristicDevice);
cycleScope.Enter();
try
{ {
return phi; if (phiFactors.FullyFactored && TrySpecialMaxCpu(phi, prime, phiFactors, divisorData))
} {
return phi;
}
ulong candidateOrder = InitializeStartingOrderCpu(prime, phi, divisorData); ulong candidateOrder = InitializeStartingOrderCpu(prime, phi, divisorData);
candidateOrder = ExponentLoweringCpu(candidateOrder, prime, phiFactors, divisorData); candidateOrder = ExponentLoweringCpu(candidateOrder, prime, phiFactors, divisorData);
if (TryConfirmOrderCpu(prime, candidateOrder, divisorData, config)) PartialFactorResult orderFactors = PartialFactor(candidateOrder, config);
{ if (orderFactors.Factors is null)
{
orderFactors.Dispose();
return candidateOrder;
}
// Reuse the computed factorization for both the confirmation check and the heuristic pass
// to avoid factoring the same order twice on the by-divisor CPU pipeline.
if (TryConfirmOrderCpu(prime, candidateOrder, divisorData, ref orderFactors))
{
orderFactors.Dispose();
return candidateOrder;
}
if (config.Mode == PrimeOrderMode.Strict)
{
orderFactors.Dispose();
return CalculateByFactorizationCpu(prime, divisorData, config);
}
if (TryHeuristicFinishCpu(prime, candidateOrder, previousOrder, divisorData, config, ref orderFactors, phiFactors, out ulong order))
{
orderFactors.Dispose();
return order;
}
orderFactors.Dispose();
return candidateOrder; return candidateOrder;
} }
finally
if (config.Mode == PrimeOrderMode.Strict)
{ {
return FinishStrictlyCpu(prime, divisorData); cycleScope.Exit();
} }
if (TryHeuristicFinishCpu(prime, candidateOrder, previousOrder, divisorData, config, phiFactors, out ulong order))
{
return order;
}
return candidateOrder;
}
private static ulong FinishStrictlyCpu(ulong prime, in MontgomeryDivisorData divisorData)
{
return CalculateByFactorizationCpu(prime, divisorData);
} }
private static bool TrySpecialMaxCpu(ulong phi, ulong prime, PartialFactorResult factors, in MontgomeryDivisorData divisorData) private static bool TrySpecialMaxCpu(ulong phi, ulong prime, PartialFactorResult factors, in MontgomeryDivisorData divisorData)
@@ -233,31 +317,52 @@ internal static partial class PrimeOrderCalculator
return order; return order;
} }
[ThreadStatic]
private static ArrayPool<FactorEntry>? _factorPool;
private static ArrayPool<FactorEntry> FactorPool => _factorPool ??= ArrayPool<FactorEntry>.Create();
private const int FactorEntryStackThreshold = 32;
[ThreadStatic]
private static ArrayPool<ulong>? _ulongPool;
private static ArrayPool<ulong> ULongPool => _ulongPool ??= ArrayPool<ulong>.Create();
private static ulong ExponentLoweringCpu(ulong order, ulong prime, PartialFactorResult factors, in MontgomeryDivisorData divisorData) private static ulong ExponentLoweringCpu(ulong order, ulong prime, PartialFactorResult factors, in MontgomeryDivisorData divisorData)
{ {
ArrayPool<FactorEntry> factorPool = FactorPool;
FactorEntry[]? tempArray = null; FactorEntry[]? tempArray = null;
ReadOnlySpan<FactorEntry> factorSpan = factors.Factors;
int length = factors.Count;
int capacity = length + 1;
Span<FactorEntry> buffer = capacity <= FactorEntryStackThreshold ? stackalloc FactorEntry[capacity] : default;
if (capacity > FactorEntryStackThreshold)
{
tempArray = factorPool.Rent(capacity);
buffer = tempArray.AsSpan(0, capacity);
}
try try
{ {
ReadOnlySpan<FactorEntry> factorSpan = factors.Factors;
int length = factors.Count;
tempArray = ArrayPool<FactorEntry>.Shared.Rent(length + 1);
Span<FactorEntry> buffer = tempArray;
factorSpan.CopyTo(buffer); factorSpan.CopyTo(buffer);
bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(factors.Cofactor); bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(factors.Cofactor);
// bool isPrime = PrimeTester.IsPrimeInternal(factors.Cofactor, CancellationToken.None); // bool isPrime = PrimeTester.IsPrimeInternal(factors.Cofactor, CancellationToken.None);
if (!factors.FullyFactored && factors.Cofactor > 1UL && isPrime) if (!factors.FullyFactored && factors.Cofactor > 1UL && isPrime)
{ {
buffer[length] = new FactorEntry(factors.Cofactor, 1); buffer[length] = new FactorEntry(factors.Cofactor);
length++; length++;
} }
buffer.Slice(0, length).Sort(static (a, b) => a.Value.CompareTo(b.Value)); buffer = buffer.Slice(0, length); // Reuse the same span restricted to populated entries.
buffer.Sort(static (a, b) => a.Value.CompareTo(b.Value));
for (int i = 0; i < length; i++) for (int i = 0; i < buffer.Length; i++)
{ {
ulong primeFactor = buffer[i].Value; FactorEntry entry = buffer[i];
int exponent = buffer[i].Exponent; ulong primeFactor = entry.Value;
int exponent = entry.Exponent;
for (int iteration = 0; iteration < exponent; iteration++) for (int iteration = 0; iteration < exponent; iteration++)
{ {
if ((order % primeFactor) != 0UL) if ((order % primeFactor) != 0UL)
@@ -282,17 +387,18 @@ internal static partial class PrimeOrderCalculator
{ {
if (tempArray is not null) if (tempArray is not null)
{ {
ArrayPool<FactorEntry>.Shared.Return(tempArray, clearArray: false); factorPool.Return(tempArray, clearArray: false);
} }
} }
} }
private static bool TryConfirmOrderCpu(ulong prime, ulong order, in MontgomeryDivisorData divisorData, in PrimeOrderSearchConfig config) private static bool TryConfirmOrderCpu(ulong prime, ulong order, in MontgomeryDivisorData divisorData, ref PartialFactorResult orderFactors)
{ {
if (order == 0UL) // The candidate order is always a positive divisor of phi in this flow, so zero never appears here.
{ // if (order == 0UL)
return false; // {
} // return false;
// }
// Calculating `a^order ≡ 1 (mod p)` is a prerequisite for `order` being the actual order of 2 modulo `p`. // Calculating `a^order ≡ 1 (mod p)` is a prerequisite for `order` being the actual order of 2 modulo `p`.
// DebugLog("Verifying a^order ≡ 1 (mod p)"); // DebugLog("Verifying a^order ≡ 1 (mod p)");
@@ -301,66 +407,57 @@ internal static partial class PrimeOrderCalculator
return false; return false;
} }
// DebugLog("Partial factoring order"); // PartialFactor returns a null factor span when it cannot peel off any divisors, so keep the guard for composite candidate orders.
if (orderFactors.Factors is null)
// TODO: Do we do partial factoring of order multiple times?
PartialFactorResult factorization = PartialFactor(order, config);
try
{ {
if (factorization.Factors is null) return false;
}
if (!orderFactors.FullyFactored)
{
// A leftover cofactor of 0 or 1 indicates the partial factorization produced no useful prime divisor.
if (orderFactors.Cofactor <= 1UL)
{ {
return false; return false;
} }
if (!factorization.FullyFactored) // DebugLog("Cofactor > 1. Testing primality of cofactor");
bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(orderFactors.Cofactor);
// bool isPrime = PrimeTester.IsPrimeInternal(orderFactors.Cofactor, CancellationToken.None);
if (!isPrime)
{ {
if (factorization.Cofactor <= 1UL) return false;
{
return false;
}
// DebugLog("Cofactor > 1. Testing primality of cofactor");
bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(factorization.Cofactor);
// bool isPrime = PrimeTester.IsPrimeInternal(factorization.Cofactor, CancellationToken.None);
if (!isPrime)
{
return false;
}
// DebugLog("Adding cofactor as prime factor");
PartialFactorResult extended = factorization.WithAdditionalPrime(factorization.Cofactor);
factorization.Dispose();
factorization = extended;
} }
ReadOnlySpan<FactorEntry> span = factorization.Factors; // DebugLog("Adding cofactor as prime factor");
// DebugLog("Verifying prime-power reductions"); PartialFactorResult extended = orderFactors.WithAdditionalPrime(orderFactors.Cofactor);
int length = factorization.Count; orderFactors.Dispose();
for (int i = 0; i < length; i++) orderFactors = extended;
{
ulong primeFactor = span[i].Value;
ulong reduced = order;
for (int iteration = 0; iteration < span[i].Exponent; iteration++)
{
if ((reduced % primeFactor) != 0UL)
{
break;
}
reduced /= primeFactor;
if (Pow2EqualsOneCpu(reduced, prime, divisorData))
{
return false;
}
}
}
return true;
} }
finally
ReadOnlySpan<FactorEntry> span = orderFactors.Factors!;
// DebugLog("Verifying prime-power reductions");
int length = orderFactors.Count;
for (int i = 0; i < length; i++)
{ {
factorization.Dispose(); ulong primeFactor = span[i].Value;
ulong reduced = order;
for (int iteration = 0; iteration < span[i].Exponent; iteration++)
{
if ((reduced % primeFactor) != 0UL)
{
break;
}
reduced /= primeFactor;
if (Pow2EqualsOneCpu(reduced, prime, divisorData))
{
return false;
}
}
} }
return true;
} }
private static bool TryHeuristicFinishCpu( private static bool TryHeuristicFinishCpu(
@@ -369,169 +466,164 @@ internal static partial class PrimeOrderCalculator
ulong? previousOrder, ulong? previousOrder,
in MontgomeryDivisorData divisorData, in MontgomeryDivisorData divisorData,
in PrimeOrderSearchConfig config, in PrimeOrderSearchConfig config,
ref PartialFactorResult orderFactors,
PartialFactorResult phiFactors, PartialFactorResult phiFactors,
out ulong result) out ulong result)
{ {
result = 0UL; result = 0UL;
if (order <= 1UL) // The candidate order stays at least 2 for odd divisors above 3, so this guard never triggers in the EvenPerfectBitScanner flow.
// if (order <= 1UL)
// {
// return false;
// }
// PartialFactor returns a null factor span when it cannot peel off any divisors, so the heuristic pass must bail in that case.
if (orderFactors.Factors is null)
{ {
return false; return false;
} }
// TODO: Do we do partial factoring of order multiple times? if (!orderFactors.FullyFactored)
// DebugLog("Trying heuristic. Partial factoring order");
PartialFactorResult orderFactors = PartialFactor(order, config);
try
{ {
if (orderFactors.Factors is null) // The partial factoring helper reports a cofactor of 0 or 1 when nothing usable remains, so short-circuit the heuristic stage.
if (orderFactors.Cofactor <= 1UL)
{ {
return false; return false;
} }
if (!orderFactors.FullyFactored) var isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(orderFactors.Cofactor);
{ // if (!PrimeTester.IsPrimeInternal(orderFactors.Cofactor, CancellationToken.None))
if (orderFactors.Cofactor <= 1UL) if (!isPrime)
{
return false;
}
var isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(orderFactors.Cofactor);
// if (!PrimeTester.IsPrimeInternal(orderFactors.Cofactor, CancellationToken.None))
if (!isPrime)
{
return false;
}
PartialFactorResult extended = orderFactors.WithAdditionalPrime(orderFactors.Cofactor);
orderFactors.Dispose();
orderFactors = extended;
}
int capacity = config.MaxPowChecks <= 0 ? 64 : config.MaxPowChecks * 4;
List<ulong> candidates = new(capacity);
FactorEntry[] factorArray = orderFactors.Factors!;
// DebugLog("Building candidates list");
BuildCandidates(order, factorArray, orderFactors.Count, candidates, capacity);
if (candidates.Count == 0)
{ {
return false; return false;
} }
// DebugLog("Sorting candidates"); PartialFactorResult extended = orderFactors.WithAdditionalPrime(orderFactors.Cofactor);
SortCandidates(prime, previousOrder, candidates); orderFactors.Dispose();
orderFactors = extended;
}
int powBudget = config.MaxPowChecks <= 0 ? candidates.Count : config.MaxPowChecks; int capacity = config.MaxPowChecks <= 0 ? 64 : config.MaxPowChecks * 4;
int powUsed = 0; List<ulong> candidates = new(capacity);
int candidateCount = candidates.Count; FactorEntry[] factorArray = orderFactors.Factors!;
bool allowGpuBatch = true; // DebugLog("Building candidates list");
Span<ulong> candidateSpan = CollectionsMarshal.AsSpan(candidates); BuildCandidates(order, factorArray, orderFactors.Count, candidates, capacity);
if (candidates.Count == 0)
{
return false;
}
// DebugLog(() => $"Checking candidates ({candidateCount} candidates, {powBudget} pow budget)"); // DebugLog("Sorting candidates");
int index = 0; SortCandidates(prime, previousOrder, candidates);
const int MaxGpuBatchSize = 1024;
const int StackGpuBatchSize = 1024; int powBudget = config.MaxPowChecks <= 0 ? candidates.Count : config.MaxPowChecks;
Span<ulong> stackGpuRemainders = stackalloc ulong[StackGpuBatchSize]; int powUsed = 0;
while (index < candidateCount && powUsed < powBudget) int candidateCount = candidates.Count;
bool allowGpuBatch = true;
Span<ulong> candidateSpan = CollectionsMarshal.AsSpan(candidates);
// DebugLog(() => $"Checking candidates ({candidateCount} candidates, {powBudget} pow budget)");
int index = 0;
const int MaxGpuBatchSize = 1024;
const int StackGpuBatchSize = 1024;
Span<ulong> stackGpuRemainders = stackalloc ulong[StackGpuBatchSize];
ArrayPool<ulong> uLongPool = ULongPool;
while (index < candidateCount && powUsed < powBudget)
{
int remaining = candidateCount - index;
int budgetRemaining = powBudget - powUsed;
int batchSize = Math.Min(remaining, Math.Min(budgetRemaining, MaxGpuBatchSize));
if (batchSize <= 0)
{ {
int remaining = candidateCount - index; break;
int budgetRemaining = powBudget - powUsed; }
int batchSize = Math.Min(remaining, Math.Min(budgetRemaining, MaxGpuBatchSize));
if (batchSize <= 0)
{
break;
}
ReadOnlySpan<ulong> batch = candidateSpan.Slice(index, batchSize); ReadOnlySpan<ulong> batch = candidateSpan.Slice(index, batchSize);
ulong[]? gpuPool = null; ulong[]? gpuPool = null;
Span<ulong> pooledGpuRemainders = default; Span<ulong> pooledGpuRemainders = default;
bool gpuSuccess = false; bool gpuSuccess = false;
bool gpuStackRemainders = false; bool gpuStackRemainders = false;
GpuPow2ModStatus status = GpuPow2ModStatus.Unavailable; GpuPow2ModStatus status = GpuPow2ModStatus.Unavailable;
if (allowGpuBatch && IsGpuPow2Allowed) if (allowGpuBatch && IsGpuPow2Allowed)
{
if (batchSize <= StackGpuBatchSize)
{ {
if (batchSize <= StackGpuBatchSize) Span<ulong> localRemainders = stackGpuRemainders.Slice(0, batchSize);
status = PrimeOrderGpuHeuristics.TryPow2ModBatch(batch, prime, localRemainders, divisorData);
if (status == GpuPow2ModStatus.Success)
{ {
Span<ulong> localRemainders = stackGpuRemainders.Slice(0, batchSize); gpuSuccess = true;
status = PrimeOrderGpuHeuristics.TryPow2ModBatch(batch, prime, localRemainders, divisorData); gpuStackRemainders = true;
if (status == GpuPow2ModStatus.Success) }
{ }
gpuSuccess = true; else
gpuStackRemainders = true; {
} gpuPool = uLongPool.Rent(batchSize);
Span<ulong> pooledRemainders = gpuPool.AsSpan(0, batchSize);
status = PrimeOrderGpuHeuristics.TryPow2ModBatch(batch, prime, pooledRemainders, divisorData);
if (status == GpuPow2ModStatus.Success)
{
pooledGpuRemainders = pooledRemainders;
gpuSuccess = true;
} }
else else
{ {
gpuPool = ArrayPool<ulong>.Shared.Rent(batchSize); uLongPool.Return(gpuPool, clearArray: false);
Span<ulong> pooledRemainders = gpuPool.AsSpan(0, batchSize); gpuPool = null;
status = PrimeOrderGpuHeuristics.TryPow2ModBatch(batch, prime, pooledRemainders, divisorData);
if (status == GpuPow2ModStatus.Success)
{
pooledGpuRemainders = pooledRemainders;
gpuSuccess = true;
}
else
{
ArrayPool<ulong>.Shared.Return(gpuPool, clearArray: false);
gpuPool = null;
}
}
if (!gpuSuccess && (status == GpuPow2ModStatus.Overflow || status == GpuPow2ModStatus.Unavailable))
{
allowGpuBatch = false;
} }
} }
for (int i = 0; i < batchSize && powUsed < powBudget; i++) if (!gpuSuccess && (status == GpuPow2ModStatus.Overflow || status == GpuPow2ModStatus.Unavailable))
{ {
ulong candidate = batch[i]; allowGpuBatch = false;
powUsed++; }
}
bool equalsOne; for (int i = 0; i < batchSize && powUsed < powBudget; i++)
if (gpuSuccess) {
{ ulong candidate = batch[i];
ulong remainderValue = gpuStackRemainders ? stackGpuRemainders[i] : pooledGpuRemainders[i]; powUsed++;
equalsOne = remainderValue == 1UL;
}
else
{
equalsOne = Pow2EqualsOneCpu(candidate, prime, divisorData);
}
if (!equalsOne)
{
continue;
}
if (!TryConfirmCandidateCpu(prime, candidate, divisorData, config, ref powUsed, powBudget)) bool equalsOne;
{ if (gpuSuccess)
continue; {
} ulong remainderValue = gpuStackRemainders ? stackGpuRemainders[i] : pooledGpuRemainders[i];
equalsOne = remainderValue == 1UL;
}
else
{
equalsOne = Pow2EqualsOneCpu(candidate, prime, divisorData);
}
if (!equalsOne)
{
continue;
}
if (gpuPool is not null) if (!TryConfirmCandidateCpu(prime, candidate, divisorData, config, ref powUsed, powBudget))
{ {
ArrayPool<ulong>.Shared.Return(gpuPool, clearArray: false); continue;
}
result = candidate;
return true;
} }
if (gpuPool is not null) if (gpuPool is not null)
{ {
ArrayPool<ulong>.Shared.Return(gpuPool, clearArray: false); uLongPool.Return(gpuPool, clearArray: false);
} }
index += batchSize; result = candidate;
return true;
} }
// DebugLog("No candidate confirmed"); if (gpuPool is not null)
return false; {
} uLongPool.Return(gpuPool, clearArray: false);
finally }
{
orderFactors.Dispose(); index += batchSize;
} }
// DebugLog("No candidate confirmed");
return false;
} }
private static void SortCandidates(ulong prime, ulong? previousOrder, List<ulong> candidates) private static void SortCandidates(ulong prime, ulong? previousOrder, List<ulong> candidates)
@@ -765,6 +857,36 @@ internal static partial class PrimeOrderCalculator
} }
} }
private static bool TryGetDivisorCycle(ulong prime, in MontgomeryDivisorData divisorData, out ulong cycleLength)
{
if (prime <= 1UL || IsDivisorCycleSuppressed)
{
cycleLength = 0UL;
return false;
}
if (s_hasCachedDivisorCycle && s_cachedDivisorCycleModulus == prime)
{
cycleLength = s_cachedDivisorCycleLength;
return cycleLength != 0UL;
}
var suppression = SuppressDivisorCycleUsage(true);
suppression.Enter();
try
{
cycleLength = MersenneDivisorCycles.CalculateCycleLength(prime, divisorData);
s_cachedDivisorCycleModulus = prime;
s_cachedDivisorCycleLength = cycleLength;
s_hasCachedDivisorCycle = true;
return cycleLength != 0UL;
}
finally
{
suppression.Exit();
}
}
private static bool Pow2EqualsOneCpu(ulong exponent, ulong prime, in MontgomeryDivisorData divisorData) private static bool Pow2EqualsOneCpu(ulong exponent, ulong prime, in MontgomeryDivisorData divisorData)
{ {
if (IsGpuPow2Allowed) if (IsGpuPow2Allowed)
@@ -776,6 +898,11 @@ internal static partial class PrimeOrderCalculator
} }
} }
if (TryGetDivisorCycle(prime, divisorData, out ulong cycleLength) && cycleLength != 0UL)
{
return exponent.Pow2MontgomeryModWithCycleCpu(cycleLength, divisorData) == 1UL;
}
return exponent.Pow2MontgomeryModWindowedCpu(divisorData, keepMontgomery: false) == 1UL; return exponent.Pow2MontgomeryModWindowedCpu(divisorData, keepMontgomery: false) == 1UL;
} }
@@ -945,7 +1072,10 @@ internal static partial class PrimeOrderCalculator
return PartialFactorResult.Rent(null, cofactor, cofactor == 1UL, 0); return PartialFactorResult.Rent(null, cofactor, cofactor == 1UL, 0);
} }
FactorEntry[] factors = ArrayPool<FactorEntry>.Shared.Rent(counts.Count); int dictionaryCount = counts.Count;
FactorEntry[] factors = dictionaryCount >= PerfectNumberConstants.PooledArrayThreshold
? ThreadLocalArrayPool<FactorEntry>.Shared.Rent(dictionaryCount)
: new FactorEntry[dictionaryCount];
int index = 0; int index = 0;
foreach (KeyValuePair<ulong, int> entry in counts) foreach (KeyValuePair<ulong, int> entry in counts)
{ {
@@ -979,7 +1109,9 @@ internal static partial class PrimeOrderCalculator
return PartialFactorResult.Rent(null, cofactor, cofactor == 1UL, 0); return PartialFactorResult.Rent(null, cofactor, cofactor == 1UL, 0);
} }
FactorEntry[] array = ArrayPool<FactorEntry>.Shared.Rent(actualCount); FactorEntry[] array = actualCount >= PerfectNumberConstants.PooledArrayThreshold
? ThreadLocalArrayPool<FactorEntry>.Shared.Rent(actualCount)
: new FactorEntry[actualCount];
int arrayIndex = 0; int arrayIndex = 0;
for (int i = 0; i < factorCount; i++) for (int i = 0; i < factorCount; i++)
{ {
@@ -1193,8 +1325,11 @@ internal static partial class PrimeOrderCalculator
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong AdvancePolynomial(ulong x, ulong c, ulong modulus) private static ulong AdvancePolynomial(ulong x, ulong c, ulong modulus)
{ {
UInt128 value = (UInt128)x * x + c; ref MutableUInt128 polynomial = ref s_pollardRhoPolynomialBuffer;
return (ulong)(value % modulus); polynomial.Set(x);
polynomial.Mul(x);
polynomial.Add(c);
return polynomial.Mod(modulus);
} }
private static ulong BinaryGcd(ulong a, ulong b) private static ulong BinaryGcd(ulong a, ulong b)
@@ -1318,8 +1453,19 @@ internal static partial class PrimeOrderCalculator
} }
} }
private static ulong CalculateByFactorizationCpu(ulong prime, in MontgomeryDivisorData divisorData) private static ulong CalculateByFactorizationCpu(ulong prime, in MontgomeryDivisorData divisorData, in PrimeOrderSearchConfig config)
{ {
if (!IsGpuHeuristicDevice && IsGpuCycleAllowed)
{
PrimeOrderSearchConfig strictConfig = config.Mode == PrimeOrderMode.Strict
? config
: new PrimeOrderSearchConfig(config.SmallFactorLimit, config.PollardRhoMilliseconds, config.MaxPowChecks, PrimeOrderMode.Strict);
if (PrimeOrderGpuHeuristics.TryCalculateOrder(prime, previousOrder: null, strictConfig, divisorData, out ulong gpuOrder))
{
return gpuOrder;
}
}
ulong phi = prime - 1UL; ulong phi = prime - 1UL;
Dictionary<ulong, int> counts = new(capacity: 8); Dictionary<ulong, int> counts = new(capacity: 8);
FactorCompletely(phi, counts); FactorCompletely(phi, counts);
+72
View File
@@ -0,0 +1,72 @@
using System;
using System.Diagnostics;
namespace PerfectNumbers.Core;
internal readonly struct PrimeTestTimeLimit
{
private readonly long _deadlineTimestamp;
private PrimeTestTimeLimit(bool isActive, long deadlineTimestamp)
{
IsActive = isActive;
_deadlineTimestamp = deadlineTimestamp;
}
internal bool IsActive { get; }
internal static bool TryCreate(TimeSpan? timeLimit, out PrimeTestTimeLimit limit)
{
if (!timeLimit.HasValue)
{
limit = new PrimeTestTimeLimit(isActive: false, deadlineTimestamp: long.MaxValue);
return true;
}
TimeSpan limitValue = timeLimit.GetValueOrDefault();
if (limitValue <= TimeSpan.Zero)
{
limit = new PrimeTestTimeLimit(isActive: true, deadlineTimestamp: long.MinValue);
return false;
}
double seconds = limitValue.TotalSeconds;
if (double.IsInfinity(seconds))
{
limit = new PrimeTestTimeLimit(isActive: true, deadlineTimestamp: long.MaxValue);
return true;
}
double scaled = seconds * Stopwatch.Frequency;
long limitTicks = scaled >= long.MaxValue ? long.MaxValue : (long)scaled;
long startTimestamp = Stopwatch.GetTimestamp();
long deadline = startTimestamp <= long.MaxValue - limitTicks ? startTimestamp + limitTicks : long.MaxValue;
if (deadline < startTimestamp)
{
deadline = long.MaxValue;
}
limit = new PrimeTestTimeLimit(isActive: true, deadlineTimestamp: deadline);
return true;
}
internal bool HasExpired()
{
if (!IsActive)
{
return false;
}
if (_deadlineTimestamp == long.MaxValue)
{
return false;
}
if (_deadlineTimestamp == long.MinValue)
{
return true;
}
return Stopwatch.GetTimestamp() >= _deadlineTimestamp;
}
}
+1 -1
View File
@@ -176,7 +176,7 @@ public sealed class PrimeTester(bool useInternal = false)
DevicePrimes.View.CopyFromCPU(primes); DevicePrimes.View.CopyFromCPU(primes);
} }
internal sealed class ScratchBuffers : IDisposable internal sealed class ScratchBuffers
{ {
public MemoryBuffer1D<ulong, Stride1D.Dense> Input { get; private set; } public MemoryBuffer1D<ulong, Stride1D.Dense> Input { get; private set; }
public MemoryBuffer1D<byte, Stride1D.Dense> Output { get; private set; } public MemoryBuffer1D<byte, Stride1D.Dense> Output { get; private set; }
+1 -1
View File
@@ -3,7 +3,7 @@ namespace PerfectNumbers.Core;
/// <summary> /// <summary>
/// Provides thread-safe file writing for both text and binary data. /// Provides thread-safe file writing for both text and binary data.
/// </summary> /// </summary>
public sealed class TextFileWriter : IDisposable public sealed class TextFileWriter
{ {
private readonly FileStream _stream; private readonly FileStream _stream;
private StreamWriter? _textWriter; private StreamWriter? _textWriter;
@@ -0,0 +1,27 @@
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
namespace PerfectNumbers.Core;
internal static class ThreadLocalArrayPool<T>
{
[ThreadStatic]
private static ArrayPool<T>? s_threadPool;
public static ArrayPool<T> Shared
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
ArrayPool<T>? pool = s_threadPool;
if (pool is null)
{
pool = ArrayPool<T>.Create();
s_threadPool = pool;
}
return pool;
}
}
}
+478 -66
View File
@@ -12,13 +12,17 @@ public static class ULongExtensions
private const int Pow2WindowSize = 8; private const int Pow2WindowSize = 8;
private const ulong Pow2WindowFallbackThreshold = 32UL; private const ulong Pow2WindowFallbackThreshold = 32UL;
private const int Pow2WindowOddPowerCount = 1 << (Pow2WindowSize - 1); private const int Pow2WindowOddPowerCount = 1 << (Pow2WindowSize - 1);
private const int Pow2WindowSizeGpu = 5;
private const int Pow2WindowOddPowerCountGpu = 1 << (Pow2WindowSizeGpu - 1);
public static ulong CalculateOrder(this ulong q) public static ulong CalculateOrder(this ulong q)
{ {
if (q <= 2UL) // Order computations only run on admissible divisors, so the small-prime guard remains commented out to keep the
{ // path branch-free.
return 0UL; // if (q <= 2UL)
} // {
// return 0UL;
// }
ulong order = q - 1UL, prime, temp; ulong order = q - 1UL, prime, temp;
uint[] smallPrimes = PrimesGenerator.SmallPrimes; uint[] smallPrimes = PrimesGenerator.SmallPrimes;
@@ -77,7 +81,7 @@ public static class ULongExtensions
public static ulong MultiplyShiftRightShiftFirst(ulong value, ulong multiplier, int shift) public static ulong MultiplyShiftRightShiftFirst(ulong value, ulong multiplier, int shift)
{ {
ulong high = value >> shift; ulong high = value >> shift;
ulong mask = (1UL << shift) - 1UL; ulong mask = (1UL << shift) - 1UL;
ulong low = value & mask; ulong low = value & mask;
UInt128 highContribution = (UInt128)high * multiplier; UInt128 highContribution = (UInt128)high * multiplier;
@@ -288,26 +292,220 @@ public static class ULongExtensions
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
// TODO: Replace this fallback with the UInt128 Montgomery helper measured fastest in public static ulong MulMod64(this ulong a, ulong b, ulong modulus)
// MulMod64Benchmarks so CPU callers stop paying for triple modulo operations.
public static ulong MulMod64(this ulong a, ulong b, ulong modulus) => (ulong)(UInt128)(((a % modulus) * (b % modulus)) % modulus);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong MulMod64GpuCompatible(this ulong a, ulong b, ulong modulus)
{ {
// TODO: Remove this GPU-compatible shim from production once callers migrate to MulMod64, // Scanner inputs always provide odd prime moduli, so the legacy modulus guard stays commented out to avoid extra
// which the benchmarks show is roughly 6-7× faster on dense 64-bit inputs. // branching.
GpuUInt128 state = new(a % modulus); // if (modulus <= 1UL)
return state.MulMod(b, modulus); // {
// return 0UL;
// }
ulong reducedA = a >= modulus ? a % modulus : a;
ulong reducedB = b >= modulus ? b % modulus : b;
GpuUInt128 state = new(reducedA);
return state.MulMod(reducedB, modulus);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong MulMod64GpuCompatibleDeferred(this ulong a, ulong b, ulong modulus) public static ulong Pow2ModWindowedCpu(this ulong exponent, ulong modulus)
{ {
// TODO: Move this deferred helper to the benchmark suite; the baseline MulMod64 avoids the // The by-divisor pipeline only schedules odd prime moduli, so the defensive check below
// 5-40× slowdown seen across real-world operand distributions. // would never trigger on that path. Leaving it commented documents the invariant without
GpuUInt128 state = new(a); // forcing a branch in the hot loop.
return state.MulModWithNativeModulo(b, modulus); // if (modulus <= 1UL)
// {
// return 0UL;
// }
// Exponents flow from the strictly positive candidate list on the by-divisor scanner, so
// this guard would be redundant there.
// if (exponent == 0UL)
// {
// return 1UL % modulus;
// }
// Even though the by-divisor pipeline only feeds very large exponents, the legacy diagnostic callers
// still hit this small-exponent fast path, so keep the fallback in place.
if (exponent <= Pow2WindowFallbackThreshold)
{
return Pow2ModBinaryFallback(exponent, modulus);
}
int bitLength = GetPortableBitLength(exponent);
int windowSize = GetWindowSize(bitLength);
int oddPowerCount = 1 << (windowSize - 1);
Span<ulong> oddPowerStorage = oddPowerCount <= PerfectNumberConstants.MaxOddPowersCount
? stackalloc ulong[PerfectNumberConstants.MaxOddPowersCount]
: new ulong[oddPowerCount];
oddPowerStorage = oddPowerStorage[..oddPowerCount];
InitializePlainOddPowers(modulus, oddPowerStorage);
// With modulus guaranteed to exceed one on the scanning path, the modulo operation would
// always yield one, so keep the literal to highlight the intended base value.
ulong result = 1UL;
int index = bitLength - 1;
while (index >= 0)
{
if (((exponent >> index) & 1UL) == 0UL)
{
result = result.MulMod64(result, modulus);
index--;
continue;
}
int windowStart = index - windowSize + 1;
if (windowStart < 0)
{
windowStart = 0;
}
while (((exponent >> windowStart) & 1UL) == 0UL)
{
windowStart++;
}
int windowLength = index - windowStart + 1;
for (int square = 0; square < windowLength; square++)
{
result = result.MulMod64(result, modulus);
}
ulong windowValue = (exponent >> windowStart) & ((1UL << windowLength) - 1UL);
int tableIndex = (int)((windowValue - 1UL) >> 1);
result = result.MulMod64(oddPowerStorage[tableIndex], modulus);
index = windowStart - 1;
}
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Pow2ModBinaryFallback(ulong exponent, ulong modulus)
{
// The scanning path keeps modulus strictly greater than one, but other utility callers still
// rely on this fallback for tiny exponents, so leave the modulo in place for safety.
ulong remainingExponent = exponent;
// Production moduli are always odd primes greater than two, so normalize the seed values directly.
ulong result = 1UL;
ulong baseValue = 2UL;
while (remainingExponent != 0UL)
{
if ((remainingExponent & 1UL) != 0UL)
{
result = result.MulMod64(baseValue, modulus);
}
remainingExponent >>= 1;
if (remainingExponent == 0UL)
{
break;
}
baseValue = baseValue.MulMod64(baseValue, modulus);
}
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Pow2ModWindowedGpu(this ulong exponent, ulong modulus)
{
// The GPU pipeline feeds only odd prime moduli, so the removed guard would never fire for
// EvenPerfectBitScanner workloads.
// if (modulus <= 1UL)
// {
// return 0UL;
// }
// The GPU pipeline only emits positive exponents on production workloads, so skip the zero guard to avoid an extra branch.
// Diagnostic harnesses must normalize their inputs before invoking this helper.
// if (exponent == 0UL)
// {
// return 1UL;
// }
// The GPU kernels mostly process large exponents as well, but the binary fallback still serves
// diagnostic harnesses that probe edge cases around the threshold.
if (exponent <= Pow2WindowFallbackThreshold)
{
return Pow2ModBinaryGpuFallback(exponent, modulus);
}
int bitLength = GetPortableBitLength(exponent);
int windowSize = GetGpuWindowSize(bitLength);
int oddPowerCount = 1 << (windowSize - 1);
PlainOddPowerTableGpu oddPowers = new PlainOddPowerTableGpu(modulus, oddPowerCount);
ulong result = 1UL;
int index = bitLength - 1;
while (index >= 0)
{
if (((exponent >> index) & 1UL) == 0UL)
{
result = result.MulMod64(result, modulus);
index--;
continue;
}
int windowStart = index - windowSize + 1;
if (windowStart < 0)
{
windowStart = 0;
}
while (((exponent >> windowStart) & 1UL) == 0UL)
{
windowStart++;
}
int windowLength = index - windowStart + 1;
for (int square = 0; square < windowLength; square++)
{
result = result.MulMod64(result, modulus);
}
ulong windowValue = (exponent >> windowStart) & ((1UL << windowLength) - 1UL);
int tableIndex = (int)((windowValue - 1UL) >> 1);
result = result.MulMod64(oddPowers.Get(tableIndex), modulus);
index = windowStart - 1;
}
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Pow2ModBinaryGpuFallback(ulong exponent, ulong modulus)
{
ulong remainingExponent = exponent;
// Production moduli are always odd primes greater than two, so normalize the seed values directly.
ulong result = 1UL;
ulong baseValue = 2UL;
while (remainingExponent != 0UL)
{
if ((remainingExponent & 1UL) != 0UL)
{
result = result.MulMod64(baseValue, modulus);
}
remainingExponent >>= 1;
if (remainingExponent == 0UL)
{
break;
}
baseValue = baseValue.MulMod64(baseValue, modulus);
}
return result;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -332,7 +530,9 @@ public static class ULongExtensions
ulong modulus = divisor.Modulus; ulong modulus = divisor.Modulus;
if (exponent == 0UL) if (exponent == 0UL)
{ {
return keepMontgomery ? divisor.MontgomeryOne : 1UL % modulus; return keepMontgomery
? divisor.MontgomeryOne
: 1UL; // Montgomery moduli stay above one on the scanning path, so the modulo would always yield one.
} }
if (exponent <= Pow2WindowFallbackThreshold) if (exponent <= Pow2WindowFallbackThreshold)
@@ -346,48 +546,69 @@ public static class ULongExtensions
ulong result = divisor.MontgomeryOne; ulong result = divisor.MontgomeryOne;
ulong nPrime = divisor.NPrime; ulong nPrime = divisor.NPrime;
ulong[] oddPowers = InitializeMontgomeryOddPowersCpu(divisor, modulus, nPrime, oddPowerCount);
Span<ulong> oddPowerStorage = oddPowerCount <= PerfectNumberConstants.MaxOddPowersCount
? stackalloc ulong[PerfectNumberConstants.MaxOddPowersCount]
: default;
ulong[]? pooledOddPowers = null;
if (oddPowerCount <= PerfectNumberConstants.MaxOddPowersCount)
{
oddPowerStorage = oddPowerStorage[..oddPowerCount];
}
else if (oddPowerCount < PerfectNumberConstants.PooledArrayThreshold)
{
oddPowerStorage = new ulong[oddPowerCount];
}
else
{
pooledOddPowers = ArrayPool<ulong>.Shared.Rent(oddPowerCount);
oddPowerStorage = pooledOddPowers;
}
Span<ulong> oddPowers = oddPowerStorage[..oddPowerCount];
InitializeMontgomeryOddPowersCpu(divisor, modulus, nPrime, oddPowers);
int index = bitLength - 1; int index = bitLength - 1;
while (index >= 0) while (index >= 0)
{ {
if (((exponent >> index) & 1UL) == 0UL) if (((exponent >> index) & 1UL) == 0UL)
{ {
result = result.MontgomeryMultiply(result, modulus, nPrime); result = result.MontgomeryMultiply(result, modulus, nPrime);
index--; index--;
continue; continue;
} }
int windowStart = index - windowSize + 1; int windowStart = index - windowSize + 1;
if (windowStart < 0) if (windowStart < 0)
{ {
windowStart = 0; windowStart = 0;
} }
while (((exponent >> windowStart) & 1UL) == 0UL) while (((exponent >> windowStart) & 1UL) == 0UL)
{ {
windowStart++; windowStart++;
} }
int windowLength = index - windowStart + 1; int windowLength = index - windowStart + 1;
for (int square = 0; square < windowLength; square++) for (int square = 0; square < windowLength; square++)
{ {
result = result.MontgomeryMultiply(result, modulus, nPrime); result = result.MontgomeryMultiply(result, modulus, nPrime);
} }
ulong mask = (1UL << windowLength) - 1UL; ulong mask = (1UL << windowLength) - 1UL;
ulong windowValue = (exponent >> windowStart) & mask; ulong windowValue = (exponent >> windowStart) & mask;
int tableIndex = (int)((windowValue - 1UL) >> 1); int tableIndex = (int)((windowValue - 1UL) >> 1);
ulong multiplier = oddPowers[tableIndex]; ulong multiplier = oddPowers[tableIndex];
result = result.MontgomeryMultiply(multiplier, modulus, nPrime); result = result.MontgomeryMultiply(multiplier, modulus, nPrime);
index = windowStart - 1; index = windowStart - 1;
} }
if (oddPowerCount >= PerfectNumberConstants.PooledArrayThreshold) if (pooledOddPowers is not null)
{ {
ArrayPool<ulong>.Shared.Return(oddPowers); ArrayPool<ulong>.Shared.Return(pooledOddPowers);
} }
if (keepMontgomery) if (keepMontgomery)
{ {
@@ -403,7 +624,9 @@ public static class ULongExtensions
ulong modulus = divisor.Modulus; ulong modulus = divisor.Modulus;
if (exponent == 0UL) if (exponent == 0UL)
{ {
return keepMontgomery ? divisor.MontgomeryOne : 1UL % modulus; return keepMontgomery
? divisor.MontgomeryOne
: 1UL; // Montgomery moduli stay above one on the scanning path, so the modulo would always yield one.
} }
if (exponent <= Pow2WindowFallbackThreshold) if (exponent <= Pow2WindowFallbackThreshold)
@@ -533,26 +756,215 @@ public static class ULongExtensions
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong[] InitializeMontgomeryOddPowersCpu(in MontgomeryDivisorData divisor, ulong modulus, ulong nPrime, int oddPowerCount) private static int GetGpuWindowSize(int bitLength)
{ {
ulong[] oddPowers = oddPowerCount < PerfectNumberConstants.PooledArrayThreshold ? new ulong[oddPowerCount] : ArrayPool<ulong>.Shared.Rent(oddPowerCount); int window = GetWindowSize(bitLength);
return window > Pow2WindowSizeGpu ? Pow2WindowSizeGpu : window;
}
oddPowers[0] = divisor.MontgomeryTwo; [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InitializePlainOddPowers(ulong modulus, Span<ulong> destination)
{
int oddPowerCount = destination.Length;
// The by-divisor kernels always request at least one odd power, but other helpers reuse this initializer
// for smaller workloads, so the early exit stays in place.
if (oddPowerCount == 0)
{
return;
}
ulong baseValue = 2UL; // Moduli arrive as odd primes greater than two, so the modulo would always return two.
destination[0] = baseValue;
if (oddPowerCount == 1) if (oddPowerCount == 1)
{ {
return oddPowers; return;
} }
ulong previous, ulong square = baseValue.MulMod64(baseValue, modulus);
square = divisor.MontgomeryTwoSquared;
for (int i = 1; i < oddPowerCount; i++) for (int i = 1; i < oddPowerCount; i++)
{ {
previous = oddPowers[i - 1]; // Reuse baseValue to accumulate successive odd powers and avoid allocating another local.
oddPowers[i] = previous.MontgomeryMultiply(square, modulus, nPrime); baseValue = baseValue.MulMod64(square, modulus);
destination[i] = baseValue;
}
}
private struct PlainOddPowerTableGpu
{
private readonly int _count;
private ulong _value0;
private ulong _value1;
private ulong _value2;
private ulong _value3;
private ulong _value4;
private ulong _value5;
private ulong _value6;
private ulong _value7;
private ulong _value8;
private ulong _value9;
private ulong _value10;
private ulong _value11;
private ulong _value12;
private ulong _value13;
private ulong _value14;
private ulong _value15;
public PlainOddPowerTableGpu(ulong modulus, int requestedCount)
{
_count = requestedCount > Pow2WindowOddPowerCountGpu ? Pow2WindowOddPowerCountGpu : requestedCount;
_value0 = 0UL;
_value1 = 0UL;
_value2 = 0UL;
_value3 = 0UL;
_value4 = 0UL;
_value5 = 0UL;
_value6 = 0UL;
_value7 = 0UL;
_value8 = 0UL;
_value9 = 0UL;
_value10 = 0UL;
_value11 = 0UL;
_value12 = 0UL;
_value13 = 0UL;
_value14 = 0UL;
_value15 = 0UL;
// Window planning always requests at least one odd power, so keep the zero-count check commented out to avoid
// branching here.
// if (_count == 0)
// {
// return;
// }
ulong baseValue = 2UL;
_value0 = baseValue;
if (_count == 1)
{
return;
}
ulong square = baseValue.MulMod64(baseValue, modulus);
ulong current = baseValue;
for (int i = 1; i < _count; i++)
{
current = current.MulMod64(square, modulus);
SetValue(i, current);
}
} }
return oddPowers; [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ulong Get(int index)
{
// Windowed lookups never exceed the populated table bounds on production workloads, so leave the check commented
// out to remove the branch.
// if ((uint)index >= (uint)_count)
// {
// return 0UL;
// }
return index switch
{
0 => _value0,
1 => _value1,
2 => _value2,
3 => _value3,
4 => _value4,
5 => _value5,
6 => _value6,
7 => _value7,
8 => _value8,
9 => _value9,
10 => _value10,
11 => _value11,
12 => _value12,
13 => _value13,
14 => _value14,
_ => _value15,
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetValue(int index, ulong value)
{
switch (index)
{
case 1:
_value1 = value;
break;
case 2:
_value2 = value;
break;
case 3:
_value3 = value;
break;
case 4:
_value4 = value;
break;
case 5:
_value5 = value;
break;
case 6:
_value6 = value;
break;
case 7:
_value7 = value;
break;
case 8:
_value8 = value;
break;
case 9:
_value9 = value;
break;
case 10:
_value10 = value;
break;
case 11:
_value11 = value;
break;
case 12:
_value12 = value;
break;
case 13:
_value13 = value;
break;
case 14:
_value14 = value;
break;
case 15:
_value15 = value;
break;
}
}
}
private static void InitializeMontgomeryOddPowersCpu(in MontgomeryDivisorData divisor, ulong modulus, ulong nPrime, Span<ulong> destination)
{
int oddPowerCount = destination.Length;
if (oddPowerCount == 0)
{
return;
}
bool computedOnGpu = oddPowerCount <= PerfectNumberConstants.MaxOddPowersCount
&& MontgomeryOddPowerGpu.TryCompute(divisor, oddPowerCount, destination);
if (computedOnGpu)
{
return;
}
destination[0] = divisor.MontgomeryTwo;
if (oddPowerCount == 1)
{
return;
}
ulong square = divisor.MontgomeryTwoSquared;
ulong current = destination[0];
for (int i = 1; i < oddPowerCount; i++)
{
current = current.MontgomeryMultiply(square, modulus, nPrime);
destination[i] = current;
}
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
+7 -4
View File
@@ -335,10 +335,13 @@ if (patchResults.Any(static r => !r.Success))
static List<PatchFile> ParsePatchFromFile(string patchFilePath) static List<PatchFile> ParsePatchFromFile(string patchFilePath)
{ {
using var stream = File.OpenRead(patchFilePath); var stream = File.OpenRead(patchFilePath);
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 16_384, leaveOpen: false); var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 16_384, leaveOpen: false);
using var lineReader = new PatchLineReader(reader); var lineReader = new PatchLineReader(reader);
var files = ParsePatch(lineReader); var files = ParsePatch(lineReader);
lineReader.Dispose();
reader.Dispose();
stream.Dispose();
return files; return files;
} }
@@ -500,7 +503,7 @@ static void FinalizePatchFile(
files.Add(new PatchFile(originalPath, modifiedPath, hunks ?? new List<PatchHunk>(), type)); files.Add(new PatchFile(originalPath, modifiedPath, hunks ?? new List<PatchHunk>(), type));
} }
private sealed class PatchLineReader : IDisposable private sealed class PatchLineReader
{ {
private const int ReadBufferSize = 4096; private const int ReadBufferSize = 4096;
private const int InitialLineBufferSize = 256; private const int InitialLineBufferSize = 256;
-13
View File
@@ -122,7 +122,6 @@ PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:203:
PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:207: // TODO: When divisorCycle is zero (cache miss), compute only that single cycle on the device PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:207: // TODO: When divisorCycle is zero (cache miss), compute only that single cycle on the device
PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:214: // TODO: Remove this no-cycle branch and require on-demand cycle computation (without PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:214: // TODO: Remove this no-cycle branch and require on-demand cycle computation (without
PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:240: // TODO: Replace this linear increment with the batched divisor-cycle walker validated in the PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:240: // TODO: Replace this linear increment with the batched divisor-cycle walker validated in the
PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:262: : prime.Pow2MontgomeryMod(divisorData); // TODO: Switch to the upcoming ProcessEightBitWindows helper once the
PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:283: // TODO: Replace this modulo with the ring-buffer style counter (subtract loop) used in the fast CLI PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:283: // TODO: Replace this modulo with the ring-buffer style counter (subtract loop) used in the fast CLI
PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:356: // TODO: Replace this guard with an on-demand CPU/GPU cycle computation path that skips PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:356: // TODO: Replace this guard with an on-demand CPU/GPU cycle computation path that skips
PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:396: // TODO: Remove this no-cycle fallback once divisor cycles are mandatory; the per-prime pow2 ladder here PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:396: // TODO: Remove this no-cycle fallback once divisor cycles are mandatory; the per-prime pow2 ladder here
@@ -266,7 +265,6 @@ todo_list.txt:121:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTeste
todo_list.txt:122:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:207: // TODO: When divisorCycle is zero (cache miss), compute only that single cycle on the device todo_list.txt:122:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:207: // TODO: When divisorCycle is zero (cache miss), compute only that single cycle on the device
todo_list.txt:123:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:214: // TODO: Remove this no-cycle branch and require on-demand cycle computation (without todo_list.txt:123:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:214: // TODO: Remove this no-cycle branch and require on-demand cycle computation (without
todo_list.txt:124:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:240: // TODO: Replace this linear increment with the batched divisor-cycle walker validated in the todo_list.txt:124:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:240: // TODO: Replace this linear increment with the batched divisor-cycle walker validated in the
todo_list.txt:125:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:262: : prime.Pow2MontgomeryMod(divisorData); // TODO: Switch to the upcoming ProcessEightBitWindows helper once the
todo_list.txt:126:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:283: // TODO: Replace this modulo with the ring-buffer style counter (subtract loop) used in the fast CLI todo_list.txt:126:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:283: // TODO: Replace this modulo with the ring-buffer style counter (subtract loop) used in the fast CLI
todo_list.txt:127:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:356: // TODO: Replace this guard with an on-demand CPU/GPU cycle computation path that skips todo_list.txt:127:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:356: // TODO: Replace this guard with an on-demand CPU/GPU cycle computation path that skips
todo_list.txt:128:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:396: // TODO: Remove this no-cycle fallback once divisor cycles are mandatory; the per-prime pow2 ladder here todo_list.txt:128:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:396: // TODO: Remove this no-cycle fallback once divisor cycles are mandatory; the per-prime pow2 ladder here
@@ -407,17 +405,6 @@ PerfectNumbers.Core\ModResidueTracker.cs:257: // TODO: Us
PerfectNumbers.Core\ModResidueTracker.cs:263: // TODO: Consult MersenneDivisorCycles.Shared (or similar caches) here so we reuse precomputed cycle lengths PerfectNumbers.Core\ModResidueTracker.cs:263: // TODO: Consult MersenneDivisorCycles.Shared (or similar caches) here so we reuse precomputed cycle lengths
PerfectNumbers.Core\ModResidueTracker.cs:295: // TODO: Route this through the planned UInt128 windowed powmod so the inner MulMod adopts the PerfectNumbers.Core\ModResidueTracker.cs:295: // TODO: Route this through the planned UInt128 windowed powmod so the inner MulMod adopts the
PerfectNumbers.Core\ModResidueTracker.cs:314: // TODO: Replace this double-and-add fallback with the UInt128 intrinsic-backed multiplier once the PerfectNumbers.Core\ModResidueTracker.cs:314: // TODO: Replace this double-and-add fallback with the UInt128 intrinsic-backed multiplier once the
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:49: set => _useDivisorCycles = value; // TODO: Remove this flag once GPU runs unconditionally fetch cycle lengths so every divisor check benefits from the measured speedups.
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:298: // TODO: Stage MontgomeryDivisorData in a GPU-ready cache keyed by divisor so we can copy
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:372: // TODO: Replace the block lease with a direct single-cycle lookup so the GPU path
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:395: // TODO: Drop this block hand-off once the cache exposes a direct
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:409: // TODO: Replace this `%` with the cycle-cache helper that subtracts precomputed
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:425: // TODO: Trigger an immediate GPU computation for the single missing cycle without storing
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:476: // TODO: Preload ProcessEightBitWindows-friendly divisor metadata for GPU batches so this
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:563: hits[index] = exponent.Pow2MontgomeryMod(divisor) == 1UL ? (byte)1 : (byte)0; // TODO: Replace Pow2MontgomeryMod with the ProcessEightBitWindows helper once available so GPU by-divisor scans reuse the benchmarked fast pow2 ladder.
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:632: // TODO: Replace this guard with an on-demand cycle computation that runs on the device selected
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:787: // TODO: Replace this `%` with the divisor-cycle remainder helper so we reuse cached
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:858: results[index] = exponent.Pow2MontgomeryModMontgomery(divisor); // TODO: Swap this Montgomery-domain pow2 with the windowed helper slated to replace the slower ladder in Pow2Minus1Mod benchmarks.
PerfectNumbers.Core\MersenneNumberDivisorByDivisorTester.cs:43: // TODO: Swap this per-run enumerator creation for the shared PrimeIterator so by-divisor scans reuse the PerfectNumbers.Core\MersenneNumberDivisorByDivisorTester.cs:43: // TODO: Swap this per-run enumerator creation for the shared PrimeIterator so by-divisor scans reuse the
PerfectNumbers.Core\MersenneNumberDivisorByDivisorTester.cs:219: // TODO: Swap Task.Factory.StartNew for the shared low-overhead work queue once the scanner's PerfectNumbers.Core\MersenneNumberDivisorByDivisorTester.cs:219: // TODO: Swap Task.Factory.StartNew for the shared low-overhead work queue once the scanner's
PerfectNumbers.Core\MersenneNumberDivisorByDivisorTester.cs:240: 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. PerfectNumbers.Core\MersenneNumberDivisorByDivisorTester.cs:240: 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.
+8 -4
View File
@@ -54,10 +54,11 @@ static ProbeResult ProbeBigInteger(Accelerator accelerator)
try try
{ {
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<int, Stride1D.Dense>>(BigIntegerKernel); var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<int, Stride1D.Dense>>(BigIntegerKernel);
using var buffer = accelerator.Allocate1D<int>(1); var buffer = accelerator.Allocate1D<int>(1);
var extent = new Index1D((int)buffer.Length); var extent = new Index1D((int)buffer.Length);
kernel(extent, buffer.View); kernel(extent, buffer.View);
accelerator.Synchronize(); accelerator.Synchronize();
buffer.Dispose();
return ProbeResult.Success( return ProbeResult.Success(
"System.Numerics.BigInteger", "System.Numerics.BigInteger",
"Addition, subtraction, multiplication, division, and modulo compile on the CPU accelerator."); "Addition, subtraction, multiplication, division, and modulo compile on the CPU accelerator.");
@@ -73,9 +74,10 @@ static ProbeResult ProbeEInteger(Accelerator accelerator)
try try
{ {
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<int, Stride1D.Dense>>(EIntegerKernel); var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<int, Stride1D.Dense>>(EIntegerKernel);
using var buffer = accelerator.Allocate1D<int>(1); var buffer = accelerator.Allocate1D<int>(1);
kernel(new Index1D((int)buffer.Length), buffer.View); kernel(new Index1D((int)buffer.Length), buffer.View);
accelerator.Synchronize(); accelerator.Synchronize();
buffer.Dispose();
return ProbeResult.Success( return ProbeResult.Success(
"PeterO.Numbers.EInteger", "PeterO.Numbers.EInteger",
"Addition, subtraction, multiplication, division, and remainder compile on the CPU accelerator."); "Addition, subtraction, multiplication, division, and remainder compile on the CPU accelerator.");
@@ -93,7 +95,7 @@ static ProbeResult ProbeERational(Accelerator accelerator)
try try
{ {
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<int, Stride1D.Dense>>(ERationalKernel); var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<int, Stride1D.Dense>>(ERationalKernel);
using var buffer = accelerator.Allocate1D<int>(1); var buffer = accelerator.Allocate1D<int>(1);
kernel(new Index1D((int)buffer.Length), buffer.View); kernel(new Index1D((int)buffer.Length), buffer.View);
accelerator.Synchronize(); accelerator.Synchronize();
@@ -101,6 +103,7 @@ static ProbeResult ProbeERational(Accelerator accelerator)
details += hasRemainder details += hasRemainder
? " ERational exposes a remainder API that requires further investigation." ? " ERational exposes a remainder API that requires further investigation."
: " ERational does not expose a remainder/modulo API."; : " ERational does not expose a remainder/modulo API.";
buffer.Dispose();
return ProbeResult.Success("PeterO.Numbers.ERational", details); return ProbeResult.Success("PeterO.Numbers.ERational", details);
} }
catch (Exception ex) catch (Exception ex)
@@ -120,9 +123,10 @@ static ProbeResult ProbeOpenNumericPrimesIsPrime(Accelerator accelerator)
try try
{ {
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<int, Stride1D.Dense>>(OpenNumericPrimesIsPrimeKernel); var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<int, Stride1D.Dense>>(OpenNumericPrimesIsPrimeKernel);
using var buffer = accelerator.Allocate1D<int>(4); var buffer = accelerator.Allocate1D<int>(4);
kernel(new Index1D((int)buffer.Length), buffer.View); kernel(new Index1D((int)buffer.Length), buffer.View);
accelerator.Synchronize(); accelerator.Synchronize();
buffer.Dispose();
return ProbeResult.Success( return ProbeResult.Success(
"Open.Numeric.Primes.Prime.Numbers.IsPrime", "Open.Numeric.Primes.Prime.Numbers.IsPrime",
"IsPrime compiles and runs on the CPU accelerator kernel."); "IsPrime compiles and runs on the CPU accelerator kernel.");
@@ -8,4 +8,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="../../PerfectNumbers.Core/PerfectNumbers.Core.csproj" /> <ProjectReference Include="../../PerfectNumbers.Core/PerfectNumbers.Core.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project> </Project>