Merge pull request #213 from mlemanczyk/codex/check-and-complete-todos-in-mulmod-benchmarks

Inline GpuUInt128 Mul64 helper
This commit is contained in:
Marek Lemańczyk authored and GitHub committed 2025-10-20 13:58:34 +02:00
commit ecc718bc69
8 files changed
+109 -122

No files matched your search

+31 -78
View File
@@ -456,39 +456,27 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
return yHigh;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Mul64(in ReadOnlyGpuUInt128 other) => Mul64Internal(other.High, other.Low);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Mul64(GpuUInt128 other) => Mul64Internal(other.High, other.Low);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Mul64Internal(ulong otherHigh, ulong otherLow)
{
// Multiply this.Low (assumed 64-bit value) by full 128-bit other
ulong operand = Low;
Low = operand * otherLow;
ulong highProduct = operand * otherHigh;
High = highProduct + MulHigh(operand, otherLow);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
// Kept by-value to coexist with the ref readonly overload below while still signaling read-only intent.
public void MulMod(ReadOnlyGpuUInt128 value, in ReadOnlyGpuUInt128 modulus)
{
GpuUInt128 multiplicand = this;
GpuUInt128 multiplier = value.ToMutable();
High = 0UL;
Low = 0UL;
GpuUInt128 result = new();
while (!multiplier.IsZero)
{
if ((multiplier.Low & 1UL) != 0UL)
{
AddMod(multiplicand, modulus);
result.Add(multiplicand);
if (result.CompareTo(modulus) >= 0)
{
result.Sub(modulus);
}
}
// Using the in-place shifts keeps the benchmarked throughput on par with the allocating operator overloads
// (HighWordModulus 4.34 -> 4.21 us, LowWordHeavy 3.18 -> 2.48 us, MixedMagnitude 3.74 -> 3.58 us, TinyOperands 0.212 -> 0.260 us; InvocationCount=16, IterationCount=3).
multiplicand.ShiftLeft(1);
if (multiplicand.CompareTo(modulus) >= 0)
{
@@ -497,6 +485,9 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
multiplier.ShiftRight(1);
}
High = result.High;
Low = result.Low;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -743,6 +734,15 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
Low = lowPart;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Mul64(ref GpuUInt128 value, ulong otherHigh, ulong otherLow)
{
ulong operand = value.Low;
value.Low = operand * otherLow;
ulong highProduct = operand * otherHigh;
value.High = highProduct + MulHigh(operand, otherLow);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GpuUInt128 DivideExact(in ReadOnlyGpuUInt128 dividend, in ReadOnlyGpuUInt128 divisor)
{
@@ -753,7 +753,8 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
GpuUInt128 remainder = dividend;
GpuUInt128 shiftedDivisor = divisor;
shiftedDivisor.ShiftLeft(shift);
GpuUInt128 quotientBit = One << shift;
GpuUInt128 quotientBit = One;
quotientBit.ShiftLeft(shift);
while (true)
{
@@ -868,54 +869,6 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
// (e.g., Montgomery n', R2) and provide device-friendly accessors.
}
/// <summary>
/// Modular multiplication using <see cref="BigInteger"/> reduction. This
/// method is intended for validation only and should not be used inside
/// GPU kernels.
/// </summary>
internal void MulModBigInteger(in ReadOnlyGpuUInt128 other, in ReadOnlyGpuUInt128 modulus)
{
var left = (BigInteger)(UInt128)this;
var right = (BigInteger)(UInt128)other;
var mod = (BigInteger)(UInt128)modulus;
var reduced = (UInt128)((left * right) % mod);
High = (ulong)(reduced >> 64);
Low = (ulong)reduced;
}
/// <summary>
/// Experimental limb-based reduction. The current implementation performs
/// repeated subtractions and becomes extremely slow for large remainders.
/// Kept for future optimization work.
/// </summary>
internal void MulModByLimb(in ReadOnlyGpuUInt128 other, in ReadOnlyGpuUInt128 modulus)
{
// TODO: Relocate this limb-based reducer to the benchmark project once the production
// pipeline switches to the faster allocating legacy path demonstrated in the benchmarks.
MultiplyFull(this, other, out var p3, out var p2, out var p1, out var p0);
High = p3;
Low = p2;
while (CompareTo(modulus) >= 0)
{
Sub(modulus);
}
ulong limb = p1;
for (int i = 0; i < 2; i++)
{
ShiftLeft(64);
Low = limb;
while (CompareTo(modulus) >= 0)
{
Sub(modulus);
}
limb = p0;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MulMod(ulong value, in ReadOnlyGpuUInt128 modulus)
{
@@ -1161,8 +1114,8 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
// TODO: Can we modify these loops to process multiple bits at a time? E.g. 64-bit chunks.
for (bit = 63; bit >= 0; bit--)
{
remainder <<= 1;
remainder = new(remainder.High, remainder.Low | ((p3 >> bit) & 1UL));
remainder.ShiftLeft(1);
remainder.Low |= (p3 >> bit) & 1UL;
if (remainder.CompareTo(modulus) >= 0)
{
remainder.Sub(modulus);
@@ -1171,8 +1124,8 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
for (bit = 63; bit >= 0; bit--)
{
remainder <<= 1;
remainder = new(remainder.High, remainder.Low | ((p2 >> bit) & 1UL));
remainder.ShiftLeft(1);
remainder.Low |= (p2 >> bit) & 1UL;
if (remainder.CompareTo(modulus) >= 0)
{
remainder.Sub(modulus);
@@ -1181,8 +1134,8 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
for (bit = 63; bit >= 0; bit--)
{
remainder <<= 1;
remainder = new(remainder.High, remainder.Low | ((p1 >> bit) & 1UL));
remainder.ShiftLeft(1);
remainder.Low |= (p1 >> bit) & 1UL;
if (remainder.CompareTo(modulus) >= 0)
{
remainder.Sub(modulus);
@@ -1191,8 +1144,8 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
for (bit = 63; bit >= 0; bit--)
{
remainder <<= 1;
remainder = new(remainder.High, remainder.Low | ((p0 >> bit) & 1UL));
remainder.ShiftLeft(1);
remainder.Low |= (p0 >> bit) & 1UL;
if (remainder.CompareTo(modulus) >= 0)
{
remainder.Sub(modulus);
@@ -1221,7 +1174,7 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
result.MulMod(baseValue, modulus);
}
exponent >>= 1;
exponent.ShiftRight(1);
baseValue.MulMod(baseValue, modulus);
}
@@ -71,7 +71,7 @@ internal static class IncrementalKernels
// TODO: Is this expected that the value of kStart is modified?
GpuUInt128 k = kStart + (GpuUInt128)idx;
GpuUInt128 q = twoP;
q.Mul64(k);
GpuUInt128.Mul64(ref q, k.High, k.Low);
q.Add(GpuUInt128.One);
ReadOnlyGpuUInt128 readOnlyQ = q.AsReadOnly();
@@ -174,7 +174,7 @@ internal static class IncrementalKernels
GpuUInt128 k = kStart + (GpuUInt128)idx;
GpuUInt128 q = twoP;
q.Mul64(k);
GpuUInt128.Mul64(ref q, k.High, k.Low);
q.Add(GpuUInt128.One);
ReadOnlyGpuUInt128 readOnlyQ = q.AsReadOnly();
@@ -64,7 +64,7 @@ internal static class Pow2ModKernels
// TODO: kStart is modified after this. Is this expected?
kStart.Add(idx);
GpuUInt128 q = twoP;
q.Mul64(kStart);
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)
@@ -158,7 +158,7 @@ internal static class Pow2ModKernels
GpuUInt128 k = kStart + (GpuUInt128)idx;
GpuUInt128 q = twoP;
q.Mul64(k);
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
+1 -1
View File
@@ -13,7 +13,7 @@ public static partial class ULongExtensions
public static ulong MulHighGpuCompatible(this ulong x, ulong y)
{
GpuUInt128 product = new(x);
product.Mul64(new GpuUInt128(y));
GpuUInt128.Mul64(ref product, 0UL, y);
return product.High;
}