Restore pre-residue mode behavior and tests

This commit is contained in:
Marek Lemańczyk committed 2025-09-21 04:05:16 +02:00
1 parent 23e8d306b1
commit aa683215b2
8 files changed
+51 -678

No files matched your search

@@ -6,6 +6,7 @@ using Xunit;
namespace PerfectNumbers.Core.Tests.Gpu;
[Collection("GpuNtt")]
public class GpuContextPoolTests
{
[Fact]
@@ -21,56 +21,10 @@ public class MersenneNumberResidueCpuTesterTests
RunCase(tester, 127UL, 1_000UL, expectedPrime: true);
}
[Fact]
[Trait("Category", "Fast")]
public void Scan_recognizes_all_known_small_mersenne_primes()
{
var tester = new MersenneNumberResidueCpuTester();
foreach (ulong exponent in MersennePrimeTestData.Exponents)
{
RunCase(tester, exponent, 1UL, expectedPrime: true);
}
}
[Theory]
[InlineData(31UL)]
[Trait("Category", "Fast")]
public void Scan_matches_cli_residue_configuration_for_primes(ulong exponent)
{
RunCase(new MersenneNumberResidueCpuTester(), exponent, 32UL, expectedPrime: true);
}
[Theory]
[InlineData(31UL, 1_024UL, 1UL)]
[InlineData(31UL, 512UL, 2UL)]
[InlineData(31UL, 256UL, 4UL)]
[InlineData(127UL, 256UL, 1UL)]
[InlineData(127UL, 128UL, 2UL)]
[InlineData(127UL, 64UL, 4UL)]
[Trait("Category", "Fast")]
public void Scan_handles_multiple_residue_sets_for_known_primes(ulong exponent, ulong perSetLimit, ulong setCount)
{
ulong overallLimit = perSetLimit * setCount;
RunCase(new MersenneNumberResidueCpuTester(), exponent, perSetLimit, setCount, overallLimit, expectedPrime: true);
}
private static void RunCase(MersenneNumberResidueCpuTester tester, ulong exponent, ulong maxK, bool expectedPrime)
{
RunCase(tester, exponent, maxK, 1UL, maxK, expectedPrime);
}
private static void RunCase(
MersenneNumberResidueCpuTester tester,
ulong exponent,
ulong perSetLimit,
ulong setCount,
ulong overallLimit,
bool expectedPrime)
{
bool isPrime = true;
tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime);
isPrime.Should().Be(expectedPrime);
}
}
@@ -1,9 +1,4 @@
using System.IO;
using System.Linq;
using System.Reflection;
using FluentAssertions;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Cpu;
using PerfectNumbers.Core.Gpu;
using Xunit;
using UInt128 = System.UInt128;
@@ -26,270 +21,13 @@ public class MersenneNumberResidueGpuTesterTests
RunCase(tester, 29UL, 37UL, expectedPrime: false);
RunCase(tester, 31UL, 1_000UL, expectedPrime: true);
RunCase(tester, 89UL, 1_001UL, expectedPrime: true);
RunCase(tester, 107UL, 1_001UL, expectedPrime: true);
RunCase(tester, 127UL, 1_001UL, expectedPrime: true);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
[Trait("Category", "Fast")]
public void Gpu_residue_scan_matches_cpu_for_small_configurations(bool useGpuOrder)
{
var gpuTester = new MersenneNumberResidueGpuTester(useGpuOrder);
var cpuTester = new MersenneNumberResidueCpuTester();
var configurations = new (ulong Exponent, UInt128 PerSetLimit, UInt128 SetCount, UInt128 OverallLimit)[]
{
(31UL, 8UL, 6UL, 48UL),
(61UL, 9UL, 9UL, 81UL),
(89UL, 12UL, 8UL, 96UL),
(107UL, 16UL, 8UL, 128UL),
};
foreach (var (exponent, perSetLimit, setCount, overallLimit) in configurations)
{
bool gpuPrime = true;
bool gpuExhausted = false;
gpuTester.Scan(
exponent,
(UInt128)exponent << 1,
LastDigitIsSeven(exponent),
perSetLimit,
setCount,
overallLimit,
ref gpuPrime,
ref gpuExhausted);
bool cpuPrime = true;
bool cpuExhausted = false;
cpuTester.Scan(
exponent,
(UInt128)exponent << 1,
LastDigitIsSeven(exponent),
perSetLimit,
setCount,
overallLimit,
ref cpuPrime,
ref cpuExhausted);
gpuPrime.Should().Be(cpuPrime, $"GPU residue scan should match CPU for exponent {exponent}");
gpuExhausted.Should().Be(cpuExhausted, $"GPU residue scan should report identical exhaustion for exponent {exponent}");
}
}
[Fact]
[Trait("Category", "Fast")]
public void Scan_residue_gpu_handles_inaccurate_small_cycle_entries()
{
const ulong exponent = 23UL;
const ulong divisor = 47UL; // 47 | M_23
string tempPath = Path.GetTempFileName();
try
{
using (var writer = new BinaryWriter(File.Open(tempPath, FileMode.Create, FileAccess.Write, FileShare.Read)))
{
writer.Write(divisor);
writer.Write((ulong)uint.MaxValue);
}
var cycles = MersenneDivisorCycles.Shared;
var tableField = typeof(MersenneDivisorCycles).GetField("_table", BindingFlags.NonPublic | BindingFlags.Instance)!;
var smallCyclesField = typeof(MersenneDivisorCycles).GetField("_smallCycles", BindingFlags.NonPublic | BindingFlags.Instance)!;
var originalTable = ((List<(ulong Divisor, ulong Cycle)>)tableField.GetValue(cycles)!).Select(x => x).ToList();
var originalSmall = (uint[]?)smallCyclesField.GetValue(cycles);
uint[]? backupSmall = originalSmall is null ? null : (uint[])originalSmall.Clone();
cycles.LoadFrom(tempPath);
GpuContextPool.DisposeAll();
try
{
var tester = new MersenneNumberResidueGpuTester(useGpuOrder: false);
bool isPrime = true;
bool exhausted = false;
tester.Scan(
exponent,
(UInt128)exponent << 1,
lastIsSeven: true,
perSetLimit: (UInt128)10UL,
setCount: UInt128.One,
overallLimit: (UInt128)10UL,
ref isPrime,
ref exhausted);
isPrime.Should().BeFalse();
exhausted.Should().BeTrue();
}
finally
{
tableField.SetValue(cycles, originalTable);
smallCyclesField.SetValue(cycles, backupSmall);
GpuContextPool.DisposeAll();
}
}
finally
{
File.Delete(tempPath);
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]
[Trait("Category", "Fast")]
public void Mersenne_tester_residue_gpu_handles_known_primes_with_many_sets(bool useGpuOrder)
{
var tester = new MersenneNumberTester(
useIncremental: true,
useOrderCache: false,
kernelType: GpuKernelType.Pow2Mod,
useModuloWorkaround: false,
useOrder: false,
useGpuLucas: false,
useGpuScan: true,
useGpuOrder: useGpuOrder,
useResidue: true,
maxK: (UInt128)2_400_000UL,
residueDivisorSets: (UInt128)512UL);
try
{
bool isPrime = tester.IsMersennePrime(127UL, out bool divisorsExhausted);
isPrime.Should().BeTrue();
divisorsExhausted.Should().BeTrue();
}
finally
{
GpuContextPool.DisposeAll();
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]
[Trait("Category", "Fast")]
public void Scan_recognizes_all_known_small_mersenne_primes(bool useGpuOrder)
{
var tester = new MersenneNumberResidueGpuTester(useGpuOrder);
foreach (ulong exponent in MersennePrimeTestData.Exponents)
{
RunCase(tester, exponent, 1UL, expectedPrime: true);
}
}
[Theory]
[InlineData(31UL)]
[Trait("Category", "Fast")]
public void Scan_matches_cli_residue_configuration_for_primes(ulong exponent)
{
RunCliCase(useGpuOrder: false, exponent);
RunCliCase(useGpuOrder: true, exponent);
}
[Theory]
[InlineData(31UL, 1_024UL, 1UL)]
[InlineData(31UL, 256UL, 4UL)]
[InlineData(107UL, 512UL, 1UL)]
[InlineData(107UL, 128UL, 4UL)]
[InlineData(107UL, 15_625UL, 64UL)]
[InlineData(127UL, 512UL, 1UL)]
[InlineData(127UL, 128UL, 4UL)]
[Trait("Category", "Fast")]
public void Scan_handles_multiple_residue_sets_for_known_primes(ulong exponent, ulong perSetLimit, ulong setCount)
{
ulong overallLimit = perSetLimit * setCount;
RunCase(new MersenneNumberResidueGpuTester(useGpuOrder: false), exponent, perSetLimit, setCount, overallLimit, expectedPrime: true);
RunCase(new MersenneNumberResidueGpuTester(useGpuOrder: true), exponent, perSetLimit, setCount, overallLimit, expectedPrime: true);
}
[Theory]
[InlineData(107UL, 1UL, 1_024UL)]
[Trait("Category", "Fast")]
public void Scan_handles_single_lane_sets_for_known_primes(ulong exponent, ulong perSetLimit, ulong overallLimit)
{
ulong setCount = overallLimit;
RunCase(new MersenneNumberResidueGpuTester(useGpuOrder: false), exponent, perSetLimit, setCount, overallLimit, expectedPrime: true);
RunCase(new MersenneNumberResidueGpuTester(useGpuOrder: true), exponent, perSetLimit, setCount, overallLimit, expectedPrime: true);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
[Trait("Category", "Fast")]
public void Scan_handles_multiple_sets_without_false_positives(bool useGpuOrder)
{
var tester = new MersenneNumberResidueGpuTester(useGpuOrder);
bool isPrime = true;
bool exhausted = false;
UInt128 perSetLimit = 1_666_667UL;
UInt128 setCount = 3UL;
UInt128 overallLimit = 5_000_000UL;
tester.Scan(
127UL,
(UInt128)127UL << 1,
LastDigitIsSeven(127UL),
perSetLimit,
setCount,
overallLimit,
ref isPrime,
ref exhausted);
isPrime.Should().BeTrue();
exhausted.Should().BeTrue();
isPrime = true;
exhausted = false;
perSetLimit = 20_000_000UL;
setCount = 5UL;
overallLimit = 100_000_000UL;
tester.Scan(
107UL,
(UInt128)107UL << 1,
LastDigitIsSeven(107UL),
perSetLimit,
setCount,
overallLimit,
ref isPrime,
ref exhausted);
isPrime.Should().BeTrue();
exhausted.Should().BeTrue();
}
private static void RunCliCase(bool useGpuOrder, ulong exponent)
{
var tester = new MersenneNumberResidueGpuTester(useGpuOrder);
RunCase(tester, exponent, 8UL, expectedPrime: true);
}
private static void RunCase(MersenneNumberResidueGpuTester tester, ulong exponent, ulong maxK, bool expectedPrime)
{
RunCase(tester, exponent, maxK, 1UL, maxK, expectedPrime);
}
private static void RunCase(
MersenneNumberResidueGpuTester tester,
ulong exponent,
ulong perSetLimit,
ulong setCount,
ulong overallLimit,
bool expectedPrime)
{
bool isPrime = true;
tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime);
isPrime.Should().Be(expectedPrime);
}
}
@@ -1,236 +0,0 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
using Xunit;
using UInt128 = System.UInt128;
namespace PerfectNumbers.Core.Tests;
public class MersenneNumberResidueIntegrationTests
{
public static IEnumerable<object[]> ResidueThreadCounts()
{
yield return new object[] { 1 };
yield return new object[] { 2 };
yield return new object[] { 4 };
}
[Theory]
[InlineData(107UL)]
[InlineData(127UL)]
[Trait("Category", "Fast")]
public void Residue_gpu_mode_handles_large_residue_set_counts_for_known_primes(ulong exponent)
{
var tester = new MersenneNumberTester(
useIncremental: false,
useOrderCache: false,
kernelType: GpuKernelType.Pow2Mod,
useModuloWorkaround: false,
useOrder: false,
useGpuLucas: false,
useGpuScan: true,
useGpuOrder: true,
useResidue: true,
maxK: (UInt128)1_024UL,
residueDivisorSets: (UInt128)1_024UL);
try
{
bool isPrime = tester.IsMersennePrime(exponent, out bool divisorsExhausted);
isPrime.Should().BeTrue();
divisorsExhausted.Should().BeTrue();
}
finally
{
GpuContextPool.DisposeAll();
}
}
[Theory]
[InlineData(107UL)]
[InlineData(127UL)]
[Trait("Category", "Fast")]
public void Residue_gpu_mode_matches_cli_limits_for_known_primes(ulong exponent)
{
var tester = new MersenneNumberTester(
useIncremental: true,
useOrderCache: false,
kernelType: GpuKernelType.Pow2Mod,
useModuloWorkaround: false,
useOrder: false,
useGpuLucas: false,
useGpuScan: true,
useGpuOrder: true,
useResidue: true,
maxK: (UInt128)5_000_000UL,
residueDivisorSets: (UInt128)1_024UL);
try
{
bool isPrime = tester.IsMersennePrime(exponent, out bool divisorsExhausted);
isPrime.Should().BeTrue();
divisorsExhausted.Should().BeTrue();
}
finally
{
GpuContextPool.DisposeAll();
}
}
[Theory]
[MemberData(nameof(ResidueThreadCounts))]
[Trait("Category", "Fast")]
public void Residue_gpu_mode_matches_cli_configuration_for_known_primes(int threadCount)
{
ulong[] exponents = MersennePrimeTestData.Exponents;
ulong[][] partitions = PartitionExponents(exponents, threadCount);
var results = new ConcurrentDictionary<ulong, (bool IsPrime, bool Exhausted)>();
UInt128 residueSetCount = (UInt128)PerfectNumberConstants.ExtraDivisorCycleSearchLimit;
try
{
Task[] tasks = partitions.Select(partition => Task.Run(() =>
{
if (partition.Length == 0)
{
return;
}
var tester = new MersenneNumberTester(
useIncremental: false,
useOrderCache: false,
kernelType: GpuKernelType.Pow2Mod,
useModuloWorkaround: false,
useOrder: false,
useGpuLucas: false,
useGpuScan: true,
useGpuOrder: true,
useResidue: true,
maxK: (UInt128)1_024UL,
residueDivisorSets: residueSetCount);
foreach (ulong exponent in partition)
{
bool isPrime = tester.IsMersennePrime(exponent, out bool divisorsExhausted);
results[exponent] = (isPrime, divisorsExhausted);
}
})).ToArray();
Task.WaitAll(tasks);
}
finally
{
GpuContextPool.DisposeAll();
}
results.Keys.Should().BeEquivalentTo(exponents);
foreach (ulong exponent in exponents)
{
results[exponent].Should().Be((true, true));
}
}
[Theory]
[MemberData(nameof(ResidueThreadCounts))]
[Trait("Category", "Fast")]
public void Residue_cpu_mode_matches_cli_configuration_for_known_primes(int threadCount)
{
ulong[] exponents = MersennePrimeTestData.Exponents;
ulong[][] partitions = PartitionExponents(exponents, threadCount);
var results = new ConcurrentDictionary<ulong, (bool IsPrime, bool Exhausted)>();
UInt128 residueSetCount = (UInt128)PerfectNumberConstants.ExtraDivisorCycleSearchLimit;
Task[] tasks = partitions.Select(partition => Task.Run(() =>
{
if (partition.Length == 0)
{
return;
}
var tester = new MersenneNumberTester(
useIncremental: false,
useOrderCache: false,
kernelType: GpuKernelType.Pow2Mod,
useModuloWorkaround: false,
useOrder: false,
useGpuLucas: false,
useGpuScan: false,
useGpuOrder: false,
useResidue: true,
maxK: (UInt128)1_024UL,
residueDivisorSets: residueSetCount);
foreach (ulong exponent in partition)
{
bool isPrime = tester.IsMersennePrime(exponent, out bool divisorsExhausted);
results[exponent] = (isPrime, divisorsExhausted);
}
})).ToArray();
Task.WaitAll(tasks);
results.Keys.Should().BeEquivalentTo(exponents);
foreach (ulong exponent in exponents)
{
results[exponent].Should().Be((true, true));
}
}
[Fact]
[Trait("Category", "Fast")]
public void Residue_cpu_mode_respects_filter_configuration()
{
var filter = new HashSet<ulong>(MersennePrimeTestData.Exponents);
ulong[] candidates = [31UL, 37UL, 73UL];
var tester = new MersenneNumberTester(
useIncremental: false,
useOrderCache: false,
kernelType: GpuKernelType.Pow2Mod,
useModuloWorkaround: false,
useOrder: false,
useGpuLucas: false,
useGpuScan: false,
useGpuOrder: false,
useResidue: true,
maxK: (UInt128)32UL,
residueDivisorSets: (UInt128)4UL);
List<ulong> processed = new();
List<ulong> skipped = new();
filter.Count.Should().Be(MersennePrimeTestData.Exponents.Length);
foreach (ulong exponent in candidates)
{
if (!filter.Contains(exponent))
{
skipped.Add(exponent);
continue;
}
bool isPrime = tester.IsMersennePrime(exponent, out bool divisorsExhausted);
isPrime.Should().BeTrue();
divisorsExhausted.Should().BeTrue();
processed.Add(exponent);
}
processed.Should().ContainSingle().Which.Should().Be(31UL);
skipped.Should().BeEquivalentTo(new[] { 37UL, 73UL });
}
private static ulong[][] PartitionExponents(ulong[] exponents, int threadCount)
{
return Enumerable
.Range(0, threadCount)
.Select(partition => exponents.Where((_, index) => index % threadCount == partition).ToArray())
.ToArray();
}
}
@@ -9,21 +9,23 @@ public class MersenneResidueModeTests
[Fact]
public void IsMersennePrime_residue_mode_rejects_composite_exponent()
{
var tester = new MersenneNumberTester(useResidue: true, useIncremental: false);
var tester = new MersenneNumberTester(
useResidue: true,
useIncremental: false,
useGpuScan: false,
useGpuOrder: false);
tester.IsMersennePrime(136_000_002UL).Should().BeFalse();
}
[Fact]
public void IsMersennePrime_residue_mode_accepts_prime_exponent()
{
var tester = new MersenneNumberTester(useResidue: true, useIncremental: true, maxK: 1_000UL);
tester.IsMersennePrime(31UL).Should().BeTrue();
}
[Fact]
public void IsMersennePrime_residue_mode_accepts_prime_exponent_when_scanning_multiple_batches()
{
var tester = new MersenneNumberTester(useResidue: true, useIncremental: true, maxK: 2_100_000UL);
var tester = new MersenneNumberTester(
useResidue: true,
useIncremental: true,
useGpuScan: false,
useGpuOrder: false,
maxK: 1_000UL);
tester.IsMersennePrime(31UL).Should().BeTrue();
}
}