Unnecessary memory zeroing commented out. TODOs added for the remainders.

This commit is contained in:
mlemanczyk committed 2025-10-23 19:42:03 +02:00
1 parent 8e3e990499
commit 0d7df2e45b
7 files changed
+246 -208

No files matched your search

@@ -68,7 +68,6 @@ internal static class IncrementalKernels
return;
}
// TODO: Is this expected that the value of kStart is modified?
GpuUInt128 k = kStart + (GpuUInt128)idx;
GpuUInt128 q = twoP;
GpuUInt128.Mul64(ref q, k.High, k.Low);
@@ -125,13 +124,16 @@ internal static class IncrementalKernels
ulong exponent,
GpuUInt128 twoP,
GpuUInt128 kStart,
byte lastIsSeven,
byte _,
ulong divMul,
ResidueAutomatonArgs ra,
ArrayView<int> found,
ArrayView1D<ulong, Stride1D.Dense> smallCycles)
{
{
// TODO: Modify this kernel to always assign the required found element to the result of the calculations and
// remove any clearing of the output buffer in the caller methods.
ulong idx = (ulong)index.X;
// TODO: Replace % operator in GPU and their outgoing paths with logical operations, where possible.
ulong idxMod3 = idx % 3UL;
ulong idxMod5 = idx % 5UL;
ulong r10 = ra.Q0M10 + (ra.Step10 * idx).Mod10(); r10 -= (r10 >= 10UL) ? 10UL : 0UL;
+167 -163
View File
@@ -6,173 +6,177 @@ namespace PerfectNumbers.Core.Gpu;
internal static class Pow2ModKernels
{
public static void Pow2ModKernelScan(
Index1D index,
ulong exponent,
GpuUInt128 twoP,
GpuUInt128 kStart,
byte lastIsSeven,
ulong _,
ResidueAutomatonArgs ra,
ArrayView<ulong> orders,
ArrayView1D<ulong, Stride1D.Dense> smallCycles,
ArrayView1D<uint, Stride1D.Dense> smallPrimesLastOne,
ArrayView1D<uint, Stride1D.Dense> smallPrimesLastSeven,
ArrayView1D<ulong, Stride1D.Dense> smallPrimesPow2LastOne,
ArrayView1D<ulong, Stride1D.Dense> smallPrimesPow2LastSeven)
{
ulong idx = (ulong)index.X;
ulong idxMod3 = idx % 3UL;
ulong idxMod5 = idx % 5UL;
ulong r10 = ra.Q0M10 + (ra.Step10 * idx).Mod10(); r10 -= (r10 >= 10UL) ? 10UL : 0UL;
bool shouldCheck = r10 != 5UL;
if (shouldCheck)
{
ulong r8 = (ra.Q0M8 + ((ra.Step8 * idx) & 7UL)) & 7UL;
if (r8 != 1UL && r8 != 7UL)
{
shouldCheck = false;
}
else
{
ulong r3 = ra.Q0M3 + (ra.Step3 * idxMod3);
if (r3 >= 6UL) r3 -= 6UL;
if (r3 >= 3UL) r3 -= 3UL;
if (r3 == 0UL)
{
shouldCheck = false;
}
else
{
ulong r5 = ra.Q0M5 + (ra.Step5 * idxMod5);
if (r5 >= 15UL) r5 -= 15UL;
if (r5 >= 10UL) r5 -= 10UL;
if (r5 >= 5UL) r5 -= 5UL;
if (r5 == 0UL)
{
shouldCheck = false;
}
}
}
}
if (!shouldCheck)
{
orders[index] = 0UL;
return;
}
/// This kernel always sets the result of the corresponding element. Callers don't need to clear the output buffers.
public static void Pow2ModKernelScan(
Index1D index,
ulong exponent,
GpuUInt128 twoP,
GpuUInt128 kStart,
byte lastIsSeven,
ulong _,
ResidueAutomatonArgs ra,
ArrayView<ulong> orders,
ArrayView1D<ulong, Stride1D.Dense> smallCycles,
ArrayView1D<uint, Stride1D.Dense> smallPrimesLastOne,
ArrayView1D<uint, Stride1D.Dense> smallPrimesLastSeven,
ArrayView1D<ulong, Stride1D.Dense> smallPrimesPow2LastOne,
ArrayView1D<ulong, Stride1D.Dense> smallPrimesPow2LastSeven)
{
ulong idx = (ulong)index.X;
// TODO: Replace % operator in GPU and their outgoing paths with logical operations, where possible.
ulong idxMod3 = idx % 3UL;
ulong idxMod5 = idx % 5UL;
ulong r10 = ra.Q0M10 + (ra.Step10 * idx).Mod10(); r10 -= (r10 >= 10UL) ? 10UL : 0UL;
bool shouldCheck = r10 != 5UL;
// TODO: Replace these "if" statements with ?:
if (shouldCheck)
{
ulong r8 = (ra.Q0M8 + ((ra.Step8 * idx) & 7UL)) & 7UL;
if (r8 != 1UL && r8 != 7UL)
{
shouldCheck = false;
}
else
{
ulong r3 = ra.Q0M3 + (ra.Step3 * idxMod3);
if (r3 >= 6UL) r3 -= 6UL;
if (r3 >= 3UL) r3 -= 3UL;
if (r3 == 0UL)
{
shouldCheck = false;
}
else
{
ulong r5 = ra.Q0M5 + (ra.Step5 * idxMod5);
if (r5 >= 15UL) r5 -= 15UL;
if (r5 >= 10UL) r5 -= 10UL;
if (r5 >= 5UL) r5 -= 5UL;
if (r5 == 0UL)
{
shouldCheck = false;
}
}
}
}
if (!shouldCheck)
{
orders[index] = 0UL;
return;
}
// TODO: kStart is modified after this. Is this expected?
kStart.Add(idx);
GpuUInt128 q = twoP;
GpuUInt128.Mul64(ref q, kStart.High, kStart.Low);
q.Add(GpuUInt128.One);
ReadOnlyGpuUInt128 readOnlyQ = q.AsReadOnly();
if (q.High == 0UL && q.Low < (ulong)smallCycles.Length)
{
ulong cycle = smallCycles[(int)q.Low];
// cycle should always be initialized if we're within array limit in production code
if (cycle != 0UL && cycle <= exponent && (exponent % cycle) != 0UL)
{
orders[index] = 0UL;
return;
}
}
if (GpuUInt128.Pow2Minus1Mod(exponent, in readOnlyQ) != GpuUInt128.Zero)
{
orders[index] = 0UL;
return;
}
kStart.Add(idx);
GpuUInt128 q = twoP;
GpuUInt128.Mul64(ref q, kStart.High, kStart.Low);
q.Add(GpuUInt128.One);
ReadOnlyGpuUInt128 readOnlyQ = q.AsReadOnly();
if (q.High == 0UL && q.Low < (ulong)smallCycles.Length)
{
ulong cycle = smallCycles[(int)q.Low];
// cycle should always be initialized if we're within array limit in production code
if (cycle != 0UL && cycle <= exponent && (exponent % cycle) != 0UL)
{
orders[index] = 0UL;
return;
}
}
if (GpuUInt128.Pow2Minus1Mod(exponent, in readOnlyQ) != GpuUInt128.Zero)
{
orders[index] = 0UL;
return;
}
ArrayView1D<uint, Stride1D.Dense> primes = lastIsSeven != 0 ? smallPrimesLastSeven : smallPrimesLastOne;
ArrayView1D<ulong, Stride1D.Dense> primesPow2 = lastIsSeven != 0 ? smallPrimesPow2LastSeven : smallPrimesPow2LastOne;
int primesLen = (int)primes.Length;
for (int i = 0; i < primesLen; i++)
{
ulong square = primesPow2[i];
if (new GpuUInt128(0UL, square) > q)
{
break;
}
ulong prime = primes[i];
if (KernelMathHelpers.Mod128By64(q, prime) == 0UL)
{
orders[index] = 0UL;
return;
}
}
ArrayView1D<uint, Stride1D.Dense> primes = lastIsSeven != 0 ? smallPrimesLastSeven : smallPrimesLastOne;
ArrayView1D<ulong, Stride1D.Dense> primesPow2 = lastIsSeven != 0 ? smallPrimesPow2LastSeven : smallPrimesPow2LastOne;
int primesLen = (int)primes.Length;
for (int i = 0; i < primesLen; i++)
{
ulong square = primesPow2[i];
if (new GpuUInt128(0UL, square) > q)
{
break;
}
ulong prime = primes[i];
if (KernelMathHelpers.Mod128By64(q, prime) == 0UL)
{
orders[index] = 0UL;
return;
}
}
orders[index] = exponent;
}
orders[index] = exponent;
}
public static void Pow2ModOrderKernelScan(
Index1D index,
ulong exponent,
GpuUInt128 twoP,
GpuUInt128 kStart,
byte lastIsSeven,
ulong _,
ResidueAutomatonArgs ra,
ArrayView<int> found,
ArrayView1D<ulong, Stride1D.Dense> smallCycles)
{
ulong idx = (ulong)index.X;
ulong idxMod3 = idx % 3UL;
ulong idxMod5 = idx % 5UL;
ulong r10 = ra.Q0M10 + (ra.Step10 * idx).Mod10(); r10 -= (r10 >= 10UL) ? 10UL : 0UL;
bool shouldCheck = r10 != 5UL;
if (shouldCheck)
{
ulong r8 = (ra.Q0M8 + ((ra.Step8 * idx) & 7UL)) & 7UL;
if (r8 != 1UL && r8 != 7UL)
{
shouldCheck = false;
}
else
{
ulong r3 = ra.Q0M3 + (ra.Step3 * idxMod3);
if (r3 >= 6UL) r3 -= 6UL;
if (r3 >= 3UL) r3 -= 3UL;
if (r3 == 0UL)
{
shouldCheck = false;
}
else
{
ulong r5 = ra.Q0M5 + (ra.Step5 * idxMod5);
if (r5 >= 15UL) r5 -= 15UL;
if (r5 >= 10UL) r5 -= 10UL;
if (r5 >= 5UL) r5 -= 5UL;
if (r5 == 0UL)
{
shouldCheck = false;
}
}
}
}
if (!shouldCheck)
{
return;
}
/// This kernel doesn't always set the result of the found array. It sets the value only when it needs to. The callers
/// must clean the output buffer before the call to get a deterministic result.
public static void Pow2ModOrderKernelScan(
Index1D index,
ulong exponent,
GpuUInt128 twoP,
GpuUInt128 kStart,
byte _,
ulong __,
ResidueAutomatonArgs ra,
ArrayView<int> found,
ArrayView1D<ulong, Stride1D.Dense> smallCycles)
{
ulong idx = (ulong)index.X;
ulong idxMod3 = idx % 3UL;
ulong idxMod5 = idx % 5UL;
ulong r10 = ra.Q0M10 + (ra.Step10 * idx).Mod10(); r10 -= (r10 >= 10UL) ? 10UL : 0UL;
bool shouldCheck = r10 != 5UL;
if (shouldCheck)
{
ulong r8 = (ra.Q0M8 + ((ra.Step8 * idx) & 7UL)) & 7UL;
if (r8 != 1UL && r8 != 7UL)
{
shouldCheck = false;
}
else
{
ulong r3 = ra.Q0M3 + (ra.Step3 * idxMod3);
if (r3 >= 6UL) r3 -= 6UL;
if (r3 >= 3UL) r3 -= 3UL;
if (r3 == 0UL)
{
shouldCheck = false;
}
else
{
ulong r5 = ra.Q0M5 + (ra.Step5 * idxMod5);
if (r5 >= 15UL) r5 -= 15UL;
if (r5 >= 10UL) r5 -= 10UL;
if (r5 >= 5UL) r5 -= 5UL;
if (r5 == 0UL)
{
shouldCheck = false;
}
}
}
}
if (!shouldCheck)
{
return;
}
GpuUInt128 k = kStart + (GpuUInt128)idx;
GpuUInt128 q = twoP;
GpuUInt128.Mul64(ref q, k.High, k.Low);
q.Add(GpuUInt128.One);
ReadOnlyGpuUInt128 readOnlyQ = q.AsReadOnly();
// Small-cycles in-kernel early rejection from device table
if (q.High == 0UL && q.Low < (ulong)smallCycles.Length)
{
ulong cycle = smallCycles[(int)q.Low];
if (cycle != 0UL && cycle <= exponent && (exponent % cycle) != 0UL)
{
return;
}
}
if (GpuUInt128.Pow2Mod(exponent, in readOnlyQ) != GpuUInt128.One)
{
return;
}
GpuUInt128 k = kStart + (GpuUInt128)idx;
GpuUInt128 q = twoP;
GpuUInt128.Mul64(ref q, k.High, k.Low);
q.Add(GpuUInt128.One);
ReadOnlyGpuUInt128 readOnlyQ = q.AsReadOnly();
// Small-cycles in-kernel early rejection from device table
if (q.High == 0UL && q.Low < (ulong)smallCycles.Length)
{
ulong cycle = smallCycles[(int)q.Low];
if (cycle != 0UL && cycle <= exponent && (exponent % cycle) != 0UL)
{
return;
}
}
if (GpuUInt128.Pow2Mod(exponent, in readOnlyQ) != GpuUInt128.One)
{
return;
}
Atomic.Or(ref found[0], 1);
}
Atomic.Or(ref found[0], 1);
}
}
@@ -10,6 +10,8 @@ namespace PerfectNumbers.Core.Gpu;
internal static partial class PrimeOrderGpuHeuristics
{
/// This kernel always sets the result and operates within the allowed / required bounds, never accessing elements outside of them.
/// It sets the value only when it needs to. The callers don't need to clear the output / input buffers.
internal static void PartialFactorKernel(
Index1D index,
ArrayView1D<uint, Stride1D.Dense> primes,
@@ -111,6 +113,8 @@ internal static partial class PrimeOrderGpuHeuristics
}
}
/// This kernel doesn't always set the result of the first element. It sets the value only when it needs to. The callers
/// must clean the output buffer before the call to get a deterministic result.
internal static void CalculateOrderKernel(
Index1D index,
ulong prime,
@@ -126,7 +130,9 @@ internal static partial class PrimeOrderGpuHeuristics
return;
}
ArrayView1D<ulong, Stride1D.Dense> phiFactors = buffers.PhiFactors;
// TODO: Modify this kernel to always set the output result so that callers never need to clear any buffers and remove
// any existing cleaning of the buffers after that.
ArrayView1D<ulong, Stride1D.Dense> phiFactors = buffers.PhiFactors;
ArrayView1D<int, Stride1D.Dense> phiExponents = buffers.PhiExponents;
ArrayView1D<ulong, Stride1D.Dense> workFactors = buffers.WorkFactors;
ArrayView1D<int, Stride1D.Dense> workExponents = buffers.WorkExponents;
@@ -143,15 +149,18 @@ internal static partial class PrimeOrderGpuHeuristics
int maxPowChecks = config.MaxPowChecks;
int mode = config.Mode;
statusOut[0] = (byte)PrimeOrderKernelStatus.Fallback;
statusOut[0] = (byte)PrimeOrderKernelStatus.Fallback;
if (prime <= 3UL)
{
ulong orderValue = prime == 3UL ? 2UL : 1UL;
resultOut[0] = orderValue;
statusOut[0] = (byte)PrimeOrderKernelStatus.Found;
return;
}
// TODO: Is this condition ever satisfied on EventPerfectBitScanner's execution paths? Comment it out with explanatory
// comment before it, if not. Otherwise add a comment explaining how it's used. If tests or benchmarks require this,
// modify such tests and benchmark to avoid that.
if (prime <= 3UL)
{
ulong orderValue = prime == 3UL ? 2UL : 1UL;
resultOut[0] = orderValue;
statusOut[0] = (byte)PrimeOrderKernelStatus.Found;
return;
}
ulong phi = prime - 1UL;
@@ -1153,11 +1162,13 @@ internal static partial class PrimeOrderGpuHeuristics
return order;
}
internal static void Pow2ModKernel(Index1D index, ArrayView1D<ulong, Stride1D.Dense> exponents, MontgomeryDivisorData divisor, ArrayView1D<ulong, Stride1D.Dense> remainders)
{
ulong exponent = exponents[index];
remainders[index] = ULongExtensions.Pow2MontgomeryModWindowedGpuConvertToStandard(divisor, exponent);
}
/// This kernel always sets the result of the corresponding element. Callers don't need to clear the output buffers.
internal static void Pow2ModKernel(Index1D index, ArrayView1D<ulong, Stride1D.Dense> exponents, MontgomeryDivisorData divisor, ArrayView1D<ulong, Stride1D.Dense> remainders)
{
ulong exponent = exponents[index];
remainders[index] = ULongExtensions.Pow2MontgomeryModWindowedGpuConvertToStandard(divisor, exponent);
}
private static Action<AcceleratorStream, Index1D, ArrayView1D<GpuUInt128, Stride1D.Dense>, GpuUInt128, ArrayView1D<GpuUInt128, Stride1D.Dense>> GetPow2ModWideKernel(Accelerator accelerator)
{
@@ -1169,6 +1180,7 @@ internal static partial class PrimeOrderGpuHeuristics
});
}
/// This kernel always sets the result of the corresponding element. Callers don't need to clear the output buffers.
internal static void Pow2ModKernelWide(Index1D index, ArrayView1D<GpuUInt128, Stride1D.Dense> exponents, GpuUInt128 modulus, ArrayView1D<GpuUInt128, Stride1D.Dense> remainders)
{
GpuUInt128 exponent = exponents[index];
@@ -41,7 +41,8 @@ public sealed class MersenneNumberDivisorGpuTester
var accelerator = gpu.Accelerator;
var kernel = GetKernel(accelerator);
var resultBuffer = _resultBuffers.GetOrAdd(accelerator, acc => acc.Allocate1D<byte>(1));
resultBuffer.MemSetToZero();
// There is no point in clearing this buffer. We always override item [0] and never use it beyond item [0]
// resultBuffer.MemSetToZero();
kernel(1, exponent, divisor, resultBuffer.View);
accelerator.Synchronize();
Span<byte> result = stackalloc byte[1];
@@ -35,6 +35,7 @@ public class MersenneNumberOrderGpuTester(GpuKernelType kernelType, bool useGpuO
{
remaining = maxK - kStart + 1UL;
currentSize = remaining > (UInt128)batchSize ? batchSize : (int)remaining;
// This clear is required because the result may not always be set. Remove it after the kernels are modified to always set the result.
foundBuffer.MemSetToZero();
// Precompute residue automaton bases for this batch
UInt128 q0 = twoP * kStart + 1UL;
@@ -143,8 +143,9 @@ public class MersenneNumberResidueGpuTester(bool useGpuOrder)
while (bag.TryTake(out var resources))
{
if (resources.Capacity >= capacity)
{
resources.OrderBuffer.MemSetToZero();
{
// The caller always overwrites the required elements. We don't need to worry about clearing the output buffer.
// resources.OrderBuffer.MemSetToZero();
return resources;
}
@@ -152,7 +153,8 @@ public class MersenneNumberResidueGpuTester(bool useGpuOrder)
}
var created = new ResidueResources(accelerator, capacity);
created.OrderBuffer.MemSetToZero();
// The caller always overwrites the required elements. We don't need to worry about clearing the output buffer.
// created.OrderBuffer.MemSetToZero();
return created;
}
@@ -199,19 +199,21 @@ internal static partial class PrimeOrderGpuHeuristics
AcceleratorStream stream = lease.Stream;
var kernel = GetPartialFactorKernel(accelerator);
SmallPrimeDeviceCache cache = GetSmallPrimeDeviceCache(accelerator);
SmallPrimeDeviceCache cache = GetSmallPrimeDeviceCache(accelerator);
// TODO: We should create / reallocate these buffer only once or if the new required length is bigger than capacity.
var factorBuffer = accelerator.Allocate1D<ulong>(primeTargets.Length);
var exponentBuffer = accelerator.Allocate1D<int>(exponentTargets.Length);
var countBuffer = accelerator.Allocate1D<int>(1);
var remainingBuffer = accelerator.Allocate1D<ulong>(1);
var fullyFactoredBuffer = accelerator.Allocate1D<byte>(1);
factorBuffer.MemSetToZero();
exponentBuffer.MemSetToZero();
countBuffer.MemSetToZero();
remainingBuffer.MemSetToZero();
fullyFactoredBuffer.MemSetToZero();
// TODO: There is no need to clear these buffers because the kernel will always assign values within the required bounds.
// factorBuffer.MemSetToZero();
// exponentBuffer.MemSetToZero();
// countBuffer.MemSetToZero();
// remainingBuffer.MemSetToZero();
// fullyFactoredBuffer.MemSetToZero();
kernel(
stream,
@@ -240,6 +242,7 @@ internal static partial class PrimeOrderGpuHeuristics
fullyFactoredBuffer.View.CopyToCPU(ref fullyFactoredFlag, 1);
fullyFactored = fullyFactoredFlag != 0;
// TODO: These buffers shouldn't be disposed but rather reused accross call with the assigned accelerator.
factorBuffer.Dispose();
exponentBuffer.Dispose();
countBuffer.Dispose();
@@ -364,8 +367,11 @@ internal static partial class PrimeOrderGpuHeuristics
AcceleratorStream stream = lease.Stream;
var kernel = GetOrderKernel(accelerator);
SmallPrimeDeviceCache cache = GetSmallPrimeDeviceCache(accelerator);
SmallPrimeDeviceCache cache = GetSmallPrimeDeviceCache(accelerator);
// TODO: These buffers should be created reallocated once and assigned to an accelerator. Callers should use their own
// thread static cache to prevent other threads from using taking accelerators with pre-allocated buffers if they don't
// use them.
var phiFactorBuffer = accelerator.Allocate1D<ulong>(GpuSmallPrimeFactorSlots);
var phiExponentBuffer = accelerator.Allocate1D<int>(GpuSmallPrimeFactorSlots);
var workFactorBuffer = accelerator.Allocate1D<ulong>(GpuSmallPrimeFactorSlots);
@@ -377,7 +383,8 @@ internal static partial class PrimeOrderGpuHeuristics
var resultBuffer = accelerator.Allocate1D<ulong>(1);
var statusBuffer = accelerator.Allocate1D<byte>(1);
phiFactorBuffer.MemSetToZero();
// TODO: Remove the cleaning after the order kernel is modified to always set the result.
phiFactorBuffer.MemSetToZero();
phiExponentBuffer.MemSetToZero();
workFactorBuffer.MemSetToZero();
workExponentBuffer.MemSetToZero();
@@ -523,26 +530,31 @@ internal static partial class PrimeOrderGpuHeuristics
Accelerator accelerator = lease.Accelerator;
AcceleratorStream stream = lease.Stream;
var kernel = GetPow2ModKernel(accelerator);
// TODO: These buffers should be allocated once per accelerator and only reallocated when the new length exceeds capacity.
// Modify the callers to use their own pool of buffers per accelerator, so that other threads don't use the accelerators
// with out preallocated buffers. Share the pool with Pow2ModWide kernel.
var exponentBuffer = accelerator.Allocate1D<ulong>(exponents.Length);
var remainderBuffer = accelerator.Allocate1D<ulong>(exponents.Length);
exponentBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(exponents), exponents.Length);
remainderBuffer.MemSetToZero();
try
{
kernel(stream, exponents.Length, exponentBuffer.View, divisorData, remainderBuffer.View);
stream.Synchronize();
}
catch (Exception)
{
Console.WriteLine($"Exception for {prime} and exponents: {string.Join(",", exponents.ToArray())}.");
exponentBuffer.Dispose();
remainderBuffer.Dispose();
execution.Dispose();
lease.Dispose();
throw;
}
// Pow2Mod kernel always assigns the required output elements so we don't need to worry about clearing these.
// remainderBuffer.MemSetToZero();
try
{
kernel(stream, exponents.Length, exponentBuffer.View, divisorData, remainderBuffer.View);
stream.Synchronize();
}
catch (Exception)
{
Console.WriteLine($"Exception for {prime} and exponents: {string.Join(",", exponents.ToArray())}.");
exponentBuffer.Dispose();
remainderBuffer.Dispose();
execution.Dispose();
lease.Dispose();
throw;
}
remainderBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(results), exponents.Length);
exponentBuffer.Dispose();
@@ -579,6 +591,9 @@ internal static partial class PrimeOrderGpuHeuristics
Accelerator accelerator = lease.Accelerator;
AcceleratorStream stream = lease.Stream;
var kernel = GetPow2ModWideKernel(accelerator);
// TODO: These buffers should be allocated once per accelerator and only reallocated when the new length exceeds capacity.
// Modify the callers to use their own pool of buffers per accelerator, so that other threads don't use the accelerators
// with out preallocated buffers. Share the pool with Pow2Mod kernel.
var exponentBuffer = accelerator.Allocate1D<GpuUInt128>(length);
var remainderBuffer = accelerator.Allocate1D<GpuUInt128>(length);
@@ -592,7 +607,8 @@ internal static partial class PrimeOrderGpuHeuristics
}
exponentBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(exponentSpan), length);
remainderBuffer.MemSetToZero();
// Pow2ModWide kernel always assigns the required output elements so we don't need to worry about clearing these.
// remainderBuffer.MemSetToZero();
GpuUInt128 modulus = (GpuUInt128)prime;
kernel(stream, length, exponentBuffer.View, modulus, remainderBuffer.View);