Initial commit.

This commit is contained in:
Marek Lemańczyk committed 2025-09-09 21:15:57 +02:00
1 parent e5b0fea18c
commit 9f35f6c558
94 files changed
+12857

No files matched your search

@@ -0,0 +1,69 @@
using FluentAssertions;
using ILGPU;
using ILGPU.Runtime;
using PerfectNumbers.Core.Gpu;
using Xunit;
namespace PerfectNumbers.Core.Tests.Gpu;
public class GpuContextPoolTests
{
[Fact]
[Trait("Category", "Fast")]
public void RentPreferred_cpu_returns_cpu_accelerator()
{
GpuContextPool.DisposeAll();
using var lease = GpuContextPool.RentPreferred(preferCpu: true);
lease.Accelerator.AcceleratorType.Should().Be(AcceleratorType.CPU);
GpuContextPool.DisposeAll();
}
[Fact]
[Trait("Category", "Fast")]
public void RentPreferred_reuses_cpu_context()
{
GpuContextPool.DisposeAll();
Context firstContext;
Accelerator firstAccelerator;
using (var lease = GpuContextPool.RentPreferred(preferCpu: true))
{
firstContext = lease.Context;
firstAccelerator = lease.Accelerator;
}
using (var lease = GpuContextPool.RentPreferred(preferCpu: true))
{
lease.Context.Should().BeSameAs(firstContext);
lease.Accelerator.Should().BeSameAs(firstAccelerator);
}
GpuContextPool.DisposeAll();
}
[Fact]
[Trait("Category", "Fast")]
public void DisposeAll_clears_pooled_contexts()
{
GpuContextPool.DisposeAll();
Context firstContext;
using (var lease = GpuContextPool.RentPreferred(preferCpu: true))
{
firstContext = lease.Context;
}
GpuContextPool.DisposeAll();
using (var lease = GpuContextPool.RentPreferred(preferCpu: true))
{
lease.Context.Should().NotBeSameAs(firstContext);
}
GpuContextPool.DisposeAll();
}
}
@@ -0,0 +1,9 @@
using Xunit;
namespace PerfectNumbers.Core.Tests.Gpu;
[CollectionDefinition("GpuNtt", DisableParallelization = true)]
public class GpuNttCollection
{
}
@@ -0,0 +1,45 @@
using System.Diagnostics;
using FluentAssertions;
using PerfectNumbers.Core.Gpu;
using Xunit;
namespace PerfectNumbers.Core.Tests.Gpu;
[Collection("GpuNtt")]
[Trait("Category", "Fast")]
public class GpuPrimeLimiterTests
{
[Fact]
public async Task Acquire_blocks_when_limit_reached()
{
// Ensure limit = 1 so second acquire must wait.
GpuPrimeWorkLimiter.SetLimit(1);
var startedSecond = new TaskCompletionSource<bool>();
var sw = new Stopwatch();
using var first = GpuPrimeWorkLimiter.Acquire();
sw.Start();
var secondTask = Task.Run(() =>
{
using var second = GpuPrimeWorkLimiter.Acquire();
startedSecond.TrySetResult(true);
});
// Give secondTask a moment to attempt acquire (it should block).
await Task.Delay(50);
startedSecond.Task.IsCompleted.Should().BeFalse();
// Release first after short delay, allowing second to continue.
// (Dispose at scope end, but we want to end now.)
first.Dispose();
await secondTask.WaitAsync(TimeSpan.FromSeconds(1));
sw.Stop();
// Should be > ~50ms because second had to wait for first to release.
sw.ElapsedMilliseconds.Should().BeGreaterThanOrEqualTo(50);
}
}
@@ -0,0 +1,342 @@
using FluentAssertions;
using PerfectNumbers.Core.Gpu;
using Xunit;
[Trait("Category", "Fast")]
public class GpuUInt128Tests
{
[Fact]
public void Mul_matches_UInt128()
{
var random = new Random(0);
for (var i = 0; i < 100; i++)
{
var high1 = ((ulong)random.Next() << 32) | (uint)random.Next();
var low1 = ((ulong)random.Next() << 32) | (uint)random.Next();
var high2 = ((ulong)random.Next() << 32) | (uint)random.Next();
var low2 = ((ulong)random.Next() << 32) | (uint)random.Next();
var a = new GpuUInt128(high1, low1);
var b = new GpuUInt128(high2, low2);
var expected = (UInt128)new GpuUInt128(high1, low1) * (UInt128)new GpuUInt128(high2, low2);
a = a.Mul(b);
((UInt128)a).Should().Be(expected);
}
}
[Fact]
public void MulMod_matches_UInt128()
{
var random = new Random(1);
for (var i = 0; i < 100; i++)
{
var high1 = ((ulong)random.Next() << 32) | (uint)random.Next();
var low1 = ((ulong)random.Next() << 32) | (uint)random.Next();
var high2 = ((ulong)random.Next() << 32) | (uint)random.Next();
var low2 = ((ulong)random.Next() << 32) | (uint)random.Next();
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var a = new GpuUInt128((UInt128)new GpuUInt128(high1, low1) % (UInt128)modulus);
var b = new GpuUInt128((UInt128)new GpuUInt128(high2, low2) % (UInt128)modulus);
var expected = MulModReference((UInt128)a, (UInt128)b, (UInt128)modulus);
a = a.MulMod(b, modulus);
((UInt128)a).Should().Be(expected);
}
}
[Fact]
public void MulMod_with_ulong_matches_UInt128()
{
var random = new Random(5);
for (var i = 0; i < 100; i++)
{
var high1 = ((ulong)random.Next() << 32) | (uint)random.Next();
var low1 = ((ulong)random.Next() << 32) | (uint)random.Next();
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var a = new GpuUInt128((UInt128)new GpuUInt128(high1, low1) % (UInt128)modulus);
ulong bRaw = ((ulong)random.Next() << 32) | (uint)random.Next();
UInt128 bBig = (UInt128)bRaw % (UInt128)modulus;
ulong b = (ulong)bBig;
var expected = MulModReference((UInt128)a, bBig, (UInt128)modulus);
a = a.MulMod(b, modulus);
((UInt128)a).Should().Be(expected);
}
}
[Fact]
public void MulMod_with_ulong_modulus_matches_UInt128()
{
var random = new Random(11);
for (var i = 0; i < 100; i++)
{
ulong modulus = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
ulong aRaw = ((ulong)random.Next() << 32) | (uint)random.Next();
ulong bRaw = ((ulong)random.Next() << 32) | (uint)random.Next();
ulong aVal = aRaw % modulus;
ulong bVal = bRaw % modulus;
var a = new GpuUInt128(aVal);
var b = new GpuUInt128(bVal);
UInt128 expected = ((UInt128)aVal * bVal) % modulus;
a = a.MulMod(b, modulus);
((UInt128)a).Should().Be(expected);
}
}
[Fact]
public void MulModBigInteger_matches_UInt128()
{
var random = new Random(13);
for (var i = 0; i < 20; i++)
{
var high1 = ((ulong)random.Next() << 32) | (uint)random.Next();
var low1 = ((ulong)random.Next() << 32) | (uint)random.Next();
var high2 = ((ulong)random.Next() << 32) | (uint)random.Next();
var low2 = ((ulong)random.Next() << 32) | (uint)random.Next();
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var a = new GpuUInt128((UInt128)new GpuUInt128(high1, low1) % (UInt128)modulus);
var b = new GpuUInt128((UInt128)new GpuUInt128(high2, low2) % (UInt128)modulus);
var expected = MulModReference((UInt128)a, (UInt128)b, (UInt128)modulus);
var result = new GpuUInt128(a.High, a.Low);
result = result.MulModBigInteger(b, modulus);
((UInt128)result).Should().Be(expected);
}
}
[Fact]
public void SquareMod_matches_UInt128()
{
var random = new Random(8);
for (var i = 0; i < 100; i++)
{
var valueHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var valueLow = ((ulong)random.Next() << 32) | (uint)random.Next();
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var a = new GpuUInt128((UInt128)new GpuUInt128(valueHigh, valueLow) % (UInt128)modulus);
var expected = MulModReference((UInt128)a, (UInt128)a, (UInt128)modulus);
a = a.SquareMod(modulus);
((UInt128)a).Should().Be(expected);
}
}
[Fact]
public void ModPow_matches_BigInteger()
{
var random = new Random(2);
for (var i = 0; i < 20; i++)
{
var baseHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var baseLow = ((ulong)random.Next() << 32) | (uint)random.Next();
var expHigh = 0UL;
var expLow = (ulong)random.Next(1, int.MaxValue);
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var value = new GpuUInt128((UInt128)new GpuUInt128(baseHigh, baseLow) % (UInt128)modulus);
var exponent = new GpuUInt128(expHigh, expLow);
var expected = (UInt128)System.Numerics.BigInteger.ModPow((System.Numerics.BigInteger)(UInt128)value, (System.Numerics.BigInteger)(UInt128)exponent, (System.Numerics.BigInteger)(UInt128)modulus);
value = value.ModPow(exponent, modulus);
((UInt128)value).Should().Be(expected);
}
}
[Fact]
public void ModPow_with_ulong_matches_BigInteger()
{
var random = new Random(9);
for (var i = 0; i < 20; i++)
{
var baseHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var baseLow = ((ulong)random.Next() << 32) | (uint)random.Next();
ulong exponent = (ulong)random.Next(1, int.MaxValue);
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var value = new GpuUInt128((UInt128)new GpuUInt128(baseHigh, baseLow) % (UInt128)modulus);
var expected = (UInt128)System.Numerics.BigInteger.ModPow((System.Numerics.BigInteger)(UInt128)value, exponent, (System.Numerics.BigInteger)(UInt128)modulus);
value = value.ModPow(exponent, modulus);
((UInt128)value).Should().Be(expected);
}
}
[Fact]
public void SubMod_matches_UInt128()
{
var random = new Random(3);
for (var i = 0; i < 100; i++)
{
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var valueHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var valueLow = ((ulong)random.Next() << 32) | (uint)random.Next();
var subHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var subLow = ((ulong)random.Next() << 32) | (uint)random.Next();
var a = new GpuUInt128((UInt128)new GpuUInt128(valueHigh, valueLow) % (UInt128)modulus);
var b = new GpuUInt128((UInt128)new GpuUInt128(subHigh, subLow) % (UInt128)modulus);
UInt128 expected = ((UInt128)a + (UInt128)modulus - (UInt128)b) % (UInt128)modulus;
a = a.SubMod(b, modulus);
((UInt128)a).Should().Be(expected);
}
}
[Fact]
public void SubMod_with_ulong_matches_UInt128()
{
var random = new Random(6);
for (var i = 0; i < 100; i++)
{
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var valueHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var valueLow = ((ulong)random.Next() << 32) | (uint)random.Next();
var a = new GpuUInt128((UInt128)new GpuUInt128(valueHigh, valueLow) % (UInt128)modulus);
ulong subRaw = ((ulong)random.Next() << 32) | (uint)random.Next();
UInt128 bBig = (UInt128)subRaw % (UInt128)modulus;
ulong b = (ulong)bBig;
UInt128 expected = ((UInt128)a + (UInt128)modulus - bBig) % (UInt128)modulus;
a = a.SubMod(b, modulus);
((UInt128)a).Should().Be(expected);
}
}
[Fact]
public void AddMod_matches_UInt128()
{
var random = new Random(4);
for (var i = 0; i < 100; i++)
{
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var valueHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var valueLow = ((ulong)random.Next() << 32) | (uint)random.Next();
var addHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var addLow = ((ulong)random.Next() << 32) | (uint)random.Next();
var a = new GpuUInt128((UInt128)new GpuUInt128(valueHigh, valueLow) % (UInt128)modulus);
var b = new GpuUInt128((UInt128)new GpuUInt128(addHigh, addLow) % (UInt128)modulus);
UInt128 expected = ((UInt128)a + (UInt128)b) % (UInt128)modulus;
a = a.AddMod(b, modulus);
((UInt128)a).Should().Be(expected);
}
}
[Fact]
public void AddMod_with_ulong_modulus_matches_UInt128()
{
var random = new Random(12);
for (var i = 0; i < 100; i++)
{
ulong modulus = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
ulong aRaw = ((ulong)random.Next() << 32) | (uint)random.Next();
ulong bRaw = ((ulong)random.Next() << 32) | (uint)random.Next();
ulong aVal = aRaw % modulus;
ulong bVal = bRaw % modulus;
var a = new GpuUInt128(aVal);
var b = new GpuUInt128(bVal);
UInt128 expected = ((UInt128)aVal + bVal) % modulus;
a = a.AddMod(b, modulus);
((UInt128)a).Should().Be(expected);
}
}
[Fact]
public void AddMod_with_ulong_operand_and_modulus_matches_UInt128()
{
var random = new Random(13);
for (var i = 0; i < 100; i++)
{
ulong modulus = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
ulong aRaw = ((ulong)random.Next() << 32) | (uint)random.Next();
ulong bRaw = ((ulong)random.Next() << 32) | (uint)random.Next();
ulong aVal = aRaw % modulus;
ulong bVal = bRaw % modulus;
var a = new GpuUInt128(aVal);
UInt128 expected = ((UInt128)aVal + bVal) % modulus;
a = a.AddMod(bVal, modulus);
((UInt128)a).Should().Be(expected);
}
}
[Fact]
public void ModInv_matches_BigInteger()
{
var random = new Random(7);
for (var i = 0; i < 20; i++)
{
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var valueHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var valueLow = ((ulong)random.Next() << 32) | (uint)random.Next();
var value = new GpuUInt128((UInt128)new GpuUInt128(valueHigh, valueLow) % (UInt128)modulus);
if (value.IsZero)
{
value = value.Add(1UL);
}
var expected = (UInt128)System.Numerics.BigInteger.ModPow((System.Numerics.BigInteger)(UInt128)value, (System.Numerics.BigInteger)(UInt128)modulus - 2, (System.Numerics.BigInteger)(UInt128)modulus);
value = value.ModInv(modulus);
((UInt128)value).Should().Be(expected);
}
}
[Fact]
public void ModInv_with_ulong_modulus_matches_BigInteger()
{
var random = new Random(8);
for (var i = 0; i < 20; i++)
{
ulong modulus = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
ulong raw = ((ulong)random.Next() << 32) | (uint)random.Next();
var value = new GpuUInt128((UInt128)(raw % modulus));
if (value.IsZero)
{
value = value.Add(1UL);
}
var expected = (UInt128)System.Numerics.BigInteger.ModPow((System.Numerics.BigInteger)(UInt128)value, (System.Numerics.BigInteger)modulus - 2, (System.Numerics.BigInteger)modulus);
value = value.ModInv(modulus);
((UInt128)value).Should().Be(expected);
}
}
private static UInt128 MulModReference(UInt128 a, UInt128 b, UInt128 modulus)
{
UInt128 result = 0;
UInt128 x = a;
UInt128 y = b;
while (y != 0)
{
if ((y & 1) != 0)
{
result += x;
if (result >= modulus)
{
result -= modulus;
}
}
x <<= 1;
if (x >= modulus)
{
x -= modulus;
}
y >>= 1;
}
return result;
}
}
@@ -0,0 +1,53 @@
using FluentAssertions;
using PerfectNumbers.Core.Gpu;
using Xunit;
namespace PerfectNumbers.Core.Tests.Gpu;
[Trait("Category", "Slow")]
public class LucasLehmerPrimeTesterTests
{
[Theory]
[InlineData(3UL, true)]
[InlineData(5UL, true)]
[InlineData(11UL, false)]
[Trait("Category", "Fast")]
public void IsMersennePrime_gpu_returns_expected_results(ulong p, bool expected)
{
new MersenneNumberLucasLehmerGpuTester().IsMersennePrime(p).Should().Be(expected);
}
[Fact]
[Trait("Category", "Fast")]
public void IsMersennePrime_gpu_large_exponent_without_cpu_fallback()
{
// Use a modest exponent that still exercises the NTT path while keeping
// runtime short enough for the unit test environment.
const ulong exponent = 128UL;
var tester = new MersenneNumberLucasLehmerGpuTester();
tester.WarmUpNttParameters(exponent);
tester.IsMersennePrime(exponent).Should().BeFalse();
}
[Theory]
[InlineData(132UL)]
[InlineData(1000UL)]
[Trait("Category", "Slow")]
public void WarmUpNttParameters_populates_cache_for_large_exponent(ulong exponent)
{
var tester = new MersenneNumberLucasLehmerGpuTester();
tester.WarmUpNttParameters(exponent);
tester.IsMersennePrime(exponent).Should().BeFalse();
}
[Theory]
[InlineData(3UL, true)]
[InlineData(5UL, true)]
[InlineData(11UL, false)]
[Trait("Category", "Fast")]
public void IsMersennePrime_cpu_matches_expected_results(ulong p, bool expected)
{
new MersenneNumberLucasLehmerCpuTester().IsPrime(p).Should().Be(expected);
}
}
@@ -0,0 +1,78 @@
using FluentAssertions;
using PerfectNumbers.Core.Gpu;
using Xunit;
namespace PerfectNumbers.Core.Tests.Gpu;
[Collection("GpuNtt")]
public class NttBackendConfigTests
{
[Fact]
[Trait("Category", "Fast")]
public void ForwardInverseGpu_reference_backend_matches_cpu_on_small_input()
{
var original = NttGpuMath.GpuTransformBackend;
try
{
Span<GpuUInt128> input = stackalloc GpuUInt128[8];
for (ulong i = 0; i < (ulong)input.Length; i++)
{
input[(int)i] = new GpuUInt128(0UL, i + 1UL);
}
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
NttGpuMath.GpuTransformBackend = NttBackend.Reference;
Span<GpuUInt128> gpu = stackalloc GpuUInt128[input.Length];
input.CopyTo(gpu);
NttGpuMath.ForwardGpu(gpu, modulus, primitiveRoot);
NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot);
Span<GpuUInt128> cpu = stackalloc GpuUInt128[input.Length];
input.CopyTo(cpu);
NttGpuMath.Forward(cpu, modulus, primitiveRoot);
NttGpuMath.Inverse(cpu, modulus, primitiveRoot);
gpu.ToArray().Should().Equal(cpu.ToArray());
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
[Fact]
[Trait("Category", "Fast")]
public void ForwardInverseGpu_staged_backend_matches_cpu_on_small_input()
{
var original = NttGpuMath.GpuTransformBackend;
try
{
Span<GpuUInt128> input = stackalloc GpuUInt128[8];
for (ulong i = 0; i < (ulong)input.Length; i++)
{
input[(int)i] = new GpuUInt128(0UL, i + 1UL);
}
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
NttGpuMath.GpuTransformBackend = NttBackend.Staged;
Span<GpuUInt128> gpu = stackalloc GpuUInt128[input.Length];
input.CopyTo(gpu);
NttGpuMath.ForwardGpu(gpu, modulus, primitiveRoot);
NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot);
Span<GpuUInt128> cpu = stackalloc GpuUInt128[input.Length];
input.CopyTo(cpu);
NttGpuMath.Forward(cpu, modulus, primitiveRoot);
NttGpuMath.Inverse(cpu, modulus, primitiveRoot);
gpu.ToArray().Should().Equal(cpu.ToArray());
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
}
@@ -0,0 +1,361 @@
using System.Reflection;
using FluentAssertions;
using ILGPU;
using ILGPU.Runtime;
using PerfectNumbers.Core.Gpu;
using System.Numerics;
using Xunit;
namespace PerfectNumbers.Core.Tests.Gpu;
// GPU-related tests are organized per-backend so both reference and staged
// implementations are exercised under the fast test suite.
[Collection("GpuNtt")]
public class NttGpuMathTests
{
[Fact]
[Trait("Category", "Fast")]
public void ForwardGpu_reference_matches_cpu_forward()
{
var original = NttGpuMath.GpuTransformBackend;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Reference;
Span<GpuUInt128> cpu = stackalloc GpuUInt128[4];
cpu[0] = new GpuUInt128(0UL, 1UL);
cpu[1] = new GpuUInt128(0UL, 2UL);
cpu[2] = new GpuUInt128(0UL, 3UL);
cpu[3] = new GpuUInt128(0UL, 4UL);
Span<GpuUInt128> gpu = stackalloc GpuUInt128[4];
cpu.CopyTo(gpu);
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
NttGpuMath.Forward(cpu, modulus, primitiveRoot);
NttGpuMath.ForwardGpu(gpu, modulus, primitiveRoot);
for (int i = 0; i < cpu.Length; i++)
{
gpu[i].Should().Be(cpu[i]);
}
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
[Fact]
[Trait("Category", "Fast")]
public void ForwardGpu_staged_matches_cpu_forward()
{
var original = NttGpuMath.GpuTransformBackend;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Staged;
Span<GpuUInt128> cpu = stackalloc GpuUInt128[4];
cpu[0] = new GpuUInt128(0UL, 1UL);
cpu[1] = new GpuUInt128(0UL, 2UL);
cpu[2] = new GpuUInt128(0UL, 3UL);
cpu[3] = new GpuUInt128(0UL, 4UL);
Span<GpuUInt128> gpu = stackalloc GpuUInt128[4];
cpu.CopyTo(gpu);
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
NttGpuMath.Forward(cpu, modulus, primitiveRoot);
NttGpuMath.ForwardGpu(gpu, modulus, primitiveRoot);
for (int i = 0; i < cpu.Length; i++)
{
gpu[i].Should().Be(cpu[i]);
}
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
[Fact]
[Trait("Category", "Fast")]
public void InverseGpu_reference_matches_cpu_inverse()
{
var original = NttGpuMath.GpuTransformBackend;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Reference;
Span<GpuUInt128> input = stackalloc GpuUInt128[4];
input[0] = new GpuUInt128(0UL, 5UL);
input[1] = new GpuUInt128(0UL, 7UL);
input[2] = new GpuUInt128(0UL, 9UL);
input[3] = new GpuUInt128(0UL, 11UL);
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
Span<GpuUInt128> transformed = stackalloc GpuUInt128[4];
input.CopyTo(transformed);
NttGpuMath.Forward(transformed, modulus, primitiveRoot);
Span<GpuUInt128> cpu = stackalloc GpuUInt128[4];
transformed.CopyTo(cpu);
NttGpuMath.Inverse(cpu, modulus, primitiveRoot);
Span<GpuUInt128> gpu = stackalloc GpuUInt128[4];
transformed.CopyTo(gpu);
NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot);
for (int i = 0; i < input.Length; i++)
{
gpu[i].Should().Be(cpu[i]);
}
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
[Fact]
[Trait("Category", "Fast")]
public void InverseGpu_staged_matches_cpu_inverse()
{
var original = NttGpuMath.GpuTransformBackend;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Staged;
Span<GpuUInt128> input = stackalloc GpuUInt128[4];
input[0] = new GpuUInt128(0UL, 5UL);
input[1] = new GpuUInt128(0UL, 7UL);
input[2] = new GpuUInt128(0UL, 9UL);
input[3] = new GpuUInt128(0UL, 11UL);
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
Span<GpuUInt128> transformed = stackalloc GpuUInt128[4];
input.CopyTo(transformed);
NttGpuMath.Forward(transformed, modulus, primitiveRoot);
Span<GpuUInt128> cpu = stackalloc GpuUInt128[4];
transformed.CopyTo(cpu);
NttGpuMath.Inverse(cpu, modulus, primitiveRoot);
Span<GpuUInt128> gpu = stackalloc GpuUInt128[4];
transformed.CopyTo(gpu);
NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot);
for (int i = 0; i < input.Length; i++)
{
gpu[i].Should().Be(cpu[i]);
}
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
[Fact]
[Trait("Category", "Fast")]
public void Montgomery_twiddle_inverses_multiply_to_r2()
{
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
using var gpu = GpuContextPool.Rent();
var accelerator = gpu.Accelerator;
var method = typeof(NttGpuMath).GetMethod("GetSquareCache", BindingFlags.NonPublic | BindingFlags.Static);
method.Should().NotBeNull();
var cache = method!.Invoke(null, new object[] { accelerator, 8, modulus, primitiveRoot })!;
var cacheType = cache.GetType();
var twiddlesMont = (MemoryBuffer1D<GpuUInt128, Stride1D.Dense>)cacheType.GetProperty("TwiddlesMont")!.GetValue(cache)!;
var twiddlesInvMont = (MemoryBuffer1D<GpuUInt128, Stride1D.Dense>)cacheType.GetProperty("TwiddlesInvMont")!.GetValue(cache)!;
var r2 = (ulong)cacheType.GetProperty("MontR2Mod64")!.GetValue(cache)!;
var mod = (ulong)cacheType.GetProperty("ModulusLow")!.GetValue(cache)!;
int count = 7; // length 8 -> 7 twiddles
var forward = new GpuUInt128[count];
var inverse = new GpuUInt128[count];
twiddlesMont.CopyToCPU(forward);
twiddlesInvMont.CopyToCPU(inverse);
for (int i = 0; i < count; i++)
{
ulong prod = (ulong)((((BigInteger)forward[i].Low) * inverse[i].Low) % mod);
prod.Should().Be(r2);
}
}
[Fact]
[Trait("Category", "Fast")]
public void ConvolveGpu_reference_matches_cpu_convolve()
{
var original = NttGpuMath.GpuTransformBackend;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Reference;
Span<GpuUInt128> leftCpu = stackalloc GpuUInt128[4];
Span<GpuUInt128> rightCpu = stackalloc GpuUInt128[4];
leftCpu[0] = new GpuUInt128(0UL, 1UL);
leftCpu[1] = new GpuUInt128(0UL, 2UL);
leftCpu[2] = new GpuUInt128(0UL, 3UL);
leftCpu[3] = new GpuUInt128(0UL, 4UL);
rightCpu[0] = new GpuUInt128(0UL, 5UL);
rightCpu[1] = new GpuUInt128(0UL, 6UL);
rightCpu[2] = new GpuUInt128(0UL, 7UL);
rightCpu[3] = new GpuUInt128(0UL, 8UL);
Span<GpuUInt128> leftGpu = stackalloc GpuUInt128[4];
Span<GpuUInt128> rightGpu = stackalloc GpuUInt128[4];
leftCpu.CopyTo(leftGpu);
rightCpu.CopyTo(rightGpu);
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
NttGpuMath.Convolve(leftCpu, rightCpu, modulus, primitiveRoot);
NttGpuMath.ConvolveGpu(leftGpu, rightGpu, modulus, primitiveRoot);
for (int i = 0; i < leftCpu.Length; i++)
{
leftGpu[i].Should().Be(leftCpu[i]);
}
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
[Fact]
[Trait("Category", "Fast")]
public void ConvolveGpu_staged_matches_cpu_convolve()
{
var original = NttGpuMath.GpuTransformBackend;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Staged;
Span<GpuUInt128> leftCpu = stackalloc GpuUInt128[4];
Span<GpuUInt128> rightCpu = stackalloc GpuUInt128[4];
leftCpu[0] = new GpuUInt128(0UL, 1UL);
leftCpu[1] = new GpuUInt128(0UL, 2UL);
leftCpu[2] = new GpuUInt128(0UL, 3UL);
leftCpu[3] = new GpuUInt128(0UL, 4UL);
rightCpu[0] = new GpuUInt128(0UL, 5UL);
rightCpu[1] = new GpuUInt128(0UL, 6UL);
rightCpu[2] = new GpuUInt128(0UL, 7UL);
rightCpu[3] = new GpuUInt128(0UL, 8UL);
Span<GpuUInt128> leftGpu = stackalloc GpuUInt128[4];
Span<GpuUInt128> rightGpu = stackalloc GpuUInt128[4];
leftCpu.CopyTo(leftGpu);
rightCpu.CopyTo(rightGpu);
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
NttGpuMath.Convolve(leftCpu, rightCpu, modulus, primitiveRoot);
NttGpuMath.ConvolveGpu(leftGpu, rightGpu, modulus, primitiveRoot);
for (int i = 0; i < leftCpu.Length; i++)
{
leftGpu[i].Should().Be(leftCpu[i]);
}
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
[Fact]
[Trait("Category", "Fast")]
public void SquareGpu_reference_matches_cpu_square()
{
var original = NttGpuMath.GpuTransformBackend;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Reference;
Span<GpuUInt128> cpu = stackalloc GpuUInt128[4];
cpu[0] = new GpuUInt128(0UL, 1UL);
cpu[1] = new GpuUInt128(0UL, 2UL);
cpu[2] = new GpuUInt128(0UL, 3UL);
cpu[3] = new GpuUInt128(0UL, 4UL);
Span<GpuUInt128> gpu = stackalloc GpuUInt128[4];
cpu.CopyTo(gpu);
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
NttGpuMath.Square(cpu, modulus, primitiveRoot);
NttGpuMath.SquareGpu(gpu, modulus, primitiveRoot);
for (int i = 0; i < cpu.Length; i++)
{
gpu[i].Should().Be(cpu[i]);
}
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
[Fact]
[Trait("Category", "Fast")]
public void SquareGpu_staged_matches_cpu_square()
{
var original = NttGpuMath.GpuTransformBackend;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Staged;
Span<GpuUInt128> cpu = stackalloc GpuUInt128[4];
cpu[0] = new GpuUInt128(0UL, 1UL);
cpu[1] = new GpuUInt128(0UL, 2UL);
cpu[2] = new GpuUInt128(0UL, 3UL);
cpu[3] = new GpuUInt128(0UL, 4UL);
Span<GpuUInt128> gpu = stackalloc GpuUInt128[4];
cpu.CopyTo(gpu);
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
NttGpuMath.Square(cpu, modulus, primitiveRoot);
NttGpuMath.SquareGpu(gpu, modulus, primitiveRoot);
for (int i = 0; i < cpu.Length; i++)
{
gpu[i].Should().Be(cpu[i]);
}
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
[Fact]
[Trait("Category", "Fast")]
public void BitReverseGpu_matches_cpu()
{
GpuUInt128[] cpu = new GpuUInt128[8];
for (ulong i = 0; i < (ulong)cpu.Length; i++)
{
cpu[i] = new GpuUInt128(0UL, i);
}
GpuUInt128[] gpu = (GpuUInt128[])cpu.Clone();
NttGpuMath.BitReverse(cpu);
NttGpuMath.BitReverseGpu(gpu);
gpu.Should().Equal(cpu);
}
[Fact]
[Trait("Category", "Fast")]
public void DisposeAll_clears_ntt_caches()
{
// Ensure the cache starts empty.
NttGpuMath.ClearCaches();
Span<GpuUInt128> values = stackalloc GpuUInt128[2];
values[0] = new GpuUInt128(0UL, 1UL);
values[1] = new GpuUInt128(0UL, 2UL);
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
using (var gpu = GpuContextPool.Rent())
{
var buffer = gpu.Accelerator.Allocate1D<GpuUInt128>(values.Length);
buffer.View.CopyFromCPU(ref values[0], values.Length);
NttGpuMath.SquareDevice(gpu.Accelerator, buffer.View, modulus, primitiveRoot);
buffer.Dispose();
}
// Verify that the square cache now holds entries for the accelerator.
var field = typeof(NttGpuMath).GetField("SquareCache", BindingFlags.NonPublic | BindingFlags.Static)!;
var cacheObj = field.GetValue(null)!;
int count = (int)cacheObj.GetType().GetProperty("Count")!.GetValue(cacheObj)!;
count.Should().BeGreaterThan(0);
GpuContextPool.DisposeAll();
count = (int)cacheObj.GetType().GetProperty("Count")!.GetValue(cacheObj)!;
count.Should().Be(0);
}
}
@@ -0,0 +1,87 @@
using FluentAssertions;
using ILGPU.Runtime;
using PerfectNumbers.Core.Gpu;
using Xunit;
namespace PerfectNumbers.Core.Tests.Gpu;
[Collection("GpuNtt")]
[Trait("Category", "Fast")]
public class NttMontgomeryTests
{
[Fact]
[Trait("Category", "Fast")]
public void SquareDevice_montgomery64_matches_cpu_square_on_small_input()
{
// Small 64-bit modulus and tiny input to keep runtime trivial.
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
Span<GpuUInt128> cpu = stackalloc GpuUInt128[8];
for (ulong i = 0; i < (ulong)cpu.Length; i++)
{
cpu[(int)i] = new GpuUInt128(0UL, i + 1UL);
}
Span<GpuUInt128> gpu = stackalloc GpuUInt128[16];
cpu.CopyTo(gpu);
for (int i = cpu.Length; i < gpu.Length; i++)
{
gpu[i] = new GpuUInt128(0UL, 0UL);
}
// CPU reference
NttGpuMath.Square(cpu, modulus, primitiveRoot);
// Device execution
using var context = GpuContextPool.Rent();
var accelerator = context.Accelerator;
using var buffer = accelerator.Allocate1D<GpuUInt128>(gpu.Length);
buffer.View.CopyFromCPU(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++)
{
gpu[i].Should().Be(cpu[i]);
}
}
[Fact]
[Trait("Category", "Fast")]
public void InverseDevice_montgomery64_matches_cpu_inverse_on_small_input()
{
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
Span<GpuUInt128> input = stackalloc GpuUInt128[8];
for (ulong i = 0; i < (ulong)input.Length; i++)
{
input[(int)i] = new GpuUInt128(0UL, i + 1UL);
}
Span<GpuUInt128> transformed = stackalloc GpuUInt128[input.Length];
input.CopyTo(transformed);
NttGpuMath.Forward(transformed, modulus, primitiveRoot);
Span<GpuUInt128> cpu = stackalloc GpuUInt128[input.Length];
transformed.CopyTo(cpu);
NttGpuMath.Inverse(cpu, modulus, primitiveRoot);
var original = NttGpuMath.GpuTransformBackend;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Staged;
Span<GpuUInt128> gpu = stackalloc GpuUInt128[input.Length];
transformed.CopyTo(gpu);
NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot);
for (int i = 0; i < input.Length; i++)
{
gpu[i].Should().Be(cpu[i]);
}
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
}
@@ -0,0 +1,90 @@
using FluentAssertions;
using PerfectNumbers.Core.Gpu;
using Xunit;
namespace PerfectNumbers.Core.Tests.Gpu;
[Collection("GpuNtt")]
public class NttReductionModeTests
{
[Theory]
[Trait("Category", "Fast")]
[InlineData(ModReductionMode.Auto)]
[InlineData(ModReductionMode.GpuUInt128)]
public void ForwardInverse_small64bit_modulus_reference_backend_works_with_mode(ModReductionMode mode)
{
var originalBackend = NttGpuMath.GpuTransformBackend;
var originalMode = NttGpuMath.ReductionMode;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Reference;
NttGpuMath.ReductionMode = mode;
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
Span<GpuUInt128> input = stackalloc GpuUInt128[8];
for (ulong i = 0; i < (ulong)input.Length; i++)
{
input[(int)i] = new GpuUInt128(0UL, i + 1UL);
}
Span<GpuUInt128> cpu = stackalloc GpuUInt128[input.Length];
input.CopyTo(cpu);
NttGpuMath.Forward(cpu, modulus, primitiveRoot);
NttGpuMath.Inverse(cpu, modulus, primitiveRoot);
Span<GpuUInt128> gpu = stackalloc GpuUInt128[input.Length];
input.CopyTo(gpu);
NttGpuMath.ForwardGpu(gpu, modulus, primitiveRoot);
NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot);
gpu.ToArray().Should().Equal(cpu.ToArray());
}
finally
{
NttGpuMath.GpuTransformBackend = originalBackend;
NttGpuMath.ReductionMode = originalMode;
}
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(ModReductionMode.Auto)]
[InlineData(ModReductionMode.GpuUInt128)]
public void ForwardInverse_small64bit_modulus_staged_backend_works_with_mode(ModReductionMode mode)
{
var originalBackend = NttGpuMath.GpuTransformBackend;
var originalMode = NttGpuMath.ReductionMode;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Staged;
NttGpuMath.ReductionMode = mode;
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
Span<GpuUInt128> input = stackalloc GpuUInt128[8];
for (ulong i = 0; i < (ulong)input.Length; i++)
{
input[(int)i] = new GpuUInt128(0UL, i + 1UL);
}
Span<GpuUInt128> cpu = stackalloc GpuUInt128[input.Length];
input.CopyTo(cpu);
NttGpuMath.Forward(cpu, modulus, primitiveRoot);
NttGpuMath.Inverse(cpu, modulus, primitiveRoot);
Span<GpuUInt128> gpu = stackalloc GpuUInt128[input.Length];
input.CopyTo(gpu);
NttGpuMath.ForwardGpu(gpu, modulus, primitiveRoot);
NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot);
gpu.ToArray().Should().Equal(cpu.ToArray());
}
finally
{
NttGpuMath.GpuTransformBackend = originalBackend;
NttGpuMath.ReductionMode = originalMode;
}
}
}