diff --git a/PerfectNumbers.Core.Tests/HeuristicCombinedPrimeTesterTests.cs b/PerfectNumbers.Core.Tests/HeuristicCombinedPrimeTesterTests.cs index 2d76983..d3d2051 100644 --- a/PerfectNumbers.Core.Tests/HeuristicCombinedPrimeTesterTests.cs +++ b/PerfectNumbers.Core.Tests/HeuristicCombinedPrimeTesterTests.cs @@ -19,7 +19,7 @@ public class HeuristicCombinedPrimeTesterTests Span groupBState = stackalloc HeuristicCombinedPrimeTester.HeuristicGroupBSequenceState[4]; - var enumerator = HeuristicCombinedPrimeTester.CreateHeuristicDivisorEnumerator(500, lastDigit, groupBState); + var enumerator = HeuristicCombinedPrimeTester.CreateHeuristicDivisorEnumerator(500UL * 500UL, lastDigit, groupBState); var results = new List(32); while (results.Count < 20 && enumerator.TryGetNext(out var candidate)) @@ -230,14 +230,10 @@ public class HeuristicCombinedPrimeTesterTests [InlineData(18_446_744_073_709_551_615UL)] [InlineData(18_446_744_073_709_551_614UL)] [InlineData(18_446_744_073_709_551_613UL)] - public void ComputeHeuristicSqrt_returns_floor_square_root(ulong value) + public void ComputeHeuristicDivisorSquareLimit_returns_input_value(ulong value) { - ulong sqrt = HeuristicCombinedPrimeTester.ComputeHeuristicSqrt(value); + ulong limit = HeuristicCombinedPrimeTester.ComputeHeuristicDivisorSquareLimit(value); - bool squareBelowOrEqual = (UInt128)sqrt * sqrt <= (UInt128)value; - bool nextSquareAbove = (UInt128)(sqrt + 1UL) * (UInt128)(sqrt + 1UL) > (UInt128)value; - - squareBelowOrEqual.Should().BeTrue(); - nextSquareAbove.Should().BeTrue(); + limit.Should().Be(value); } } diff --git a/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs b/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs index a713bb5..5aea0f8 100644 --- a/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs @@ -152,36 +152,36 @@ public sealed class HeuristicCombinedPrimeTester public static bool IsPrimeCpu(ulong n) { byte nMod10 = (byte)n.Mod10(); - ulong sqrtLimit = ComputeHeuristicSqrt(n); + ulong maxDivisorSquare = ComputeHeuristicDivisorSquareLimit(n); - if (sqrtLimit < 3UL) + if (maxDivisorSquare < 9UL) { return EvaluateWithOpenNumericFallback(n); } - return HeuristicTrialDivisionCpu(n, sqrtLimit, nMod10); + return HeuristicTrialDivisionCpu(n, maxDivisorSquare, nMod10); } public static bool IsPrimeGpu(ulong n) { byte nMod10 = (byte)n.Mod10(); - ulong sqrtLimit = ComputeHeuristicSqrt(n); - return IsPrimeGpu(n, sqrtLimit, nMod10); + ulong maxDivisorSquare = ComputeHeuristicDivisorSquareLimit(n); + return IsPrimeGpu(n, maxDivisorSquare, nMod10); } - public static bool IsPrimeGpu(ulong n, ulong sqrtLimit, byte nMod10) + public static bool IsPrimeGpu(ulong n, ulong maxDivisorSquare, byte nMod10) { // TODO: Is this condition ever met on the execution path in EvenPerfectBitScanner? - if (sqrtLimit < 3UL) + if (maxDivisorSquare < 9UL) { return EvaluateWithOpenNumericFallback(n); } - bool compositeDetected = HeuristicTrialDivisionGpuDetectsDivisor(n, sqrtLimit, nMod10); + bool compositeDetected = HeuristicTrialDivisionGpuDetectsDivisor(n, maxDivisorSquare, nMod10); return !compositeDetected; } - private static bool HeuristicTrialDivisionCpu(ulong n, ulong sqrtLimit, byte nMod10) + private static bool HeuristicTrialDivisionCpu(ulong n, ulong maxDivisorSquare, byte nMod10) { ReadOnlySpan combinedDivisors = GetCombinedDivisors(nMod10); ReadOnlySpan combinedDivisorSquares = GetCombinedDivisorSquares(nMod10); @@ -191,7 +191,7 @@ public sealed class HeuristicCombinedPrimeTester for (int i = 0; i < length; i++) { ulong divisorSquare = combinedDivisorSquares[i]; - if (divisorSquare > n) + if (divisorSquare > maxDivisorSquare) { break; } @@ -205,7 +205,7 @@ public sealed class HeuristicCombinedPrimeTester return EvaluateWithOpenNumericFallback(n); } - private static bool HeuristicTrialDivisionGpuDetectsDivisor(ulong n, ulong sqrtLimit, byte nMod10) + private static bool HeuristicTrialDivisionGpuDetectsDivisor(ulong n, ulong maxDivisorSquare, byte nMod10) { int batchCapacity = HeuristicGpuDivisorBatchSize; var divisorPool = ThreadStaticPools.UlongPool; @@ -243,7 +243,7 @@ public sealed class HeuristicCombinedPrimeTester for (int i = 0; i < length; i++) { ulong divisorSquare = combinedDivisorSquares[i]; - if (divisorSquare > n) + if (divisorSquare > maxDivisorSquare) { break; } @@ -282,29 +282,10 @@ Cleanup: { return Prime.Numbers.IsPrime(n); } - - internal static ulong ComputeHeuristicSqrt(ulong n) - { - ulong sqrt = (ulong)Math.Sqrt(n); - UInt128 square = (UInt128)sqrt * sqrt; - - while (square > n) - { - sqrt--; - square = (UInt128)sqrt * sqrt; - } - - ulong next = sqrt + 1UL; - UInt128 nextSquare = (UInt128)next * next; - while (nextSquare <= n) - { - sqrt = next; - next++; - nextSquare = (UInt128)next * next; - } - - return sqrt; - } + internal static ulong ComputeHeuristicDivisorSquareLimit(ulong n) + { + return n; + } private static ReadOnlySpan GetCombinedDivisors(byte nMod10) { @@ -514,9 +495,9 @@ Cleanup: }; [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static HeuristicDivisorEnumerator CreateHeuristicDivisorEnumerator(ulong sqrtLimit, byte nMod10, Span groupBBuffer) + internal static HeuristicDivisorEnumerator CreateHeuristicDivisorEnumerator(ulong maxDivisorSquare, byte nMod10, Span groupBBuffer) { - return new HeuristicDivisorEnumerator(sqrtLimit, nMod10, groupBBuffer); + return new HeuristicDivisorEnumerator(maxDivisorSquare, nMod10, groupBBuffer); } internal struct HeuristicGroupBSequenceState @@ -597,44 +578,50 @@ Cleanup: } internal ref struct HeuristicDivisorEnumerator - { - private readonly ulong sqrtLimit; - private readonly ReadOnlySpan combinedDivisors; - private int index; + { + private readonly ulong maxDivisorSquare; + private readonly ReadOnlySpan combinedDivisors; + private readonly ReadOnlySpan combinedDivisorSquares; + private int index; - public HeuristicDivisorEnumerator(ulong sqrtLimit, byte nMod10, Span groupBBuffer) - { - this.sqrtLimit = sqrtLimit; - _ = groupBBuffer; - combinedDivisors = GetCombinedDivisors(nMod10); - index = 0; - } + public HeuristicDivisorEnumerator(ulong maxDivisorSquare, byte nMod10, Span groupBBuffer) + { + this.maxDivisorSquare = maxDivisorSquare; + _ = groupBBuffer; + combinedDivisors = GetCombinedDivisors(nMod10); + combinedDivisorSquares = GetCombinedDivisorSquares(nMod10); + index = 0; + } - public bool TryGetNext(out HeuristicDivisorCandidate candidate) - { - while (index < combinedDivisors.Length) - { - uint entry = combinedDivisors[index++]; - if (entry > sqrtLimit) - { - index = combinedDivisors.Length; - break; - } + public bool TryGetNext(out HeuristicDivisorCandidate candidate) + { + while (index < combinedDivisors.Length) + { + int currentIndex = index; + ulong divisorSquare = combinedDivisorSquares[currentIndex]; + if (divisorSquare > maxDivisorSquare) + { + index = combinedDivisors.Length; + break; + } - HeuristicDivisorGroup group = ResolveGroup(entry); - ulong value = entry; - byte ending = (byte)(value % 10UL); - ushort residue = (ushort)(value % Wheel210); - candidate = new HeuristicDivisorCandidate(value, group, ending, 0, residue); - return true; - } + uint entry = combinedDivisors[currentIndex]; + index = currentIndex + 1; - candidate = default; - return false; - } - } + HeuristicDivisorGroup group = ResolveGroup(entry); + ulong value = entry; + byte ending = (byte)(value % 10UL); + ushort residue = (ushort)(value % Wheel210); + candidate = new HeuristicDivisorCandidate(value, group, ending, 0, residue); + return true; + } - internal ref struct MersenneHeuristicDivisorEnumerator + candidate = default; + return false; + } + } + + internal ref struct MersenneHeuristicDivisorEnumerator { private readonly GpuUInt128 step; private readonly GpuUInt128 limit; diff --git a/PerfectNumbers.Core/HeuristicPrimeTester.cs b/PerfectNumbers.Core/HeuristicPrimeTester.cs index cdaf7e2..5da7eab 100644 --- a/PerfectNumbers.Core/HeuristicPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicPrimeTester.cs @@ -86,15 +86,15 @@ public sealed class HeuristicPrimeTester ct.ThrowIfCancellationRequested(); byte nMod10 = (byte)n.Mod10(); - ulong sqrtLimit = ComputeHeuristicSqrt(n); + ulong maxDivisorSquare = ComputeHeuristicDivisorSquareLimit(n); - if (sqrtLimit < 3UL) + if (maxDivisorSquare < 9UL) { return EvaluateWithOpenNumericFallback(n); } bool includeGroupB = UseHeuristicGroupBTrialDivision; - return HeuristicTrialDivisionCpu(n, sqrtLimit, nMod10, includeGroupB); + return HeuristicTrialDivisionCpu(n, maxDivisorSquare, nMod10, includeGroupB); } public bool IsPrimeCpu(ulong n) @@ -105,33 +105,33 @@ public sealed class HeuristicPrimeTester public bool IsPrimeGpu(ulong n) { byte nMod10 = (byte)n.Mod10(); - ulong sqrtLimit = ComputeHeuristicSqrt(n); - return IsPrimeGpu(n, sqrtLimit, nMod10); + ulong maxDivisorSquare = ComputeHeuristicDivisorSquareLimit(n); + return IsPrimeGpu(n, maxDivisorSquare, nMod10); } - public bool IsPrimeGpu(ulong n, ulong sqrtLimit, byte nMod10) + public bool IsPrimeGpu(ulong n, ulong maxDivisorSquare, byte nMod10) { - return HeuristicIsPrimeGpuCore(n, sqrtLimit, nMod10); + return HeuristicIsPrimeGpuCore(n, maxDivisorSquare, nMod10); } - private bool HeuristicIsPrimeGpuCore(ulong n, ulong sqrtLimit, byte nMod10) + private bool HeuristicIsPrimeGpuCore(ulong n, ulong maxDivisorSquare, byte nMod10) { // TODO: Is this condition ever met on the execution path in EvenPerfectBitScanner? - if (sqrtLimit < 3UL) + if (maxDivisorSquare < 9UL) { return EvaluateWithOpenNumericFallback(n); } // if (!UseHeuristicGroupBTrialDivision) // { - // return HeuristicTrialDivisionCpu(n, sqrtLimit, nMod10, includeGroupB: false); + // return HeuristicTrialDivisionCpu(n, maxDivisorSquare, nMod10, includeGroupB: false); // } - bool compositeDetected = HeuristicTrialDivisionGpuDetectsDivisor(n, sqrtLimit, nMod10); + bool compositeDetected = HeuristicTrialDivisionGpuDetectsDivisor(n, maxDivisorSquare, nMod10); return !compositeDetected; } - private static bool HeuristicTrialDivisionCpu(ulong n, ulong sqrtLimit, byte nMod10, bool includeGroupB) + private static bool HeuristicTrialDivisionCpu(ulong n, ulong maxDivisorSquare, byte nMod10, bool includeGroupB) { ReadOnlySpan groupADivisors = HeuristicPrimeSieves.GroupADivisors; int interleaveBatchSize = Math.Max(1, HeuristicDivisorInterleaveBatchSize); @@ -145,7 +145,7 @@ public sealed class HeuristicPrimeTester while (processed < interleaveBatchSize && groupAIndex < groupADivisors.Length) { ulong divisor = (ulong)groupADivisors[groupAIndex]; - if (divisor > sqrtLimit) + if (!DivisorWithinSquareLimit(divisor, maxDivisorSquare)) { groupAIndex = groupADivisors.Length; break; @@ -165,7 +165,7 @@ public sealed class HeuristicPrimeTester break; } - if ((ulong)groupADivisors[groupAIndex] > sqrtLimit) + if (!DivisorWithinSquareLimit((ulong)groupADivisors[groupAIndex], maxDivisorSquare)) { groupAIndex = groupADivisors.Length; break; @@ -185,8 +185,8 @@ public sealed class HeuristicPrimeTester ? stackalloc int[endingOrder.Length] : new int[endingOrder.Length]; - bool groupAHasMore = groupAIndex < groupADivisors.Length && (ulong)groupADivisors[groupAIndex] <= sqrtLimit; - bool groupBHasMore = HasGroupBCandidates(endingOrder, indices, sqrtLimit); + bool groupAHasMore = groupAIndex < groupADivisors.Length && DivisorWithinSquareLimit((ulong)groupADivisors[groupAIndex], maxDivisorSquare); + bool groupBHasMore = HasGroupBCandidates(endingOrder, indices, maxDivisorSquare); while (groupAHasMore || groupBHasMore) { @@ -196,7 +196,7 @@ public sealed class HeuristicPrimeTester while (processed < interleaveBatchSize && groupAIndex < groupADivisors.Length) { ulong divisor = (ulong)groupADivisors[groupAIndex]; - if (divisor > sqrtLimit) + if (!DivisorWithinSquareLimit(divisor, maxDivisorSquare)) { groupAIndex = groupADivisors.Length; groupAHasMore = false; @@ -218,7 +218,7 @@ public sealed class HeuristicPrimeTester } else { - groupAHasMore = (ulong)groupADivisors[groupAIndex] <= sqrtLimit; + groupAHasMore = DivisorWithinSquareLimit((ulong)groupADivisors[groupAIndex], maxDivisorSquare); if (!groupAHasMore) { groupAIndex = groupADivisors.Length; @@ -231,7 +231,7 @@ public sealed class HeuristicPrimeTester int processed = 0; while (processed < interleaveBatchSize) { - if (!TrySelectNextGroupBDivisor(endingOrder, indices, sqrtLimit, out ulong divisor)) + if (!TrySelectNextGroupBDivisor(endingOrder, indices, maxDivisorSquare, out ulong divisor)) { groupBHasMore = false; break; @@ -247,7 +247,7 @@ public sealed class HeuristicPrimeTester if (groupBHasMore) { - groupBHasMore = HasGroupBCandidates(endingOrder, indices, sqrtLimit); + groupBHasMore = HasGroupBCandidates(endingOrder, indices, maxDivisorSquare); } } } @@ -258,7 +258,7 @@ public sealed class HeuristicPrimeTester - private bool HeuristicTrialDivisionGpuDetectsDivisor(ulong n, ulong sqrtLimit, byte nMod10) + private bool HeuristicTrialDivisionGpuDetectsDivisor(ulong n, ulong maxDivisorSquare, byte nMod10) { int batchCapacity = HeuristicGpuDivisorBatchSize; int interleaveBatchSize = HeuristicDivisorInterleaveBatchSize; @@ -296,8 +296,8 @@ public sealed class HeuristicPrimeTester ? stackalloc int[endingOrder.Length] : new int[endingOrder.Length]; - bool groupAHasMore = groupAIndex < groupADivisors.Length && (ulong)groupADivisors[groupAIndex] <= sqrtLimit; - bool groupBHasMore = !endingOrder.IsEmpty && HasGroupBCandidates(endingOrder, indices, sqrtLimit); + bool groupAHasMore = groupAIndex < groupADivisors.Length && DivisorWithinSquareLimit((ulong)groupADivisors[groupAIndex], maxDivisorSquare); + bool groupBHasMore = !endingOrder.IsEmpty && HasGroupBCandidates(endingOrder, indices, maxDivisorSquare); while (!compositeDetected && (groupAHasMore || groupBHasMore)) { @@ -307,7 +307,7 @@ public sealed class HeuristicPrimeTester while (processed < interleaveBatchSize && groupAIndex < groupADivisors.Length) { ulong divisor = (ulong)groupADivisors[groupAIndex]; - if (divisor > sqrtLimit) + if (!DivisorWithinSquareLimit(divisor, maxDivisorSquare)) { groupAIndex = groupADivisors.Length; groupAHasMore = false; @@ -338,7 +338,7 @@ public sealed class HeuristicPrimeTester } else { - groupAHasMore = (ulong)groupADivisors[groupAIndex] <= sqrtLimit; + groupAHasMore = DivisorWithinSquareLimit((ulong)groupADivisors[groupAIndex], maxDivisorSquare); if (!groupAHasMore) { groupAIndex = groupADivisors.Length; @@ -351,7 +351,7 @@ public sealed class HeuristicPrimeTester int processed = 0; while (processed < interleaveBatchSize) { - if (!TrySelectNextGroupBDivisor(endingOrder, indices, sqrtLimit, out ulong divisor)) + if (!TrySelectNextGroupBDivisor(endingOrder, indices, maxDivisorSquare, out ulong divisor)) { groupBHasMore = false; break; @@ -376,7 +376,7 @@ public sealed class HeuristicPrimeTester if (groupBHasMore) { - groupBHasMore = HasGroupBCandidates(endingOrder, indices, sqrtLimit); + groupBHasMore = HasGroupBCandidates(endingOrder, indices, maxDivisorSquare); } } } @@ -399,28 +399,15 @@ Cleanup: { return Prime.Numbers.IsPrime(n); } - - internal static ulong ComputeHeuristicSqrt(ulong n) + internal static ulong ComputeHeuristicDivisorSquareLimit(ulong n) { - ulong sqrt = (ulong)Math.Sqrt(n); - UInt128 square = (UInt128)sqrt * sqrt; + return n; + } - while (square > n) - { - sqrt--; - square = (UInt128)sqrt * sqrt; - } - - ulong next = sqrt + 1UL; - UInt128 nextSquare = (UInt128)next * next; - while (nextSquare <= n) - { - sqrt = next; - next++; - nextSquare = (UInt128)next * next; - } - - return sqrt; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool DivisorWithinSquareLimit(ulong divisor, ulong maxDivisorSquare) + { + return divisor != 0UL && divisor <= maxDivisorSquare / divisor; } private static ReadOnlySpan GetGroupBEndingOrder(byte nMod10) => nMod10 switch @@ -434,7 +421,7 @@ Cleanup: [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TrySelectNextGroupBDivisor(ReadOnlySpan endingOrder, Span indices, ulong sqrtLimit, out ulong divisor) + private static bool TrySelectNextGroupBDivisor(ReadOnlySpan endingOrder, Span indices, ulong maxDivisorSquare, out ulong divisor) { ulong bestCandidate = ulong.MaxValue; int bestEndingIndex = -1; @@ -449,7 +436,7 @@ Cleanup: } ulong candidate = (ulong)divisors[index]; - if (candidate > sqrtLimit) + if (!DivisorWithinSquareLimit(candidate, maxDivisorSquare)) { indices[i] = divisors.Length; continue; @@ -474,7 +461,7 @@ Cleanup: } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool HasGroupBCandidates(ReadOnlySpan endingOrder, Span indices, ulong sqrtLimit) + private static bool HasGroupBCandidates(ReadOnlySpan endingOrder, Span indices, ulong maxDivisorSquare) { for (int i = 0; i < endingOrder.Length; i++) { @@ -486,7 +473,7 @@ Cleanup: } ulong candidate = (ulong)divisors[index]; - if (candidate > sqrtLimit) + if (!DivisorWithinSquareLimit(candidate, maxDivisorSquare)) { indices[i] = divisors.Length; continue; @@ -533,9 +520,9 @@ Cleanup: } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static HeuristicDivisorEnumerator CreateHeuristicDivisorEnumerator(ulong sqrtLimit, byte nMod10, Span groupBBuffer) + internal static HeuristicDivisorEnumerator CreateHeuristicDivisorEnumerator(ulong maxDivisorSquare, byte nMod10, Span groupBBuffer) { - return new HeuristicDivisorEnumerator(sqrtLimit, nMod10, groupBBuffer); + return new HeuristicDivisorEnumerator(maxDivisorSquare, nMod10, groupBBuffer); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -613,20 +600,20 @@ Cleanup: internal ref struct HeuristicDivisorEnumerator { - private readonly ulong sqrtLimit; + private readonly ulong maxDivisorSquare; private int groupAConstantIndex; private ulong groupAWheelCandidate; private int groupAIncrementIndex; private bool groupAWheelActive; private Span groupBStates; - public HeuristicDivisorEnumerator(ulong sqrtLimit, byte nMod10, Span groupBBuffer) + public HeuristicDivisorEnumerator(ulong maxDivisorSquare, byte nMod10, Span groupBBuffer) { - this.sqrtLimit = sqrtLimit; + this.maxDivisorSquare = maxDivisorSquare; groupAConstantIndex = 0; groupAWheelCandidate = 23UL; groupAIncrementIndex = 0; - groupAWheelActive = sqrtLimit >= 23UL; + groupAWheelActive = DivisorWithinSquareLimit(23UL, maxDivisorSquare); int count = InitializeGroupBStates(nMod10, groupBBuffer); groupBStates = count == 0 ? Span.Empty : groupBBuffer[..count]; @@ -643,7 +630,7 @@ Cleanup: while (groupAConstantIndex < GroupAConstantDivisors.Length) { ulong value = GroupAConstantDivisors[groupAConstantIndex++]; - if (value > sqrtLimit) + if (!DivisorWithinSquareLimit(value, maxDivisorSquare)) { groupAConstantIndex = GroupAConstantDivisors.Length; break; @@ -657,7 +644,7 @@ Cleanup: if (groupAWheelActive) { ulong value = groupAWheelCandidate; - if (value <= sqrtLimit) + if (DivisorWithinSquareLimit(value, maxDivisorSquare)) { candidate = new HeuristicDivisorCandidate(value, HeuristicDivisorGroup.GroupAWheel, 3, 0, (ushort)(value % Wheel210)); byte increment = GroupAIncrementPattern[groupAIncrementIndex]; @@ -691,7 +678,7 @@ Cleanup: continue; } - if (value > sqrtLimit) + if (!DivisorWithinSquareLimit(value, maxDivisorSquare)) { state.Candidate = ulong.MaxValue; continue; diff --git a/docs/HeuristicIsPrimePlan.md b/docs/HeuristicIsPrimePlan.md index e2157c9..cc024cd 100644 --- a/docs/HeuristicIsPrimePlan.md +++ b/docs/HeuristicIsPrimePlan.md @@ -131,7 +131,7 @@ The plan incorporates the updated divisor-class heuristics (Groups A/B, wheels, * [done] The CPU heuristic now operates as a pure trial-division sweep without maintaining per-call summaries, relying on `PrepareHeuristicDivisor` only when downstream consumers request Montgomery data. * [done] Introduced the temporary `UseHeuristicGroupBTrialDivision` gate so current builds execute Group A locally and then fall back to `Open.Numeric.Primes.Prime.Numbers.IsPrime`, keeping the full Group B implementation available for reactivation. 7. [done] **Implement `IsPrimeGpu`** - * [done] Mirror CPU structure while batching divisor checks on the accelerator via `HeuristicTrialDivisionGpu`, drawing divisors from `HeuristicPrimeSieves`, reusing caller-provided `sqrtLimit`/`nMod10` inputs, and falling back to CPU when GPUs are disabled. + * [done] Mirror CPU structure while batching divisor checks on the accelerator via `HeuristicTrialDivisionGpu`, drawing divisors from `HeuristicPrimeSieves`, reusing caller-provided `maxDivisorSquare`/`nMod10` inputs, and falling back to CPU when GPUs are disabled. * [done] Added `PrimeTesterKernels.HeuristicTrialDivisionKernel` so GPU batches respect Group A/B ordering while emitting hit flags for early exits. * [done] Kept GPU divisibility in standard modular arithmetic while reserving Montgomery transforms for CPU confirmation logic. * [done] When the temporary Group B gate is disabled, delegate GPU calls to the CPU fallback so Group A coverage remains in place while the accelerator path stays available for future re-enablement.