README updated.

This commit is contained in:
mlemanczyk committed 2025-12-06 20:11:56 +01:00
1 parent c44d188720
commit 788e11588e
1 file changed
+85 -33
+85 -33
View File
@@ -40,8 +40,12 @@ The source code relays on selected functionality from the following packages:
* [PeterO.Numbers](https://www.nuget.org/packages/PeterO.Numbers)
* [xunit](https://www.nuget.org/packages/xunit)
# Excluded
The candidates below were excluded by the scanner.
# Excluded p candidates by `--mersenne=residue` method.
Below you can find the filtering results coming from `--mersenne=residue` method, sorted and filtered to primes, only.
* [138,000,001 - 200,000,000](/sorted-primes-from-residue-method.zip)
# Excluded p candidates by `--mersenne=bydivisor` method.
The candidates below were excluded by the scanner. All calculated on AMD Ryzen 7 laptop. The divisors are randomly checked as sanity. The list is due complete validation when all candidates within each range are calculated.
* [138,000,001 - 139,000,000](/excluded-138m.csv)
# Description
@@ -56,7 +60,14 @@ This document explains **what the program does**, **how it works step-by-step**,
* `--mersenne=divisor`
* `--mersenne=bydivisor`
# EvenPerfectBitScanner – Configuration and Pipeline
## Two-stage scanning flow
* Stage 1 - `--mersenne=residue` acts as the fast, coarse filter for entire ranges of candidate exponents. It quickly rules out most `p` values before any deep work, producing result files that record which primes survived.
* Stage 2 - `--mersenne=bydivisor` consumes the survivors from stage 1 via `--filter-p=<results>` and performs the expensive divisor-by-divisor scan. It will not run without that filter.
* Sample stage-1 output: `[sorted-primes-from-residue-method.zip](sorted-primes-from-residue-method.zip)` contains primes that passed the residue sweep and can seed stage 2 directly.
* Heuristic order/residue rules reused by stage 2 are documented in `docs/HeuristicIsPrimeSpecification.md`; the by-divisor path reuses those wheel priorities, residue steppers, and cycle heuristics when walking `q = 2kp + 1`.
# EvenPerfectBitScanner - Configuration and Pipeline
Please run `EvenPerfectBitScanner.exe /?` to display all command line parameters with a short description. The description below describes `--mersenne=residue` mode.
## What the exact CLI config does
@@ -75,13 +86,13 @@ Please run `EvenPerfectBitScanner.exe /?` to display all command line parameters
````
* **Candidate stepping**:
`--increment=add` → the producer uses `TransformPAdd` (see my `Program.cs`), which locks into `p ≡ 1 or 5 (mod 6)`.
`--increment=add` -> the producer uses `TransformPAdd` (see my `Program.cs`), which locks into `p == 1 or 5 (mod 6)`.
* **Prime tests (p)**:
`--primes-device=gpu` → `PrimeTester` runs a GPU-batched small-prime sieve (and light CPU checks) (`PrimeTester.cs`).
`--primes-device=gpu` -> `PrimeTester` runs a GPU-batched small-prime sieve (and light CPU checks) (`PrimeTester.cs`).
* **Mersenne stage**:
`--mersenne=residue` + `--mersenne-device=gpu` → `MersenneNumberTester` selects the residue divisor path and routes its scanning kernels to GPU (`MersenneNumberTester.cs` + `Gpu/MersenneNumberResidueGpuTester.cs`). We do not run Lucas–Lehmer in this config.
`--mersenne=residue` + `--mersenne-device=gpu` -> `MersenneNumberTester` selects the residue divisor path and routes its scanning kernels to GPU (`MersenneNumberTester.cs` + `Gpu/MersenneNumberResidueGpuTester.cs`). We do not run Lucas-Lehmer in this config.
* **GPU tuning**:
`--gpu-prime-threads=1024` gates concurrent GPU prime work via `GpuPrimeWorkLimiter`.
@@ -94,14 +105,14 @@ Please run `EvenPerfectBitScanner.exe /?` to display all command line parameters
## The pipeline, line-by-line logic of each stage (and every early rejection)
I’ll walk you through what happens for a single candidate exponent `p`. The exact code paths below are the ones used in my run.
I'll walk you through what happens for a single candidate exponent `p`. The exact code paths below are the ones used in my run.
### Candidate acquisition (producer)
* We atomically reserve the next `p` using a packed `ulong` CAS over `{p << 3 | remainder}` (my `_state`). That’s the lock-free distributor in `Program.cs`.
* We atomically reserve the next `p` using a packed `ulong` CAS over `{p << 3 | remainder}` (my `_state`). That's the lock-free distributor in `Program.cs`.
* **TransformPAdd**:
First step normalizes to `p % 6 ∈ {1,5}` and after that it fixes remainders to 1 or 5 with each increase.
First step normalizes to `p % 6 in {1,5}` and after that it fixes remainders to 1 or 5 with each increase.
### Cheap filters on p
@@ -113,20 +124,20 @@ I’ll walk you through what happens for a single candidate exponent `p`. The ex
* even `n`: reject.
* `n % 5 == 0`: reject quickly.
* **“Ends-with-1” GCD heuristic**:
If `n % 10 == 1` and `SharesFactorWithMaxExponent(n)` → reject.
* **"Ends-with-1" GCD heuristic**:
If `n % 10 == 1` and `SharesFactorWithMaxExponent(n)` -> reject.
`SharesFactorWithMaxExponent(n)` computes `m = floor(log2 n)` and rejects if `gcd(n, m) ≠ 1`.
There’s also a GPU batch form `SharesFactorWithMaxExponentBatch` used elsewhere when screening arrays.
`SharesFactorWithMaxExponent(n)` computes `m = floor(log2 n)` and rejects if `gcd(n, m) != 1`.
There's also a GPU batch form `SharesFactorWithMaxExponentBatch` used elsewhere when screening arrays.
*Why it’s valid*: numbers of the form `10t+1` have specific small-factor structure; combining that with `gcd(n, floor(log2 n))` cheaply kills many composites without MR. (This is a heuristic prefilter based on my analysis of primes ending with 1 or 7 and testing; it won’t reject primes incorrectly.)
*Why it's valid*: numbers of the form `10t+1` have specific small-factor structure; combining that with `gcd(n, floor(log2 n))` cheaply kills many composites without MR. (This is a heuristic prefilter based on my analysis of primes ending with 1 or 7 and testing; it won't reject primes incorrectly.)
* **Small-prime sieve (CPU loop, still inside PrimeTester)**:
Iterate `PrimesGenerator.SmallPrimes` while `prime^2 ≤ n` using the ready-made `SmallPrimesPow2` to avoid sqrt. Reject immediately if divisible.
Iterate `PrimesGenerator.SmallPrimes` while `prime^2 <= n` using the ready-made `SmallPrimesPow2` to avoid sqrt. Reject immediately if divisible.
With `--primes-device=gpu`, we also have a device-side sieve for batches (`IsPrimeGpuBatch`): it copies chunks to the device, runs a simple sieve kernel over an uploaded small-primes table, and copies flags back. My per-candidate path uses the single-value version, but the machinery (stateful kernel cache, scratch buffers) is there for vectorized checks.
➡ If `p` fails any of the above → we never touch `M_p`. That’s my first wall of early rejections.
-> If `p` fails any of the above -> we never touch `M_p`. That's my first wall of early rejections.
### Mersenne stage
@@ -134,43 +145,84 @@ I’ll walk you through what happens for a single candidate exponent `p`. The ex
* **Hard mathematical pre-rejects (top of IsMersennePrime)**:
* If `p % 3 == 0` and `p != 3` → reject (since `7 ∣ M_p` when `3 ∣ p`).
* If `(p & 3) == 1` and `p.SharesFactorWithExponentMinusOne()` → reject.
* If `p % 3 == 0` and `p != 3` -> reject (since `7 | M_p` when `3 | p`).
* If `(p & 3) == 1` and `p.SharesFactorWithExponentMinusOne()` -> reject.
* `SharesFactorWithExponentMinusOne` (in `ULongExtensions.cs`) factors `p-1` by trial over `SmallPrimes`, removes multiplicities, and for each distinct prime `r` checks whether the multiplicative order of 2 modulo `r` divides `p` (via `CalculateOrder()`), also handling the leftover cofactor. If any such order divides `p`, we can certify a small factor for `M_p` and reject `p` outright.
* If `p` is even → reject (trivial, here `p` is already odd from the prime screen, but it’s kept for safety).
* If `p` is even -> reject (trivial, here `p` is already odd from the prime screen, but it's kept for safety).
* **Residue divisor scanning (`_useResidue == true`)**:
* We set `twoP = 2*p`. Valid Mersenne divisors have the form `q = 2kp + 1, k ≥ 1` with congruence constraints `q ≡ 1 (mod 2p)` and `q ≡ 1 or 7 (mod 8)`.
* We set `twoP = 2*p`. Valid Mersenne divisors have the form `q = 2kp + 1, k >= 1` with congruence constraints `q == 1 (mod 2p)` and `q == 1 or 7 (mod 8)`.
* **Digit filter**:
* Compute `lastIsSeven = ((p & 3) == 3)` which encodes the last decimal digit of `M_p = 2^p - 1`.
* If last digit of `M_p` is 7 → we allow only `q` with last digit in `{3, 7, 9}`.
* If last digit of `M_p` is 7 -> we allow only `q` with last digit in `{3, 7, 9}`.
* Else (last digit 1) → we allow only `{1, 3, 7, 9}`.
* Else (last digit 1) -> we allow only `{1, 3, 7, 9}`.
This is implemented both on CPU (`CpuConstants.LastSevenMask10`, `LastOneMask10`) and GPU by calculating `q % 10` incrementally as `k` grows.
* **Rolling residues**: we keep four rolling residues for each lane: `q mod 10, 8, 5, 3`. That allows immediate rejection of `q` divisible by 2, 3, or 5 without any division (just adds, bit-ands and subtracts).
* **CPU residue scanner**: uses `ModResidueTracker` with update formula `r′ ≡ 2^Δ⋅r+(2^Δ−1) (mod d)`.
* **CPU residue scanner**: uses `ModResidueTracker` with update formula `r' == 2^Delta*r+(2^Delta-1) (mod d)`.
* **GPU residue scanner**: batches `k`, evaluates residues, prunes with cycles table, runs mod-pow on survivors. Uses pooled contexts and kernel caches.
* **Short-circuit**: as soon as we find any valid divisor `q | M_p` in the scanned window, we stop.
➡ If scanning completes up to `k 2^p / 2 + 1` and no divisor was found → `IsMersennePrime(p)` returns true and the CSV line has `isPerfect=true`. This means that `p` passed all the tests and is a **candidate** for constructing a perfect number, which requires testing with other methods.
-> If scanning completes up to `k 2^p / 2 + 1` and no divisor was found -> `IsMersennePrime(p)` returns true and the CSV line has `isPerfect=true`. This means that `p` passed all the tests and is a **candidate** for constructing a perfect number, which requires testing with other methods.
## --mersenne=bydivisor: divisor-first sweep for filtered primes
This is the second-stage scan that only runs after a residue pass. Feed it the stage-1 results (for example from `sorted-primes-from-residue-method.zip`) via `--filter-p=<path>` so it can focus solely on primes that already cleared the residue filter.
* **Prerequisites and setup**: `--filter-p` is mandatory (skips execution otherwise), candidates come from prior results filtered to `passedAllTests=true`, and the list is sorted, deduplicated, and optionally trimmed by `--start-prime` and `--max-prime`. No fresh prime sieve runs: the path assumes the filter already contains prime exponents. `--min-k` seeds the scan window and per-prime state is persisted under `Checks/<p>.bin` (CPU path only) every `ByDivisorStateSaveInterval` to resume after restarts.
* **Divisor generation and residue stepper**:
* Divisors follow `q = 2kp + 1` with monotonically increasing integer `k`, bounded by `allowedMax = min(2^(p-1)-1, divisorLimitFromMaxPrime)`. The limit comes from the largest prime in the batch (`ConfigureFromMaxPrime`) and saturates at 2^256-1 for huge inputs.
* Decimal masks come from my composite-divisor residue study: when `(p & 3) == 3` the Mersenne number ends with 7, so only divisors with last digit `{3,7,9}` pass; otherwise `{1,3,7,9}`. All divisors must satisfy `q % 8 in {1,7}`.
* Rolling remainders are maintained for each small modulus instead of recomputing divisions: CPU checks mod {3,7,11,13,17,19,8,10}, GPU checks mod {3,7,11,8,10}. The stride `step = 2p` is pre-split into per-modulus deltas (`step10`, `step8`, ...) and residues advance via branch-free `AddMod*` helpers, so the stepper cheaply skips entire residue classes that never yield valid divisors.
* **CPU scan path**:
* Fast lane when `k`/`q` fit in 64 bits: `GpuUInt128` holds stride, divisor, and limit; residues drive the wheel, `MontgomeryDivisorDataPool` supplies reduction constants, and every admissible divisor asks `MersenneDivisorCycles.TryCalculateCycleLengthForExponent*` for the order of 2 modulo `q`. If the order equals `p`, the divisor is accepted immediately; otherwise the code falls back to `CalculateCycleLength*` (CPU/GPU/hybrid based on `--order-device`) and short-circuits on the same condition. The scan records progress (`RecordState`) every 10k steps so it can resume from the last `k`.
* When bounds overflow 64 bits, the loop switches to `BigInteger`: the same residue masks apply, composites are rejected with a Miller-Rabin check before the expensive `BigInteger.ModPow(2, p, q)`, and hits still require `2^p mod q == 1`. `divisorsExhausted` reports whether the configured window was fully covered (including resumes from `--min-k`).
* **GPU scan path**:
* Requires `MinK <= ulong.MaxValue`; allowedMax is `min(2^(p-1)-1, divisorLimitFromMaxPrime)` with an extra 170M cap for tiny test primes. `CheckDivisors` chunks the `k` range, applies the same decimal mask and small-modulus filters on CPU, and asks `ResolveDivisorCycle` (GPU order solver with CPU fallback) for each admissible divisor. A cycle equal to `p` ends the scan immediately.
* Survivors are batched into pooled buffers and pushed to `DivisorByDivisorKernels.CheckKernel`. The kernel reuses `ExponentRemainderStepperGpu` plus cycle remainders to avoid recomputing full powers for consecutive exponents (counts are 1 today but the stepper is shared). Hits are captured via an atomic `firstHit` flag; accelerator-local bags reuse streams, buffers, and even divisor scan sessions. Unlike the CPU path, the GPU path does not persist state files; resuming relies solely on `--min-k`.
* **Decision flags**: `IsPrime` returns `(isPrime, divisorsExhausted, divisor)`. The main loop treats `divisorsExhausted=true` as a detailed check; a composite divisor sets `isPrime=false` and stops the scan as soon as it is confirmed.
For the residue wheels, digit masks, and the heuristic order calculation that drive this stage, see `docs/HeuristicIsPrimeSpecification.md` (statistics from the composite study remain unchanged even as the code evolved).
## --mersenne=divisor: cycle-gated GPU sweep
* **Where it plugs in**: shares the same producer, residue locks (p % 6 is 1 or 5), and prefilters as the `--mersenne=residue` description above. Once `--mersenne=divisor` is selected, the Mersenne stage delegates to `_divisorTester.IsPrime` and skips Lucas/residue kernels.
* **Cached small-divisor stage**: `MersenneNumberDivisorGpuTester.BuildDivisorCandidates` lifts all divisors <= 4,000,000 with known cycle lengths from the `divisor_cycles` snapshot (generated from my composite-divisor residue analysis). Each cached `(divisor, cycle)` is tried only when `p % cycle == 0`, cutting out residue groups whose order of 2 provably cannot divide `p`.
* **Generated divisor sweep**:
* If a specific divisor is supplied (tests/manual runs), it is checked directly via `DivisorKernels.Kernel`.
* Otherwise the scanner walks `k = 1..divisorCyclesSearchLimit` (`--divisor-cycles-limit`, default 64), building `q = 2kp + 1` and skipping the trivial `q == M_p` case for small `p`. For each `q`, `MersenneDivisorCycles.GetCycleGpu` computes the order of 2 (using the same heuristic factorization/cycle grouping as the snapshot); only when `p % cycle == 0` do we launch the GPU divisibility kernel. The first hit stops the loop.
* `divisorsExhausted` flips to true only if the configured limit covers all admissible `k` up to `floor(UInt128.MaxValue / (2p))`, so callers know whether the divisor window was fully searched.
* **GPU resource handling**: per-accelerator queues reuse the one-byte result buffers used by `DivisorKernels`, and kernels run through `AcceleratorPool` without CPU fallbacks on this path (the exponent primality screen already ran on CPU/GPU earlier). Cycle data are loaded up front from `--divisor-cycles` for both the cached and generated phases.
* **Early-rejection rules specific to this mode**: Only divisors of shape `2kp+1` whose order divides `p` ever hit the GPU. The small-cycle snapshot already bakes in the decimal-digit and residue exclusions observed across millions of composite divisors, so the runtime sweep focuses exclusively on the most promising groups.
---
## What we cache, pool, and batch (and why)
* **Per-thread singletons**: `PrimeTester`, `MersenneNumberTester`, `ModResidueTracker` → avoid hot locks, reuse GPU/CPU state.
* **Per-thread singletons**: `PrimeTester`, `MersenneNumberTester`, `ModResidueTracker` -> avoid hot locks, reuse GPU/CPU state.
* **Mod residue tracker**: avoids recomputing residues from scratch, supports incremental updates.
* **Divisor cycles**: persistent, precomputed orders for `divisors ≤ 4,000,000`, shared singleton, uploaded to GPU.
* **Divisor cycles**: persistent, precomputed orders for `divisors <= 4,000,000`, shared singleton, uploaded to GPU.
* **GPU infra**: pooled contexts/kernels, concurrency gating.
@@ -178,12 +230,12 @@ I’ll walk you through what happens for a single candidate exponent `p`. The ex
---
## Every mathematical rule that we’re using to reject early
## Every mathematical rule that we're using to reject early
* Only prime exponents matter.
* 7 divides `M_p` if `3 ∣ p`.
* Order-based rejections using factors of `p−1`.
* Shape of Mersenne divisor: `q=2kp+1`, `q≡1 or 7 (mod 8)`.
* 7 divides `M_p` if `3 | p`.
* Order-based rejections using factors of `p-1`.
* Shape of Mersenne divisor: `q=2kp+1`, `q==1 or 7 (mod 8)`.
* Cycle length filter for small `divisors`.
* Monotone residue stepping with closed forms.
@@ -200,15 +252,15 @@ I’ll walk you through what happens for a single candidate exponent `p`. The ex
## Risks, correctness gaps, and performance pitfalls (with precise fixes)
* **Per-thread buffers are huge**: `--block-size=10_000_000` → \~80 GB at 1024 threads.
* **Per-thread buffers are huge**: `--block-size=10_000_000` -> \~80 GB at 1024 threads.
* **1024 CPU threads + 1024 GPU permits** oversubscribe virtually any GPU. When running such configurations, make sure that you have enough RAM or swap file.
* How: set `--threads` to `Environment.ProcessorCount` and `--gpu-prime-threads` to \~8–32.
* How: set `--threads` to `Environment.ProcessorCount` and `--gpu-prime-threads` to \~8-32.
---
## “Full accounting” of what gets computed, rejected, cached, or pooled
## "Full accounting" of what gets computed, rejected, cached, or pooled
### Per p