From 9f35f6c558457a09528f12b58297269620988268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Tue, 9 Sep 2025 21:15:57 +0200 Subject: [PATCH] Initial commit. --- .gitignore | 9 + .vscode/launch.json | 70 + .vscode/settings.json | 7 + .vscode/tasks.json | 96 + AGENTS.md | 94 + .../EvenPerfectBitScanner.Tests.csproj | 25 + EvenPerfectBitScanner.Tests/ProgramTests.cs | 92 + .../ResultsFileNameTests.cs | 54 + .../EvenPerfectBitScanner.csproj | 15 + EvenPerfectBitScanner/Program.cs | 1130 ++++++++++++ EvenPerfectScanner.sln | 40 + PerfectNumbers.Core.Tests/AlphaCacheTests.cs | 12 + .../AlphaCalculationsTests.cs | 12 + .../EulerPrimeTesterTests.cs | 22 + .../Gpu/GpuContextPoolTests.cs | 69 + .../Gpu/GpuNttCollection.cs | 9 + .../Gpu/GpuPrimeLimiterTests.cs | 45 + .../Gpu/GpuUInt128Tests.cs | 342 ++++ .../Gpu/LucasLehmerPrimeTesterTests.cs | 53 + .../Gpu/NttBackendConfigTests.cs | 78 + .../Gpu/NttGpuMathTests.cs | 361 ++++ .../Gpu/NttMontgomeryTests.cs | 87 + .../Gpu/NttReductionModeTests.cs | 90 + .../GpuKernelPoolTests.cs | 39 + .../GpuWorkLimiterTests.cs | 39 + PerfectNumbers.Core.Tests/InputParserTests.cs | 12 + .../KRangeFinderTests.cs | 12 + .../MersenneDivisorCyclesTests.cs | 25 + ...MersenneNumberIncrementalCpuTesterTests.cs | 36 + ...MersenneNumberIncrementalGpuTesterTests.cs | 37 + .../MersenneNumberOrderCpuTesterTests.cs | 37 + .../MersenneNumberOrderGpuTesterTests.cs | 38 + .../MersenneNumberResidueCpuTesterTests.cs | 31 + .../MersenneNumberResidueGpuTesterTests.cs | 33 + .../MersenneNumberTesterTests.cs | 36 + .../MersenneResidueModeTests.cs | 16 + .../MersennePrimeFactorTesterTests.cs | 29 + .../MersenneResidueStepperTests.cs | 37 + .../ModResidueTrackerTests.cs | 66 + .../ModuloAutomata/ModuloAutomatonTests.cs | 91 + .../PerfectNumbers.Core.Tests.csproj | 25 + PerfectNumbers.Core.Tests/PowerCacheTests.cs | 27 + PerfectNumbers.Core.Tests/PrimeCacheTests.cs | 39 + .../PrimeTester/PrimeTesterGcdTests.cs | 25 + .../PrimeTester/PrimeTesterIsPrimeTests.cs | 39 + .../PrimesGeneratorTests.cs | 25 + .../RationalHelperTests.cs | 53 + .../RleBlacklistTests.cs | 12 + .../StringBuilderPoolTests.cs | 36 + .../TextFileWriterTests.cs | 12 + .../UInt128ExtensionsTests.cs | 218 +++ .../ULongExtensionsTests.cs | 185 ++ PerfectNumbers.Core/AlphaCache.cs | 41 + PerfectNumbers.Core/AlphaCalculations.cs | 29 + .../AlphaMScannerCandidateStatus.cs | 13 + PerfectNumbers.Core/Cpu/CpuConstants.cs | 9 + .../Cpu/MersenneNumberIncrementalCpuTester.cs | 83 + .../Cpu/MersenneNumberLucasLehmerCpuTester.cs | 43 + .../Cpu/MersenneNumberOrderCpuTester.cs | 74 + .../Cpu/MersenneNumberResidueCpuTester.cs | 169 ++ PerfectNumbers.Core/EulerPrimeTester.cs | 19 + PerfectNumbers.Core/Gpu/GpuConstants.cs | 7 + PerfectNumbers.Core/Gpu/GpuContextPool.cs | 152 ++ PerfectNumbers.Core/Gpu/GpuKernelPool.cs | 550 ++++++ .../Gpu/GpuPrimeWorkLimiter.cs | 57 + PerfectNumbers.Core/Gpu/GpuUInt128.cs | 991 +++++++++++ PerfectNumbers.Core/Gpu/GpuWorkLimiter.cs | 60 + .../Gpu/MersenneNumberIncrementalGpuTester.cs | 77 + .../Gpu/MersenneNumberLucasLehmerGpuTester.cs | 864 +++++++++ .../Gpu/MersenneNumberOrderGpuTester.cs | 63 + .../Gpu/MersenneNumberResidueGpuTester.cs | 83 + PerfectNumbers.Core/Gpu/NttGpuMath.cs | 1544 +++++++++++++++++ PerfectNumbers.Core/InputParser.cs | 26 + PerfectNumbers.Core/KRangeFinder.cs | 79 + PerfectNumbers.Core/MersenneDivisorCycles.cs | 381 ++++ PerfectNumbers.Core/MersenneNumberTester.cs | 312 ++++ .../MersennePrimeFactorTester.cs | 558 ++++++ PerfectNumbers.Core/MersenneResidueStepper.cs | 69 + PerfectNumbers.Core/ModResidueTracker.cs | 313 ++++ PerfectNumbers.Core/ModuloAutomata.cs | 215 +++ PerfectNumbers.Core/PerfectNumberConstants.cs | 11 + .../PerfectNumbers.Core.csproj | 23 + PerfectNumbers.Core/PowerCache.cs | 65 + PerfectNumbers.Core/PrimeCache.cs | 96 + PerfectNumbers.Core/PrimeTester.cs | 371 ++++ PerfectNumbers.Core/PrimesGenerator.cs | 106 ++ PerfectNumbers.Core/RationalHelper.cs | 35 + PerfectNumbers.Core/RationalNumbers.cs | 8 + PerfectNumbers.Core/RleBlacklist.cs | 361 ++++ PerfectNumbers.Core/StringBuilderPool.cs | 17 + PerfectNumbers.Core/TextFileWriter.cs | 77 + PerfectNumbers.Core/UInt128Extensions.cs | 251 +++ PerfectNumbers.Core/UInt128Numbers.cs | 10 + PerfectNumbers.Core/ULongExtensions.cs | 423 +++++ 94 files changed, 12857 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 AGENTS.md create mode 100644 EvenPerfectBitScanner.Tests/EvenPerfectBitScanner.Tests.csproj create mode 100644 EvenPerfectBitScanner.Tests/ProgramTests.cs create mode 100644 EvenPerfectBitScanner.Tests/ResultsFileNameTests.cs create mode 100644 EvenPerfectBitScanner/EvenPerfectBitScanner.csproj create mode 100644 EvenPerfectBitScanner/Program.cs create mode 100644 EvenPerfectScanner.sln create mode 100644 PerfectNumbers.Core.Tests/AlphaCacheTests.cs create mode 100644 PerfectNumbers.Core.Tests/AlphaCalculationsTests.cs create mode 100644 PerfectNumbers.Core.Tests/EulerPrimeTesterTests.cs create mode 100644 PerfectNumbers.Core.Tests/Gpu/GpuContextPoolTests.cs create mode 100644 PerfectNumbers.Core.Tests/Gpu/GpuNttCollection.cs create mode 100644 PerfectNumbers.Core.Tests/Gpu/GpuPrimeLimiterTests.cs create mode 100644 PerfectNumbers.Core.Tests/Gpu/GpuUInt128Tests.cs create mode 100644 PerfectNumbers.Core.Tests/Gpu/LucasLehmerPrimeTesterTests.cs create mode 100644 PerfectNumbers.Core.Tests/Gpu/NttBackendConfigTests.cs create mode 100644 PerfectNumbers.Core.Tests/Gpu/NttGpuMathTests.cs create mode 100644 PerfectNumbers.Core.Tests/Gpu/NttMontgomeryTests.cs create mode 100644 PerfectNumbers.Core.Tests/Gpu/NttReductionModeTests.cs create mode 100644 PerfectNumbers.Core.Tests/GpuKernelPoolTests.cs create mode 100644 PerfectNumbers.Core.Tests/GpuWorkLimiterTests.cs create mode 100644 PerfectNumbers.Core.Tests/InputParserTests.cs create mode 100644 PerfectNumbers.Core.Tests/KRangeFinderTests.cs create mode 100644 PerfectNumbers.Core.Tests/MersenneDivisorCyclesTests.cs create mode 100644 PerfectNumbers.Core.Tests/MersenneNumberIncrementalCpuTesterTests.cs create mode 100644 PerfectNumbers.Core.Tests/MersenneNumberIncrementalGpuTesterTests.cs create mode 100644 PerfectNumbers.Core.Tests/MersenneNumberOrderCpuTesterTests.cs create mode 100644 PerfectNumbers.Core.Tests/MersenneNumberOrderGpuTesterTests.cs create mode 100644 PerfectNumbers.Core.Tests/MersenneNumberResidueCpuTesterTests.cs create mode 100644 PerfectNumbers.Core.Tests/MersenneNumberResidueGpuTesterTests.cs create mode 100644 PerfectNumbers.Core.Tests/MersenneNumberTester/MersenneNumberTesterTests.cs create mode 100644 PerfectNumbers.Core.Tests/MersenneNumberTester/MersenneResidueModeTests.cs create mode 100644 PerfectNumbers.Core.Tests/MersennePrimeFactorTester/MersennePrimeFactorTesterTests.cs create mode 100644 PerfectNumbers.Core.Tests/MersenneResidueStepperTests.cs create mode 100644 PerfectNumbers.Core.Tests/ModResidueTrackerTests.cs create mode 100644 PerfectNumbers.Core.Tests/ModuloAutomata/ModuloAutomatonTests.cs create mode 100644 PerfectNumbers.Core.Tests/PerfectNumbers.Core.Tests.csproj create mode 100644 PerfectNumbers.Core.Tests/PowerCacheTests.cs create mode 100644 PerfectNumbers.Core.Tests/PrimeCacheTests.cs create mode 100644 PerfectNumbers.Core.Tests/PrimeTester/PrimeTesterGcdTests.cs create mode 100644 PerfectNumbers.Core.Tests/PrimeTester/PrimeTesterIsPrimeTests.cs create mode 100644 PerfectNumbers.Core.Tests/PrimesGeneratorTests.cs create mode 100644 PerfectNumbers.Core.Tests/RationalHelperTests.cs create mode 100644 PerfectNumbers.Core.Tests/RleBlacklistTests.cs create mode 100644 PerfectNumbers.Core.Tests/StringBuilderPoolTests.cs create mode 100644 PerfectNumbers.Core.Tests/TextFileWriterTests.cs create mode 100644 PerfectNumbers.Core.Tests/UInt128Extensions/UInt128ExtensionsTests.cs create mode 100644 PerfectNumbers.Core.Tests/ULongExtensionsTests.cs create mode 100644 PerfectNumbers.Core/AlphaCache.cs create mode 100644 PerfectNumbers.Core/AlphaCalculations.cs create mode 100644 PerfectNumbers.Core/AlphaMScannerCandidateStatus.cs create mode 100644 PerfectNumbers.Core/Cpu/CpuConstants.cs create mode 100644 PerfectNumbers.Core/Cpu/MersenneNumberIncrementalCpuTester.cs create mode 100644 PerfectNumbers.Core/Cpu/MersenneNumberLucasLehmerCpuTester.cs create mode 100644 PerfectNumbers.Core/Cpu/MersenneNumberOrderCpuTester.cs create mode 100644 PerfectNumbers.Core/Cpu/MersenneNumberResidueCpuTester.cs create mode 100644 PerfectNumbers.Core/EulerPrimeTester.cs create mode 100644 PerfectNumbers.Core/Gpu/GpuConstants.cs create mode 100644 PerfectNumbers.Core/Gpu/GpuContextPool.cs create mode 100644 PerfectNumbers.Core/Gpu/GpuKernelPool.cs create mode 100644 PerfectNumbers.Core/Gpu/GpuPrimeWorkLimiter.cs create mode 100644 PerfectNumbers.Core/Gpu/GpuUInt128.cs create mode 100644 PerfectNumbers.Core/Gpu/GpuWorkLimiter.cs create mode 100644 PerfectNumbers.Core/Gpu/MersenneNumberIncrementalGpuTester.cs create mode 100644 PerfectNumbers.Core/Gpu/MersenneNumberLucasLehmerGpuTester.cs create mode 100644 PerfectNumbers.Core/Gpu/MersenneNumberOrderGpuTester.cs create mode 100644 PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs create mode 100644 PerfectNumbers.Core/Gpu/NttGpuMath.cs create mode 100644 PerfectNumbers.Core/InputParser.cs create mode 100644 PerfectNumbers.Core/KRangeFinder.cs create mode 100644 PerfectNumbers.Core/MersenneDivisorCycles.cs create mode 100644 PerfectNumbers.Core/MersenneNumberTester.cs create mode 100644 PerfectNumbers.Core/MersennePrimeFactorTester.cs create mode 100644 PerfectNumbers.Core/MersenneResidueStepper.cs create mode 100644 PerfectNumbers.Core/ModResidueTracker.cs create mode 100644 PerfectNumbers.Core/ModuloAutomata.cs create mode 100644 PerfectNumbers.Core/PerfectNumberConstants.cs create mode 100644 PerfectNumbers.Core/PerfectNumbers.Core.csproj create mode 100644 PerfectNumbers.Core/PowerCache.cs create mode 100644 PerfectNumbers.Core/PrimeCache.cs create mode 100644 PerfectNumbers.Core/PrimeTester.cs create mode 100644 PerfectNumbers.Core/PrimesGenerator.cs create mode 100644 PerfectNumbers.Core/RationalHelper.cs create mode 100644 PerfectNumbers.Core/RationalNumbers.cs create mode 100644 PerfectNumbers.Core/RleBlacklist.cs create mode 100644 PerfectNumbers.Core/StringBuilderPool.cs create mode 100644 PerfectNumbers.Core/TextFileWriter.cs create mode 100644 PerfectNumbers.Core/UInt128Extensions.cs create mode 100644 PerfectNumbers.Core/UInt128Numbers.cs create mode 100644 PerfectNumbers.Core/ULongExtensions.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ecc1fb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +PerfectNumbers.Core/bin/* +PerfectNumbers.Core/obj/* +EvenPerfectBitScanner/bin/* +EvenPerfectBitScanner/obj/* +EvenPerfectBitScanner.Tests/bin/* +EvenPerfectBitScanner.Tests/obj/* +PerfectNumbers.Core.Tests/bin/* +PerfectNumbers.Core.Tests/obj/* +.DS_Store diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d6eecc8 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,70 @@ +{ + "version": "0.2.0", + "inputs": [ + { + "id": "projectName", + "type": "pickString", + "description": "Select project to run", + "options": [ + "EvenPerfectBitScanner", + ], + "default": "EvenPerfectBitScanner" + } + ], + "configurations": [ + { + "name": "Debug selected project (F5)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Build Debug", + "program": "${workspaceFolder}/${input:projectName}/bin/Debug/net8.0/${input:projectName}.dll", + "args": [ + "--increment=add --mersenne=residue --threads=128 --gpu-scan-batch=512000 --primes-device=gpu --mersenne-device=gpu --mod-reduction=barrett128 --block-size=10000000 --gpu-prime-threads=32 --write-batch-size=10000 --divisor-cycles-device=gpu" + ], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "console": "internalConsole" + }, + { + "name": "Run selected project (Alt+F5)", + "type": "coreclr", + "request": "launch", + "noDebug": true, + "preLaunchTask": "Build Release", + "program": "${workspaceFolder}/${input:projectName}/bin/Release/net8.0/${input:projectName}.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "console": "internalConsole", + "internalConsoleOptions": "neverOpen", + "justMyCode": false + }, + { + "name": "Run EvenPerfectBitScanner", + "type": "coreclr", + "request": "launch", + "noDebug": true, + "preLaunchTask": "Build EvenPerfectBitScanner Release", + "program": "${workspaceFolder}/EvenPerfectBitScanner/bin/Release/net8.0/EvenPerfectBitScanner.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "justMyCode": false + }, + { + "name": "Debug EvenPerfectBitScanner", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Build EvenPerfectBitScanner Debug", + "program": "${workspaceFolder}/EvenPerfectBitScanner/bin/Debug/net8.0/EvenPerfectBitScanner.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "justMyCode": false + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4d01735 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "dotnet.defaultSolution": "FlexiCoder.sln", + "chatgpt.openOnStartup": true, + "cSpell.words": [ + "ILGPU" + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..5c54148 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,96 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Build Debug", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/EvenPerfectScanner.sln", + "--configuration", + "Debug", + "--framework", + "net8.0" + ], + "group": { + "kind": "build", + "isDefault": false + }, + "problemMatcher": "$msCompile" + }, + { + "label": "Build Release", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/EvenPerfectScanner.sln", + "--configuration", + "Release", + "--framework", + "net8.0" + ], + "group": "build", + "problemMatcher": "$msCompile" + }, + { + "label": "Build EvenPerfectBitScanner Debug", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/EvenPerfectBitScanner/EvenPerfectBitScanner.csproj", + "--configuration", + "Debug", + "--framework", + "net8.0" + ], + "group": "build", + "problemMatcher": "$msCompile" + }, + { + "label": "Build EvenPerfectBitScanner Release", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/EvenPerfectBitScanner/EvenPerfectBitScanner.csproj", + "--configuration", + "Release", + "--framework", + "net8.0" + ], + "group": "build", + "problemMatcher": "$msCompile" + }, + { + "label": "Run EvenPerfectBitScanner", + "type": "process", + "command": "dotnet", + "args": [ + "run", + "--project", + "${workspaceFolder}/EvenPerfectBitScanner/EvenPerfectBitScanner.csproj", + "--configuration", + "Release", + "--framework", + "net8.0" + ], + "problemMatcher": [] + }, + { + "label": "Run Release (no debug)", + "type": "process", + "command": "dotnet", + "args": [ + "run", + "--configuration", + "Release", + "--framework", + "net8.0" + ], + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5125129 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,94 @@ +# Repository Guidelines + +This repository contains only one solution: + +- `EvenPerfectScanner.sln` – a console application that scans numeric ranges. + +Changes should be isolated to the relevant solution and project. When working on one +project, avoid modifying the other unless explicitly required. Each solution +has or will have its own test suite and build configuration. + +To build `EvenPerfectScanner` run: + +```bash +dotnet build EvenPerfectScanner.sln +``` + +Run only the tests related to the solution you modify. Always run tests without --no-build parameter to avoid run failures due to missing assembly files. Tests should be always written using XUnit with FluentAssertions. Before executing any tests, make sure their running time is reasonable; skip test suites that are expected to take excessively long. + +### 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. +- 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. + +#### How to run tests within the 2-minute limit + +- Windows (PowerShell): + - Run a single class or test with a 120s guard: + - `$proj = ".csproj"` + - `$filter = 'FullyQualifiedName~SomeTests' # or FullyQualifiedName=Namespace.Class.Method` + - `$job = Start-Job -ScriptBlock { param($p,$f) dotnet test $p -c Debug --filter $f } -ArgumentList $proj,$filter` + - `if (-not (Wait-Job $job -Timeout 120)) { Stop-Job $job; throw 'Test run exceeded 120s timeout.' }` + - `Receive-Job $job` + +- Linux/macOS (bash): + - Use GNU timeout (install coreutils if needed on macOS): + - `timeout 120s dotnet test .csproj -c Debug --filter "FullyQualifiedName~SomeTests"` + - Note: exit code 124 indicates timeout; handle accordingly in scripts. + +- Codex/agent runs: when invoked as separate steps where inline time control isn’t available, wrap calls through the shell using the guards above rather than calling `dotnet test` directly from the tool. + +Examples: +- Quick GCD check: `--filter "FullyQualifiedName~PrimeTesterGcdTests"` +- Single test method: `--filter "FullyQualifiedName=EvenPerfectBitScanner.Tests.ResultsFileNameTests.BuildResultsFileName_contains_reduction_and_devices"` + +Always implement the functionality to achieve the highest possible performance, even if that means allowing for some code duplication. E.g. use List instead of IList, IEnumerable etc.; calculate modulo "%" result in the most efficient way, use aggresive inlining when appropriate and so on. Always take arrays from pools, when it's desired and possible depending on the required array size. + +Don't simplify the functionality and / or algorithms to behave differently, unless you're resolving a bug that you've found. Always attempt preservint the functionality, is it's designed in iterations and there is a reason for specific behaviors / implementations. + +The only .Net SDK available in Codex environment is .Net 8.0. It'll be periodically updated to newer versions as they become available. Specify target framework to the one currently available, but always target all .csproj files against all .Net versions currently supported by MS, using the latest official release. .Net 6.0 is no longer supported, so projects should target only .Net 8.0 and .Net 9.0, with 8+ being the default. + +Always enter blank line after closing "}" parenthesis, unless there is a consecutive closing "}" bracket. IF's, loops, in practice all code blocks should be put in their "{ (...) }" blocks. Even if there is only a single line in such a code block. Always use the latest officially released language features. All .csproj files should define nullable = enabled and we should always use "?" and/or "!" where appropriate. + +Never ever run tests marked with trait Category = Slow. Only run tests with category "Fast", when you want to validate if you haven't broken anything. If there are no categories assigned to the tests, when you attempt running them always watch out the 2 mins execution time, to prevent hanging up on test execution. In such case, marked all timeout out tests with category Slow and tests which executed within time limit, even if they failed, mark with category Fast, so that we can recognize them in the future. + +There is only one agents file - this one. Don't search for another one. + +Always use indentation with 4 spaces and / or 1 tab in source code files. + +All code, comments, commit messages, branch names and PR descriptions must be written in American English. + +## Performance implementation checklist + +- Prefer dedicated kernels over flag-controlled or parameterized kernels. Avoid conditional branches in kernels and tight loops by creating separate kernels when necessary. +- Remove `if` statements inside hot loops whenever possible. Duplicated code in additional kernels or methods is acceptable when it improves speed. +- Replace runtime flags in hot paths with dedicated method variants to eliminate conditional branches. +- Reuse existing variables of the same type instead of allocating new ones if some become unused. Add comments whenever a variable's meaning changes. +- Extract functionality into separate classes instead of growing existing classes. Place reusable components into `*.Core` projects. +- Rent arrays from `ArrayPool` when stack allocation is impractical or less efficient, and return them after use. +- Provide cancellation-aware and cancellation-free variants of hot methods so cancellation tokens are not checked in every loop iteration; handle cancellation in outer scopes. +- Prefer `for` loops over `foreach` to reduce enumerator allocations. +- Declare loop control variables outside the loop and reuse them across loops when possible. +- Cache static or read-only arrays in local variables before looping to avoid repeated class field lookups. +- Store collection `Count` or array `Length` in a local variable before the loop and use the cached value. +- Declare variables together outside loops when feasible and reuse them instead of creating new ones. +- Avoid repeated casting within loops by precomputing or using wider variables when necessary. + +## GPU-specific notes + +The following checks were executed with ILGPU 1.5.3 on CPU and OpenCL accelerators: + +- [x] `%` operator (kernels): supported for 32-bit and 64-bit integer types; not supported for `UInt128` inside kernels. +- [x] Integer division (kernels): supported for 32-bit and 64-bit types; not supported for `UInt128` inside kernels. +- [x] Arrays or vectors of `bool`: unsupported (`NotSupportedException`); use `byte` instead. +- [x] `UInt128` multiplication: unsupported in kernels (`AggregateException/NullReferenceException`); manual implementation required. +- [x] `UInt128` division: unsupported in kernels; manual implementation required. +- [x] `UInt128` bit shifts: attempts failed in kernels; avoid inside device code. +- [x] GPU context pooling: supported and recommended (validated in a separate program). + +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. diff --git a/EvenPerfectBitScanner.Tests/EvenPerfectBitScanner.Tests.csproj b/EvenPerfectBitScanner.Tests/EvenPerfectBitScanner.Tests.csproj new file mode 100644 index 0000000..97b8c66 --- /dev/null +++ b/EvenPerfectBitScanner.Tests/EvenPerfectBitScanner.Tests.csproj @@ -0,0 +1,25 @@ + + + net8.0 + enable + enable + false + true + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + diff --git a/EvenPerfectBitScanner.Tests/ProgramTests.cs b/EvenPerfectBitScanner.Tests/ProgramTests.cs new file mode 100644 index 0000000..b2d3d91 --- /dev/null +++ b/EvenPerfectBitScanner.Tests/ProgramTests.cs @@ -0,0 +1,92 @@ +using FluentAssertions; +using Xunit; +using PerfectNumbers.Core; +using System.Reflection; + +namespace EvenPerfectBitScanner.Tests; + +[Trait("Category", "Fast")] +public class ProgramTests +{ + [Fact] + public void CountOnes_returns_correct_count() + { + Program.CountOnes(0b101010UL).Should().Be(3UL); + } + + [Theory] + [InlineData(2UL, true)] + [InlineData(4UL, false)] + [InlineData(9UL, false)] + [InlineData(11UL, true)] + [InlineData(81UL, false)] + public void IsPrime_identifies_primes_correctly(ulong n, bool expected) + { + new PrimeTester().IsPrime(n, CancellationToken.None).Should().Be(expected); + } + + [Fact] + public void TransformPAdd_moves_to_next_candidate() + { + ulong remainder = 3UL; + Program.TransformPAdd(3UL, ref remainder).Should().Be(5UL); + Program.TransformPAdd(5UL, ref remainder).Should().Be(7UL); + Program.TransformPAdd(7UL, ref remainder).Should().Be(11UL); + } + + [Fact] + public void TransformPBit_appends_one_bit_and_skips_to_candidate() + { + ulong remainder = 5UL; + Program.TransformPBit(5UL, ref remainder).Should().Be(13UL); + } + + [Fact] + public void TransformPBit_detects_overflow_and_stops() + { + typeof(Program).GetField("_limitReached", BindingFlags.NonPublic | BindingFlags.Static)! + .SetValue(null, false); + + ulong start = (ulong.MaxValue >> 1) + 1UL; + ulong remainder = start % 6UL; + Program.TransformPBit(start, ref remainder); + + bool limit = (bool)typeof(Program).GetField("_limitReached", BindingFlags.NonPublic | BindingFlags.Static)! + .GetValue(null)!; + limit.Should().BeTrue(); + + typeof(Program).GetField("_limitReached", BindingFlags.NonPublic | BindingFlags.Static)! + .SetValue(null, false); + } + + [Fact] + public void Gcd_filter_detects_some_composites() + { + Program.IsCompositeByGcd(15UL).Should().BeTrue(); + Program.IsCompositeByGcd(5UL).Should().BeFalse(); + } + + [Fact] + [Trait("Category", "Fast")] + public void Main_displays_help_when_requested() + { + var main = typeof(Program).GetMethod("Main", BindingFlags.NonPublic | BindingFlags.Static)!; + using var writer = new StringWriter(); + TextWriter original = Console.Out; + Console.SetOut(writer); + + try + { + main.Invoke(null, [new[] { "--help" }]); + } + finally + { + Console.SetOut(original); + } + + string output = writer.ToString(); + output.Should().Contain("Usage:"); + output.Should().Contain("--mersenne-device=cpu|gpu"); + output.Should().Contain("--primes-device=cpu|gpu"); + } +} diff --git a/EvenPerfectBitScanner.Tests/ResultsFileNameTests.cs b/EvenPerfectBitScanner.Tests/ResultsFileNameTests.cs new file mode 100644 index 0000000..806ebbb --- /dev/null +++ b/EvenPerfectBitScanner.Tests/ResultsFileNameTests.cs @@ -0,0 +1,54 @@ +using System.Reflection; +using FluentAssertions; +using PerfectNumbers.Core.Gpu; +using Xunit; + +namespace EvenPerfectBitScanner.Tests; + +[Trait("Category", "Fast")] +public class ResultsFileNameTests +{ + [Fact] + public void BuildResultsFileName_contains_reduction_and_devices() + { + var programType = typeof(global::EvenPerfectBitScanner.Program); + var method = programType.GetMethod( + "BuildResultsFileName", + BindingFlags.NonPublic | BindingFlags.Static, + binder: null, + types: new[] + { + typeof(bool), typeof(int), typeof(int), typeof(global::PerfectNumbers.Core.GpuKernelType), + typeof(bool), typeof(bool), typeof(bool), typeof(bool), typeof(bool), typeof(NttBackend), typeof(int), typeof(int), typeof(int), typeof(ulong), typeof(ModReductionMode), typeof(string), typeof(string), typeof(string) + }, + modifiers: null)!; + + string name = (string)method.Invoke(null, new object[] + { + true, // bitInc + 8, // threads + 1, // block + global::PerfectNumbers.Core.GpuKernelType.Incremental, + false, // useLucasFlag + true, // mersenneOnGpu (unused inside name content, devices passed explicitly below) + false, // useOrder + false, // useModWorkaround + false, // useGcd + NttBackend.Staged, + 1, // gpuPrimeThreads + 32, // llSlice + 1024, // gpuScanBatch + 1000UL, // warmupLimit + ModReductionMode.Barrett128, + "gpu", // mersenneDevice + "gpu", // primesDevice + "gpu" // orderDevice + })!; + + name.Should().Contain("ntt-staged"); + name.Should().Contain("red-barrett128"); + name.Should().Contain("mersdev-gpu"); + name.Should().Contain("primesdev-gpu"); + name.Should().Contain("orderdev-gpu"); + } +} diff --git a/EvenPerfectBitScanner/EvenPerfectBitScanner.csproj b/EvenPerfectBitScanner/EvenPerfectBitScanner.csproj new file mode 100644 index 0000000..b57e28b --- /dev/null +++ b/EvenPerfectBitScanner/EvenPerfectBitScanner.csproj @@ -0,0 +1,15 @@ + + + Exe + net8.0 + enable + enable + true + + + + + <_Parameter1>EvenPerfectBitScanner.Tests + + + diff --git a/EvenPerfectBitScanner/Program.cs b/EvenPerfectBitScanner/Program.cs new file mode 100644 index 0000000..dae6d58 --- /dev/null +++ b/EvenPerfectBitScanner/Program.cs @@ -0,0 +1,1130 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Text; +using PerfectNumbers.Core; +using PerfectNumbers.Core.Gpu; + +namespace EvenPerfectBitScanner; + +internal static class Program +{ + private static ThreadLocal PrimeTesters = null!; + private static ThreadLocal MersenneTesters = null!; + private static ThreadLocal PResidue = null!; // p mod d tracker (per-thread) + private const ulong InitialP = PerfectNumberConstants.BiggestKnownEvenPerfectP; + + private const int ConsoleInterval = 100_000; + private const int WriteBatchSize = 100; + private const string DefaultCyclesPath = "divisor_cycles.bin"; + private static string ResultsFileName = "even_perfect_bit_scan_results.csv"; + private const string PrimeFoundSuffix = " (PRIME FOUND)"; + private static StringBuilder? _outputBuilder; + private static readonly object Sync = new(); + private static int _consoleCounter; + private static int _writeIndex; + private static int _primeCount; + private static bool _primeFoundAfterInit; + private static bool _useGcdFilter; + private static bool _useResidue; + private static ulong? _orderWarmupLimitOverride; + private static unsafe delegate* _transformP; + private static long _state; + private static bool _limitReached; + private static string? _resultsDir; + private static string? _resultsPrefix; + + private static long _lastProfileMs; + // RLE/bit-pattern filtering options + private static string? _rleBlacklistPath; + private static ulong _rleHardMaxP = ulong.MaxValue; + private static bool _rleOnlyLast7 = true; + private static int _writeBatchSize = WriteBatchSize; + private static double _zeroFracHard = -1.0; // disabled when < 0 + private static double _zeroFracConj = -1.0; // disabled when < 0 + private static int _maxZeroConj = -1; // disabled when < 0 + + private static unsafe void Main(string[] args) + { + ulong currentP = InitialP; + int threadCount = Environment.ProcessorCount; + int blockSize = 1; + int gpuPrimeThreads = 1; + int gpuPrimeBatch = 262_144; + GpuKernelType kernelType = GpuKernelType.Incremental; + bool useModuloWorkaround = false; + // removed: useModAutomaton + bool useOrder = false; + bool showHelp = false; + bool useBitTransform = false; + bool useLucas = false; + bool useResidue = false; // M_p test via residue divisors (replaces LL/incremental) + // Device routing + bool useGpuCycles = true; + bool mersenneOnGpu = true; // controls Lucas/incremental/pow2mod device + bool orderOnGpu = true; // controls order computations device + int scanBatchSize = 2_097_152, sliceSize = 32; + ulong residueKMax = 5_000_000UL; + string filterFile = string.Empty; + string cyclesPath = DefaultCyclesPath; + int cyclesBatchSize = 512; + bool continueCyclesGeneration = false; + + // NTT backend selection (GPU): reference vs staged + for (int i = 0; i < args.Length; i++) + { + string arg = args[i]; + if (arg.Equals("--?", StringComparison.OrdinalIgnoreCase) || arg.Equals("-?", StringComparison.OrdinalIgnoreCase) || arg.Equals("--help", StringComparison.OrdinalIgnoreCase) || arg.Equals("-help", StringComparison.OrdinalIgnoreCase) || arg.Equals("/?", StringComparison.OrdinalIgnoreCase)) + { + showHelp = true; + break; + } + else if (arg.StartsWith("--prime=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + currentP = ulong.Parse(arg.AsSpan(eq + 1)); + } + else if (arg.Equals("--increment=bit", StringComparison.OrdinalIgnoreCase)) + { + useBitTransform = true; + } + else if (arg.Equals("--increment=add", StringComparison.OrdinalIgnoreCase)) + { + useBitTransform = false; + } + else if (arg.StartsWith("--threads=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + threadCount = Math.Max(1, int.Parse(arg.AsSpan(eq + 1))); + } + else if (arg.StartsWith("--mersenne=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + ReadOnlySpan value = arg.AsSpan(eq + 1); + if (value.Equals("pow2mod", StringComparison.OrdinalIgnoreCase)) + { + kernelType = GpuKernelType.Pow2Mod; + } + else if (value.Equals("lucas", StringComparison.OrdinalIgnoreCase)) + { + useLucas = true; + } + else if (value.Equals("residue", StringComparison.OrdinalIgnoreCase)) + { + useResidue = true; + useLucas = false; + } + else + { + kernelType = GpuKernelType.Incremental; + } + } + else if (arg.StartsWith("--residue-max-k=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + if (ulong.TryParse(arg.AsSpan(eq + 1), out var kmax)) + { + residueKMax = kmax; + } + } + // Replaces --lucas=cpu|gpu; controls device for Lucas and for + // incremental/pow2mod scanning + else if (arg.StartsWith("--mersenne-device=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + ReadOnlySpan value = arg.AsSpan(eq + 1); + mersenneOnGpu = !value.Equals("cpu", StringComparison.OrdinalIgnoreCase); + } + else if (arg.Equals("--workaround-mod", StringComparison.OrdinalIgnoreCase)) + { + useModuloWorkaround = true; + } + else if (arg.Equals("--use-order", StringComparison.OrdinalIgnoreCase)) + { + useOrder = true; + } + else if (arg.StartsWith("--order-warmup-limit=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + // stored and used below when initializing testers + if (ulong.TryParse(arg.AsSpan(eq + 1), out var limit)) + { + _orderWarmupLimitOverride = limit; + } + } + else if (arg.Equals("--gcd-filter", StringComparison.OrdinalIgnoreCase)) + { + _useGcdFilter = true; + } + else if (arg.StartsWith("--ntt=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + ReadOnlySpan value = arg.AsSpan(eq + 1); + if (value.Equals("staged", StringComparison.OrdinalIgnoreCase)) + { + NttGpuMath.GpuTransformBackend = NttBackend.Staged; + } + else + { + NttGpuMath.GpuTransformBackend = NttBackend.Reference; + } + } + else if (arg.StartsWith("--mod-reduction=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + ReadOnlySpan value = arg.AsSpan(eq + 1); + if (value.Equals("mont64", StringComparison.OrdinalIgnoreCase)) + { + NttGpuMath.ReductionMode = ModReductionMode.Mont64; + } + else if (value.Equals("barrett128", StringComparison.OrdinalIgnoreCase)) + { + NttGpuMath.ReductionMode = ModReductionMode.Barrett128; + } + else if (value.Equals("uint128", StringComparison.OrdinalIgnoreCase)) + { + NttGpuMath.ReductionMode = ModReductionMode.GpuUInt128; + } + else + { + NttGpuMath.ReductionMode = ModReductionMode.Auto; + } + } + else if (arg.StartsWith("--primes-device=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + ReadOnlySpan value = arg.AsSpan(eq + 1); + // Controls default device for library prime-related GPU kernels + // that are not explicitly parameterized (backward compatibility). + GpuContextPool.ForceCpu = value.Equals("cpu", StringComparison.OrdinalIgnoreCase); + } + // New: choose device for order computations (warm-ups and order scans) + else if (arg.StartsWith("--order-device=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + ReadOnlySpan value = arg.AsSpan(eq + 1); + orderOnGpu = !value.Equals("cpu", StringComparison.OrdinalIgnoreCase); + } + // Optional: RLE blacklist for p + else if (arg.StartsWith("--rle-blacklist=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + _rleBlacklistPath = arg.Substring(eq + 1); + } + else if (arg.StartsWith("--rle-hard-max=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + if (ulong.TryParse(arg.AsSpan(eq + 1), out var maxP)) + { + _rleHardMaxP = maxP; + } + } + else if (arg.StartsWith("--rle-only-last7=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + ReadOnlySpan value = arg.AsSpan(eq + 1); + _rleOnlyLast7 = !value.Equals("false", StringComparison.OrdinalIgnoreCase) && !value.Equals("0", StringComparison.OrdinalIgnoreCase); + } + else if (arg.StartsWith("--zero-hard=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + if (double.TryParse(arg.AsSpan(eq + 1), out var z)) + { + _zeroFracHard = z; + } + } + else if (arg.StartsWith("--zero-conj=", StringComparison.OrdinalIgnoreCase)) + { + // format: : + int eq = arg.IndexOf('='); + ReadOnlySpan value = arg.AsSpan(eq + 1); + int colon = value.IndexOf(':'); + if (colon > 0) + { + if (double.TryParse(value[..colon], out var zf) && int.TryParse(value[(colon + 1)..], out var mz)) + { + _zeroFracConj = zf; + _maxZeroConj = mz; + } + } + } + // Backward-compat: accept deprecated device flags and map to new ones + else if (arg.StartsWith("--lucas=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + ReadOnlySpan value = arg.AsSpan(eq + 1); + mersenneOnGpu = !value.Equals("cpu", StringComparison.OrdinalIgnoreCase); + } + else if (arg.StartsWith("--primes=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + ReadOnlySpan value = arg.AsSpan(eq + 1); + GpuContextPool.ForceCpu = value.Equals("cpu", StringComparison.OrdinalIgnoreCase); + } + else if (arg.StartsWith("--gpu-kernels=", StringComparison.OrdinalIgnoreCase) || arg.StartsWith("--accelerator=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + ReadOnlySpan value = arg.AsSpan(eq + 1); + // Legacy alias: treat as primes-device for backward compat + GpuContextPool.ForceCpu = value.Equals("cpu", StringComparison.OrdinalIgnoreCase); + } + else if (arg.StartsWith("--results-dir=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + _resultsDir = arg.Substring(eq + 1); + } + else if (arg.StartsWith("--results-prefix=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + _resultsPrefix = arg.Substring(eq + 1); + } + + else if (arg.StartsWith("--gpu-prime-threads=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + gpuPrimeThreads = Math.Max(1, int.Parse(arg.AsSpan(eq + 1))); + } + else if (arg.StartsWith("--gpu-prime-batch=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + gpuPrimeBatch = Math.Max(1, int.Parse(arg.AsSpan(eq + 1))); + } + else if (arg.StartsWith("--ll-slice=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + sliceSize = Math.Max(1, int.Parse(arg.AsSpan(eq + 1))); + } + else if (arg.StartsWith("--gpu-scan-batch=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + scanBatchSize = Math.Max(1, int.Parse(arg.AsSpan(eq + 1))); + } + else if (arg.StartsWith("--block-size=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + blockSize = Math.Max(1, int.Parse(arg.AsSpan(eq + 1))); + } + else if (arg.StartsWith("--filter-p=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + filterFile = arg.AsSpan(eq + 1).ToString(); + } + else if (arg.StartsWith("--write-batch-size=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + _writeBatchSize = Math.Max(1, int.Parse(arg.AsSpan(eq + 1))); + } + if (arg.StartsWith("--divisor-cycles=")) + { + int eq = arg.IndexOf('='); + cyclesPath = arg.AsSpan(eq + 1).ToString(); + } + else if (arg.StartsWith("--divisor-cycles-device=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + ReadOnlySpan value = arg.AsSpan(eq + 1); + useGpuCycles = !value.Equals("cpu", StringComparison.OrdinalIgnoreCase); + } + else if (arg.StartsWith("--divisor-cycles-batch-size=", StringComparison.OrdinalIgnoreCase)) + { + int eq = arg.IndexOf('='); + cyclesBatchSize = Math.Max(1, int.Parse(arg.AsSpan(eq + 1))); + } + else if (arg.Equals("--divisor-cycles-continue", StringComparison.OrdinalIgnoreCase)) + { + continueCyclesGeneration = true; + } + } + + if (showHelp) + { + PrintHelp(); + return; + } + + // Apply GPU prime sieve runtime configuration + GpuPrimeWorkLimiter.SetLimit(gpuPrimeThreads); + PerfectNumbers.Core.PrimeTester.GpuBatchSize = Math.Max(1, gpuPrimeBatch); + + MersenneDivisorCycles mersenneDivisorCycles = new(); + if (!File.Exists(cyclesPath) || continueCyclesGeneration) + { + Console.WriteLine($"Generating divisor cycles '{cyclesPath}' for all p <= {PerfectNumberConstants.MaxQForDivisorCycles}..."); + + long nextPosition = 0L, completeCount = 0L; + if (continueCyclesGeneration && File.Exists(cyclesPath)) + { + Console.WriteLine("Finding last divisor position..."); + (nextPosition, completeCount) = MersenneDivisorCycles.FindLast(cyclesPath); + } + + if (useGpuCycles) + { + if (!continueCyclesGeneration || completeCount == 0L) + { + Console.WriteLine("Starting generation..."); + } + else + { + Console.WriteLine("Resuming generation..."); + } + + MersenneDivisorCycles.GenerateGpu(cyclesPath, PerfectNumberConstants.MaxQForDivisorCycles, cyclesBatchSize, skipCount: completeCount, nextPosition: nextPosition); + } + else + { + MersenneDivisorCycles.Generate(cyclesPath, PerfectNumberConstants.MaxQForDivisorCycles); + } + } + + Console.WriteLine($"Loading divisor cycles into memory..."); + if (string.Equals(Path.GetExtension(cyclesPath), ".csv", StringComparison.OrdinalIgnoreCase)) + { + MersenneDivisorCycles.Shared.LoadFrom(cyclesPath); + } + else + { + MersenneDivisorCycles.Shared.LoadFrom(cyclesPath); + } + + Console.WriteLine("Divisor cycles are ready"); + + _useResidue = useResidue; + _transformP = useBitTransform ? &TransformPBit : &TransformPAdd; + PResidue = new ThreadLocal(() => new ModResidueTracker(ResidueModel.Identity, initialNumber: currentP, initialized: true), trackAllValues: true); + PrimeTesters = new ThreadLocal(() => new PrimeTester(), trackAllValues: true); + // Note: --primes-device controls default device for library kernels; p primality remains CPU here. + // Initialize per-thread p residue tracker (Identity model) at currentP + MersenneTesters = new ThreadLocal(() => + { + var tester = new MersenneNumberTester( + useIncremental: !useLucas, + useOrderCache: false, + kernelType: kernelType, + useModuloWorkaround: useModuloWorkaround, + useOrder: useOrder, + useGpuLucas: mersenneOnGpu, + useGpuScan: mersenneOnGpu, + useGpuOrder: orderOnGpu, + useResidue: useResidue, + maxK: residueKMax); + if (!useLucas) + { + tester.WarmUpOrders(currentP, _orderWarmupLimitOverride ?? 5_000_000UL); + } + + return tester; + }, trackAllValues: true); + + // Load RLE blacklist (optional) + if (!string.IsNullOrEmpty(_rleBlacklistPath)) + { + PerfectNumbers.Core.RleBlacklist.Load(_rleBlacklistPath!); + } + + ulong remainder = currentP % 6UL; + if (currentP == InitialP) + { + // bool isPerfect = IsEvenPerfectCandidate(InitialP, out bool searchedMersenne, out bool detailedCheck); + // Skip the already processed range below 138 million + while (currentP < 138_000_000UL && !Volatile.Read(ref _limitReached)) + { + currentP = _transformP(currentP, ref remainder); + } + + if (Volatile.Read(ref _limitReached)) + { + return; + } + } + + Console.WriteLine("Initialization..."); + // Compose a results file name that encodes configuration (before opening file) + var builtName = BuildResultsFileName( + useBitTransform, + threadCount, + blockSize, + kernelType, + useLucas, + mersenneOnGpu, + useOrder, + useModuloWorkaround, + _useGcdFilter, + NttGpuMath.GpuTransformBackend, + gpuPrimeThreads, + sliceSize, + scanBatchSize, + _orderWarmupLimitOverride ?? 5_000_000UL, + NttGpuMath.ReductionMode, + mersenneOnGpu ? "gpu" : "cpu", + (GpuContextPool.ForceCpu ? "cpu" : "gpu"), + orderOnGpu ? "gpu" : "cpu"); + + if (!string.IsNullOrEmpty(_resultsPrefix)) + { + builtName = _resultsPrefix + "_" + builtName; + } + ResultsFileName = string.IsNullOrEmpty(_resultsDir) ? builtName : Path.Combine(_resultsDir!, builtName); + var dir = Path.GetDirectoryName(ResultsFileName); + if (!string.IsNullOrEmpty(dir)) + { + Directory.CreateDirectory(dir!); + } + + + if (File.Exists(ResultsFileName)) + { + Console.WriteLine("Processing previous results..."); + LoadResultsFile(ResultsFileName, (p, detailedCheck, isPerfect) => + { + if (detailedCheck && isPerfect) + { + _primeCount++; + } + }); + } + else + { + File.WriteAllText(ResultsFileName, "p,searchedMersenne,detailedCheck,isPerfect"); + } + + bool useFilter = !string.IsNullOrEmpty(filterFile); + List filter = []; + if (useFilter) + { + Console.WriteLine("Loading filter..."); + LoadResultsFile(filterFile, (p, detailedCheck, isPerfect) => + { + if (detailedCheck && isPerfect) + { + Console.WriteLine($"Adding {p}"); + filter.Add(p); + } + }); + + filter.Sort(); + } + + if (_primeCount >= 2) + { + _primeFoundAfterInit = true; + } + + _outputBuilder = StringBuilderPool.Rent(); + + // Limit GPU concurrency only for prime checks (LL/NTT & GPU order scans). + GpuPrimeWorkLimiter.SetLimit(gpuPrimeThreads); + // Configure batch size for GPU primality sieve + PerfectNumbers.Core.PrimeTester.GpuBatchSize = gpuPrimeBatch; + + // Compose a results file name that encodes configuration + ResultsFileName = BuildResultsFileName( + useBitTransform, + threadCount, + blockSize, + kernelType, + useLucas, + mersenneOnGpu, + useOrder, + useModuloWorkaround, + _useGcdFilter, + NttGpuMath.GpuTransformBackend, + gpuPrimeThreads, + sliceSize, + scanBatchSize, + _orderWarmupLimitOverride ?? 5_000_000UL, + NttGpuMath.ReductionMode, + mersenneOnGpu ? "gpu" : "cpu", + GpuContextPool.ForceCpu ? "cpu" : "gpu", + orderOnGpu ? "gpu" : "cpu"); + + Console.WriteLine("Warming up orders..."); + + threadCount = Math.Max(1, threadCount); + _ = MersenneTesters.Value; + if (!useLucas) + { + MersenneTesters.Value!.WarmUpOrders(currentP, _orderWarmupLimitOverride ?? 5_000_000UL); + } + + Console.WriteLine("Starting scan..."); + _state = ((long)currentP << 3) | (long)remainder; + Task[] tasks = new Task[threadCount]; + + for (int i = 0; i < threadCount; i++) + { + tasks[i] = Task.Run(() => + { + ulong[] buffer = new ulong[blockSize]; + while (!Volatile.Read(ref _limitReached)) + { + int count = 0; + while (count < blockSize && !Volatile.Read(ref _limitReached)) + { + buffer[count] = ReserveNextP(); + count++; + } + + if (count == 0) + { + break; + } + + for (int j = 0; j < count && !Volatile.Read(ref _limitReached); j++) + { + ulong p = buffer[j]; + if (useFilter && !filter.Contains(p)) + { + continue; + } + else if (useFilter) + { + Console.WriteLine($"Testing {p}"); + } + + bool isPerfect = IsEvenPerfectCandidate(p, out bool searchedMersenne, out bool detailedCheck); + PrintResult(p, searchedMersenne, detailedCheck, isPerfect); + } + } + }); + } + + Task.WaitAll(tasks); + FlushBuffer(); + StringBuilderPool.Return(_outputBuilder!); + } + + private static unsafe void LoadResultsFile(string resultsFileName, Action lineProcessorAction) + { + using FileStream readStream = new(resultsFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using StreamReader reader = new(readStream); + string? line; + bool headerSkipped = false; + while ((line = reader.ReadLine()) != null) + { + if (!headerSkipped) + { + headerSkipped = true; + continue; + } + + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + ReadOnlySpan span = line.AsSpan(); + int first = span.IndexOf(','); + if (first < 0) + { + continue; + } + + ulong p = ulong.Parse(span[..first]); + span = span[(first + 1)..]; + int second = span.IndexOf(','); + if (second < 0) + { + continue; + } + + span = span[(second + 1)..]; + int third = span.IndexOf(','); + if (third < 0) + { + continue; + } + + ReadOnlySpan detailedSpan = span[..third]; + ReadOnlySpan perfectSpan = span[(third + 1)..]; + + if (bool.TryParse(detailedSpan, out bool detailed) && bool.TryParse(perfectSpan, out bool isPerfect)) + { + lineProcessorAction(p, detailed, isPerfect); + } + } + } + + private static void PrintHelp() + { + Console.WriteLine("Usage: EvenPerfectBitScanner [options]"); + Console.WriteLine(); + Console.WriteLine("Options:"); + Console.WriteLine(" --prime= starting exponent (p)"); + Console.WriteLine(" --increment=bit|add exponent increment method"); + Console.WriteLine(" --threads= number of worker threads"); + Console.WriteLine(" --block-size= values processed per thread batch"); + Console.WriteLine(" --mersenne=pow2mod|incremental|lucas|residue Mersenne test method"); + Console.WriteLine(" --residue-max-k= max k for residue Mersenne test (q = 2*p*k + 1)"); + Console.WriteLine(" --mersenne-device=cpu|gpu Device for Mersenne method (default gpu)"); + Console.WriteLine(" --primes-device=cpu|gpu Device for prime-scan kernels (default gpu)"); + Console.WriteLine(" --gpu-prime-batch= Batch size for GPU primality sieve (default 262144)"); + Console.WriteLine(" --order-device=cpu|gpu Device for order computations (default gpu)"); + Console.WriteLine(" --ntt=reference|staged GPU NTT backend (default staged)"); + Console.WriteLine(" --mod-reduction=auto|uint128|mont64|barrett128 staged NTT reduction (default auto)"); + Console.WriteLine(" --gpu-prime-threads= max concurrent GPU prime checks (default 1)"); + Console.WriteLine(" --ll-slice= Lucas–Lehmer iterations per slice (default 32)"); + Console.WriteLine(" --gpu-scan-batch= GPU q-scan batch size (default 2_097_152)"); + Console.WriteLine(" --order-warmup-limit= Warm-up order candidates (default 5_000_000)"); + Console.WriteLine(" --rle-blacklist= enable RLE blacklist for p (hard filter up to --rle-hard-max)"); + Console.WriteLine(" --rle-hard-max=

apply RLE blacklist only for p <= this (default ulong.MaxValue = no limit)"); + Console.WriteLine(" --rle-only-last7=true|false apply RLE only when p % 10 == 7 (default true)"); + Console.WriteLine(" --zero-hard= hard reject if zero_fraction(p) > f (default off)"); + Console.WriteLine(" --zero-conj=: hard reject if zero_fraction(p) > f AND max_zero_block >= r (default off)"); + Console.WriteLine(" --results-dir= directory for results file"); + Console.WriteLine(" --results-prefix= prefix to prepend to results filename"); + + Console.WriteLine(" --use-order test primality via q order"); + Console.WriteLine(" --workaround-mod avoid '%' operator on the GPU"); + // mod-automaton removed + Console.WriteLine(" --filter-p= process only p from previous run results"); + Console.WriteLine(" --write-batch-size= overwrite frequency of disk writes (default 100 lines)"); + Console.WriteLine(" --gcd-filter enable early sieve based on GCD"); + Console.WriteLine(" --help, -help, --?, -?, /? show this help message"); + } + + private static string BuildResultsFileName(bool bitInc, int threads, int block, GpuKernelType kernelType, bool useLucasFlag, bool mersenneOnGpu, bool useOrder, bool useModWorkaround, bool useGcd, NttBackend nttBackend, int gpuPrimeThreads, int llSlice, int gpuScanBatch, ulong warmupLimit, ModReductionMode reduction, string mersenneDevice, string primesDevice, string orderDevice) + { + string inc = bitInc ? "bit" : "add"; + string mers = useLucasFlag ? "lucas" : (kernelType == GpuKernelType.Pow2Mod ? "pow2mod" : "incremental"); + string ntt = nttBackend == NttBackend.Staged ? "staged" : "reference"; + string red = reduction switch { ModReductionMode.Mont64 => "mont64", ModReductionMode.Barrett128 => "barrett128", ModReductionMode.GpuUInt128 => "uint128", _ => "auto" }; + string order = useOrder ? "order-on" : "order-off"; + string gcd = useGcd ? "gcd-on" : "gcd-off"; + string work = useModWorkaround ? "modfix-on" : "modfix-off"; + return $"even_perfect_bit_scan_inc-{inc}_thr-{threads}_blk-{block}_mers-{mers}_mersdev-{mersenneDevice}_ntt-{ntt}_red-{red}_primesdev-{primesDevice}_{order}_orderdev-{orderDevice}_{gcd}_{work}_gputh-{gpuPrimeThreads}_llslice-{llSlice}_scanb-{gpuScanBatch}_warm-{warmupLimit}.csv"; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe ulong ReserveNextP() + { + while (true) + { + long state = Volatile.Read(ref _state); + ulong current = (ulong)state >> 3; + ulong remainder = ((ulong)state) & 7UL; + + ulong next = _transformP(current, ref remainder); + long newState = ((long)next << 3) | (long)remainder; + long original = Interlocked.CompareExchange(ref _state, newState, state); + if (original == state) + { + return current; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void PrintResult(ulong currentP, bool searchedMersenne, bool detailedCheck, bool isPerfect) + { + if (isPerfect && detailedCheck) + { + int newCount = Interlocked.Increment(ref _primeCount); + if (newCount >= 2) + { + Volatile.Write(ref _primeFoundAfterInit, true); + } + } + + bool primeFlag = false, printToConsole = false; + + StringBuilder localBuilder = StringBuilderPool.Rent(); + _ = localBuilder + .Append(currentP).Append(',') + .Append(searchedMersenne).Append(',') + .Append(detailedCheck).Append(',') + .Append(isPerfect).Append('\n'); + + lock (Sync) + { + _ = _outputBuilder!.Append(localBuilder); + + if (_consoleCounter >= ConsoleInterval) + { + printToConsole = true; + primeFlag = _primeFoundAfterInit; + _consoleCounter = 0; + } + else + { + _consoleCounter++; + } + + _writeIndex++; + if (_writeIndex >= _writeBatchSize) + { + FlushBuffer(); + } + } + + if (printToConsole) + { + if (primeFlag) + { + _ = localBuilder + .Remove(localBuilder.Length - 1, 1) + .Append(PrimeFoundSuffix) + .Append('\n'); + } + + Console.WriteLine(localBuilder.Remove(localBuilder.Length - 1, 1).ToString()); + } + + StringBuilderPool.Return(localBuilder); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void FlushBuffer() + { + var stream = new FileStream(ResultsFileName, FileMode.Append, FileAccess.Write, FileShare.Read); + var writer = new StreamWriter(stream) { AutoFlush = false }; + + if (_writeIndex > 0) + { + writer.Write(_outputBuilder); + writer.Flush(); + _outputBuilder!.Clear(); + _writeIndex = 0; + } + + writer.Dispose(); + stream.Dispose(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ulong CountOnes(ulong value) + { + return (ulong)BitOperations.PopCount(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ulong TransformPBit(ulong value, ref ulong remainder) + { + ulong original = value; + if (value > (ulong.MaxValue >> 1)) + { + Volatile.Write(ref _limitReached, true); + return value; + } + + ulong next = (value << 1) | 1UL; + remainder = (remainder << 1) + 1UL; + remainder -= (remainder >= 6UL) ? 6UL : 0UL; + remainder -= (remainder >= 6UL) ? 6UL : 0UL; + + value = remainder switch + { + 0UL => 1UL, + 1UL => 0UL, + 2UL => 5UL, + 3UL => 4UL, + 4UL => 3UL, + _ => 2UL, + }; // 'value' now holds diff + + if (next > ulong.MaxValue - value) + { + Volatile.Write(ref _limitReached, true); + return original; + } + + remainder += value; + remainder -= (remainder >= 6UL) ? 6UL : 0UL; + + return next + value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ulong TransformPAdd(ulong value, ref ulong remainder) + { + ulong next = value; + value = remainder switch + { + 0UL => 1UL, + 1UL => 4UL, + 2UL => 3UL, + 3UL => 2UL, + 4UL => 1UL, + _ => 2UL, + }; // 'value' now holds diff + + if (next > ulong.MaxValue - value) + { + Volatile.Write(ref _limitReached, true); + return next; + } + + remainder += value; + remainder -= (remainder >= 6UL) ? 6UL : 0UL; + + return next + value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsEvenPerfectCandidate(ulong p) + { + return IsEvenPerfectCandidate(p, out _, out _); + } + + internal static bool IsEvenPerfectCandidate(ulong p, out bool searchedMersenne, out bool detailedCheck) + { + searchedMersenne = false; + detailedCheck = false; + + if (_useGcdFilter && IsCompositeByGcd(p)) + { + return false; + } + + // Optional: RLE blacklist and binary-threshold filters on p (safe only when configured) + if (p <= _rleHardMaxP) + { + if (!_rleOnlyLast7 || (p % 10UL) == 7UL) + { + if (PerfectNumbers.Core.RleBlacklist.IsLoaded() && PerfectNumbers.Core.RleBlacklist.Matches(p)) + { + return false; + } + } + } + + if (_zeroFracHard >= 0 || (_zeroFracConj >= 0 && _maxZeroConj >= 0)) + { + double zf; + ComputeBitStats(p, out int bitLen, out int zeroCount, out int maxZeroBlock); + if (_zeroFracHard >= 0) + { + zf = (double)zeroCount / bitLen; + if (zf > _zeroFracHard) + { + return false; + } + } + + if (_zeroFracConj >= 0 && _maxZeroConj >= 0) + { + zf = (double)zeroCount / bitLen; + if (zf > _zeroFracConj && maxZeroBlock >= _maxZeroConj) + { + return false; + } + } + } + + // Fast residue-based composite check for p using small primes + if (IsCompositeByResidues(p)) + { + searchedMersenne = false; + detailedCheck = false; + return false; + } + + // If primes-device=gpu, route p primality through GPU-assisted sieve (optional MR later) + // TODO: Add deterministic Miller–Rabin rounds in GPU path for 64-bit range. + bool isPrimeP = GpuContextPool.ForceCpu + ? PrimeTesters.Value!.IsPrime(p, CancellationToken.None) + : PrimeTesters.Value!.IsPrimeGpu(p, CancellationToken.None); + if (!isPrimeP) + { + return false; + } + + searchedMersenne = true; + detailedCheck = MersenneTesters.Value!.IsMersennePrime(p); + return detailedCheck; + } + + // Use ModResidueTracker with a small set of primes to prefilter composite p. + private static bool IsCompositeByResidues(ulong p) + { + var tracker = PResidue.Value!; + tracker.BeginMerge(p); + // Use the small prime list from PerfectNumbers.Core to the extent of sqrt(p) + var primes = PrimesGenerator.SmallPrimes; + var primesPow2 = PrimesGenerator.SmallPrimesPow2; + int i, len = primes.Length; + bool divisible; + for (i = 0; i < len; i++) + { + if (primesPow2[i] > p) { break; } + + if (tracker.MergeOrAppend(p, primes[i], out divisible) && divisible) + { + return true; + } + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ComputeBitStats(ulong value, out int bitLen, out int zeroCount, out int maxZeroBlock) + { + bitLen = 64 - int.CreateChecked(ulong.LeadingZeroCount(value)); + zeroCount = 0; + maxZeroBlock = 0; + if (bitLen <= 0) + { + return; + } + + int msbIndex = (bitLen - 1) >> 3; // 0..7 + int bitsInTopByte = ((bitLen - 1) & 7) + 1; // 1..8 + int currentRun = 0; + + for (int bi = msbIndex; bi >= 0; bi--) + { + byte b = (byte)(value >> (bi * 8)); + if (bi == msbIndex && bitsInTopByte < 8) + { + // Mask off leading unused bits by setting them to 1 (ignore) + b |= (byte)(0xFF << bitsInTopByte); + } + + int zc = ByteZeroCount[b]; + zeroCount += zc; + + if (zc == 8) + { + currentRun += 8; + if (currentRun > maxZeroBlock) + { + maxZeroBlock = currentRun; + } + + continue; + } + + int pref = BytePrefixZero[b]; + int suff = ByteSuffixZero[b]; + int maxIn = ByteMaxZeroRun[b]; + + int candidate = currentRun + pref; + if (candidate > maxZeroBlock) + { + maxZeroBlock = candidate; + } + + if (maxIn > maxZeroBlock) + { + maxZeroBlock = maxIn; + } + + currentRun = suff; + } + + if (currentRun > maxZeroBlock) + { + maxZeroBlock = currentRun; + } + } + + // LUTs for fast per-byte zero stats (MSB-first in each byte) + private static readonly byte[] ByteZeroCount = new byte[256]; + private static readonly byte[] BytePrefixZero = new byte[256]; + private static readonly byte[] ByteSuffixZero = new byte[256]; + private static readonly byte[] ByteMaxZeroRun = new byte[256]; + + static Program() + { + for (int v = 0; v < 256; v++) + { + int zeros = 0; + int pref = 0; + int suff = 0; + int maxIn = 0; + int run = 0; + + // MSB-first within byte: bit 7 .. bit 0 + for (int bit = 7; bit >= 0; bit--) + { + bool isZero = ((v >> bit) & 1) == 0; + if (isZero) + { + zeros++; + run++; + if (run > maxIn) + { + maxIn = run; + } + } + else + { + run = 0; + } + + if (bit == 7) + { + // leading zeros (prefix) + int t = 0; + for (int b2 = 7; b2 >= 0; b2--) + { + if (((v >> b2) & 1) == 0) { t++; } else { break; } + } + pref = t; + } + + if (bit == 0) + { + // trailing zeros (suffix) + int t = 0; + for (int b2 = 0; b2 < 8; b2++) + { + if (((v >> b2) & 1) == 0) { t++; } else { break; } + } + suff = t; + } + } + + ByteZeroCount[v] = (byte)zeros; + BytePrefixZero[v] = (byte)pref; + ByteSuffixZero[v] = (byte)suff; + ByteMaxZeroRun[v] = (byte)maxIn; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsCompositeByGcd(ulong p) + { + if (p < 2) + { + return true; + } + + ulong m = (ulong)BitOperations.Log2(p); + return BinaryGcd(p, m) > 1UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong BinaryGcd(ulong a, ulong b) + { + if (a == 0UL) + { + return b; + } + + if (b == 0UL) + { + return a; + } + + int shift = BitOperations.TrailingZeroCount(a | b); + a >>= BitOperations.TrailingZeroCount(a); + + do + { + b >>= BitOperations.TrailingZeroCount(b); + + if (a > b) + { + (a, b) = (b, a); + } + + b -= a; + } + while (b != 0UL); + + return a << shift; + } + + +} diff --git a/EvenPerfectScanner.sln b/EvenPerfectScanner.sln new file mode 100644 index 0000000..2321df5 --- /dev/null +++ b/EvenPerfectScanner.sln @@ -0,0 +1,40 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PerfectNumbers.Core", "PerfectNumbers.Core\PerfectNumbers.Core.csproj", "{FFD557C9-12AE-4D8D-A21A-44CEE197BFB6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PerfectNumbers.Core.Tests", "PerfectNumbers.Core.Tests\PerfectNumbers.Core.Tests.csproj", "{FA4063F7-CB3C-4A66-BE70-4C6535D90197}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EvenPerfectBitScanner", "EvenPerfectBitScanner\EvenPerfectBitScanner.csproj", "{553D1735-214B-4097-B647-D73DB2CB9ADA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EvenPerfectBitScanner.Tests", "EvenPerfectBitScanner.Tests\EvenPerfectBitScanner.Tests.csproj", "{65ED9CC9-BB94-4FA5-940E-B84B3EE87DA7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FFD557C9-12AE-4D8D-A21A-44CEE197BFB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FFD557C9-12AE-4D8D-A21A-44CEE197BFB6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FFD557C9-12AE-4D8D-A21A-44CEE197BFB6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FFD557C9-12AE-4D8D-A21A-44CEE197BFB6}.Release|Any CPU.Build.0 = Release|Any CPU + {FA4063F7-CB3C-4A66-BE70-4C6535D90197}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA4063F7-CB3C-4A66-BE70-4C6535D90197}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA4063F7-CB3C-4A66-BE70-4C6535D90197}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA4063F7-CB3C-4A66-BE70-4C6535D90197}.Release|Any CPU.Build.0 = Release|Any CPU + {553D1735-214B-4097-B647-D73DB2CB9ADA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {553D1735-214B-4097-B647-D73DB2CB9ADA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {553D1735-214B-4097-B647-D73DB2CB9ADA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {553D1735-214B-4097-B647-D73DB2CB9ADA}.Release|Any CPU.Build.0 = Release|Any CPU + {65ED9CC9-BB94-4FA5-940E-B84B3EE87DA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65ED9CC9-BB94-4FA5-940E-B84B3EE87DA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65ED9CC9-BB94-4FA5-940E-B84B3EE87DA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65ED9CC9-BB94-4FA5-940E-B84B3EE87DA7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal + diff --git a/PerfectNumbers.Core.Tests/AlphaCacheTests.cs b/PerfectNumbers.Core.Tests/AlphaCacheTests.cs new file mode 100644 index 0000000..7cd8edf --- /dev/null +++ b/PerfectNumbers.Core.Tests/AlphaCacheTests.cs @@ -0,0 +1,12 @@ +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +public class AlphaCacheTests +{ + [Fact(Skip = "TODO: implement test")] + [Trait("Category", "Fast")] + public void Placeholder() + { + } +} diff --git a/PerfectNumbers.Core.Tests/AlphaCalculationsTests.cs b/PerfectNumbers.Core.Tests/AlphaCalculationsTests.cs new file mode 100644 index 0000000..575f834 --- /dev/null +++ b/PerfectNumbers.Core.Tests/AlphaCalculationsTests.cs @@ -0,0 +1,12 @@ +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +public class AlphaCalculationsTests +{ + [Fact(Skip = "TODO: implement test")] + [Trait("Category", "Fast")] + public void Placeholder() + { + } +} diff --git a/PerfectNumbers.Core.Tests/EulerPrimeTesterTests.cs b/PerfectNumbers.Core.Tests/EulerPrimeTesterTests.cs new file mode 100644 index 0000000..20f587b --- /dev/null +++ b/PerfectNumbers.Core.Tests/EulerPrimeTesterTests.cs @@ -0,0 +1,22 @@ +using FluentAssertions; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +[Trait("Category", "Fast")] +public class EulerPrimeTesterTests +{ + private readonly EulerPrimeTester _tester = new(new PrimeTester()); + + [Theory] + [InlineData(13UL, true)] + [InlineData(29UL, true)] + [InlineData(3UL, false)] + [InlineData(21UL, false)] + [InlineData(1UL, false)] + public void IsEulerPrime_returns_expected(ulong n, bool expected) + { + _tester.IsEulerPrime(n).Should().Be(expected); + } +} + diff --git a/PerfectNumbers.Core.Tests/Gpu/GpuContextPoolTests.cs b/PerfectNumbers.Core.Tests/Gpu/GpuContextPoolTests.cs new file mode 100644 index 0000000..9d60dd9 --- /dev/null +++ b/PerfectNumbers.Core.Tests/Gpu/GpuContextPoolTests.cs @@ -0,0 +1,69 @@ +using FluentAssertions; +using ILGPU; +using ILGPU.Runtime; +using PerfectNumbers.Core.Gpu; +using Xunit; + +namespace PerfectNumbers.Core.Tests.Gpu; + +public class GpuContextPoolTests +{ + [Fact] + [Trait("Category", "Fast")] + public void RentPreferred_cpu_returns_cpu_accelerator() + { + GpuContextPool.DisposeAll(); + + using var lease = GpuContextPool.RentPreferred(preferCpu: true); + + lease.Accelerator.AcceleratorType.Should().Be(AcceleratorType.CPU); + + GpuContextPool.DisposeAll(); + } + + [Fact] + [Trait("Category", "Fast")] + public void RentPreferred_reuses_cpu_context() + { + GpuContextPool.DisposeAll(); + + Context firstContext; + Accelerator firstAccelerator; + using (var lease = GpuContextPool.RentPreferred(preferCpu: true)) + { + firstContext = lease.Context; + firstAccelerator = lease.Accelerator; + } + + using (var lease = GpuContextPool.RentPreferred(preferCpu: true)) + { + lease.Context.Should().BeSameAs(firstContext); + lease.Accelerator.Should().BeSameAs(firstAccelerator); + } + + GpuContextPool.DisposeAll(); + } + + [Fact] + [Trait("Category", "Fast")] + public void DisposeAll_clears_pooled_contexts() + { + GpuContextPool.DisposeAll(); + + Context firstContext; + using (var lease = GpuContextPool.RentPreferred(preferCpu: true)) + { + firstContext = lease.Context; + } + + GpuContextPool.DisposeAll(); + + using (var lease = GpuContextPool.RentPreferred(preferCpu: true)) + { + lease.Context.Should().NotBeSameAs(firstContext); + } + + GpuContextPool.DisposeAll(); + } +} + diff --git a/PerfectNumbers.Core.Tests/Gpu/GpuNttCollection.cs b/PerfectNumbers.Core.Tests/Gpu/GpuNttCollection.cs new file mode 100644 index 0000000..aa5f97a --- /dev/null +++ b/PerfectNumbers.Core.Tests/Gpu/GpuNttCollection.cs @@ -0,0 +1,9 @@ +using Xunit; + +namespace PerfectNumbers.Core.Tests.Gpu; + +[CollectionDefinition("GpuNtt", DisableParallelization = true)] +public class GpuNttCollection +{ +} + diff --git a/PerfectNumbers.Core.Tests/Gpu/GpuPrimeLimiterTests.cs b/PerfectNumbers.Core.Tests/Gpu/GpuPrimeLimiterTests.cs new file mode 100644 index 0000000..3b605cc --- /dev/null +++ b/PerfectNumbers.Core.Tests/Gpu/GpuPrimeLimiterTests.cs @@ -0,0 +1,45 @@ +using System.Diagnostics; +using FluentAssertions; +using PerfectNumbers.Core.Gpu; +using Xunit; + +namespace PerfectNumbers.Core.Tests.Gpu; + +[Collection("GpuNtt")] +[Trait("Category", "Fast")] +public class GpuPrimeLimiterTests +{ + [Fact] + public async Task Acquire_blocks_when_limit_reached() + { + // Ensure limit = 1 so second acquire must wait. + GpuPrimeWorkLimiter.SetLimit(1); + + var startedSecond = new TaskCompletionSource(); + var sw = new Stopwatch(); + + using var first = GpuPrimeWorkLimiter.Acquire(); + sw.Start(); + + var secondTask = Task.Run(() => + { + using var second = GpuPrimeWorkLimiter.Acquire(); + startedSecond.TrySetResult(true); + }); + + // Give secondTask a moment to attempt acquire (it should block). + await Task.Delay(50); + startedSecond.Task.IsCompleted.Should().BeFalse(); + + // Release first after short delay, allowing second to continue. + // (Dispose at scope end, but we want to end now.) + first.Dispose(); + + await secondTask.WaitAsync(TimeSpan.FromSeconds(1)); + sw.Stop(); + + // Should be > ~50ms because second had to wait for first to release. + sw.ElapsedMilliseconds.Should().BeGreaterThanOrEqualTo(50); + } +} + diff --git a/PerfectNumbers.Core.Tests/Gpu/GpuUInt128Tests.cs b/PerfectNumbers.Core.Tests/Gpu/GpuUInt128Tests.cs new file mode 100644 index 0000000..04b68a1 --- /dev/null +++ b/PerfectNumbers.Core.Tests/Gpu/GpuUInt128Tests.cs @@ -0,0 +1,342 @@ +using FluentAssertions; +using PerfectNumbers.Core.Gpu; +using Xunit; + +[Trait("Category", "Fast")] +public class GpuUInt128Tests +{ + [Fact] + public void Mul_matches_UInt128() + { + var random = new Random(0); + for (var i = 0; i < 100; i++) + { + var high1 = ((ulong)random.Next() << 32) | (uint)random.Next(); + var low1 = ((ulong)random.Next() << 32) | (uint)random.Next(); + var high2 = ((ulong)random.Next() << 32) | (uint)random.Next(); + var low2 = ((ulong)random.Next() << 32) | (uint)random.Next(); + var a = new GpuUInt128(high1, low1); + var b = new GpuUInt128(high2, low2); + var expected = (UInt128)new GpuUInt128(high1, low1) * (UInt128)new GpuUInt128(high2, low2); + a = a.Mul(b); + ((UInt128)a).Should().Be(expected); + } + } + + [Fact] + public void MulMod_matches_UInt128() + { + var random = new Random(1); + for (var i = 0; i < 100; i++) + { + var high1 = ((ulong)random.Next() << 32) | (uint)random.Next(); + var low1 = ((ulong)random.Next() << 32) | (uint)random.Next(); + var high2 = ((ulong)random.Next() << 32) | (uint)random.Next(); + var low2 = ((ulong)random.Next() << 32) | (uint)random.Next(); + var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next(); + var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL; + var modulus = new GpuUInt128(modHigh, modLow); + var a = new GpuUInt128((UInt128)new GpuUInt128(high1, low1) % (UInt128)modulus); + var b = new GpuUInt128((UInt128)new GpuUInt128(high2, low2) % (UInt128)modulus); + var expected = MulModReference((UInt128)a, (UInt128)b, (UInt128)modulus); + a = a.MulMod(b, modulus); + ((UInt128)a).Should().Be(expected); + } + } + + [Fact] + public void MulMod_with_ulong_matches_UInt128() + { + var random = new Random(5); + for (var i = 0; i < 100; i++) + { + var high1 = ((ulong)random.Next() << 32) | (uint)random.Next(); + var low1 = ((ulong)random.Next() << 32) | (uint)random.Next(); + var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next(); + var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL; + var modulus = new GpuUInt128(modHigh, modLow); + var a = new GpuUInt128((UInt128)new GpuUInt128(high1, low1) % (UInt128)modulus); + ulong bRaw = ((ulong)random.Next() << 32) | (uint)random.Next(); + UInt128 bBig = (UInt128)bRaw % (UInt128)modulus; + ulong b = (ulong)bBig; + var expected = MulModReference((UInt128)a, bBig, (UInt128)modulus); + a = a.MulMod(b, modulus); + ((UInt128)a).Should().Be(expected); + } + } + + [Fact] + public void MulMod_with_ulong_modulus_matches_UInt128() + { + var random = new Random(11); + for (var i = 0; i < 100; i++) + { + ulong modulus = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL; + ulong aRaw = ((ulong)random.Next() << 32) | (uint)random.Next(); + ulong bRaw = ((ulong)random.Next() << 32) | (uint)random.Next(); + ulong aVal = aRaw % modulus; + ulong bVal = bRaw % modulus; + var a = new GpuUInt128(aVal); + var b = new GpuUInt128(bVal); + UInt128 expected = ((UInt128)aVal * bVal) % modulus; + a = a.MulMod(b, modulus); + ((UInt128)a).Should().Be(expected); + } + } + + [Fact] + public void MulModBigInteger_matches_UInt128() + { + var random = new Random(13); + for (var i = 0; i < 20; i++) + { + var high1 = ((ulong)random.Next() << 32) | (uint)random.Next(); + var low1 = ((ulong)random.Next() << 32) | (uint)random.Next(); + var high2 = ((ulong)random.Next() << 32) | (uint)random.Next(); + var low2 = ((ulong)random.Next() << 32) | (uint)random.Next(); + var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next(); + var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL; + var modulus = new GpuUInt128(modHigh, modLow); + var a = new GpuUInt128((UInt128)new GpuUInt128(high1, low1) % (UInt128)modulus); + var b = new GpuUInt128((UInt128)new GpuUInt128(high2, low2) % (UInt128)modulus); + var expected = MulModReference((UInt128)a, (UInt128)b, (UInt128)modulus); + var result = new GpuUInt128(a.High, a.Low); + result = result.MulModBigInteger(b, modulus); + ((UInt128)result).Should().Be(expected); + } + } + + [Fact] + public void SquareMod_matches_UInt128() + { + var random = new Random(8); + for (var i = 0; i < 100; i++) + { + var valueHigh = ((ulong)random.Next() << 32) | (uint)random.Next(); + var valueLow = ((ulong)random.Next() << 32) | (uint)random.Next(); + var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next(); + var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL; + var modulus = new GpuUInt128(modHigh, modLow); + var a = new GpuUInt128((UInt128)new GpuUInt128(valueHigh, valueLow) % (UInt128)modulus); + var expected = MulModReference((UInt128)a, (UInt128)a, (UInt128)modulus); + a = a.SquareMod(modulus); + ((UInt128)a).Should().Be(expected); + } + } + + [Fact] + public void ModPow_matches_BigInteger() + { + var random = new Random(2); + for (var i = 0; i < 20; i++) + { + var baseHigh = ((ulong)random.Next() << 32) | (uint)random.Next(); + var baseLow = ((ulong)random.Next() << 32) | (uint)random.Next(); + var expHigh = 0UL; + var expLow = (ulong)random.Next(1, int.MaxValue); + var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next(); + var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL; + + var modulus = new GpuUInt128(modHigh, modLow); + var value = new GpuUInt128((UInt128)new GpuUInt128(baseHigh, baseLow) % (UInt128)modulus); + var exponent = new GpuUInt128(expHigh, expLow); + + var expected = (UInt128)System.Numerics.BigInteger.ModPow((System.Numerics.BigInteger)(UInt128)value, (System.Numerics.BigInteger)(UInt128)exponent, (System.Numerics.BigInteger)(UInt128)modulus); + value = value.ModPow(exponent, modulus); + ((UInt128)value).Should().Be(expected); + } + } + + [Fact] + public void ModPow_with_ulong_matches_BigInteger() + { + var random = new Random(9); + for (var i = 0; i < 20; i++) + { + var baseHigh = ((ulong)random.Next() << 32) | (uint)random.Next(); + var baseLow = ((ulong)random.Next() << 32) | (uint)random.Next(); + ulong exponent = (ulong)random.Next(1, int.MaxValue); + var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next(); + var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL; + + var modulus = new GpuUInt128(modHigh, modLow); + var value = new GpuUInt128((UInt128)new GpuUInt128(baseHigh, baseLow) % (UInt128)modulus); + + var expected = (UInt128)System.Numerics.BigInteger.ModPow((System.Numerics.BigInteger)(UInt128)value, exponent, (System.Numerics.BigInteger)(UInt128)modulus); + value = value.ModPow(exponent, modulus); + ((UInt128)value).Should().Be(expected); + } + } + + [Fact] + public void SubMod_matches_UInt128() + { + var random = new Random(3); + for (var i = 0; i < 100; i++) + { + var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next(); + var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL; + var modulus = new GpuUInt128(modHigh, modLow); + var valueHigh = ((ulong)random.Next() << 32) | (uint)random.Next(); + var valueLow = ((ulong)random.Next() << 32) | (uint)random.Next(); + var subHigh = ((ulong)random.Next() << 32) | (uint)random.Next(); + var subLow = ((ulong)random.Next() << 32) | (uint)random.Next(); + var a = new GpuUInt128((UInt128)new GpuUInt128(valueHigh, valueLow) % (UInt128)modulus); + var b = new GpuUInt128((UInt128)new GpuUInt128(subHigh, subLow) % (UInt128)modulus); + UInt128 expected = ((UInt128)a + (UInt128)modulus - (UInt128)b) % (UInt128)modulus; + a = a.SubMod(b, modulus); + ((UInt128)a).Should().Be(expected); + } + } + + [Fact] + public void SubMod_with_ulong_matches_UInt128() + { + var random = new Random(6); + for (var i = 0; i < 100; i++) + { + var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next(); + var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL; + var modulus = new GpuUInt128(modHigh, modLow); + var valueHigh = ((ulong)random.Next() << 32) | (uint)random.Next(); + var valueLow = ((ulong)random.Next() << 32) | (uint)random.Next(); + var a = new GpuUInt128((UInt128)new GpuUInt128(valueHigh, valueLow) % (UInt128)modulus); + ulong subRaw = ((ulong)random.Next() << 32) | (uint)random.Next(); + UInt128 bBig = (UInt128)subRaw % (UInt128)modulus; + ulong b = (ulong)bBig; + UInt128 expected = ((UInt128)a + (UInt128)modulus - bBig) % (UInt128)modulus; + a = a.SubMod(b, modulus); + ((UInt128)a).Should().Be(expected); + } + } + + [Fact] + public void AddMod_matches_UInt128() + { + var random = new Random(4); + for (var i = 0; i < 100; i++) + { + var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next(); + var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL; + var modulus = new GpuUInt128(modHigh, modLow); + var valueHigh = ((ulong)random.Next() << 32) | (uint)random.Next(); + var valueLow = ((ulong)random.Next() << 32) | (uint)random.Next(); + var addHigh = ((ulong)random.Next() << 32) | (uint)random.Next(); + var addLow = ((ulong)random.Next() << 32) | (uint)random.Next(); + var a = new GpuUInt128((UInt128)new GpuUInt128(valueHigh, valueLow) % (UInt128)modulus); + var b = new GpuUInt128((UInt128)new GpuUInt128(addHigh, addLow) % (UInt128)modulus); + UInt128 expected = ((UInt128)a + (UInt128)b) % (UInt128)modulus; + a = a.AddMod(b, modulus); + ((UInt128)a).Should().Be(expected); + } + } + + [Fact] + public void AddMod_with_ulong_modulus_matches_UInt128() + { + var random = new Random(12); + for (var i = 0; i < 100; i++) + { + ulong modulus = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL; + ulong aRaw = ((ulong)random.Next() << 32) | (uint)random.Next(); + ulong bRaw = ((ulong)random.Next() << 32) | (uint)random.Next(); + ulong aVal = aRaw % modulus; + ulong bVal = bRaw % modulus; + var a = new GpuUInt128(aVal); + var b = new GpuUInt128(bVal); + UInt128 expected = ((UInt128)aVal + bVal) % modulus; + a = a.AddMod(b, modulus); + ((UInt128)a).Should().Be(expected); + } + } + + [Fact] + public void AddMod_with_ulong_operand_and_modulus_matches_UInt128() + { + var random = new Random(13); + for (var i = 0; i < 100; i++) + { + ulong modulus = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL; + ulong aRaw = ((ulong)random.Next() << 32) | (uint)random.Next(); + ulong bRaw = ((ulong)random.Next() << 32) | (uint)random.Next(); + ulong aVal = aRaw % modulus; + ulong bVal = bRaw % modulus; + var a = new GpuUInt128(aVal); + UInt128 expected = ((UInt128)aVal + bVal) % modulus; + a = a.AddMod(bVal, modulus); + ((UInt128)a).Should().Be(expected); + } + } + + [Fact] + public void ModInv_matches_BigInteger() + { + var random = new Random(7); + for (var i = 0; i < 20; i++) + { + var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next(); + var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL; + var modulus = new GpuUInt128(modHigh, modLow); + var valueHigh = ((ulong)random.Next() << 32) | (uint)random.Next(); + var valueLow = ((ulong)random.Next() << 32) | (uint)random.Next(); + var value = new GpuUInt128((UInt128)new GpuUInt128(valueHigh, valueLow) % (UInt128)modulus); + if (value.IsZero) + { + value = value.Add(1UL); + } + + var expected = (UInt128)System.Numerics.BigInteger.ModPow((System.Numerics.BigInteger)(UInt128)value, (System.Numerics.BigInteger)(UInt128)modulus - 2, (System.Numerics.BigInteger)(UInt128)modulus); + value = value.ModInv(modulus); + ((UInt128)value).Should().Be(expected); + } + } + + [Fact] + public void ModInv_with_ulong_modulus_matches_BigInteger() + { + var random = new Random(8); + for (var i = 0; i < 20; i++) + { + ulong modulus = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL; + ulong raw = ((ulong)random.Next() << 32) | (uint)random.Next(); + var value = new GpuUInt128((UInt128)(raw % modulus)); + if (value.IsZero) + { + value = value.Add(1UL); + } + + var expected = (UInt128)System.Numerics.BigInteger.ModPow((System.Numerics.BigInteger)(UInt128)value, (System.Numerics.BigInteger)modulus - 2, (System.Numerics.BigInteger)modulus); + value = value.ModInv(modulus); + ((UInt128)value).Should().Be(expected); + } + } + + private static UInt128 MulModReference(UInt128 a, UInt128 b, UInt128 modulus) + { + UInt128 result = 0; + UInt128 x = a; + UInt128 y = b; + while (y != 0) + { + if ((y & 1) != 0) + { + result += x; + if (result >= modulus) + { + result -= modulus; + } + } + + x <<= 1; + if (x >= modulus) + { + x -= modulus; + } + + y >>= 1; + } + + return result; + } +} + diff --git a/PerfectNumbers.Core.Tests/Gpu/LucasLehmerPrimeTesterTests.cs b/PerfectNumbers.Core.Tests/Gpu/LucasLehmerPrimeTesterTests.cs new file mode 100644 index 0000000..79ac24c --- /dev/null +++ b/PerfectNumbers.Core.Tests/Gpu/LucasLehmerPrimeTesterTests.cs @@ -0,0 +1,53 @@ +using FluentAssertions; +using PerfectNumbers.Core.Gpu; +using Xunit; + +namespace PerfectNumbers.Core.Tests.Gpu; + +[Trait("Category", "Slow")] +public class LucasLehmerPrimeTesterTests +{ + [Theory] + [InlineData(3UL, true)] + [InlineData(5UL, true)] + [InlineData(11UL, false)] + [Trait("Category", "Fast")] + public void IsMersennePrime_gpu_returns_expected_results(ulong p, bool expected) + { + new MersenneNumberLucasLehmerGpuTester().IsMersennePrime(p).Should().Be(expected); + } + + [Fact] + [Trait("Category", "Fast")] + public void IsMersennePrime_gpu_large_exponent_without_cpu_fallback() + { + // Use a modest exponent that still exercises the NTT path while keeping + // runtime short enough for the unit test environment. + const ulong exponent = 128UL; + var tester = new MersenneNumberLucasLehmerGpuTester(); + tester.WarmUpNttParameters(exponent); + tester.IsMersennePrime(exponent).Should().BeFalse(); + } + + [Theory] + [InlineData(132UL)] + [InlineData(1000UL)] + [Trait("Category", "Slow")] + public void WarmUpNttParameters_populates_cache_for_large_exponent(ulong exponent) + { + var tester = new MersenneNumberLucasLehmerGpuTester(); + tester.WarmUpNttParameters(exponent); + tester.IsMersennePrime(exponent).Should().BeFalse(); + } + + [Theory] + [InlineData(3UL, true)] + [InlineData(5UL, true)] + [InlineData(11UL, false)] + [Trait("Category", "Fast")] + public void IsMersennePrime_cpu_matches_expected_results(ulong p, bool expected) + { + new MersenneNumberLucasLehmerCpuTester().IsPrime(p).Should().Be(expected); + } +} + diff --git a/PerfectNumbers.Core.Tests/Gpu/NttBackendConfigTests.cs b/PerfectNumbers.Core.Tests/Gpu/NttBackendConfigTests.cs new file mode 100644 index 0000000..49438ff --- /dev/null +++ b/PerfectNumbers.Core.Tests/Gpu/NttBackendConfigTests.cs @@ -0,0 +1,78 @@ +using FluentAssertions; +using PerfectNumbers.Core.Gpu; +using Xunit; + +namespace PerfectNumbers.Core.Tests.Gpu; + +[Collection("GpuNtt")] +public class NttBackendConfigTests +{ + [Fact] + [Trait("Category", "Fast")] + public void ForwardInverseGpu_reference_backend_matches_cpu_on_small_input() + { + var original = NttGpuMath.GpuTransformBackend; + try + { + Span input = stackalloc GpuUInt128[8]; + for (ulong i = 0; i < (ulong)input.Length; i++) + { + input[(int)i] = new GpuUInt128(0UL, i + 1UL); + } + + var modulus = new GpuUInt128(0UL, 17UL); + var primitiveRoot = new GpuUInt128(0UL, 3UL); + + NttGpuMath.GpuTransformBackend = NttBackend.Reference; + Span gpu = stackalloc GpuUInt128[input.Length]; + input.CopyTo(gpu); + NttGpuMath.ForwardGpu(gpu, modulus, primitiveRoot); + NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot); + + Span cpu = stackalloc GpuUInt128[input.Length]; + input.CopyTo(cpu); + NttGpuMath.Forward(cpu, modulus, primitiveRoot); + NttGpuMath.Inverse(cpu, modulus, primitiveRoot); + gpu.ToArray().Should().Equal(cpu.ToArray()); + } + finally + { + NttGpuMath.GpuTransformBackend = original; + } + } + + [Fact] + [Trait("Category", "Fast")] + public void ForwardInverseGpu_staged_backend_matches_cpu_on_small_input() + { + var original = NttGpuMath.GpuTransformBackend; + try + { + Span input = stackalloc GpuUInt128[8]; + for (ulong i = 0; i < (ulong)input.Length; i++) + { + input[(int)i] = new GpuUInt128(0UL, i + 1UL); + } + + var modulus = new GpuUInt128(0UL, 17UL); + var primitiveRoot = new GpuUInt128(0UL, 3UL); + + NttGpuMath.GpuTransformBackend = NttBackend.Staged; + Span gpu = stackalloc GpuUInt128[input.Length]; + input.CopyTo(gpu); + NttGpuMath.ForwardGpu(gpu, modulus, primitiveRoot); + NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot); + + Span cpu = stackalloc GpuUInt128[input.Length]; + input.CopyTo(cpu); + NttGpuMath.Forward(cpu, modulus, primitiveRoot); + NttGpuMath.Inverse(cpu, modulus, primitiveRoot); + gpu.ToArray().Should().Equal(cpu.ToArray()); + } + finally + { + NttGpuMath.GpuTransformBackend = original; + } + } +} + diff --git a/PerfectNumbers.Core.Tests/Gpu/NttGpuMathTests.cs b/PerfectNumbers.Core.Tests/Gpu/NttGpuMathTests.cs new file mode 100644 index 0000000..24fb2d0 --- /dev/null +++ b/PerfectNumbers.Core.Tests/Gpu/NttGpuMathTests.cs @@ -0,0 +1,361 @@ +using System.Reflection; +using FluentAssertions; +using ILGPU; +using ILGPU.Runtime; +using PerfectNumbers.Core.Gpu; +using System.Numerics; +using Xunit; + +namespace PerfectNumbers.Core.Tests.Gpu; + +// GPU-related tests are organized per-backend so both reference and staged +// implementations are exercised under the fast test suite. +[Collection("GpuNtt")] +public class NttGpuMathTests +{ + [Fact] + [Trait("Category", "Fast")] + public void ForwardGpu_reference_matches_cpu_forward() + { + var original = NttGpuMath.GpuTransformBackend; + try + { + NttGpuMath.GpuTransformBackend = NttBackend.Reference; + Span cpu = stackalloc GpuUInt128[4]; + cpu[0] = new GpuUInt128(0UL, 1UL); + cpu[1] = new GpuUInt128(0UL, 2UL); + cpu[2] = new GpuUInt128(0UL, 3UL); + cpu[3] = new GpuUInt128(0UL, 4UL); + Span gpu = stackalloc GpuUInt128[4]; + cpu.CopyTo(gpu); + var modulus = new GpuUInt128(0UL, 17UL); + var primitiveRoot = new GpuUInt128(0UL, 3UL); + NttGpuMath.Forward(cpu, modulus, primitiveRoot); + NttGpuMath.ForwardGpu(gpu, modulus, primitiveRoot); + for (int i = 0; i < cpu.Length; i++) + { + gpu[i].Should().Be(cpu[i]); + } + } + finally + { + NttGpuMath.GpuTransformBackend = original; + } + } + + [Fact] + [Trait("Category", "Fast")] + public void ForwardGpu_staged_matches_cpu_forward() + { + var original = NttGpuMath.GpuTransformBackend; + try + { + NttGpuMath.GpuTransformBackend = NttBackend.Staged; + Span cpu = stackalloc GpuUInt128[4]; + cpu[0] = new GpuUInt128(0UL, 1UL); + cpu[1] = new GpuUInt128(0UL, 2UL); + cpu[2] = new GpuUInt128(0UL, 3UL); + cpu[3] = new GpuUInt128(0UL, 4UL); + Span gpu = stackalloc GpuUInt128[4]; + cpu.CopyTo(gpu); + var modulus = new GpuUInt128(0UL, 17UL); + var primitiveRoot = new GpuUInt128(0UL, 3UL); + NttGpuMath.Forward(cpu, modulus, primitiveRoot); + NttGpuMath.ForwardGpu(gpu, modulus, primitiveRoot); + for (int i = 0; i < cpu.Length; i++) + { + gpu[i].Should().Be(cpu[i]); + } + } + finally + { + NttGpuMath.GpuTransformBackend = original; + } + } + + [Fact] + [Trait("Category", "Fast")] + public void InverseGpu_reference_matches_cpu_inverse() + { + var original = NttGpuMath.GpuTransformBackend; + try + { + NttGpuMath.GpuTransformBackend = NttBackend.Reference; + Span input = stackalloc GpuUInt128[4]; + input[0] = new GpuUInt128(0UL, 5UL); + input[1] = new GpuUInt128(0UL, 7UL); + input[2] = new GpuUInt128(0UL, 9UL); + input[3] = new GpuUInt128(0UL, 11UL); + var modulus = new GpuUInt128(0UL, 17UL); + var primitiveRoot = new GpuUInt128(0UL, 3UL); + Span transformed = stackalloc GpuUInt128[4]; + input.CopyTo(transformed); + NttGpuMath.Forward(transformed, modulus, primitiveRoot); + Span cpu = stackalloc GpuUInt128[4]; + transformed.CopyTo(cpu); + NttGpuMath.Inverse(cpu, modulus, primitiveRoot); + Span gpu = stackalloc GpuUInt128[4]; + transformed.CopyTo(gpu); + NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot); + for (int i = 0; i < input.Length; i++) + { + gpu[i].Should().Be(cpu[i]); + } + } + finally + { + NttGpuMath.GpuTransformBackend = original; + } + } + + [Fact] + [Trait("Category", "Fast")] + public void InverseGpu_staged_matches_cpu_inverse() + { + var original = NttGpuMath.GpuTransformBackend; + try + { + NttGpuMath.GpuTransformBackend = NttBackend.Staged; + Span input = stackalloc GpuUInt128[4]; + input[0] = new GpuUInt128(0UL, 5UL); + input[1] = new GpuUInt128(0UL, 7UL); + input[2] = new GpuUInt128(0UL, 9UL); + input[3] = new GpuUInt128(0UL, 11UL); + var modulus = new GpuUInt128(0UL, 17UL); + var primitiveRoot = new GpuUInt128(0UL, 3UL); + Span transformed = stackalloc GpuUInt128[4]; + input.CopyTo(transformed); + NttGpuMath.Forward(transformed, modulus, primitiveRoot); + Span cpu = stackalloc GpuUInt128[4]; + transformed.CopyTo(cpu); + NttGpuMath.Inverse(cpu, modulus, primitiveRoot); + Span gpu = stackalloc GpuUInt128[4]; + transformed.CopyTo(gpu); + NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot); + for (int i = 0; i < input.Length; i++) + { + gpu[i].Should().Be(cpu[i]); + } + } + finally + { + NttGpuMath.GpuTransformBackend = original; + } + } + + [Fact] + [Trait("Category", "Fast")] + public void Montgomery_twiddle_inverses_multiply_to_r2() + { + var modulus = new GpuUInt128(0UL, 17UL); + var primitiveRoot = new GpuUInt128(0UL, 3UL); + + using var gpu = GpuContextPool.Rent(); + var accelerator = gpu.Accelerator; + + var method = typeof(NttGpuMath).GetMethod("GetSquareCache", BindingFlags.NonPublic | BindingFlags.Static); + method.Should().NotBeNull(); + var cache = method!.Invoke(null, new object[] { accelerator, 8, modulus, primitiveRoot })!; + var cacheType = cache.GetType(); + + var twiddlesMont = (MemoryBuffer1D)cacheType.GetProperty("TwiddlesMont")!.GetValue(cache)!; + var twiddlesInvMont = (MemoryBuffer1D)cacheType.GetProperty("TwiddlesInvMont")!.GetValue(cache)!; + var r2 = (ulong)cacheType.GetProperty("MontR2Mod64")!.GetValue(cache)!; + var mod = (ulong)cacheType.GetProperty("ModulusLow")!.GetValue(cache)!; + + int count = 7; // length 8 -> 7 twiddles + var forward = new GpuUInt128[count]; + var inverse = new GpuUInt128[count]; + twiddlesMont.CopyToCPU(forward); + twiddlesInvMont.CopyToCPU(inverse); + + for (int i = 0; i < count; i++) + { + ulong prod = (ulong)((((BigInteger)forward[i].Low) * inverse[i].Low) % mod); + prod.Should().Be(r2); + } + } + + [Fact] + [Trait("Category", "Fast")] + public void ConvolveGpu_reference_matches_cpu_convolve() + { + var original = NttGpuMath.GpuTransformBackend; + try + { + NttGpuMath.GpuTransformBackend = NttBackend.Reference; + Span leftCpu = stackalloc GpuUInt128[4]; + Span rightCpu = stackalloc GpuUInt128[4]; + leftCpu[0] = new GpuUInt128(0UL, 1UL); + leftCpu[1] = new GpuUInt128(0UL, 2UL); + leftCpu[2] = new GpuUInt128(0UL, 3UL); + leftCpu[3] = new GpuUInt128(0UL, 4UL); + rightCpu[0] = new GpuUInt128(0UL, 5UL); + rightCpu[1] = new GpuUInt128(0UL, 6UL); + rightCpu[2] = new GpuUInt128(0UL, 7UL); + rightCpu[3] = new GpuUInt128(0UL, 8UL); + Span leftGpu = stackalloc GpuUInt128[4]; + Span rightGpu = stackalloc GpuUInt128[4]; + leftCpu.CopyTo(leftGpu); + rightCpu.CopyTo(rightGpu); + var modulus = new GpuUInt128(0UL, 17UL); + var primitiveRoot = new GpuUInt128(0UL, 3UL); + NttGpuMath.Convolve(leftCpu, rightCpu, modulus, primitiveRoot); + NttGpuMath.ConvolveGpu(leftGpu, rightGpu, modulus, primitiveRoot); + for (int i = 0; i < leftCpu.Length; i++) + { + leftGpu[i].Should().Be(leftCpu[i]); + } + } + finally + { + NttGpuMath.GpuTransformBackend = original; + } + } + + [Fact] + [Trait("Category", "Fast")] + public void ConvolveGpu_staged_matches_cpu_convolve() + { + var original = NttGpuMath.GpuTransformBackend; + try + { + NttGpuMath.GpuTransformBackend = NttBackend.Staged; + Span leftCpu = stackalloc GpuUInt128[4]; + Span rightCpu = stackalloc GpuUInt128[4]; + leftCpu[0] = new GpuUInt128(0UL, 1UL); + leftCpu[1] = new GpuUInt128(0UL, 2UL); + leftCpu[2] = new GpuUInt128(0UL, 3UL); + leftCpu[3] = new GpuUInt128(0UL, 4UL); + rightCpu[0] = new GpuUInt128(0UL, 5UL); + rightCpu[1] = new GpuUInt128(0UL, 6UL); + rightCpu[2] = new GpuUInt128(0UL, 7UL); + rightCpu[3] = new GpuUInt128(0UL, 8UL); + Span leftGpu = stackalloc GpuUInt128[4]; + Span rightGpu = stackalloc GpuUInt128[4]; + leftCpu.CopyTo(leftGpu); + rightCpu.CopyTo(rightGpu); + var modulus = new GpuUInt128(0UL, 17UL); + var primitiveRoot = new GpuUInt128(0UL, 3UL); + NttGpuMath.Convolve(leftCpu, rightCpu, modulus, primitiveRoot); + NttGpuMath.ConvolveGpu(leftGpu, rightGpu, modulus, primitiveRoot); + for (int i = 0; i < leftCpu.Length; i++) + { + leftGpu[i].Should().Be(leftCpu[i]); + } + } + finally + { + NttGpuMath.GpuTransformBackend = original; + } + } + + [Fact] + [Trait("Category", "Fast")] + public void SquareGpu_reference_matches_cpu_square() + { + var original = NttGpuMath.GpuTransformBackend; + try + { + NttGpuMath.GpuTransformBackend = NttBackend.Reference; + Span cpu = stackalloc GpuUInt128[4]; + cpu[0] = new GpuUInt128(0UL, 1UL); + cpu[1] = new GpuUInt128(0UL, 2UL); + cpu[2] = new GpuUInt128(0UL, 3UL); + cpu[3] = new GpuUInt128(0UL, 4UL); + Span gpu = stackalloc GpuUInt128[4]; + cpu.CopyTo(gpu); + var modulus = new GpuUInt128(0UL, 17UL); + var primitiveRoot = new GpuUInt128(0UL, 3UL); + NttGpuMath.Square(cpu, modulus, primitiveRoot); + NttGpuMath.SquareGpu(gpu, modulus, primitiveRoot); + for (int i = 0; i < cpu.Length; i++) + { + gpu[i].Should().Be(cpu[i]); + } + } + finally + { + NttGpuMath.GpuTransformBackend = original; + } + } + + [Fact] + [Trait("Category", "Fast")] + public void SquareGpu_staged_matches_cpu_square() + { + var original = NttGpuMath.GpuTransformBackend; + try + { + NttGpuMath.GpuTransformBackend = NttBackend.Staged; + Span cpu = stackalloc GpuUInt128[4]; + cpu[0] = new GpuUInt128(0UL, 1UL); + cpu[1] = new GpuUInt128(0UL, 2UL); + cpu[2] = new GpuUInt128(0UL, 3UL); + cpu[3] = new GpuUInt128(0UL, 4UL); + Span gpu = stackalloc GpuUInt128[4]; + cpu.CopyTo(gpu); + var modulus = new GpuUInt128(0UL, 17UL); + var primitiveRoot = new GpuUInt128(0UL, 3UL); + NttGpuMath.Square(cpu, modulus, primitiveRoot); + NttGpuMath.SquareGpu(gpu, modulus, primitiveRoot); + for (int i = 0; i < cpu.Length; i++) + { + gpu[i].Should().Be(cpu[i]); + } + } + finally + { + NttGpuMath.GpuTransformBackend = original; + } + } + + [Fact] + [Trait("Category", "Fast")] + public void BitReverseGpu_matches_cpu() + { + GpuUInt128[] cpu = new GpuUInt128[8]; + for (ulong i = 0; i < (ulong)cpu.Length; i++) + { + cpu[i] = new GpuUInt128(0UL, i); + } + + GpuUInt128[] gpu = (GpuUInt128[])cpu.Clone(); + NttGpuMath.BitReverse(cpu); + NttGpuMath.BitReverseGpu(gpu); + gpu.Should().Equal(cpu); + } + + [Fact] + [Trait("Category", "Fast")] + public void DisposeAll_clears_ntt_caches() + { + // Ensure the cache starts empty. + NttGpuMath.ClearCaches(); + + Span values = stackalloc GpuUInt128[2]; + values[0] = new GpuUInt128(0UL, 1UL); + values[1] = new GpuUInt128(0UL, 2UL); + var modulus = new GpuUInt128(0UL, 17UL); + var primitiveRoot = new GpuUInt128(0UL, 3UL); + using (var gpu = GpuContextPool.Rent()) + { + var buffer = gpu.Accelerator.Allocate1D(values.Length); + buffer.View.CopyFromCPU(ref values[0], values.Length); + NttGpuMath.SquareDevice(gpu.Accelerator, buffer.View, modulus, primitiveRoot); + buffer.Dispose(); + } + + // Verify that the square cache now holds entries for the accelerator. + var field = typeof(NttGpuMath).GetField("SquareCache", BindingFlags.NonPublic | BindingFlags.Static)!; + var cacheObj = field.GetValue(null)!; + int count = (int)cacheObj.GetType().GetProperty("Count")!.GetValue(cacheObj)!; + count.Should().BeGreaterThan(0); + + GpuContextPool.DisposeAll(); + + count = (int)cacheObj.GetType().GetProperty("Count")!.GetValue(cacheObj)!; + count.Should().Be(0); + } +} + diff --git a/PerfectNumbers.Core.Tests/Gpu/NttMontgomeryTests.cs b/PerfectNumbers.Core.Tests/Gpu/NttMontgomeryTests.cs new file mode 100644 index 0000000..84398a1 --- /dev/null +++ b/PerfectNumbers.Core.Tests/Gpu/NttMontgomeryTests.cs @@ -0,0 +1,87 @@ +using FluentAssertions; +using ILGPU.Runtime; +using PerfectNumbers.Core.Gpu; +using Xunit; + +namespace PerfectNumbers.Core.Tests.Gpu; + +[Collection("GpuNtt")] +[Trait("Category", "Fast")] +public class NttMontgomeryTests +{ + [Fact] + [Trait("Category", "Fast")] + public void SquareDevice_montgomery64_matches_cpu_square_on_small_input() + { + // Small 64-bit modulus and tiny input to keep runtime trivial. + var modulus = new GpuUInt128(0UL, 17UL); + var primitiveRoot = new GpuUInt128(0UL, 3UL); + + Span cpu = stackalloc GpuUInt128[8]; + for (ulong i = 0; i < (ulong)cpu.Length; i++) + { + cpu[(int)i] = new GpuUInt128(0UL, i + 1UL); + } + Span gpu = stackalloc GpuUInt128[16]; + cpu.CopyTo(gpu); + for (int i = cpu.Length; i < gpu.Length; i++) + { + gpu[i] = new GpuUInt128(0UL, 0UL); + } + + // CPU reference + NttGpuMath.Square(cpu, modulus, primitiveRoot); + + // Device execution + using var context = GpuContextPool.Rent(); + var accelerator = context.Accelerator; + using var buffer = accelerator.Allocate1D(gpu.Length); + buffer.View.CopyFromCPU(ref gpu[0], gpu.Length); + NttGpuMath.SquareDevice(accelerator, buffer.View, modulus, primitiveRoot); + buffer.View.CopyToCPU(ref gpu[0], gpu.Length); + + for (int i = 0; i < cpu.Length; i++) + { + gpu[i].Should().Be(cpu[i]); + } + } + + [Fact] + [Trait("Category", "Fast")] + public void InverseDevice_montgomery64_matches_cpu_inverse_on_small_input() + { + var modulus = new GpuUInt128(0UL, 17UL); + var primitiveRoot = new GpuUInt128(0UL, 3UL); + + Span input = stackalloc GpuUInt128[8]; + for (ulong i = 0; i < (ulong)input.Length; i++) + { + input[(int)i] = new GpuUInt128(0UL, i + 1UL); + } + + Span transformed = stackalloc GpuUInt128[input.Length]; + input.CopyTo(transformed); + NttGpuMath.Forward(transformed, modulus, primitiveRoot); + + Span cpu = stackalloc GpuUInt128[input.Length]; + transformed.CopyTo(cpu); + NttGpuMath.Inverse(cpu, modulus, primitiveRoot); + + var original = NttGpuMath.GpuTransformBackend; + try + { + NttGpuMath.GpuTransformBackend = NttBackend.Staged; + Span gpu = stackalloc GpuUInt128[input.Length]; + transformed.CopyTo(gpu); + NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot); + for (int i = 0; i < input.Length; i++) + { + gpu[i].Should().Be(cpu[i]); + } + } + finally + { + NttGpuMath.GpuTransformBackend = original; + } + } +} diff --git a/PerfectNumbers.Core.Tests/Gpu/NttReductionModeTests.cs b/PerfectNumbers.Core.Tests/Gpu/NttReductionModeTests.cs new file mode 100644 index 0000000..f645cee --- /dev/null +++ b/PerfectNumbers.Core.Tests/Gpu/NttReductionModeTests.cs @@ -0,0 +1,90 @@ +using FluentAssertions; +using PerfectNumbers.Core.Gpu; +using Xunit; + +namespace PerfectNumbers.Core.Tests.Gpu; + +[Collection("GpuNtt")] +public class NttReductionModeTests +{ + [Theory] + [Trait("Category", "Fast")] + [InlineData(ModReductionMode.Auto)] + [InlineData(ModReductionMode.GpuUInt128)] + public void ForwardInverse_small64bit_modulus_reference_backend_works_with_mode(ModReductionMode mode) + { + var originalBackend = NttGpuMath.GpuTransformBackend; + var originalMode = NttGpuMath.ReductionMode; + try + { + NttGpuMath.GpuTransformBackend = NttBackend.Reference; + NttGpuMath.ReductionMode = mode; + + var modulus = new GpuUInt128(0UL, 17UL); + var primitiveRoot = new GpuUInt128(0UL, 3UL); + Span input = stackalloc GpuUInt128[8]; + for (ulong i = 0; i < (ulong)input.Length; i++) + { + input[(int)i] = new GpuUInt128(0UL, i + 1UL); + } + + Span cpu = stackalloc GpuUInt128[input.Length]; + input.CopyTo(cpu); + NttGpuMath.Forward(cpu, modulus, primitiveRoot); + NttGpuMath.Inverse(cpu, modulus, primitiveRoot); + + Span gpu = stackalloc GpuUInt128[input.Length]; + input.CopyTo(gpu); + NttGpuMath.ForwardGpu(gpu, modulus, primitiveRoot); + NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot); + + gpu.ToArray().Should().Equal(cpu.ToArray()); + } + finally + { + NttGpuMath.GpuTransformBackend = originalBackend; + NttGpuMath.ReductionMode = originalMode; + } + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(ModReductionMode.Auto)] + [InlineData(ModReductionMode.GpuUInt128)] + public void ForwardInverse_small64bit_modulus_staged_backend_works_with_mode(ModReductionMode mode) + { + var originalBackend = NttGpuMath.GpuTransformBackend; + var originalMode = NttGpuMath.ReductionMode; + try + { + NttGpuMath.GpuTransformBackend = NttBackend.Staged; + NttGpuMath.ReductionMode = mode; + + var modulus = new GpuUInt128(0UL, 17UL); + var primitiveRoot = new GpuUInt128(0UL, 3UL); + Span input = stackalloc GpuUInt128[8]; + for (ulong i = 0; i < (ulong)input.Length; i++) + { + input[(int)i] = new GpuUInt128(0UL, i + 1UL); + } + + Span cpu = stackalloc GpuUInt128[input.Length]; + input.CopyTo(cpu); + NttGpuMath.Forward(cpu, modulus, primitiveRoot); + NttGpuMath.Inverse(cpu, modulus, primitiveRoot); + + Span gpu = stackalloc GpuUInt128[input.Length]; + input.CopyTo(gpu); + NttGpuMath.ForwardGpu(gpu, modulus, primitiveRoot); + NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot); + + gpu.ToArray().Should().Equal(cpu.ToArray()); + } + finally + { + NttGpuMath.GpuTransformBackend = originalBackend; + NttGpuMath.ReductionMode = originalMode; + } + } +} + diff --git a/PerfectNumbers.Core.Tests/GpuKernelPoolTests.cs b/PerfectNumbers.Core.Tests/GpuKernelPoolTests.cs new file mode 100644 index 0000000..1c8fe35 --- /dev/null +++ b/PerfectNumbers.Core.Tests/GpuKernelPoolTests.cs @@ -0,0 +1,39 @@ +using FluentAssertions; +using PerfectNumbers.Core.Gpu; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +[Collection("GpuNtt")] +public class GpuKernelPoolTests +{ + [Fact] + [Trait("Category", "Fast")] + public void EnsureSmallCyclesOnDevice_returns_same_view_on_subsequent_calls() + { + using var lease = GpuContextPool.RentPreferred(preferCpu: true); + var accelerator = lease.Accelerator; + + var first = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator); + var second = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator); + + second.Equals(first).Should().BeTrue(); + second.Length.Should().Be(MersenneDivisorCycles.SmallDivisorsMax + 1); + } + + [Fact] + [Trait("Category", "Fast")] + public void Run_provides_accelerator_and_stream() + { + bool executed = false; + + GpuKernelPool.Run((acc, stream) => + { + acc.Should().NotBeNull(); + stream.Should().NotBeNull(); + executed = true; + }); + + executed.Should().BeTrue(); + } +} diff --git a/PerfectNumbers.Core.Tests/GpuWorkLimiterTests.cs b/PerfectNumbers.Core.Tests/GpuWorkLimiterTests.cs new file mode 100644 index 0000000..7e51ad4 --- /dev/null +++ b/PerfectNumbers.Core.Tests/GpuWorkLimiterTests.cs @@ -0,0 +1,39 @@ +using System.Diagnostics; +using FluentAssertions; +using PerfectNumbers.Core.Gpu; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +[Collection("GpuNtt")] +public class GpuWorkLimiterTests +{ + [Fact] + [Trait("Category", "Fast")] + public async Task Acquire_blocks_when_limit_reached() + { + GpuWorkLimiter.SetLimit(1); + + var startedSecond = new TaskCompletionSource(); + var sw = new Stopwatch(); + + using var first = GpuWorkLimiter.Acquire(); + sw.Start(); + + var secondTask = Task.Run(() => + { + using var second = GpuWorkLimiter.Acquire(); + startedSecond.TrySetResult(true); + }); + + await Task.Delay(50); + startedSecond.Task.IsCompleted.Should().BeFalse(); + + first.Dispose(); + + await secondTask.WaitAsync(TimeSpan.FromSeconds(1)); + sw.Stop(); + + sw.ElapsedMilliseconds.Should().BeGreaterThanOrEqualTo(50); + } +} diff --git a/PerfectNumbers.Core.Tests/InputParserTests.cs b/PerfectNumbers.Core.Tests/InputParserTests.cs new file mode 100644 index 0000000..978d747 --- /dev/null +++ b/PerfectNumbers.Core.Tests/InputParserTests.cs @@ -0,0 +1,12 @@ +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +public class InputParserTests +{ + [Fact(Skip = "TODO: implement test")] + [Trait("Category", "Fast")] + public void Placeholder() + { + } +} diff --git a/PerfectNumbers.Core.Tests/KRangeFinderTests.cs b/PerfectNumbers.Core.Tests/KRangeFinderTests.cs new file mode 100644 index 0000000..057ecb0 --- /dev/null +++ b/PerfectNumbers.Core.Tests/KRangeFinderTests.cs @@ -0,0 +1,12 @@ +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +public class KRangeFinderTests +{ + [Fact(Skip = "TODO: implement test")] + [Trait("Category", "Fast")] + public void Placeholder() + { + } +} diff --git a/PerfectNumbers.Core.Tests/MersenneDivisorCyclesTests.cs b/PerfectNumbers.Core.Tests/MersenneDivisorCyclesTests.cs new file mode 100644 index 0000000..05461b4 --- /dev/null +++ b/PerfectNumbers.Core.Tests/MersenneDivisorCyclesTests.cs @@ -0,0 +1,25 @@ +using FluentAssertions; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +public class MersenneDivisorCyclesTests +{ + public static IEnumerable Divisors() => + new[] + { + new object[] {3UL, 2UL}, + new object[] {7UL, 3UL}, + new object[] {11UL, 10UL}, + }; + + [Theory] + [Trait("Category", "Fast")] + [MemberData(nameof(Divisors))] + public void CalculateCycleLength_returns_expected_value(ulong divisor, ulong expected) + { + MersenneDivisorCycles.CalculateCycleLength(divisor).Should().Be(expected); + MersenneDivisorCycles.CalculateCycleLengthGpu(divisor).Should().Be(expected); + } +} + diff --git a/PerfectNumbers.Core.Tests/MersenneNumberIncrementalCpuTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberIncrementalCpuTesterTests.cs new file mode 100644 index 0000000..9edcee3 --- /dev/null +++ b/PerfectNumbers.Core.Tests/MersenneNumberIncrementalCpuTesterTests.cs @@ -0,0 +1,36 @@ +using FluentAssertions; +using PerfectNumbers.Core.Cpu; +using Xunit; +using UInt128 = System.UInt128; + +namespace PerfectNumbers.Core.Tests; + +public class MersenneNumberIncrementalCpuTesterTests +{ + private static bool LastDigitIsSeven(ulong exponent) => (exponent & 3UL) == 3UL; + + [Theory] + [InlineData(GpuKernelType.Incremental)] + [InlineData(GpuKernelType.Pow2Mod)] + [Trait("Category", "Fast")] + public void Scan_handles_various_prime_exponents(GpuKernelType type) + { + var tester = new MersenneNumberIncrementalCpuTester(type); + + if (type == GpuKernelType.Pow2Mod) + { + RunCase(tester, 11UL, 1UL, expectedPrime: false); + RunCase(tester, 71UL, 341_860UL, expectedPrime: false); + } + + RunCase(tester, 89UL, 1_000UL, expectedPrime: true); + RunCase(tester, 127UL, 1_000UL, expectedPrime: true); + } + + private static void RunCase(MersenneNumberIncrementalCpuTester tester, ulong exponent, ulong maxK, bool expectedPrime) + { + bool isPrime = true; + tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime); + isPrime.Should().Be(expectedPrime); + } +} diff --git a/PerfectNumbers.Core.Tests/MersenneNumberIncrementalGpuTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberIncrementalGpuTesterTests.cs new file mode 100644 index 0000000..4930c92 --- /dev/null +++ b/PerfectNumbers.Core.Tests/MersenneNumberIncrementalGpuTesterTests.cs @@ -0,0 +1,37 @@ +using FluentAssertions; +using PerfectNumbers.Core.Gpu; +using Xunit; +using UInt128 = System.UInt128; + +namespace PerfectNumbers.Core.Tests; + +public class MersenneNumberIncrementalGpuTesterTests +{ + private static bool LastDigitIsSeven(ulong exponent) => (exponent & 3UL) == 3UL; + + [Theory] + [InlineData(GpuKernelType.Pow2Mod, false)] + [InlineData(GpuKernelType.Incremental, false)] + [InlineData(GpuKernelType.Pow2Mod, true)] + [Trait("Category", "Fast")] + public void Scan_handles_various_prime_exponents(GpuKernelType type, bool useGpuOrder) + { + var tester = new MersenneNumberIncrementalGpuTester(type, useGpuOrder); + + if (type == GpuKernelType.Pow2Mod) + { + RunCase(tester, 11UL, 1UL, expectedPrime: false); + RunCase(tester, 71UL, 341_860UL, expectedPrime: false); + } + + RunCase(tester, 89UL, 1_000UL, expectedPrime: true); + RunCase(tester, 127UL, 1_000UL, expectedPrime: true); + } + + private static void RunCase(MersenneNumberIncrementalGpuTester tester, ulong exponent, ulong maxK, bool expectedPrime) + { + bool isPrime = true; + tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime); + isPrime.Should().Be(expectedPrime); + } +} diff --git a/PerfectNumbers.Core.Tests/MersenneNumberOrderCpuTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberOrderCpuTesterTests.cs new file mode 100644 index 0000000..3348ceb --- /dev/null +++ b/PerfectNumbers.Core.Tests/MersenneNumberOrderCpuTesterTests.cs @@ -0,0 +1,37 @@ +using FluentAssertions; +using PerfectNumbers.Core.Cpu; +using Xunit; +using UInt128 = System.UInt128; + +namespace PerfectNumbers.Core.Tests; + +public class MersenneNumberOrderCpuTesterTests +{ + private static bool LastDigitIsSeven(ulong exponent) => (exponent & 3UL) == 3UL; + + [Theory] + [InlineData(GpuKernelType.Incremental)] + [InlineData(GpuKernelType.Pow2Mod)] + [Trait("Category", "Fast")] + public void Scan_handles_various_prime_exponents(GpuKernelType type) + { + var tester = new MersenneNumberOrderCpuTester(type); + + if (type == GpuKernelType.Pow2Mod) + { + RunCase(tester, 23UL, 1UL, expectedPrime: false); + RunCase(tester, 29UL, 36UL, expectedPrime: false); + } + + RunCase(tester, 89UL, 1_000UL, expectedPrime: true); + RunCase(tester, 127UL, 1_000UL, expectedPrime: true); + } + + private static void RunCase(MersenneNumberOrderCpuTester tester, ulong exponent, ulong maxK, bool expectedPrime) + { + bool isPrime = true; + tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime); + isPrime.Should().Be(expectedPrime); + } +} + diff --git a/PerfectNumbers.Core.Tests/MersenneNumberOrderGpuTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberOrderGpuTesterTests.cs new file mode 100644 index 0000000..0751886 --- /dev/null +++ b/PerfectNumbers.Core.Tests/MersenneNumberOrderGpuTesterTests.cs @@ -0,0 +1,38 @@ +using FluentAssertions; +using PerfectNumbers.Core.Gpu; +using Xunit; +using UInt128 = System.UInt128; + +namespace PerfectNumbers.Core.Tests; + +public class MersenneNumberOrderGpuTesterTests +{ + private static bool LastDigitIsSeven(ulong exponent) => (exponent & 3UL) == 3UL; + + [Theory] + [InlineData(GpuKernelType.Pow2Mod, false)] + [InlineData(GpuKernelType.Incremental, false)] + [InlineData(GpuKernelType.Pow2Mod, true)] + [Trait("Category", "Fast")] + public void Scan_handles_various_prime_exponents(GpuKernelType type, bool useGpuOrder) + { + var tester = new MersenneNumberOrderGpuTester(type, useGpuOrder); + + if (type == GpuKernelType.Pow2Mod) + { + RunCase(tester, 23UL, 1UL, expectedPrime: false); + RunCase(tester, 29UL, 36UL, expectedPrime: false); + } + + RunCase(tester, 89UL, 1_000UL, expectedPrime: true); + RunCase(tester, 127UL, 1_000UL, expectedPrime: true); + } + + private static void RunCase(MersenneNumberOrderGpuTester tester, ulong exponent, ulong maxK, bool expectedPrime) + { + bool isPrime = true; + tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime); + isPrime.Should().Be(expectedPrime); + } +} + diff --git a/PerfectNumbers.Core.Tests/MersenneNumberResidueCpuTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberResidueCpuTesterTests.cs new file mode 100644 index 0000000..e083d11 --- /dev/null +++ b/PerfectNumbers.Core.Tests/MersenneNumberResidueCpuTesterTests.cs @@ -0,0 +1,31 @@ +using FluentAssertions; +using PerfectNumbers.Core.Cpu; +using Xunit; +using UInt128 = System.UInt128; + +namespace PerfectNumbers.Core.Tests; + +public class MersenneNumberResidueCpuTesterTests +{ + private static bool LastDigitIsSeven(ulong exponent) => (exponent & 3UL) == 3UL; + + [Fact] + [Trait("Category", "Fast")] + public void Scan_handles_various_prime_exponents() + { + var tester = new MersenneNumberResidueCpuTester(); + + RunCase(tester, 23UL, 1UL, expectedPrime: false); + RunCase(tester, 29UL, 36UL, expectedPrime: false); + RunCase(tester, 89UL, 1_000UL, expectedPrime: true); + RunCase(tester, 127UL, 1_000UL, expectedPrime: true); + } + + private static void RunCase(MersenneNumberResidueCpuTester tester, ulong exponent, ulong maxK, bool expectedPrime) + { + bool isPrime = true; + tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime); + isPrime.Should().Be(expectedPrime); + } +} + diff --git a/PerfectNumbers.Core.Tests/MersenneNumberResidueGpuTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberResidueGpuTesterTests.cs new file mode 100644 index 0000000..307cffb --- /dev/null +++ b/PerfectNumbers.Core.Tests/MersenneNumberResidueGpuTesterTests.cs @@ -0,0 +1,33 @@ +using FluentAssertions; +using PerfectNumbers.Core.Gpu; +using Xunit; +using UInt128 = System.UInt128; + +namespace PerfectNumbers.Core.Tests; + +public class MersenneNumberResidueGpuTesterTests +{ + private static bool LastDigitIsSeven(ulong exponent) => (exponent & 3UL) == 3UL; + + [Theory] + [InlineData(false)] + [InlineData(true)] + [Trait("Category", "Fast")] + public void Scan_handles_various_prime_exponents(bool useGpuOrder) + { + var tester = new MersenneNumberResidueGpuTester(useGpuOrder); + + RunCase(tester, 23UL, 2UL, expectedPrime: false); + RunCase(tester, 29UL, 37UL, expectedPrime: false); + RunCase(tester, 89UL, 1_001UL, expectedPrime: true); + RunCase(tester, 127UL, 1_001UL, expectedPrime: true); + } + + private static void RunCase(MersenneNumberResidueGpuTester tester, ulong exponent, ulong maxK, bool expectedPrime) + { + bool isPrime = true; + tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime); + isPrime.Should().Be(expectedPrime); + } +} + diff --git a/PerfectNumbers.Core.Tests/MersenneNumberTester/MersenneNumberTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberTester/MersenneNumberTesterTests.cs new file mode 100644 index 0000000..5933008 --- /dev/null +++ b/PerfectNumbers.Core.Tests/MersenneNumberTester/MersenneNumberTesterTests.cs @@ -0,0 +1,36 @@ +using FluentAssertions; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +[Collection("GpuNtt")] +[Trait("Category", "Fast")] +public class MersenneNumberTesterTests +{ + [Theory] + [InlineData(136_000_002UL)] + [InlineData(136_000_005UL)] + public void IsMersennePrime_detects_small_prime_divisors_for_large_exponents(ulong p) + { + var tester = new MersenneNumberTester(); + tester.IsMersennePrime(p).Should().BeFalse(); + } + + [Theory] + [InlineData(125UL)] + public void WarmUpOrders_populates_cache_without_affecting_results(ulong p) + { + var tester = new MersenneNumberTester(useOrderCache: true, useGpuOrder: true); + tester.WarmUpOrders(p, 1_000UL); + tester.IsMersennePrime(p).Should().BeFalse(); + } + + [Theory] + [InlineData(125UL, true)] + [InlineData(127UL, false)] + public void SharesFactorWithExponentMinusOne_detects_common_factors(ulong p, bool expected) + { + p.SharesFactorWithExponentMinusOne().Should().Be(expected); + } +} + diff --git a/PerfectNumbers.Core.Tests/MersenneNumberTester/MersenneResidueModeTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberTester/MersenneResidueModeTests.cs new file mode 100644 index 0000000..75d9d45 --- /dev/null +++ b/PerfectNumbers.Core.Tests/MersenneNumberTester/MersenneResidueModeTests.cs @@ -0,0 +1,16 @@ +using FluentAssertions; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +[Trait("Category", "Fast")] +public class MersenneResidueModeTests +{ + [Fact] + public void IsMersennePrime_residue_mode_rejects_composite_exponent() + { + var tester = new MersenneNumberTester(useResidue: true, useIncremental: false); + tester.IsMersennePrime(136_000_002UL).Should().BeFalse(); + } +} + diff --git a/PerfectNumbers.Core.Tests/MersennePrimeFactorTester/MersennePrimeFactorTesterTests.cs b/PerfectNumbers.Core.Tests/MersennePrimeFactorTester/MersennePrimeFactorTesterTests.cs new file mode 100644 index 0000000..45975a5 --- /dev/null +++ b/PerfectNumbers.Core.Tests/MersennePrimeFactorTester/MersennePrimeFactorTesterTests.cs @@ -0,0 +1,29 @@ +using FluentAssertions; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +public class MersennePrimeFactorTesterTests +{ + [Theory] + [InlineData(5UL, 31UL, true)] + [InlineData(11UL, 23UL, true)] + [InlineData(11UL, 89UL, true)] + [InlineData(5UL, 7UL, false)] + [InlineData(7UL, 17UL, false)] + [InlineData(11UL, 331UL, false)] + [Trait("Category", "Fast")] + public void IsPrimeFactor_identifies_divisibility(ulong p, ulong q, bool expected) + { + MersennePrimeFactorTester.IsPrimeFactor(p, q, CancellationToken.None).Should().Be(expected); + } + + [Fact] + [Trait("Category", "Slow")] + public void GetOrderOf2ModPrime_handles_large_divisors() + { + UInt128 q = UInt128.Parse("618970019642690137449562111"); + MersennePrimeFactorTester.GetOrderOf2ModPrime(q, CancellationToken.None) + .Should().Be((UInt128)89); + } +} diff --git a/PerfectNumbers.Core.Tests/MersenneResidueStepperTests.cs b/PerfectNumbers.Core.Tests/MersenneResidueStepperTests.cs new file mode 100644 index 0000000..bdb575f --- /dev/null +++ b/PerfectNumbers.Core.Tests/MersenneResidueStepperTests.cs @@ -0,0 +1,37 @@ +using FluentAssertions; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +public class MersenneResidueStepperTests +{ + [Fact] + [Trait("Category", "Fast")] + public void Step_advances_residue() + { + var stepper = new MersenneResidueStepper(13, 4, 2); + + stepper.State().Should().Be((4UL, 2UL)); + + stepper.Step(3).Should().Be((7UL, 10UL)); + } + + [Fact] + [Trait("Category", "Fast")] + public void To_moves_directly_to_target_exponent() + { + var stepper = new MersenneResidueStepper(13, 4, 2); + + stepper.To(10).Should().Be((10UL, 9UL)); + } + + [Fact] + [Trait("Category", "Fast")] + public void Constructor_rejects_zero_modulus() + { + var act = () => new MersenneResidueStepper(0, 0, 0); + + act.Should().Throw(); + } +} + diff --git a/PerfectNumbers.Core.Tests/ModResidueTrackerTests.cs b/PerfectNumbers.Core.Tests/ModResidueTrackerTests.cs new file mode 100644 index 0000000..34eef95 --- /dev/null +++ b/PerfectNumbers.Core.Tests/ModResidueTrackerTests.cs @@ -0,0 +1,66 @@ +using FluentAssertions; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +[Trait("Category", "Fast")] +public class ModResidueTrackerTests +{ + [Fact] + public void IsDivisible_advances_residue_for_identity() + { + var tracker = new ModResidueTracker(ResidueModel.Identity); + + tracker.IsDivisible(5, 2).Should().BeFalse(); + tracker.IsDivisible(6, 2).Should().BeTrue(); + tracker.IsDivisible(7, 2).Should().BeFalse(); + } + + [Theory] + [InlineData(3UL, 7UL, true)] + [InlineData(5UL, 31UL, true)] + [InlineData(5UL, 7UL, false)] + [InlineData(11UL, 23UL, true)] + [InlineData(11UL, 31UL, false)] + public void IsDivisible_handles_mersenne_model(ulong p, ulong divisor, bool expected) + { + var tracker = new ModResidueTracker(ResidueModel.Mersenne); + + tracker.IsDivisible(p, divisor).Should().Be(expected); + } + + [Fact] + public void MergeOrAppend_tracks_existing_and_new_divisors() + { + var tracker = new ModResidueTracker(ResidueModel.Identity); + + tracker.BeginMerge(10); + tracker.MergeOrAppend(10, 3, out bool d3).Should().BeTrue(); + d3.Should().BeFalse(); + tracker.MergeOrAppend(10, 5, out bool d5).Should().BeTrue(); + d5.Should().BeTrue(); + + tracker.BeginMerge(10); + tracker.MergeOrAppend(10, 4, out bool d4).Should().BeTrue(); + d4.Should().BeFalse(); + + tracker.BeginMerge(11); + tracker.MergeOrAppend(11, 3, out d3).Should().BeTrue(); + d3.Should().BeFalse(); + tracker.MergeOrAppend(11, 4, out d4).Should().BeTrue(); + d4.Should().BeFalse(); + tracker.MergeOrAppend(11, 5, out d5).Should().BeTrue(); + d5.Should().BeFalse(); + } + + [Fact] + public void IsDivisible_requires_non_decreasing_numbers() + { + var tracker = new ModResidueTracker(ResidueModel.Identity); + tracker.IsDivisible(5, 2); + + Action act = () => tracker.IsDivisible(4, 2); + act.Should().Throw(); + } +} + diff --git a/PerfectNumbers.Core.Tests/ModuloAutomata/ModuloAutomatonTests.cs b/PerfectNumbers.Core.Tests/ModuloAutomata/ModuloAutomatonTests.cs new file mode 100644 index 0000000..91b87f0 --- /dev/null +++ b/PerfectNumbers.Core.Tests/ModuloAutomata/ModuloAutomatonTests.cs @@ -0,0 +1,91 @@ +using FluentAssertions; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +[Trait("Category", "Fast")] +public class ModuloAutomatonTests +{ + [Fact] + public void Ending7Automaton_mod9_hits_every_9th() + { + // Start below 7; sequence: 7, 17, 27, 37, ... + var auto = new Ending7Automaton(1, 9); + int hits = 0; + ulong firstHit = 0; + for (int i = 0; i < 40; i++) + { + ulong n = auto.Current(); + bool div9 = auto.DivisibleBy(9); + if (div9) + { + hits++; + if (firstHit == 0) + { + firstHit = n; + } + } + + auto.Next(); + } + + // Known CRT class: n ≡ 27 (mod 90) when n ends with 7 and n ≡ 0 (mod 9) + firstHit.Should().Be(27); + hits.Should().BeGreaterThan(1); + } + + [Theory] + [InlineData(13UL, 2000)] + [InlineData(17UL, 2000)] + public void MersenneResidueAutomaton_matches_direct_mods(ulong p, int count) + { + // Compare residue progression against direct modular computations + var auto = new MersenneResidueAutomaton(p); + var twoP = (System.UInt128)p << 1; + var q = twoP + 1UL; + + for (int i = 0; i < count; i++) + { + ulong r10 = Mod10(q); + ulong r8 = (ulong)q & 7UL; + ulong r3 = Mod3(q); + ulong r5 = Mod5(q); + + auto.Mod10R.Should().Be(r10); + auto.Mod8R.Should().Be(r8); + auto.Mod3R.Should().Be(r3); + auto.Mod5R.Should().Be(r5); + + auto.Next(); + q += twoP; + } + } + + private static ulong Mod10(System.UInt128 value) + { + ulong high = (ulong)(value >> 64); + ulong low = (ulong)value; + ulong highRem = Mod10(high); + ulong lowRem = Mod10(low); + ulong combined = lowRem + highRem * 6UL; + return Mod10(combined); + } + + private static ulong Mod10(ulong value) => value % 10UL; + + private static ulong Mod3(System.UInt128 value) + { + ulong high = (ulong)(value >> 64); + ulong low = (ulong)value; + ulong rem = (low % 3UL) + (high % 3UL); + return rem >= 3UL ? rem - 3UL : rem; + } + + private static ulong Mod5(System.UInt128 value) + { + ulong high = (ulong)(value >> 64); + ulong low = (ulong)value; + ulong rem = (low % 5UL) + (high % 5UL); + return rem >= 5UL ? rem - 5UL : rem; + } +} diff --git a/PerfectNumbers.Core.Tests/PerfectNumbers.Core.Tests.csproj b/PerfectNumbers.Core.Tests/PerfectNumbers.Core.Tests.csproj new file mode 100644 index 0000000..4b979b2 --- /dev/null +++ b/PerfectNumbers.Core.Tests/PerfectNumbers.Core.Tests.csproj @@ -0,0 +1,25 @@ + + + net8.0 + enable + enable + false + true + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + diff --git a/PerfectNumbers.Core.Tests/PowerCacheTests.cs b/PerfectNumbers.Core.Tests/PowerCacheTests.cs new file mode 100644 index 0000000..ef3bcda --- /dev/null +++ b/PerfectNumbers.Core.Tests/PowerCacheTests.cs @@ -0,0 +1,27 @@ +using System.Numerics; +using FluentAssertions; +using PeterO.Numbers; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +public class PowerCacheTests +{ + [Fact] + [Trait("Category", "Fast")] + public void Get_returns_big_integer_power() + { + PowerCache.Get(2UL, 5).Should().Be(new BigInteger(32)); + PowerCache.Get(2UL, 10).Should().Be(new BigInteger(1024)); + } + + [Fact] + [Trait("Category", "Fast")] + public void Get_returns_einteger_power() + { + EInteger baseVal = EInteger.FromInt32(3); + PowerCache.Get(baseVal, 4).Should().Be(EInteger.FromInt32(81)); + PowerCache.Get(baseVal, 2).Should().Be(EInteger.FromInt32(9)); + } +} + diff --git a/PerfectNumbers.Core.Tests/PrimeCacheTests.cs b/PerfectNumbers.Core.Tests/PrimeCacheTests.cs new file mode 100644 index 0000000..307fcf8 --- /dev/null +++ b/PerfectNumbers.Core.Tests/PrimeCacheTests.cs @@ -0,0 +1,39 @@ +using FluentAssertions; +using PeterO.Numbers; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +public class PrimeCacheTests +{ + [Fact] + [Trait("Category", "Fast")] + public void GetEulerPrimes_returns_primes_congruent_to_one_mod_four() + { + var cache = new PrimeCache(); + EInteger[] result = cache.GetEulerPrimes(EInteger.FromInt32(2), EInteger.FromInt32(20)); + + result.Should().Equal( + EInteger.FromInt32(5), + EInteger.FromInt32(13), + EInteger.FromInt32(17)); + } + + [Fact] + [Trait("Category", "Fast")] + public void EnumeratePrimes_returns_sequence_starting_from_value() + { + var cache = new PrimeCache(); + EInteger[] result = cache.EnumeratePrimes(EInteger.FromInt32(10)) + .Take(5) + .ToArray(); + + result.Should().Equal( + EInteger.FromInt32(11), + EInteger.FromInt32(13), + EInteger.FromInt32(17), + EInteger.FromInt32(19), + EInteger.FromInt32(23)); + } +} + diff --git a/PerfectNumbers.Core.Tests/PrimeTester/PrimeTesterGcdTests.cs b/PerfectNumbers.Core.Tests/PrimeTester/PrimeTesterGcdTests.cs new file mode 100644 index 0000000..3b36034 --- /dev/null +++ b/PerfectNumbers.Core.Tests/PrimeTester/PrimeTesterGcdTests.cs @@ -0,0 +1,25 @@ +using FluentAssertions; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +[Trait("Category", "Fast")] +public class PrimeTesterGcdTests +{ + [Theory] + [InlineData(81UL, true)] + [InlineData(101UL, false)] + public void SharesFactorWithMaxExponent_detects_gcd(ulong n, bool expected) + { + PrimeTester.SharesFactorWithMaxExponent(n).Should().Be(expected); + } + + [Fact] + public void SharesFactorWithMaxExponentBatch_filters_values() + { + ulong[] values = [81UL, 101UL]; + Span results = stackalloc byte[values.Length]; + PrimeTester.SharesFactorWithMaxExponentBatch(values, results); + results.ToArray().Should().Equal(new byte[] { 1, 0 }); + } +} diff --git a/PerfectNumbers.Core.Tests/PrimeTester/PrimeTesterIsPrimeTests.cs b/PerfectNumbers.Core.Tests/PrimeTester/PrimeTesterIsPrimeTests.cs new file mode 100644 index 0000000..622448f --- /dev/null +++ b/PerfectNumbers.Core.Tests/PrimeTester/PrimeTesterIsPrimeTests.cs @@ -0,0 +1,39 @@ +using FluentAssertions; +using PerfectNumbers.Core.Gpu; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +public class PrimeTesterIsPrimeTests +{ + [Theory] + [Trait("Category", "Fast")] + [InlineData(1UL, false)] + [InlineData(2UL, true)] + [InlineData(3UL, true)] + [InlineData(4UL, false)] + [InlineData(7UL, true)] + [InlineData(97UL, true)] + [InlineData(121UL, false)] + public void IsPrime_returns_expected_results(ulong n, bool expected) + { + var tester = new PrimeTester(); + + tester.IsPrime(n, CancellationToken.None).Should().Be(expected); + } + + [Fact] + [Trait("Category", "Fast")] + public void IsPrimeGpu_falls_back_to_cpu_when_forced() + { + var tester = new PrimeTester(); + GpuContextPool.ForceCpu = true; + + tester.IsPrimeGpu(11UL, CancellationToken.None).Should().BeTrue(); + + tester.IsPrimeGpu(12UL, CancellationToken.None).Should().BeFalse(); + + GpuContextPool.ForceCpu = false; + } +} + diff --git a/PerfectNumbers.Core.Tests/PrimesGeneratorTests.cs b/PerfectNumbers.Core.Tests/PrimesGeneratorTests.cs new file mode 100644 index 0000000..7f3cf81 --- /dev/null +++ b/PerfectNumbers.Core.Tests/PrimesGeneratorTests.cs @@ -0,0 +1,25 @@ +using FluentAssertions; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +public class PrimesGeneratorTests +{ + [Fact] + [Trait("Category", "Fast")] + public void Small_primes_and_squares_match() + { + var primes = PrimesGenerator.SmallPrimes; + var squares = PrimesGenerator.SmallPrimesPow2; + + primes[..5].Should().Equal(new ulong[] { 2UL, 3UL, 5UL, 7UL, 11UL }); + + squares[..5].Should().Equal(new ulong[] { 4UL, 9UL, 25UL, 49UL, 121UL }); + + for (int i = 0; i < 5; i++) + { + squares[i].Should().Be(primes[i] * primes[i]); + } + } +} + diff --git a/PerfectNumbers.Core.Tests/RationalHelperTests.cs b/PerfectNumbers.Core.Tests/RationalHelperTests.cs new file mode 100644 index 0000000..85993e2 --- /dev/null +++ b/PerfectNumbers.Core.Tests/RationalHelperTests.cs @@ -0,0 +1,53 @@ +using System.Numerics; +using FluentAssertions; +using PeterO.Numbers; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +public class RationalHelperTests +{ + [Fact] + [Trait("Category", "Fast")] + public void FromDoubleRounded_ReducesFraction() + { + ERational result = RationalHelper.FromDoubleRounded(0.75); + + result.Numerator.ToInt32Checked().Should().Be(3); + result.Denominator.ToInt32Checked().Should().Be(4); + } + + [Fact] + [Trait("Category", "Fast")] + public void ToEInteger_ReturnsMatchingValue() + { + BigInteger value = BigInteger.Parse("12345678901234567890"); + + EInteger einteger = RationalHelper.ToEInteger(value); + + einteger.ToString().Should().Be(value.ToString()); + } + + [Fact] + [Trait("Category", "Fast")] + public void FloorToUInt64_ReturnsFloor() + { + ERational value = ERational.Create(5, 2); + + ulong result = RationalHelper.FloorToUInt64(value); + + result.Should().Be(2UL); + } + + [Fact] + [Trait("Category", "Fast")] + public void CeilingToUInt64_RoundsUpWhenFractional() + { + ERational fractional = ERational.Create(5, 2); + ERational integral = ERational.Create(6, 2); + + RationalHelper.CeilingToUInt64(fractional).Should().Be(3UL); + RationalHelper.CeilingToUInt64(integral).Should().Be(3UL); + } +} + diff --git a/PerfectNumbers.Core.Tests/RleBlacklistTests.cs b/PerfectNumbers.Core.Tests/RleBlacklistTests.cs new file mode 100644 index 0000000..0d9fd2c --- /dev/null +++ b/PerfectNumbers.Core.Tests/RleBlacklistTests.cs @@ -0,0 +1,12 @@ +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +public class RleBlacklistTests +{ + [Fact(Skip = "TODO: implement test")] + [Trait("Category", "Fast")] + public void Placeholder() + { + } +} diff --git a/PerfectNumbers.Core.Tests/StringBuilderPoolTests.cs b/PerfectNumbers.Core.Tests/StringBuilderPoolTests.cs new file mode 100644 index 0000000..c06f606 --- /dev/null +++ b/PerfectNumbers.Core.Tests/StringBuilderPoolTests.cs @@ -0,0 +1,36 @@ +using FluentAssertions; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +public class StringBuilderPoolTests +{ + [Fact] + [Trait("Category", "Fast")] + public void Rent_ReturnsClearedBuilder() + { + var sb = StringBuilderPool.Rent(); + + sb.Length.Should().Be(0); + + StringBuilderPool.Return(sb); + } + + [Fact] + [Trait("Category", "Fast")] + public void Return_AllowsReuseOfBuilder() + { + var sb = StringBuilderPool.Rent(); + sb.Append("data"); + + StringBuilderPool.Return(sb); + + var reused = StringBuilderPool.Rent(); + + reused.Should().BeSameAs(sb); + reused.Length.Should().Be(0); + + StringBuilderPool.Return(reused); + } +} + diff --git a/PerfectNumbers.Core.Tests/TextFileWriterTests.cs b/PerfectNumbers.Core.Tests/TextFileWriterTests.cs new file mode 100644 index 0000000..e584efe --- /dev/null +++ b/PerfectNumbers.Core.Tests/TextFileWriterTests.cs @@ -0,0 +1,12 @@ +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +public class TextFileWriterTests +{ + [Fact(Skip = "TODO: implement test")] + [Trait("Category", "Fast")] + public void Placeholder() + { + } +} diff --git a/PerfectNumbers.Core.Tests/UInt128Extensions/UInt128ExtensionsTests.cs b/PerfectNumbers.Core.Tests/UInt128Extensions/UInt128ExtensionsTests.cs new file mode 100644 index 0000000..d735285 --- /dev/null +++ b/PerfectNumbers.Core.Tests/UInt128Extensions/UInt128ExtensionsTests.cs @@ -0,0 +1,218 @@ +using System.Numerics; +using FluentAssertions; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +public class UInt128ExtensionsTests +{ + public static IEnumerable BinaryGcdCases() + { + yield return new object[] { UInt128.Zero, (UInt128)5, (UInt128)5 }; + yield return new object[] { (UInt128)18, (UInt128)24, (UInt128)6 }; + + UInt128 big1 = (UInt128.One << 96) + 12345; + UInt128 big2 = (UInt128.One << 80) + 67890; + yield return new object[] + { + big1, + big2, + (UInt128)BigInteger.GreatestCommonDivisor((BigInteger)big1, (BigInteger)big2) + }; + } + + [Theory] + [Trait("Category", "Fast")] + [MemberData(nameof(BinaryGcdCases))] + public void BinaryGcd_matches_BigInteger(UInt128 a, UInt128 b, UInt128 expected) + { + a.BinaryGcd(b).Should().Be(expected); + } + + public static IEnumerable TrailingZeroCases() + { + yield return new object[] { UInt128.Zero, 128 }; + yield return new object[] { (UInt128)1, 0 }; + yield return new object[] { (UInt128)8, 3 }; + yield return new object[] { UInt128.One << 80, 80 }; + } + + [Theory] + [Trait("Category", "Fast")] + [MemberData(nameof(TrailingZeroCases))] + public void CountTrailingZeros_returns_expected(UInt128 value, int expected) + { + value.CountTrailingZeros().Should().Be(expected); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(3UL)] + [InlineData(5UL)] + [InlineData(7UL)] + public void CalculateOrder_matches_naive(ulong q) + { + UInt128 q128 = q; + ulong expected = NaiveOrder(q); + q128.CalculateOrder().Should().Be(expected); + } + + private static ulong NaiveOrder(ulong q) + { + if (q <= 2UL) + { + return 0UL; + } + + ulong order = 1UL; + ulong value = 2UL % q; + while (value != 1UL) + { + value = (value << 1) % q; + order++; + } + + return order; + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(2UL, true)] + [InlineData(9UL, false)] + [InlineData(97UL, true)] + public void IsPrimeCandidate_checks_small(ulong n, bool expected) + { + ((UInt128)n).IsPrimeCandidate().Should().Be(expected); + } + + [Fact] + [Trait("Category", "Fast")] + public void IsPrimeCandidate_detects_large_composite() + { + UInt128 n = (UInt128.One << 64) + 1; + n.IsPrimeCandidate().Should().BeFalse(); + } + + [Fact] + [Trait("Category", "Fast")] + public void Mod10_8_5_3_matches_operator() + { + UInt128 value = (UInt128.One << 100) + 12345678901234567890UL; + + value.Mod10_8_5_3(out ulong mod10, out ulong mod8, out ulong mod5, out ulong mod3); + + ulong expected10 = (ulong)(value % 10); + ulong expected8 = (ulong)(value % 8); + ulong expected5 = (ulong)(value % 5); + ulong expected3 = (ulong)(value % 3); + + mod10.Should().Be(expected10); + mod8.Should().Be(expected8); + mod5.Should().Be(expected5); + mod3.Should().Be(expected3); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(2UL, 10UL, 17UL)] + [InlineData(123456789UL, 3UL, 97UL)] + public void ModPow_matches_BigInteger(ulong value, ulong exponent, ulong modulus) + { + UInt128 v = value; + UInt128 e = exponent; + UInt128 m = modulus; + + UInt128 expected = (UInt128)BigInteger.ModPow(value, exponent, modulus); + + v.ModPow(e, m).Should().Be(expected); + } + + [Fact] + [Trait("Category", "Fast")] + public void ModPow_handles_large_values() + { + UInt128 v = (UInt128.One << 96) + 12345; + UInt128 e = 17; + UInt128 m = 97; + + UInt128 expected = (UInt128)BigInteger.ModPow((BigInteger)v, (BigInteger)e, (BigInteger)m); + + v.ModPow(e, m).Should().Be(expected); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(0UL)] + [InlineData(1UL)] + [InlineData(123456789UL)] + public void Mod10_matches_operator(ulong low) + { + UInt128 value = (UInt128.One << 100) + low; + value.Mod10().Should().Be((ulong)(value % 10)); + } + + [Fact] + [Trait("Category", "Fast")] + public void Mod8_matches_operator() + { + UInt128 value = (UInt128.One << 100) + 12345; + value.Mod8().Should().Be((ulong)(value % 8)); + } + + [Fact] + [Trait("Category", "Fast")] + public void Mod3_matches_operator() + { + UInt128 value = (UInt128.One << 100) + 12345; + value.Mod3().Should().Be((ulong)(value % 3)); + } + + [Fact] + [Trait("Category", "Fast")] + public void Mod5_matches_operator() + { + UInt128 value = (UInt128.One << 100) + 12345; + value.Mod5().Should().Be((ulong)(value % 5)); + } + + [Fact] + [Trait("Category", "Fast")] + public void Mul64_matches_expected_product() + { + UInt128 a = (UInt128.One << 80) + 123456789; + UInt128 b = (UInt128.One << 64) + 987654321; + + UInt128 expected = (UInt128)((UInt128)(ulong)a * b); + + a.Mul64(b).Should().Be(expected); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(123UL, 456UL, 789UL)] + [InlineData(123456789UL, 987654321UL, 1009UL)] + public void MulMod_matches_BigInteger(ulong a, ulong b, ulong modulus) + { + UInt128 ua = a % modulus; + UInt128 ub = b % modulus; + UInt128 mod = modulus; + + UInt128 expected = (UInt128)(((BigInteger)ua * (BigInteger)ub) % modulus); + + ua.MulMod(ub, mod).Should().Be(expected); + } + + [Fact] + [Trait("Category", "Fast")] + public void MulMod_handles_large_inputs() + { + UInt128 a = (UInt128.One << 80) + 12345; + UInt128 b = (UInt128.One << 96) + 67890; + UInt128 mod = (UInt128.One << 100) - 93; + + UInt128 expected = (UInt128)(((BigInteger)a * (BigInteger)b) % (BigInteger)mod); + + a.MulMod(b, mod).Should().Be(expected); + } +} + diff --git a/PerfectNumbers.Core.Tests/ULongExtensionsTests.cs b/PerfectNumbers.Core.Tests/ULongExtensionsTests.cs new file mode 100644 index 0000000..68df302 --- /dev/null +++ b/PerfectNumbers.Core.Tests/ULongExtensionsTests.cs @@ -0,0 +1,185 @@ +using System.Numerics; +using FluentAssertions; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +public class ULongExtensionsTests +{ + [Theory] + [Trait("Category", "Fast")] + [InlineData(0UL)] + [InlineData(1UL)] + [InlineData(9UL)] + [InlineData(10UL)] + [InlineData(1234567890UL)] + [InlineData(ulong.MaxValue)] + public void Mod10_returns_same_as_operator(ulong value) + { + ulong expected = value % 10UL; + + ULongExtensions.Mod10(value).Should().Be(expected); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(2UL, true)] + [InlineData(9UL, false)] + [InlineData(97UL, true)] + public void IsPrimeCandidate_filters_composites(ulong candidate, bool expected) + { + candidate.IsPrimeCandidate().Should().Be(expected); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(3UL)] + [InlineData(5UL)] + [InlineData(7UL)] + [InlineData(11UL)] + public void CalculateOrder_matches_naive_algorithm(ulong q) + { + ulong expected = NaiveOrder(q); + + q.CalculateOrder().Should().Be(expected); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(2UL, 10UL, 17UL)] + [InlineData(123456789UL, 3UL, 97UL)] + public void ModPow64_matches_BigInteger(ulong value, ulong exponent, ulong modulus) + { + ulong expected = (ulong)BigInteger.ModPow(value, exponent, modulus); + + value.ModPow64(exponent, modulus).Should().Be(expected); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(10UL, 17UL)] + [InlineData(123UL, 97UL)] + public void PowMod_returns_same_as_BigInteger(ulong exponent, ulong modulus) + { + UInt128 expected = (UInt128)BigInteger.ModPow(2, exponent, modulus); + + exponent.PowMod((UInt128)modulus).Should().Be(expected); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(5UL, 7UL, 3UL, 4UL)] + [InlineData(10UL, 7UL, 3UL, 2UL)] + public void PowModWithCycle_uses_cycle_length(ulong exponent, ulong modulus, ulong cycle, ulong expected) + { + ((UInt128)exponent).PowModWithCycle((UInt128)modulus, cycle).Should().Be((UInt128)expected); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(15UL, true)] + [InlineData(7UL, false)] + [InlineData(9UL, false)] + public void SharesFactorWithExponentMinusOne_detects_relationship(ulong exponent, bool expected) + { + exponent.SharesFactorWithExponentMinusOne().Should().Be(expected); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(1234567890123456789UL, 3UL)] + [InlineData(ulong.MaxValue, 7UL)] + public void FastDiv64_matches_division(ulong value, ulong divisor) + { + ulong mul = (ulong)(((UInt128)1 << 64) / divisor); + + value.FastDiv64(divisor, mul).Should().Be(value / divisor); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(0UL, 0UL)] + [InlineData(ulong.MaxValue, 1UL)] + [InlineData(123456789UL, 987654321UL)] + public void MulHigh_matches_high_bits(ulong x, ulong y) + { + ulong expected = (ulong)(((UInt128)x * y) >> 64); + + x.MulHigh(y).Should().Be(expected); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(0UL, 0UL)] + [InlineData(ulong.MaxValue, 2UL)] + [InlineData(123456789UL, 987654321UL)] + public void Mul64_matches_UInt128_product(ulong a, ulong b) + { + UInt128 expected = (UInt128)a * b; + + a.Mul64(b).Should().Be(expected); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(123456789UL, 987654321UL, 97UL)] + [InlineData(ulong.MaxValue, ulong.MaxValue, 1009UL)] + public void MulMod64_matches_BigInteger(ulong a, ulong b, ulong modulus) + { + ulong expected = (ulong)BigInteger.Remainder((BigInteger)a * b, modulus); + + a.MulMod64(b, modulus).Should().Be(expected); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(100UL, 7UL, 3UL)] + [InlineData(123UL, 9UL, 6UL)] + public void PowModWithCycle_ulong_overload_uses_cycle(ulong exponent, ulong modulus, ulong cycle) + { + UInt128 expected = (UInt128)BigInteger.ModPow(2, exponent, modulus); + + exponent.PowModWithCycle((UInt128)modulus, cycle).Should().Be(expected); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(100UL, 7UL, 3UL)] + [InlineData(123UL, 9UL, 6UL)] + public void PowModWithCycle_ulong_uint128_cycle_uses_cycle(ulong exponent, ulong modulus, ulong cycle) + { + UInt128 expected = (UInt128)BigInteger.ModPow(2, exponent, modulus); + + exponent.PowModWithCycle((UInt128)modulus, (UInt128)cycle).Should().Be(expected); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(11UL, 13UL)] + public void PowModCrt_matches_BigInteger(ulong exponent, ulong modulus) + { + MersenneDivisorCycles cycles = MersenneDivisorCycles.Shared; + UInt128 expected = (UInt128)BigInteger.ModPow(2, exponent, modulus); + + exponent.PowModCrt((UInt128)modulus, cycles).Should().Be(expected); + } + + private static ulong NaiveOrder(ulong q) + { + if (q <= 2UL) + { + return 0UL; + } + + ulong order = 1UL; + ulong value = 2UL % q; + while (value != 1UL) + { + value = (value << 1) % q; + order++; + } + + return order; + } + +} diff --git a/PerfectNumbers.Core/AlphaCache.cs b/PerfectNumbers.Core/AlphaCache.cs new file mode 100644 index 0000000..b9aa435 --- /dev/null +++ b/PerfectNumbers.Core/AlphaCache.cs @@ -0,0 +1,41 @@ +using PeterO.Numbers; + +namespace PerfectNumbers.Core; + +public sealed class AlphaCache +{ + private readonly Dictionary<(EInteger P, int K), AlphaValues> _cache = new(); + + public AlphaValues Get(EInteger p, int k) + { + if (!_cache.TryGetValue((p, k), out var values)) + { + ERational alphaPValue = AlphaCalculations.ComputeAlphaP(p, k); + ERational alphaMValue = RationalNumbers.Two.Divide(alphaPValue); + values = new AlphaValues(alphaMValue, alphaPValue); + _cache[(p, k)] = values; + } + + return values; + } + + public void Clear() + { + _cache.Clear(); + } + + public int Count => _cache.Count; + + public readonly struct AlphaValues + { + public readonly ERational AlphaM; + public readonly ERational AlphaP; + + public AlphaValues(ERational alphaM, ERational alphaP) + { + AlphaM = alphaM; + AlphaP = alphaP; + } + } +} + diff --git a/PerfectNumbers.Core/AlphaCalculations.cs b/PerfectNumbers.Core/AlphaCalculations.cs new file mode 100644 index 0000000..ee8d2b2 --- /dev/null +++ b/PerfectNumbers.Core/AlphaCalculations.cs @@ -0,0 +1,29 @@ +using PeterO.Numbers; + +namespace PerfectNumbers.Core; + +public static class AlphaCalculations +{ + public static ERational ComputeAlphaP(EInteger p, int k) + { + int exponent = checked(4 * k + 1); + EInteger y = PowerCache.Get(p, exponent); + return ERational.Create(y * p - 1, y * (p - 1)); + } + + public static ERational ComputeAlphaFactor(EInteger q, int a2) + { + EInteger num = PowerCache.Get(q, a2 + 1) - EInteger.One; + EInteger den = PowerCache.Get(q, a2) * (q - EInteger.One); + return ERational.Create(num, den); + } + + public static ERational ComputeAlphaM(ReadOnlySpan primes, int maxR, int a2) + { + ERational result = ERational.One; + for (int i = 0; i < maxR; i++) + result = result.Multiply(ComputeAlphaFactor(primes[i], a2)); + return result; + } +} + diff --git a/PerfectNumbers.Core/AlphaMScannerCandidateStatus.cs b/PerfectNumbers.Core/AlphaMScannerCandidateStatus.cs new file mode 100644 index 0000000..d322a94 --- /dev/null +++ b/PerfectNumbers.Core/AlphaMScannerCandidateStatus.cs @@ -0,0 +1,13 @@ +namespace PerfectNumbers.Core; + +public enum AlphaMScannerCandidateStatus +{ + UNKNOWN, + PRODUCT_ABOVE_TARGET, + PRODUCT_BELOW_TARGET, + ALPHA_P_AND_ALPHA_M_OUT_OF_RANGE, + ALPHA_P_OUT_OF_RANGE, + ALPHA_M_OUT_OF_RANGE, + OK, +} + diff --git a/PerfectNumbers.Core/Cpu/CpuConstants.cs b/PerfectNumbers.Core/Cpu/CpuConstants.cs new file mode 100644 index 0000000..6ad8832 --- /dev/null +++ b/PerfectNumbers.Core/Cpu/CpuConstants.cs @@ -0,0 +1,9 @@ +namespace PerfectNumbers.Core.Cpu; + +public static class CpuConstants +{ + // Allowed q mod 10 sets depending on last digit of M_p + public const int LastSevenMask10 = (1 << 7) | (1 << 9); + // For M_p ending with 1, restrict to {1,3,9}; 7 considered only for q == 7 (not possible for large p) + public const int LastOneMask10 = (1 << 1) | (1 << 3) | (1 << 9); +} \ No newline at end of file diff --git a/PerfectNumbers.Core/Cpu/MersenneNumberIncrementalCpuTester.cs b/PerfectNumbers.Core/Cpu/MersenneNumberIncrementalCpuTester.cs new file mode 100644 index 0000000..0cb4c21 --- /dev/null +++ b/PerfectNumbers.Core/Cpu/MersenneNumberIncrementalCpuTester.cs @@ -0,0 +1,83 @@ +namespace PerfectNumbers.Core.Cpu; + +public class MersenneNumberIncrementalCpuTester(GpuKernelType kernelType) +{ + private static readonly MersenneDivisorCycles _cycles = MersenneDivisorCycles.Shared; + private readonly GpuKernelType _kernelType = kernelType; + + public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime) + { + UInt128 k, one = k = UInt128.One; + ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL; + // Use residue automaton always on CPU + var auto = new MersenneResidueAutomaton(exponent); + + bool reject, shouldCheck; + UInt128 q = auto.CurrentQ(), + divPow, halfPow, phi, qCycle; + + ulong div, mod8, phi64; + + while (k <= maxK && Volatile.Read(ref isPrime)) + { + qCycle = MersenneDivisorCycles.GetCycle(q); + shouldCheck = lastIsSeven + ? (auto.Mod10R != 5UL) + : (auto.Mod10R != 5UL); + if (shouldCheck) + { + mod8 = auto.Mod8R; + if (mod8 != 1UL && mod8 != 7UL) + { + shouldCheck = false; + } + else if (auto.Mod3R == 0UL || auto.Mod5R == 0UL) + { + shouldCheck = false; + } + } + + if (shouldCheck) + { + if (_kernelType == GpuKernelType.Pow2Mod) + { + reject = exponent.PowModWithCycle(q, qCycle) == UInt128.One; + } + else + { + phi = q - one; + reject = false; + if (phi <= ulong.MaxValue) + { + phi64 = (ulong)phi; + if (phi64.PowModWithCycle(q, qCycle) == one) + { + halfPow = (phi64 >> 1).PowModWithCycle(q, qCycle) - one; + if (halfPow.BinaryGcd(q) == one) + { + div = phi64.FastDiv64(exponent, divMul); + divPow = div.PowModWithCycle(q, qCycle) - one; + if (divPow.BinaryGcd(q) == one) + { + reject = true; + } + } + } + } + } + + if (reject && q.IsPrimeCandidate()) + { + Volatile.Write(ref isPrime, false); + break; + } + } + + k += 1UL; + auto.Next(); + q = auto.CurrentQ(); + } + + return; + } +} \ No newline at end of file diff --git a/PerfectNumbers.Core/Cpu/MersenneNumberLucasLehmerCpuTester.cs b/PerfectNumbers.Core/Cpu/MersenneNumberLucasLehmerCpuTester.cs new file mode 100644 index 0000000..c0b4a4f --- /dev/null +++ b/PerfectNumbers.Core/Cpu/MersenneNumberLucasLehmerCpuTester.cs @@ -0,0 +1,43 @@ +using PeterO.Numbers; + +namespace PerfectNumbers.Core; + +public class MersenneNumberLucasLehmerCpuTester +{ + public bool IsPrime(ulong exponent) + { + // Early rejections aligned with incremental/order sieves, but safe for small p: + // - If 3 | p and p != 3, then 7 | M_p -> composite. + // - If p ≡ 1 (mod 4) and p shares a factor with (p-1), reject fast. + // - If p is divisible by divisors specific to numbers ending with 1 or 7, reject fast. + if ( + ((exponent % 3UL) == 0UL && exponent != 3UL) || + ((exponent % 5UL) == 0UL && exponent != 5UL) || + ((exponent % 7UL) == 0UL && exponent != 7UL) || + ((exponent % 11UL) == 0UL && exponent != 11UL) + ) + { + return false; + } + + if ((exponent & 3UL) == 1UL && exponent.SharesFactorWithExponentMinusOne()) + { + return false; + } + + UInt128 s = UInt128Numbers.Four, two = UInt128Numbers.Two, + m = (UInt128.One << (int)exponent) - UInt128.One; + + UInt128 limit = exponent - UInt128Numbers.Two; + for (UInt128 i = UInt128.Zero; i < limit; i++) + { + s = s.PowModWithCycle(two, MersenneDivisorCycles.GetCycle(m)) - two; + if (s < UInt128.Zero) + { + s += m; + } + } + + return s == UInt128.Zero; + } +} diff --git a/PerfectNumbers.Core/Cpu/MersenneNumberOrderCpuTester.cs b/PerfectNumbers.Core/Cpu/MersenneNumberOrderCpuTester.cs new file mode 100644 index 0000000..22b9048 --- /dev/null +++ b/PerfectNumbers.Core/Cpu/MersenneNumberOrderCpuTester.cs @@ -0,0 +1,74 @@ +namespace PerfectNumbers.Core.Cpu; + +public class MersenneNumberOrderCpuTester(GpuKernelType kernelType) +{ + private readonly GpuKernelType _kernelType = kernelType; + + public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime) + { + UInt128 k = 1UL; + var auto = new MersenneResidueAutomaton(exponent); + UInt128 qCycle, + q = auto.CurrentQ(); + + while (k <= maxK && Volatile.Read(ref isPrime)) + { + qCycle = MersenneDivisorCycles.GetCycle(q); + int allowMask2 = lastIsSeven ? CpuConstants.LastSevenMask10 : CpuConstants.LastOneMask10; + bool shouldCheck = ((allowMask2 >> (int)auto.Mod10R) & 1) != 0; + if (shouldCheck) + { + ulong mod8 = auto.Mod8R; + if (mod8 != 1UL && mod8 != 7UL) + { + shouldCheck = false; + } + else if (auto.Mod3R == 0UL || auto.Mod5R == 0UL) + { + shouldCheck = false; + } + } + if (shouldCheck) + { + if (_kernelType == GpuKernelType.Pow2Mod) + { + if (exponent.PowModWithCycle(q, qCycle) == 1UL) + { + Volatile.Write(ref isPrime, false); + break; + } + } + else + { + UInt128 phi = q - 1UL; + if (phi <= ulong.MaxValue) + { + ulong phi64 = (ulong)phi; + if (phi64.PowModWithCycle(q, qCycle) == 1UL) + { + UInt128 halfPow = (phi64 >> 1).PowModWithCycle(q, qCycle) - 1UL; + if (halfPow.BinaryGcd(q) == 1UL) + { + ulong divMul = (ulong)((((UInt128)1 << 64) - 1UL) / exponent) + 1UL; + ulong div = phi64.FastDiv64(exponent, divMul); + UInt128 divPow = div.PowModWithCycle(q, qCycle) - 1UL; + if (divPow.BinaryGcd(q) == 1UL) + { + Volatile.Write(ref isPrime, false); + break; + } + } + } + } + } + } + + k += 1UL; + auto.Next(); + q = auto.CurrentQ(); + } + + return; + } + +} \ No newline at end of file diff --git a/PerfectNumbers.Core/Cpu/MersenneNumberResidueCpuTester.cs b/PerfectNumbers.Core/Cpu/MersenneNumberResidueCpuTester.cs new file mode 100644 index 0000000..9d912c5 --- /dev/null +++ b/PerfectNumbers.Core/Cpu/MersenneNumberResidueCpuTester.cs @@ -0,0 +1,169 @@ +namespace PerfectNumbers.Core.Cpu; + +public class MersenneNumberResidueCpuTester +{ + private ModResidueTracker? _mersenneResidueTracker; + + // CPU residue variant using tracker + unrolled residue updates (no method calls in the loop). + public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime) + { + // Initialize/update Mersenne residue tracker and start a merge walk for ascending q divisors + + _mersenneResidueTracker ??= new(ResidueModel.Mersenne); + // For small divisors q <= 4,000,000 use cached cycle length to fast-path checks + // and avoid per-q powmods when possible when scanning lanes. + _mersenneResidueTracker.BeginMerge(exponent); + + // Start at k = 1: q0 = 2*p*1 + 1 + UInt128 k = UInt128.One; + UInt128 q = twoP + UInt128.One; + // Current residues + q.Mod10_8_5_3(out ulong r10, out ulong r8, out ulong r5, out ulong r3); + // Step residues for q += 2*p + ulong step10 = (ulong)(twoP % 10UL) % 10UL; + ulong step8 = (ulong)(twoP % 8UL); + ulong step3 = (ulong)(twoP % 3UL); + ulong step5 = (ulong)(twoP % 5UL); + + // Allowed last-digit sets for q depending on last digit of M_p + // lastIsSeven == true => allow {7,9} + // lastIsSeven == false => allow {1,3,7,9} + int allowMask = lastIsSeven ? CpuConstants.LastSevenMask10 : CpuConstants.LastOneMask10; + + // Predeclare temps to avoid redeclaration overhead in the loop + UInt128 remaining; + int i, lanes; + // per-lane residues + ulong r10_0, r10_1, r10_2, r10_3, r8_0, r8_1, r8_2, r8_3, r3_0, r3_1, r3_2, r3_3, r5_0, r5_1, r5_2, r5_3; + // q lanes + UInt128 twoP2 = twoP + twoP, + twoP3 = twoP2 + twoP, + twoP4 = twoP3 + twoP, + qIncremental; + + bool localIsPrime, isDivider; + + maxK++; + + ModResidueTracker tracker = _mersenneResidueTracker!; + while (k < maxK && (localIsPrime = Volatile.Read(ref isPrime))) + { + remaining = maxK - k; + if (remaining == UInt128.Zero) + { + break; + } + + lanes = remaining > 4UL ? 4 : (int)remaining; + + // Compute lane residues incrementally (avoid multiplications, minimize reductions) + // r10 + r10_0 = r10; + r10_1 = r10_0 + step10; if (r10_1 >= 20UL) r10_1 -= 20UL; if (r10_1 >= 10UL) r10_1 -= 10UL; + r10_2 = r10_1 + step10; if (r10_2 >= 20UL) r10_2 -= 20UL; if (r10_2 >= 10UL) r10_2 -= 10UL; + r10_3 = r10_2 + step10; if (r10_3 >= 20UL) r10_3 -= 20UL; if (r10_3 >= 10UL) r10_3 -= 10UL; + + // r8 + r8_0 = r8 & 7UL; + r8_1 = (r8_0 + step8) & 7UL; + r8_2 = (r8_1 + step8) & 7UL; + r8_3 = (r8_2 + step8) & 7UL; + + // r3 (pattern 0,1,2,0) + r3_0 = r3; if (r3_0 >= 3UL) r3_0 -= 3UL; + r3_1 = r3_0 + step3; if (r3_1 >= 3UL) r3_1 -= 3UL; + r3_2 = r3_1 + step3; if (r3_2 >= 3UL) r3_2 -= 3UL; + r3_3 = r3; if (r3_3 >= 3UL) r3_3 -= 3UL; + + // r5 (pattern 0,1,2,3) + r5_0 = r5; if (r5_0 >= 5UL) r5_0 -= 5UL; + r5_1 = r5_0 + step5; if (r5_1 >= 10UL) r5_1 -= 10UL; if (r5_1 >= 5UL) r5_1 -= 5UL; + r5_2 = r5_1 + step5; if (r5_2 >= 10UL) r5_2 -= 10UL; if (r5_2 >= 5UL) r5_2 -= 5UL; + r5_3 = r5_2 + step5; if (r5_3 >= 10UL) r5_3 -= 10UL; if (r5_3 >= 5UL) r5_3 -= 5UL; + + // lane 0 + if (lanes >= 1 && + (((allowMask >> (int)r10_0) & 1) != 0 && (r8_0 == 1UL || r8_0 == 7UL) && r3_0 != 0UL && r5_0 != 0UL)) + { + // cycle-based quick check for small q + if (localIsPrime && tracker.MergeOrAppend(exponent, q, out isDivider) && isDivider && + (q <= ulong.MaxValue ? ((ulong)q).IsPrimeCandidate() : q.IsPrimeCandidate())) + { + localIsPrime = false; + } + } + + // lane 1 + qIncremental = q + twoP; + if (lanes >= 2 && + ((allowMask >> (int)r10_1) & 1) != 0 && (r8_1 == 1UL || r8_1 == 7UL) && r3_1 != 0UL && r5_1 != 0UL && + localIsPrime) + { + if (localIsPrime && tracker.MergeOrAppend(exponent, qIncremental, out isDivider) && isDivider && + (qIncremental <= ulong.MaxValue ? ((ulong)qIncremental).IsPrimeCandidate() : qIncremental.IsPrimeCandidate())) + { + localIsPrime = false; + } + } + + // lane 2 + qIncremental += twoP; + if (lanes >= 3 && + ((allowMask >> (int)r10_2) & 1) != 0 && (r8_2 == 1UL || r8_2 == 7UL) && r3_2 != 0UL && r5_2 != 0UL && + localIsPrime) + { + if (localIsPrime && tracker.MergeOrAppend(exponent, qIncremental, out isDivider) && isDivider && + (qIncremental <= ulong.MaxValue ? ((ulong)qIncremental).IsPrimeCandidate() : qIncremental.IsPrimeCandidate())) + { + localIsPrime = false; + } + } + + // lane 3 + qIncremental += twoP; + if (lanes >= 4 && + ((allowMask >> (int)r10_3) & 1) != 0 && (r8_3 == 1UL || r8_3 == 7UL) && r3_3 != 0UL && r5_3 != 0UL && + localIsPrime) + { + if (localIsPrime && tracker.MergeOrAppend(exponent, qIncremental, out isDivider) && isDivider && + (qIncremental <= ulong.MaxValue ? ((ulong)qIncremental).IsPrimeCandidate() : qIncremental.IsPrimeCandidate())) + { + localIsPrime = false; + } + } + + if (!localIsPrime) + { + Volatile.Write(ref isPrime, false); + return; + } + else if (!Volatile.Read(ref isPrime)) + { + return; + } + + // advance base by processed lanes + k += (UInt128)lanes; + q += lanes switch + { + 0 => 0UL, + 1 => twoP, + 2 => twoP2, + 3 => twoP3, + 4 => twoP4, + _ => throw new ArgumentException("Unsupproted value", nameof(twoP)) + }; + + // advance residues by 'lanes' steps (consistent with base advance) + for (i = 0; i < lanes; i++) + { + r10 += step10; if (r10 >= 20UL) r10 -= 20UL; if (r10 >= 10UL) r10 -= 10UL; + r8 = (r8 + step8) & 7UL; + r3 += step3; if (r3 >= 3UL) r3 -= 3UL; + r5 += step5; if (r5 >= 10UL) r5 -= 10UL; if (r5 >= 5UL) r5 -= 5UL; + } + } + + return; + } +} diff --git a/PerfectNumbers.Core/EulerPrimeTester.cs b/PerfectNumbers.Core/EulerPrimeTester.cs new file mode 100644 index 0000000..ebe54f3 --- /dev/null +++ b/PerfectNumbers.Core/EulerPrimeTester.cs @@ -0,0 +1,19 @@ +using System.Runtime.CompilerServices; + +namespace PerfectNumbers.Core; + +public sealed class EulerPrimeTester +{ + private readonly PrimeTester _primeTester; + + public EulerPrimeTester(PrimeTester primeTester) + { + _primeTester = primeTester; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsEulerPrime(ulong n) + { + return (n & 3UL) == 1UL && _primeTester.IsPrime(n, CancellationToken.None); + } +} diff --git a/PerfectNumbers.Core/Gpu/GpuConstants.cs b/PerfectNumbers.Core/Gpu/GpuConstants.cs new file mode 100644 index 0000000..976de4d --- /dev/null +++ b/PerfectNumbers.Core/Gpu/GpuConstants.cs @@ -0,0 +1,7 @@ +namespace PerfectNumbers.Core.Gpu +{ + public static class GpuConstants + { + public const int ScanBatchSize = 2_097_152; + } +} \ No newline at end of file diff --git a/PerfectNumbers.Core/Gpu/GpuContextPool.cs b/PerfectNumbers.Core/Gpu/GpuContextPool.cs new file mode 100644 index 0000000..be545a3 --- /dev/null +++ b/PerfectNumbers.Core/Gpu/GpuContextPool.cs @@ -0,0 +1,152 @@ +using System.Collections.Concurrent; +using ILGPU; +using ILGPU.Runtime; + +namespace PerfectNumbers.Core.Gpu; + +public static class GpuContextPool +{ + private static readonly bool PoolingEnabled = false; + // Default device preference for generic GPU kernels (prime scans, NTT, etc.) + public static bool ForceCpu { get; set; } = false; + + internal sealed class PooledContext + { + private bool disposed; + + public Context Context { get; } + public Accelerator Accelerator { get; } + public bool IsCpu { get; } + + public PooledContext(bool preferCpu) + { + Context = Context.CreateDefault(); + Accelerator = Context.GetPreferredDevice(preferCpu).CreateAccelerator(Context); + IsCpu = Accelerator.AcceleratorType == AcceleratorType.CPU; + // NOTE: Avoid loading/compiling any kernel here to prevent implicit + // CL stream/queue creation during accelerator construction. + // Some OpenCL drivers are fragile when a queue is created immediately + // after device init. Real kernels will be JIT-loaded at first use. + } + + public void Dispose() + { + if (disposed) + { + return; + } + + // Ensure all cached GPU buffers for this accelerator are released. + NttGpuMath.ClearCaches(Accelerator); + // Release PrimeTester GPU state for this accelerator (kernel/device primes) + // Clear any per-accelerator cached resources to avoid releasing + // after the accelerator is destroyed. + PrimeTester.ClearGpuCaches(Accelerator); + + Accelerator.Dispose(); + Context.Dispose(); + disposed = true; + } + } + + private static readonly ConcurrentQueue CpuPool = new(); + private static readonly ConcurrentQueue GpuPool = new(); + private static readonly object CreationLock = new(); + + public static GpuContextLease Rent() + { + return RentPreferred(ForceCpu); + } + + // Allows callers to choose CPU/GPU per use-case, decoupled from ForceCpu. + public static GpuContextLease RentPreferred(bool preferCpu) + { + if (PoolingEnabled) + { + if (preferCpu && CpuPool.TryDequeue(out var cpu)) + { + return new GpuContextLease(cpu, CreationLock); + } + + if (!preferCpu && GpuPool.TryDequeue(out var gpu)) + { + return new GpuContextLease(gpu, CreationLock); + } + } + + // Serialize accelerator creation to avoid concurrent CL device/stream setup across threads. + lock (CreationLock) + { + return new GpuContextLease(new PooledContext(preferCpu), CreationLock); + } + } + + public static void DisposeAll() + { + if (!PoolingEnabled) + { + return; + } + + while (CpuPool.TryDequeue(out var cpu)) + { + cpu.Dispose(); + } + + while (GpuPool.TryDequeue(out var gpu)) + { + gpu.Dispose(); + } + } + + private static void Return(PooledContext ctx) + { + if (PoolingEnabled) + { + ctx.Accelerator.Synchronize(); + if (ctx.IsCpu) + { + CpuPool.Enqueue(ctx); + } + else + { + GpuPool.Enqueue(ctx); + } + } + + else + { + ctx.Dispose(); + } + } + + public struct GpuContextLease : IDisposable + { + private readonly PooledContext _ctx; + private bool disposed; + + internal GpuContextLease(PooledContext ctx, object kernelInitLock) + { + _ctx = ctx; + KernelInitLock = kernelInitLock; + } + + public Context Context => _ctx.Context; + + public Accelerator Accelerator => _ctx.Accelerator; + + public object KernelInitLock { get; } + + + public void Dispose() + { + if (disposed) + { + return; + } + + Return(_ctx); + disposed = true; + } + } +} diff --git a/PerfectNumbers.Core/Gpu/GpuKernelPool.cs b/PerfectNumbers.Core/Gpu/GpuKernelPool.cs new file mode 100644 index 0000000..a0fa444 --- /dev/null +++ b/PerfectNumbers.Core/Gpu/GpuKernelPool.cs @@ -0,0 +1,550 @@ +using System.Collections.Concurrent; +using ILGPU; +using ILGPU.Runtime; +using static PerfectNumbers.Core.Gpu.GpuContextPool; + +namespace PerfectNumbers.Core.Gpu; + +public readonly struct ResidueAutomatonArgs +{ + public readonly ulong Q0M10; + public readonly ulong Step10; + public readonly ulong Q0M8; + public readonly ulong Step8; + public readonly ulong Q0M3; + public readonly ulong Step3; + public readonly ulong Q0M5; + public readonly ulong Step5; + + public ResidueAutomatonArgs(ulong q0m10, ulong step10, ulong q0m8, ulong step8, ulong q0m3, ulong step3, ulong q0m5, ulong step5) + { + Q0M10 = q0m10; Step10 = step10; Q0M8 = q0m8; Step8 = step8; Q0M3 = q0m3; Step3 = step3; Q0M5 = q0m5; Step5 = step5; + } +} + +public sealed class KernelContainer +{ + // Serializes first-time initialization of kernels/buffers per accelerator. + public Action, ArrayView>? Order; + public Action, ArrayView1D>? Incremental; + public Action, ArrayView1D>? Pow2Mod; + public Action, ArrayView1D>? IncrementalOrder; + public Action, ArrayView1D>? Pow2ModOrder; + + // Optional device buffer with small divisor cycles (<= 4M). Index = divisor, value = cycle length. + public MemoryBuffer1D? SmallCycles; + + public static T InitOnce(ref T? slot, Func factory) where T : class + { + var current = Volatile.Read(ref slot); + if (current is not null) + { + return current; + } + + current = Volatile.Read(ref slot); + if (current is null) + { + current = factory(); + Volatile.Write(ref slot, current); + } + return current; + } +} + +public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, KernelContainer kernels) +{ + private bool disposedValue; + private IDisposable? _limiter = limiter; + private GpuContextLease? _gpu = gpu; + private KernelContainer _kernels = kernels; + + public readonly Accelerator Accelerator => _gpu?.Accelerator ?? throw new NullReferenceException("GPU context is already released"); + + public readonly Action, ArrayView> OrderKernel + { + get + { + var accel = Accelerator; // avoid capturing 'this' in lambda + lock (_gpu!.Value.KernelInitLock) + { + return KernelContainer.InitOnce(ref _kernels.Order, () => accel.LoadAutoGroupedStreamKernel, ArrayView>(OrderKernelScan)); + } + } + } + + public readonly Action, ArrayView1D> Pow2ModKernel + { + get + { + var accel = Accelerator; + lock (_gpu!.Value.KernelInitLock) + { + return KernelContainer.InitOnce(ref _kernels.Pow2Mod, () => accel.LoadAutoGroupedStreamKernel, ArrayView1D>(Pow2ModKernelScan)); + } + } + } + + public readonly Action, ArrayView1D> IncrementalKernel + { + get + { + var accel = Accelerator; + lock (_gpu!.Value.KernelInitLock) + { + return KernelContainer.InitOnce(ref _kernels.Incremental, () => accel.LoadAutoGroupedStreamKernel, ArrayView1D>(IncrementalKernelScan)); + } + } + } + + public readonly Action, ArrayView1D> IncrementalOrderKernel + { + get + { + var accel = Accelerator; + lock (_gpu!.Value.KernelInitLock) + { + return KernelContainer.InitOnce(ref _kernels.IncrementalOrder, () => accel.LoadAutoGroupedStreamKernel, ArrayView1D>(IncrementalOrderKernelScan)); + } + } + } + + public readonly Action, ArrayView1D> Pow2ModOrderKernel + { + get + { + var accel = Accelerator; + lock (_gpu!.Value.KernelInitLock) + { + return KernelContainer.InitOnce(ref _kernels.Pow2ModOrder, () => accel.LoadAutoGroupedStreamKernel, ArrayView1D>(Pow2ModOrderKernelScan)); + } + } + } + + // TODO: Plumb the small-cycles device buffer into all kernels that can benefit + // (some already accept it). Consider a compact type (byte/ushort) for memory footprint. + + private static void IncrementalKernelScan(Index1D index, ulong exponent, GpuUInt128 twoP, GpuUInt128 kStart, byte lastIsSeven, ulong divMul, + ulong q0m10, ulong q0m8, ulong q0m3, ulong q0m5, ArrayView orders, + ArrayView1D smallCycles) + { + ulong idx = (ulong)index.X; + // residue automaton + ulong step10 = ((exponent % 10UL) << 1) % 10UL; + ulong r10 = q0m10 + (step10 * idx) % 10UL; + r10 -= (r10 >= 10UL) ? 10UL : 0UL; + bool shouldCheck = r10 != 5UL; // skip q ≡ 5 (mod 10); other residues handled by r8/r3/r5 + if (shouldCheck) + { + ulong step8 = ((exponent & 7UL) << 1) & 7UL; + ulong r8 = (q0m8 + ((step8 * idx) & 7UL)) & 7UL; + if (r8 != 1UL && r8 != 7UL) + { + shouldCheck = false; + } + else + { + ulong step3 = ((exponent % 3UL) << 1) % 3UL; + ulong r3 = q0m3 + (step3 * (idx % 3UL)); r3 -= (r3 >= 3UL) ? 3UL : 0UL; + if (r3 == 0UL) + { + shouldCheck = false; + } + else + { + ulong step5Loc = ((exponent % 5UL) << 1) % 5UL; + ulong r5 = q0m5 + (step5Loc * (idx % 5UL)); if (r5 >= 5UL) r5 -= 5UL; + if (r5 == 0UL) + { + shouldCheck = false; + } + } + } + } + + if (!shouldCheck) + { + orders[index] = 0UL; + return; + } + + GpuUInt128 k = kStart + (GpuUInt128)idx; + GpuUInt128 q = twoP.Mul64(k) + GpuUInt128.One; + + // Small-cycles in-kernel early rejection from device table + if (q.High == 0UL && q.Low <= (ulong)(smallCycles.Length - 1)) + { + uint cyc = smallCycles[(int)q.Low]; + if (cyc != 0U && (exponent % cyc) == 0UL) + { + orders[index] = exponent; + return; + } + } + + GpuUInt128 phi = q - GpuUInt128.One; + if (phi.High != 0UL) + { + orders[index] = 0UL; + return; + } + + ulong phi64 = phi.Low; + if (GpuUInt128.Pow2Mod(phi64, q) != GpuUInt128.One) + { + orders[index] = 0UL; + return; + } + + GpuUInt128 halfPow = GpuUInt128.Pow2Mod(phi64 >> 1, q) - GpuUInt128.One; + if (GpuUInt128.BinaryGcd(halfPow, q) != GpuUInt128.One) + { + orders[index] = 0UL; + return; + } + + ulong div = phi64.FastDiv64(exponent, divMul); + GpuUInt128 divPow = GpuUInt128.Pow2Mod(div, q) - GpuUInt128.One; + if (GpuUInt128.BinaryGcd(divPow, q) != GpuUInt128.One) + { + orders[index] = 0UL; + return; + } + + orders[index] = 1UL; + } + + private static void OrderKernelScan(Index1D index, ulong exponent, ulong divMul, ArrayView qs, ArrayView orders) + { + GpuUInt128 q = qs[index]; + GpuUInt128 phi = q - GpuUInt128.One; + if (phi.High != 0UL) + { + orders[index] = 0UL; + return; + } + + ulong phi64 = phi.Low; + GpuUInt128 pow = GpuUInt128.Pow2Mod(phi64, q); + if (pow != GpuUInt128.One) + { + orders[index] = 0UL; + return; + } + + GpuUInt128 halfPow = GpuUInt128.Pow2Mod(phi64 >> 1, q) - GpuUInt128.One; + if (GpuUInt128.BinaryGcd(halfPow, q) != GpuUInt128.One) + { + orders[index] = 0UL; + return; + } + + ulong div = phi64.FastDiv64(exponent, divMul); + GpuUInt128 divPow = GpuUInt128.Pow2Mod(div, q) - GpuUInt128.One; + if (GpuUInt128.BinaryGcd(divPow, q) != GpuUInt128.One) + { + orders[index] = 0UL; + return; + } + + orders[index] = exponent; + } + + private void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + _gpu?.Dispose(); + _gpu = null; + + _limiter?.Dispose(); + _limiter = null; + } + + // free unmanaged resources (unmanaged objects) and override finalizer + // set large fields to null + disposedValue = true; + } + } + + private static void Pow2ModKernelScan(Index1D index, ulong exponent, GpuUInt128 twoP, GpuUInt128 kStart, byte lastIsSeven, ulong _, + ulong q0m10, ulong q0m8, ulong q0m3, ulong q0m5, ArrayView orders, + ArrayView1D smallCycles) + { + ulong idx = (ulong)index.X; + ulong step10 = ((exponent % 10UL) << 1) % 10UL; + ulong r10 = q0m10 + (step10 * idx) % 10UL; r10 -= (r10 >= 10UL) ? 10UL : 0UL; + bool shouldCheck = r10 != 5UL; + if (shouldCheck) + { + ulong step8 = ((exponent & 7UL) << 1) & 7UL; + ulong r8 = (q0m8 + ((step8 * idx) & 7UL)) & 7UL; + if (r8 != 1UL && r8 != 7UL) + { + shouldCheck = false; + } + else + { + ulong step3 = ((exponent % 3UL) << 1) % 3UL; + ulong r3 = q0m3 + (step3 * (idx % 3UL)); r3 -= (r3 >= 3UL) ? 3UL : 0UL; + if (r3 == 0UL) + { + shouldCheck = false; + } + else + { + ulong step5Loc = ((exponent % 5UL) << 1) % 5UL; + ulong r5 = q0m5 + (step5Loc * (idx % 5UL)); if (r5 >= 5UL) r5 -= 5UL; + if (r5 == 0UL) + { + shouldCheck = false; + } + } + } + } + if (!shouldCheck) + { + orders[index] = 0UL; + return; + } + + GpuUInt128 k = kStart + (GpuUInt128)idx; + GpuUInt128 q = twoP.Mul64(k) + GpuUInt128.One; + // Small-cycles in-kernel early rejection from device table + if (q.High == 0UL && q.Low <= (ulong)(smallCycles.Length - 1)) + { + uint cyc = smallCycles[(int)q.Low]; + if (cyc != 0U && (exponent % cyc) == 0UL) + { + orders[index] = exponent; + return; + } + } + if (GpuUInt128.Pow2Mod(exponent, q) != GpuUInt128.One) + { + orders[index] = 0UL; + return; + } + + orders[index] = exponent; + } + + // Device-friendly small cycle length for 2 mod divisor + private static ulong CalculateCycleLengthSmall(ulong divisor) + { + if ((divisor & (divisor - 1UL)) == 0UL) + return 1UL; + + ulong order = 1UL, pow = 2UL; + while (pow != 1UL) + { + pow <<= 1; + if (pow > divisor) + pow -= divisor; + + order++; + } + + return order; + } + + private static void IncrementalOrderKernelScan(Index1D index, ulong exponent, GpuUInt128 twoP, GpuUInt128 kStart, byte lastIsSeven, ulong divMul, + ResidueAutomatonArgs ra, ArrayView found, ArrayView1D smallCycles) + { + ulong idx = (ulong)index.X; + ulong r10 = ra.Q0M10 + (ra.Step10 * idx) % 10UL; r10 -= (r10 >= 10UL) ? 10UL : 0UL; + bool shouldCheck = r10 != 5UL; + if (shouldCheck) + { + ulong r8 = (ra.Q0M8 + ((ra.Step8 * idx) & 7UL)) & 7UL; + if (r8 != 1UL && r8 != 7UL) + { + shouldCheck = false; + } + else + { + ulong r3 = ra.Q0M3 + (ra.Step3 * (idx % 3UL)); r3 -= (r3 >= 3UL) ? 3UL : 0UL; + if (r3 == 0UL) + { + shouldCheck = false; + } + else + { + ulong r5 = ra.Q0M5 + (ra.Step5 * (idx % 5UL)); if (r5 >= 5UL) r5 -= 5UL; + if (r5 == 0UL) + { + shouldCheck = false; + } + } + } + } + if (!shouldCheck) + { + return; + } + + GpuUInt128 k = kStart + (GpuUInt128)idx; + GpuUInt128 q = twoP.Mul64(k) + GpuUInt128.One; + + // Small-cycles in-kernel early rejection from device table + if (q.High == 0UL && q.Low <= (ulong)(smallCycles.Length - 1)) + { + uint cyc = smallCycles[(int)q.Low]; + if (cyc != 0U && (exponent % (ulong)cyc) == 0UL) + { + return; + } + } + GpuUInt128 phi = q - GpuUInt128.One; + if (phi.High != 0UL) + { + return; + } + + ulong phi64 = phi.Low; + if (GpuUInt128.Pow2Mod(phi64, q) != GpuUInt128.One) + { + return; + } + + GpuUInt128 halfPow = GpuUInt128.Pow2Mod(phi64 >> 1, q) - GpuUInt128.One; + if (GpuUInt128.BinaryGcd(halfPow, q) != GpuUInt128.One) + { + return; + } + + ulong div = phi64.FastDiv64(exponent, divMul); + GpuUInt128 divPow = GpuUInt128.Pow2Mod(div, q) - GpuUInt128.One; + if (GpuUInt128.BinaryGcd(divPow, q) != GpuUInt128.One) + { + return; + } + + Atomic.Or(ref found[0], 1); + } + + private static void Pow2ModOrderKernelScan(Index1D index, ulong exponent, GpuUInt128 twoP, GpuUInt128 kStart, byte lastIsSeven, ulong _, + ResidueAutomatonArgs ra, ArrayView found, ArrayView1D smallCycles) + { + ulong idx = (ulong)index.X; + ulong r10 = ra.Q0M10 + (ra.Step10 * idx) % 10UL; r10 -= (r10 >= 10UL) ? 10UL : 0UL; + bool shouldCheck = r10 != 5UL; + if (shouldCheck) + { + ulong r8 = (ra.Q0M8 + ((ra.Step8 * idx) & 7UL)) & 7UL; + if (r8 != 1UL && r8 != 7UL) + { + shouldCheck = false; + } + else + { + ulong r3 = ra.Q0M3 + (ra.Step3 * (idx % 3UL)); r3 -= (r3 >= 3UL) ? 3UL : 0UL; + if (r3 == 0UL) + { + shouldCheck = false; + } + else + { + ulong r5 = ra.Q0M5 + (ra.Step5 * (idx % 5UL)); if (r5 >= 5UL) r5 -= 5UL; + if (r5 == 0UL) + { + shouldCheck = false; + } + } + } + } + if (!shouldCheck) + { + return; + } + + GpuUInt128 k = kStart + (GpuUInt128)idx; + GpuUInt128 q = twoP.Mul64(k) + GpuUInt128.One; + // Small-cycles in-kernel early rejection from device table + if (q.High == 0UL && q.Low <= (ulong)(smallCycles.Length - 1)) + { + uint cyc = smallCycles[(int)q.Low]; + if (cyc != 0U && (exponent % (ulong)cyc) == 0UL) + { + return; + } + } + if (GpuUInt128.Pow2Mod(exponent, q) != GpuUInt128.One) + { + return; + } + + Atomic.Or(ref found[0], 1); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + // Enabled this when the struct is converted to a class + // GC.SuppressFinalize(this); + } +} + +public class GpuKernelPool +{ + private static readonly ConcurrentDictionary KernelCache = new(); + + private static KernelContainer GetKernels(Accelerator accelerator) + { + return KernelCache.GetOrAdd(accelerator, _ => new KernelContainer()); + } + + // Ensures the small cycles table is uploaded to the device for the given accelerator. + // Returns the ArrayView to pass into kernels (when kernels are extended to accept it). + public static ArrayView1D EnsureSmallCyclesOnDevice(Accelerator accelerator) + { + var kernels = GetKernels(accelerator); + if (kernels.SmallCycles is { } buffer) + { + return buffer.View; + } + + // Ensure single upload per accelerator even if multiple threads race here. + lock (kernels) + { + if (kernels.SmallCycles is { } existing) + { + return existing.View; + } + + var host = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot(); + var device = accelerator.Allocate1D(host.Length); + device.View.CopyFromCPU(host); + kernels.SmallCycles = device; + return device.View; + } + } + + public static GpuKernelLease GetKernel(bool useGpuOrder) + { + var limiter = GpuPrimeWorkLimiter.Acquire(); + var gpu = RentPreferred(preferCpu: !useGpuOrder); + var accelerator = gpu.Accelerator; + var kernels = GetKernels(accelerator); + return new(limiter, gpu, kernels); + } + + ///

+ /// Runs a GPU action with an acquired accelerator and stream. + /// + /// Action to run with (Accelerator, Stream). + public static void Run(Action action) + { + using var lease = GetKernel(useGpuOrder: true); + var accelerator = lease.Accelerator; + using var stream = accelerator.CreateStream(); + action(accelerator, stream); + } +} diff --git a/PerfectNumbers.Core/Gpu/GpuPrimeWorkLimiter.cs b/PerfectNumbers.Core/Gpu/GpuPrimeWorkLimiter.cs new file mode 100644 index 0000000..8efd707 --- /dev/null +++ b/PerfectNumbers.Core/Gpu/GpuPrimeWorkLimiter.cs @@ -0,0 +1,57 @@ +namespace PerfectNumbers.Core.Gpu; + +public static class GpuPrimeWorkLimiter +{ + private static SemaphoreSlim _semaphore = new(1, int.MaxValue); + private static readonly object _lock = new(); + private static int _currentLimit = 1; + + public static IDisposable Acquire() + { + var sem = _semaphore; + sem.Wait(); + return new Releaser(sem); + } + + public static void SetLimit(int value) + { + if (value < 1) + { + value = 1; + } + + lock (_lock) + { + if (value == _currentLimit) + { + return; + } + + _semaphore = new SemaphoreSlim(value, value); + _currentLimit = value; + } + } + + private sealed class Releaser : IDisposable + { + private readonly SemaphoreSlim _sem; + private bool _disposed; + + public Releaser(SemaphoreSlim sem) + { + _sem = sem; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _sem.Release(); + _disposed = true; + } + } +} + diff --git a/PerfectNumbers.Core/Gpu/GpuUInt128.cs b/PerfectNumbers.Core/Gpu/GpuUInt128.cs new file mode 100644 index 0000000..4528a74 --- /dev/null +++ b/PerfectNumbers.Core/Gpu/GpuUInt128.cs @@ -0,0 +1,991 @@ +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace PerfectNumbers.Core.Gpu; + +public readonly struct GpuUInt128 : IComparable, IEquatable +{ + public readonly ulong High; + + public readonly ulong Low; + + public static readonly GpuUInt128 Zero = new(0UL, 0UL); + public static readonly GpuUInt128 One = new(0UL, 1UL); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128(ulong high, ulong low) + { + High = high; + Low = low; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128(UInt128 value) + { + High = (ulong)(value >> 64); + Low = (ulong)value; + } + + public readonly bool IsZero + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => High == 0UL && Low == 0UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator GpuUInt128(UInt128 value) => new(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator GpuUInt128(ulong value) => new(0UL, value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator UInt128(GpuUInt128 value) => + ((UInt128)value.High << 64) | value.Low; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GpuUInt128 operator +(GpuUInt128 left, GpuUInt128 right) + { + var low = left.Low + right.Low; + var high = left.High + right.High + (low < left.Low ? 1UL : 0UL); + return new(high, low); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GpuUInt128 operator -(GpuUInt128 left, GpuUInt128 right) + { + var borrow = left.Low < right.Low ? 1UL : 0UL; + var low = left.Low - right.Low; + var high = left.High - right.High - borrow; + return new(high, low); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GpuUInt128 operator ^(GpuUInt128 left, GpuUInt128 right) => + new(left.High ^ right.High, left.Low ^ right.Low); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GpuUInt128 operator *(GpuUInt128 left, GpuUInt128 right) + { + Multiply(left, right, out var high, out var low); + return new(high, low); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GpuUInt128 operator <<(GpuUInt128 value, int shift) + { + shift &= 127; + if (shift == 0) + { + return value; + } + + if (shift >= 64) + { + ulong high = value.Low << (shift - 64); + return new(high, 0UL); + } + + ulong highPart = (value.High << shift) | (value.Low >> (64 - shift)); + ulong lowPart = value.Low << shift; + return new(highPart, lowPart); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GpuUInt128 operator >>(GpuUInt128 value, int shift) + { + shift &= 127; + if (shift == 0) + { + return value; + } + + if (shift >= 64) + { + ulong low = value.High >> (shift - 64); + return new(0UL, low); + } + + ulong highPart = value.High >> shift; + ulong lowPart = (value.Low >> shift) | (value.High << (64 - shift)); + return new(highPart, lowPart); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(GpuUInt128 left, GpuUInt128 right) => left.Equals(right); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(GpuUInt128 left, GpuUInt128 right) => !left.Equals(right); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator <(GpuUInt128 left, GpuUInt128 right) => left.CompareTo(right) < 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator >(GpuUInt128 left, GpuUInt128 right) => left.CompareTo(right) > 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator <=(GpuUInt128 left, GpuUInt128 right) => left.CompareTo(right) <= 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator >=(GpuUInt128 left, GpuUInt128 right) => left.CompareTo(right) >= 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GpuUInt128 operator ++(GpuUInt128 value) => value.Add(1UL); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly int CompareTo(GpuUInt128 other) + { + if (High < other.High) + { + return -1; + } + + if (High > other.High) + { + return 1; + } + + if (Low < other.Low) + { + return -1; + } + + if (Low > other.Low) + { + return 1; + } + + return 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool Equals(GpuUInt128 other) => High == other.High && Low == other.Low; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override readonly bool Equals(object? obj) => obj is GpuUInt128 value && Equals(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override readonly int GetHashCode() => HashCode.Combine(High, Low); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 Add(GpuUInt128 other) + { + ulong low = Low + other.Low; + return new(High + other.High + (low < Low ? 1UL : 0UL), low); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 Add(ulong value) + { + var low = Low + value; + return new(High + (low < Low ? 1UL : 0UL), low); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 AddMod(GpuUInt128 value, GpuUInt128 modulus) + { + if (modulus.High == 0UL) + { + return AddMod(value, modulus.Low); + } + + value = Add(value); + if (value.CompareTo(modulus) >= 0) + { + value = value.Sub(modulus); + } + + return value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 AddMod(ulong value, GpuUInt128 modulus) + { + if (modulus.High == 0UL) + { + return AddMod(value, modulus.Low); + } + + var result = Add(value); + if (result.CompareTo(modulus) >= 0) + { + result = result.Sub(modulus); + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 AddMod(GpuUInt128 value, ulong modulus) + { + ulong a = Low % modulus; + ulong b = value.Low % modulus; + ulong sum = a + b; + if (sum >= modulus || sum < a) + { + sum -= modulus; + } + + return new(0UL, sum); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 AddMod(ulong value, ulong modulus) + { + ulong a = Low % modulus; + ulong b = value % modulus; + ulong sum = a + b; + if (sum >= modulus || sum < a) + { + sum -= modulus; + } + + return new(0UL, sum); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 Sub(GpuUInt128 other) + { + ulong borrow = Low < other.Low ? 1UL : 0UL; + return new(High - other.High + borrow, Low - other.Low); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong MulHigh(ulong x, ulong y) + { + ulong xLow = (uint)x; + ulong xHigh = x >> 32; + ulong yLow = (uint)y; + ulong yHigh = y >> 32; + + ulong w1 = xLow * yHigh; + ulong w2 = xHigh * yLow; + ulong carry = + (((xLow * yLow) >> 32) + + (uint)w1 + + (uint)w2) >> 32; + return (xHigh * yHigh) + (w1 >> 32) + (w2 >> 32) + carry; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly GpuUInt128 Mul64(GpuUInt128 other) + { + // Multiply this.Low (assumed 64-bit value) by full 128-bit other + ulong a = Low; + ulong low = a * other.Low; + ulong high = a * other.High + MulHigh(a, other.Low); + return new(high, low); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GpuUInt128 AddMod(GpuUInt128 a, GpuUInt128 b, GpuUInt128 modulus) + { + a = a.Add(b); + if (a >= modulus) + { + a = a.Sub(modulus); + } + + return a; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GpuUInt128 MulMod(GpuUInt128 a, GpuUInt128 b, GpuUInt128 modulus) + { + GpuUInt128 result = new(0UL, 0UL); + while (!b.IsZero) + { + if ((b.Low & 1UL) != 0UL) + { + result = AddMod(result, a, modulus); + } + + // a = (a << 1) % modulus + a <<= 1; + if (a >= modulus) + { + a = a.Sub(modulus); + } + + // b >>= 1 + if (b.High == 0UL) + { + b = new GpuUInt128(0UL, b.Low >> 1); + } + else + { + ulong low = (b.Low >> 1) | (b.High << 63); + ulong high = b.High >> 1; + b = new GpuUInt128(high, low); + } + } + + return result; + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GpuUInt128 Pow2Mod(ulong exponent, GpuUInt128 modulus) + { + if (modulus.IsZero || modulus == One) + { + return new(0UL, 0UL); + } + + GpuUInt128 result = new(0UL, 1UL); + GpuUInt128 baseVal = new(0UL, 2UL); + + ulong e = exponent; + while (e != 0UL) + { + if ((e & 1UL) != 0UL) + { + result = MulMod(result, baseVal, modulus); + } + + baseVal = MulMod(baseVal, baseVal, modulus); + e >>= 1; + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int TrailingZeroCount(GpuUInt128 value) + { + if (value.Low != 0UL) + { + return BitOperations.TrailingZeroCount(value.Low); + } + + return 64 + BitOperations.TrailingZeroCount(value.High); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GpuUInt128 BinaryGcd(GpuUInt128 u, GpuUInt128 v) + { + if (u.IsZero) + { + return v; + } + + if (v.IsZero) + { + return u; + } + + int shift = TrailingZeroCount(new GpuUInt128(u.High | v.High, u.Low | v.Low)); + // remove factors of 2 + int zu = TrailingZeroCount(u); + u >>= zu; + + do + { + int zv = TrailingZeroCount(v); + v >>= zv; + if (u > v) + { + (u, v) = (v, u); + } + + v -= u; + } + while (!v.IsZero); + + return u << shift; + } + + + public GpuUInt128 Sub(ulong value) + { + ulong borrow = Low < value ? 1UL : 0UL; + return new(High - borrow, Low - value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 SubMod(GpuUInt128 value, GpuUInt128 modulus) + { + if (CompareTo(value) >= 0) + { + return Sub(value); + } + else + { + return Add(modulus).Sub(value); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 SubMod(ulong value, GpuUInt128 modulus) + { + ulong low, high; + if (High == 0UL && Low < value) + { + low = Low + modulus.Low; + ulong carry = low < Low ? 1UL : 0UL; + high = High + modulus.High + carry; + } + else + { + high = High; + low = Low; + } + + ulong borrow = low < value ? 1UL : 0UL; + return new (high - borrow, low - value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 Mul(GpuUInt128 other) + { + Multiply(this, other, out var high, out var low); + return new(high, low); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 Mul(ulong value) + { + var (highPart, lowPart) = Mul64(Low, value); + return new(highPart + High * value, lowPart); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 Xor(GpuUInt128 other) => new(High ^ other.High, Low ^ other.Low); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 Xor(ulong value) => new(High, Low ^ value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 ShiftLeft(int shift) => this << shift; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 ShiftRight(int shift) => this >> shift; + + public GpuUInt128 MulMod(GpuUInt128 other, GpuUInt128 modulus) + { + if (modulus.High == 0UL) + { + return MulMod(other, modulus.Low); + } + + MultiplyFull(this, other, out var p3, out var p2, out var p1, out var p0); + + GpuUInt128 remainder = new(0UL, 0UL); + int bit; + for (bit = 63; bit >= 0; bit--) + { + remainder <<= 1; + remainder = new(remainder.High, remainder.Low | ((p3 >> bit) & 1UL)); + if (remainder.CompareTo(modulus) >= 0) + { + remainder = remainder.Sub(modulus); + } + } + + for (bit = 63; bit >= 0; bit--) + { + remainder <<= 1; + remainder = new(remainder.High, remainder.Low | ((p2 >> bit) & 1UL)); + if (remainder.CompareTo(modulus) >= 0) + { + remainder = remainder.Sub(modulus); + } + } + + for (bit = 63; bit >= 0; bit--) + { + remainder <<= 1; + remainder = new(remainder.High, remainder.Low | ((p1 >> bit) & 1UL)); + if (remainder.CompareTo(modulus) >= 0) + { + remainder = remainder.Sub(modulus); + } + } + + for (bit = 63; bit >= 0; bit--) + { + remainder <<= 1; + remainder = new(remainder.High, remainder.Low | ((p0 >> bit) & 1UL)); + if (remainder.CompareTo(modulus) >= 0) + { + remainder = remainder.Sub(modulus); + } + } + + return remainder; + // TODO(MOD-OPT): Replace this bitwise long-division style reduction + // with a faster algorithm suitable for GPU kernels without '%' support. + // Options: + // - Montgomery reduction for 128-bit moduli (R=2^128), requires pre- + // computed modulus-dependent constants (n' and R^2 mod n). This is + // ideal for NTT primes (odd modulus). Cache per-modulus constants. + // - Barrett reduction with a 256/128 quotient approximation using only + // multiplies and shifts. Cache mu = floor(2^k / n) for k=256. + // Implement a fast path for common 64-bit NTT moduli and a separate + // path for 128-bit moduli. This will significantly reduce the cost of + // each butterfly in NTT and LL steps. + // TODO(MOD-OPT): Plumb constants through caches in NttGpuMath.SquareCacheEntry + // (e.g., Montgomery n', R2) and provide device-friendly accessors. + } + + /// + /// Modular multiplication using reduction. This + /// method is intended for validation only and should not be used inside + /// GPU kernels. + /// + internal GpuUInt128 MulModBigInteger(GpuUInt128 other, GpuUInt128 modulus) + { + var left = (BigInteger)(UInt128)this; + var right = (BigInteger)(UInt128)other; + var mod = (BigInteger)(UInt128)modulus; + var reduced = (UInt128)((left * right) % mod); + return new((ulong)(reduced >> 64), (ulong)reduced); + } + + /// + /// Experimental limb-based reduction. The current implementation performs + /// repeated subtractions and becomes extremely slow for large remainders. + /// Kept for future optimization work. + /// + internal GpuUInt128 MulModByLimb(GpuUInt128 other, GpuUInt128 modulus) + { + MultiplyFull(this, other, out var p3, out var p2, out var p1, out var p0); + + GpuUInt128 remainder = new(p3, p2); + while (remainder.CompareTo(modulus) >= 0) + { + remainder = remainder.Sub(modulus); + } + + ulong limb = p1; + for (int i = 0; i < 2; i++) + { + remainder <<= 64; + remainder = new(remainder.High, limb); + while (remainder.CompareTo(modulus) >= 0) + { + remainder = remainder.Sub(modulus); + } + + limb = p0; + } + + return remainder; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 MulMod(ulong value, GpuUInt128 modulus) + { + if (modulus.High == 0UL) + { + return MulMod(value, modulus.Low); + } + + GpuUInt128 other = new(0UL, value); + return MulMod(other, modulus); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 MulMod(GpuUInt128 other, ulong modulus) + { + // Fast-path: use single 64-bit multiply when it cannot overflow + // (enabled by ILGPU support for 64-bit % in kernels). Otherwise fall + // back to the generic branchless double-and-add implementation. + ulong a = Low % modulus; + ulong b = other.Low % modulus; + ulong result; + if (a == 0UL || b == 0UL) + { + result = 0UL; + } + else if (b <= ulong.MaxValue / a) + { + // Safe to multiply without overflow + result = (a * b) % modulus; + } + else + { + result = MulMod64(a, b, modulus); + } + + return new(0UL, result); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 MulMod(ulong value, ulong modulus) + { + // Same fast-path as above for 64-bit operands + ulong a = Low % modulus; + ulong b = value % modulus; + ulong result; + if (a == 0UL || b == 0UL) + { + result = 0UL; + } + else if (b <= ulong.MaxValue / a) + { + result = (a * b) % modulus; + } + else + { + result = MulMod64(a, b, modulus); + } + + return new(0UL, result); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 MulModMontgomery64(GpuUInt128 other, ulong modulus, ulong nPrime, ulong r2) + { + // Montgomery-based 64-bit modular multiplication without division. + // Computes (this * other) % modulus using: + // aR = MontMul(a, R^2), bR = MontMul(b, R^2), cR = MontMul(aR, bR), c = MontMul(cR, 1) + // where R = 2^64, nPrime = -mod^{-1} mod 2^64, r2 = R^2 mod mod. + ulong a = Low % modulus; + ulong b = other.Low % modulus; + if (a == 0UL || b == 0UL) + { + return Zero; + } + + ulong aR = MontMul64(a, r2, modulus, nPrime); + ulong bR = MontMul64(b, r2, modulus, nPrime); + ulong cR = MontMul64(aR, bR, modulus, nPrime); + ulong c = MontMul64(cR, 1UL, modulus, nPrime); + return new(0UL, c); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 MulModMontgomery64(ulong other, ulong modulus, ulong nPrime, ulong r2) + { + ulong a = Low % modulus; + ulong b = other % modulus; + if (a == 0UL || b == 0UL) + { + return Zero; + } + + ulong aR = MontMul64(a, r2, modulus, nPrime); + ulong bR = MontMul64(b, r2, modulus, nPrime); + ulong cR = MontMul64(aR, bR, modulus, nPrime); + ulong c = MontMul64(cR, 1UL, modulus, nPrime); + return new(0UL, c); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 SquareMod(GpuUInt128 modulus) + { + if (modulus.High == 0UL) + { + // Use fast 64-bit path. + return MulMod(Low, modulus.Low); + } + + // Specialized 128-bit squaring with bitwise reduction (faster than generic multiply). + SquareFull(this, out var p3, out var p2, out var p1, out var p0); + + GpuUInt128 remainder = new(0UL, 0UL); + int bit; + for (bit = 63; bit >= 0; bit--) + { + remainder <<= 1; + remainder = new(remainder.High, remainder.Low | ((p3 >> bit) & 1UL)); + if (remainder.CompareTo(modulus) >= 0) + { + remainder = remainder.Sub(modulus); + } + } + + for (bit = 63; bit >= 0; bit--) + { + remainder <<= 1; + remainder = new(remainder.High, remainder.Low | ((p2 >> bit) & 1UL)); + if (remainder.CompareTo(modulus) >= 0) + { + remainder = remainder.Sub(modulus); + } + } + + for (bit = 63; bit >= 0; bit--) + { + remainder <<= 1; + remainder = new(remainder.High, remainder.Low | ((p1 >> bit) & 1UL)); + if (remainder.CompareTo(modulus) >= 0) + { + remainder = remainder.Sub(modulus); + } + } + + for (bit = 63; bit >= 0; bit--) + { + remainder <<= 1; + remainder = new(remainder.High, remainder.Low | ((p0 >> bit) & 1UL)); + if (remainder.CompareTo(modulus) >= 0) + { + remainder = remainder.Sub(modulus); + } + } + + return remainder; + } + + public GpuUInt128 ModPow(GpuUInt128 exponent, GpuUInt128 modulus) + { + GpuUInt128 result = new(0UL, 1UL); + GpuUInt128 baseValue = this; + + while (!exponent.IsZero) + { + if ((exponent.Low & 1UL) != 0UL) + { + result = result.MulMod(baseValue, modulus); + } + + exponent >>= 1; + baseValue = baseValue.MulMod(baseValue, modulus); + } + + return result; + } + + public GpuUInt128 ModPow(ulong exponent, GpuUInt128 modulus) + { + GpuUInt128 result = new(0UL, 1UL); + GpuUInt128 baseValue = this; + + while (exponent != 0UL) + { + if ((exponent & 1UL) != 0UL) + { + result = result.MulMod(baseValue, modulus); + } + + exponent >>= 1; + baseValue = baseValue.MulMod(baseValue, modulus); + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 ModInv(ulong modulus) => ModPow(modulus - 2UL, new GpuUInt128(0UL, modulus)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GpuUInt128 ModInv(GpuUInt128 modulus) + { + if (modulus.High == 0UL) + { + return ModInv(modulus.Low); + } + + var exponent = new GpuUInt128(modulus).Sub(2UL); + return ModPow(exponent, modulus); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override readonly string ToString() => + ((UInt128)this).ToString(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void MultiplyFull(GpuUInt128 left, GpuUInt128 right, out ulong p3, out ulong p2, out ulong p1, out ulong p0) + { + var (h0, l0) = Mul64(left.Low, right.Low); + var (h1, l1) = Mul64(left.Low, right.High); + var (h2, l2) = Mul64(left.High, right.Low); + var (h3, l3) = Mul64(left.High, right.High); + + p0 = l0; + ulong carry = 0UL; + ulong sum = h0; + sum += l1; + if (sum < l1) + { + carry++; + } + + sum += l2; + if (sum < l2) + { + carry++; + } + + p1 = sum; + sum = h1; + sum += h2; + ulong carry2 = sum < h2 ? 1UL : 0UL; + sum += l3; + if (sum < l3) + { + carry2++; + } + + sum += carry; + if (sum < carry) + { + carry2++; + } + + p2 = sum; + p3 = h3 + carry2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Multiply(GpuUInt128 left, GpuUInt128 right, out ulong high, out ulong low) + { + var (h0, l0) = Mul64(left.Low, right.Low); + var (_, l1) = Mul64(left.Low, right.High); + var (_, l2) = Mul64(left.High, right.Low); + + low = l0; + high = h0 + l1 + l2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static (ulong High, ulong Low) Mul64(ulong left, ulong right) + { + ulong a0 = (uint)left; + ulong a1 = left >> 32; + ulong b0 = (uint)right; + ulong b1 = right >> 32; + + ulong lo = a0 * b0; + ulong mid1 = a1 * b0; + ulong mid2 = a0 * b1; + ulong hi = a1 * b1; + + ulong carry = (lo >> 32) + (uint)mid1 + (uint)mid2; + ulong low = (lo & 0xFFFFFFFFUL) | (carry << 32); + hi += (mid1 >> 32) + (mid2 >> 32) + (carry >> 32); + + return (hi, low); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SquareFull(GpuUInt128 value, out ulong p3, out ulong p2, out ulong p1, out ulong p0) + { + // Compute (H*2^64 + L)^2 = H^2*2^128 + 2*H*L*2^64 + L^2 + ulong L = value.Low; + ulong H = value.High; + var (hLL, lLL) = Mul64(L, L); // L^2 -> (hLL,lLL) + var (hHH, lHH) = Mul64(H, H); // H^2 -> (hHH,lHH) + var (hLH, lLH) = Mul64(L, H); // L*H -> (hLH,lLH) + + // double LH: (hLH,lLH) << 1 + ulong dLH_low = lLH << 1; + ulong carry = (lLH >> 63) & 1UL; + ulong dLH_high = (hLH << 1) | carry; + + // Assemble 256-bit result parts + p0 = lLL; + + // p1 = hLL + dLH_low (with carry) + ulong sum1 = hLL + dLH_low; + ulong c1 = sum1 < hLL ? 1UL : 0UL; + p1 = sum1; + + // p2 = lHH + dLH_high + c1 + ulong sum2 = lHH + dLH_high; + ulong c2 = sum2 < lHH ? 1UL : 0UL; + sum2 += c1; + if (sum2 < c1) + { + c2++; + } + p2 = sum2; + + // p3 = hHH + c2 + p3 = hHH + c2; + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong MulMod64(ulong a, ulong b, ulong modulus) + { + // Note: 64-bit % is supported in ILGPU kernels, but the straightforward + // (UInt128)a*b % modulus is not, because 128-bit multiply is not + // supported in device code. We therefore implement a branch-free + // shift-add reduction here based solely on 64-bit ops. + ulong result = 0UL; + ulong x = a % modulus; + ulong y = b; + while (y != 0UL) + { + if ((y & 1UL) != 0UL) + { + result += x; + if (result >= modulus) + { + result -= modulus; + } + } + + x <<= 1; + if (x >= modulus) + { + x -= modulus; + } + + y >>= 1; + } + + return result; + } + + // Montgomery core for 64-bit operands. Returns a*b*R^{-1} mod modulus. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong MontMul64(ulong aR, ulong bR, ulong modulus, ulong nPrime) + { + // t = aR * bR (128-bit) + ulong tLow, tHigh; + Mul64Parts(aR, bR, out tHigh, out tLow); + // m = (tLow * nPrime) mod 2^64 (low 64 bits only) + ulong mLow, mHigh; + Mul64Parts(tLow, nPrime, out mHigh, out mLow); + // u = (t + m * modulus) >> 64 + ulong mmLow, mmHigh; + Mul64Parts(mLow, modulus, out mmHigh, out mmLow); + ulong carry = 0UL; + ulong low = tLow + mmLow; + if (low < tLow) + { + carry = 1UL; + } + + ulong high = tHigh + mmHigh + carry; + ulong u = high; // (t + m*n) >> 64 + if (u >= modulus) + { + u -= modulus; + } + + return u; + } + + // 64x64 -> 128 multiply into (high, low) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Mul64Parts(ulong a, ulong b, out ulong high, out ulong low) + { + ulong a0 = (uint)a; + ulong a1 = a >> 32; + ulong b0 = (uint)b; + ulong b1 = b >> 32; + + ulong lo = a0 * b0; + ulong mid1 = a1 * b0; + ulong mid2 = a0 * b1; + ulong hi = a1 * b1; + + ulong carry = (lo >> 32) + (uint)mid1 + (uint)mid2; + low = (lo & 0xFFFFFFFFUL) | (carry << 32); + hi += (mid1 >> 32) + (mid2 >> 32) + (carry >> 32); + high = hi; + } +} + + // TODO(MOD-OPT): Montgomery/Barrett integration plan + // - Introduce caches of modulus-dependent constants: + // * Montgomery: n' (-(n^{-1}) mod 2^64 or 2^128), R2 = (R^2 mod n) + // * Barrett: mu = floor(2^k / n) for k ∈ {128, 192, 256} + // - Add fast-path for 64-bit NTT primes (modulus.High == 0UL) using pure 64-bit ops. + // - Expose helpers to retrieve/calc constants once per modulus and reuse in kernels. + // - Wire these into MulMod and SquareMod hot paths under feature toggles. + // - Ensure ILGPU compatibility (no BigInteger, no % inside kernels). diff --git a/PerfectNumbers.Core/Gpu/GpuWorkLimiter.cs b/PerfectNumbers.Core/Gpu/GpuWorkLimiter.cs new file mode 100644 index 0000000..78f1229 --- /dev/null +++ b/PerfectNumbers.Core/Gpu/GpuWorkLimiter.cs @@ -0,0 +1,60 @@ +namespace PerfectNumbers.Core.Gpu; + +public static class GpuWorkLimiter +{ + private static SemaphoreSlim _semaphore = new(1, int.MaxValue); + private static readonly object _lock = new(); + private static int _currentLimit = 1; + + public static IDisposable Acquire() + { + var sem = _semaphore; + sem.Wait(); + return new Releaser(sem); + } + + public static void SetLimit(int value) + { + if (value < 1) + { + value = 1; + } + + lock (_lock) + { + if (value == _currentLimit) + { + return; + } + + // Swap to a new semaphore for the new limit. We intentionally + // do not dispose the old semaphore to avoid releasing handles + // that may still be in use by existing Releaser instances. + _semaphore = new SemaphoreSlim(value, value); + _currentLimit = value; + } + } + + private sealed class Releaser : IDisposable + { + private readonly SemaphoreSlim _sem; + private bool _disposed; + + public Releaser(SemaphoreSlim sem) + { + _sem = sem; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _sem.Release(); + _disposed = true; + } + } +} + diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberIncrementalGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberIncrementalGpuTester.cs new file mode 100644 index 0000000..4b876fb --- /dev/null +++ b/PerfectNumbers.Core/Gpu/MersenneNumberIncrementalGpuTester.cs @@ -0,0 +1,77 @@ +using System.Buffers; +using System.Runtime.InteropServices; +using ILGPU.Runtime; + +namespace PerfectNumbers.Core.Gpu; + +public class MersenneNumberIncrementalGpuTester(GpuKernelType kernelType, bool useGpuOrder) +{ + private readonly GpuKernelType _kernelType = kernelType; + private readonly bool _useGpuOrder = useGpuOrder; + + public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime) + { + using var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder); + var accelerator = gpuLease.Accelerator; + int batchSize = GpuConstants.ScanBatchSize; // large batch improves GPU occupancy + UInt128 kStart = 1UL; + ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL; + byte last = lastIsSeven ? (byte)1 : (byte)0; // ILGPU kernels do not support bool parameters + + var kernel = (_kernelType == GpuKernelType.Pow2Mod) + ? gpuLease.Pow2ModKernel + : gpuLease.IncrementalKernel; + + using var orderBuffer = accelerator.Allocate1D(batchSize); + // Avoid giant stack allocations that can trigger StackOverflow when batchSize is large. + // Rent a reusable array from the shared pool instead. + ulong[] orderArray = ArrayPool.Shared.Rent(batchSize); + UInt128 remaining; + int currentSize; + int i; + UInt128 q = UInt128.Zero; + try + { + while (kStart <= maxK && Volatile.Read(ref isPrime)) + { + remaining = maxK - kStart + UInt128.One; + currentSize = remaining > (UInt128)batchSize ? batchSize : (int)remaining; + Span orders = orderArray.AsSpan(0, currentSize); + // Precompute residue automaton bases for this batch + UInt128 q0 = twoP * kStart + UInt128.One; + ulong q0m10 = q0.Mod10(); + ulong q0m8 = q0.Mod8(); + ulong q0m3 = q0.Mod3(); + ulong q0m5 = q0.Mod5(); + ulong step10 = ((exponent % 10UL) << 1) % 10UL; + ulong step8 = ((exponent & 7UL) << 1) & 7UL; + ulong step3 = ((exponent % 3UL) << 1) % 3UL; + ulong step5 = ((exponent % 5UL) << 1) % 5UL; + + var smallCyclesView = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator); + kernel(currentSize, exponent, (GpuUInt128)twoP, (GpuUInt128)kStart, last, divMul, + q0m10, q0m8, q0m3, q0m5, orderBuffer.View, smallCyclesView); + + accelerator.Synchronize(); + orderBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(orders), currentSize); + q = twoP * kStart + UInt128.One; + for (i = 0; i < currentSize && Volatile.Read(ref isPrime); i++, q += twoP) + { + if (orders[i] != 0UL) + { + if (q.IsPrimeCandidate()) + { + Volatile.Write(ref isPrime, false); + } + } + } + + kStart += (UInt128)currentSize; + } + } + finally + { + ArrayPool.Shared.Return(orderArray); + } + } +} diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberLucasLehmerGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberLucasLehmerGpuTester.cs new file mode 100644 index 0000000..4fb6487 --- /dev/null +++ b/PerfectNumbers.Core/Gpu/MersenneNumberLucasLehmerGpuTester.cs @@ -0,0 +1,864 @@ +using System.Collections.Concurrent; +using ILGPU; +using ILGPU.Runtime; +using System.Runtime.CompilerServices; + +namespace PerfectNumbers.Core.Gpu; + +public class MersenneNumberLucasLehmerGpuTester +{ + private readonly ConcurrentDictionary>> KernelCache = new(); + private readonly ConcurrentDictionary>> AddSmallKernelCache = new(); + private readonly ConcurrentDictionary>> SubSmallKernelCache = new(); + private readonly ConcurrentDictionary>> ReduceKernelCache = new(); + private readonly ConcurrentDictionary, ArrayView>> IsZeroKernelCache = new(); + + private Action> GetKernel(Accelerator accelerator) + { + return KernelCache.GetOrAdd(accelerator, accel => accel.LoadAutoGroupedStreamKernel>(Kernel)); + } + + private Action> GetAddSmallKernel(Accelerator accelerator) => + AddSmallKernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel>(AddSmallKernel)); + + private Action> GetSubSmallKernel(Accelerator accelerator) => + SubSmallKernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel>(SubtractSmallKernel)); + + private Action> GetReduceKernel(Accelerator accelerator) => + ReduceKernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel>(ReduceModMersenneKernel)); + + private Action, ArrayView> GetIsZeroKernel(Accelerator accelerator) => + IsZeroKernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel, ArrayView>(IsZeroKernel)); + + // Configurable LL slice size to keep kernels short. Default 32. + public int SliceSize = 32; + + public bool IsMersennePrime(ulong exponent) + { + // Default to global kernel preference for backward compatibility. + bool runOnGpu = !GpuContextPool.ForceCpu; + return IsPrime(exponent, runOnGpu); + } + + public bool IsPrime(ulong exponent, bool runOnGpu) + { + // Early rejections aligned with incremental/order sieves, but safe for small p: + // - If 3 | p and p != 3, then 7 | M_p -> composite. + // - If p ≡ 1 (mod 4) and p shares a factor with (p-1), reject fast. + if ((exponent % 3UL) == 0UL && exponent != 3UL) + { + return false; + } + + if ((exponent & 3UL) == 1UL && exponent.SharesFactorWithExponentMinusOne()) + { + return false; + } + + using var limiter = GpuPrimeWorkLimiter.Acquire(); + if (exponent >= 128UL) + { + if (!TryGetNttParameters(exponent, out var nttMod, out var primitiveRoot)) + { + throw new NotSupportedException("NTT parameters for the given exponent are not available."); + } + + return IsMersennePrimeNtt(exponent, nttMod, primitiveRoot, runOnGpu); + } + + using var gpu = GpuContextPool.RentPreferred(preferCpu: !runOnGpu); + var accelerator = gpu.Accelerator; + var kernel = GetKernel(accelerator); + var modulus = new GpuUInt128(((UInt128)1 << (int)exponent) - 1UL); + using var buffer = accelerator.Allocate1D(1); + kernel(1, exponent, modulus, buffer.View); + accelerator.Synchronize(); + return buffer.GetAsArray1D()[0].IsZero; + } + + private bool IsMersennePrimeNtt(ulong exponent, GpuUInt128 nttMod, GpuUInt128 primitiveRoot, bool runOnGpu) + { + using var gpu = GpuContextPool.RentPreferred(preferCpu: !runOnGpu); + var accelerator = gpu.Accelerator; + var addKernel = GetAddSmallKernel(accelerator); + var subKernel = GetSubSmallKernel(accelerator); + var reduceKernel = GetReduceKernel(accelerator); + var zeroKernel = GetIsZeroKernel(accelerator); + + int limbCount = (int)((exponent + 127UL) / 128UL); + int target = limbCount * 2; + int transformLength = 1; + while (transformLength < target) + { + transformLength <<= 1; + } + using var stateBuffer = accelerator.Allocate1D(transformLength); + stateBuffer.MemSetToZero(); + addKernel(1, 4UL, stateBuffer.View); + + // TODO(LL-SLICE): Slice Lucas–Lehmer iterations into short batches to + // avoid long-running kernels and TDR. Example: process 8–64 iterations + // per slice, with synchronization between slices. Combine this with + // stage-wise NTT to ensure each kernel stays < ~0.5–1.0s. + ulong i = 0UL; + ulong limit = exponent - 2UL; + int slice = Math.Max(1, SliceSize); // process LL iterations in small slices + while (i < limit) + { + int iter = (int)Math.Min((ulong)slice, limit - i); + for (int s = 0; s < iter; s++) + { + // Square in NTT domain with staged kernels (short runtime per kernel). + NttGpuMath.SquareDevice(accelerator, stateBuffer.View, nttMod, primitiveRoot); + subKernel(1, 2UL, stateBuffer.View); + reduceKernel(1, exponent, stateBuffer.View); + } + + // Synchronize between slices to yield to scheduler and avoid TDR. + accelerator.Synchronize(); + i += (ulong)iter; + } + + reduceKernel(1, exponent, stateBuffer.View); + + using var resultBuffer = accelerator.Allocate1D(1); + zeroKernel(1, stateBuffer.View, resultBuffer.View); + accelerator.Synchronize(); + byte[] result = new byte[1]; + resultBuffer.View.CopyToCPU(result); + return result[0] != 0; + } + + private static readonly ConcurrentDictionary NttParameterCache = new() + { + // 2^12 transform length using a 64-bit NTT-friendly prime + [4096] = (new GpuUInt128(0UL, 18446744069414584321UL), new GpuUInt128(0UL, 7UL)), + // 2^22 transform length precomputed for p ~= 138,000,000 + [4194304] = (new GpuUInt128(0UL, 104857601UL), new GpuUInt128(0UL, 39193363UL)) + }; + + private readonly object ParameterFileLock = new(); + private readonly string ParameterFilePath = Path.Combine(AppContext.BaseDirectory, "ntt-params.txt"); + + public void WarmUpNttParameters(ulong exponent) + { + _ = TryGetNttParameters(exponent, out _, out _); + } + + private bool TryGetNttParameters(ulong exponent, out GpuUInt128 modulus, out GpuUInt128 primitiveRoot) + { + int limbCount = (int)((exponent + 127UL) / 128UL); + int length = 1; + int target = limbCount * 2; + while (length < target) + { + length <<= 1; + } + + if (NttParameterCache.TryGetValue(length, out var cached)) + { + modulus = cached.Modulus; + primitiveRoot = cached.PrimitiveRoot; + return true; + } + + if (TryLoadPersistedParameters(length, out modulus, out primitiveRoot)) + { + NttParameterCache[length] = (modulus, primitiveRoot); + return true; + } + + if (GenerateNttParameters(length, out modulus, out primitiveRoot)) + { + NttParameterCache[length] = (modulus, primitiveRoot); + PersistParameters(length, modulus, primitiveRoot); + return true; + } + + modulus = new GpuUInt128(0UL, 18446744069414584321UL); // 2^64 - 2^32 + 1 + primitiveRoot = new GpuUInt128(0UL, 7UL); + return false; + } + + private bool TryLoadPersistedParameters(int length, out GpuUInt128 modulus, out GpuUInt128 primitiveRoot) + { + if (!File.Exists(ParameterFilePath)) + { + modulus = new GpuUInt128(0UL, 0UL); + primitiveRoot = new GpuUInt128(0UL, 0UL); + return false; + } + + foreach (var line in File.ReadLines(ParameterFilePath)) + { + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length != 5) + { + continue; + } + + if (!int.TryParse(parts[0], out var storedLength) || storedLength != length) + { + continue; + } + + ulong modHigh = ulong.Parse(parts[1]); + ulong modLow = ulong.Parse(parts[2]); + ulong rootHigh = ulong.Parse(parts[3]); + ulong rootLow = ulong.Parse(parts[4]); + modulus = new GpuUInt128(modHigh, modLow); + primitiveRoot = new GpuUInt128(rootHigh, rootLow); + return true; + } + + modulus = new GpuUInt128(0UL, 0UL); + primitiveRoot = new GpuUInt128(0UL, 0UL); + return false; + } + + private void PersistParameters(int length, GpuUInt128 modulus, GpuUInt128 primitiveRoot) + { + lock (ParameterFileLock) + { + using var writer = new StreamWriter(ParameterFilePath, append: true); + writer.WriteLine($"{length} {modulus.High} {modulus.Low} {primitiveRoot.High} {primitiveRoot.Low}"); + } + } + + private static readonly ulong[] SmallPrimes = GenerateSmallPrimes(1000); + + private static ulong[] GenerateSmallPrimes(int limit) + { + var isComposite = new bool[limit + 1]; + var primes = new List(); + for (int p = 2; p <= limit; p++) + { + if (isComposite[p]) + { + continue; + } + + primes.Add((ulong)p); + long square = (long)p * p; + for (long multiple = square; multiple <= limit; multiple += p) + { + isComposite[(int)multiple] = true; + } + } + + return primes.ToArray(); + } + + private static bool GenerateNttParameters(int length, out GpuUInt128 modulus, out GpuUInt128 primitiveRoot) + { + ulong foundCandidate = 0UL; + ulong foundRoot = 0UL; + bool success = false; + var processorCount = Environment.ProcessorCount; + using var cts = new System.Threading.CancellationTokenSource(); + object sync = new(); + + System.Threading.Tasks.Parallel.For(0, processorCount, (worker, state) => + { + ulong k = (ulong)worker + 1UL; + while (!cts.IsCancellationRequested) + { + ulong candidate; + try + { + candidate = checked((ulong)length * k + 1UL); + } + catch (OverflowException) + { + break; + } + + bool divisible = false; + foreach (ulong p in SmallPrimes) + { + if (candidate % p == 0UL) + { + divisible = true; + break; + } + } + + if (!divisible && IsPrime(candidate)) + { + ulong root = FindPrimitiveRoot(candidate, (ulong)length); + lock (sync) + { + foundCandidate = candidate; + foundRoot = root; + success = true; + cts.Cancel(); + state.Stop(); + } + break; + } + + k += (ulong)processorCount; + } + }); + + if (success) + { + modulus = new GpuUInt128(0UL, foundCandidate); + primitiveRoot = new GpuUInt128(0UL, foundRoot); + return true; + } + + modulus = new GpuUInt128(0UL, 18446744069414584321UL); + primitiveRoot = new GpuUInt128(0UL, 7UL); + return false; + } + + private static ulong FindPrimitiveRoot(ulong modulus, ulong order) + { + ulong phi = modulus - 1UL; + var factors = Factorize(phi); + for (ulong g = 2UL; g < modulus; g++) + { + bool ok = true; + foreach (ulong f in factors) + { + if (ModPow(g, phi / f, modulus) == 1UL) + { + ok = false; + break; + } + } + + if (ok) + { + return ModPow(g, phi / order, modulus); + } + } + + throw new InvalidOperationException("Primitive root not found."); + } + + private static List Factorize(ulong n) + { + var factors = new List(); + for (ulong p = 2UL; p * p <= n; p += p == 2UL ? 1UL : 2UL) + { + if (n % p == 0UL) + { + factors.Add(p); + while (n % p == 0UL) + { + n /= p; + } + } + } + + if (n > 1UL) + { + factors.Add(n); + } + + return factors; + } + + private static bool IsPrime(ulong n) + { + if (n < 2UL) + { + return false; + } + + ulong[] bases = [2UL, 3UL, 5UL, 7UL, 11UL, 13UL]; + ulong d = n - 1UL; + int s = 0; + while ((d & 1UL) == 0UL) + { + d >>= 1; + s++; + } + + foreach (ulong a in bases) + { + if (a >= n) + { + continue; + } + + ulong x = ModPow(a, d, n); + if (x == 1UL || x == n - 1UL) + { + continue; + } + + bool cont = false; + for (int r = 1; r < s; r++) + { + x = MulMod(x, x, n); + if (x == n - 1UL) + { + cont = true; + break; + } + } + + if (!cont) + { + return false; + } + } + + return true; + } + + private static ulong ModPow(ulong value, ulong exponent, ulong modulus) + { + ulong result = 1UL; + while (exponent > 0UL) + { + if ((exponent & 1UL) != 0UL) + { + result = MulMod(result, value, modulus); + } + + value = MulMod(value, value, modulus); + exponent >>= 1; + } + + return result; + } + + private static ulong MulMod(ulong a, ulong b, ulong modulus) + { + return (ulong)(((UInt128)a * b) % modulus); + } + + private static void AddSmallKernel(Index1D index, ulong add, ArrayView value) + { + ulong carry = add; + for (int i = 0; i < value.Length && carry != 0UL; i++) + { + var limb = value[i]; + ulong low = limb.Low + carry; + ulong carryOut = low < carry ? 1UL : 0UL; + ulong high = limb.High + carryOut; + carry = high < limb.High ? 1UL : 0UL; + value[i] = new GpuUInt128(high, low); + } + } + + private static void SubtractSmallKernel(Index1D index, ulong subtract, ArrayView value) + { + ulong borrow = subtract; + for (int i = 0; i < value.Length && borrow != 0UL; i++) + { + var limb = value[i]; + ulong low = limb.Low; + ulong high = limb.High; + if (low >= borrow) + { + low -= borrow; + borrow = 0UL; + } + else + { + low = unchecked(low - borrow); + borrow = 1UL; + if (high != 0UL) + { + high--; + borrow = 0UL; + } + else + { + high = ulong.MaxValue; + } + } + + value[i] = new GpuUInt128(high, low); + } + } + + private static void ReduceModMersenneKernel(Index1D index, ulong exponent, ArrayView value) + { + int limbCount = (int)((exponent + 127UL) / 128UL); + for (int i = limbCount; i < value.Length; i++) + { + var limb = value[i]; + if (limb.IsZero) + { + continue; + } + + int target = i - limbCount; + var sum = value[target]; + var original = sum; + sum = sum.Add(limb); + value[target] = sum; + value[i] = new GpuUInt128(0UL, 0UL); + if (sum.CompareTo(original) < 0) + { + int j = target + 1; + while (j < limbCount) + { + var next = value[j]; + var nextOriginal = next; + next = next.Add(1UL); + value[j] = next; + if (next.CompareTo(nextOriginal) >= 0) + { + break; + } + + j++; + } + } + } + + int topBits = (int)(exponent % 128UL); + if (topBits == 0) + { + topBits = 128; + } + + int topIndex = limbCount - 1; + ulong topLowMask = topBits >= 64 ? ulong.MaxValue : (1UL << topBits) - 1UL; + ulong topHighMask = topBits > 64 ? (1UL << (topBits - 64)) - 1UL : 0UL; + var top = value[topIndex]; + var carryBits = top >> topBits; + top = new(top.High & topHighMask, top.Low & topLowMask); + value[topIndex] = top; + + if (!carryBits.IsZero) + { + int j = 0; + while (j < limbCount && !carryBits.IsZero) + { + var cur = value[j]; + var curOriginal = cur; + cur = cur.Add(carryBits); + value[j] = cur; + carryBits = cur.CompareTo(curOriginal) < 0 ? new GpuUInt128(0UL, 1UL) : new GpuUInt128(0UL, 0UL); + j++; + } + } + + bool geq = true; + for (int i = topIndex; i >= 0; i--) + { + var limb = value[i]; + ulong modHigh = i == topIndex ? topHighMask : ulong.MaxValue; + ulong modLow = i == topIndex ? topLowMask : ulong.MaxValue; + if (limb.High > modHigh || (limb.High == modHigh && limb.Low > modLow)) + { + geq = true; + break; + } + + if (limb.High < modHigh || (limb.High == modHigh && limb.Low < modLow)) + { + geq = false; + break; + } + } + + if (geq) + { + ulong borrow = 0UL; + for (int i = 0; i < limbCount - 1; i++) + { + var limb = value[i]; + ulong newLow = limb.Low - ulong.MaxValue - borrow; + borrow = limb.Low < ulong.MaxValue + borrow ? 1UL : 0UL; + ulong newHigh = limb.High - ulong.MaxValue - borrow; + borrow = limb.High < ulong.MaxValue + borrow ? 1UL : 0UL; + value[i] = new GpuUInt128(newHigh, newLow); + } + + var topLimb = value[topIndex]; + ulong newTopLow = topLimb.Low - topLowMask - borrow; + borrow = topLimb.Low < topLowMask + borrow ? 1UL : 0UL; + ulong newTopHigh = topLimb.High - topHighMask - borrow; + value[topIndex] = new GpuUInt128(newTopHigh, newTopLow); + } + + for (int i = limbCount; i < value.Length; i++) + { + value[i] = new GpuUInt128(0UL, 0UL); + } + } + + private static void IsZeroKernel(Index1D index, ArrayView value, ArrayView result) + { + byte isZero = 1; + for (int i = 0; i < value.Length; i++) + { + var limb = value[i]; + if (limb.High != 0UL || limb.Low != 0UL) + { + isZero = 0; + break; + } + } + + result[0] = isZero; + } + + private static void ReduceModMersenne(Span value, ulong exponent) + { + int limbCount = (int)((exponent + 127UL) / 128UL); + for (int i = limbCount; i < value.Length; i++) + { + var limb = value[i]; + if (limb.IsZero) + { + continue; + } + + int target = i - limbCount; + UInt128 original = value[target]; + UInt128 sum = original + (UInt128)limb; + bool carry = sum < original; + value[target] = new GpuUInt128(sum); + value[i] = new GpuUInt128(0UL, 0UL); + if (carry) + { + int j = target + 1; + while (j < limbCount) + { + UInt128 next = value[j]; + UInt128 nextSum = next + 1UL; + value[j] = new GpuUInt128(nextSum); + if (nextSum != 0UL) + { + break; + } + + j++; + } + } + } + + int topBits = (int)(exponent % 128UL); + if (topBits == 0) + { + topBits = 128; + } + + int topIndex = limbCount - 1; + ulong topLowMask = topBits >= 64 ? ulong.MaxValue : (1UL << topBits) - 1UL; + ulong topHighMask = topBits > 64 ? (1UL << (topBits - 64)) - 1UL : 0UL; + UInt128 mask = ((UInt128)topHighMask << 64) | topLowMask; + UInt128 top = value[topIndex]; + UInt128 carryBits = top >> topBits; + top &= mask; + value[topIndex] = new GpuUInt128(top); + if (carryBits != 0UL) + { + int j = 0; + UInt128 carry = carryBits; + while (j < limbCount && carry != 0UL) + { + UInt128 cur = value[j]; + UInt128 sum = cur + carry; + bool overflow = sum < cur; + value[j] = new GpuUInt128(sum); + carry = overflow ? 1UL : 0UL; + j++; + } + } + + bool geq = true; + for (int i = topIndex; i >= 0; i--) + { + UInt128 limb = value[i]; + UInt128 modLimb = i == topIndex ? mask : UInt128.MaxValue; + if (limb > modLimb) + { + geq = true; + break; + } + + if (limb < modLimb) + { + geq = false; + break; + } + } + + if (geq) + { + ulong borrow = 0UL; + for (int i = 0; i < limbCount - 1; i++) + { + ulong low = value[i].Low; + ulong high = value[i].High; + ulong newLow = low - ulong.MaxValue - borrow; + borrow = low < ulong.MaxValue + borrow ? 1UL : 0UL; + ulong newHigh = high - ulong.MaxValue - borrow; + borrow = high < ulong.MaxValue + borrow ? 1UL : 0UL; + value[i] = new GpuUInt128(newHigh, newLow); + } + + ulong topLow = value[topIndex].Low; + ulong topHigh = value[topIndex].High; + ulong newTopLow = topLow - topLowMask - borrow; + borrow = topLow < topLowMask + borrow ? 1UL : 0UL; + ulong newTopHigh = topHigh - topHighMask - borrow; + value[topIndex] = new GpuUInt128(newTopHigh, newTopLow); + } + + for (int i = limbCount; i < value.Length; i++) + { + value[i] = new GpuUInt128(0UL, 0UL); + } + } + + private static void Kernel(Index1D index, ulong exponent, GpuUInt128 modulus, ArrayView state) + { + // Lucas–Lehmer iteration in the field GF(2^p-1). + // For p < 128, use a Mersenne-specific squaring + reduction to avoid + // the generic 128-bit long-division-like reduction. + var s = new GpuUInt128(0UL, 4UL); + ulong i = 0UL, limit = exponent - 2UL; + if (exponent < 128UL) + { + for (; i < limit; i++) + { + s = SquareModMersenne128(s, exponent).SubMod(2UL, modulus); + } + } + else + { + for (; i < limit; i++) + { + s = s.SquareMod(modulus).SubMod(2UL, modulus); + } + } + + state[0] = s; + } + + // Squares a 128-bit value and reduces modulo M_p = 2^p - 1 using + // Mersenne folding. Valid for 0 < p < 128 and input s in [0, M_p]. + private static GpuUInt128 SquareModMersenne128(GpuUInt128 s, ulong p) + { + // Full square: (H*2^64 + L)^2 = H^2*2^128 + 2HL*2^64 + L^2 + SquareFull128(s, out var q3, out var q2, out var q1, out var q0); + + // v = low128 + high128 (fold by 128 bits once) + var vHigh = new GpuUInt128(q3, q2); + var vLow = new GpuUInt128(q1, q0); + vLow = vLow.Add(vHigh); + + // Mask to keep only p low bits and fold the carry bits (v >> p) + int topBits = (int)(p % 128UL); + if (topBits == 0) + { + topBits = 128; + } + + ulong maskLow = topBits >= 64 ? ulong.MaxValue : (1UL << topBits) - 1UL; + ulong maskHigh = topBits > 64 ? (1UL << (topBits - 64)) - 1UL : 0UL; + + var carryBits = vLow >> topBits; + vLow = new(vLow.High & maskHigh, vLow.Low & maskLow); + + // Propagate carryBits back into the masked value (single-limb fold) + GpuUInt128 before; + while (!carryBits.IsZero) + { + before = vLow; + vLow = vLow.Add(carryBits); + carryBits = vLow.CompareTo(before) < 0 ? GpuUInt128.One : GpuUInt128.Zero; + } + + // Final correction if v >= modulus (mask) + if (vLow.High > maskHigh || (vLow.High == maskHigh && vLow.Low >= maskLow)) + { + ulong borrow = 0UL; + ulong newLow = vLow.Low - maskLow; + borrow = vLow.Low < maskLow ? 1UL : 0UL; + ulong newHigh = vLow.High - maskHigh - borrow; + vLow = new GpuUInt128(newHigh, newLow); + } + + return vLow; + } + + // Helper: 128-bit square -> 256-bit product as four 64-bit limbs (q3..q0) + private static void SquareFull128(GpuUInt128 value, out ulong q3, out ulong q2, out ulong q1, out ulong q0) + { + ulong L = value.Low; + ulong H = value.High; + Mul64Parts(L, L, out var hLL, out var lLL); // L^2 + Mul64Parts(H, H, out var hHH, out var lHH); // H^2 + Mul64Parts(L, H, out var hLH, out var lLH); // L*H + + // double LH + ulong dLH_low = lLH << 1; + ulong carry = (lLH >> 63) & 1UL; + ulong dLH_high = (hLH << 1) | carry; + + q0 = lLL; + ulong sum1 = hLL + dLH_low; + ulong c1 = sum1 < hLL ? 1UL : 0UL; + q1 = sum1; + + ulong sum2 = lHH + dLH_high; + ulong c2 = sum2 < lHH ? 1UL : 0UL; + sum2 += c1; + if (sum2 < c1) + { + c2++; + } + q2 = sum2; + q3 = hHH + c2; + } + + // 64x64 -> 128 multiply (high,low) + [MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + private static void Mul64Parts(ulong a, ulong b, out ulong high, out ulong low) + { + ulong a0 = (uint)a; + ulong a1 = a >> 32; + ulong b0 = (uint)b; + ulong b1 = b >> 32; + + ulong lo = a0 * b0; + ulong mid1 = a1 * b0; + ulong mid2 = a0 * b1; + ulong hi = a1 * b1; + + ulong carry = (lo >> 32) + (uint)mid1 + (uint)mid2; + low = (lo & 0xFFFFFFFFUL) | (carry << 32); + hi += (mid1 >> 32) + (mid2 >> 32) + (carry >> 32); + high = hi; + } + + [MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + private static ulong ModPow64(ulong value, ulong exponent, ulong modulus) + { + ulong result = 1UL; + ulong baseValue = value % modulus; + ulong exp = exponent; + + while (exp != 0UL) + { + if ((exp & 1UL) != 0UL) + { + result = (ulong)(((UInt128)result * baseValue) % modulus); + } + + baseValue = (ulong)(((UInt128)baseValue * baseValue) % modulus); + exp >>= 1; + } + + return result; + } +} + diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberOrderGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberOrderGpuTester.cs new file mode 100644 index 0000000..39a6aad --- /dev/null +++ b/PerfectNumbers.Core/Gpu/MersenneNumberOrderGpuTester.cs @@ -0,0 +1,63 @@ +using ILGPU.Runtime; + +namespace PerfectNumbers.Core.Gpu; + +public class MersenneNumberOrderGpuTester(GpuKernelType kernelType, bool useGpuOrder) +{ + private readonly GpuKernelType _kernelType = kernelType; + private readonly bool _useGpuOrder = useGpuOrder; + + public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime) + { + using var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder); + var accelerator = gpuLease.Accelerator; + int batchSize = GpuConstants.ScanBatchSize; // large batch improves GPU occupancy and avoids TDR + UInt128 kStart = UInt128.One; + ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL; + byte last = lastIsSeven ? (byte)1 : (byte)0; // ILGPU kernels do not support bool parameters + + var kernel = _kernelType switch + { + GpuKernelType.Pow2Mod => gpuLease.Pow2ModOrderKernel, + _ => gpuLease.IncrementalOrderKernel, + }; + + using var foundBuffer = accelerator.Allocate1D(1); + UInt128 remaining; + int currentSize; + int found = 0; + while (kStart <= maxK && Volatile.Read(ref isPrime)) + { + remaining = maxK - kStart + 1UL; + currentSize = remaining > (UInt128)batchSize ? batchSize : (int)remaining; + foundBuffer.MemSetToZero(); + // Precompute residue automaton bases for this batch + UInt128 q0 = twoP * kStart + 1UL; + ulong q0m10 = q0.Mod10(); + ulong q0m8 = q0.Mod8(); + ulong q0m3 = q0.Mod3(); + ulong q0m5 = q0.Mod5(); + ulong step10 = ((exponent % 10UL) << 1) % 10UL; + ulong step8 = ((exponent & 7UL) << 1) & 7UL; + ulong step3 = ((exponent % 3UL) << 1) % 3UL; + ulong step5 = ((exponent % 5UL) << 1) % 5UL; + var ra = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5); + + + // Ensure device has small cycles table for early rejections in order kernels + var smallCyclesView = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator); + kernel(currentSize, exponent, (GpuUInt128)twoP, (GpuUInt128)kStart, last, divMul, + ra, foundBuffer.View, smallCyclesView); + + accelerator.Synchronize(); + foundBuffer.View.CopyToCPU(ref found, 1); + if (found != 0) + { + Volatile.Write(ref isPrime, false); + break; + } + + kStart += (UInt128)currentSize; + } + } +} diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs new file mode 100644 index 0000000..5679911 --- /dev/null +++ b/PerfectNumbers.Core/Gpu/MersenneNumberResidueGpuTester.cs @@ -0,0 +1,83 @@ +using System.Buffers; +using System.Runtime.InteropServices; +using ILGPU.Runtime; + +namespace PerfectNumbers.Core.Gpu; + +public class MersenneNumberResidueGpuTester(bool useGpuOrder) +{ + private readonly bool _useGpuOrder = useGpuOrder; + // TODO: Integrate MersenneDivisorCycles.Shared to consult cycle lengths for small q (<= 4M) and fast-reject + // candidates without launching heavy kernels. Consider a device-visible constant buffer per-accelerator + // via GpuKernelPool to avoid host round-trips. + + // GPU residue variant: check 2^p % q == 1 for q = 2*p*k + 1. + public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime) + { + using var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder); + var accelerator = gpuLease.Accelerator; + // Ensure device has small cycles table for in-kernel lookup + var smallCyclesView = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator); + int batchSize = GpuConstants.ScanBatchSize; + UInt128 kStart = 1UL; + byte last = lastIsSeven ? (byte)1 : (byte)0; + var kernel = gpuLease.Pow2ModKernel; + + using var orderBuffer = accelerator.Allocate1D(batchSize); + ulong[] orderArray = ArrayPool.Shared.Rent(batchSize); + UInt128 remaining; + int currentSize; + int i; + UInt128 batchSize128 = (UInt128)batchSize, + q = UInt128.Zero; + Span orders = orderArray.AsSpan(0, batchSize); + try + { + while (kStart < maxK && Volatile.Read(ref isPrime)) + { + remaining = maxK - kStart; + if (remaining > batchSize128) + { + currentSize = batchSize; + } + else + { + currentSize = (int)remaining; + orders = orderArray.AsSpan(0, currentSize); + } + + q = twoP * kStart + UInt128.One; + q.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3); + ulong step10 = ((exponent % 10UL) << 1) % 10UL; + ulong step8 = ((exponent & 7UL) << 1) & 7UL; + ulong step3 = ((exponent % 3UL) << 1) % 3UL; + ulong step5 = ((exponent % 5UL) << 1) % 5UL; + + kernel(currentSize, exponent, (GpuUInt128)twoP, (GpuUInt128)kStart, last, 0UL, + q0m10, q0m8, q0m3, q0m5, orderBuffer.View, smallCyclesView); + + accelerator.Synchronize(); + orderBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(orders), currentSize); + if (!Volatile.Read(ref isPrime)) + { + return; + } + + for (i = 0; i < currentSize; i++, q += twoP) + { + if (orders[i] != 0UL && q.IsPrimeCandidate()) + { + Volatile.Write(ref isPrime, false); + break; + } + } + + kStart += batchSize128; + } + } + finally + { + ArrayPool.Shared.Return(orderArray); + } + } +} diff --git a/PerfectNumbers.Core/Gpu/NttGpuMath.cs b/PerfectNumbers.Core/Gpu/NttGpuMath.cs new file mode 100644 index 0000000..4e206cf --- /dev/null +++ b/PerfectNumbers.Core/Gpu/NttGpuMath.cs @@ -0,0 +1,1544 @@ +using System.Buffers; +using System.Collections.Concurrent; +using ILGPU; +using ILGPU.Runtime; +using System.Numerics; +using System.Runtime.CompilerServices; + + +namespace PerfectNumbers.Core.Gpu; + +public enum NttBackend +{ + Reference, + Staged, +} + +public enum ModReductionMode +{ + Auto, + GpuUInt128, + Mont64, + Barrett128, +} + +public static class NttGpuMath +{ + // Global backend setting for GPU transforms. Used by ForwardGpu/InverseGpu. + // TODO(NTT-OPT): Consider passing backend explicitly to avoid global state. + public static NttBackend GpuTransformBackend { get; set; } = NttBackend.Reference; + // Controls modular reduction strategy inside staged NTT paths. + public static ModReductionMode ReductionMode { get; set; } = ModReductionMode.Auto; + private static readonly ConcurrentDictionary, ArrayView, GpuUInt128>> MulKernelCache = new(); + private static readonly ConcurrentDictionary, int, int, int, ArrayView, GpuUInt128>> StageKernelCache = new(); + private static readonly ConcurrentDictionary, GpuUInt128, GpuUInt128>> ScaleKernelCache = new(); + private static readonly ConcurrentDictionary, int, int, int, ArrayView, ulong, ulong>> StageMontKernelCache = new(); + private static readonly ConcurrentDictionary, int, int, int, ArrayView, ulong, ulong, ulong, ulong>> StageBarrett128KernelCache = new(); + private static readonly ConcurrentDictionary, GpuUInt128, ulong, ulong, ulong, ulong>> ScaleBarrett128KernelCache = new(); + private static readonly ConcurrentDictionary, ulong, ulong, ulong, ulong>> SquareBarrett128KernelCache = new(); + private static readonly ConcurrentDictionary, ulong, ulong, ulong>> ToMont64KernelCache = new(); + private static readonly ConcurrentDictionary, ulong, ulong>> FromMont64KernelCache = new(); + private static readonly ConcurrentDictionary, ulong, ulong>> SquareMont64KernelCache = new(); + private static readonly ConcurrentDictionary, ulong, ulong, ulong>> ScaleMont64KernelCache = new(); + + private static readonly ConcurrentDictionary, ArrayView, int, GpuUInt128, GpuUInt128>> ForwardKernelCache = new(); + + private static readonly ConcurrentDictionary, ArrayView, int, GpuUInt128, GpuUInt128, GpuUInt128>> InverseKernelCache = new(); + + private static readonly ConcurrentDictionary, int>> BitReverseKernelCache = new(); + + private sealed class SquareCacheEntry : IDisposable + { + public MemoryBuffer1D Buffer { get; } + public GpuUInt128 Root { get; } + public GpuUInt128 RootInv { get; } + public GpuUInt128 NInv { get; } + public int Bits { get; } + public int[] StageOffsets { get; } + public MemoryBuffer1D Twiddles { get; } + public MemoryBuffer1D TwiddlesInv { get; } + // TODO(MOD-OPT): Precompute and store Montgomery/Barrett constants for modulus here + // (e.g., Montgomery n', R2; Barrett mu) to avoid recomputation. + public bool UseMontgomery64 { get; } + public bool UseBarrett128 { get; } + public ulong ModulusLow { get; } + public ulong ModulusHigh { get; } + public ulong MontNPrime64 { get; } + public ulong MontRMod64 { get; } + public ulong MontR2Mod64 { get; } + public ulong MontNInvR64 { get; } + public MemoryBuffer1D? TwiddlesMont { get; } + public MemoryBuffer1D? TwiddlesInvMont { get; } + public ulong BarrettMuHigh { get; } + public ulong BarrettMuLow { get; } + + public SquareCacheEntry(Accelerator accelerator, int length, GpuUInt128 modulus, GpuUInt128 primitiveRoot) + { + Buffer = accelerator.Allocate1D(length); + Root = new GpuUInt128(primitiveRoot); + RootInv = new GpuUInt128(primitiveRoot); + RootInv = modulus.High == 0UL + ? RootInv.ModInv(modulus.Low) + : RootInv.ModInv(modulus); + + NInv = new GpuUInt128((UInt128)length); + NInv = modulus.High == 0UL + ? NInv.ModInv(modulus.Low) + : NInv.ModInv(modulus); + + // Precompute stage twiddle factors (forward and inverse) and upload to GPU. + Bits = (int)Math.Log2(length); + int twiddleCount = length - 1; + var pool = ArrayPool.Shared; + var forward = pool.Rent(twiddleCount); + var inverse = pool.Rent(twiddleCount); + StageOffsets = new int[Bits]; + int offset = 0; + UInt128 modValue = modulus; + for (int stage = 1; stage <= Bits; stage++) + { + int len = 1 << stage; + int half = len >> 1; + StageOffsets[stage - 1] = offset; + UInt128 exp = (modValue - 1UL) / (ulong)len; + var wLen = new GpuUInt128(Root); + wLen = wLen.ModPow((ulong)exp, modulus); + var w = new GpuUInt128(0UL, 1UL); + for (int j = 0; j < half; j++) + { + forward[offset + j] = w; + w = w.MulMod(wLen, modulus); + } + var wLenInv = new GpuUInt128(RootInv); + wLenInv = wLenInv.ModPow((ulong)exp, modulus); + var wi = new GpuUInt128(0UL, 1UL); + for (int j = 0; j < half; j++) + { + inverse[offset + j] = wi; + wi = wi.MulMod(wLenInv, modulus); + } + offset += half; + } + + Twiddles = accelerator.Allocate1D(twiddleCount); + Twiddles.View.CopyFromCPU(ref forward[0], twiddleCount); + TwiddlesInv = accelerator.Allocate1D(twiddleCount); + TwiddlesInv.View.CopyFromCPU(ref inverse[0], twiddleCount); + + // Prepare Montgomery constants for 64-bit moduli and twiddles in Montgomery domain. + if (modulus.High == 0UL && (ReductionMode == ModReductionMode.Auto || ReductionMode == ModReductionMode.Mont64)) + { + UseMontgomery64 = true; + ModulusLow = modulus.Low; + MontNPrime64 = ComputeMontgomeryNPrime64(ModulusLow); + var mBI = new BigInteger(ModulusLow); + MontRMod64 = (ulong)((BigInteger.One << 64) % mBI); + MontR2Mod64 = (ulong)((BigInteger.One << 128) % mBI); + // nInvR = (nInv * R) mod m where nInv is scalar n^{-1} mod m + ulong nInv64 = NInv.Low % ModulusLow; + MontNInvR64 = (ulong)((((BigInteger)nInv64) * MontRMod64) % mBI); + + var forwardMont = pool.Rent(twiddleCount); + var inverseMont = pool.Rent(twiddleCount); + for (int i = 0; i < twiddleCount; i++) + { + ulong w = forward[i].Low % ModulusLow; + ulong wi = inverse[i].Low % ModulusLow; + forwardMont[i] = new GpuUInt128(0UL, (ulong)((((BigInteger)w) * MontRMod64) % mBI)); + inverseMont[i] = new GpuUInt128(0UL, (ulong)((((BigInteger)wi) * MontRMod64) % mBI)); + } + + TwiddlesMont = accelerator.Allocate1D(twiddleCount); + TwiddlesMont.View.CopyFromCPU(ref forwardMont[0], twiddleCount); + TwiddlesInvMont = accelerator.Allocate1D(twiddleCount); + TwiddlesInvMont.View.CopyFromCPU(ref inverseMont[0], twiddleCount); + + pool.Return(forwardMont, clearArray: true); + pool.Return(inverseMont, clearArray: true); + } + else if (modulus.High != 0UL && (ReductionMode == ModReductionMode.Auto || ReductionMode == ModReductionMode.Barrett128)) + { + // Enable Barrett for 128-bit moduli + UseBarrett128 = true; + ModulusHigh = modulus.High; + ModulusLow = modulus.Low; + // mu = floor(2^256 / n) + var nBI = ((BigInteger)modulus.High << 64) | modulus.Low; + var muBI = (BigInteger.One << 256) / nBI; + BarrettMuHigh = (ulong)(muBI >> 64); + BarrettMuLow = (ulong)muBI; + } + else + { + // Fallback to generic GpuUInt128 path in staged butterflies + UseMontgomery64 = false; + UseBarrett128 = false; + ModulusHigh = modulus.High; + ModulusLow = modulus.Low; + } + + pool.Return(forward, clearArray: true); + pool.Return(inverse, clearArray: true); + } + + public void Dispose() + { + Buffer.Dispose(); + Twiddles.Dispose(); + TwiddlesInv.Dispose(); + TwiddlesMont?.Dispose(); + TwiddlesInvMont?.Dispose(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong ComputeMontgomeryNPrime64(ulong modulus) + { + // Compute inv = modulus^{-1} mod 2^64 via Newton–Raphson, then return -inv mod 2^64. + // Works because modulus is odd for NTT-friendly primes. + ulong inv = 1UL; + for (int i = 0; i < 6; i++) + { + // inv = inv * (2 - m * inv) mod 2^64 + ulong tHigh, tLow; + Mul64Parts(modulus, inv, out tHigh, out tLow); + ulong twoMinus = unchecked(2UL - tLow); + ulong h2, low2; + Mul64Parts(inv, twoMinus, out h2, out low2); + inv = low2; + } + + return 0UL - inv; + } + + private readonly record struct SquareCacheKey(int Length, GpuUInt128 Modulus, GpuUInt128 PrimitiveRoot); + + private static readonly ConcurrentDictionary> SquareCache = new(); + + // TODO(NTT-OPT): Add a TwiddleCache for stage-wise Cooley–Tukey NTT. + // Design: + // - Keyed by (Length, Modulus, PrimitiveRoot) per Accelerator. + // - Store one GPU buffer with all stage twiddles, or per-stage slices. + // - Expose accessors to retrieve per-stage views used by butterfly kernels. + + private static SquareCacheEntry GetSquareCache(Accelerator accelerator, int length, GpuUInt128 modulus, GpuUInt128 primitiveRoot) + { + var perAccel = SquareCache.GetOrAdd(accelerator, _ => new ConcurrentDictionary()); + var key = new SquareCacheKey(length, modulus, primitiveRoot); + return perAccel.GetOrAdd(key, _ => new SquareCacheEntry(accelerator, length, modulus, primitiveRoot)); + } + + private static readonly ulong[] SmallPrimes = GenerateSmallPrimes(1000); + + public static bool TryGenerateParameters(int length, out GpuUInt128 modulus, out GpuUInt128 primitiveRoot) + { + GpuUInt128 foundMod = default; + GpuUInt128 foundRoot = default; + using CancellationTokenSource cts = new(); + Parallel.For(1, int.MaxValue, new ParallelOptions { CancellationToken = cts.Token }, (i, state) => + { + ulong candidate; + try + { + candidate = checked((ulong)length * (ulong)i + 1UL); + } + catch (OverflowException) + { + state.Stop(); + cts.Cancel(); + return; + } + + bool divisible = false; + foreach (ulong p in SmallPrimes) + { + if (candidate % p == 0UL) + { + divisible = true; + break; + } + } + + if (divisible || !IsPrime(candidate)) + { + return; + } + + ulong root = FindPrimitiveRoot(candidate, (ulong)length); + foundMod = new GpuUInt128(0UL, candidate); + foundRoot = new GpuUInt128(0UL, root); + state.Stop(); + cts.Cancel(); + }); + + if (foundMod.High != 0UL || foundMod.Low != 0UL) + { + modulus = foundMod; + primitiveRoot = foundRoot; + return true; + } + + if (TryFindLargeModulus(length, SmallPrimes, out modulus, out primitiveRoot)) + { + return true; + } + + modulus = new GpuUInt128(0UL, 18446744069414584321UL); + primitiveRoot = new GpuUInt128(0UL, 7UL); + // Moduli larger than 128 bits are not supported by the current implementation. + return false; + } + + public static void ClearCaches(Accelerator? accelerator = null) + { + if (accelerator is null) + { + foreach (var perAccel in SquareCache.Values) + { + foreach (var entry in perAccel.Values) + { + entry.Dispose(); + } + } + + SquareCache.Clear(); + MulKernelCache.Clear(); + StageKernelCache.Clear(); + ScaleKernelCache.Clear(); + StageMontKernelCache.Clear(); + StageBarrett128KernelCache.Clear(); + ToMont64KernelCache.Clear(); + FromMont64KernelCache.Clear(); + SquareMont64KernelCache.Clear(); + ScaleMont64KernelCache.Clear(); + ForwardKernelCache.Clear(); + InverseKernelCache.Clear(); + // TODO(NTT-OPT): Clear TwiddleCache once introduced. + return; + } + + if (SquareCache.TryRemove(accelerator, out var cache)) + { + foreach (var entry in cache.Values) + { + entry.Dispose(); + } + } + + MulKernelCache.TryRemove(accelerator, out _); + StageKernelCache.TryRemove(accelerator, out _); + ScaleKernelCache.TryRemove(accelerator, out _); + StageMontKernelCache.TryRemove(accelerator, out _); + StageBarrett128KernelCache.TryRemove(accelerator, out _); + ToMont64KernelCache.TryRemove(accelerator, out _); + FromMont64KernelCache.TryRemove(accelerator, out _); + SquareMont64KernelCache.TryRemove(accelerator, out _); + ScaleMont64KernelCache.TryRemove(accelerator, out _); + ForwardKernelCache.TryRemove(accelerator, out _); + InverseKernelCache.TryRemove(accelerator, out _); + // TODO(NTT-OPT): Remove per-accelerator twiddle buffers once added. + } + + private static Action, int> GetBitReverseKernel(Accelerator accelerator) => + BitReverseKernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel, int>(BitReverseKernel)); + + private static Action, ArrayView, GpuUInt128> GetMulKernel(Accelerator accelerator) => + MulKernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel, ArrayView, GpuUInt128>(MulKernel)); + + private static Action, int, int, int, ArrayView, GpuUInt128> GetStageKernel(Accelerator accelerator) => + StageKernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel, int, int, int, ArrayView, GpuUInt128>(StageKernel)); + + private static Action, GpuUInt128, GpuUInt128> GetScaleKernel(Accelerator accelerator) => + ScaleKernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel, GpuUInt128, GpuUInt128>(ScaleKernel)); + + private static Action, int, int, int, ArrayView, ulong, ulong> GetStageMontKernel(Accelerator accelerator) => + StageMontKernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel, int, int, int, ArrayView, ulong, ulong>(StageMontKernel)); + + private static Action, int, int, int, ArrayView, ulong, ulong, ulong, ulong> GetStageBarrett128Kernel(Accelerator accelerator) => + StageBarrett128KernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel, int, int, int, ArrayView, ulong, ulong, ulong, ulong>(StageBarrett128Kernel)); + + private static Action, GpuUInt128, ulong, ulong, ulong, ulong> GetScaleBarrett128Kernel(Accelerator accelerator) => + ScaleBarrett128KernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel, GpuUInt128, ulong, ulong, ulong, ulong>(ScaleBarrett128Kernel)); + + private static Action, ulong, ulong, ulong, ulong> GetSquareBarrett128Kernel(Accelerator accelerator) => + SquareBarrett128KernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel, ulong, ulong, ulong, ulong>(SquareBarrett128Kernel)); + + private static Action, ulong, ulong, ulong> GetToMont64Kernel(Accelerator accelerator) => + ToMont64KernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel, ulong, ulong, ulong>(ToMont64Kernel)); + + private static Action, ulong, ulong> GetFromMont64Kernel(Accelerator accelerator) => + FromMont64KernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel, ulong, ulong>(FromMont64Kernel)); + + private static Action, ulong, ulong> GetSquareMont64Kernel(Accelerator accelerator) => + SquareMont64KernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel, ulong, ulong>(SquareMont64Kernel)); + + private static Action, ulong, ulong, ulong> GetScaleMont64Kernel(Accelerator accelerator) => + ScaleMont64KernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel, ulong, ulong, ulong>(ScaleMont64Kernel)); + + private static Action, ArrayView, int, GpuUInt128, GpuUInt128> GetForwardKernel(Accelerator accelerator) => + ForwardKernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel, ArrayView, int, GpuUInt128, GpuUInt128>(ForwardKernel)); + + // TODO(NTT-OPT): Introduce stage-wise Cooley–Tukey kernels and a + // per-(accelerator,length,modulus,root) twiddle cache. The plan: + // - Precompute twiddle factors for all stages once and cache on GPU. + // - Replace the current O(n^2) ForwardKernel with O(n log n) butterflies. + // - Provide accessors like GetStageKernel(...) and GetTwiddleBuffer(...). + + private static Action, ArrayView, int, GpuUInt128, GpuUInt128, GpuUInt128> GetInverseKernel(Accelerator accelerator) => + InverseKernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel, ArrayView, int, GpuUInt128, GpuUInt128, GpuUInt128>(InverseKernel)); + + // TODO(NTT-OPT): Mirror the forward stage-wise design for the inverse + // transform. Precompute inverse twiddles and a normalization factor + // (n^-1 mod m). Launch one short kernel per stage to avoid TDR. + + private static void MulKernel(Index1D index, ArrayView a, ArrayView b, GpuUInt128 modulus) + { + a[index] = a[index].MulMod(b[index], modulus); + } + + private static void StageKernel(Index1D index, ArrayView data, int len, int half, int stageOffset, ArrayView twiddles, GpuUInt128 modulus) + { + // TODO(NTT-OPT): Consider shared-memory tiling to improve locality: + // load a block of size `len` into shared memory, do butterflies, write back. + // Requires explicit grouped kernels and chosen group size. + int t = index.X; + int j = t % half; + int block = t / half; + int k = block * len; + int i1 = k + j; + int i2 = i1 + half; + var u = data[i1]; + var v = data[i2]; + var w = twiddles[stageOffset + j]; + v = v.MulMod(w, modulus); + var sum = new GpuUInt128(u); + sum = sum.AddMod(v, modulus); + u = u.SubMod(v, modulus); + data[i1] = sum; + data[i2] = u; + } + + private static void ScaleKernel(Index1D index, ArrayView data, GpuUInt128 scale, GpuUInt128 modulus) + { + var v = data[index]; + v = v.MulMod(scale, modulus); + data[index] = v; + } + + // Barrett-based scaling: v = v * scale mod n for 128-bit n + private static void ScaleBarrett128Kernel(Index1D index, ArrayView data, GpuUInt128 scale, ulong modHigh, ulong modLow, ulong muHigh, ulong muLow) + { + var v = data[index]; + // 128x128 -> 256 + Mul128Full(v.High, v.Low, scale.High, scale.Low, out var z3, out var z2, out var z1, out var z0); + // q approx + Mul3x2Top(z3, z2, z1, muHigh, muLow, out var qHigh, out var qLow); + // q*n + Mul128Full(qHigh, qLow, modHigh, modLow, out var qq3, out var qq2, out var qq1, out var qq0); + // r = z - q*n + Sub256(z3, z2, z1, z0, qq3, qq2, qq1, qq0, out var r3, out var r2, out var r1, out var r0); + ulong resHigh = r1; + ulong resLow = r0; + if (Geq128(resHigh, resLow, modHigh, modLow)) + { + Sub128(ref resHigh, ref resLow, modHigh, modLow); + if (Geq128(resHigh, resLow, modHigh, modLow)) + { + Sub128(ref resHigh, ref resLow, modHigh, modLow); + } + } + + data[index] = new GpuUInt128(resHigh, resLow); + } + + // Barrett-based squaring: v = v^2 mod n for 128-bit n + private static void SquareBarrett128Kernel(Index1D index, ArrayView data, ulong modHigh, ulong modLow, ulong muHigh, ulong muLow) + { + var v = data[index]; + Mul128Full(v.High, v.Low, v.High, v.Low, out var z3, out var z2, out var z1, out var z0); + Mul3x2Top(z3, z2, z1, muHigh, muLow, out var qHigh, out var qLow); + Mul128Full(qHigh, qLow, modHigh, modLow, out var qq3, out var qq2, out var qq1, out var qq0); + Sub256(z3, z2, z1, z0, qq3, qq2, qq1, qq0, out var r3, out var r2, out var r1, out var r0); + ulong resHigh = r1; + ulong resLow = r0; + if (Geq128(resHigh, resLow, modHigh, modLow)) + { + Sub128(ref resHigh, ref resLow, modHigh, modLow); + if (Geq128(resHigh, resLow, modHigh, modLow)) + { + Sub128(ref resHigh, ref resLow, modHigh, modLow); + } + } + data[index] = new GpuUInt128(resHigh, resLow); + } + + // Barrett reduction based stage for 128-bit moduli (no '%'). + private static void StageBarrett128Kernel(Index1D index, ArrayView data, int len, int half, int stageOffset, ArrayView twiddles, ulong modHigh, ulong modLow, ulong muHigh, ulong muLow) + { + int t = index.X; + int j = t % half; + int block = t / half; + int k = block * len; + int i1 = k + j; + int i2 = i1 + half; + var u = data[i1]; + var v = data[i2]; + var w = twiddles[stageOffset + j]; + + // Compute v * w (128x128) -> 256-bit z3..z0 + Mul128Full(v.High, v.Low, w.High, w.Low, out var z3, out var z2, out var z1, out var z0); + + // t = floor(z / b) where b=2^64 => take (z3,z2,z1) + // Compute q = floor((t * mu) / b^3). Only need top two limbs of 5-limb product. + Mul3x2Top(z3, z2, z1, muHigh, muLow, out var qHigh, out var qLow); + + // q * n (128x128) -> 256-bit qq3..qq0 + Mul128Full(qHigh, qLow, modHigh, modLow, out var qq3, out var qq2, out var qq1, out var qq0); + + // r = z - q*n (256-bit) + Sub256(z3, z2, z1, z0, qq3, qq2, qq1, qq0, out var r3, out var r2, out var r1, out var r0); + + // Reduce r to [0, n) by at most two subtractions + // Discard higher limbs (should be zero if r < 2n), keep low 128-bit + ulong resHigh = r1; // r1 is limb1 after subtraction (since lower two limbs are r1 (high) and r0 (low)) + ulong resLow = r0; + + // while (res >= n) res -= n; (at most twice) + if (Geq128(resHigh, resLow, modHigh, modLow)) + { + Sub128(ref resHigh, ref resLow, modHigh, modLow); + if (Geq128(resHigh, resLow, modHigh, modLow)) + { + Sub128(ref resHigh, ref resLow, modHigh, modLow); + } + } + + var sum = new GpuUInt128(u); + var mulred = new GpuUInt128(resHigh, resLow); + sum = sum.AddMod(mulred, new GpuUInt128(modHigh, modLow)); + u = u.SubMod(mulred, new GpuUInt128(modHigh, modLow)); + data[i1] = sum; + data[i2] = u; + } + + // TODO(MOD-OPT): Stage kernel using 64-bit Montgomery multiplication. Requires data and twiddles in Montgomery domain. + private static void StageMontKernel(Index1D index, ArrayView data, int len, int half, int stageOffset, ArrayView twiddlesMont, ulong modulus, ulong nPrime) + { + int t = index.X; + int j = t % half; + int block = t / half; + int k = block * len; + int i1 = k + j; + int i2 = i1 + half; + ulong u = data[i1].Low; + ulong v = data[i2].Low; + ulong wR = twiddlesMont[stageOffset + j].Low; + v = MontMul64(v, wR, modulus, nPrime); + ulong sum = u + v; + if (sum >= modulus) + { + sum -= modulus; + } + ulong diff = u >= v ? u - v : unchecked(u + modulus - v); + data[i1] = new GpuUInt128(0UL, sum); + data[i2] = new GpuUInt128(0UL, diff); + } + + // Computes Montgomery multiplication for 64-bit operands in Montgomery domain. + // Returns a*b*R^{-1} mod modulus, where R=2^64 and nPrime = -modulus^{-1} mod 2^64. + private static ulong MontMul64(ulong aR, ulong bR, ulong modulus, ulong nPrime) + { + // t = aR * bR (128-bit) + ulong tLow, tHigh; + Mul64Parts(aR, bR, out tHigh, out tLow); + // m = (tLow * nPrime) mod 2^64 -> just low 64 bits of the product + ulong mLow, mHigh; + Mul64Parts(tLow, nPrime, out mHigh, out mLow); + // u = (t + m*modulus) >> 64 + ulong mmLow, mmHigh; + Mul64Parts(mLow, modulus, out mmHigh, out mmLow); + // add tLow + mmLow, keep carry + ulong carry = 0UL; + ulong low = tLow + mmLow; + if (low < tLow) + { + carry = 1UL; + } + // high = tHigh + mmHigh + carry + ulong high = tHigh + mmHigh + carry; + ulong u = high; // this is (t + m*n) >> 64 + if (u >= modulus) + { + u -= modulus; + } + return u; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Mul64Parts(ulong a, ulong b, out ulong high, out ulong low) + { + // 64x64 -> 128 using 32-bit limbs + ulong a0 = (uint)a; + ulong a1 = a >> 32; + ulong b0 = (uint)b; + ulong b1 = b >> 32; + + ulong lo = a0 * b0; + ulong mid1 = a1 * b0; + ulong mid2 = a0 * b1; + ulong hi = a1 * b1; + + ulong carry = (lo >> 32) + (uint)mid1 + (uint)mid2; + low = (lo & 0xFFFFFFFFUL) | (carry << 32); + hi += (mid1 >> 32) + (mid2 >> 32) + (carry >> 32); + high = hi; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Mul128Full(ulong aHigh, ulong aLow, ulong bHigh, ulong bLow, out ulong p3, out ulong p2, out ulong p1, out ulong p0) + { + // Full 128x128 -> 256 multiply using 64-bit limbs + Mul64Parts(aLow, bLow, out var h0, out var l0); + Mul64Parts(aLow, bHigh, out var h1, out var l1); + Mul64Parts(aHigh, bLow, out var h2, out var l2); + Mul64Parts(aHigh, bHigh, out var h3, out var l3); + + p0 = l0; + ulong carry = 0UL; + ulong sum = h0; + sum += l1; if (sum < l1) carry++; + sum += l2; if (sum < l2) carry++; + p1 = sum; + + sum = h1; + sum += h2; ulong carry2 = sum < h2 ? 1UL : 0UL; + sum += l3; if (sum < l3) carry2++; + sum += carry; if (sum < carry) carry2++; + p2 = sum; + p3 = h3 + carry2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Mul3x2Top(ulong t2, ulong t1, ulong t0, ulong muHigh, ulong muLow, out ulong top1, out ulong top0) + { + // Compute top two limbs of ( (t2,t1,t0) * (muHigh,muLow) ) after shifting right by 192 bits. + // We build full 5 limbs and return p4 (top1) and p3 (top0). + ulong p0 = 0, p1 = 0, p2 = 0, p3 = 0, p4 = 0; + + void Add128To(ref ulong lo, ref ulong hi, ulong addLo, ulong addHi) + { + ulong s = lo + addLo; + ulong c = s < lo ? 1UL : 0UL; + lo = s; + ulong s2 = hi + addHi + c; + hi = s2; + } + + // t0 * muLow -> contributes to p0,p1 + Mul64Parts(t0, muLow, out var h, out var l); + Add128To(ref p0, ref p1, l, h); + + // t0 * muHigh -> to p1,p2 + Mul64Parts(t0, muHigh, out h, out l); + Add128To(ref p1, ref p2, l, h); + + // t1 * muLow -> to p1,p2 + Mul64Parts(t1, muLow, out h, out l); + Add128To(ref p1, ref p2, l, h); + + // t1 * muHigh -> to p2,p3 + Mul64Parts(t1, muHigh, out h, out l); + Add128To(ref p2, ref p3, l, h); + + // t2 * muLow -> to p2,p3 + Mul64Parts(t2, muLow, out h, out l); + Add128To(ref p2, ref p3, l, h); + + // t2 * muHigh -> to p3,p4 + Mul64Parts(t2, muHigh, out h, out l); + Add128To(ref p3, ref p4, l, h); + + top1 = p4; top0 = p3; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Sub256(ulong a3, ulong a2, ulong a1, ulong a0, ulong b3, ulong b2, ulong b1, ulong b0, out ulong r3, out ulong r2, out ulong r1, out ulong r0) + { + ulong borrow = 0; + r0 = SubWithBorrow(a0, b0, ref borrow); + r1 = SubWithBorrow(a1, b1, ref borrow); + r2 = SubWithBorrow(a2, b2, ref borrow); + r3 = SubWithBorrow(a3, b3, ref borrow); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong SubWithBorrow(ulong a, ulong b, ref ulong borrow) + { + ulong res = a - b - borrow; + borrow = ((a ^ b) & (a ^ res) & 0x8000_0000_0000_0000UL) != 0 ? 1UL : (a < b + borrow ? 1UL : 0UL); + return res; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool Geq128(ulong aHigh, ulong aLow, ulong bHigh, ulong bLow) + { + return aHigh > bHigh || (aHigh == bHigh && aLow >= bLow); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Sub128(ref ulong aHigh, ref ulong aLow, ulong bHigh, ulong bLow) + { + ulong borrow = 0; + ulong low = aLow - bLow; + borrow = aLow < bLow ? 1UL : 0UL; + ulong high = aHigh - bHigh - borrow; + aHigh = high; aLow = low; + } + + private static void ToMont64Kernel(Index1D index, ArrayView data, ulong modulus, ulong nPrime, ulong r2) + { + ulong a = data[index].Low; + ulong v = MontMul64(a, r2, modulus, nPrime); + data[index] = new GpuUInt128(0UL, v); + } + + private static void FromMont64Kernel(Index1D index, ArrayView data, ulong modulus, ulong nPrime) + { + ulong aR = data[index].Low; + ulong v = MontMul64(aR, 1UL, modulus, nPrime); + data[index] = new GpuUInt128(0UL, v); + } + + private static void SquareMont64Kernel(Index1D index, ArrayView data, ulong modulus, ulong nPrime) + { + ulong aR = data[index].Low; + ulong v = MontMul64(aR, aR, modulus, nPrime); + data[index] = new GpuUInt128(0UL, v); + } + + private static void ScaleMont64Kernel(Index1D index, ArrayView data, ulong modulus, ulong nPrime, ulong scaleMont) + { + ulong aR = data[index].Low; + ulong v = MontMul64(aR, scaleMont, modulus, nPrime); + data[index] = new GpuUInt128(0UL, v); + } + + private static void ForwardKernel(Index1D index, ArrayView input, ArrayView output, int length, GpuUInt128 modulus, GpuUInt128 root) + { + // TODO(NTT-OPT): Replace this reference O(n^2) kernel with a stage-wise + // Cooley–Tukey butterfly kernel (O(n log n)). This version uses a per- + // element ModPow in the hot loop which is far too slow and causes long + // single-kernel runtimes. After twiddle precomputation, each butterfly + // should perform: (u, v) -> (u+v*w, u-v*w) with a single MulMod. + var sum = new GpuUInt128(0UL, 0UL); + for (int i = 0; i < length; i++) + { + var term = input[i]; + var power = new GpuUInt128(root); + power = power.ModPow((ulong)index.X * (ulong)i, modulus); + term = term.MulMod(power, modulus); + sum = sum.AddMod(term, modulus); + } + + output[index] = sum; + } + + private static void InverseKernel(Index1D index, ArrayView input, ArrayView output, int length, GpuUInt128 modulus, GpuUInt128 rootInv, GpuUInt128 nInv) + { + // TODO(NTT-OPT): Replace this O(n^2) inverse with stage-wise butterflies + // using precomputed inverse twiddles. Apply final scaling by nInv after + // all stages, preferably in a small dedicated kernel. + var sum = new GpuUInt128(0UL, 0UL); + for (int i = 0; i < length; i++) + { + var term = input[i]; + var power = new GpuUInt128(rootInv); + power = power.ModPow((ulong)index.X * (ulong)i, modulus); + term = term.MulMod(power, modulus); + sum = sum.AddMod(term, modulus); + } + + sum = sum.MulMod(nInv, modulus); + output[index] = sum; + } + + private static void BitReverseKernel(Index1D index, ArrayView data, int bits) + { + int i = index.X; + int j = ReverseBits(i, bits); + if (i < j) + { + var tmp = data[i]; + data[i] = data[j]; + data[j] = tmp; + } + } + + private static int ReverseBits(int value, int bits) + { + int result = 0; + for (int i = 0; i < bits; i++) + { + result = (result << 1) | (value & 1); + value >>= 1; + } + + return result; + } + + public static void BitReverse(Span values) + { + int n = values.Length; + int bits = (int)Math.Log2(n); + for (int i = 0; i < n; i++) + { + int j = ReverseBits(i, bits); + if (i < j) + { + (values[j], values[i]) = (values[i], values[j]); + } + } + } + + public static void BitReverseGpu(Span values) + { + int n = values.Length; + int bits = (int)Math.Log2(n); + using var gpu = GpuContextPool.Rent(); + var accelerator = gpu.Accelerator; + var kernel = GetBitReverseKernel(accelerator); + var pool = ArrayPool.Shared; + var array = pool.Rent(n); + values.CopyTo(array); + using var buffer = accelerator.Allocate1D(n); + buffer.View.CopyFromCPU(ref array[0], n); + kernel(n, buffer.View, bits); + accelerator.Synchronize(); + buffer.View.CopyToCPU(ref array[0], n); + array.AsSpan(0, n).CopyTo(values); + pool.Return(array, clearArray: true); + } + + public static void Forward(Span values, GpuUInt128 modulus, GpuUInt128 primitiveRoot) + { + int n = values.Length; + BitReverse(values); + UInt128 modValue = modulus; + for (int len = 2; len <= n; len <<= 1) + { + UInt128 expBase = (modValue - 1UL) / (ulong)len; + var wLen = new GpuUInt128(primitiveRoot); + wLen = wLen.ModPow((ulong)expBase, modulus); + for (int i = 0; i < n; i += len) + { + var w = new GpuUInt128(0UL, 1UL); + int half = len >> 1; + for (int j = 0; j < half; j++) + { + var u = values[i + j]; + var v = values[i + j + half]; + v = v.MulMod(w, modulus); + var sum = new GpuUInt128(u); + sum = sum.AddMod(v, modulus); + u = u.SubMod(v, modulus); + values[i + j] = sum; + values[i + j + half] = u; + w = w.MulMod(wLen, modulus); + } + } + } + } + + public static void ForwardGpu(Span values, GpuUInt128 modulus, GpuUInt128 primitiveRoot) + { + if (GpuTransformBackend == NttBackend.Reference) + { + ForwardGpuReference(values, modulus, primitiveRoot); + return; + } + + ForwardGpuStaged(values, modulus, primitiveRoot); + } + + private static void ForwardGpuReference(Span values, GpuUInt128 modulus, GpuUInt128 primitiveRoot) + { + int n = (int)values.Length; + using var gpu = GpuContextPool.Rent(); + var accelerator = gpu.Accelerator; + var kernel = GetForwardKernel(accelerator); + var pool = ArrayPool.Shared; + var inputArray = pool.Rent(n); + var outputArray = pool.Rent(n); + values.CopyTo(inputArray); + using var inputBuffer = accelerator.Allocate1D(n); + using var outputBuffer = accelerator.Allocate1D(n); + inputBuffer.View.CopyFromCPU(ref inputArray[0], n); + UInt128 modValue = modulus; + UInt128 expBase = (modValue - 1UL) / (ulong)n; + var root = new GpuUInt128(primitiveRoot); + root = root.ModPow((ulong)expBase, modulus); + kernel(n, inputBuffer.View, outputBuffer.View, n, modulus, root); + accelerator.Synchronize(); + outputBuffer.View.CopyToCPU(ref outputArray[0], n); + outputArray.AsSpan(0, n).CopyTo(values); + pool.Return(inputArray, clearArray: true); + pool.Return(outputArray, clearArray: true); + } + + public static void Inverse(Span values, GpuUInt128 modulus, GpuUInt128 primitiveRoot) + { + int n = values.Length; + BitReverse(values); + UInt128 modValue = modulus; + var root = new GpuUInt128(primitiveRoot); + if (modulus.High == 0UL) + { + root = root.ModInv(modulus.Low); + } + else + { + root = root.ModInv(modulus); + } + + for (int len = 2; len <= n; len <<= 1) + { + UInt128 expBase = (modValue - 1UL) / (ulong)len; + var wLen = new GpuUInt128(root); + wLen = wLen.ModPow((ulong)expBase, modulus); + for (int i = 0; i < n; i += len) + { + var w = new GpuUInt128(0UL, 1UL); + int half = len >> 1; + for (int j = 0; j < half; j++) + { + var u = values[i + j]; + var v = values[i + j + half]; + v = v.MulMod(w, modulus); + var sum = new GpuUInt128(u); + sum = sum.AddMod(v, modulus); + u = u.SubMod(v, modulus); + values[i + j] = sum; + values[i + j + half] = u; + w = w.MulMod(wLen, modulus); + } + } + } + + var nInv = new GpuUInt128((UInt128)n); + if (modulus.High == 0UL) + { + nInv = nInv.ModInv(modulus.Low); + } + else + { + nInv = nInv.ModInv(modulus); + } + + for (int i = 0; i < n; i++) + { + values[i] = values[i].MulMod(nInv, modulus); + } + } + + public static void InverseGpu(Span values, GpuUInt128 modulus, GpuUInt128 primitiveRoot) + { + if (GpuTransformBackend == NttBackend.Reference) + { + InverseGpuReference(values, modulus, primitiveRoot); + return; + } + + InverseGpuStaged(values, modulus, primitiveRoot); + } + + private static void InverseGpuReference(Span values, GpuUInt128 modulus, GpuUInt128 primitiveRoot) + { + int n = values.Length; + using var gpu = GpuContextPool.Rent(); + var accelerator = gpu.Accelerator; + var kernel = GetInverseKernel(accelerator); + var pool = ArrayPool.Shared; + var inputArray = pool.Rent(n); + var outputArray = pool.Rent(n); + values.CopyTo(inputArray); + using var inputBuffer = accelerator.Allocate1D(n); + using var outputBuffer = accelerator.Allocate1D(n); + inputBuffer.View.CopyFromCPU(ref inputArray[0], n); + UInt128 modValue = modulus; + UInt128 expBase = (modValue - 1UL) / (ulong)n; + var root = new GpuUInt128(primitiveRoot); + root = root.ModPow((ulong)expBase, modulus); + var rootInv = new GpuUInt128(root); + if (modulus.High == 0UL) + { + rootInv = rootInv.ModInv(modulus.Low); + } + else + { + rootInv = rootInv.ModInv(modulus); + } + + var nInv = new GpuUInt128((UInt128)n); + if (modulus.High == 0UL) + { + nInv = nInv.ModInv(modulus.Low); + } + else + { + nInv = nInv.ModInv(modulus); + } + kernel(n, inputBuffer.View, outputBuffer.View, n, modulus, rootInv, nInv); + accelerator.Synchronize(); + outputBuffer.View.CopyToCPU(ref outputArray[0], n); + outputArray.AsSpan(0, n).CopyTo(values); + pool.Return(inputArray, clearArray: true); + pool.Return(outputArray, clearArray: true); + } + + private static void ForwardGpuStaged(Span values, GpuUInt128 modulus, GpuUInt128 primitiveRoot) + { + int n = values.Length; + using var gpu = GpuContextPool.Rent(); + var accelerator = gpu.Accelerator; + using var buffer = accelerator.Allocate1D(n); + buffer.View.CopyFromCPU(ref values[0], n); + var cache = GetSquareCache(accelerator, n, modulus, primitiveRoot); + ForwardDevice(accelerator, buffer.View, n, modulus, cache); + buffer.View.CopyToCPU(ref values[0], n); + } + + private static void InverseGpuStaged(Span values, GpuUInt128 modulus, GpuUInt128 primitiveRoot) + { + int n = values.Length; + using var gpu = GpuContextPool.Rent(); + var accelerator = gpu.Accelerator; + using var buffer = accelerator.Allocate1D(n); + buffer.View.CopyFromCPU(ref values[0], n); + var cache = GetSquareCache(accelerator, n, modulus, primitiveRoot); + InverseDevice(accelerator, buffer.View, n, modulus, cache); + buffer.View.CopyToCPU(ref values[0], n); + } + + private static void ForwardDevice(Accelerator accelerator, ArrayView data, int length, GpuUInt128 modulus, SquareCacheEntry cache) + { + var bitKernel = GetBitReverseKernel(accelerator); + bitKernel(length, data, cache.Bits); + accelerator.Synchronize(); + + if (cache.UseMontgomery64) + { + var toMont = GetToMont64Kernel(accelerator); + toMont(length, data, cache.ModulusLow, cache.MontNPrime64, cache.MontR2Mod64); + accelerator.Synchronize(); + var stageKernel = GetStageMontKernel(accelerator); + int butterflies = length >> 1; + for (int s = 0; s < cache.Bits; s++) + { + int len = 1 << (s + 1); + int half = len >> 1; + stageKernel(butterflies, data, len, half, cache.StageOffsets[s], cache.TwiddlesMont!.View, cache.ModulusLow, cache.MontNPrime64); + accelerator.Synchronize(); + } + var fromMont = GetFromMont64Kernel(accelerator); + fromMont(length, data, cache.ModulusLow, cache.MontNPrime64); + } + else if (cache.UseBarrett128) + { + var stageKernel = GetStageBarrett128Kernel(accelerator); + int butterflies = length >> 1; + for (int s = 0; s < cache.Bits; s++) + { + int len = 1 << (s + 1); + int half = len >> 1; + stageKernel(butterflies, data, len, half, cache.StageOffsets[s], cache.Twiddles.View, cache.ModulusHigh, cache.ModulusLow, cache.BarrettMuHigh, cache.BarrettMuLow); + accelerator.Synchronize(); + } + } + else + { + var stageKernel = GetStageKernel(accelerator); + int butterflies = length >> 1; + for (int s = 0; s < cache.Bits; s++) + { + int len = 1 << (s + 1); + int half = len >> 1; + stageKernel(butterflies, data, len, half, cache.StageOffsets[s], cache.Twiddles.View, modulus); + accelerator.Synchronize(); + } + } + + accelerator.Synchronize(); + } + + private static void InverseDevice(Accelerator accelerator, ArrayView data, int length, GpuUInt128 modulus, SquareCacheEntry cache) + { + var bitKernel = GetBitReverseKernel(accelerator); + bitKernel(length, data, cache.Bits); + accelerator.Synchronize(); + + if (cache.UseMontgomery64) + { + var toMont = GetToMont64Kernel(accelerator); + toMont(length, data, cache.ModulusLow, cache.MontNPrime64, cache.MontR2Mod64); + accelerator.Synchronize(); + var stageKernel = GetStageMontKernel(accelerator); + int butterflies = length >> 1; + for (int s = 0; s < cache.Bits; s++) + { + int len = 1 << (s + 1); + int half = len >> 1; + stageKernel(butterflies, data, len, half, cache.StageOffsets[s], cache.TwiddlesInvMont!.View, cache.ModulusLow, cache.MontNPrime64); + accelerator.Synchronize(); + } + var scaleKernel = GetScaleMont64Kernel(accelerator); + scaleKernel(length, data, cache.ModulusLow, cache.MontNPrime64, cache.MontNInvR64); + accelerator.Synchronize(); + var fromMont = GetFromMont64Kernel(accelerator); + fromMont(length, data, cache.ModulusLow, cache.MontNPrime64); + } + else if (cache.UseBarrett128) + { + var stageKernel = GetStageBarrett128Kernel(accelerator); + int butterflies = length >> 1; + for (int s = 0; s < cache.Bits; s++) + { + int len = 1 << (s + 1); + int half = len >> 1; + stageKernel(butterflies, data, len, half, cache.StageOffsets[s], cache.TwiddlesInv.View, cache.ModulusHigh, cache.ModulusLow, cache.BarrettMuHigh, cache.BarrettMuLow); + accelerator.Synchronize(); + } + var scaleKernel = GetScaleBarrett128Kernel(accelerator); + scaleKernel(length, data, cache.NInv, cache.ModulusHigh, cache.ModulusLow, cache.BarrettMuHigh, cache.BarrettMuLow); + accelerator.Synchronize(); + } + else + { + var stageKernel = GetStageKernel(accelerator); + int butterflies = length >> 1; + for (int s = 0; s < cache.Bits; s++) + { + int len = 1 << (s + 1); + int half = len >> 1; + stageKernel(butterflies, data, len, half, cache.StageOffsets[s], cache.TwiddlesInv.View, modulus); + accelerator.Synchronize(); + } + var scaleKernel = GetScaleKernel(accelerator); + scaleKernel(length, data, cache.NInv, modulus); + accelerator.Synchronize(); + } + + accelerator.Synchronize(); + } + + public static void Convolve(Span left, Span right, GpuUInt128 modulus, GpuUInt128 primitiveRoot) + { + int n = 1; + int resultLength = left.Length + right.Length; + while (n < resultLength) + { + n <<= 1; + } + + var pool = ArrayPool.Shared; + var aBuffer = pool.Rent(n); + var bBuffer = pool.Rent(n); + var a = aBuffer.AsSpan(0, n); + var b = bBuffer.AsSpan(0, n); + left.CopyTo(a); + right.CopyTo(b); + for (int i = left.Length; i < n; i++) + { + a[i] = new GpuUInt128(0UL, 0UL); + } + + for (int i = right.Length; i < n; i++) + { + b[i] = new GpuUInt128(0UL, 0UL); + } + + Forward(a, modulus, primitiveRoot); + Forward(b, modulus, primitiveRoot); + PointwiseMultiply(a, b, modulus); + + Inverse(a, modulus, primitiveRoot); + int copyLength = Math.Min(left.Length, resultLength); + a.Slice(0, copyLength).CopyTo(left); + pool.Return(aBuffer, clearArray: true); + pool.Return(bBuffer, clearArray: true); + } + + public static void ConvolveGpu(Span left, Span right, GpuUInt128 modulus, GpuUInt128 primitiveRoot) + { + int n = 1; + int resultLength = left.Length + right.Length; + while (n < resultLength) + { + n <<= 1; + } + + var pool = ArrayPool.Shared; + var aBuffer = pool.Rent(n); + var bBuffer = pool.Rent(n); + var a = aBuffer.AsSpan(0, n); + var b = bBuffer.AsSpan(0, n); + left.CopyTo(a); + right.CopyTo(b); + for (int i = left.Length; i < n; i++) + { + a[i] = new GpuUInt128(0UL, 0UL); + } + + for (int i = right.Length; i < n; i++) + { + b[i] = new GpuUInt128(0UL, 0UL); + } + + ForwardGpu(a, modulus, primitiveRoot); + ForwardGpu(b, modulus, primitiveRoot); + PointwiseMultiply(a, b, modulus); + InverseGpu(a, modulus, primitiveRoot); + + int copyLength = Math.Min(left.Length, resultLength); + a.Slice(0, copyLength).CopyTo(left); + pool.Return(aBuffer, clearArray: true); + pool.Return(bBuffer, clearArray: true); + } + + public static void Square(Span values, GpuUInt128 modulus, GpuUInt128 primitiveRoot) => + Convolve(values, values, modulus, primitiveRoot); + + public static void SquareGpu(Span values, GpuUInt128 modulus, GpuUInt128 primitiveRoot) => + ConvolveGpu(values, values, modulus, primitiveRoot); + + public static void SquareDevice(Accelerator accelerator, ArrayView values, GpuUInt128 modulus, GpuUInt128 primitiveRoot) + { + var cache = GetSquareCache(accelerator, (int)values.Length, modulus, primitiveRoot); + ForwardDevice(accelerator, values, (int)values.Length, modulus, cache); + var mul = GetMulKernel(accelerator); + mul((int)values.Length, values, values, modulus); + InverseDevice(accelerator, values, (int)values.Length, modulus, cache); + } + + public static void PointwiseMultiply(Span left, Span right, GpuUInt128 modulus) + { + using var gpu = GpuContextPool.Rent(); + var accelerator = gpu.Accelerator; + var kernel = GetMulKernel(accelerator); + var pool = ArrayPool.Shared; + var leftArray = pool.Rent(left.Length); + var rightArray = pool.Rent(right.Length); + left.CopyTo(leftArray); + right.CopyTo(rightArray); + using var leftBuffer = accelerator.Allocate1D(left.Length); + using var rightBuffer = accelerator.Allocate1D(right.Length); + leftBuffer.View.CopyFromCPU(ref leftArray[0], left.Length); + rightBuffer.View.CopyFromCPU(ref rightArray[0], right.Length); + kernel(left.Length, leftBuffer.View, rightBuffer.View, modulus); + accelerator.Synchronize(); + leftBuffer.View.CopyToCPU(ref leftArray[0], left.Length); + rightBuffer.View.CopyToCPU(ref rightArray[0], right.Length); + leftArray.AsSpan(0, left.Length).CopyTo(left); + rightArray.AsSpan(0, right.Length).CopyTo(right); + pool.Return(leftArray, clearArray: true); + pool.Return(rightArray, clearArray: true); + } + + private static ulong[] GenerateSmallPrimes(int limit) + { + var isComposite = new bool[limit + 1]; + var primes = new List(); + for (int p = 2; p <= limit; p++) + { + if (isComposite[p]) + { + continue; + } + + primes.Add((ulong)p); + long square = (long)p * p; + for (long multiple = square; multiple <= limit; multiple += p) + { + isComposite[(int)multiple] = true; + } + } + + var result = new ulong[primes.Count]; + primes.CopyTo(result, 0); + return result; + } + + private static bool IsPrime(ulong n) + { + if (n < 2UL) + { + return false; + } + + ulong[] bases = [2UL, 3UL, 5UL, 7UL, 11UL, 13UL]; + ulong d = n - 1UL; + int s = 0; + while ((d & 1UL) == 0UL) + { + d >>= 1; + s++; + } + + foreach (ulong a in bases) + { + if (a >= n) + { + continue; + } + + ulong x = ModPow(a, d, n); + if (x == 1UL || x == n - 1UL) + { + continue; + } + + bool cont = false; + for (int r = 1; r < s; r++) + { + x = MulMod(x, x, n); + if (x == n - 1UL) + { + cont = true; + break; + } + } + + if (!cont) + { + return false; + } + } + + return true; + } + + private static ulong FindPrimitiveRoot(ulong modulus, ulong order) + { + ulong phi = modulus - 1UL; + var factors = Factorize(phi); + for (ulong g = 2UL; g < modulus; g++) + { + bool ok = true; + foreach (ulong f in factors) + { + if (ModPow(g, phi / f, modulus) == 1UL) + { + ok = false; + break; + } + } + + if (ok) + { + return ModPow(g, phi / order, modulus); + } + } + + throw new InvalidOperationException("Primitive root not found."); + } + + private static List Factorize(ulong n) + { + var factors = new List(); + for (ulong p = 2UL; p * p <= n; p += p == 2UL ? 1UL : 2UL) + { + if (n % p == 0UL) + { + factors.Add(p); + while (n % p == 0UL) + { + n /= p; + } + } + } + + if (n > 1UL) + { + factors.Add(n); + } + + return factors; + } + + private static ulong ModPow(ulong value, ulong exponent, ulong modulus) + { + ulong result = 1UL; + while (exponent > 0UL) + { + if ((exponent & 1UL) != 0UL) + { + result = MulMod(result, value, modulus); + } + + value = MulMod(value, value, modulus); + exponent >>= 1; + } + + return result; + } + + private static ulong MulMod(ulong a, ulong b, ulong modulus) + { + return (ulong)(((UInt128)a * b) % modulus); + } + + private static bool TryFindLargeModulus(int length, ulong[] smallPrimes, out GpuUInt128 modulus, out GpuUInt128 primitiveRoot) + { + BigInteger bigLength = new BigInteger(length); + BigInteger k = (BigInteger)(ulong.MaxValue / (ulong)length) + 1; + while (true) + { + BigInteger candidate = bigLength * k + BigInteger.One; + if (candidate.GetBitLength() > 128) + { + break; + } + + bool divisible = false; + foreach (ulong p in smallPrimes) + { + if (candidate % p == 0) + { + divisible = true; + break; + } + } + + if (!divisible && IsPrime(candidate)) + { + BigInteger root = FindPrimitiveRoot(candidate, length); + modulus = ToGpuUInt128(candidate); + primitiveRoot = ToGpuUInt128(root); + return true; + } + + k++; + } + + modulus = new GpuUInt128(0UL, 0UL); + primitiveRoot = new GpuUInt128(0UL, 0UL); + return false; + } + + private static bool IsPrime(BigInteger n) + { + if (n < 2) + { + return false; + } + + ulong[] bases = [2UL, 3UL, 5UL, 7UL, 11UL, 13UL, 17UL]; + BigInteger d = n - 1; + int s = 0; + while ((d & 1) == 0) + { + d >>= 1; + s++; + } + + foreach (ulong a in bases) + { + if (a >= n) + { + continue; + } + + BigInteger x = BigInteger.ModPow(a, d, n); + if (x == 1 || x == n - 1) + { + continue; + } + + bool cont = false; + for (int r = 1; r < s; r++) + { + x = (x * x) % n; + if (x == n - 1) + { + cont = true; + break; + } + } + + if (!cont) + { + return false; + } + } + + return true; + } + + private static BigInteger FindPrimitiveRoot(BigInteger modulus, int order) + { + BigInteger phi = modulus - 1; + var factors = FactorizeBig(phi); + for (BigInteger g = 2; g < modulus; g++) + { + bool ok = true; + foreach (BigInteger f in factors) + { + if (BigInteger.ModPow(g, phi / f, modulus) == 1) + { + ok = false; + break; + } + } + + if (ok) + { + return BigInteger.ModPow(g, phi / order, modulus); + } + } + + throw new InvalidOperationException("Primitive root not found."); + } + + private static List FactorizeBig(BigInteger n) + { + var factors = new List(); + for (BigInteger p = 2; p * p <= n; p += p == 2 ? 1 : 2) + { + if (n % p == 0) + { + factors.Add(p); + while (n % p == 0) + { + n /= p; + } + } + } + + if (n > 1) + { + factors.Add(n); + } + + return factors; + } + + private static GpuUInt128 ToGpuUInt128(BigInteger value) + { + Span bytes = stackalloc byte[16]; + value.TryWriteBytes(bytes, out int written, isUnsigned: true, isBigEndian: true); + ulong high = 0UL; + ulong low = 0UL; + int start = 16 - written; + for (int i = 0; i < 8; i++) + { + high = (high << 8) | bytes[start + i]; + } + + for (int i = 8; i < 16; i++) + { + low = (low << 8) | bytes[start + i]; + } + + return new GpuUInt128(high, low); + } +} + diff --git a/PerfectNumbers.Core/InputParser.cs b/PerfectNumbers.Core/InputParser.cs new file mode 100644 index 0000000..b2cd6f0 --- /dev/null +++ b/PerfectNumbers.Core/InputParser.cs @@ -0,0 +1,26 @@ +using System.Globalization; +using System.Numerics; + +namespace PerfectNumbers.Core; + +public static class InputParser +{ + public static BigInteger ParseBigInteger(string value) + { + if (value.Contains('^')) + { + var parts = value.Split('^'); + BigInteger b = BigInteger.Parse(parts[0], CultureInfo.InvariantCulture); + int e = int.Parse(parts[1], CultureInfo.InvariantCulture); + return BigInteger.Pow(b, e); + } + + return BigInteger.Parse(value, CultureInfo.InvariantCulture); + } + + public static decimal ParseDecimal(string value) + { + return decimal.Parse(value, CultureInfo.InvariantCulture); + } +} + diff --git a/PerfectNumbers.Core/KRangeFinder.cs b/PerfectNumbers.Core/KRangeFinder.cs new file mode 100644 index 0000000..918a4ce --- /dev/null +++ b/PerfectNumbers.Core/KRangeFinder.cs @@ -0,0 +1,79 @@ +using PeterO.Numbers; + +namespace PerfectNumbers.Core; + +public static class KRangeFinder +{ + public static (int? Min, int? Max) FindForPrime(ERational alphaM, EInteger p, int kStart, int kEnd) + { + int? min = null; + int? max = null; + for (int k = kStart; k <= kEnd; k++) + { + ERational aP = AlphaCalculations.ComputeAlphaP(p, k); + ERational prod = aP.Multiply(alphaM); + int cmp = prod.CompareTo(RationalNumbers.Two); + if (cmp == 0) + { + if (min == null || k < min) + { + min = k; + } + if (max == null || k > max) + { + max = k; + } + } + if (cmp < 0) + { + break; + } + } + return (min, max); + } + + public static bool TryFindForPrime(ERational alphaM, EInteger p, out int? min, out int? max, int kStart, int kEnd) + { + (min, max) = FindForPrime(alphaM, p, kStart, kEnd); + return min.HasValue && max.HasValue && min.Value <= max.Value; + } + + public static (int? Min, int? Max) Find(ERational alphaM, EInteger pMin, EInteger pMax, int kStart, int kEnd, PrimeCache primes) + { + int? min = null; + int? max = null; + EInteger[] eulerPrimes = primes.GetEulerPrimes(pMin, pMax); + object sync = new(); + Parallel.For( + 0, + eulerPrimes.Length, + new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, + i => + { + (int? localMin, int? localMax) = FindForPrime(alphaM, eulerPrimes[i], kStart, kEnd); + if (localMin == null) + { + return; + } + lock (sync) + { + if (min == null || localMin < min) + { + min = localMin; + } + if (max == null || localMax > max) + { + max = localMax; + } + } + }); + return (min, max); + } + + public static bool TryFind(ERational alphaM, EInteger pMin, EInteger pMax, out int? min, out int? max, int kStart, int kEnd, PrimeCache primes) + { + (min, max) = Find(alphaM, pMin, pMax, kStart, kEnd, primes); + return min.HasValue && max.HasValue && min.Value <= max.Value; + } +} + diff --git a/PerfectNumbers.Core/MersenneDivisorCycles.cs b/PerfectNumbers.Core/MersenneDivisorCycles.cs new file mode 100644 index 0000000..a97d1ef --- /dev/null +++ b/PerfectNumbers.Core/MersenneDivisorCycles.cs @@ -0,0 +1,381 @@ +using System.Buffers; +using System.Text; +using PerfectNumbers.Core.Gpu; +using ILGPU; +using ILGPU.Runtime; +using System.Runtime.CompilerServices; +using Open.Collections.Synchronized; + +namespace PerfectNumbers.Core; + +public class MersenneDivisorCycles +{ + private List<(ulong divisor, ulong cycleLength)> _table = []; + // Lightweight read-mostly cache for small divisors (<= 4,000,000). 0 => unknown + private uint[]? _smallCycles; + + public static MersenneDivisorCycles Shared { get; } = new MersenneDivisorCycles(); + + public MersenneDivisorCycles() + { + } + + public void LoadFrom(string path) + { + EnsureSmallBuffer(); + List<(ulong divisor, ulong cycle)> cycles = []; + using Stream outputStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize10M, useAsync: false); + foreach (var (d, c) in EnumerateStream(outputStream)) + { + cycles.Add((d, c)); + if (d <= PerfectNumberConstants.MaxQForDivisorCycles) + { + _smallCycles![(int)d] = (uint)Math.Min(uint.MaxValue, c == 0UL ? 1UL : c); + } + } + + cycles.Sort((a, b) => a.divisor < b.divisor ? -1 : (a.divisor > b.divisor ? 1 : 0)); + _table = cycles; + } + + public const int SmallDivisorsMax = PerfectNumberConstants.MaxQForDivisorCycles; + + // Provides a snapshot array of small cycles [0..PerfectNumberConstants.MaxQForDivisorCycles]. Index is the divisor. + // The returned array is a copy safe for use across threads and device uploads. + public uint[] ExportSmallCyclesSnapshot() + { + EnsureSmallBuffer(); + uint[] snapshot = new uint[PerfectNumberConstants.MaxQForDivisorCycles + 1]; + Array.Copy(_smallCycles!, snapshot, snapshot.Length); + return snapshot; + } + + public static IEnumerable<(ulong divisor, ulong cycleLength)> EnumerateStream(Stream compressor) + { + // Binary pairs: (ulong divisor, ulong cycle) + using var reader = new BinaryReader(compressor, Encoding.UTF8, leaveOpen: true); + while (true) + { + ulong d, c; + try + { + d = reader.ReadUInt64(); + c = reader.ReadUInt64(); + } + catch (EndOfStreamException) + { + yield break; + } + + yield return (d, c); + } + } + + public ulong GetCycle(ulong divisor) + { + // Fast-path: in-memory array for small divisors + if (divisor <= (ulong)PerfectNumberConstants.MaxQForDivisorCycles) + { + var arr = _smallCycles; + if (arr is not null) + { + uint cached = arr[(int)divisor]; + if (cached != 0U) + { + return cached; + } + } + } + + // Binary search for divisor in sorted _table + int mid, right, left = 0; + ulong d; + List<(ulong divisor, ulong cycleLength)> cycles = _table; + + right = cycles.Count - 1; + while (left <= right) + { + mid = left + ((right - left) >> 1); + d = cycles[mid].divisor; + + if (d == divisor) + { + return cycles[mid].cycleLength; + } + + if (d < divisor) + { + left = mid + 1; + } + else + { + right = mid - 1; + } + } + + return CalculateCycleLength(divisor); + } + + public static UInt128 GetCycle(UInt128 divisor) + { + // For divisor = 2^k, cycle is 1 + if ((divisor & (divisor - UInt128.One)) == UInt128.Zero) + { + return UInt128.One; + } + + // Otherwise, find order of 2 mod divisor + UInt128 order = UInt128.One, pow = UInt128Numbers.Two; + while (pow != UInt128.One) + { + pow <<= 1; + if (pow >= divisor) + pow -= divisor; + + order++; + } + + return order; + } + + public static (long nextPosition, long completeCount) FindLast(string path) + { + using Stream outputStream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read, BufferSize10M, useAsync: false); + long count = 0; + foreach (var _ in EnumerateStream(outputStream)) + { + count++; + } + return (outputStream.Position, count); + } + + private const int BufferSize10M = 10 * 1024 * 1024; + + public static void Generate(string path, ulong maxDivisor, int threads = 16) + { + // Binary-only generation of (divisor, cycle) pairs. CSV scaffolding removed. + ulong start, d = start = 2; + ulong blockSize = maxDivisor / (ulong)threads + 1; + Task[] tasks = new Task[threads]; + using Stream outputStream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read, BufferSize10M, useAsync: false); + // Start fresh to avoid mixing formats + outputStream.SetLength(0L); + outputStream.Position = 0L; + using var writer = new BinaryWriter(outputStream, Encoding.UTF8, leaveOpen: false); + for (var taskIndex = 0; taskIndex < threads; taskIndex++) + { + Task t = Task.Factory.StartNew((taskIndex) => + { + ulong cycle, d = start + (ulong)(int)taskIndex! * blockSize; + (ulong d, ulong cycle)[] localCycles = ArrayPool<(ulong, ulong)>.Shared.Rent((int)(maxDivisor - d + 1UL)); + int localCycleIndex = 0; + for (; d <= maxDivisor; d++) + { + if ((d & 1UL) == 0UL) + { + // skip even divisors + continue; + } + + cycle = CalculateCycleLength(d); + + localCycles[localCycleIndex++] = (d, cycle); + } + + if (localCycleIndex > 0) + { + lock (writer) + { + writer.BaseStream.Position = writer.BaseStream.Length; + for (int i = 0; i < localCycleIndex; i++) + { + (d, cycle) = localCycles[i]; + if (d % 3UL == 0UL || d % 5UL == 0UL || d % 7UL == 0UL || d % 11UL == 0UL) + continue; + + writer.Write(d); + writer.Write(cycle); + } + + writer.Flush(); + } + } + + ArrayPool<(ulong, ulong)>.Shared.Return(localCycles, clearArray: false); + + }, taskIndex); + + tasks[taskIndex] = t; + } + + Task.WaitAll(tasks); + } + + public static void GenerateGpu(string path, ulong maxDivisor, int batchSize = 1_000_000, long skipCount = 0L, long nextPosition = 0L) + { + // Prepare output file with header + + ulong start = 3UL; + var locker = new object(); + + var pool = ArrayPool.Shared; + ulong batchSizeUL = (ulong)batchSize, d, end; + int count = (int)Math.Min(batchSizeUL, maxDivisor), i, idx; + ulong[] divisors, outCycles, validDivisors; + + divisors = pool.Rent(count); + outCycles = pool.Rent(count); + using var lease = GpuKernelPool.GetKernel(useGpuOrder: true); + var accelerator = lease.Accelerator; + using var stream = accelerator.CreateStream(); + var kernel = accelerator.LoadAutoGroupedStreamKernel< + Index1D, + ArrayView1D, + ArrayView1D>(GpuDivisorCycleKernel); + + + using Stream outputStream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read, BufferSize10M, useAsync: true); + if (nextPosition > 0L) + { + outputStream.Position = nextPosition; + } + else + { + outputStream.SetLength(0L); + outputStream.Position = 0L; + } + + using var writer = new BinaryWriter(outputStream, Encoding.UTF8, leaveOpen: false); + try + { + while (skipCount > 0L && start <= maxDivisor) + { + if ((start & 1UL) == 0UL || start % 3UL == 0UL || start % 5UL == 0UL || start % 7UL == 0UL || start % 11UL == 0UL) + { + start++; + continue; + } + + start++; + skipCount--; + } + + while (start <= maxDivisor) + { + end = Math.Min(start + batchSizeUL - 1UL, maxDivisor); + count = checked((int)(end - start + 1UL)); + + idx = 0; + for (d = start; d <= end; d++) + { + if ((d & 1UL) == 0UL) + continue; + + divisors[idx++] = d; + } + + validDivisors = divisors[..idx]; + + // Use GpuKernelPool to get a kernel and context + // GpuKernelPool.Run((accelerator, stream) => + var bufferDiv = accelerator.Allocate1D(validDivisors); + var bufferCycle = accelerator.Allocate1D(idx); + + kernel( + count, + bufferDiv.View, + bufferCycle.View); + + accelerator.Synchronize(); + + bufferCycle.View.CopyToCPU(ref outCycles[0], idx); + bufferCycle.Dispose(); + bufferDiv.Dispose(); + + // Collect results in order + for (i = 0; i < idx; i++) + { + d = divisors[i]; + + if (d % 3UL == 0UL || d % 5UL == 0UL || d % 7UL == 0UL || d % 11UL == 0UL) + continue; + + writer.Write(d); + writer.Write(outCycles[i]); + } + + writer.Flush(); + Console.WriteLine($"...processed divisor = {d}"); + start = end + 1UL; + } + } + finally + { + pool.Return(divisors, clearArray: false); + pool.Return(outCycles, clearArray: false); + } + } + + const byte ByteZero = 0; + const byte ByteOne = 1; + + // GPU kernel for divisor cycle calculation + static void GpuDivisorCycleKernel( + Index1D index, + ArrayView1D divisors, + ArrayView1D outCycles) + { + int i = index.X; + outCycles[i] = CalculateCycleLengthGpu(divisors[i]); + } + + // GPU-friendly version of cycle length calculation + public static ulong CalculateCycleLengthGpu(ulong divisor) + { + if ((divisor & (divisor - 1UL)) == 0UL) + return 1UL; + + ulong order = 1UL, pow = 2UL; + while (pow != 1UL) + { + pow <<= 1; + if (pow >= divisor) + pow -= divisor; + + order++; + } + + return order; + } + + public static ulong CalculateCycleLength(ulong divisor) + { + // For divisor = 2^k, cycle is 1 + if ((divisor & (divisor - 1UL)) == 0UL) + { + return 1UL; + } + + // Otherwise, find order of 2 mod divisor + ulong order = 1UL, pow = 2UL; + while (pow != 1) + { + pow <<= 1; + if (pow >= divisor) + pow -= divisor; + + order++; + } + + return order; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureSmallBuffer() + { + if (_smallCycles is null) + { + _smallCycles = new uint[PerfectNumberConstants.MaxQForDivisorCycles + 1]; + } + } +} diff --git a/PerfectNumbers.Core/MersenneNumberTester.cs b/PerfectNumbers.Core/MersenneNumberTester.cs new file mode 100644 index 0000000..af4d838 --- /dev/null +++ b/PerfectNumbers.Core/MersenneNumberTester.cs @@ -0,0 +1,312 @@ +using System.Runtime.CompilerServices; +using System.Collections.Concurrent; +using System.Buffers; +using ILGPU; +using ILGPU.Runtime; +using PerfectNumbers.Core.Cpu; +using PerfectNumbers.Core.Gpu; +using UInt128 = System.UInt128; + +namespace PerfectNumbers.Core; + +public enum GpuKernelType +{ + Incremental, + Pow2Mod, +} + +public sealed class MersenneNumberTester( + bool useIncremental = true, + bool useOrderCache = false, + GpuKernelType kernelType = GpuKernelType.Incremental, + bool useModuloWorkaround = false, + bool useOrder = false, + bool useGpuLucas = true, + bool useGpuScan = true, + bool useGpuOrder = true, + bool useResidue = true, + ulong maxK = 5_000_000UL) +{ + private readonly bool _useIncremental = useIncremental; + private readonly bool _useResidue = useResidue; + private readonly ulong _maxK = maxK; + private readonly GpuKernelType _kernelType = kernelType; + private readonly bool _useModuloWorkaround = useModuloWorkaround; + private readonly bool _useGpuLucas = useGpuLucas; + private readonly bool _useGpuScan = useGpuScan; // device for pow2mod/incremental scanning + private readonly bool _useGpuOrder = useGpuOrder; // device for order computations + private static readonly ConcurrentDictionary OrderCache = new(); + private readonly MersenneNumberIncrementalGpuTester _incrementalGpuTester = new(kernelType, useGpuOrder); + private readonly MersenneNumberIncrementalCpuTester _incrementalCpuTester = new(kernelType); + private readonly MersenneNumberOrderGpuTester _orderGpuTester = new(kernelType, useGpuOrder); + private readonly MersenneNumberOrderCpuTester _orderCpuTester = new(kernelType); + private readonly MersenneNumberLucasLehmerGpuTester _lucasLehmerGpuTester = new(); + private readonly MersenneNumberLucasLehmerCpuTester _lucasLehmerCpuTester = new(); + private readonly MersenneNumberResidueGpuTester? _residueGpuTester = useResidue ? new(useGpuOrder) : null; + private readonly MersenneNumberResidueCpuTester? _residueCpuTester = useResidue ? new() : null; + + // TODO: Ensure Program passes useResidue correctly and that residue-vs-incremental-vs-LL selection respects CLI flags. + // Also consider injecting a shared cycles cache (MersenneDivisorCycles.Shared) to both CPU and GPU testers. + + public void WarmUpOrders(ulong exponent, ulong limit = 5_000_000UL) + { + bool cacheEnabled = useOrderCache; + if (!cacheEnabled) + { + return; + } + + UInt128 twoP = (UInt128)exponent << 1; // 2 * p + bool lastIsSeven = (exponent & 3UL) == 3UL; + // CPU path: compute orders directly and cache + if (!_useGpuOrder) + { + UInt128 k = UInt128.One; + UInt128 q; + ulong remainder; + for (; k <= limit; k++) + { + q = twoP * k + UInt128.One; + remainder = q.Mod10(); + bool shouldCheck = lastIsSeven + ? remainder == 7UL || remainder == 9UL || remainder == 3UL + : remainder == 1UL || remainder == 3UL || remainder == 9UL; + + // Early modular filters for odd order and small-prime rejections + if (shouldCheck) + { + ulong mod8 = q.Mod8(); + if (mod8 != 1UL && mod8 != 7UL) + { + continue; + } + else if (q.Mod3() == 0UL || q.Mod5() == 0UL) + { + continue; + } + } + + if (!shouldCheck || !cacheEnabled || (cacheEnabled && OrderCache.ContainsKey(q))) + { + continue; + } + + ulong ord = q.CalculateOrder(); + OrderCache[q] = ord; + } + + return; + } + + UInt128[] qsBuffer = ArrayPool.Shared.Rent((int)limit); + int idx = 0; + UInt128 k2 = 1UL; + UInt128 q2; + ulong remainder2; + for (; k2 <= limit; k2++) + { + q2 = twoP * k2 + 1UL; + remainder2 = q2.Mod10(); + bool shouldCheck2 = lastIsSeven + ? remainder2 == 7UL || remainder2 == 9UL || remainder2 == 3UL + : remainder2 == 1UL || remainder2 == 3UL || remainder2 == 9UL; + + // Early modular filters for odd order and small-prime rejections + if (shouldCheck2) + { + ulong mod8 = q2.Mod8(); + if (mod8 != 1UL && mod8 != 7UL) + { + shouldCheck2 = false; + } + else if (q2.Mod3() == 0UL || q2.Mod5() == 0UL) + { + shouldCheck2 = false; + } + } + + if (!shouldCheck2 || (cacheEnabled && OrderCache.ContainsKey(q2))) + { + continue; + } + + qsBuffer[idx++] = q2; + } + + if (idx == 0) + { + ArrayPool.Shared.Return(qsBuffer); + return; + } + + UInt128[] qs = qsBuffer; + + using var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder); + var accelerator = gpuLease.Accelerator; + var orderKernel = gpuLease.OrderKernel!; + + // Guard long-running kernels by chunking the warm-up across batches. + // Reuse the same ScanBatchSize knob used for scanning. + int batchSize = GpuConstants.ScanBatchSize; + ulong divMul = (ulong)((((UInt128)1 << 64) - 1UL) / exponent) + 1UL; + int offset = 0; + while (offset < idx) + { + int count = Math.Min(batchSize, idx - offset); + using var qBuffer = accelerator.Allocate1D(count); + using var orderBuffer = accelerator.Allocate1D(count); + // Convert to device-friendly GpuUInt128 on the fly + var tmp = System.Buffers.ArrayPool.Shared.Rent(count); + for (int i = 0; i < count; i++) + { + tmp[i] = (PerfectNumbers.Core.Gpu.GpuUInt128)qs[offset + i]; + } + // Copy only the populated elements to the device buffer to avoid overrun on pooled arrays. + qBuffer.View.CopyFromCPU(ref tmp[0], count); + System.Buffers.ArrayPool.Shared.Return(tmp); + orderKernel(count, exponent, divMul, qBuffer.View, orderBuffer.View); + accelerator.Synchronize(); + + ulong[] orders = orderBuffer.GetAsArray1D(); + for (int i = 0; i < count; i++) + { + ulong order = orders[i]; + if (order == 0UL) + { + order = qs[offset + i].CalculateOrder(); + } + + if (useOrderCache) + { + OrderCache[qs[offset + i]] = order; + } + } + + offset += count; + } + + ArrayPool.Shared.Return(qs); + } + + public bool IsMersennePrime(ulong exponent) + { + // Safe early rejections using known orders for tiny primes: + // 7 | M_p iff 3 | p. Avoid rejecting p == 3 where M_3 == 7 is prime. + if ((exponent % 3UL) == 0UL && exponent != 3UL) + { + return false; + } + + if ((exponent & 3UL) == 1UL && exponent.SharesFactorWithExponentMinusOne()) + { + return false; + } + + // Skip even exponents: for even p > 2, M_p is composite; here p is large. + if ((exponent & 1UL) == 0UL) + { + return false; + } + + bool prePrime = true; + UInt128 twoP = (UInt128)exponent << 1; // 2 * p + // UInt128 maxDivisor = new(ulong.MaxValue, ulong.MaxValue); + UInt128 maxK = (UInt128Numbers.Two ^ exponent) / 2 + 1; + bool lastIsSeven = (exponent & 3UL) == 3UL; + + // This is how maxK was for residue test before changes UInt128 maxK = maxKOverride ?? ResidueMaxK; + if (_useResidue) + { + // Disable this is if we don't want to hard limit maxK + // maxK = _maxK > maxK ? maxK : _maxK; + + if (_useGpuScan) + { + _residueGpuTester!.Scan(exponent, twoP, lastIsSeven, maxK, ref prePrime); + } + else + { + _residueCpuTester!.Scan(exponent, twoP, lastIsSeven, maxK, ref prePrime); + } + + return prePrime; + + // if (!prePrime) + // { + // return false; + // } + } + + if (!_useIncremental) + { + // Optional prefilter before Lucas–Lehmer using the same GPU/CPU scan path. + // Scan a limited range of k to quickly reject obvious cases. + UInt128 twoPPre = (UInt128)exponent << 1; + UInt128 maxKPre = (UInt128)PreLucasPreScanK; + bool lastIsSevenPre = (exponent & 3UL) == 3UL; + if (_useGpuScan) + { + _incrementalGpuTester.Scan(exponent, twoPPre, lastIsSevenPre, maxKPre, ref prePrime); + } + else + { + _incrementalCpuTester.Scan(exponent, twoPPre, lastIsSevenPre, maxKPre, ref prePrime); + } + + if (!prePrime) + { + return false; + } + + return _useGpuLucas + ? _lucasLehmerGpuTester.IsPrime(exponent, runOnGpu: true) + : _lucasLehmerCpuTester.IsPrime(exponent); + } + + bool isPrime = true; + + if (useOrder) + { + if (_useGpuOrder) + { + _orderGpuTester.Scan(exponent, twoP, lastIsSeven, maxK, ref isPrime); + } + else + { + _orderCpuTester.Scan(exponent, twoP, lastIsSeven, maxK, ref isPrime); + } + } + else + { + if (_useGpuScan) + { + _incrementalGpuTester.Scan(exponent, twoP, lastIsSeven, maxK, ref isPrime); + } + else + { + _incrementalCpuTester.Scan(exponent, twoP, lastIsSeven, maxK, ref isPrime); + } + } + + return isPrime; + } + + // Limit of k to scan in the pre-Lucas prescan + public const int PreLucasPreScanK = 5_000_000; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool DividesMersenne(ulong exponent, ulong prime) + { + return 2UL.ModPow64(exponent, prime) == 1UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void LegacyOrderKernel(Index1D index, ulong exponent, ArrayView qs, ArrayView orders) + { + UInt128 q = qs[index]; + UInt128 pow = exponent.PowMod(q); + orders[index] = pow == 1UL ? exponent : 0UL; + } + + +} diff --git a/PerfectNumbers.Core/MersennePrimeFactorTester.cs b/PerfectNumbers.Core/MersennePrimeFactorTester.cs new file mode 100644 index 0000000..7727f79 --- /dev/null +++ b/PerfectNumbers.Core/MersennePrimeFactorTester.cs @@ -0,0 +1,558 @@ +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using System.Numerics; +using UInt128 = System.UInt128; + +namespace PerfectNumbers.Core; + +public static class MersennePrimeFactorTester +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsPrimeFactor(ulong p, ulong q, CancellationToken ct) + { + if (q < 2UL || (q & 1UL) == 0) + { + return false; + } + + if ((q & 7UL) != 1UL && (q & 7UL) != 7UL) + { + return false; + } + + if (ct.IsCancellationRequested) + { + return false; + } + + if (!PrimeTester.IsPrimeInternal(q, ct)) + { + return false; + } + + if (p < 2UL || (p & 1UL) == 0) + { + return false; + } + + ulong value = checked(p << 1); + if ((q - 1UL) % value != 0UL) + { + return false; + } + + ulong order = GetOrderOf2ModPrime(q, ct); + return !ct.IsCancellationRequested && order == p; + } + + private static readonly Dictionary _orderCache = []; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ulong GetOrderOf2ModPrime(ulong q, CancellationToken ct) => + (ulong)GetOrderOf2ModPrime((UInt128)q, ct); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static UInt128 GetOrderOf2ModPrime(UInt128 q, CancellationToken ct) => + OrderOf2ModPrime(q, ct); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static UInt128 OrderOf2ModPrime(UInt128 q, CancellationToken ct) + { + lock (_orderCache) + { + if (_orderCache.TryGetValue(q, out var cached)) + { + return cached; + } + } + + UInt128 phi = q - 1; + UInt128[] factors = phi <= ulong.MaxValue + ? Array.ConvertAll(Factor64((ulong)phi, ct), x => (UInt128)x) + : Factor128(phi, ct); + UInt128 order = phi; + + UInt128 candidate, prime; + int i, factorsCount = factors.Length; + for (i = 0; i < factorsCount; i++) + { + prime = factors[i]; + if (ct.IsCancellationRequested) + { + return 0; + } + + while (order % prime == 0) + { + if (ct.IsCancellationRequested) + { + return 0; + } + + candidate = order / prime; + if (ModPow128(2, candidate, q, ct) == 1) + { + order = candidate; + } + else + { + break; + } + } + } + + if (!ct.IsCancellationRequested) + { + lock (_orderCache) + { + _orderCache[q] = order; + } + } + + return order; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong ModPow64(ulong value, ulong exponent, ulong modulus, CancellationToken ct) + { + ulong result = 1UL; + ulong baseValue = value % modulus; + ulong exp = exponent; + + while (exp != 0UL) + { + if (ct.IsCancellationRequested) + { + return 0UL; + } + + if ((exp & 1UL) != 0UL) + { + result = MulMod64(result, baseValue, modulus); + } + + if (ct.IsCancellationRequested) + { + return 0UL; + } + + baseValue = MulMod64(baseValue, baseValue, modulus); + exp >>= 1; + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong MulMod64(ulong a, ulong b, ulong mod) => (ulong)((UInt128)a * b % mod); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static UInt128 ModPow128(UInt128 value, UInt128 exponent, UInt128 modulus, CancellationToken ct) + { + UInt128 result = 1; + UInt128 baseValue = value % modulus; + UInt128 exp = exponent; + + while (exp != 0) + { + if (ct.IsCancellationRequested) + { + return 0; + } + + if ((exp & 1) != 0) + { + result = MulMod128(result, baseValue, modulus); + } + + if (ct.IsCancellationRequested) + { + return 0; + } + + baseValue = MulMod128(baseValue, baseValue, modulus); + exp >>= 1; + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static UInt128 MulMod128(UInt128 a, UInt128 b, UInt128 mod) + { + BigInteger product = (BigInteger)a * b; + product %= (BigInteger)mod; + return (UInt128)product; + } + + private static readonly ConcurrentBag> ListPool = new(); + private static readonly ConcurrentBag> DictPool = new(); + private static readonly Dictionary FactorCache = new(); + private static readonly object FactorCacheLock = new(); + + private static List RentList() => ListPool.TryTake(out var list) ? list : new List(4); + + private static void ReturnList(List list) + { + list.Clear(); + ListPool.Add(list); + } + + private static Dictionary RentDictionary() => DictPool.TryTake(out var dict) ? dict : new Dictionary(); + + private static void ReturnDictionary(Dictionary dict) + { + dict.Clear(); + DictPool.Add(dict); + } + + private static readonly ConcurrentBag> DictPool128 = new(); + + private static Dictionary RentDictionary128() => + DictPool128.TryTake(out var dict) ? dict : new Dictionary(); + + private static void ReturnDictionary128(Dictionary dict) + { + dict.Clear(); + DictPool128.Add(dict); + } + + private static ulong[] Factor64(ulong n, CancellationToken ct) + { + // lock (FactorCacheLock) + // { + // if (FactorCache.TryGetValue(n, out var cached)) + // { + // return cached; + // } + // } + + if (n == 1UL) + { + return []; + } + + var dict = RentDictionary(); + var queue = new Queue(); + queue.Enqueue(n); + + while (queue.Count != 0) + { + if (ct.IsCancellationRequested) + { + break; + } + + ulong m = queue.Dequeue(); + if (m == 1UL) + { + continue; + } + + if (PrimeTester.IsPrimeInternal(m, ct)) + { + dict[m] = 1; + continue; + } + + if (ct.IsCancellationRequested) + { + break; + } + + ulong d = PollardRho(m, ct); + if (ct.IsCancellationRequested) + { + break; + } + + queue.Enqueue(d); + queue.Enqueue(m / d); + } + + ulong[] result = [.. dict.Keys]; + // lock (FactorCacheLock) + // { + // FactorCache[n] = result; + // } + + ReturnDictionary(dict); + return result; + } + + private static UInt128[] Factor128(UInt128 n, CancellationToken ct) + { + if (n == 1) + { + return []; + } + + var dict = RentDictionary128(); + var queue = new Queue(); + queue.Enqueue(n); + + while (queue.Count != 0) + { + if (ct.IsCancellationRequested) + { + break; + } + + UInt128 m = queue.Dequeue(); + if (m == 1) + { + continue; + } + + if (IsPrime128(m, ct)) + { + dict[m] = 1; + continue; + } + + if (ct.IsCancellationRequested) + { + break; + } + + UInt128 d = PollardRho128(m, ct); + if (ct.IsCancellationRequested) + { + break; + } + + queue.Enqueue(d); + queue.Enqueue(m / d); + } + + var result = new UInt128[dict.Count]; + int index = 0; + foreach (var key in dict.Keys) + { + result[index++] = key; + } + + ReturnDictionary128(dict); + return result; + } + + private static ulong PollardRho(ulong n, CancellationToken ct) + { + if ((n & 1UL) == 0UL) + { + return 2UL; + } + + var rng = new Random(1234567); + + ulong c, d, x, y, diff; + while (true) + { + if (ct.IsCancellationRequested) + { + return 0UL; + } + + c = (ulong)rng.Next(2, int.MaxValue); + x = (ulong)rng.Next(2, int.MaxValue); + y = x; + d = 1UL; + + while (d == 1UL) + { + if (ct.IsCancellationRequested) + { + return 0UL; + } + + x = (MulMod64(x, x, n) + c) % n; + if (ct.IsCancellationRequested) + { + return 0UL; + } + + y = (MulMod64(y, y, n) + c) % n; + if (ct.IsCancellationRequested) + { + return 0UL; + } + + y = (MulMod64(y, y, n) + c) % n; + diff = x > y ? x - y : y - x; + if (ct.IsCancellationRequested) + { + return 0UL; + } + + d = Gcd(diff, n, ct); + } + + if (d != n) + { + return d; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Gcd(ulong a, ulong b, CancellationToken ct) + { + ulong t; + while (b != 0UL) + { + if (ct.IsCancellationRequested) + { + return 1UL; // We're returning 1 to avoid division by 0 in any place + } + + t = a % b; + a = b; + b = t; + } + + return a; + } + + private static bool IsPrime128(UInt128 n, CancellationToken ct) + { + if (n <= 3) + { + return n >= 2; + } + + if ((n & 1) == 0) + { + return false; + } + + BigInteger bn = (BigInteger)n; + BigInteger d = bn - 1; + int s = 0; + while ((d & 1) == 0) + { + d >>= 1; + s++; + } + + BigInteger[] bases = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]; + for (int i = 0; i < bases.Length; i++) + { + BigInteger a = bases[i]; + if (a >= bn) + { + continue; + } + + if (ct.IsCancellationRequested) + { + return false; + } + + BigInteger x = BigInteger.ModPow(a, d, bn); + if (x == 1 || x == bn - 1) + { + continue; + } + + bool cont = false; + for (int r = 1; r < s; r++) + { + if (ct.IsCancellationRequested) + { + return false; + } + + x = BigInteger.ModPow(x, 2, bn); + if (x == bn - 1) + { + cont = true; + break; + } + } + + if (!cont) + { + return false; + } + } + + return true; + } + + private static UInt128 PollardRho128(UInt128 n, CancellationToken ct) + { + if ((n & 1) == 0) + { + return 2; + } + + var rng = new Random(1234567); + + UInt128 c, d, x, y, diff; + while (true) + { + if (ct.IsCancellationRequested) + { + return 0; + } + + c = (UInt128)rng.NextInt64(2, long.MaxValue); + x = (UInt128)rng.NextInt64(2, long.MaxValue); + y = x; + d = 1; + + while (d == 1) + { + if (ct.IsCancellationRequested) + { + return 0; + } + + x = (MulMod128(x, x, n) + c) % n; + if (ct.IsCancellationRequested) + { + return 0; + } + + y = (MulMod128(y, y, n) + c) % n; + if (ct.IsCancellationRequested) + { + return 0; + } + + y = (MulMod128(y, y, n) + c) % n; + diff = x > y ? x - y : y - x; + if (ct.IsCancellationRequested) + { + return 0; + } + + d = Gcd128(diff, n, ct); + } + + if (d != n) + { + return d; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static UInt128 Gcd128(UInt128 a, UInt128 b, CancellationToken ct) + { + UInt128 t; + while (b != 0) + { + if (ct.IsCancellationRequested) + { + return 1; + } + + t = a % b; + a = b; + b = t; + } + + return a; + } +} diff --git a/PerfectNumbers.Core/MersenneResidueStepper.cs b/PerfectNumbers.Core/MersenneResidueStepper.cs new file mode 100644 index 0000000..c2aa392 --- /dev/null +++ b/PerfectNumbers.Core/MersenneResidueStepper.cs @@ -0,0 +1,69 @@ +namespace PerfectNumbers.Core; + +public class MersenneResidueStepper +{ + private UInt128 m; + private ulong p; + private UInt128 pow2_mod; + private UInt128 M_mod; + + public MersenneResidueStepper(UInt128 modulus, ulong p0, UInt128 M_mod_m_at_p0) + { + if (modulus == 0) + throw new ArgumentException("Modulus must be positive."); + m = modulus; + p = p0; + M_mod = M_mod_m_at_p0 % m; + pow2_mod = (M_mod + 1) % m; + } + + public (ulong, UInt128) State() => (p, M_mod); + + public (ulong, UInt128) Step(long delta) + { + if (delta == 0) + return State(); + UInt128 g = UInt128.One; + ulong absDelta = (ulong)Math.Abs(delta); + for (ulong i = 0; i < absDelta; i++) + { + g <<= 1; + if (g >= m) + g -= m; + } + if (delta > 0) + pow2_mod = (pow2_mod * g) % m; + else + { + // Modular inverse for negative delta + pow2_mod = (pow2_mod * ModInverse(g, m)) % m; + } + p = (ulong)((long)p + delta); + M_mod = (pow2_mod + m - 1) % m; + return State(); + } + + public (ulong, UInt128) To(ulong p_target) + { + long delta = (long)p_target - (long)p; + return Step(delta); + } + + private static UInt128 ModInverse(UInt128 a, UInt128 m) + { + UInt128 m0 = m, t, q; + UInt128 x0 = 0, x1 = 1; + if (m == 1) return 0; + while (a > 1) + { + q = a / m; + t = m; + m = a % m; a = t; + t = x0; + x0 = x1 - q * x0; + x1 = t; + } + if (x1 < 0) x1 += m0; + return x1; + } +} diff --git a/PerfectNumbers.Core/ModResidueTracker.cs b/PerfectNumbers.Core/ModResidueTracker.cs new file mode 100644 index 0000000..67679e6 --- /dev/null +++ b/PerfectNumbers.Core/ModResidueTracker.cs @@ -0,0 +1,313 @@ +using System.Runtime.CompilerServices; + +using UInt128 = System.UInt128; + +namespace PerfectNumbers.Core; + +public enum ResidueModel +{ + Identity, // residue of N modulo d + Mersenne, // residue of (2^N - 1) modulo d +} + +/// Tracks residues for a monotonically increasing sequence of numbers against +/// a dynamic set of divisors. Designed to support repeated divisor checks +/// while the tested number grows step-by-step without ever decreasing. +public sealed class ModResidueTracker +{ + public static readonly ModResidueTracker Shared = new(ResidueModel.Mersenne, initialNumber: PerfectNumberConstants.BiggestKnownEvenPerfectP, initialized: true); + + private readonly ResidueModel _model; + private UInt128 _currentNumber; + private bool _initialized; + // Per-divisor sorted state at _currentNumber. Aligned arrays for speed. + private readonly List _divisors = new(1024); // sorted ascending + private readonly List _residues = new(1024); // same index as _divisors + private int _cursor; // merge pointer for ascending candidate stream + private bool _hasLastAppended; + private UInt128 _lastAppended; + + public ModResidueTracker(ResidueModel model, UInt128 initialNumber = default, bool initialized = false) + { + _model = model; + _currentNumber = initialNumber; + _initialized = initialized; + } + + /// Returns true if the provided number (either p or M_p depending on model) + /// is divisible by the divisor. Advances internal state forward if needed + /// (number must be non-decreasing across calls). Adds and initializes new + /// divisors on first use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsDivisible(UInt128 number, UInt128 divisor) + { + if (divisor == UInt128.Zero) + { + throw new ArgumentOutOfRangeException(nameof(divisor)); + } + + EnsureAt(number); + + List divisors = _divisors; + int idx = LowerBound(divisors, divisor); + if (idx == divisors.Count || divisors[idx] != divisor) + { + UInt128 residue = ComputeInitialResidue(number, divisor); + divisors.Insert(idx, divisor); + _residues.Insert(idx, residue); + if (_hasLastAppended && idx == divisors.Count - 1 && divisor >= _lastAppended) + { + _lastAppended = divisor; + } + else if (!_hasLastAppended) + { + _hasLastAppended = true; + _lastAppended = divisor; + } + + return residue == UInt128.Zero; + } + + return _residues[idx] == UInt128.Zero; + } + + // Merge-walk support: prepare for increasing sequence of candidate divisors. + public void BeginMerge(UInt128 number) + { + EnsureAt(number); + _cursor = 0; + } + + // Merge or append current candidate divisor (ascending order required across calls). + // Returns true and sets 'divisible' based on residue at 'number'. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MergeOrAppend(UInt128 number, UInt128 divisor, out bool divisible) + { + // Assume number is non-decreasing; advance residues lazily when number changes. + List divisors = _divisors; + if (number != _currentNumber) + { + EnsureAt(number); + // After EnsureAt we can keep _cursor (still valid monotonic pointer) + if (_cursor > divisors.Count) _cursor = divisors.Count; + } + + UInt128 residue; + // Fast path: append to tail when candidates are strictly increasing. + List residues = _residues; + if (divisors.Count == 0 || (_hasLastAppended && divisor >= _lastAppended && divisors[^1] <= divisor)) + { + residue = ComputeInitialResidue(number, divisor); + divisors.Add(divisor); + residues.Add(residue); + _hasLastAppended = true; + _lastAppended = divisor; + _cursor = divisors.Count; // position after last + divisible = residue == UInt128.Zero; + return true; + } + + // Merge pointer: advance until current >= divisor + var cursor = _cursor; + while (cursor < divisors.Count && divisors[cursor] < divisor) + { + cursor++; + } + + if (cursor < divisors.Count && divisors[cursor] == divisor) + { + divisible = residues[cursor] == UInt128.Zero; + _cursor = cursor + 1; // position after matched + return true; + } + + + // Insert at cursor to keep order + residue = ComputeInitialResidue(number, divisor); + divisors.Insert(cursor, divisor); + residues.Insert(cursor, residue); + divisible = residue == UInt128.Zero; + _cursor = cursor + 1; + + // update last appended hint + if (!_hasLastAppended || divisor >= _lastAppended) + { + _hasLastAppended = true; + _lastAppended = divisor; + } + + return true; + } + + /// Ensures the internal state corresponds to the provided number by moving + /// forward from the current number (never backwards). O(Count(divisors)). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void EnsureAt(UInt128 number) + { + if (!_initialized) + { + _currentNumber = number; + // Initialize residues lazily upon first check for each divisor. + _initialized = true; + return; + } + + if (number == _currentNumber) + { + return; + } + + if (number < _currentNumber) + { + throw new InvalidOperationException("Number must be non-decreasing."); + } + + UInt128 delta = number - _currentNumber; + List divisors = _divisors; + int divisorsCount = divisors.Count; + if (divisorsCount == 0) + { + _currentNumber = number; + return; + } + + // Advance all tracked residues from _currentNumber to 'number'. + // For Identity: r' = (r + delta) mod d + // For Mersenne: r' = (2^delta * r + (2^delta - 1)) mod d + // Optimize small deltas with simple forms when possible. + int i = 0; + UInt128 d, value; + List residues = _residues; + + if (_model == ResidueModel.Identity) + { + // Avoid 128-bit division: delta is small in scanner (<= 6) so use add/sub. + for (; i < divisorsCount; i++) + { + d = divisors[i]; + value = residues[i] + delta; + // For tiny moduli (2..6) we may need a few subtractions; keep the loop tight. + while (value >= d) + { + value -= d; + } + + residues[i] = value; + } + } + else + { + for (; i < divisors.Count; i++) + { + d = divisors[i]; + value = PowMod128(UInt128Numbers.Two, delta, d); + // r' = pow2Delta*r + (pow2Delta - 1) mod d + value = MulMod128(value, residues[i], d) + value - UInt128.One; + if (value >= d) + { + value -= d; + } + + residues[i] = value; + } + } + + _currentNumber = number; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int LowerBound(List list, UInt128 value) + { + int lo = 0, hi = list.Count, mid; + while (lo < hi) + { + mid = (int)(((uint)lo + (uint)hi) >> 1); + if (list[mid] < value) + { + lo = mid + 1; + } + else + { + hi = mid; + } + } + + return lo; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private UInt128 ComputeInitialResidue(UInt128 number, UInt128 divisor) + { + if (_model == ResidueModel.Identity) + { + // Fast path for 64-bit inputs + if (number <= ulong.MaxValue && divisor <= ulong.MaxValue) + { + return (ulong)number % (ulong)divisor; + } + + return number % divisor; + } + + // residue of Mersenne number M_number = 2^number - 1 modulo divisor + UInt128 pow = PowMod128(UInt128Numbers.Two, number, divisor); + + // q divides M_p ⇔ 2^p ≡ 1 (mod q) AND ord_q(2) | p + if (pow != UInt128.One) + return (pow - UInt128.One) % divisor; + + ulong ord = divisor.CalculateOrder(); + if (ord == 0UL || divisor % ord != UInt128.Zero) + return UInt128.One; // does NOT divide + + return UInt128.Zero; // q really divides M_p + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static UInt128 PowMod128(UInt128 baseValue, UInt128 exponent, UInt128 modulus) + { + if (modulus == UInt128.One) + { + return UInt128.Zero;; + } + + UInt128 result = UInt128.One; + UInt128 value = baseValue % modulus; + + while (exponent != UInt128.Zero) + { + if ((exponent & UInt128.One) != UInt128.Zero) + { + result = MulMod128(result, value, modulus); + } + + exponent >>= 1; + if (exponent != UInt128.Zero) + { + value = MulMod128(value, value, modulus); + } + } + + return result; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static UInt128 MulMod128(UInt128 a, UInt128 b, UInt128 modulus) + { + // Same approach as MulMod in MersenneNumberTester: double-and-add to avoid overflow. + UInt128 result = UInt128.Zero; + a %= modulus; + + while (b != UInt128.Zero) + { + if ((b & UInt128.One) != UInt128.Zero) + { + result += result < modulus ? a : a - modulus; + } + + a = a < modulus ? a << 1 : (a << 1) - modulus; + b >>= 1; + } + + return result; + } +} diff --git a/PerfectNumbers.Core/ModuloAutomata.cs b/PerfectNumbers.Core/ModuloAutomata.cs new file mode 100644 index 0000000..2b8fede --- /dev/null +++ b/PerfectNumbers.Core/ModuloAutomata.cs @@ -0,0 +1,215 @@ +using System.Runtime.CompilerServices; +using UInt128 = System.UInt128; + +namespace PerfectNumbers.Core; + +/// +/// Tracks residues for a fixed-step arithmetic progression without divisions. +/// Optimized variant for the Mersenne scan q = 2*p*k + 1 that needs mod 10, 8, 3 and 5. +/// +public sealed class MersenneResidueAutomaton +{ + private readonly UInt128 _step; // 2*p + private UInt128 _currentQ; // current q value + + // Cached residues for current q + public ulong Mod10R { get; private set; } + public ulong Mod8R { get; private set; } + public ulong Mod3R { get; private set; } + public ulong Mod5R { get; private set; } + + // Cached residues for step (2*p mod m) + private readonly ulong _step10; + private readonly ulong _step8; + private readonly ulong _step3; + private readonly ulong _step5; + + public MersenneResidueAutomaton(ulong exponent) + { + _step = (UInt128)exponent << 1; // 2 * p + _currentQ = _step + 1UL; // start at k = 1 + + // init residues + Mod10R = Mod10(_currentQ); + Mod8R = Mod8(_currentQ); + Mod3R = Mod3(_currentQ); + Mod5R = Mod5(_currentQ); + + _step10 = Mod10((ulong)(_step % 10UL)); + _step8 = (ulong)(_step & 7UL); // % 8 + _step3 = Mod3(_step); + _step5 = Mod5(_step); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public UInt128 CurrentQ() => _currentQ; + + /// + /// Advance to next k (q += 2*p), updating residues branchlessly. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Next() + { + _currentQ += _step; + + ulong r10 = Mod10R + _step10; + r10 -= (r10 >= 10UL) ? 10UL : 0UL; + Mod10R = r10; + + ulong r8 = Mod8R + _step8; + r8 &= 7UL; // % 8 + Mod8R = r8; + + ulong r3 = Mod3R + _step3; + r3 -= (r3 >= 3UL) ? 3UL : 0UL; + Mod3R = r3; + + ulong r5 = Mod5R + _step5; + if (r5 >= 5UL) + { + r5 -= 5UL; + } + Mod5R = r5; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Mod10(UInt128 value) + { + // Split and fold under mod 10: 2^64 ≡ 6 (mod 10) + ulong high = (ulong)(value >> 64); + ulong low = (ulong)value; + ulong highRem = Mod10(high); + ulong lowRem = Mod10(low); + ulong combined = lowRem + highRem * 6UL; + return Mod10(combined); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Mod10(ulong value) => value % 10UL; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Mod8(UInt128 value) => (ulong)value & 7UL; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Mod3(UInt128 value) + { + // 2^64 ≡ 1 (mod 3) + ulong high = (ulong)(value >> 64); + ulong low = (ulong)value; + ulong rem = (low % 3UL) + (high % 3UL); + return rem >= 3UL ? rem - 3UL : rem; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Mod5(UInt128 value) + { + // 2^64 ≡ 1 (mod 5) + ulong high = (ulong)(value >> 64); + ulong low = (ulong)value; + ulong rem = (low % 5UL) + (high % 5UL); + return rem >= 5UL ? rem - 5UL : rem; + } +} + +/// +/// Generic automaton for scanning integers ending with digit 7: n = 10*t + 7. +/// Maintains residues r_m without divisions while advancing by +10 each step. +/// +public sealed class Ending7Automaton +{ + private ulong _n; // current candidate n, ends with 7 + + private readonly ulong[] _moduli; + private readonly ulong[] _steps; // 10 % m + private readonly ulong[] _res; // n % m + private readonly bool[] _reachable; + + public Ending7Automaton(ulong start, params ulong[] moduli) + { + if (moduli == null || moduli.Length == 0) + { + _moduli = Array.Empty(); + _steps = Array.Empty(); + _res = Array.Empty(); + _reachable = Array.Empty(); + _n = AlignTo7(start); + return; + } + + _n = AlignTo7(start); + int len = moduli.Length; + _moduli = new ulong[len]; + _steps = new ulong[len]; + _res = new ulong[len]; + _reachable = new bool[len]; + for (int i = 0; i < len; i++) + { + ulong m = moduli[i]; + _moduli[i] = m; + ulong step = 10UL % m; + _steps[i] = step; + _res[i] = _n % m; // one-time division at construction + // Zero is reachable iff gcd(step, m) | res + ulong g = Gcd(step, m); + _reachable[i] = (g == 0UL) ? false : (_res[i] % g == 0UL); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong Current() => _n; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool DivisibleBy(ulong modulus) + { + for (int i = 0; i < _moduli.Length; i++) + { + if (_moduli[i] == modulus) + { + return _reachable[i] && _res[i] == 0UL; + } + } + + return (_n % modulus) == 0UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Next() + { + _n += 10UL; + for (int i = 0; i < _moduli.Length; i++) + { + if (!_reachable[i]) + { + continue; + } + + ulong m = _moduli[i]; + ulong r = _res[i] + _steps[i]; + if (r >= m) + { + r -= m; + } + _res[i] = r; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong AlignTo7(ulong start) + { + ulong r = start % 10UL; + ulong add = (7UL + 10UL - r) % 10UL; + return start + add; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Gcd(ulong a, ulong b) + { + while (b != 0UL) + { + ulong t = a % b; + a = b; + b = t; + } + return a; + } +} diff --git a/PerfectNumbers.Core/PerfectNumberConstants.cs b/PerfectNumbers.Core/PerfectNumberConstants.cs new file mode 100644 index 0000000..9a8bec8 --- /dev/null +++ b/PerfectNumbers.Core/PerfectNumberConstants.cs @@ -0,0 +1,11 @@ +namespace PerfectNumbers.Core; + +public static class PerfectNumberConstants +{ + public const ulong BiggestKnownEvenPerfectP = 136279841UL; + public static readonly ulong PrimesLimit = 1_000_000; //(ulong)Array.MaxLength;// 1_000_000; + + // Maximum K for residue-based Mersenne test (q = 2*p*k + 1 up to this k) + public static readonly UInt128 ResidueMaxK = 5_000_000UL; + public const int MaxQForDivisorCycles = 4_000_000; +} \ No newline at end of file diff --git a/PerfectNumbers.Core/PerfectNumbers.Core.csproj b/PerfectNumbers.Core/PerfectNumbers.Core.csproj new file mode 100644 index 0000000..cf40ee4 --- /dev/null +++ b/PerfectNumbers.Core/PerfectNumbers.Core.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + 12.0 + + + + + + + + + + + + + + + + diff --git a/PerfectNumbers.Core/PowerCache.cs b/PerfectNumbers.Core/PowerCache.cs new file mode 100644 index 0000000..e24c9fc --- /dev/null +++ b/PerfectNumbers.Core/PowerCache.cs @@ -0,0 +1,65 @@ +using System.Collections.Concurrent; +using System.Numerics; +using PeterO.Numbers; + +namespace PerfectNumbers.Core; + +public static class PowerCache +{ + private static readonly ConcurrentDictionary BigCache = new(); + private static readonly ConcurrentDictionary ECache = new(); + + public static BigInteger Get(ulong baseVal, int exponent) + { + if (!BigCache.TryGetValue(baseVal, out var powers)) + { + powers = new BigInteger[exponent + 1]; + powers[0] = BigInteger.One; + BigCache[baseVal] = powers; + } + + if (powers.Length <= exponent) + { + int oldLength = powers.Length; + Array.Resize(ref powers, exponent + 1); + for (int i = oldLength; i <= exponent; i++) + powers[i] = BigInteger.Zero; + + BigCache[baseVal] = powers; + } + + if (powers[exponent].IsZero) + { + for (int i = 1; i <= exponent; i++) + if (powers[i].IsZero) + powers[i] = powers[i - 1] * baseVal; + } + + return powers[exponent]; + } + + public static EInteger Get(EInteger baseVal, int exponent) + { + if (!ECache.TryGetValue(baseVal, out var powers)) + { + powers = new EInteger[exponent + 1]; + powers[0] = EInteger.One; + ECache[baseVal] = powers; + } + if (powers.Length <= exponent) + { + int oldLength = powers.Length; + Array.Resize(ref powers, exponent + 1); + for (int i = oldLength; i <= exponent; i++) + powers[i] = null; + ECache[baseVal] = powers; + } + if (powers[exponent] == null) + { + for (int i = 1; i <= exponent; i++) + if (powers[i] == null) + powers[i] = powers[i - 1]! * baseVal; + } + return powers[exponent]!; + } +} diff --git a/PerfectNumbers.Core/PrimeCache.cs b/PerfectNumbers.Core/PrimeCache.cs new file mode 100644 index 0000000..6b900af --- /dev/null +++ b/PerfectNumbers.Core/PrimeCache.cs @@ -0,0 +1,96 @@ +using System.Buffers; +using PeterO.Numbers; +using Open.Numeric.Primes; + +namespace PerfectNumbers.Core; + +public sealed class PrimeCache +{ + private readonly List _primes = new(); + private readonly List _primeMod4 = new(); + private IEnumerator _enumerator = Prime.Numbers.GetEnumerator(); + private static readonly EInteger[] EmptyEulerPrimesArray = Array.Empty(); + + private void EnsurePrimesUpTo(EInteger limit) + { + EInteger last = _primes.Count > 0 ? _primes[^1] : EInteger.Zero; + if (last.CompareTo(limit) >= 0) + { + return; + } + + while (last.CompareTo(limit) < 0 && _enumerator.MoveNext()) + { + ulong current = _enumerator.Current; + last = current; + _primes.Add(last); + _primeMod4.Add((byte)(current & 3)); + } + } + + public EInteger[] GetEulerPrimes(EInteger start, EInteger end) + { + EnsurePrimesUpTo(end); + + int index = _primes.BinarySearch(start); + if (index < 0) + { + index = ~index; + } + + EInteger[] primesArray = ArrayPool.Shared.Rent(_primes.Count); + try + { + Span primes = new(primesArray); + int count = 0; + for (; index < _primes.Count; index++) + { + EInteger val = _primes[index]; + if (val.CompareTo(end) > 0) + { + break; + } + if (_primeMod4[index] == 1) + { + primes[count++] = val; + } + } + + return count > 0 ? primes[..count].ToArray() : EmptyEulerPrimesArray; + } + finally + { + ArrayPool.Shared.Return(primesArray); + } + } + + public IEnumerable EnumeratePrimes(EInteger start) + { + ulong s = (ulong)start; + EnsurePrimesUpTo(start); + + int index = _primes.BinarySearch(s); + if (index < 0) + { + index = ~index; + } + + for (; index < _primes.Count; index++) + { + yield return _primes[index]; + } + + while (_enumerator.MoveNext()) + { + ulong current = _enumerator.Current; + EInteger val = EInteger.FromUInt64(current); + _primes.Add(val); + _primeMod4.Add((byte)(current & 3)); + if (current >= s) + { + yield return val; + } + } + } +} + diff --git a/PerfectNumbers.Core/PrimeTester.cs b/PerfectNumbers.Core/PrimeTester.cs new file mode 100644 index 0000000..0d74ec2 --- /dev/null +++ b/PerfectNumbers.Core/PrimeTester.cs @@ -0,0 +1,371 @@ +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using ILGPU; +using ILGPU.Algorithms; +using ILGPU.Runtime; +using PerfectNumbers.Core.Gpu; + +namespace PerfectNumbers.Core; + +public sealed class PrimeTester(bool useInternal = false) +{ + public bool IsPrime(ulong n, CancellationToken ct) => IsPrimeInternal(n, ct); + + // Optional GPU-assisted primality: batched small-prime sieve on device. + // TODO: Consider extending with deterministic Miller–Rabin bases for 64-bit range when stable on ILGPU. + public bool IsPrimeGpu(ulong n, CancellationToken ct) + { + if (GpuContextPool.ForceCpu) + { + return IsPrimeInternal(n, ct); + } + + Span one = stackalloc ulong[1]; + Span outFlags = stackalloc byte[1]; + one[0] = n; + IsPrimeBatchGpu(one, outFlags); + return outFlags[0] != 0; + } + + // Note: GPU-backed primality path is implemented via IsPrimeGpu/IsPrimeBatchGpu and is routed + // from EvenPerfectBitScanner based on --primes-device. Follow-up: add deterministic MR rounds. + + internal static bool IsPrimeInternal(ulong n, CancellationToken ct) + { + bool result = true; + if (n <= 3UL) + { + result = n >= 2UL; + } + else if ((n & 1UL) == 0) + { + result = false; + } + else if (n % 5 == 0) + { + result = false; + } + else + { + if ((n % 10UL) == 1UL && SharesFactorWithMaxExponent(n)) + { + result = false; + } + else + { + var smallPrimeDivisorsLength = PrimesGenerator.SmallPrimes.Length; + ulong[] smallPrimeDivisors = PrimesGenerator.SmallPrimes; + ulong[] smallPrimeDivisorsMul = PrimesGenerator.SmallPrimesPow2; + for (int i = 0; i < smallPrimeDivisorsLength; i++) + { + if (smallPrimeDivisorsMul[i] > n) + { + break; + } + + if (n % smallPrimeDivisors[i] == 0) + { + result = false; + break; + } + } + } + } + + return result; + } + + public static int GpuBatchSize { get; set; } = 262_144; + + public static void IsPrimeBatchGpu(ReadOnlySpan values, Span results) + { + // Limit concurrency and declare variables outside loops for performance and reuse + using var limiter = GpuPrimeWorkLimiter.Acquire(); + using var gpu = GpuContextPool.Rent(); + var accelerator = gpu.Accelerator; + + // Get per-accelerator cached kernel and device primes buffer. + var state = GpuKernelState.GetOrCreate(accelerator); + + MemoryBuffer1D input; + MemoryBuffer1D output; + int totalLength, batchSize, pos, remaining, count; + ulong[] temp; + + totalLength = values.Length; + batchSize = Math.Max(1, GpuBatchSize); + + // Rent per-call scratch buffers sized to current batch (thread-safe) + var scratch = state.RentScratch(batchSize, accelerator); + input = scratch.Input; + output = scratch.Output; + + // Reusable host buffer per call + temp = ArrayPool.Shared.Rent(batchSize); + + pos = 0; + while (pos < totalLength) + { + remaining = totalLength - pos; + count = remaining > batchSize ? batchSize : remaining; + + // Copy chunk to host temp then to device + values.Slice(pos, count).CopyTo(temp); + input.View.CopyFromCPU(ref temp[0], count); + + state.Kernel(count, input.View, state.DevicePrimes.View, output.View); + accelerator.Synchronize(); + output.View.CopyToCPU(ref results[pos], count); + + pos += count; + } + + ArrayPool.Shared.Return(temp); + state.ReturnScratch(scratch); + } + + // Per-accelerator GPU state for prime sieve (kernel + uploaded primes). + private sealed class KernelState + { + public Action, ArrayView, ArrayView> Kernel { get; } + public MemoryBuffer1D DevicePrimes { get; } + private readonly Accelerator _accel; + private readonly System.Collections.Concurrent.ConcurrentBag _scratchPool = new(); + + public KernelState(Accelerator accelerator) + { + _accel = accelerator; + // Compile once per accelerator and upload primes once. + Kernel = accelerator.LoadAutoGroupedStreamKernel, ArrayView, ArrayView>(SmallPrimeSieveKernel); + + var primes = PrimesGenerator.SmallPrimes; + DevicePrimes = accelerator.Allocate1D(primes.Length); + DevicePrimes.View.CopyFromCPU(primes); + } + + internal sealed class ScratchBuffers : IDisposable + { + public MemoryBuffer1D Input { get; private set; } + public MemoryBuffer1D Output { get; private set; } + public int Capacity { get; private set; } + + public ScratchBuffers(Accelerator accel, int capacity) + { + Capacity = Math.Max(1, capacity); + Input = accel.Allocate1D(Capacity); + Output = accel.Allocate1D(Capacity); + } + + public void Dispose() + { + Output.Dispose(); + Input.Dispose(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ScratchBuffers RentScratch(int minCapacity, Accelerator accel) + { + minCapacity = Math.Max(1, minCapacity); + while (_scratchPool.TryTake(out var sb)) + { + if (sb.Capacity >= minCapacity) + { + return sb; + } + + sb.Dispose(); + } + + return new ScratchBuffers(accel, minCapacity); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReturnScratch(ScratchBuffers scratch) + { + _scratchPool.Add(scratch); + } + + public void Clear() + { + while (_scratchPool.TryTake(out var sb)) + { + sb.Dispose(); + } + DevicePrimes.Dispose(); + } + } + + private static class GpuKernelState + { + // Map accelerator to cached state; use Lazy to serialize kernel creation + private static readonly System.Collections.Concurrent.ConcurrentDictionary> States = new(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static KernelState GetOrCreate(Accelerator accelerator) + { + var lazy = States.GetOrAdd( + accelerator, + acc => new Lazy(() => new KernelState(acc), System.Threading.LazyThreadSafetyMode.ExecutionAndPublication)); + return lazy.Value; + } + + public static void Clear(Accelerator accelerator) + { + if (States.TryRemove(accelerator, out var lazy)) + { + if (lazy.IsValueCreated) + { + var state = lazy.Value; + state.Clear(); + } + } + } + } + + // Expose cache clearing for accelerator disposal coordination + public static void ClearGpuCaches(Accelerator accelerator) + { + GpuKernelState.Clear(accelerator); + } + + // GPU kernel: small-prime sieve only. Returns 1 if passes sieve (probable prime), 0 otherwise. + // TODO: Follow-up: consider integration with MR rounds for bases {2,3,5,7,11,13} to make it deterministic for 64-bit. + private static void SmallPrimeSieveKernel(Index1D index, ArrayView numbers, ArrayView smallPrimes, ArrayView results) + { + ulong n = numbers[index]; + if (n <= 3UL) + { + results[index] = (byte)(n >= 2UL ? 1 : 0); + return; + } + + if ((n & 1UL) == 0UL || (n % 5UL) == 0UL) + { + results[index] = 0; + return; + } + + // Early reject special gcd heuristic with floor(log2 n) + ulong m = 63UL - (ulong)ILGPU.Algorithms.XMath.LeadingZeroCount(n); + if (BinaryGcdGpu(n, m) != 1UL) + { + results[index] = 0; + return; + } + + long len = smallPrimes.Length; + for (long i = 0; i < len; i++) + { + ulong p = smallPrimes[(int)i]; + if (p * p > n) + { + break; + } + + if ((n % p) == 0UL) + { + results[index] = 0; + return; + } + } + + results[index] = 1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool SharesFactorWithMaxExponent(ulong n) + { + ulong m = (ulong)BitOperations.Log2(n); + return BinaryGcd(n, m) != 1UL; + } + + internal static void SharesFactorWithMaxExponentBatch(ReadOnlySpan values, Span results) + { + using var gpu = GpuContextPool.Rent(); + var accelerator = gpu.Accelerator; + var kernel = accelerator.LoadAutoGroupedStreamKernel, ArrayView>(SharesFactorKernel); + + int length = values.Length; + using var inputBuffer = accelerator.Allocate1D(length); + using var resultBuffer = accelerator.Allocate1D(length); + + ulong[] temp = ArrayPool.Shared.Rent(length); + values.CopyTo(temp); + inputBuffer.View.CopyFromCPU(ref temp[0], length); + kernel(length, inputBuffer.View, resultBuffer.View); + accelerator.Synchronize(); + resultBuffer.View.CopyToCPU(ref results[0], length); + ArrayPool.Shared.Return(temp); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong BinaryGcd(ulong u, ulong v) + { + if (u == 0UL) + { + return v; + } + + if (v == 0UL) + { + return u; + } + + int shift = BitOperations.TrailingZeroCount(u | v); + u >>= BitOperations.TrailingZeroCount(u); + + do + { + v >>= BitOperations.TrailingZeroCount(v); + if (u > v) + { + (u, v) = (v, u); + } + + v -= u; + } + while (v != 0UL); + + return u << shift; + } + + private static void SharesFactorKernel(Index1D index, ArrayView numbers, ArrayView results) + { + ulong n = numbers[index]; + ulong m = 63UL - (ulong)XMath.LeadingZeroCount(n); + ulong gcd = BinaryGcdGpu(n, m); + results[index] = gcd == 1UL ? (byte)0 : (byte)1; + } + + private static ulong BinaryGcdGpu(ulong u, ulong v) + { + if (u == 0UL) + { + return v; + } + + if (v == 0UL) + { + return u; + } + + int shift = XMath.TrailingZeroCount(u | v); + u >>= XMath.TrailingZeroCount(u); + + do + { + v >>= XMath.TrailingZeroCount(v); + if (u > v) + { + (u, v) = (v, u); + } + + v -= u; + } + while (v != 0UL); + + return u << shift; + } +} diff --git a/PerfectNumbers.Core/PrimesGenerator.cs b/PerfectNumbers.Core/PrimesGenerator.cs new file mode 100644 index 0000000..226b7c5 --- /dev/null +++ b/PerfectNumbers.Core/PrimesGenerator.cs @@ -0,0 +1,106 @@ +namespace PerfectNumbers.Core; + +public static class PrimesGenerator +{ + private static ulong[]? _primes; + private static ulong[]? _primesPow2; + // private static UInt128[]? _primes128; + // private static UInt128[]? _primesPow2_128; + + public static readonly ulong[] SmallPrimes = BuildSmallPrimesInternal(); + public static readonly ulong[] SmallPrimesPow2 = BuildSmallPrimesPow2Internal(); + // public static readonly UInt128[] SmallPrimes128 = BuildSmallPrimes128Internal(); + // public static readonly UInt128[] SmallPrimesPow2_128 = BuildSmallPrimesPow2_128Internal(); + + + private static ulong[] BuildSmallPrimesInternal() + { + if (_primes == null) + { + BuildSmallPrimes(); + } + + return _primes!; + } + + private static ulong[] BuildSmallPrimesPow2Internal() + { + if (_primesPow2 == null) + { + BuildSmallPrimes(); + } + + return _primesPow2!; + } + + // private static UInt128[] BuildSmallPrimes128Internal() + // { + // if (_primes == null) + // { + // BuildSmallPrimes(); + // } + + // return _primes128!; + // } + + // private static UInt128[] BuildSmallPrimesPow2_128Internal() + // { + // if (_primesPow2 == null) + // { + // BuildSmallPrimes(); + // } + + // return _primesPow2_128!; + // } + + private static void BuildSmallPrimes() + { + ulong limit = PerfectNumberConstants.PrimesLimit; + var primes = new ulong[limit]; + primes[0] = 2UL; + var primesPow2 = new ulong[limit]; + primesPow2[0] = 4UL; + + int j; + ulong i = 3UL, p, sqrt; + bool isPrime; + int primeIndex = 1; + + for (; i <= limit; i += 2UL) + { + isPrime = true; + sqrt = (ulong)Math.Sqrt(i); + for (j = 0; j < primeIndex; j++) + { + p = primes[j]; + if (p > sqrt) + { + break; + } + + if (i % p == 0UL) + { + isPrime = false; + break; + } + } + + if (isPrime) + { + primes[primeIndex] = i; + primesPow2[primeIndex] = i * i; + primeIndex++; + } + } + + Array.Resize(ref primes, primeIndex); + Array.Resize(ref primesPow2, primeIndex); + + _primes = primes; + _primesPow2 = primesPow2; + // _primes128 = Array.ConvertAll(_primes, p => (UInt128)p); + // _primesPow2_128 = Array.ConvertAll(_primesPow2, p => (UInt128)p); + + return; + } +} \ No newline at end of file diff --git a/PerfectNumbers.Core/RationalHelper.cs b/PerfectNumbers.Core/RationalHelper.cs new file mode 100644 index 0000000..5806730 --- /dev/null +++ b/PerfectNumbers.Core/RationalHelper.cs @@ -0,0 +1,35 @@ +using System.Numerics; +using PeterO.Numbers; + +namespace PerfectNumbers.Core; + +public static class RationalHelper +{ + public static ERational FromDoubleRounded(double value, int scale = 100) + { + long numerator = checked((long)Math.Round(value * scale)); + long denominator = scale; + long gcd = (long)BigInteger.GreatestCommonDivisor(numerator, denominator); + return ERational.Create(EInteger.FromInt64(numerator / gcd), EInteger.FromInt64(denominator / gcd)); + } + + public static EInteger ToEInteger(BigInteger value) + { + return EInteger.FromBytes(value.ToByteArray(), true); + } + + public static ulong FloorToUInt64(ERational value) + { + EInteger ei = value.ToEInteger(); + return ei.ToUInt64Checked(); + } + + public static ulong CeilingToUInt64(ERational value) + { + EInteger ei = value.ToEInteger(); + if (value.CompareTo(ERational.Create(ei, EInteger.One)) > 0) + ei += EInteger.One; + return ei.ToUInt64Checked(); + } +} + diff --git a/PerfectNumbers.Core/RationalNumbers.cs b/PerfectNumbers.Core/RationalNumbers.cs new file mode 100644 index 0000000..4ac6627 --- /dev/null +++ b/PerfectNumbers.Core/RationalNumbers.cs @@ -0,0 +1,8 @@ +using PeterO.Numbers; + +namespace PerfectNumbers.Core; + +public static class RationalNumbers +{ + public static ERational Two { get; } = ERational.FromInt32(2); +} diff --git a/PerfectNumbers.Core/RleBlacklist.cs b/PerfectNumbers.Core/RleBlacklist.cs new file mode 100644 index 0000000..5c9f80d --- /dev/null +++ b/PerfectNumbers.Core/RleBlacklist.cs @@ -0,0 +1,361 @@ +using System.Runtime.CompilerServices; + +namespace PerfectNumbers.Core; + +public static class RleBlacklist +{ + private static readonly object Sync = new(); + private static volatile bool _loaded; + private static HashSet? _patterns; + + public static void Load(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return; + } + + if (_loaded) + { + return; + } + + lock (Sync) + { + if (_loaded) + { + return; + } + + var set = new HashSet(StringComparer.Ordinal); + foreach (var line in File.ReadLines(path)) + { + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + var span = line.AsSpan().Trim(); + if (span.Length == 0 || span[0] == '#') + { + continue; + } + + // CSV support: Results/rle_blacklist_composite_only_end7_base100k_validated_primes1M.csv + // Header: rle_pattern,label,count_in_composites_end7_le_100k,appears_in_primes_le_1M + // Row example: "[1, 1, 3]",composite,12,False + // We only include rows with label==composite and appears_in_primes_le_1M==False. + bool handledCsv = false; + int commaIdx = span.IndexOf(','); + if (commaIdx > 0) + { + ReadOnlySpan firstField; + int startNext; + if (span[0] == '"') + { + int endQuote = span.Slice(1).IndexOf('"'); + if (endQuote > 0) + { + endQuote += 1; // adjust to original span + firstField = span.Slice(1, endQuote - 1); + // move to char after closing quote, expect comma + int after = endQuote + 1; + if (after < span.Length && span[after] == ',') + { + startNext = after + 1; + } + else + { + startNext = commaIdx + 1; + } + } + else + { + firstField = span.Slice(0, commaIdx); + startNext = commaIdx + 1; + } + } + else + { + firstField = span.Slice(0, commaIdx); + startNext = commaIdx + 1; + } + + // Parse remaining CSV fields (label, count, appears_in_primes) + // label + int comma2 = span.Slice(startNext).IndexOf(','); + if (comma2 > 0) + { + ReadOnlySpan label = span.Slice(startNext, comma2).Trim(); + int startCount = startNext + comma2 + 1; + int comma3 = span.Slice(startCount).IndexOf(','); + if (comma3 > 0) + { + // Read appears_in_primes flag + int startFlag = startCount + comma3 + 1; + ReadOnlySpan appears = span.Slice(startFlag).Trim().Trim('"'); + bool composite = label.Equals("composite", StringComparison.OrdinalIgnoreCase); + bool appearsInPrimes = appears.Equals("True", StringComparison.OrdinalIgnoreCase); + if (composite && !appearsInPrimes) + { + var key = NormalizeBracketList(firstField); + if (key != null) + { + set.Add(key); + } + } + + handledCsv = true; + } + } + } + + if (handledCsv) + { + continue; + } + + // Accept patterns as space/comma/dash-separated integers; optionally prefixed with firstbit|. + var normalized = NormalizePattern(span); + if (normalized != null) + { + set.Add(normalized); + } + } + + _patterns = set; + _loaded = true; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsLoaded() => _loaded && _patterns != null && _patterns.Count != 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Matches(ulong p) + { + if (!IsLoaded()) + { + return false; + } + + string key = BuildRleKey(p); + return _patterns!.Contains(key); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string BuildRleKey(ulong value) + { + // Build RLE from MSB to LSB runs. + int bitLen = 64 - int.CreateChecked(ulong.LeadingZeroCount(value)); + if (bitLen <= 0) + { + return ""; + } + + Span runs = stackalloc int[64]; + int runCount = 0; + int currentBit = (int)((value >> (bitLen - 1)) & 1UL); + int count = 1; + for (int i = bitLen - 2; i >= 0; i--) + { + int b = (int)((value >> i) & 1UL); + if (b == currentBit) + { + count++; + } + else + { + runs[runCount++] = count; + currentBit = b; + count = 1; + } + } + + runs[runCount++] = count; + + // Key format: first bit value; then run lengths joined by '-' + // Example: for binary 101100 -> first=1, runs: 1,1,2,2 -> "1|1-1-2-2" + // This preserves starting bit which some datasets may rely on. + var sb = StringBuilderPool.Rent(); + try + { + _ = sb.Append(((value >> (bitLen - 1)) & 1UL) == 0 ? '0' : '1'); + _ = sb.Append('|'); + for (int i = 0; i < runCount; i++) + { + if (i != 0) + { + _ = sb.Append('-'); + } + + _ = sb.Append(runs[i]); + } + + return sb.ToString(); + } + finally + { + StringBuilderPool.Return(sb); + } + } + + private static string? NormalizePattern(ReadOnlySpan span) + { + // Accept optional "firstbit|a-b-c" or just "a b c" / "a,b,c". + int pipe = span.IndexOf('|'); + int start = 0; + char first = '1'; + if (pipe >= 0) + { + if (pipe > 0 && (span[0] == '0' || span[0] == '1')) + { + first = span[0]; + } + + start = pipe + 1; + } + + Span tmp = stackalloc int[128]; + int count = 0; + int i = start; + while (i <= span.Length) + { + // Read token until separator: space/comma/dash or end + int j = i; + while (j < span.Length) + { + char c = span[j]; + if (c == ' ' || c == '\t' || c == ',' || c == ';' || c == '-') + { + break; + } + + j++; + } + + if (j > i) + { + if (int.TryParse(span.Slice(i, j - i), out int v) && v > 0) + { + tmp[count++] = v; + } + else + { + return null; + } + } + + if (j >= span.Length) + { + break; + } + + i = j + 1; + } + + if (count == 0) + { + return null; + } + + // Build normalized key + var sb = StringBuilderPool.Rent(); + try + { + _ = sb.Append(first); + _ = sb.Append('|'); + for (int k = 0; k < count; k++) + { + if (k != 0) + { + _ = sb.Append('-'); + } + + _ = sb.Append(tmp[k]); + } + + return sb.ToString(); + } + finally + { + StringBuilderPool.Return(sb); + } + } + + private static string? NormalizeBracketList(ReadOnlySpan span) + { + // Expect a JSON-like list: [1, 2, 3] + int l = span.IndexOf('['); + int r = span.LastIndexOf(']'); + if (l < 0 || r <= l) + { + return null; + } + + ReadOnlySpan inner = span.Slice(l + 1, r - l - 1); + Span tmp = stackalloc int[128]; + int count = 0; + int i = 0; + while (i <= inner.Length) + { + while (i < inner.Length && (inner[i] == ' ' || inner[i] == '\t' || inner[i] == ',')) + { + i++; + } + + if (i >= inner.Length) + { + break; + } + + int j = i; + while (j < inner.Length && inner[j] != ',' && inner[j] != ' ' && inner[j] != '\t') + { + j++; + } + + if (j > i) + { + if (int.TryParse(inner.Slice(i, j - i), out int v) && v > 0) + { + tmp[count++] = v; + } + else + { + return null; + } + } + + i = j + 1; + } + + if (count == 0) + { + return null; + } + + // Assume first bit is 1 (MSB is 1 for any positive number), so starting run is ones. + var sb = StringBuilderPool.Rent(); + try + { + _ = sb.Append('1'); + _ = sb.Append('|'); + for (int k = 0; k < count; k++) + { + if (k != 0) + { + _ = sb.Append('-'); + } + + _ = sb.Append(tmp[k]); + } + + return sb.ToString(); + } + finally + { + StringBuilderPool.Return(sb); + } + } +} diff --git a/PerfectNumbers.Core/StringBuilderPool.cs b/PerfectNumbers.Core/StringBuilderPool.cs new file mode 100644 index 0000000..8bfa4c8 --- /dev/null +++ b/PerfectNumbers.Core/StringBuilderPool.cs @@ -0,0 +1,17 @@ +using System.Collections.Concurrent; +using System.Text; + +namespace PerfectNumbers.Core; + +public static class StringBuilderPool +{ + private static readonly ConcurrentQueue _stringBuilderPool = new(); + + public static StringBuilder Rent() => _stringBuilderPool.TryDequeue(out var sb) ? sb : new(); + + public static void Return(StringBuilder sb) + { + _ = sb.Clear(); + _stringBuilderPool.Enqueue(sb); + } +} \ No newline at end of file diff --git a/PerfectNumbers.Core/TextFileWriter.cs b/PerfectNumbers.Core/TextFileWriter.cs new file mode 100644 index 0000000..1abd842 --- /dev/null +++ b/PerfectNumbers.Core/TextFileWriter.cs @@ -0,0 +1,77 @@ +namespace PerfectNumbers.Core; + +/// +/// Provides thread-safe file writing for both text and binary data. +/// +public sealed class TextFileWriter : IDisposable +{ + private readonly FileStream _stream; + private StreamWriter? _textWriter; + private BinaryWriter? _binaryWriter; + private readonly object _lock = new(); + + public TextFileWriter(string path, FileMode mode) + { + var options = new FileStreamOptions + { + Access = FileAccess.Write, + Mode = mode, + Share = FileShare.Read + }; + _stream = new FileStream(path, options); + } + + public long Position + { + get { lock (_lock) { return _stream.Position; } } + } + + private StreamWriter TextWriter => _textWriter ??= new StreamWriter(_stream, leaveOpen: true); + private BinaryWriter BinaryWriter => _binaryWriter ??= new BinaryWriter(_stream, System.Text.Encoding.UTF8, leaveOpen: true); + + /// + /// Clears the file by truncating it to zero length. + /// + public void Clear() + { + lock (_lock) + { + _stream.SetLength(0); + _stream.Position = 0; + _textWriter?.Flush(); + _binaryWriter?.Flush(); + } + } + + /// + /// Writes a line of text to the file. + /// + public void WriteLine(string line) + { + lock (_lock) + { + TextWriter.WriteLine(line); + TextWriter.Flush(); + } + } + + /// + /// Executes a binary writing action within the internal lock. + /// + public void WriteBinary(Action action) + { + lock (_lock) + { + action(BinaryWriter); + BinaryWriter.Flush(); + } + } + + public void Dispose() + { + _binaryWriter?.Dispose(); + _textWriter?.Dispose(); + _stream.Dispose(); + } +} + diff --git a/PerfectNumbers.Core/UInt128Extensions.cs b/PerfectNumbers.Core/UInt128Extensions.cs new file mode 100644 index 0000000..85670dd --- /dev/null +++ b/PerfectNumbers.Core/UInt128Extensions.cs @@ -0,0 +1,251 @@ +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace PerfectNumbers.Core; + +public static class UInt128Extensions +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static UInt128 BinaryGcd(this UInt128 u, UInt128 v) + { + UInt128 zero = UInt128.Zero; + if (u == zero) + { + return v; + } + + if (v == zero) + { + return u; + } + + int shift = CountTrailingZeros(u | v); + u >>= CountTrailingZeros(u); + + do + { + v >>= CountTrailingZeros(v); + if (u > v) + { + (u, v) = (v, u); + } + + v -= u; + } + while (v != zero); + + return u << shift; + } + + public static ulong CalculateOrder(this UInt128 q) + { + if (q <= UInt128Numbers.Two) + { + return 0UL; + } + + UInt128 one = UInt128.One; + UInt128 phi = q - one; + if (phi > ulong.MaxValue) + { + throw new NotImplementedException("Such big values are not yet supported"); + } + + ulong order = (ulong)phi; + ulong[] smallPrimes = PrimesGenerator.SmallPrimes, + smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2; + + int i = 0, primesLength = smallPrimes.Length; + UInt128 cycle = MersenneDivisorCycles.GetCycle(q); + ulong prime, temp; + for (; i < primesLength; i++) + { + if (smallPrimesPow2[i] > order) + { + break; + } + + prime = smallPrimes[i]; + while (order % prime == 0UL) + { + temp = order / prime; + if (temp.PowModWithCycle(q, cycle) == one) + { + order = temp; + } + else + { + break; + } + } + } + + return order; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CountTrailingZeros(this UInt128 value) + { + ulong valuePart = (ulong)value; + if (valuePart != 0UL) + { + return BitOperations.TrailingZeroCount(valuePart); + } + + return 64 + BitOperations.TrailingZeroCount((ulong)(value >> 64)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsPrimeCandidate(this UInt128 n) + { + UInt128 p, zero = UInt128.Zero; + + ulong[] smallPrimes = PrimesGenerator.SmallPrimes; + ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2; + ulong i, smallPrimesCount = (ulong)smallPrimes.Length; + for (i = 0UL; i < smallPrimesCount; i++) + { + p = smallPrimes[i]; + if (smallPrimesPow2[i] > n) + { + break; + } + + if (n % p == zero) + { + return n == p; + } + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Mod10_8_5_3(this UInt128 value, out ulong mod10, out ulong mod8, out ulong mod5, out ulong mod3) + { + UInt128 zero = UInt128.Zero; + if (value == zero) + { + mod3 = 0UL; + mod5 = 0UL; + mod8 = 0UL; + mod10 = 0UL; + return; + } + + UInt128 high = value >> 64; + ulong result = (ulong)value; + mod8 = result & 7UL; + // 2^64 ≡ 1 (mod 3) + ulong modRem = (result % 3UL) + ((ulong)high % 3UL); + mod3 = modRem >= 3UL ? modRem - 3UL : modRem; + // 2^64 ≡ 1 (mod 5) + modRem = (result % 5UL) + ((ulong)high % 5UL); + mod5 = modRem >= 5UL ? modRem - 5UL : modRem; + + while (high != zero) + { + // 2^64 ≡ 6 (mod 10) + result = (result + (ulong)high * 6UL) % 10UL; + high >>= 64; + } + + mod10 = result % 10UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static UInt128 ModPow(this UInt128 value, UInt128 exponent, UInt128 modulus) + { + UInt128 result, one = result = UInt128.One, + zero = UInt128.Zero, + baseValue = value % modulus; + + while (exponent != zero) + { + if ((exponent & one) != zero) + { + result = MulMod(result, baseValue, modulus); + } + + baseValue = MulMod(baseValue, baseValue, modulus); + exponent >>= 1; + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Mod10(this UInt128 value) + { + UInt128 zero = UInt128.Zero; + if (value == zero) + return 0UL; + + ulong result = (ulong)value; + UInt128 high = value >> 64; + + while (high != zero) + { + // 2^64 ≡ 6 (mod 10) + result = (result + (ulong)high * 6UL) % 10UL; + high >>= 64; + } + + return result % 10UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Mod8(this UInt128 value) => (ulong)value & 7UL; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Mod3(this UInt128 value) + { + // 2^64 ≡ 1 (mod 3) + ulong rem = ((ulong)value % 3UL) + ((ulong)(value >> 64) % 3UL); + return rem >= 3UL ? rem - 3UL : rem; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Mod5(this UInt128 value) + { + // 2^64 ≡ 1 (mod 5) + ulong rem = ((ulong)value % 5UL) + ((ulong)(value >> 64) % 5UL); + return rem >= 5UL ? rem - 5UL : rem; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static UInt128 Mul64(this UInt128 a, UInt128 b) + { + ulong aLow = (ulong)a; + ulong bLow = (ulong)b; + return ((UInt128)(aLow * (ulong)(b >> 64) + aLow.MulHigh(bLow)) << 64) | (aLow * bLow); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static UInt128 MulMod(this UInt128 a, UInt128 b, UInt128 modulus) + { + UInt128 zero, result = zero = UInt128.Zero, one = UInt128.One; + + while (b != zero) + { + if ((b & one) != zero) + { + result += a; + if (result >= modulus) + { + result -= modulus; + } + } + + a <<= 1; + if (a >= modulus) + { + a -= modulus; + } + + b >>= 1; + } + + return result; + } +} \ No newline at end of file diff --git a/PerfectNumbers.Core/UInt128Numbers.cs b/PerfectNumbers.Core/UInt128Numbers.cs new file mode 100644 index 0000000..bd83f68 --- /dev/null +++ b/PerfectNumbers.Core/UInt128Numbers.cs @@ -0,0 +1,10 @@ +namespace PerfectNumbers.Core +{ + public static class UInt128Numbers + { + public static readonly UInt128 Two = 2UL; + public static readonly UInt128 Four = 4UL; + public static readonly UInt128 SixtyFour = 64UL; + public static readonly UInt128 OneHundredTwentySeven = 127UL; + } +} \ No newline at end of file diff --git a/PerfectNumbers.Core/ULongExtensions.cs b/PerfectNumbers.Core/ULongExtensions.cs new file mode 100644 index 0000000..cc7df4d --- /dev/null +++ b/PerfectNumbers.Core/ULongExtensions.cs @@ -0,0 +1,423 @@ +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace PerfectNumbers.Core; + +public static class ULongExtensions +{ + public static ulong CalculateOrder(this ulong q) + { + if (q <= 2UL) + { + return 0UL; + } + + ulong order = q - 1UL; + ulong[] smallPrimes = PrimesGenerator.SmallPrimes; + ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2; + + int i = 0, primesLength = smallPrimes.Length; + ulong prime, temp; + UInt128 q128 = q, + cycle = MersenneDivisorCycles.GetCycle(q128); + + for (; i < primesLength; i++) + { + prime = smallPrimes[i]; + if (smallPrimesPow2[i] > order) + { + break; + } + + while (order % prime == 0UL) + { + temp = order / prime; + if (temp.PowModWithCycle(q128, cycle) == UInt128.One) + { + order = temp; + } + else + { + break; + } + } + } + + return order; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong FastDiv64(this ulong value, ulong divisor, ulong mul) + { + ulong q = value.MulHigh(mul); + if ((UInt128)value - q.Mul64(divisor) >= divisor) + { + q++; + } + + return q; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsPrimeCandidate(this ulong n) + { + int i = 0; + ulong p; + ulong[] smallPrimes = PrimesGenerator.SmallPrimes; + ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2; + int len = smallPrimes.Length; + for (; i < len; i++) + { + if (smallPrimesPow2[i] > n) + { + break; + } + + p = smallPrimes[i]; + if ((n % p) == 0UL) + { + return n == p; + } + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Mod10(ulong value) + { + ulong quotient = (ulong)(((UInt128)value * 0xCCCCCCCCCCCCCCCDUL) >> 67); + return value - quotient * 10UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static UInt128 Mul64(this ulong a, ulong b) => ((UInt128)a.MulHigh(b) << 64) | (UInt128)(a * b); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong MulHigh(this ulong x, ulong y) + { + ulong xLow = (uint)x; + ulong xHigh = x >> 32; + ulong yLow = (uint)y; + ulong yHigh = y >> 32; + + ulong w1 = xLow * yHigh; + ulong w2 = xHigh * yLow; + ulong carry = + (((xLow * yLow) >> 32) + + (uint)w1 + + (uint)w2) >> 32; + + return (xHigh * yHigh) + (w1 >> 32) + (w2 >> 32) + carry; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong ModPow64(this ulong value, ulong exponent, ulong modulus) + { + ulong result = 1UL; + ulong baseValue = value % modulus; + + while (exponent != 0UL) + { + if ((exponent & 1UL) != 0UL) + { + result = MulMod64(result, baseValue, modulus); + } + + baseValue = MulMod64(baseValue, baseValue, modulus); + exponent >>= 1; + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong MulMod64(this ulong a, ulong b, ulong modulus) => (ulong)(UInt128)(((a % modulus) * (b % modulus)) % modulus); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static UInt128 PowMod(this ulong exponent, UInt128 modulus) + { + // Return 1 because 2^0 = 1 + if (exponent == 0UL) + return UInt128.One; + + // Any number mod 1 is 0 + if (modulus == UInt128.One) + return UInt128.Zero; + + UInt128 result; + // For small exponents, do classic method + if (exponent < 64 || modulus < 4) + { + result = UInt128.One; + for (ulong i = 0; i < exponent; i++) + { + result <<= 1; + if (result >= modulus) + result -= modulus; + } + + return result; + } + + // Special case: if modulus is a power of two, use bitmasking for efficiency and correctness + if ((modulus & (modulus - 1)) == 0) + { + result = UInt128.One << (int)(exponent & 127); + return result & (modulus - 1); + } + + // For general modulus, use classic left-to-right binary method for 2^exponent mod modulus + UInt128 resultBin = UInt128.One; + for (ulong i = 0; i < exponent; i++) + { + resultBin <<= 1; + if (resultBin >= modulus) + resultBin -= modulus; + } + + return resultBin; + } + + /// + /// Computes 2^exponent mod modulus using a known cycle length. + /// + public static UInt128 PowModWithCycle(this ulong exponent, UInt128 modulus, ulong cycleLength) + { + // Return 1 because 2^0 = 1 + if (exponent == 0UL) + return UInt128.One; + + // Any number mod 1 is 0 + if (modulus == UInt128.One) + return UInt128.Zero; + + // For small exponents, do classic method + UInt128 result; + if (exponent < 64 || modulus < 4) + { + result = UInt128.One; + for (ulong i = 0; i < exponent; i++) + { + result <<= 1; + if (result >= modulus) + result -= modulus; + } + + return result; + } + + // Special case: if modulus is a power of two, use bitmasking for efficiency and correctness + if ((modulus & (modulus - 1)) == 0) + { + result = UInt128.One << (int)(exponent & 127); + return result & (modulus - 1); + } + + // For general modulus, use classic left-to-right binary method for 2^exponent mod modulus + result = UInt128.One; + UInt128 rotations = exponent % cycleLength; + for (ulong i = 0; i < rotations; i++) + { + result <<= 1; + if (result >= modulus) + result -= modulus; + } + + return result; + } + + /// + /// Computes 2^exponent mod modulus using a known cycle length. + /// + public static UInt128 PowModWithCycle(this ulong exponent, UInt128 modulus, UInt128 cycleLength) + { + // Return 1 because 2^0 = 1 + if (exponent == UInt128.Zero) + return UInt128.One; + + // Any number mod 1 is 0 + if (modulus == UInt128.One) + return UInt128.Zero; + + // For small exponents, do classic method + UInt128 result; + if (exponent < UInt128Numbers.SixtyFour || modulus < UInt128Numbers.Four) + { + result = UInt128.One; + for (ulong i = 0; i < exponent; i++) + { + result <<= 1; + if (result >= modulus) + result -= modulus; + } + + return result; + } + + // Special case: if modulus is a power of two, use bitmasking for efficiency and correctness + if ((modulus & (modulus - UInt128.One)) == UInt128.Zero) + { + result = UInt128.One << (int)(exponent & UInt128Numbers.OneHundredTwentySeven); + return result & (modulus - 1); + } + + // For general modulus, use classic left-to-right binary method for 2^exponent mod modulus + result = UInt128.One; + UInt128 rotations = exponent % cycleLength; + for (UInt128 i = 0; i < rotations; i++) + { + result <<= 1; + if (result >= modulus) + result -= modulus; + } + + return result; + } + + /// + /// Computes 2^exponent mod modulus using a known cycle length. + /// + public static UInt128 PowModWithCycle(this UInt128 exponent, UInt128 modulus, UInt128 cycleLength) + { + // Return 1 because 2^0 = 1 + if (exponent == UInt128.Zero) + return UInt128.One; + + // Any number mod 1 is 0 + if (modulus == UInt128.One) + return UInt128.Zero; + + UInt128 result; + + // For small exponents, do classic method + if (exponent < UInt128Numbers.SixtyFour || modulus < UInt128Numbers.Four) + { + result = UInt128.One; + for (ulong i = 0; i < exponent; i++) + { + result <<= 1; + if (result >= modulus) + result -= modulus; + } + + return result; + } + + // Special case: if modulus is a power of two, use bitmasking for efficiency and correctness + if ((modulus & (modulus - UInt128.One)) == UInt128.Zero) + { + result = UInt128.One << (int)(exponent & UInt128Numbers.OneHundredTwentySeven); + return result & (modulus - 1); + } + + // For general modulus, use classic left-to-right binary method for 2^exponent mod modulus + result = UInt128.One; + UInt128 rotations = exponent % cycleLength; + for (UInt128 i = 0; i < rotations; i++) + { + result <<= 1; + if (result >= modulus) + result -= modulus; + } + + return result; + } + + /// + /// Computes 2^exponent mod modulus using iterative CRT composition from mod 10 up to modulus. + /// Only for modulus >= 10 and reasonable size. + /// + public static UInt128 PowModCrt(this ulong exponent, UInt128 modulus, MersenneDivisorCycles cycles) + { + if (modulus < 10) + return PowMod(exponent, modulus); // fallback to classic + + // Use cycle length 4 for mod 10 + UInt128 result = PowModWithCycle(exponent, 10, 4); + + UInt128 currentModulus = 10; + for (UInt128 m = 11; m <= modulus; m++) + { + UInt128 cycle = MersenneDivisorCycles.GetCycle(m); + UInt128 remM = cycle > 0 + ? PowModWithCycle(exponent, m, cycle) + : PowMod(exponent, m); + + // Solve x ≡ result mod currentModulus + // x ≡ remM mod m + // Find x mod (currentModulus * m) + // Since currentModulus and m are coprime, use CRT: + // x = result + currentModulus * t, where t ≡ (remM - result) * inv(currentModulus, m) mod m + + UInt128 inv = ModInverse(currentModulus, m); + UInt128 t = ((remM + m - (result % m)) * inv) % m; + result += currentModulus * t; + currentModulus *= m; + + if (currentModulus >= modulus) + break; + } + + return result % modulus; + } + + // Helper: modular inverse (extended Euclidean algorithm) + private static UInt128 ModInverse(UInt128 a, UInt128 m) + { + UInt128 m0 = m, t, q; + UInt128 x0 = 0, x1 = 1; + if (m == 1) return 0; + while (a > 1) + { + q = a / m; + t = m; + m = a % m; a = t; + t = x0; + x0 = x1 - q * x0; + x1 = t; + } + + if (x1 < 0) x1 += m0; + + return x1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool SharesFactorWithExponentMinusOne(this ulong exponent) + { + ulong value = exponent - 1UL; + value >>= BitOperations.TrailingZeroCount(value); + ulong[] smallPrimes = PrimesGenerator.SmallPrimes; + ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2; + int i = 1, smallPrimesLength = smallPrimes.Length; + ulong prime; + + for (; i < smallPrimesLength && smallPrimesPow2[i] <= value; i++) + { + prime = smallPrimes[i]; + if (value % prime != 0UL) + { + continue; + } + + if (exponent % prime.CalculateOrder() == 0UL) + { + return true; + } + + do + { + value /= prime; + } + while (value % prime == 0UL); + } + + if (value > 1UL && exponent % value.CalculateOrder() == 0UL) + { + return true; + } + + return false; + } +} \ No newline at end of file