Restore GPU-backed UInt128 pow2 Montgomery helper

This commit is contained in:
Marek Lemańczyk committed 2025-10-05 02:13:31 +02:00
1 parent 7ffd438eee
commit 0bc97937aa
11 files changed
+1561 -30

No files matched your search

+269
View File
@@ -0,0 +1,269 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
namespace PerfectNumbers.Core.Gpu;
public struct GpuRational : IEquatable<GpuRational>, IComparable<GpuRational>
{
private const double Pow64 = 18446744073709551616.0;
public GpuUInt128 Numerator;
public GpuUInt128 Denominator;
public static readonly GpuRational Zero = new(GpuUInt128.Zero, GpuUInt128.One);
public static readonly GpuRational One = new(GpuUInt128.One, GpuUInt128.One);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public GpuRational(GpuUInt128 numerator)
: this(numerator, GpuUInt128.One)
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public GpuRational(ulong numerator)
: this(new GpuUInt128(numerator), GpuUInt128.One)
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public GpuRational(GpuUInt128 numerator, GpuUInt128 denominator)
{
if (denominator.IsZero)
{
throw new DivideByZeroException("GpuRational denominator cannot be zero.");
}
Numerator = numerator;
Denominator = denominator;
Normalize();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public GpuRational(ulong numerator, ulong denominator)
: this(new GpuUInt128(numerator), new GpuUInt128(denominator))
{
}
public readonly bool IsZero
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Numerator.IsZero;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator GpuRational(GpuUInt128 value) => new(value);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator GpuRational(ulong value) => new(value);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(in GpuRational other)
{
GpuUInt128 leftDenominator = Denominator;
GpuUInt128 rightDenominator = other.Denominator;
GpuUInt128 gcd = GpuUInt128.BinaryGcd(leftDenominator, rightDenominator);
if (gcd != GpuUInt128.One)
{
leftDenominator = GpuUInt128.DivideExact(leftDenominator, gcd);
rightDenominator = GpuUInt128.DivideExact(rightDenominator, gcd);
// Reusing both denominator variables to store their reduced values because the unreduced inputs are no longer needed.
}
Numerator.Mul(rightDenominator);
GpuUInt128 scaledOtherNumerator = other.Numerator;
scaledOtherNumerator.Mul(leftDenominator);
Numerator.Add(scaledOtherNumerator);
Denominator = leftDenominator;
// Denominator now holds the reduced left denominator; multiply by the original right denominator to form the LCM.
Denominator.Mul(other.Denominator);
Normalize();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Sub(in GpuRational other)
{
GpuUInt128 leftDenominator = Denominator;
GpuUInt128 rightDenominator = other.Denominator;
GpuUInt128 gcd = GpuUInt128.BinaryGcd(leftDenominator, rightDenominator);
if (gcd != GpuUInt128.One)
{
leftDenominator = GpuUInt128.DivideExact(leftDenominator, gcd);
rightDenominator = GpuUInt128.DivideExact(rightDenominator, gcd);
// Reusing the denominator variables to keep the reduced values since the raw denominators are no longer required.
}
Numerator.Mul(rightDenominator);
GpuUInt128 scaledOtherNumerator = other.Numerator;
scaledOtherNumerator.Mul(leftDenominator);
if (Numerator.CompareTo(scaledOtherNumerator) < 0)
{
throw new InvalidOperationException("Resulting GpuRational would be negative.");
}
Numerator.Sub(scaledOtherNumerator);
Denominator = leftDenominator;
Denominator.Mul(other.Denominator);
Normalize();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Mul(in GpuRational other)
{
GpuUInt128 rightDenominator = other.Denominator;
GpuUInt128 gcd = GpuUInt128.BinaryGcd(Numerator, rightDenominator);
if (gcd != GpuUInt128.One)
{
Numerator = GpuUInt128.DivideExact(Numerator, gcd);
rightDenominator = GpuUInt128.DivideExact(rightDenominator, gcd);
// Reusing rightDenominator to keep the reduced value because the original denominator is no longer required.
}
GpuUInt128 rightNumerator = other.Numerator;
GpuUInt128 leftDenominator = Denominator;
gcd = GpuUInt128.BinaryGcd(rightNumerator, leftDenominator);
if (gcd != GpuUInt128.One)
{
rightNumerator = GpuUInt128.DivideExact(rightNumerator, gcd);
leftDenominator = GpuUInt128.DivideExact(leftDenominator, gcd);
// Reusing rightNumerator and leftDenominator after cross-cancellation keeps the intermediates compact.
}
Numerator.Mul(rightNumerator);
Denominator = leftDenominator;
Denominator.Mul(rightDenominator);
Normalize();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Div(in GpuRational other)
{
if (other.Numerator.IsZero)
{
throw new DivideByZeroException("Cannot divide by a zero GpuRational.");
}
GpuUInt128 rightNumerator = other.Numerator;
GpuUInt128 gcd = GpuUInt128.BinaryGcd(Numerator, rightNumerator);
if (gcd != GpuUInt128.One)
{
Numerator = GpuUInt128.DivideExact(Numerator, gcd);
rightNumerator = GpuUInt128.DivideExact(rightNumerator, gcd);
// Reusing rightNumerator to store the reduced numerator keeps temporary allocations out of the hot path.
}
GpuUInt128 rightDenominator = other.Denominator;
GpuUInt128 leftDenominator = Denominator;
gcd = GpuUInt128.BinaryGcd(rightDenominator, leftDenominator);
if (gcd != GpuUInt128.One)
{
rightDenominator = GpuUInt128.DivideExact(rightDenominator, gcd);
leftDenominator = GpuUInt128.DivideExact(leftDenominator, gcd);
// Reusing both denominator variables here mirrors the cross-cancellation performed on the numerators.
}
Numerator.Mul(rightDenominator);
Denominator = leftDenominator;
Denominator.Mul(rightNumerator);
Normalize();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GpuRational operator +(GpuRational left, GpuRational right)
{
left.Add(right);
return left;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GpuRational operator -(GpuRational left, GpuRational right)
{
left.Sub(right);
return left;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GpuRational operator *(GpuRational left, GpuRational right)
{
left.Mul(right);
return left;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GpuRational operator /(GpuRational left, GpuRational right)
{
left.Div(right);
return left;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly int CompareTo(GpuRational other)
{
GpuUInt128 leftScaled = Numerator;
leftScaled.Mul(other.Denominator);
GpuUInt128 rightScaled = other.Numerator;
rightScaled.Mul(Denominator);
return leftScaled.CompareTo(rightScaled);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly bool Equals(GpuRational other) => Numerator == other.Numerator && Denominator == other.Denominator;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override readonly bool Equals(object? obj) => obj is GpuRational other && Equals(other);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override readonly int GetHashCode() => HashCode.Combine(Numerator, Denominator);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(GpuRational left, GpuRational right) => left.Equals(right);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(GpuRational left, GpuRational right) => !left.Equals(right);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator <(GpuRational left, GpuRational right) => left.CompareTo(right) < 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator >(GpuRational left, GpuRational right) => left.CompareTo(right) > 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator <=(GpuRational left, GpuRational right) => left.CompareTo(right) <= 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator >=(GpuRational left, GpuRational right) => left.CompareTo(right) >= 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly double ToDouble()
{
double numerator = Numerator.High == 0UL ? Numerator.Low : Numerator.High * Pow64 + Numerator.Low;
double denominator = Denominator.High == 0UL ? Denominator.Low : Denominator.High * Pow64 + Denominator.Low;
return numerator / denominator;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override readonly string ToString() => $"{Numerator}/{Denominator}";
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Normalize()
{
if (Numerator.IsZero)
{
Denominator = GpuUInt128.One;
return;
}
GpuUInt128 gcd = GpuUInt128.BinaryGcd(Numerator, Denominator);
if (gcd != GpuUInt128.One)
{
Numerator = GpuUInt128.DivideExact(Numerator, gcd);
Denominator = GpuUInt128.DivideExact(Denominator, gcd);
}
}
}
+57
View File
@@ -54,6 +54,26 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
get => High == 0UL && Low == 0UL;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly int GetBitLength()
{
if (High != 0UL)
{
return 64 + (64 - BitOperations.LeadingZeroCount(High));
}
ulong low = Low;
if (low == 0UL)
{
return 0;
}
return 64 - BitOperations.LeadingZeroCount(low);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetBitLength(GpuUInt128 value) => value.GetBitLength();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator GpuUInt128(UInt128 value) => new(value);
@@ -564,6 +584,43 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
Low = lowPart;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GpuUInt128 DivideExact(GpuUInt128 dividend, GpuUInt128 divisor)
{
int divisorBits = divisor.GetBitLength();
int shift = dividend.GetBitLength() - divisorBits;
GpuUInt128 quotient = Zero;
GpuUInt128 remainder = dividend;
GpuUInt128 shiftedDivisor = divisor << shift;
GpuUInt128 quotientBit = One << shift;
while (true)
{
if (remainder.CompareTo(shiftedDivisor) >= 0)
{
remainder.Sub(shiftedDivisor);
quotient.Add(quotientBit);
}
if (shift == 0)
{
break;
}
shift--;
shiftedDivisor.ShiftRight(1);
quotientBit.ShiftRight(1);
}
if (!remainder.IsZero)
{
throw new InvalidOperationException("DivideExact requires divisible operands.");
}
return quotient;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Xor(GpuUInt128 other)
{
@@ -1,9 +1,12 @@
using System;
using System.Buffers;
using System.Collections.Concurrent;
using ILGPU;
using ILGPU.Runtime;
using System.Numerics;
using PerfectNumbers.Core;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
namespace PerfectNumbers.Core.Gpu;
internal enum GpuPow2ModStatus
@@ -16,10 +19,24 @@ internal enum GpuPow2ModStatus
internal static class PrimeOrderGpuHeuristics
{
private static readonly ConcurrentDictionary<ulong, byte> OverflowedPrimes = new();
private static readonly ConcurrentDictionary<UInt128, byte> OverflowedPrimesWide = new();
private static readonly ConcurrentDictionary<Accelerator, Action<AcceleratorStream, Index1D, ArrayView1D<ulong, Stride1D.Dense>, MontgomeryDivisorData, ArrayView1D<ulong, Stride1D.Dense>>> Pow2ModKernelCache = new();
private static readonly ConcurrentDictionary<Accelerator, Action<AcceleratorStream, Index1D, ArrayView1D<GpuUInt128, Stride1D.Dense>, GpuUInt128, ArrayView1D<GpuUInt128, Stride1D.Dense>>> Pow2ModKernelWideCache = new();
private const int WideStackThreshold = 8;
private static PrimeOrderGpuCapability s_capability = PrimeOrderGpuCapability.Default;
private const int Pow2WindowSizeBits = 8;
private const int Pow2WindowOddPowerCount = 1 << (Pow2WindowSizeBits - 1);
private const ulong Pow2WindowFallbackThreshold = 32UL;
[InlineArray(Pow2WindowOddPowerCount)]
private struct Pow2OddPowerTable
{
private GpuUInt128 _element0;
}
internal static ConcurrentDictionary<ulong, byte> OverflowRegistry => OverflowedPrimes;
internal static ConcurrentDictionary<UInt128, byte> OverflowRegistryWide => OverflowedPrimesWide;
internal static void OverrideCapabilitiesForTesting(PrimeOrderGpuCapability capability)
{
@@ -89,6 +106,59 @@ internal static class PrimeOrderGpuHeuristics
return computed ? GpuPow2ModStatus.Success : GpuPow2ModStatus.Unavailable;
}
public static GpuPow2ModStatus TryPow2Mod(UInt128 exponent, UInt128 prime, out UInt128 remainder)
{
Span<UInt128> exponents = stackalloc UInt128[1];
Span<UInt128> remainders = stackalloc UInt128[1];
exponents[0] = exponent;
GpuPow2ModStatus status = TryPow2ModBatch(exponents, prime, remainders);
remainder = remainders[0];
return status;
}
public static GpuPow2ModStatus TryPow2ModBatch(ReadOnlySpan<UInt128> exponents, UInt128 prime, Span<UInt128> remainders)
{
if (exponents.Length == 0)
{
return GpuPow2ModStatus.Success;
}
if (remainders.Length < exponents.Length)
{
throw new ArgumentException("Remainder span is shorter than the exponent span.", nameof(remainders));
}
Span<UInt128> target = remainders.Slice(0, exponents.Length);
target.Clear();
ConcurrentDictionary<UInt128, byte> overflowRegistryWide = OverflowedPrimesWide;
if (overflowRegistryWide.ContainsKey(prime))
{
return GpuPow2ModStatus.Overflow;
}
PrimeOrderGpuCapability capability = s_capability;
if (prime.GetBitLength() > capability.ModulusBits)
{
overflowRegistryWide[prime] = 0;
return GpuPow2ModStatus.Overflow;
}
for (int i = 0; i < exponents.Length; i++)
{
if (exponents[i].GetBitLength() > capability.ExponentBits)
{
return GpuPow2ModStatus.Overflow;
}
}
bool computed = TryComputeOnGpuWide(exponents, prime, target);
return computed ? GpuPow2ModStatus.Success : GpuPow2ModStatus.Unavailable;
}
private static bool TryComputeOnGpu(ReadOnlySpan<ulong> exponents, ulong prime, Span<ulong> results)
{
try
@@ -128,12 +198,94 @@ internal static class PrimeOrderGpuHeuristics
private static void ComputePow2ModCpu(ReadOnlySpan<ulong> exponents, ulong prime, Span<ulong> results)
{
for (int i = 0; i < exponents.Length; i++)
int length = exponents.Length;
for (int i = 0; i < length; i++)
{
results[i] = Pow2ModCpu(exponents[i], prime);
}
}
private static bool TryComputeOnGpuWide(ReadOnlySpan<UInt128> exponents, UInt128 prime, Span<UInt128> results)
{
int length = exponents.Length;
if (length == 0)
{
return true;
}
GpuUInt128[]? rentedExponents = null;
GpuUInt128[]? rentedResults = null;
var lease = GpuKernelPool.GetKernel(useGpuOrder: true);
try
{
using var execution = lease.EnterExecutionScope();
Accelerator accelerator = lease.Accelerator;
AcceleratorStream stream = lease.Stream;
var kernel = GetPow2ModWideKernel(accelerator);
using var exponentBuffer = accelerator.Allocate1D<GpuUInt128>(length);
using var remainderBuffer = accelerator.Allocate1D<GpuUInt128>(length);
Span<GpuUInt128> exponentSpan = length <= WideStackThreshold
? stackalloc GpuUInt128[length]
: new Span<GpuUInt128>(rentedExponents = ArrayPool<GpuUInt128>.Shared.Rent(length), 0, length);
for (int i = 0; i < length; i++)
{
exponentSpan[i] = (GpuUInt128)exponents[i];
}
exponentBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(exponentSpan), length);
remainderBuffer.MemSetToZero();
GpuUInt128 modulus = (GpuUInt128)prime;
kernel(stream, length, exponentBuffer.View, modulus, remainderBuffer.View);
stream.Synchronize();
Span<GpuUInt128> resultSpan = length <= WideStackThreshold
? stackalloc GpuUInt128[length]
: new Span<GpuUInt128>(rentedResults = ArrayPool<GpuUInt128>.Shared.Rent(length), 0, length);
remainderBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(resultSpan), length);
for (int i = 0; i < length; i++)
{
results[i] = (UInt128)resultSpan[i];
}
return true;
}
catch (Exception ex) when (ex is AcceleratorException or InternalCompilerException or NotSupportedException or InvalidOperationException or AggregateException)
{
ComputePow2ModCpuWide(exponents, prime, results);
return true;
}
finally
{
if (rentedExponents is not null)
{
ArrayPool<GpuUInt128>.Shared.Return(rentedExponents, clearArray: false);
}
if (rentedResults is not null)
{
ArrayPool<GpuUInt128>.Shared.Return(rentedResults, clearArray: false);
}
lease.Dispose();
}
}
private static void ComputePow2ModCpuWide(ReadOnlySpan<UInt128> exponents, UInt128 prime, Span<UInt128> results)
{
int length = exponents.Length;
for (int i = 0; i < length; i++)
{
results[i] = exponents[i].Pow2MontgomeryModWindowed(prime);
}
}
private static ulong Pow2ModCpu(ulong exponent, ulong modulus)
{
if (modulus <= 1UL)
@@ -161,8 +313,191 @@ internal static class PrimeOrderGpuHeuristics
remainders[index] = exponent.Pow2MontgomeryModWindowed(divisor, keepMontgomery: false);
}
private static Action<AcceleratorStream, Index1D, ArrayView1D<GpuUInt128, Stride1D.Dense>, GpuUInt128, ArrayView1D<GpuUInt128, Stride1D.Dense>> GetPow2ModWideKernel(Accelerator accelerator)
{
return Pow2ModKernelWideCache.GetOrAdd(accelerator, static accel =>
{
var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<GpuUInt128, Stride1D.Dense>, GpuUInt128, ArrayView1D<GpuUInt128, Stride1D.Dense>>(Pow2ModKernelWide);
var kernel = KernelUtil.GetKernel(loaded);
return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ArrayView1D<GpuUInt128, Stride1D.Dense>, GpuUInt128, ArrayView1D<GpuUInt128, Stride1D.Dense>>>();
});
}
private static void Pow2ModKernelWide(Index1D index, ArrayView1D<GpuUInt128, Stride1D.Dense> exponents, GpuUInt128 modulus, ArrayView1D<GpuUInt128, Stride1D.Dense> remainders)
{
GpuUInt128 exponent = exponents[index];
remainders[index] = Pow2ModKernelCore(exponent, modulus);
}
private static GpuUInt128 Pow2ModKernelCore(GpuUInt128 exponent, GpuUInt128 modulus)
{
if (modulus == GpuUInt128.One)
{
return GpuUInt128.Zero;
}
if (exponent.IsZero)
{
return GpuUInt128.One;
}
GpuUInt128 baseValue = new GpuUInt128(2UL);
if (baseValue.CompareTo(modulus) >= 0)
{
baseValue.Sub(modulus);
}
if (ShouldUseSingleBit(exponent))
{
return Pow2MontgomeryModSingleBit(exponent, modulus, baseValue);
}
int bitLength = exponent.GetBitLength();
int windowSize = GetWindowSize(bitLength);
int oddPowerCount = 1 << (windowSize - 1);
Pow2OddPowerTable oddPowers = default;
InitializeOddPowers(ref oddPowers, baseValue, modulus, oddPowerCount);
GpuUInt128 result = GpuUInt128.One;
int index = bitLength - 1;
while (index >= 0)
{
if (!IsBitSet(exponent, index))
{
result.MulMod(result, modulus);
index--;
continue;
}
int windowStart = index - windowSize + 1;
if (windowStart < 0)
{
windowStart = 0;
}
while (!IsBitSet(exponent, windowStart))
{
windowStart++;
}
int windowBitCount = index - windowStart + 1;
for (int square = 0; square < windowBitCount; square++)
{
result.MulMod(result, modulus);
}
ulong windowValue = ExtractWindowValue(exponent, windowStart, windowBitCount);
int tableIndex = (int)((windowValue - 1UL) >> 1);
GpuUInt128 factor = oddPowers[tableIndex];
result.MulMod(factor, modulus);
index = windowStart - 1;
}
return result;
}
private static bool ShouldUseSingleBit(in GpuUInt128 exponent) => exponent.High == 0UL && exponent.Low <= Pow2WindowFallbackThreshold;
private static int GetWindowSize(int bitLength)
{
if (bitLength <= Pow2WindowSizeBits)
{
return Math.Max(bitLength, 1);
}
if (bitLength <= 23)
{
return 4;
}
if (bitLength <= 79)
{
return 5;
}
if (bitLength <= 239)
{
return 6;
}
if (bitLength <= 671)
{
return 7;
}
return Pow2WindowSizeBits;
}
private static void InitializeOddPowers(ref Pow2OddPowerTable oddPowers, GpuUInt128 baseValue, GpuUInt128 modulus, int oddPowerCount)
{
oddPowers[0] = baseValue;
if (oddPowerCount == 1)
{
return;
}
// Reusing baseValue to hold base^2 for the shared odd-power ladder that follows.
baseValue.MulMod(baseValue, modulus);
for (int i = 1; i < oddPowerCount; i++)
{
oddPowers[i] = oddPowers[i - 1];
oddPowers[i].MulMod(baseValue, modulus);
}
}
private static GpuUInt128 Pow2MontgomeryModSingleBit(GpuUInt128 exponent, GpuUInt128 modulus, GpuUInt128 baseValue)
{
GpuUInt128 result = GpuUInt128.One;
while (!exponent.IsZero)
{
if ((exponent.Low & 1UL) != 0UL)
{
result.MulMod(baseValue, modulus);
}
exponent.ShiftRight(1);
if (exponent.IsZero)
{
break;
}
// Reusing baseValue to store the squared base for the next iteration.
baseValue.MulMod(baseValue, modulus);
}
return result;
}
private static bool IsBitSet(in GpuUInt128 value, int bitIndex)
{
if (bitIndex >= 64)
{
return ((value.High >> (bitIndex - 64)) & 1UL) != 0UL;
}
return ((value.Low >> bitIndex) & 1UL) != 0UL;
}
private static ulong ExtractWindowValue(in GpuUInt128 exponent, int windowStart, int windowBitCount)
{
if (windowStart != 0)
{
GpuUInt128 shifted = exponent;
shifted.ShiftRight(windowStart);
ulong mask = (1UL << windowBitCount) - 1UL;
return shifted.Low & mask;
}
ulong directMask = (1UL << windowBitCount) - 1UL;
return exponent.Low & directMask;
}
internal readonly record struct PrimeOrderGpuCapability(int ModulusBits, int ExponentBits)
{
public static PrimeOrderGpuCapability Default => new(128, 64);
public static PrimeOrderGpuCapability Default => new(128, 128);
}
}