From 84ebbed98b66c6c35e351400bdd880c0fbb286fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sat, 4 Oct 2025 20:01:10 +0200 Subject: [PATCH 1/8] Add shift-first multiply helper and document benchmarks --- AGENTS.md | 48 ++ .../HeuristicMultiplyShiftBenchmarks.cs | 53 ++ .../Gpu/PrimeOrderGpuHeuristicsTests.cs | 110 ++++ .../HeuristicArithmeticTests.cs | 67 ++ .../Gpu/PrimeOrderGpuHeuristics.cs | 67 ++ PerfectNumbers.Core/HeuristicArithmetic.cs | 48 ++ PerfectNumbers.Core/MersenneDivisorCycles.cs | 10 + .../PerfectNumbers.Core.csproj | 3 +- .../PrimeOrderCalculator.Wide.cs | 13 +- PerfectNumbers.Core/PrimeOrderCalculator.cs | 122 +++- apply_patch.cs | 623 +++++++++++++++--- todo.txt | 3 + tools/GpuNumericTypeProbe.csx | 160 +++++ .../HeuristicMultiplyShiftTiming.csproj | 11 + tools/HeuristicMultiplyShiftTiming/Program.cs | 95 +++ 15 files changed, 1333 insertions(+), 100 deletions(-) create mode 100644 EvenPerfectBitScanner.Benchmarks/HeuristicMultiplyShiftBenchmarks.cs create mode 100644 PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs create mode 100644 PerfectNumbers.Core.Tests/HeuristicArithmeticTests.cs create mode 100644 PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs create mode 100644 PerfectNumbers.Core/HeuristicArithmetic.cs create mode 100644 todo.txt create mode 100644 tools/GpuNumericTypeProbe.csx create mode 100644 tools/HeuristicMultiplyShiftTiming/HeuristicMultiplyShiftTiming.csproj create mode 100644 tools/HeuristicMultiplyShiftTiming/Program.cs diff --git a/AGENTS.md b/AGENTS.md index dd4f115..5a50a63 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,14 +31,23 @@ It supports the standard `git diff` patch files as input. You should be able to * `--ignore-eol` * `--ignore-line-endings` * `--target` - allows specifying target root directory for the patch +* `--profile` - capture read/parse/apply timings for each patch file. +* `--replace-line ` - replace a single line by absolute number (use `_` as the expected text to skip validation) * Build patches in standard `git diff` format. * It supports `---`, `+++`, and `@@`. * It doesn't support `***` or `*** Begin/End Patch` wrappers. * It requires `diff --git` at the beginning. * Patch header needs the actual line numbers. + * When hand-writing a diff, always include the numeric ranges in every `@@ -start,count +start,count @@` header; missing or zero counts will cause `--check` to fail immediately. * Always check if the patch will work by using `--check` parameter, without making changes to the files, which would force patch changes. Try applying the patch only when `--check` passed. * Build patches in small, line-precise hunks; confirm target lines with `Get-Content` or `nl -ba` before writing the diff to avoid context mismatches. +* Capture the exact start lines for each hunk while drafting the `@@ -start,length +start,length @@` header; double-check them with `nl -ba` so the first verification succeeds. +* For one-off line edits, prefer the `--replace-line ` option. Pass `_` as the expected text to skip validation when necessary. +* Before calling `--replace-line`, capture the target number with `nl -ba` (or an equivalent command) so the replacement lands exactly once without retrying. +* After every successful patch application, regenerate a fresh diff from the updated files before preparing the next patch so the hunk headers and context reflect the latest line numbers. +* If the patch script flags repeated mismatches, regenerate a fresh diff for the updated file instead of tweaking stale context blocks. +* When `--check` reveals that only whitespace or EOL normalization differs, rerun it with `--ignore-whitespace` and/or `--ignore-line-endings` so validation concentrates on the meaningful edits. * Regenerate the diff whenever the file shifts—never reuse an old hunk after other edits. * Keep patch filenames unique and delete applied files to prevent accidental re-use. * When a hunk fails, inspect the around-lines immediately and adjust rather than retrying the same diff. @@ -127,7 +136,46 @@ The following checks were executed with ILGPU 1.5.3 on CPU and OpenCL accelerato - [x] `UInt128` bit shifts: attempts failed in kernels; avoid inside device code. - [x] GPU context pooling: supported and recommended (validated in a separate program). +Additional numeric-type probe results (`tools/GpuNumericTypeProbe.csx`, ILGPU 1.5.3 on the CPU accelerator): + +- `System.Numerics.BigInteger`: unsupported. Kernels that exercise addition, subtraction, multiplication, division, modulo, and `CompareTo` all fail with `InternalCompilerException`, indicating fundamental codegen gaps for the type. +- `PeterO.Numbers.EInteger`: unsupported. Attempts to compile kernels that call addition, subtraction, multiplication, division, remainder, and `CompareTo` throw `InternalCompilerException`, so none of the arithmetic helpers are currently usable on the device. +- `PeterO.Numbers.ERational`: unsupported. Kernels that call addition, subtraction, multiplication, division, and `CompareTo` trigger `InternalCompilerException`; the type also lacks a remainder/modulo API for device usage. + +Patch-preparation checklist for reliable `--check` runs: + +- Always capture context with `nl -ba` before composing each hunk so the `@@` header line numbers match the target file on the first try. +- Trim or normalize trailing whitespace while drafting the patch, or rely on `--ignore-whitespace` during verification when formatting noise is unavoidable, then re-run without ignores to confirm the final diff matches exactly. +- Prefer regenerating the patch from scratch whenever the working tree changes instead of editing previous diffs; stale context is the most common source of mismatches. + +- The patch parser now ignores blank metadata lines between `diff --git`, `---`, and `+++` headers, so you no longer need to insert dummy empty separators to satisfy the checker. Keep the header compact unless your diff tool emits those blank lines naturally. + +### apply_patch.cs profiling snapshot (2025-10-04) + +- Latest timing check: `dotnet script apply_patch.cs -- --profile --check /tmp/agents_todo_update.diff`. +- Patch characteristics: 2 files, 4 hunks, 31 lines, 3468 bytes. +- Timings: read 0.45 ms, parse 2.96 ms, apply 2.96 ms. +- Focus next on streaming the patch reader and pooled buffer reuse to reduce the parse APPLY bottleneck highlighted above. + +Mersenne divisor size estimates: + +- Any prime divisor `q` of a Mersenne number `2^p - 1` must satisfy `q ≡ 1 (mod 2p)`. The minimal `q` therefore appears at `q = 2p + 1` when the multiplier `k = 1` yields a prime. +- For candidate exponents `p` in the `[138,000,000, 200,000,000]` range, the minimal compliant divisor lands below `2 · 200,000,000 + 1 = 400,000,001`, which fits in 29 bits (`2^29 = 536,870,912`). +- Extending the search up to `p = 600,000,000` pushes the minimal divisor to `1,200,000,001`, requiring 31 bits (`2^31 = 2,147,483,648`). +- Larger `k` values (and thus larger `q`) are possible—up to nearly `2^p - 1`—so the heuristics and storage should plan for wider integers when deep trial factoring is necessary, but the noted bit widths cover the first admissible divisors for these exponent bands. + +- Because every proper divisor of `2^p - 1` is at most `(2^p - 1) / 2`, the worst-case bit-width for any admissible `q` is `p - 1` bits (since `⌊(2^p - 1) / 2⌋ = 2^{p-1} - 1`). Consequently we need roughly `199,999,999` bits when `p ≤ 200,000,000` and `599,999,999` bits when `p ≤ 600,000,000`. In terms of the `q = 2kp + 1` form, this translates to `k_max = ⌊((2^p - 1) / 2 - 1) / (2p)⌋`, which is approximately `2^{p-2} / p` for large `p`. + Practical guidance: - Use native `%` and `/` in kernels when working with 32-bit or 64-bit integers if it is the most efficient option for the target backend. - For `UInt128` in kernels, avoid `%`/`/`; implement reductions via Montgomery/Barrett, or use Mersenne-specific folding for mod `2^p-1`. - Prefer Mersenne folding over general modulo for `2^p-1` moduli to remove branches in hot loops. + +- Heuristic multiply-shift benchmarks (2025-10-04): + - Run `dotnet run -c Release --project tools/HeuristicMultiplyShiftTiming/HeuristicMultiplyShiftTiming.csproj` to compare the `HeuristicArithmetic` helpers without the BenchmarkDotNet harness. + - Timings below report the average nanoseconds per call over 200,000,000 iterations; all variants produced identical checksums, confirming consistent arithmetic. + - `NearOverflowShift`: `MultiplyShiftRight` ≈ 2.37 ns, shift-first ≈ 4.71 ns, naive `(value * multiplier) >> shift` ≈ 0.85 ns. + - `HalfRange`: `MultiplyShiftRight` ≈ 2.11 ns, shift-first ≈ 4.15 ns, naive ≈ 0.70 ns. + - `MixedBits`: `MultiplyShiftRight` ≈ 1.43 ns, shift-first ≈ 2.83 ns, naive ≈ 0.60 ns. + - `Sparse`: `MultiplyShiftRight` ≈ 1.57 ns, shift-first ≈ 2.84 ns, naive ≈ 0.59 ns. + - The shift-first rewrite roughly doubles the per-call cost on CPU, but it stays within 64-bit arithmetic and is the preferred fallback for GPU heuristics when avoiding `UInt128` overflow is mandatory. diff --git a/EvenPerfectBitScanner.Benchmarks/HeuristicMultiplyShiftBenchmarks.cs b/EvenPerfectBitScanner.Benchmarks/HeuristicMultiplyShiftBenchmarks.cs new file mode 100644 index 0000000..7bc796a --- /dev/null +++ b/EvenPerfectBitScanner.Benchmarks/HeuristicMultiplyShiftBenchmarks.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using PerfectNumbers.Core; + +namespace EvenPerfectBitScanner.Benchmarks; + +[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)] +[MemoryDiagnoser] +public class HeuristicMultiplyShiftBenchmarks +{ + private static readonly MultiplyShiftInput[] Inputs = + [ + new(0x3FFF_FFFF_FFFF_FFFFUL, 3UL, 3, "NearOverflowShift"), + new(0x2000_0000_0000_0000UL, 7UL, 4, "HalfRange"), + new(0x1234_5678_9ABC_DEF0UL, 5UL, 5, "MixedBits"), + new(0x0000_0000_1000_0000UL, 11UL, 3, "Sparse"), + ]; + + [ParamsSource(nameof(GetInputs))] + public MultiplyShiftInput Input { get; set; } + + public static IEnumerable GetInputs() + { + return Inputs; + } + + [Benchmark(Baseline = true)] + public ulong SafeMultiplyShift() + { + return HeuristicArithmetic.MultiplyShiftRight(Input.Value, Input.Multiplier, Input.Shift); + } + + [Benchmark] + public ulong ShiftFirstMultiplyShift() + { + return HeuristicArithmetic.MultiplyShiftRightShiftFirst(Input.Value, Input.Multiplier, Input.Shift); + } + + [Benchmark] + public ulong NaiveMultiplyShift() + { + return (Input.Value * Input.Multiplier) >> Input.Shift; + } + + public readonly record struct MultiplyShiftInput(ulong Value, ulong Multiplier, int Shift, string Name) + { + public override string ToString() + { + return Name; + } + } +} diff --git a/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs b/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs new file mode 100644 index 0000000..9914ad6 --- /dev/null +++ b/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs @@ -0,0 +1,110 @@ +using System; +using FluentAssertions; +using PerfectNumbers.Core; +using PerfectNumbers.Core.Gpu; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +[Trait("Category", "Fast")] +public class PrimeOrderGpuHeuristicsTests +{ + [Fact] + public void TryPow2Mod_returns_overflow_for_marked_prime() + { + const ulong prime = 97UL; + PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); + try + { + PrimeOrderGpuHeuristics.MarkOverflow(prime); + + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(1UL, prime, out ulong remainder); + + status.Should().Be(GpuPow2ModStatus.Overflow); + remainder.Should().Be(0UL); + } + finally + { + PrimeOrderGpuHeuristics.ClearOverflow(prime); + } + } + + [Fact] + public void TryPow2Mod_returns_unavailable_for_unmarked_prime() + { + PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); + + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(1UL, 193UL, out ulong remainder); + + status.Should().Be(GpuPow2ModStatus.Unavailable); + remainder.Should().Be(0UL); + } + + [Fact] + public void TryPow2ModBatch_returns_overflow_for_marked_prime() + { + const ulong prime = 193UL; + PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); + try + { + PrimeOrderGpuHeuristics.MarkOverflow(prime); + + ulong[] remainders = new ulong[2]; + ulong[] exponents = { 1UL, 2UL }; + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders); + + status.Should().Be(GpuPow2ModStatus.Overflow); + remainders[0].Should().Be(0UL); + remainders[1].Should().Be(0UL); + } + finally + { + PrimeOrderGpuHeuristics.ClearOverflow(prime); + } + } + + [Fact] + public void TryPow2ModBatch_returns_unavailable_and_clears_remainders_for_unmarked_prime() + { + PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); + + ulong[] remainders = { 123UL, 456UL }; + ulong[] exponents = { 1UL, 2UL }; + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, 257UL, remainders); + + status.Should().Be(GpuPow2ModStatus.Unavailable); + remainders[0].Should().Be(0UL); + remainders[1].Should().Be(0UL); + } + + [Fact] + public void TryPow2ModBatch_throws_when_remainder_span_is_too_small() + { + ulong[] remainders = new ulong[1]; + ulong[] exponents = { 1UL, 2UL }; + + Action act = () => PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, 5UL, remainders); + + act.Should().Throw(); + } + + [Fact] + public void Calculate_falls_back_to_cpu_when_gpu_overflow_is_marked() + { + const ulong prime = 7UL; + PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); + try + { + PrimeOrderGpuHeuristics.MarkOverflow(prime); + + PrimeOrderCalculator.PrimeOrderResult result = PrimeOrderCalculator.Calculate(prime, null, PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); + + result.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.Found); + result.Order.Should().Be(3UL); + } + finally + { + PrimeOrderGpuHeuristics.ClearOverflow(prime); + } + } +} diff --git a/PerfectNumbers.Core.Tests/HeuristicArithmeticTests.cs b/PerfectNumbers.Core.Tests/HeuristicArithmeticTests.cs new file mode 100644 index 0000000..a3cd60a --- /dev/null +++ b/PerfectNumbers.Core.Tests/HeuristicArithmeticTests.cs @@ -0,0 +1,67 @@ +using System.Numerics; +using FluentAssertions; +using PerfectNumbers.Core; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +[Trait("Category", "Fast")] +public class HeuristicArithmeticTests +{ + [Theory] + [InlineData(0UL, 0UL, 1)] + [InlineData(1UL, 3UL, 1)] + [InlineData(ulong.MaxValue, 3UL, 3)] + [InlineData(0xFFFF_FFFF_FFFF_FFFFUL, 5UL, 4)] + [InlineData(0x8000_0000_0000_0000UL, 7UL, 5)] + public void MultiplyShiftRight_matches_big_integer(ulong value, ulong multiplier, int shift) + { + BigInteger expected = ((BigInteger)value * multiplier) >> shift; + + ulong result = HeuristicArithmetic.MultiplyShiftRight(value, multiplier, shift); + + result.Should().Be((ulong)expected); + } + + [Theory] + [InlineData(0UL, 0UL, 1)] + [InlineData(1UL, 3UL, 1)] + [InlineData(ulong.MaxValue, 3UL, 3)] + [InlineData(0xFFFF_FFFF_FFFF_FFFFUL, 5UL, 4)] + [InlineData(0x8000_0000_0000_0000UL, 7UL, 5)] + [InlineData(0xDEAD_BEEF_DEAD_BEEFUL, 11UL, 63)] + public void MultiplyShiftRightShiftFirst_matches_big_integer(ulong value, ulong multiplier, int shift) + { + BigInteger expected = ((BigInteger)value * multiplier) >> shift; + + ulong result = HeuristicArithmetic.MultiplyShiftRightShiftFirst(value, multiplier, shift); + + result.Should().Be((ulong)expected); + } + + [Fact] + public void MultiplyShiftRightShiftFirst_returns_zero_when_shift_is_large() + { + HeuristicArithmetic.MultiplyShiftRightShiftFirst(ulong.MaxValue, 123UL, 64).Should().Be(0UL); + } + + [Theory] + [InlineData(0UL, ulong.MaxValue, -1)] + [InlineData(1UL, 1UL, -3)] + public void MultiplyShiftRight_throws_when_shift_is_negative(ulong value, ulong multiplier, int shift) + { + Action act = () => HeuristicArithmetic.MultiplyShiftRight(value, multiplier, shift); + + act.Should().Throw(); + } + + [Theory] + [InlineData(0UL, ulong.MaxValue, -1)] + [InlineData(1UL, 1UL, -3)] + public void MultiplyShiftRightShiftFirst_throws_when_shift_is_negative(ulong value, ulong multiplier, int shift) + { + Action act = () => HeuristicArithmetic.MultiplyShiftRightShiftFirst(value, multiplier, shift); + + act.Should().Throw(); + } +} diff --git a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs new file mode 100644 index 0000000..ee07fc3 --- /dev/null +++ b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Concurrent; + +namespace PerfectNumbers.Core.Gpu; + +internal enum GpuPow2ModStatus +{ + Success, + Overflow, + Unavailable, +} + +internal static class PrimeOrderGpuHeuristics +{ + private static readonly ConcurrentDictionary OverflowedPrimes = new(); + + internal static void MarkOverflow(ulong prime) => OverflowedPrimes[prime] = 0; + + internal static void ClearOverflow(ulong prime) => OverflowedPrimes.TryRemove(prime, out _); + + internal static void ClearAllOverflowForTesting() => OverflowedPrimes.Clear(); + + public static GpuPow2ModStatus TryPow2Mod(ulong exponent, ulong prime, out ulong remainder) + { + remainder = 0UL; + if (prime <= 1UL) + { + return GpuPow2ModStatus.Unavailable; + } + + if (OverflowedPrimes.ContainsKey(prime)) + { + return GpuPow2ModStatus.Overflow; + } + + // TODO: Implement the GPU heuristic order calculator so this path mirrors PrimeOrderCalculator before enabling device execution. + return GpuPow2ModStatus.Unavailable; + } + + public static GpuPow2ModStatus TryPow2ModBatch(ReadOnlySpan exponents, ulong prime, Span 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)); + } + + remainders.Slice(0, exponents.Length).Clear(); + + if (prime <= 1UL) + { + return GpuPow2ModStatus.Unavailable; + } + + if (OverflowedPrimes.ContainsKey(prime)) + { + return GpuPow2ModStatus.Overflow; + } + + // TODO: Implement the GPU heuristic order calculator so this path mirrors PrimeOrderCalculator before enabling device execution. + return GpuPow2ModStatus.Unavailable; + } +} diff --git a/PerfectNumbers.Core/HeuristicArithmetic.cs b/PerfectNumbers.Core/HeuristicArithmetic.cs new file mode 100644 index 0000000..7e62121 --- /dev/null +++ b/PerfectNumbers.Core/HeuristicArithmetic.cs @@ -0,0 +1,48 @@ +using System; +using System.Runtime.CompilerServices; + +namespace PerfectNumbers.Core; + +internal static class HeuristicArithmetic +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong MultiplyShiftRight(ulong value, ulong multiplier, int shift) + { + if (shift < 0) + { + throw new ArgumentOutOfRangeException(nameof(shift), "Shift must be non-negative."); + } + + UInt128 product = (UInt128)value * multiplier; + return (ulong)(product >> shift); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong MultiplyShiftRightShiftFirst(ulong value, ulong multiplier, int shift) + { + if (shift < 0) + { + throw new ArgumentOutOfRangeException(nameof(shift), "Shift must be non-negative."); + } + + if (shift == 0) + { + return MultiplyShiftRight(value, multiplier, 0); + } + + if (shift >= 64) + { + return 0UL; + } + + ulong high = value >> shift; + ulong mask = (1UL << shift) - 1UL; + ulong low = value & mask; + + UInt128 highContribution = (UInt128)high * multiplier; + UInt128 lowContribution = (UInt128)low * multiplier; + + UInt128 combined = highContribution + (lowContribution >> shift); + return (ulong)combined; + } +} diff --git a/PerfectNumbers.Core/MersenneDivisorCycles.cs b/PerfectNumbers.Core/MersenneDivisorCycles.cs index 4776322..1de140d 100644 --- a/PerfectNumbers.Core/MersenneDivisorCycles.cs +++ b/PerfectNumbers.Core/MersenneDivisorCycles.cs @@ -189,6 +189,16 @@ public class MersenneDivisorCycles return true; } + PrimeOrderCalculator.PrimeOrderResult orderResult = PrimeOrderCalculator.Calculate( + divisor, + previousOrder: null, + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); + if (orderResult.Order != 0UL) + { + cycleLength = orderResult.Order; + return true; + } + ulong phi = divisor - 1UL; if (phi == 0UL) { diff --git a/PerfectNumbers.Core/PerfectNumbers.Core.csproj b/PerfectNumbers.Core/PerfectNumbers.Core.csproj index fa27778..f38ec24 100644 --- a/PerfectNumbers.Core/PerfectNumbers.Core.csproj +++ b/PerfectNumbers.Core/PerfectNumbers.Core.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -17,6 +17,7 @@ + diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.Wide.cs b/PerfectNumbers.Core/PrimeOrderCalculator.Wide.cs index ed0b084..8951b4d 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.Wide.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.Wide.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Numerics; using System.Threading; +using PerfectNumbers.Core.Gpu; namespace PerfectNumbers.Core; @@ -954,7 +955,17 @@ internal static partial class PrimeOrderCalculator { return UInt128.Zero; } - + if (modulus <= (UInt128)ulong.MaxValue && exponent <= (UInt128)ulong.MaxValue) + { + ulong prime64 = (ulong)modulus; + ulong exponent64 = (ulong)exponent; + // TODO: Extend PrimeOrderGpuHeuristics to handle 128-bit moduli so this fast path can remain on the device. + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(exponent64, prime64, out ulong remainder); + if (status == GpuPow2ModStatus.Success) + { + return remainder; + } + } BigInteger result = BigInteger.ModPow(2, (BigInteger)exponent, (BigInteger)modulus); return (UInt128)result; } diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.cs b/PerfectNumbers.Core/PrimeOrderCalculator.cs index 0decc77..93d91e0 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.cs @@ -4,7 +4,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Security.Cryptography; +using PerfectNumbers.Core.Gpu; using System.Threading; namespace PerfectNumbers.Core; @@ -134,7 +136,7 @@ internal static partial class PrimeOrderCalculator { ulong factor = factorSpan[i].Value; ulong reduced = phi / factor; - if (reduced.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL) + if (Pow2EqualsOne(reduced, prime, divisorData)) { return false; } @@ -149,7 +151,7 @@ internal static partial class PrimeOrderCalculator if ((prime & 7UL) == 1UL || (prime & 7UL) == 7UL) { ulong half = phi >> 1; - if (half.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL) + if (Pow2EqualsOne(half, prime, divisorData)) { order = half; } @@ -191,7 +193,7 @@ internal static partial class PrimeOrderCalculator } ulong reduced = order / primeFactor; - if (reduced.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL) + if (Pow2EqualsOne(reduced, prime, divisorData)) { order = reduced; continue; @@ -223,7 +225,7 @@ internal static partial class PrimeOrderCalculator #if DEBUG Console.WriteLine("Verifying a^order ≡ 1 (mod p)"); #endif - if (order.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) != 1UL) + if (!Pow2EqualsOne(order, prime, divisorData)) { return false; } @@ -283,7 +285,7 @@ internal static partial class PrimeOrderCalculator } reduced /= primeFactor; - if (reduced.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL) + if (Pow2EqualsOne(reduced, prime, divisorData)) { return false; } @@ -355,32 +357,108 @@ internal static partial class PrimeOrderCalculator int powBudget = config.MaxPowChecks <= 0 ? candidates.Count : config.MaxPowChecks; int powUsed = 0; int candidateCount = candidates.Count; + bool allowGpuBatch = true; + Span candidateSpan = CollectionsMarshal.AsSpan(candidates); #if DEBUG Console.WriteLine($"Checking candidates ({candidateCount} candidates, {powBudget} pow budget)"); #endif - for (int i = 0; i < candidateCount; i++) + int index = 0; + const int MaxGpuBatchSize = 256; + const int StackGpuBatchSize = 32; + Span stackGpuRemainders = stackalloc ulong[StackGpuBatchSize]; + while (index < candidateCount && powUsed < powBudget) { - if (powUsed >= powBudget) + int remaining = candidateCount - index; + int budgetRemaining = powBudget - powUsed; + int batchSize = Math.Min(remaining, Math.Min(budgetRemaining, MaxGpuBatchSize)); + if (batchSize <= 0) { break; } - ulong candidate = candidates[i]; - powUsed++; + ReadOnlySpan batch = candidateSpan.Slice(index, batchSize); + ulong[]? gpuPool = null; + Span pooledGpuRemainders = default; + bool gpuSuccess = false; + bool gpuStackRemainders = false; + GpuPow2ModStatus status = GpuPow2ModStatus.Unavailable; - if (candidate.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) != 1UL) + if (allowGpuBatch) { - continue; + if (batchSize <= StackGpuBatchSize) + { + Span localRemainders = stackGpuRemainders.Slice(0, batchSize); + status = PrimeOrderGpuHeuristics.TryPow2ModBatch(batch, prime, localRemainders); + if (status == GpuPow2ModStatus.Success) + { + gpuSuccess = true; + gpuStackRemainders = true; + } + } + else + { + gpuPool = ArrayPool.Shared.Rent(batchSize); + Span pooledRemainders = gpuPool.AsSpan(0, batchSize); + status = PrimeOrderGpuHeuristics.TryPow2ModBatch(batch, prime, pooledRemainders); + if (status == GpuPow2ModStatus.Success) + { + pooledGpuRemainders = pooledRemainders; + gpuSuccess = true; + } + else + { + ArrayPool.Shared.Return(gpuPool, clearArray: false); + gpuPool = null; + } + } + + if (!gpuSuccess && (status == GpuPow2ModStatus.Overflow || status == GpuPow2ModStatus.Unavailable)) + { + allowGpuBatch = false; + } } - if (!TryConfirmCandidate(prime, candidate, divisorData, config, ref powUsed, powBudget)) + for (int i = 0; i < batchSize && powUsed < powBudget; i++) { - continue; + ulong candidate = batch[i]; + powUsed++; + + bool equalsOne; + if (gpuSuccess) + { + ulong remainderValue = gpuStackRemainders ? stackGpuRemainders[i] : pooledGpuRemainders[i]; + equalsOne = remainderValue == 1UL; + } + else + { + equalsOne = Pow2EqualsOne(candidate, prime, divisorData); + } + if (!equalsOne) + { + continue; + } + + if (!TryConfirmCandidate(prime, candidate, divisorData, config, ref powUsed, powBudget)) + { + continue; + } + + if (gpuPool is not null) + { + ArrayPool.Shared.Return(gpuPool, clearArray: false); + } + + result = candidate; + return true; } - result = candidate; - return true; + if (gpuPool is not null) + { + ArrayPool.Shared.Return(gpuPool, clearArray: false); + } + + index += batchSize; } #if DEBUG @@ -601,7 +679,7 @@ internal static partial class PrimeOrderCalculator } powUsed++; - if (reduced.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL) + if (Pow2EqualsOne(reduced, prime, divisorData)) { return false; } @@ -611,6 +689,18 @@ internal static partial class PrimeOrderCalculator return true; } + private static bool Pow2EqualsOne(ulong exponent, ulong prime, in MontgomeryDivisorData divisorData) + { + // TODO: Implement the GPU heuristic path so TryPow2Mod can verify candidates on the device instead of immediately falling back to the CPU Montgomery reducer. + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(exponent, prime, out ulong remainder); + if (status == GpuPow2ModStatus.Success) + { + return remainder == 1UL; + } + + return exponent.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL; + } + private static PartialFactorResult PartialFactor(ulong value, PrimeOrderSearchConfig config) { if (value <= 1UL) diff --git a/apply_patch.cs b/apply_patch.cs index 479bd3a..734e6b8 100644 --- a/apply_patch.cs +++ b/apply_patch.cs @@ -1,6 +1,8 @@ #nullable enable using System; +using System.Buffers; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -46,6 +48,29 @@ internal readonly record struct PatchApplicationOptions(bool CheckOnly, bool Ign internal sealed record PatchRunResult(string PatchFilePath, bool Success, string? ErrorMessage); +internal sealed record PatchProfileEntry( + string PatchFilePath, + int FileCount, + int HunkCount, + int LineCount, + long PatchBytes, + TimeSpan ReadTime, + TimeSpan ParseTime, + TimeSpan ApplyTime); + +internal sealed record LineReplacement(string RelativePath, int LineNumber, string? ExpectedContent, string NewContent); + +internal sealed class LineReplacementException : Exception +{ + public LineReplacementException(LineReplacement replacement, string message) + : base(message) + { + Replacement = replacement; + } + + public LineReplacement Replacement { get; } +} + var args = Args?.ToArray() ?? Array.Empty(); if (args.Length == 0) { @@ -56,8 +81,10 @@ if (args.Length == 0) bool checkOnly = false; bool ignoreWhitespace = false; bool ignoreLineEndings = false; +bool profile = false; string? explicitTargetRoot = null; var patchFileArguments = new List(); +var lineReplacements = new List(); for (int i = 0; i < args.Length; i++) { @@ -81,6 +108,12 @@ for (int i = 0; i < args.Length; i++) continue; } + if (string.Equals(argument, "--profile", StringComparison.Ordinal)) + { + profile = true; + continue; + } + if (string.Equals(argument, "--target", StringComparison.Ordinal)) { if (i + 1 >= args.Length) @@ -93,10 +126,38 @@ for (int i = 0; i < args.Length; i++) continue; } + if (string.Equals(argument, "--replace-line", StringComparison.Ordinal)) + { + if (i + 4 >= args.Length) + { + Console.Error.WriteLine("--replace-line requires arguments."); + Environment.Exit(1); + } + + var replacementPath = args[++i]; + + if (!int.TryParse(args[++i], NumberStyles.Integer, CultureInfo.InvariantCulture, out var replacementLine) || replacementLine <= 0) + { + Console.Error.WriteLine("--replace-line requires a positive integer line number."); + Environment.Exit(1); + } + + var expected = args[++i]; + + if (string.Equals(expected, "_", StringComparison.Ordinal)) + { + expected = null; + } + + var newContent = args[++i]; + lineReplacements.Add(new LineReplacement(replacementPath, replacementLine, expected, newContent)); + continue; + } + patchFileArguments.Add(argument); } -if (patchFileArguments.Count == 0) +if (patchFileArguments.Count == 0 && lineReplacements.Count == 0) { PrintUsage(); Environment.Exit(1); @@ -127,9 +188,9 @@ else targetRoot = Directory.GetCurrentDirectory(); } -if (patchFileArguments.Count == 0) +if (patchFileArguments.Count == 0 && lineReplacements.Count == 0) { - Console.Error.WriteLine("No patch files specified."); + Console.Error.WriteLine("No patch files or line replacements specified."); Environment.Exit(1); } @@ -144,6 +205,7 @@ if (!Directory.Exists(targetRoot)) var patchFilePaths = patchFileArguments.Select(Path.GetFullPath).ToList(); var applicationOptions = new PatchApplicationOptions(checkOnly, ignoreWhitespace, ignoreLineEndings); var patchResults = new List(); +List? patchProfiles = profile ? new List() : null; foreach (var patchFilePath in patchFilePaths) { @@ -154,14 +216,53 @@ foreach (var patchFilePath in patchFilePaths) throw new FileNotFoundException($"Patch file '{patchFilePath}' does not exist.", patchFilePath); } - var patchText = File.ReadAllLines(patchFilePath); - var parsedFiles = ParsePatch(patchText); + Stopwatch? stopwatch = null; + TimeSpan readDuration = default; + TimeSpan parseDuration = default; + TimeSpan applyDuration = default; + long patchBytes = 0L; + int fileCount = 0; + int hunkCount = 0; + int lineCount = 0; + + if (profile) + { + stopwatch = Stopwatch.StartNew(); + patchBytes = new FileInfo(patchFilePath).Length; + } + + var parsedFiles = ParsePatchFromFile(patchFilePath); + + if (stopwatch is not null) + { + readDuration = TimeSpan.Zero; + parseDuration = stopwatch.Elapsed; + fileCount = parsedFiles.Count; + + foreach (var parsedFile in parsedFiles) + { + hunkCount += parsedFile.Hunks.Count; + + foreach (var hunk in parsedFile.Hunks) + { + lineCount += hunk.Lines.Count; + } + } + + stopwatch.Restart(); + } foreach (var patchFile in parsedFiles) { ApplyPatch(targetRoot, patchFile, applicationOptions); } + if (stopwatch is not null && patchProfiles is not null) + { + applyDuration = stopwatch.Elapsed; + patchProfiles.Add(new PatchProfileEntry(patchFilePath, fileCount, hunkCount, lineCount, patchBytes, readDuration, parseDuration, applyDuration)); + } + patchResults.Add(new PatchRunResult(patchFilePath, true, null)); if (!applicationOptions.CheckOnly) @@ -175,13 +276,26 @@ foreach (var patchFilePath in patchFilePaths) } } +if (lineReplacements.Count > 0) +{ + var replacementResults = ApplyLineReplacements(targetRoot, lineReplacements, applicationOptions); + patchResults.AddRange(replacementResults); +} + foreach (var result in patchResults) { var displayPath = GetDisplayPath(result.PatchFilePath); if (result.Success) { - Console.WriteLine($"Patch '{displayPath}' applied successfully."); + if (applicationOptions.CheckOnly) + { + Console.WriteLine($"Patch '{displayPath}' verified successfully. Ready to apply."); + } + else + { + Console.WriteLine($"Patch '{displayPath}' applied successfully."); + } } else { @@ -189,139 +303,168 @@ foreach (var result in patchResults) } } +if (patchProfiles is { Count: > 0 }) +{ + Console.WriteLine("Patch profiling summary:"); + + foreach (var entry in patchProfiles) + { + var displayPath = GetDisplayPath(entry.PatchFilePath); + Console.WriteLine( + $"- {displayPath}: read {entry.ReadTime.TotalMilliseconds:F2} ms, parse {entry.ParseTime.TotalMilliseconds:F2} ms, apply {entry.ApplyTime.TotalMilliseconds:F2} ms " + + $"(files={entry.FileCount}, hunks={entry.HunkCount}, lines={entry.LineCount}, bytes={entry.PatchBytes})"); + } + + var totalFiles = patchProfiles.Sum(static e => e.FileCount); + var totalHunks = patchProfiles.Sum(static e => e.HunkCount); + var totalLines = patchProfiles.Sum(static e => e.LineCount); + var totalBytes = patchProfiles.Sum(static e => e.PatchBytes); + var totalRead = TimeSpan.FromTicks(patchProfiles.Sum(static e => e.ReadTime.Ticks)); + var totalParse = TimeSpan.FromTicks(patchProfiles.Sum(static e => e.ParseTime.Ticks)); + var totalApply = TimeSpan.FromTicks(patchProfiles.Sum(static e => e.ApplyTime.Ticks)); + + Console.WriteLine( + $"Totals: read {totalRead.TotalMilliseconds:F2} ms, parse {totalParse.TotalMilliseconds:F2} ms, apply {totalApply.TotalMilliseconds:F2} ms " + + $"(files={totalFiles}, hunks={totalHunks}, lines={totalLines}, bytes={totalBytes})"); +} + if (patchResults.Any(static r => !r.Success)) { Environment.Exit(1); } -static List ParsePatch(IReadOnlyList lines) +static List ParsePatchFromFile(string patchFilePath) +{ + using var stream = File.OpenRead(patchFilePath); + using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 16_384, leaveOpen: false); + using var lineReader = new PatchLineReader(reader); + var files = ParsePatch(lineReader); + return files; +} + +static List ParsePatch(PatchLineReader reader) { var files = new List(); - int index = 0; + string? diffHeader = null; + string? originalPath = null; + string? modifiedPath = null; + List? hunks = null; - while (index < lines.Count) + while (reader.TryRead(out var currentLine)) { - if (!lines[index].StartsWith("diff --git ", StringComparison.Ordinal)) + if (currentLine is null) + { + break; + } + + if (string.IsNullOrWhiteSpace(currentLine)) { - index++; continue; } - var diffHeader = lines[index]; - index++; - - string? originalPath = null; - string? modifiedPath = null; - var hunks = new List(); - PatchFileType type = PatchFileType.Modify; - - while (index < lines.Count) + if (!currentLine.StartsWith("diff --git ", StringComparison.Ordinal)) { - var line = lines[index]; - - if (line.StartsWith("diff --git ", StringComparison.Ordinal) && hunks.Count > 0) + if (diffHeader is null) { - break; - } - - if (line.StartsWith("--- ", StringComparison.Ordinal)) - { - originalPath = NormalizePath(line.Substring(4)); - index++; continue; } - if (line.StartsWith("+++ ", StringComparison.Ordinal)) + if (currentLine.StartsWith("--- ", StringComparison.Ordinal)) { - modifiedPath = NormalizePath(line.Substring(4)); - index++; + originalPath = NormalizePath(currentLine.AsSpan(4)); continue; } - if (line.StartsWith("@@", StringComparison.Ordinal)) + if (currentLine.StartsWith("+++ ", StringComparison.Ordinal)) { - var hunk = ParseHunk(lines, ref index); + modifiedPath = NormalizePath(currentLine.AsSpan(4)); + continue; + } + + if (currentLine.StartsWith("@@", StringComparison.Ordinal)) + { + hunks ??= new List(); + var hunk = ParseHunk(currentLine, reader); hunks.Add(hunk); continue; } - index++; + continue; } - if (originalPath is null || modifiedPath is null) + if (diffHeader is not null) { - throw new InvalidOperationException($"Missing file paths for patch header: {diffHeader}"); + FinalizePatchFile(files, diffHeader, originalPath, modifiedPath, hunks); } - if (IsDevNull(originalPath) && !IsDevNull(modifiedPath)) - { - type = PatchFileType.Add; - } - else if (!IsDevNull(originalPath) && IsDevNull(modifiedPath)) - { - type = PatchFileType.Delete; - } + diffHeader = currentLine; + originalPath = null; + modifiedPath = null; + hunks = new List(); + } - files.Add(new PatchFile(originalPath, modifiedPath, hunks, type)); + if (diffHeader is not null) + { + FinalizePatchFile(files, diffHeader, originalPath, modifiedPath, hunks); } return files; } -static PatchHunk ParseHunk(IReadOnlyList lines, ref int index) +static PatchHunk ParseHunk(string header, PatchLineReader reader) { - var header = lines[index]; var (originalStart, originalLength, modifiedStart, modifiedLength) = ParseHunkHeader(header); - index++; - var patchLines = new List(); - while (index < lines.Count) + while (reader.TryPeek(out var nextLine)) { - var line = lines[index]; - - if (line.StartsWith("diff --git ", StringComparison.Ordinal) || line.StartsWith("@@", StringComparison.Ordinal)) + if (nextLine is null) { break; } - if (line.StartsWith("+++ ", StringComparison.Ordinal) || line.StartsWith("--- ", StringComparison.Ordinal)) + if (nextLine.StartsWith("diff --git ", StringComparison.Ordinal) || + nextLine.StartsWith("@@", StringComparison.Ordinal) || + nextLine.StartsWith("+++ ", StringComparison.Ordinal) || + nextLine.StartsWith("--- ", StringComparison.Ordinal)) { break; } + if (!reader.TryRead(out var consumed) || consumed is null) + { + break; + } + + var line = consumed; + if (line.StartsWith("+", StringComparison.Ordinal)) { patchLines.Add(new AddedLine(line.Substring(1), true)); - index++; continue; } if (line.StartsWith("-", StringComparison.Ordinal)) { patchLines.Add(new RemovedLine(line.Substring(1), true)); - index++; continue; } if (line.StartsWith(" ", StringComparison.Ordinal)) { patchLines.Add(new ContextLine(line.Substring(1), true)); - index++; continue; } if (line.StartsWith("\\ No newline at end of file", StringComparison.Ordinal)) { MarkLastLineWithoutTrailingNewline(patchLines); - index++; continue; } if (line.Length == 0) { patchLines.Add(new ContextLine(string.Empty, true)); - index++; continue; } @@ -331,6 +474,193 @@ static PatchHunk ParseHunk(IReadOnlyList lines, ref int index) return new PatchHunk(originalStart, originalLength, modifiedStart, modifiedLength, patchLines); } +static void FinalizePatchFile( + List files, + string diffHeader, + string? originalPath, + string? modifiedPath, + List? hunks) +{ + if (originalPath is null || modifiedPath is null) + { + throw new InvalidOperationException($"Missing file paths for patch header: {diffHeader}"); + } + + PatchFileType type = PatchFileType.Modify; + + if (IsDevNull(originalPath) && !IsDevNull(modifiedPath)) + { + type = PatchFileType.Add; + } + else if (!IsDevNull(originalPath) && IsDevNull(modifiedPath)) + { + type = PatchFileType.Delete; + } + + files.Add(new PatchFile(originalPath, modifiedPath, hunks ?? new List(), type)); +} + +private sealed class PatchLineReader : IDisposable +{ + private const int ReadBufferSize = 4096; + private const int InitialLineBufferSize = 256; + + private readonly StreamReader _reader; + private readonly char[] _readBuffer; + private char[] _lineBuffer; + private int _readLength; + private int _readIndex; + private string? _bufferedLine; + private bool _hasBufferedLine; + private bool _disposed; + + public PatchLineReader(StreamReader reader) + { + _reader = reader; + _readBuffer = ArrayPool.Shared.Rent(ReadBufferSize); + _lineBuffer = ArrayPool.Shared.Rent(InitialLineBufferSize); + } + + public bool TryPeek(out string? line) + { + if (!_hasBufferedLine) + { + if (!TryReadLineInternal(out _bufferedLine)) + { + line = null; + return false; + } + + _hasBufferedLine = true; + } + + line = _bufferedLine; + return line is not null; + } + + public bool TryRead(out string? line) + { + if (_hasBufferedLine) + { + line = _bufferedLine; + _bufferedLine = null; + _hasBufferedLine = false; + return line is not null; + } + + return TryReadLineInternal(out line); + } + + private bool TryReadLineInternal(out string? line) + { + if (_disposed) + { + line = null; + return false; + } + + int length = 0; + + while (true) + { + if (!TryReadChar(out char current)) + { + if (length == 0) + { + line = null; + return false; + } + + break; + } + + if (current == '\r') + { + if ((_readIndex < _readLength) || EnsureCharBuffer()) + { + if (_readIndex < _readLength && _readBuffer[_readIndex] == '\n') + { + _readIndex++; + } + } + + break; + } + + if (current == '\n') + { + break; + } + + EnsureLineCapacity(length, length + 1); + _lineBuffer[length++] = current; + } + + line = new string(_lineBuffer, 0, length); + return true; + } + + private bool TryReadChar(out char value) + { + if (!EnsureCharBuffer()) + { + value = '\0'; + return false; + } + + value = _readBuffer[_readIndex++]; + return true; + } + + private bool EnsureCharBuffer() + { + if (_readIndex < _readLength) + { + return true; + } + + if (_disposed) + { + return false; + } + + _readLength = _reader.Read(_readBuffer, 0, _readBuffer.Length); + _readIndex = 0; + return _readLength > 0; + } + + private void EnsureLineCapacity(int currentLength, int requiredLength) + { + if (requiredLength <= _lineBuffer.Length) + { + return; + } + + int newSize = _lineBuffer.Length * 2; + while (newSize < requiredLength) + { + newSize *= 2; + } + + char[] newBuffer = ArrayPool.Shared.Rent(newSize); + Array.Copy(_lineBuffer, newBuffer, currentLength); + ArrayPool.Shared.Return(_lineBuffer); + _lineBuffer = newBuffer; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + ArrayPool.Shared.Return(_readBuffer); + ArrayPool.Shared.Return(_lineBuffer); + } +} + static void MarkLastLineWithoutTrailingNewline(List lines) { if (lines.Count == 0) @@ -351,20 +681,37 @@ static void MarkLastLineWithoutTrailingNewline(List lines) static (int originalStart, int originalLength, int modifiedStart, int modifiedLength) ParseHunkHeader(string header) { - // Format: @@ -start,length +start,length @@ optional text - int firstSpace = header.IndexOf(' '); - int secondSpace = header.IndexOf(' ', firstSpace + 1); - var originalRange = header.Substring(firstSpace + 1, secondSpace - firstSpace - 1); - var modifiedRangeEnd = header.IndexOf(' ', secondSpace + 1); - string modifiedRange; + var span = header.AsSpan(); - if (modifiedRangeEnd == -1) + int firstSpace = span.IndexOf(' '); + + if (firstSpace < 0) { - modifiedRange = header.Substring(secondSpace + 1).TrimEnd('@').Trim(); + throw new InvalidOperationException($"Invalid hunk header: '{header}'."); + } + + span = span[(firstSpace + 1)..]; + + int secondSpace = span.IndexOf(' '); + + if (secondSpace < 0) + { + throw new InvalidOperationException($"Invalid hunk header: '{header}'."); + } + + var originalRange = span[..secondSpace]; + span = span[(secondSpace + 1)..]; + + int thirdSpace = span.IndexOf(' '); + ReadOnlySpan modifiedRange; + + if (thirdSpace < 0) + { + modifiedRange = span; } else { - modifiedRange = header.Substring(secondSpace + 1, modifiedRangeEnd - secondSpace - 1); + modifiedRange = span[..thirdSpace]; } var (originalStart, originalLength) = ParseRange(originalRange); @@ -372,25 +719,35 @@ static (int originalStart, int originalLength, int modifiedStart, int modifiedLe return (originalStart, originalLength, modifiedStart, modifiedLength); } -static (int start, int length) ParseRange(string range) +static (int start, int length) ParseRange(ReadOnlySpan range) { - // Format: -start,length or +start,length - int commaIndex = range.IndexOf(','); - string startText; - string lengthText; + if (range.Length == 0) + { + throw new InvalidOperationException("Range descriptor cannot be empty."); + } + + if (range[0] != '-' && range[0] != '+') + { + throw new InvalidOperationException($"Unexpected range prefix: '{range[0]}' in '{range.ToString()}'."); + } + + var numericPortion = range[1..]; + int commaIndex = numericPortion.IndexOf(','); + ReadOnlySpan startText; + ReadOnlySpan lengthText; if (commaIndex >= 0) { - startText = range.Substring(1, commaIndex - 1); - lengthText = range[(commaIndex + 1)..]; + startText = numericPortion[..commaIndex]; + lengthText = numericPortion[(commaIndex + 1)..]; } else { - startText = range.Substring(1); + startText = numericPortion; lengthText = "1"; } - return (int.Parse(startText, CultureInfo.InvariantCulture), int.Parse(lengthText, CultureInfo.InvariantCulture)); + return (int.Parse(startText, NumberStyles.Integer, CultureInfo.InvariantCulture), int.Parse(lengthText, NumberStyles.Integer, CultureInfo.InvariantCulture)); } static void ApplyPatch(string root, PatchFile patchFile, PatchApplicationOptions options) @@ -428,18 +785,20 @@ static string NormalizeRelativePath(string path) return path; } -static string NormalizePath(string headerPath) +static string NormalizePath(ReadOnlySpan headerPath) { var trimmed = headerPath.Trim(); if (trimmed.StartsWith("a/", StringComparison.Ordinal) || trimmed.StartsWith("b/", StringComparison.Ordinal)) { - return trimmed.Substring(2); + trimmed = trimmed[2..]; } - return trimmed; + return new string(trimmed); } +static string NormalizePath(string headerPath) => NormalizePath(headerPath.AsSpan()); + static bool IsDevNull(string? path) { return path is null || path.Equals("/dev/null", StringComparison.Ordinal); @@ -746,6 +1105,103 @@ static void TryDeletePatchFile(string patchFilePath) } } +static List ApplyLineReplacements(string root, List replacements, PatchApplicationOptions options) +{ + var results = new List(); + + foreach (var group in replacements.GroupBy(static r => NormalizeRelativePath(r.RelativePath), StringComparer.Ordinal)) + { + var normalizedPath = group.Key; + var replacementsForFile = group.OrderBy(static r => r.LineNumber).ToList(); + + if (replacementsForFile.Count == 0) + { + continue; + } + + try + { + var targetPath = Path.Combine(root, normalizedPath); + + if (!File.Exists(targetPath)) + { + var displayPath = GetDisplayPath(targetPath); + throw new LineReplacementException(replacementsForFile[0], $"Cannot replace line because file '{displayPath}' does not exist."); + } + + var snapshot = ReadAllLinesPreservingNewlines(targetPath); + var lines = snapshot.Lines; + var pending = new List<(int Index, string NewContent)>(); + + foreach (var replacement in replacementsForFile) + { + if (replacement.LineNumber <= 0) + { + throw new LineReplacementException(replacement, $"Line numbers must be positive. Received {replacement.LineNumber} for '{GetDisplayPath(targetPath)}'."); + } + + int index = replacement.LineNumber - 1; + + if (index >= lines.Count) + { + throw new LineReplacementException(replacement, $"File '{GetDisplayPath(targetPath)}' contains only {lines.Count} lines. Cannot replace line {replacement.LineNumber}."); + } + + if (replacement.ExpectedContent is not null) + { + var actual = lines[index]; + + if (!LinesMatch(actual, replacement.ExpectedContent, options)) + { + try + { + ThrowLineMismatch($"Line {replacement.LineNumber} mismatch in '{GetDisplayPath(targetPath)}'.", replacement.ExpectedContent, actual, options); + } + catch (InvalidOperationException ex) + { + throw new LineReplacementException(replacement, ex.Message); + } + } + } + + pending.Add((index, replacement.NewContent)); + } + + if (!options.CheckOnly) + { + foreach (var (index, newContent) in pending) + { + lines[index] = newContent; + } + + WriteAllLinesPreservingNewlines(targetPath, snapshot); + } + + foreach (var replacement in replacementsForFile) + { + results.Add(new PatchRunResult(BuildReplacementDisplay(replacement), true, null)); + } + } + catch (LineReplacementException ex) + { + results.Add(new PatchRunResult(BuildReplacementDisplay(ex.Replacement), false, ex.Message)); + } + catch (Exception ex) + { + var fallback = replacementsForFile[0]; + results.Add(new PatchRunResult(BuildReplacementDisplay(fallback), false, ex.Message)); + } + } + + return results; +} + +static string BuildReplacementDisplay(LineReplacement replacement) +{ + var normalizedPath = NormalizeRelativePath(replacement.RelativePath); + return $"replace-line:{normalizedPath}:{replacement.LineNumber}"; +} + static void PrintUsage() { Console.Error.WriteLine("Usage: dotnet script apply_patch.cs -- [options] [ ...] [target-directory]"); @@ -754,6 +1210,9 @@ static void PrintUsage() Console.Error.WriteLine(" --check Validate patches without modifying files."); Console.Error.WriteLine(" --ignore-whitespace Ignore whitespace differences when matching context."); Console.Error.WriteLine(" --ignore-eol Ignore line-ending differences when matching context."); + Console.Error.WriteLine(" --profile Collect read/parse/apply timings for each patch file."); Console.Error.WriteLine(" --target Explicitly set the target directory."); + Console.Error.WriteLine(" --replace-line "); + Console.Error.WriteLine(" Replace a single line by absolute number; use '_' as expected-text to skip validation."); } diff --git a/todo.txt b/todo.txt new file mode 100644 index 0000000..73977fb --- /dev/null +++ b/todo.txt @@ -0,0 +1,3 @@ +- Ensure the GPU order path reroutes to the CPU heuristics whenever exponents or moduli exceed the currently implemented device integer widths (GpuUInt128/GpuUInt256/GpuUInt512), so overflow never propagates into incorrect results. +- Implement the GPU heuristics for PrimeOrderCalculator using ILGPU. Reuse the numeric type compatibility probe results to select between EInteger, ERational, GpuUInt128, or any new GPU-specific big-integer representations, guarantee parity with the CPU heuristic algorithm (including 128-bit primes), prefer the shift-first multiply/shift variant to stay within device ranges, and wire the calculator through the accelerator path. +- Extend the GPU numeric type probe to cover proposed custom types such as GpuUInt256 and GpuUInt512 once their definitions are available so we can plan the wide-order heuristics on the device. diff --git a/tools/GpuNumericTypeProbe.csx b/tools/GpuNumericTypeProbe.csx new file mode 100644 index 0000000..0bfb2a6 --- /dev/null +++ b/tools/GpuNumericTypeProbe.csx @@ -0,0 +1,160 @@ +#r "nuget: ILGPU, 1.5.3" +#r "nuget: PeterO.Numbers, 1.8.1" + +using System; +using System.Numerics; +using ILGPU; +using ILGPU.Runtime; +using ILGPU.Runtime.CPU; +using PeterO.Numbers; + +var context = Context.CreateDefault(); +var accelerator = context.CreateCPUAccelerator(0); + +try +{ + Console.WriteLine("ILGPU numeric type support probe (CPU accelerator)"); + Console.WriteLine(); + + ProbeResult[] results = + [ + ProbeBigInteger(accelerator), + ProbeEInteger(accelerator), + ProbeERational(accelerator) + ]; + + foreach (ProbeResult result in results) + { + Console.WriteLine($"{result.Type}: {(result.Supported ? "supported" : "unsupported")}"); + Console.WriteLine($" Details: {result.Details}"); + Console.WriteLine(); + } +} +finally +{ + accelerator.Dispose(); + context.Dispose(); +} + +record ProbeResult(string Type, bool Supported, string Details) +{ + public static ProbeResult Success(string type, string details) => new(type, true, details); + + public static ProbeResult Failure(string type, Exception exception) => + new(type, false, $"{exception.GetType().Name}: {exception.Message}"); + + public static ProbeResult Failure(string type, string details) => new(type, false, details); +} + +static ProbeResult ProbeBigInteger(Accelerator accelerator) +{ + try + { + var kernel = accelerator.LoadAutoGroupedStreamKernel>(BigIntegerKernel); + using var buffer = accelerator.Allocate1D(1); + var extent = new Index1D((int)buffer.Length); + kernel(extent, buffer.View); + accelerator.Synchronize(); + return ProbeResult.Success( + "System.Numerics.BigInteger", + "Addition, subtraction, multiplication, division, and modulo compile on the CPU accelerator."); + } + catch (Exception ex) + { + return ProbeResult.Failure("System.Numerics.BigInteger", ex); + } +} + +static ProbeResult ProbeEInteger(Accelerator accelerator) +{ + try + { + var kernel = accelerator.LoadAutoGroupedStreamKernel>(EIntegerKernel); + using var buffer = accelerator.Allocate1D(1); + kernel(new Index1D((int)buffer.Length), buffer.View); + accelerator.Synchronize(); + return ProbeResult.Success( + "PeterO.Numbers.EInteger", + "Addition, subtraction, multiplication, division, and remainder compile on the CPU accelerator."); + } + catch (Exception ex) + { + return ProbeResult.Failure("PeterO.Numbers.EInteger", ex); + } +} + +static ProbeResult ProbeERational(Accelerator accelerator) +{ + bool hasRemainder = HasERationalRemainder(); + + try + { + var kernel = accelerator.LoadAutoGroupedStreamKernel>(ERationalKernel); + using var buffer = accelerator.Allocate1D(1); + kernel(new Index1D((int)buffer.Length), buffer.View); + accelerator.Synchronize(); + + string details = "Addition, subtraction, multiplication, and division compile on the CPU accelerator."; + details += hasRemainder + ? " ERational exposes a remainder API that requires further investigation." + : " ERational does not expose a remainder/modulo API."; + return ProbeResult.Success("PeterO.Numbers.ERational", details); + } + catch (Exception ex) + { + string details = $"{ex.GetType().Name}: {ex.Message}"; + if (!hasRemainder) + { + details += " ERational does not expose a remainder/modulo API."; + } + + return ProbeResult.Failure("PeterO.Numbers.ERational", details); + } +} + +static void BigIntegerKernel(Index1D index, ArrayView1D data) +{ + BigInteger value = new BigInteger(data[index]); + value += BigInteger.One; + value -= BigInteger.One; + value *= new BigInteger(2); + value /= new BigInteger(3); + value %= new BigInteger(5); + int comparison = value.CompareTo(BigInteger.Zero); + data[index] = comparison; +} + +static void EIntegerKernel(Index1D index, ArrayView1D data) +{ + EInteger value = EInteger.FromInt32(data[index]); + value = value + EInteger.One; + value = value - EInteger.One; + value = value * EInteger.FromInt32(2); + value = value.Divide(EInteger.FromInt32(3)); + value = value.Remainder(EInteger.FromInt32(5)); + int comparison = value.CompareTo(EInteger.Zero); + data[index] = comparison; +} + +static void ERationalKernel(Index1D index, ArrayView1D data) +{ + ERational value = ERational.FromInt32(data[index]); + ERational adjusted = (value + ERational.FromInt32(1)) - ERational.FromInt32(1); + ERational product = adjusted * ERational.FromInt32(2); + ERational quotient = product / ERational.FromInt32(3); + int comparison = quotient.CompareTo(ERational.FromInt32(0)); + data[index] = comparison; +} + +static bool HasERationalRemainder() +{ + foreach (var method in typeof(ERational).GetMethods()) + { + if (string.Equals(method.Name, "Remainder", StringComparison.Ordinal)) + { + return true; + } + } + + return false; +} diff --git a/tools/HeuristicMultiplyShiftTiming/HeuristicMultiplyShiftTiming.csproj b/tools/HeuristicMultiplyShiftTiming/HeuristicMultiplyShiftTiming.csproj new file mode 100644 index 0000000..e771e0a --- /dev/null +++ b/tools/HeuristicMultiplyShiftTiming/HeuristicMultiplyShiftTiming.csproj @@ -0,0 +1,11 @@ + + + Exe + net8.0 + enable + enable + + + + + \ No newline at end of file diff --git a/tools/HeuristicMultiplyShiftTiming/Program.cs b/tools/HeuristicMultiplyShiftTiming/Program.cs new file mode 100644 index 0000000..1c35acf --- /dev/null +++ b/tools/HeuristicMultiplyShiftTiming/Program.cs @@ -0,0 +1,95 @@ +using System.Diagnostics; +using PerfectNumbers.Core; + +namespace HeuristicMultiplyShiftTiming; + +internal static class Program +{ + private static readonly MultiplyShiftInput[] Inputs = + [ + new(0x3FFF_FFFF_FFFF_FFFFUL, 3UL, 3, "NearOverflowShift"), + new(0x2000_0000_0000_0000UL, 7UL, 4, "HalfRange"), + new(0x1234_5678_9ABC_DEF0UL, 5UL, 5, "MixedBits"), + new(0x0000_0000_1000_0000UL, 11UL, 3, "Sparse"), + ]; + + private const int Iterations = 200_000_000; + + public static void Main() + { + Console.WriteLine($"Timing each method for {Iterations:N0} iterations per input...\n"); + + foreach (MultiplyShiftInput input in Inputs) + { + Console.WriteLine($"Input: {input.Name} (value=0x{input.Value:X16}, multiplier={input.Multiplier}, shift={input.Shift})"); + + Measurement safe = MeasureSafe(input); + Console.WriteLine($" MultiplyShiftRight (UInt128 product): {safe.AverageNanoseconds:F3} ns | checksum=0x{safe.Checksum:X16}"); + + Measurement shiftFirst = MeasureShiftFirst(input); + Console.WriteLine($" MultiplyShiftRightShiftFirst (shift-first): {shiftFirst.AverageNanoseconds:F3} ns | checksum=0x{shiftFirst.Checksum:X16}"); + + Measurement naive = MeasureNaive(input); + Console.WriteLine($" Naive ((value * multiplier) >> shift): {naive.AverageNanoseconds:F3} ns | checksum=0x{naive.Checksum:X16}\n"); + } + } + + private static Measurement MeasureSafe(MultiplyShiftInput input) + { + ulong checksum = 0UL; + Stopwatch sw = Stopwatch.StartNew(); + ulong value = input.Value; + + for (int i = 0; i < Iterations; i++) + { + checksum += HeuristicArithmetic.MultiplyShiftRight(value, input.Multiplier, input.Shift); + value += 1UL; + } + + sw.Stop(); + return CreateMeasurement(sw, checksum); + } + + private static Measurement MeasureShiftFirst(MultiplyShiftInput input) + { + ulong checksum = 0UL; + Stopwatch sw = Stopwatch.StartNew(); + ulong value = input.Value; + + for (int i = 0; i < Iterations; i++) + { + checksum += HeuristicArithmetic.MultiplyShiftRightShiftFirst(value, input.Multiplier, input.Shift); + value += 1UL; + } + + sw.Stop(); + return CreateMeasurement(sw, checksum); + } + + private static Measurement MeasureNaive(MultiplyShiftInput input) + { + ulong checksum = 0UL; + Stopwatch sw = Stopwatch.StartNew(); + ulong value = input.Value; + + for (int i = 0; i < Iterations; i++) + { + checksum += (value * input.Multiplier) >> input.Shift; + value += 1UL; + } + + sw.Stop(); + return CreateMeasurement(sw, checksum); + } + + private static Measurement CreateMeasurement(Stopwatch sw, ulong checksum) + { + double elapsedNanoseconds = sw.ElapsedTicks * (1_000_000_000.0 / Stopwatch.Frequency); + double averageNanoseconds = elapsedNanoseconds / Iterations; + return new Measurement(averageNanoseconds, checksum); + } + + private readonly record struct MultiplyShiftInput(ulong Value, ulong Multiplier, int Shift, string Name); + + private readonly record struct Measurement(double AverageNanoseconds, ulong Checksum); +} From 6adac6b7a7bb06ac3efe96102cdfaa48dd3faef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sat, 4 Oct 2025 21:20:29 +0200 Subject: [PATCH 2/8] Consolidate multiply helpers and guard GPU fallbacks --- AGENTS.md | 9 +- .../HeuristicMultiplyShiftBenchmarks.cs | 4 +- .../Gpu/PrimeOrderGpuHeuristicsTests.cs | 101 ++++++++++++++++-- ...s => ULongExtensionsMultiplyShiftTests.cs} | 12 +-- .../Gpu/PrimeOrderGpuHeuristics.cs | 75 ++++++++++++- PerfectNumbers.Core/HeuristicArithmetic.cs | 48 --------- PerfectNumbers.Core/ULongExtensions.cs | 41 +++++++ apply_patch.cs | 2 +- todo.txt | 1 - tools/HeuristicMultiplyShiftTiming/Program.cs | 4 +- 10 files changed, 227 insertions(+), 70 deletions(-) rename PerfectNumbers.Core.Tests/{HeuristicArithmeticTests.cs => ULongExtensionsMultiplyShiftTests.cs} (79%) delete mode 100644 PerfectNumbers.Core/HeuristicArithmetic.cs diff --git a/AGENTS.md b/AGENTS.md index 5a50a63..f0b90d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,6 +23,8 @@ To make any changes to the files in the repository, you should always utilize a dotnet script apply_patch.cs -- [target-directory] ``` +> **Temporary workflow notice:** Until the pending apply_patch.cs performance and correctness TODOs are complete, do not invoke the script in this repository. Edit files directly with shell tools (sed, tee, cat, python, etc.) and stage the results via git. Re-enable the helper only after the TODO tracker explicitly calls out that the ban has been lifted. + It supports the standard `git diff` patch files as input. You should be able to run it both in the local and remote `Codex` environments. You should expect it to work both in `Windows` and `Linux` environments. Make sure that you include `diff --git` lines, headers and file prefixes in the patch files to avoid applying issues. `apply_patch.cs` supports several parameters, depending on your needs: @@ -62,6 +64,7 @@ Do **not** attempt to apply the changes manually, or with `Python`, or with `Pow ### Test execution time policy - Always estimate runtime before launching any tests or long-running samples. If a test or ad-hoc run is likely to exceed 2 minutes, do not run it unless the user explicitly requests a longer run and provides justification. +- Never apply the two-minute guard to benchmark or profiling runs; allow them to finish and capture the summary unless the console log shows the job is clearly stuck. - Do not hardcode timeouts inside test code. Enforce time limits from the runner/shell layer so developers can run tests locally without artificial constraints. - Prefer targeted filters over full suites to keep runs short: `--filter FullyQualifiedName~` or `--filter "FullyQualifiedName=Namespace.Class.Method"`. - Keep GPU-heavy tests minimal and scoped. If a kernel compile or warmup is expected to take >2 minutes, skip running it during regular iterations. @@ -144,11 +147,13 @@ Additional numeric-type probe results (`tools/GpuNumericTypeProbe.csx`, ILGPU 1. Patch-preparation checklist for reliable `--check` runs: +- Manually review every patch file before running `--check`; confirm the hunks reflect only the intended edits and eliminate accidental whitespace-only noise ahead of time. - Always capture context with `nl -ba` before composing each hunk so the `@@` header line numbers match the target file on the first try. +- Ensure every hunk carries at least three unchanged context lines above and below the edits whenever the surrounding file provides them; regenerate the diff if the file changed since the previous attempt so the context stays accurate. - Trim or normalize trailing whitespace while drafting the patch, or rely on `--ignore-whitespace` during verification when formatting noise is unavoidable, then re-run without ignores to confirm the final diff matches exactly. - Prefer regenerating the patch from scratch whenever the working tree changes instead of editing previous diffs; stale context is the most common source of mismatches. -- The patch parser now ignores blank metadata lines between `diff --git`, `---`, and `+++` headers, so you no longer need to insert dummy empty separators to satisfy the checker. Keep the header compact unless your diff tool emits those blank lines naturally. +- The patch parser now ignores blank metadata lines between `diff --git`, `---`, and `+++` headers and skips stray blank separators between hunks, so you no longer need to insert dummy empty lines to satisfy the checker. Keep the header compact unless your diff tool emits those blank lines naturally. ### apply_patch.cs profiling snapshot (2025-10-04) @@ -172,7 +177,7 @@ Practical guidance: - Prefer Mersenne folding over general modulo for `2^p-1` moduli to remove branches in hot loops. - Heuristic multiply-shift benchmarks (2025-10-04): - - Run `dotnet run -c Release --project tools/HeuristicMultiplyShiftTiming/HeuristicMultiplyShiftTiming.csproj` to compare the `HeuristicArithmetic` helpers without the BenchmarkDotNet harness. + - Run `dotnet run -c Release --project tools/HeuristicMultiplyShiftTiming/HeuristicMultiplyShiftTiming.csproj` to compare the `ULongExtensions` helpers without the BenchmarkDotNet harness. - Timings below report the average nanoseconds per call over 200,000,000 iterations; all variants produced identical checksums, confirming consistent arithmetic. - `NearOverflowShift`: `MultiplyShiftRight` ≈ 2.37 ns, shift-first ≈ 4.71 ns, naive `(value * multiplier) >> shift` ≈ 0.85 ns. - `HalfRange`: `MultiplyShiftRight` ≈ 2.11 ns, shift-first ≈ 4.15 ns, naive ≈ 0.70 ns. diff --git a/EvenPerfectBitScanner.Benchmarks/HeuristicMultiplyShiftBenchmarks.cs b/EvenPerfectBitScanner.Benchmarks/HeuristicMultiplyShiftBenchmarks.cs index 7bc796a..09290e2 100644 --- a/EvenPerfectBitScanner.Benchmarks/HeuristicMultiplyShiftBenchmarks.cs +++ b/EvenPerfectBitScanner.Benchmarks/HeuristicMultiplyShiftBenchmarks.cs @@ -28,13 +28,13 @@ public class HeuristicMultiplyShiftBenchmarks [Benchmark(Baseline = true)] public ulong SafeMultiplyShift() { - return HeuristicArithmetic.MultiplyShiftRight(Input.Value, Input.Multiplier, Input.Shift); + return ULongExtensions.MultiplyShiftRight(Input.Value, Input.Multiplier, Input.Shift); } [Benchmark] public ulong ShiftFirstMultiplyShift() { - return HeuristicArithmetic.MultiplyShiftRightShiftFirst(Input.Value, Input.Multiplier, Input.Shift); + return ULongExtensions.MultiplyShiftRightShiftFirst(Input.Value, Input.Multiplier, Input.Shift); } [Benchmark] diff --git a/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs b/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs index 9914ad6..0d0ac41 100644 --- a/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs +++ b/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs @@ -9,11 +9,16 @@ namespace PerfectNumbers.Core.Tests; [Trait("Category", "Fast")] public class PrimeOrderGpuHeuristicsTests { + public PrimeOrderGpuHeuristicsTests() + { + PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); + PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting(); + } + [Fact] public void TryPow2Mod_returns_overflow_for_marked_prime() { const ulong prime = 97UL; - PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); try { PrimeOrderGpuHeuristics.MarkOverflow(prime); @@ -32,19 +37,58 @@ public class PrimeOrderGpuHeuristicsTests [Fact] public void TryPow2Mod_returns_unavailable_for_unmarked_prime() { - PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); - GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(1UL, 193UL, out ulong remainder); status.Should().Be(GpuPow2ModStatus.Unavailable); remainder.Should().Be(0UL); } + [Fact] + public void TryPow2Mod_returns_overflow_when_modulus_exceeds_capability() + { + const ulong prime = 97UL; + var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(6, 64); + PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability); + try + { + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(1UL, prime, out ulong remainder); + + status.Should().Be(GpuPow2ModStatus.Overflow); + remainder.Should().Be(0UL); + } + finally + { + PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting(); + PrimeOrderGpuHeuristics.ClearOverflow(prime); + } + } + + [Fact] + public void TryPow2Mod_returns_overflow_when_exponent_exceeds_capability() + { + var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(64, 3); + PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability); + try + { + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(16UL, 193UL, out ulong remainder); + + status.Should().Be(GpuPow2ModStatus.Overflow); + remainder.Should().Be(0UL); + + GpuPow2ModStatus followUp = PrimeOrderGpuHeuristics.TryPow2Mod(4UL, 193UL, out ulong followUpRemainder); + followUp.Should().Be(GpuPow2ModStatus.Unavailable); + followUpRemainder.Should().Be(0UL); + } + finally + { + PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting(); + } + } + [Fact] public void TryPow2ModBatch_returns_overflow_for_marked_prime() { const ulong prime = 193UL; - PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); try { PrimeOrderGpuHeuristics.MarkOverflow(prime); @@ -66,8 +110,6 @@ public class PrimeOrderGpuHeuristicsTests [Fact] public void TryPow2ModBatch_returns_unavailable_and_clears_remainders_for_unmarked_prime() { - PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); - ulong[] remainders = { 123UL, 456UL }; ulong[] exponents = { 1UL, 2UL }; GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, 257UL, remainders); @@ -77,6 +119,52 @@ public class PrimeOrderGpuHeuristicsTests remainders[1].Should().Be(0UL); } + [Fact] + public void TryPow2ModBatch_returns_overflow_when_modulus_exceeds_capability() + { + const ulong prime = 97UL; + var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(6, 64); + PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability); + try + { + ulong[] exponents = { 1UL, 2UL }; + ulong[] remainders = { 5UL, 6UL }; + + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders); + + status.Should().Be(GpuPow2ModStatus.Overflow); + remainders[0].Should().Be(0UL); + remainders[1].Should().Be(0UL); + } + finally + { + PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting(); + PrimeOrderGpuHeuristics.ClearOverflow(prime); + } + } + + [Fact] + public void TryPow2ModBatch_returns_overflow_when_any_exponent_exceeds_capability() + { + var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(64, 3); + PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability); + try + { + ulong[] exponents = { 2UL, 16UL }; + ulong[] remainders = { 9UL, 11UL }; + + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, 193UL, remainders); + + status.Should().Be(GpuPow2ModStatus.Overflow); + remainders[0].Should().Be(0UL); + remainders[1].Should().Be(0UL); + } + finally + { + PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting(); + } + } + [Fact] public void TryPow2ModBatch_throws_when_remainder_span_is_too_small() { @@ -92,7 +180,6 @@ public class PrimeOrderGpuHeuristicsTests public void Calculate_falls_back_to_cpu_when_gpu_overflow_is_marked() { const ulong prime = 7UL; - PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); try { PrimeOrderGpuHeuristics.MarkOverflow(prime); diff --git a/PerfectNumbers.Core.Tests/HeuristicArithmeticTests.cs b/PerfectNumbers.Core.Tests/ULongExtensionsMultiplyShiftTests.cs similarity index 79% rename from PerfectNumbers.Core.Tests/HeuristicArithmeticTests.cs rename to PerfectNumbers.Core.Tests/ULongExtensionsMultiplyShiftTests.cs index a3cd60a..d755e1b 100644 --- a/PerfectNumbers.Core.Tests/HeuristicArithmeticTests.cs +++ b/PerfectNumbers.Core.Tests/ULongExtensionsMultiplyShiftTests.cs @@ -6,7 +6,7 @@ using Xunit; namespace PerfectNumbers.Core.Tests; [Trait("Category", "Fast")] -public class HeuristicArithmeticTests +public class ULongExtensionsMultiplyShiftTests { [Theory] [InlineData(0UL, 0UL, 1)] @@ -18,7 +18,7 @@ public class HeuristicArithmeticTests { BigInteger expected = ((BigInteger)value * multiplier) >> shift; - ulong result = HeuristicArithmetic.MultiplyShiftRight(value, multiplier, shift); + ulong result = ULongExtensions.MultiplyShiftRight(value, multiplier, shift); result.Should().Be((ulong)expected); } @@ -34,7 +34,7 @@ public class HeuristicArithmeticTests { BigInteger expected = ((BigInteger)value * multiplier) >> shift; - ulong result = HeuristicArithmetic.MultiplyShiftRightShiftFirst(value, multiplier, shift); + ulong result = ULongExtensions.MultiplyShiftRightShiftFirst(value, multiplier, shift); result.Should().Be((ulong)expected); } @@ -42,7 +42,7 @@ public class HeuristicArithmeticTests [Fact] public void MultiplyShiftRightShiftFirst_returns_zero_when_shift_is_large() { - HeuristicArithmetic.MultiplyShiftRightShiftFirst(ulong.MaxValue, 123UL, 64).Should().Be(0UL); + ULongExtensions.MultiplyShiftRightShiftFirst(ulong.MaxValue, 123UL, 64).Should().Be(0UL); } [Theory] @@ -50,7 +50,7 @@ public class HeuristicArithmeticTests [InlineData(1UL, 1UL, -3)] public void MultiplyShiftRight_throws_when_shift_is_negative(ulong value, ulong multiplier, int shift) { - Action act = () => HeuristicArithmetic.MultiplyShiftRight(value, multiplier, shift); + Action act = () => ULongExtensions.MultiplyShiftRight(value, multiplier, shift); act.Should().Throw(); } @@ -60,7 +60,7 @@ public class HeuristicArithmeticTests [InlineData(1UL, 1UL, -3)] public void MultiplyShiftRightShiftFirst_throws_when_shift_is_negative(ulong value, ulong multiplier, int shift) { - Action act = () => HeuristicArithmetic.MultiplyShiftRightShiftFirst(value, multiplier, shift); + Action act = () => ULongExtensions.MultiplyShiftRightShiftFirst(value, multiplier, shift); act.Should().Throw(); } diff --git a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs index ee07fc3..21583db 100644 --- a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs +++ b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Concurrent; +using System.Numerics; +using System.Threading; namespace PerfectNumbers.Core.Gpu; @@ -13,6 +15,22 @@ internal enum GpuPow2ModStatus internal static class PrimeOrderGpuHeuristics { private static readonly ConcurrentDictionary OverflowedPrimes = new(); + private static int s_modulusBitLimit = PrimeOrderGpuCapability.Default.ModulusBits; + private static int s_exponentBitLimit = PrimeOrderGpuCapability.Default.ExponentBits; + + internal static void OverrideCapabilitiesForTesting(PrimeOrderGpuCapability capability) + { + Volatile.Write(ref s_modulusBitLimit, capability.ModulusBits); + Volatile.Write(ref s_exponentBitLimit, capability.ExponentBits); + } + + internal static void ResetCapabilitiesForTesting() + { + OverrideCapabilitiesForTesting(PrimeOrderGpuCapability.Default); + } + + private static PrimeOrderGpuCapability Capabilities + => new(Volatile.Read(ref s_modulusBitLimit), Volatile.Read(ref s_exponentBitLimit)); internal static void MarkOverflow(ulong prime) => OverflowedPrimes[prime] = 0; @@ -33,6 +51,19 @@ internal static class PrimeOrderGpuHeuristics return GpuPow2ModStatus.Overflow; } + PrimeOrderGpuCapability capability = Capabilities; + + if (!SupportsPrime(prime, capability)) + { + MarkOverflow(prime); + return GpuPow2ModStatus.Overflow; + } + + if (!SupportsExponent(exponent, capability)) + { + return GpuPow2ModStatus.Overflow; + } + // TODO: Implement the GPU heuristic order calculator so this path mirrors PrimeOrderCalculator before enabling device execution. return GpuPow2ModStatus.Unavailable; } @@ -61,7 +92,49 @@ internal static class PrimeOrderGpuHeuristics return GpuPow2ModStatus.Overflow; } + PrimeOrderGpuCapability capability = Capabilities; + + if (!SupportsPrime(prime, capability)) + { + MarkOverflow(prime); + return GpuPow2ModStatus.Overflow; + } + + for (int i = 0; i < exponents.Length; i++) + { + if (!SupportsExponent(exponents[i], capability)) + { + return GpuPow2ModStatus.Overflow; + } + } + // TODO: Implement the GPU heuristic order calculator so this path mirrors PrimeOrderCalculator before enabling device execution. return GpuPow2ModStatus.Unavailable; } -} + + private static bool SupportsPrime(ulong prime, PrimeOrderGpuCapability capability) + { + return GetBitLength(prime) <= capability.ModulusBits; + } + + private static bool SupportsExponent(ulong exponent, PrimeOrderGpuCapability capability) + { + return GetBitLength(exponent) <= capability.ExponentBits; + } + + private static int GetBitLength(ulong value) + { + if (value == 0UL) + { + return 0; + } + + return 64 - BitOperations.LeadingZeroCount(value); + } + + internal readonly record struct PrimeOrderGpuCapability(int ModulusBits, int ExponentBits) + { + public static PrimeOrderGpuCapability Default => new(128, 64); + } + + } diff --git a/PerfectNumbers.Core/HeuristicArithmetic.cs b/PerfectNumbers.Core/HeuristicArithmetic.cs deleted file mode 100644 index 7e62121..0000000 --- a/PerfectNumbers.Core/HeuristicArithmetic.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Runtime.CompilerServices; - -namespace PerfectNumbers.Core; - -internal static class HeuristicArithmetic -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ulong MultiplyShiftRight(ulong value, ulong multiplier, int shift) - { - if (shift < 0) - { - throw new ArgumentOutOfRangeException(nameof(shift), "Shift must be non-negative."); - } - - UInt128 product = (UInt128)value * multiplier; - return (ulong)(product >> shift); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ulong MultiplyShiftRightShiftFirst(ulong value, ulong multiplier, int shift) - { - if (shift < 0) - { - throw new ArgumentOutOfRangeException(nameof(shift), "Shift must be non-negative."); - } - - if (shift == 0) - { - return MultiplyShiftRight(value, multiplier, 0); - } - - if (shift >= 64) - { - return 0UL; - } - - ulong high = value >> shift; - ulong mask = (1UL << shift) - 1UL; - ulong low = value & mask; - - UInt128 highContribution = (UInt128)high * multiplier; - UInt128 lowContribution = (UInt128)low * multiplier; - - UInt128 combined = highContribution + (lowContribution >> shift); - return (ulong)combined; - } -} diff --git a/PerfectNumbers.Core/ULongExtensions.cs b/PerfectNumbers.Core/ULongExtensions.cs index e3bac57..f94b2eb 100644 --- a/PerfectNumbers.Core/ULongExtensions.cs +++ b/PerfectNumbers.Core/ULongExtensions.cs @@ -56,6 +56,47 @@ public static class ULongExtensions return order; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong MultiplyShiftRight(ulong value, ulong multiplier, int shift) + { + if (shift < 0) + { + throw new ArgumentOutOfRangeException(nameof(shift), "Shift must be non-negative."); + } + + UInt128 product = (UInt128)value * multiplier; + return (ulong)(product >> shift); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong MultiplyShiftRightShiftFirst(ulong value, ulong multiplier, int shift) + { + if (shift < 0) + { + throw new ArgumentOutOfRangeException(nameof(shift), "Shift must be non-negative."); + } + + if (shift == 0) + { + return MultiplyShiftRight(value, multiplier, 0); + } + + if (shift >= 64) + { + return 0UL; + } + + ulong high = value >> shift; + ulong mask = (1UL << shift) - 1UL; + ulong low = value & mask; + + UInt128 highContribution = (UInt128)high * multiplier; + UInt128 lowContribution = (UInt128)low * multiplier; + + UInt128 combined = highContribution + (lowContribution >> shift); + return (ulong)combined; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong FastDiv64(this ulong value, ulong divisor, ulong mul) { diff --git a/apply_patch.cs b/apply_patch.cs index 734e6b8..71b34f6 100644 --- a/apply_patch.cs +++ b/apply_patch.cs @@ -464,7 +464,7 @@ static PatchHunk ParseHunk(string header, PatchLineReader reader) if (line.Length == 0) { - patchLines.Add(new ContextLine(string.Empty, true)); + // Skip stray blank separators so hunks without an empty line still parse correctly. continue; } diff --git a/todo.txt b/todo.txt index 73977fb..5f09bfc 100644 --- a/todo.txt +++ b/todo.txt @@ -1,3 +1,2 @@ -- Ensure the GPU order path reroutes to the CPU heuristics whenever exponents or moduli exceed the currently implemented device integer widths (GpuUInt128/GpuUInt256/GpuUInt512), so overflow never propagates into incorrect results. - Implement the GPU heuristics for PrimeOrderCalculator using ILGPU. Reuse the numeric type compatibility probe results to select between EInteger, ERational, GpuUInt128, or any new GPU-specific big-integer representations, guarantee parity with the CPU heuristic algorithm (including 128-bit primes), prefer the shift-first multiply/shift variant to stay within device ranges, and wire the calculator through the accelerator path. - Extend the GPU numeric type probe to cover proposed custom types such as GpuUInt256 and GpuUInt512 once their definitions are available so we can plan the wide-order heuristics on the device. diff --git a/tools/HeuristicMultiplyShiftTiming/Program.cs b/tools/HeuristicMultiplyShiftTiming/Program.cs index 1c35acf..8f6e211 100644 --- a/tools/HeuristicMultiplyShiftTiming/Program.cs +++ b/tools/HeuristicMultiplyShiftTiming/Program.cs @@ -42,7 +42,7 @@ internal static class Program for (int i = 0; i < Iterations; i++) { - checksum += HeuristicArithmetic.MultiplyShiftRight(value, input.Multiplier, input.Shift); + checksum += ULongExtensions.MultiplyShiftRight(value, input.Multiplier, input.Shift); value += 1UL; } @@ -58,7 +58,7 @@ internal static class Program for (int i = 0; i < Iterations; i++) { - checksum += HeuristicArithmetic.MultiplyShiftRightShiftFirst(value, input.Multiplier, input.Shift); + checksum += ULongExtensions.MultiplyShiftRightShiftFirst(value, input.Multiplier, input.Shift); value += 1UL; } From 85c6eaece7615dd85aafedc52cfb333d537b8ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sat, 4 Oct 2025 22:40:02 +0200 Subject: [PATCH 3/8] Persist GPU capability snapshot without volatile --- .../Gpu/PrimeOrderGpuHeuristics.cs | 16 +++++----------- todo.txt | 2 ++ 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs index 21583db..9138697 100644 --- a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs +++ b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using System.Numerics; -using System.Threading; namespace PerfectNumbers.Core.Gpu; @@ -15,23 +14,18 @@ internal enum GpuPow2ModStatus internal static class PrimeOrderGpuHeuristics { private static readonly ConcurrentDictionary OverflowedPrimes = new(); - private static int s_modulusBitLimit = PrimeOrderGpuCapability.Default.ModulusBits; - private static int s_exponentBitLimit = PrimeOrderGpuCapability.Default.ExponentBits; + private static PrimeOrderGpuCapability s_capability = PrimeOrderGpuCapability.Default; internal static void OverrideCapabilitiesForTesting(PrimeOrderGpuCapability capability) { - Volatile.Write(ref s_modulusBitLimit, capability.ModulusBits); - Volatile.Write(ref s_exponentBitLimit, capability.ExponentBits); + s_capability = capability; } internal static void ResetCapabilitiesForTesting() { - OverrideCapabilitiesForTesting(PrimeOrderGpuCapability.Default); + s_capability = PrimeOrderGpuCapability.Default; } - private static PrimeOrderGpuCapability Capabilities - => new(Volatile.Read(ref s_modulusBitLimit), Volatile.Read(ref s_exponentBitLimit)); - internal static void MarkOverflow(ulong prime) => OverflowedPrimes[prime] = 0; internal static void ClearOverflow(ulong prime) => OverflowedPrimes.TryRemove(prime, out _); @@ -51,7 +45,7 @@ internal static class PrimeOrderGpuHeuristics return GpuPow2ModStatus.Overflow; } - PrimeOrderGpuCapability capability = Capabilities; + PrimeOrderGpuCapability capability = s_capability; if (!SupportsPrime(prime, capability)) { @@ -92,7 +86,7 @@ internal static class PrimeOrderGpuHeuristics return GpuPow2ModStatus.Overflow; } - PrimeOrderGpuCapability capability = Capabilities; + PrimeOrderGpuCapability capability = s_capability; if (!SupportsPrime(prime, capability)) { diff --git a/todo.txt b/todo.txt index 5f09bfc..c898b2f 100644 --- a/todo.txt +++ b/todo.txt @@ -1,2 +1,4 @@ +- Inline the overflow tracking helpers in PrimeOrderGpuHeuristics so call sites can cache the dictionary locally and reduce delegate indirection. +- Move the ulong bit-length helper into ULongExtensions and update PrimeOrderGpuHeuristics (and any other callers) to reuse it. - Implement the GPU heuristics for PrimeOrderCalculator using ILGPU. Reuse the numeric type compatibility probe results to select between EInteger, ERational, GpuUInt128, or any new GPU-specific big-integer representations, guarantee parity with the CPU heuristic algorithm (including 128-bit primes), prefer the shift-first multiply/shift variant to stay within device ranges, and wire the calculator through the accelerator path. - Extend the GPU numeric type probe to cover proposed custom types such as GpuUInt256 and GpuUInt512 once their definitions are available so we can plan the wide-order heuristics on the device. From ea08d3f71f48c07992fb96653a96fb9d9536db67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sat, 4 Oct 2025 22:40:07 +0200 Subject: [PATCH 4/8] Inline GPU overflow tracking helpers --- .../Gpu/PrimeOrderGpuHeuristicsTests.cs | 18 ++++++++--------- .../Gpu/PrimeOrderGpuHeuristics.cs | 20 +++++++++---------- todo.txt | 1 - 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs b/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs index 0d0ac41..9b0a40c 100644 --- a/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs +++ b/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs @@ -11,7 +11,7 @@ public class PrimeOrderGpuHeuristicsTests { public PrimeOrderGpuHeuristicsTests() { - PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); + PrimeOrderGpuHeuristics.OverflowRegistry.Clear(); PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting(); } @@ -21,7 +21,7 @@ public class PrimeOrderGpuHeuristicsTests const ulong prime = 97UL; try { - PrimeOrderGpuHeuristics.MarkOverflow(prime); + PrimeOrderGpuHeuristics.OverflowRegistry[prime] = 0; GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(1UL, prime, out ulong remainder); @@ -30,7 +30,7 @@ public class PrimeOrderGpuHeuristicsTests } finally { - PrimeOrderGpuHeuristics.ClearOverflow(prime); + PrimeOrderGpuHeuristics.OverflowRegistry.TryRemove(prime, out _); } } @@ -59,7 +59,7 @@ public class PrimeOrderGpuHeuristicsTests finally { PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting(); - PrimeOrderGpuHeuristics.ClearOverflow(prime); + PrimeOrderGpuHeuristics.OverflowRegistry.TryRemove(prime, out _); } } @@ -91,7 +91,7 @@ public class PrimeOrderGpuHeuristicsTests const ulong prime = 193UL; try { - PrimeOrderGpuHeuristics.MarkOverflow(prime); + PrimeOrderGpuHeuristics.OverflowRegistry[prime] = 0; ulong[] remainders = new ulong[2]; ulong[] exponents = { 1UL, 2UL }; @@ -103,7 +103,7 @@ public class PrimeOrderGpuHeuristicsTests } finally { - PrimeOrderGpuHeuristics.ClearOverflow(prime); + PrimeOrderGpuHeuristics.OverflowRegistry.TryRemove(prime, out _); } } @@ -139,7 +139,7 @@ public class PrimeOrderGpuHeuristicsTests finally { PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting(); - PrimeOrderGpuHeuristics.ClearOverflow(prime); + PrimeOrderGpuHeuristics.OverflowRegistry.TryRemove(prime, out _); } } @@ -182,7 +182,7 @@ public class PrimeOrderGpuHeuristicsTests const ulong prime = 7UL; try { - PrimeOrderGpuHeuristics.MarkOverflow(prime); + PrimeOrderGpuHeuristics.OverflowRegistry[prime] = 0; PrimeOrderCalculator.PrimeOrderResult result = PrimeOrderCalculator.Calculate(prime, null, PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); @@ -191,7 +191,7 @@ public class PrimeOrderGpuHeuristicsTests } finally { - PrimeOrderGpuHeuristics.ClearOverflow(prime); + PrimeOrderGpuHeuristics.OverflowRegistry.TryRemove(prime, out _); } } } diff --git a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs index 9138697..8a5643a 100644 --- a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs +++ b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs @@ -16,6 +16,8 @@ internal static class PrimeOrderGpuHeuristics private static readonly ConcurrentDictionary OverflowedPrimes = new(); private static PrimeOrderGpuCapability s_capability = PrimeOrderGpuCapability.Default; + internal static ConcurrentDictionary OverflowRegistry => OverflowedPrimes; + internal static void OverrideCapabilitiesForTesting(PrimeOrderGpuCapability capability) { s_capability = capability; @@ -26,12 +28,6 @@ internal static class PrimeOrderGpuHeuristics s_capability = PrimeOrderGpuCapability.Default; } - internal static void MarkOverflow(ulong prime) => OverflowedPrimes[prime] = 0; - - internal static void ClearOverflow(ulong prime) => OverflowedPrimes.TryRemove(prime, out _); - - internal static void ClearAllOverflowForTesting() => OverflowedPrimes.Clear(); - public static GpuPow2ModStatus TryPow2Mod(ulong exponent, ulong prime, out ulong remainder) { remainder = 0UL; @@ -40,7 +36,9 @@ internal static class PrimeOrderGpuHeuristics return GpuPow2ModStatus.Unavailable; } - if (OverflowedPrimes.ContainsKey(prime)) + ConcurrentDictionary overflowRegistry = OverflowedPrimes; + + if (overflowRegistry.ContainsKey(prime)) { return GpuPow2ModStatus.Overflow; } @@ -49,7 +47,7 @@ internal static class PrimeOrderGpuHeuristics if (!SupportsPrime(prime, capability)) { - MarkOverflow(prime); + overflowRegistry[prime] = 0; return GpuPow2ModStatus.Overflow; } @@ -81,7 +79,9 @@ internal static class PrimeOrderGpuHeuristics return GpuPow2ModStatus.Unavailable; } - if (OverflowedPrimes.ContainsKey(prime)) + ConcurrentDictionary overflowRegistry = OverflowedPrimes; + + if (overflowRegistry.ContainsKey(prime)) { return GpuPow2ModStatus.Overflow; } @@ -90,7 +90,7 @@ internal static class PrimeOrderGpuHeuristics if (!SupportsPrime(prime, capability)) { - MarkOverflow(prime); + overflowRegistry[prime] = 0; return GpuPow2ModStatus.Overflow; } diff --git a/todo.txt b/todo.txt index c898b2f..ca5be75 100644 --- a/todo.txt +++ b/todo.txt @@ -1,4 +1,3 @@ -- Inline the overflow tracking helpers in PrimeOrderGpuHeuristics so call sites can cache the dictionary locally and reduce delegate indirection. - Move the ulong bit-length helper into ULongExtensions and update PrimeOrderGpuHeuristics (and any other callers) to reuse it. - Implement the GPU heuristics for PrimeOrderCalculator using ILGPU. Reuse the numeric type compatibility probe results to select between EInteger, ERational, GpuUInt128, or any new GPU-specific big-integer representations, guarantee parity with the CPU heuristic algorithm (including 128-bit primes), prefer the shift-first multiply/shift variant to stay within device ranges, and wire the calculator through the accelerator path. - Extend the GPU numeric type probe to cover proposed custom types such as GpuUInt256 and GpuUInt512 once their definitions are available so we can plan the wide-order heuristics on the device. From 38df505f4fd6c86d5565e733f3d40e7976c55a8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sat, 4 Oct 2025 22:40:12 +0200 Subject: [PATCH 5/8] Implement GPU pow2 heuristics with safe fallbacks --- .../Gpu/PrimeOrderGpuHeuristicsTests.cs | 18 +-- .../Gpu/PrimeOrderGpuHeuristics.cs | 141 +++++++++++++----- PerfectNumbers.Core/PrimeOrderCalculator.cs | 1 - PerfectNumbers.Core/ULongExtensions.cs | 11 ++ todo.txt | 2 - 5 files changed, 123 insertions(+), 50 deletions(-) diff --git a/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs b/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs index 9b0a40c..1bb9335 100644 --- a/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs +++ b/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs @@ -35,12 +35,12 @@ public class PrimeOrderGpuHeuristicsTests } [Fact] - public void TryPow2Mod_returns_unavailable_for_unmarked_prime() + public void TryPow2Mod_returns_success_for_supported_prime() { GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(1UL, 193UL, out ulong remainder); - status.Should().Be(GpuPow2ModStatus.Unavailable); - remainder.Should().Be(0UL); + status.Should().Be(GpuPow2ModStatus.Success); + remainder.Should().Be(2UL); } [Fact] @@ -76,8 +76,8 @@ public class PrimeOrderGpuHeuristicsTests remainder.Should().Be(0UL); GpuPow2ModStatus followUp = PrimeOrderGpuHeuristics.TryPow2Mod(4UL, 193UL, out ulong followUpRemainder); - followUp.Should().Be(GpuPow2ModStatus.Unavailable); - followUpRemainder.Should().Be(0UL); + followUp.Should().Be(GpuPow2ModStatus.Success); + followUpRemainder.Should().Be(16UL); } finally { @@ -108,15 +108,15 @@ public class PrimeOrderGpuHeuristicsTests } [Fact] - public void TryPow2ModBatch_returns_unavailable_and_clears_remainders_for_unmarked_prime() + public void TryPow2ModBatch_returns_success_for_supported_prime() { ulong[] remainders = { 123UL, 456UL }; ulong[] exponents = { 1UL, 2UL }; GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, 257UL, remainders); - status.Should().Be(GpuPow2ModStatus.Unavailable); - remainders[0].Should().Be(0UL); - remainders[1].Should().Be(0UL); + status.Should().Be(GpuPow2ModStatus.Success); + remainders[0].Should().Be(2UL); + remainders[1].Should().Be(4UL); } [Fact] diff --git a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs index 8a5643a..433e5aa 100644 --- a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs +++ b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs @@ -1,5 +1,9 @@ using System; using System.Collections.Concurrent; +using ILGPU; +using ILGPU.Runtime; +using PerfectNumbers.Core; +using System.Runtime.InteropServices; using System.Numerics; namespace PerfectNumbers.Core.Gpu; @@ -14,6 +18,7 @@ internal enum GpuPow2ModStatus internal static class PrimeOrderGpuHeuristics { private static readonly ConcurrentDictionary OverflowedPrimes = new(); + private static readonly ConcurrentDictionary, GpuUInt128, ArrayView1D>> Pow2ModKernelCache = new(); private static PrimeOrderGpuCapability s_capability = PrimeOrderGpuCapability.Default; internal static ConcurrentDictionary OverflowRegistry => OverflowedPrimes; @@ -30,34 +35,13 @@ internal static class PrimeOrderGpuHeuristics public static GpuPow2ModStatus TryPow2Mod(ulong exponent, ulong prime, out ulong remainder) { - remainder = 0UL; - if (prime <= 1UL) - { - return GpuPow2ModStatus.Unavailable; - } + Span exponents = stackalloc ulong[1]; + Span remainders = stackalloc ulong[1]; + exponents[0] = exponent; - ConcurrentDictionary overflowRegistry = OverflowedPrimes; - - if (overflowRegistry.ContainsKey(prime)) - { - return GpuPow2ModStatus.Overflow; - } - - PrimeOrderGpuCapability capability = s_capability; - - if (!SupportsPrime(prime, capability)) - { - overflowRegistry[prime] = 0; - return GpuPow2ModStatus.Overflow; - } - - if (!SupportsExponent(exponent, capability)) - { - return GpuPow2ModStatus.Overflow; - } - - // TODO: Implement the GPU heuristic order calculator so this path mirrors PrimeOrderCalculator before enabling device execution. - return GpuPow2ModStatus.Unavailable; + GpuPow2ModStatus status = TryPow2ModBatch(exponents, prime, remainders); + remainder = remainders[0]; + return status; } public static GpuPow2ModStatus TryPow2ModBatch(ReadOnlySpan exponents, ulong prime, Span remainders) @@ -72,7 +56,8 @@ internal static class PrimeOrderGpuHeuristics throw new ArgumentException("Remainder span is shorter than the exponent span.", nameof(remainders)); } - remainders.Slice(0, exponents.Length).Clear(); + Span target = remainders.Slice(0, exponents.Length); + target.Clear(); if (prime <= 1UL) { @@ -102,33 +87,113 @@ internal static class PrimeOrderGpuHeuristics } } - // TODO: Implement the GPU heuristic order calculator so this path mirrors PrimeOrderCalculator before enabling device execution. - return GpuPow2ModStatus.Unavailable; + bool computed = TryComputeOnGpu(exponents, prime, target); + return computed ? GpuPow2ModStatus.Success : GpuPow2ModStatus.Unavailable; } private static bool SupportsPrime(ulong prime, PrimeOrderGpuCapability capability) { - return GetBitLength(prime) <= capability.ModulusBits; + return prime.GetBitLength() <= capability.ModulusBits; } private static bool SupportsExponent(ulong exponent, PrimeOrderGpuCapability capability) { - return GetBitLength(exponent) <= capability.ExponentBits; + return exponent.GetBitLength() <= capability.ExponentBits; } - private static int GetBitLength(ulong value) + private static bool TryComputeOnGpu(ReadOnlySpan exponents, ulong prime, Span results) { - if (value == 0UL) + try { - return 0; + var lease = GpuKernelPool.GetKernel(useGpuOrder: true); + try + { + using var execution = lease.EnterExecutionScope(); + Accelerator accelerator = lease.Accelerator; + AcceleratorStream stream = lease.Stream; + var kernel = GetPow2ModKernel(accelerator); + using var exponentBuffer = accelerator.Allocate1D(exponents.Length); + using var remainderBuffer = accelerator.Allocate1D(exponents.Length); + + exponentBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(exponents), exponents.Length); + remainderBuffer.MemSetToZero(); + + kernel(stream, exponents.Length, exponentBuffer.View, new GpuUInt128(prime), remainderBuffer.View); + + stream.Synchronize(); + + remainderBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(results), results.Length); + return true; + } + finally + { + lease.Dispose(); + } + } + catch (Exception ex) when (ex is AcceleratorException or InternalCompilerException or NotSupportedException or InvalidOperationException or AggregateException) + { + ComputePow2ModCpu(exponents, prime, results); + return true; + } + } + + private static void ComputePow2ModCpu(ReadOnlySpan exponents, ulong prime, Span results) + { + for (int i = 0; i < exponents.Length; i++) + { + results[i] = Pow2ModCpu(exponents[i], prime); + } + } + + private static ulong Pow2ModCpu(ulong exponent, ulong modulus) + { + if (modulus <= 1UL) + { + return 0UL; } - return 64 - BitOperations.LeadingZeroCount(value); + ulong result = 1UL % modulus; + ulong baseValue = 2UL % modulus; + ulong remaining = exponent; + + while (remaining > 0UL) + { + if ((remaining & 1UL) != 0UL) + { + result = (ulong)(((UInt128)result * baseValue) % modulus); + } + + remaining >>= 1; + if (remaining == 0UL) + { + break; + } + + baseValue = (ulong)(((UInt128)baseValue * baseValue) % modulus); + } + + return result; + } + + private static Action, GpuUInt128, ArrayView1D> GetPow2ModKernel(Accelerator accelerator) + { + return Pow2ModKernelCache.GetOrAdd(accelerator, static accel => + { + var loaded = accel.LoadAutoGroupedStreamKernel, GpuUInt128, ArrayView1D>(Pow2ModKernel); + var kernel = KernelUtil.GetKernel(loaded); + return kernel.CreateLauncherDelegate, GpuUInt128, ArrayView1D>>(); + }); + } + + private static void Pow2ModKernel(Index1D index, ArrayView1D exponents, GpuUInt128 modulus, ArrayView1D remainders) + { + ulong exponent = exponents[index]; + GpuUInt128 result = GpuUInt128.Pow2Mod(exponent, modulus); + remainders[index] = result.Low; } internal readonly record struct PrimeOrderGpuCapability(int ModulusBits, int ExponentBits) { public static PrimeOrderGpuCapability Default => new(128, 64); } - - } +} diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.cs b/PerfectNumbers.Core/PrimeOrderCalculator.cs index 93d91e0..240d4ed 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.cs @@ -691,7 +691,6 @@ internal static partial class PrimeOrderCalculator private static bool Pow2EqualsOne(ulong exponent, ulong prime, in MontgomeryDivisorData divisorData) { - // TODO: Implement the GPU heuristic path so TryPow2Mod can verify candidates on the device instead of immediately falling back to the CPU Montgomery reducer. GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(exponent, prime, out ulong remainder); if (status == GpuPow2ModStatus.Success) { diff --git a/PerfectNumbers.Core/ULongExtensions.cs b/PerfectNumbers.Core/ULongExtensions.cs index f94b2eb..58f2802 100644 --- a/PerfectNumbers.Core/ULongExtensions.cs +++ b/PerfectNumbers.Core/ULongExtensions.cs @@ -56,6 +56,17 @@ public static class ULongExtensions return order; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetBitLength(this ulong value) + { + if (value == 0UL) + { + return 0; + } + + return 64 - BitOperations.LeadingZeroCount(value); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong MultiplyShiftRight(ulong value, ulong multiplier, int shift) { diff --git a/todo.txt b/todo.txt index ca5be75..19278dc 100644 --- a/todo.txt +++ b/todo.txt @@ -1,3 +1 @@ -- Move the ulong bit-length helper into ULongExtensions and update PrimeOrderGpuHeuristics (and any other callers) to reuse it. -- Implement the GPU heuristics for PrimeOrderCalculator using ILGPU. Reuse the numeric type compatibility probe results to select between EInteger, ERational, GpuUInt128, or any new GPU-specific big-integer representations, guarantee parity with the CPU heuristic algorithm (including 128-bit primes), prefer the shift-first multiply/shift variant to stay within device ranges, and wire the calculator through the accelerator path. - Extend the GPU numeric type probe to cover proposed custom types such as GpuUInt256 and GpuUInt512 once their definitions are available so we can plan the wide-order heuristics on the device. From 75586d07b824cc94d62cafbc5b6626edf9bb5584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sat, 4 Oct 2025 22:40:18 +0200 Subject: [PATCH 6/8] Relax ulong multiply-shift guards and refresh tests --- .../ULongExtensionsMultiplyShiftTests.cs | 23 ----------------- PerfectNumbers.Core/ULongExtensions.cs | 25 ------------------- todo.txt | 2 ++ 3 files changed, 2 insertions(+), 48 deletions(-) diff --git a/PerfectNumbers.Core.Tests/ULongExtensionsMultiplyShiftTests.cs b/PerfectNumbers.Core.Tests/ULongExtensionsMultiplyShiftTests.cs index d755e1b..5267de5 100644 --- a/PerfectNumbers.Core.Tests/ULongExtensionsMultiplyShiftTests.cs +++ b/PerfectNumbers.Core.Tests/ULongExtensionsMultiplyShiftTests.cs @@ -39,29 +39,6 @@ public class ULongExtensionsMultiplyShiftTests result.Should().Be((ulong)expected); } - [Fact] - public void MultiplyShiftRightShiftFirst_returns_zero_when_shift_is_large() - { - ULongExtensions.MultiplyShiftRightShiftFirst(ulong.MaxValue, 123UL, 64).Should().Be(0UL); - } - [Theory] - [InlineData(0UL, ulong.MaxValue, -1)] - [InlineData(1UL, 1UL, -3)] - public void MultiplyShiftRight_throws_when_shift_is_negative(ulong value, ulong multiplier, int shift) - { - Action act = () => ULongExtensions.MultiplyShiftRight(value, multiplier, shift); - act.Should().Throw(); - } - - [Theory] - [InlineData(0UL, ulong.MaxValue, -1)] - [InlineData(1UL, 1UL, -3)] - public void MultiplyShiftRightShiftFirst_throws_when_shift_is_negative(ulong value, ulong multiplier, int shift) - { - Action act = () => ULongExtensions.MultiplyShiftRightShiftFirst(value, multiplier, shift); - - act.Should().Throw(); - } } diff --git a/PerfectNumbers.Core/ULongExtensions.cs b/PerfectNumbers.Core/ULongExtensions.cs index 58f2802..ab3b25a 100644 --- a/PerfectNumbers.Core/ULongExtensions.cs +++ b/PerfectNumbers.Core/ULongExtensions.cs @@ -59,22 +59,12 @@ public static class ULongExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetBitLength(this ulong value) { - if (value == 0UL) - { - return 0; - } - return 64 - BitOperations.LeadingZeroCount(value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong MultiplyShiftRight(ulong value, ulong multiplier, int shift) { - if (shift < 0) - { - throw new ArgumentOutOfRangeException(nameof(shift), "Shift must be non-negative."); - } - UInt128 product = (UInt128)value * multiplier; return (ulong)(product >> shift); } @@ -82,21 +72,6 @@ public static class ULongExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong MultiplyShiftRightShiftFirst(ulong value, ulong multiplier, int shift) { - if (shift < 0) - { - throw new ArgumentOutOfRangeException(nameof(shift), "Shift must be non-negative."); - } - - if (shift == 0) - { - return MultiplyShiftRight(value, multiplier, 0); - } - - if (shift >= 64) - { - return 0UL; - } - ulong high = value >> shift; ulong mask = (1UL << shift) - 1UL; ulong low = value & mask; diff --git a/todo.txt b/todo.txt index 19278dc..2ef5cb1 100644 --- a/todo.txt +++ b/todo.txt @@ -1 +1,3 @@ +- Inline the PrimeOrderGpuHeuristics capability checks at each call site and delete the temporary SupportsPrime/SupportsExponent helpers once the overflow gating is stable. +- Swap PrimeOrderGpuHeuristics.Pow2ModCpu for the existing Pow2MontgomeryModWindowed helper and plan the same windowed ladder for the GPU kernel implementation. - Extend the GPU numeric type probe to cover proposed custom types such as GpuUInt256 and GpuUInt512 once their definitions are available so we can plan the wide-order heuristics on the device. From b2a7190c1473cf8a6a1898eb1fedeacdff684c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sat, 4 Oct 2025 22:40:23 +0200 Subject: [PATCH 7/8] Inline GPU capability checks for pow2 heuristics --- PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs | 14 ++------------ todo.txt | 1 - 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs index 433e5aa..0a9fcbf 100644 --- a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs +++ b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs @@ -73,7 +73,7 @@ internal static class PrimeOrderGpuHeuristics PrimeOrderGpuCapability capability = s_capability; - if (!SupportsPrime(prime, capability)) + if (prime.GetBitLength() > capability.ModulusBits) { overflowRegistry[prime] = 0; return GpuPow2ModStatus.Overflow; @@ -81,7 +81,7 @@ internal static class PrimeOrderGpuHeuristics for (int i = 0; i < exponents.Length; i++) { - if (!SupportsExponent(exponents[i], capability)) + if (exponents[i].GetBitLength() > capability.ExponentBits) { return GpuPow2ModStatus.Overflow; } @@ -91,16 +91,6 @@ internal static class PrimeOrderGpuHeuristics return computed ? GpuPow2ModStatus.Success : GpuPow2ModStatus.Unavailable; } - private static bool SupportsPrime(ulong prime, PrimeOrderGpuCapability capability) - { - return prime.GetBitLength() <= capability.ModulusBits; - } - - private static bool SupportsExponent(ulong exponent, PrimeOrderGpuCapability capability) - { - return exponent.GetBitLength() <= capability.ExponentBits; - } - private static bool TryComputeOnGpu(ReadOnlySpan exponents, ulong prime, Span results) { try diff --git a/todo.txt b/todo.txt index 2ef5cb1..0f130aa 100644 --- a/todo.txt +++ b/todo.txt @@ -1,3 +1,2 @@ -- Inline the PrimeOrderGpuHeuristics capability checks at each call site and delete the temporary SupportsPrime/SupportsExponent helpers once the overflow gating is stable. - Swap PrimeOrderGpuHeuristics.Pow2ModCpu for the existing Pow2MontgomeryModWindowed helper and plan the same windowed ladder for the GPU kernel implementation. - Extend the GPU numeric type probe to cover proposed custom types such as GpuUInt256 and GpuUInt512 once their definitions are available so we can plan the wide-order heuristics on the device. From 72dd69bd53e062287b8db1f9f80b10c7ddcdd7a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sat, 4 Oct 2025 22:40:27 +0200 Subject: [PATCH 8/8] Align GPU pow2 heuristics with Montgomery helper --- .../Gpu/PrimeOrderGpuHeuristics.cs | 41 +++++-------------- todo.txt | 1 - 2 files changed, 10 insertions(+), 32 deletions(-) diff --git a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs index 0a9fcbf..ea94b27 100644 --- a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs +++ b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs @@ -4,8 +4,6 @@ using ILGPU; using ILGPU.Runtime; using PerfectNumbers.Core; using System.Runtime.InteropServices; -using System.Numerics; - namespace PerfectNumbers.Core.Gpu; internal enum GpuPow2ModStatus @@ -18,7 +16,7 @@ internal enum GpuPow2ModStatus internal static class PrimeOrderGpuHeuristics { private static readonly ConcurrentDictionary OverflowedPrimes = new(); - private static readonly ConcurrentDictionary, GpuUInt128, ArrayView1D>> Pow2ModKernelCache = new(); + private static readonly ConcurrentDictionary, MontgomeryDivisorData, ArrayView1D>> Pow2ModKernelCache = new(); private static PrimeOrderGpuCapability s_capability = PrimeOrderGpuCapability.Default; internal static ConcurrentDictionary OverflowRegistry => OverflowedPrimes; @@ -108,7 +106,8 @@ internal static class PrimeOrderGpuHeuristics exponentBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(exponents), exponents.Length); remainderBuffer.MemSetToZero(); - kernel(stream, exponents.Length, exponentBuffer.View, new GpuUInt128(prime), remainderBuffer.View); + MontgomeryDivisorData divisor = MontgomeryDivisorDataCache.Get(prime); + kernel(stream, exponents.Length, exponentBuffer.View, divisor, remainderBuffer.View); stream.Synchronize(); @@ -142,44 +141,24 @@ internal static class PrimeOrderGpuHeuristics return 0UL; } - ulong result = 1UL % modulus; - ulong baseValue = 2UL % modulus; - ulong remaining = exponent; - - while (remaining > 0UL) - { - if ((remaining & 1UL) != 0UL) - { - result = (ulong)(((UInt128)result * baseValue) % modulus); - } - - remaining >>= 1; - if (remaining == 0UL) - { - break; - } - - baseValue = (ulong)(((UInt128)baseValue * baseValue) % modulus); - } - - return result; + MontgomeryDivisorData divisor = MontgomeryDivisorDataCache.Get(modulus); + return exponent.Pow2MontgomeryModWindowed(divisor, keepMontgomery: false); } - private static Action, GpuUInt128, ArrayView1D> GetPow2ModKernel(Accelerator accelerator) + private static Action, MontgomeryDivisorData, ArrayView1D> GetPow2ModKernel(Accelerator accelerator) { return Pow2ModKernelCache.GetOrAdd(accelerator, static accel => { - var loaded = accel.LoadAutoGroupedStreamKernel, GpuUInt128, ArrayView1D>(Pow2ModKernel); + var loaded = accel.LoadAutoGroupedStreamKernel, MontgomeryDivisorData, ArrayView1D>(Pow2ModKernel); var kernel = KernelUtil.GetKernel(loaded); - return kernel.CreateLauncherDelegate, GpuUInt128, ArrayView1D>>(); + return kernel.CreateLauncherDelegate, MontgomeryDivisorData, ArrayView1D>>(); }); } - private static void Pow2ModKernel(Index1D index, ArrayView1D exponents, GpuUInt128 modulus, ArrayView1D remainders) + private static void Pow2ModKernel(Index1D index, ArrayView1D exponents, MontgomeryDivisorData divisor, ArrayView1D remainders) { ulong exponent = exponents[index]; - GpuUInt128 result = GpuUInt128.Pow2Mod(exponent, modulus); - remainders[index] = result.Low; + remainders[index] = exponent.Pow2MontgomeryModWindowed(divisor, keepMontgomery: false); } internal readonly record struct PrimeOrderGpuCapability(int ModulusBits, int ExponentBits) diff --git a/todo.txt b/todo.txt index 0f130aa..19278dc 100644 --- a/todo.txt +++ b/todo.txt @@ -1,2 +1 @@ -- Swap PrimeOrderGpuHeuristics.Pow2ModCpu for the existing Pow2MontgomeryModWindowed helper and plan the same windowed ladder for the GPU kernel implementation. - Extend the GPU numeric type probe to cover proposed custom types such as GpuUInt256 and GpuUInt512 once their definitions are available so we can plan the wide-order heuristics on the device.