Prime Hunt in Big-Number Territory
Scans through large integers (default: 1500-bit) looking for probable primes using Miller-Rabin, distributed across Knitting workers. This is a long-running compute pipeline — it scans windows of candidates, prints progress, and keeps going until you stop it.
How it works
Section titled “How it works”- The host picks a starting odd integer in the chosen bit-width.
- It scans windows of 10,000,000 odd candidates, printing progress after each window.
- Each window is split across threads — threads scan disjoint candidate sequences (no overlap, no duplicated work).
- Each worker runs
candidate -> Miller-Rabin -> next candidate -> ...in a tight loop. - If any worker finds a probable prime, it reports it for that window.
- The process runs continuously until Ctrl+C.
The scanning uses an interleaved stride pattern: thread 0 checks start + 0, start + 2T, start + 4T, …, thread 1 checks start + 2, start + 2 + 2T, …, and so on. This guarantees full coverage with zero overlap.
Install
Section titled “Install”deno add --npm jsr:@vixeny/knittingnpx jsr add @vixeny/knitting# pnpm 10.9+pnpm add jsr:@vixeny/knitting
# fallback (older pnpm)pnpm dlx jsr add @vixeny/knitting# yarn 4.9+yarn add jsr:@vixeny/knitting
# fallback (older yarn)yarn dlx jsr add @vixeny/knittingbunx jsr add @vixeny/knittingbun src/run_prime_hunt.ts --threads 4 --bits 1500 --total 10000000 --chunk 200000 --rounds 8deno run -A src/run_prime_hunt.ts --threads 4 --bits 1500 --total 10000000 --chunk 200000 --rounds 8npx tsx src/run_prime_hunt.ts --threads 4 --bits 1500 --total 10000000 --chunk 200000 --rounds 8Expected output:
starting at 1500-bit odd integer (4 threads, 8 MR rounds)window 1: scanned 10,000,000 candidates in 4.2s -- no prime foundwindow 2: scanned 10,000,000 candidates in 4.1s -- no prime foundwindow 3: scanned 10,000,000 candidates in 4.3s -- OK probable prime found! digits: 452 hex prefix: 0xA3F7...import { createPool, isMain } from "@vixeny/knitting";import { scanForProbablePrime } from "./prime_scan.ts";
function intArg(name: string, fallback: number) { const i = process.argv.indexOf(`--${name}`); if (i !== -1 && i + 1 < process.argv.length) { const v = Number(process.argv[i + 1]); if (Number.isFinite(v) && v > 0) return Math.floor(v); } return fallback;}
const THREADS = intArg("threads", 4);const BITS = intArg("bits", 1500);const WINDOW = intArg("window", 10_000_000);const CHUNK = intArg("chunk", 500_000);const ROUNDS = intArg("rounds", 10);
function xorshift32(s: number): number { s |= 0; s ^= s << 13; s ^= s >>> 17; s ^= s << 5; return s | 0;}
function makeRandomOdd(bits: number, seed: number): bigint { // Build a BigInt from 3x 32-bit chunks, mask to bits, set top bit, make odd. let s = seed | 0; let x = 0n; for (let k = 0; k < 3; k++) { s = xorshift32(s); x = (x << 32n) | BigInt(s >>> 0); } const mask = (1n << BigInt(bits)) - 1n; x &= mask; x |= 1n << BigInt(bits - 1); x |= 1n; return x;}
const seedBase = (Date.now() | 0) ^ 0x9e3779b9;let windowStartOdd = makeRandomOdd(BITS, seedBase);
const { call, shutdown } = createPool({ threads: THREADS, balancer: "firstIdle",})({ scanForProbablePrime });
let stopping = false;process.on("SIGINT", () => { if (stopping) return; stopping = true; console.log("\nCtrl+C received. Shutting down..."); shutdown(); process.exit(0);});
function splitCounts(total: number, parts: number): number[] { const base = Math.floor(total / parts); const rem = total % parts; const out = new Array(parts); for (let i = 0; i < parts; i++) out[i] = base + (i < rem ? 1 : 0); return out;}
async function scanOneWindow(): Promise< { hit: string | null; tested: number }> { // We interleave odds across threads: thread i tests start+2i, start+2i+2T, ... const stepNum = 2 * THREADS;
// Divide WINDOW across threads, and within each thread further divide into CHUNK-sized tasks. const perThread = splitCounts(WINDOW, THREADS);
let bestHit: string | null = null; let tested = 0;
// For each thread, we run sequential “subtasks” so each thread covers its share of WINDOW. // But all threads run in parallel each wave. const subTasksPerThread = perThread.map((c) => Math.ceil(c / CHUNK)); const maxSubs = Math.max(...subTasksPerThread);
for (let sub = 0; sub < maxSubs; sub++) { const jobs: Promise<[number, string, number]>[] = [];
for (let t = 0; t < THREADS; t++) { const threadTotal = perThread[t]; const startAt = sub * CHUNK; if (startAt >= threadTotal) continue;
const count = Math.min(CHUNK, threadTotal - startAt);
// offset in "odd steps": 2*t + 2*THREADS*startAt const offsetNum = 2 * t + stepNum * startAt;
jobs.push( call.scanForProbablePrime([ windowStartOdd.toString(), count, stepNum, offsetNum, ROUNDS, ]), );
tested += count; }
const results = await Promise.all(jobs);
// If any job found a hit, keep the smallest hit (nice for consistency) for (const [found, primeStr] of results) { if (found) { if (bestHit === null) bestHit = primeStr; else { // compare as BigInt safely const a = BigInt(bestHit); const b = BigInt(primeStr); if (b < a) bestHit = primeStr; } } } }
return { hit: bestHit, tested };}
async function main() { console.log("Prime hunt (probable primes via Miller–Rabin)"); console.log( "threads:", THREADS, "bits:", BITS, "window:", WINDOW.toLocaleString(), "chunk:", CHUNK.toLocaleString(), "rounds:", ROUNDS, ); console.log("start :", windowStartOdd.toString()); console.log("mode : infinite windows (Ctrl+C to stop)");
let windowsDone = 0; let totalTested = 0n;
while (true) { const { hit, tested } = await scanOneWindow(); windowsDone++; totalTested += BigInt(tested);
if (hit) { console.log( `[window ${windowsDone}] +${tested.toLocaleString()} tested (total ${totalTested.toString()}) | HIT: ${hit}`, ); } else { console.log( `[window ${windowsDone}] +${tested.toLocaleString()} tested (total ${totalTested.toString()}) | no hit (Ctrl+C to stop)`, ); }
// Move start forward by WINDOW odd candidates (i.e., +2*WINDOW) windowStartOdd += 2n * BigInt(WINDOW); }}
if (isMain) { main().finally(shutdown);}import { task } from "@vixeny/knitting";
/** * Payload-safe: * - args: strings + numbers only * - return: numbers + strings only * This avoids any accidental BigInt/number mixing at the transport boundary. */
// args: [startOddStr, count, stepNum, offsetNum, rounds]export const scanForProbablePrime = task< [string, number, number, number, number], // ret: [found(0/1), primeStrOrEmpty, tested] [number, string, number]>({ f: ([startOddStr, count, stepNum, offsetNum, rounds]) => { // Convert once, keep everything BigInt inside. let x = BigInt(startOddStr) + BigInt(offsetNum); if ((x & 1n) === 0n) x += 1n;
const step = BigInt(stepNum); const rds = rounds | 0;
for (let i = 0; i < count; i++) { if (isProbablePrime(x, rds)) return [1, x.toString(), i + 1]; x += step; } return [0, "", count]; },});
function modPow(base: bigint, exp: bigint, mod: bigint): bigint { let r = 1n; let b = base % mod; let e = exp; // must be bigint
while (e > 0n) { if ((e & 1n) === 1n) r = (r * b) % mod; e >>= 1n; if (e) b = (b * b) % mod; } return r;}
const small = [3n, 5n, 7n, 11n, 13n, 17n, 19n, 23n, 29n, 31n, 37n];const bases = [2n, 325n, 9375n, 28178n, 450775n, 9780504n, 1795265022n];
function isProbablePrime(n: bigint, rounds: number): boolean { if (n < 2n) return false; if (n === 2n || n === 3n) return true; if ((n & 1n) === 0n) return false;
// quick small-prime filter for (const p of small) { if (n === p) return true; if (n % p === 0n) return false; }
// n-1 = d * 2^s let d = n - 1n; let s = 0; while ((d & 1n) === 0n) { d >>= 1n; s++; }
// good practical bases (still "probable prime" for 65-bit+)
const rds = rounds | 0; for (let i = 0; i < rds; i++) { const a = (bases[i % bases.length] % (n - 3n)) + 2n; // [2, n-2] let x = modPow(a, d, n);
if (x === 1n || x === n - 1n) continue;
let composite = true; for (let r = 1; r < s; r++) { x = (x * x) % n; if (x === n - 1n) { composite = false; break; } } if (composite) return false; }
return true;}The math behind it
Section titled “The math behind it”Why primes are findable: Around a number of size N, prime density is roughly 1/ln(N). Primes get rarer as numbers get bigger, but not impossibly rare — even at 1500 bits, you’ll find them.
Probable vs proven primes: Miller-Rabin is a probabilistic test. With 8 rounds, the false-positive rate is vanishingly small (less than 4^-8). In practice, this is what cryptographic libraries use for key generation.
Tuning --rounds: More rounds = higher confidence, but more compute per candidate. 8 rounds is a solid default. You can go higher if you need cryptographic-grade confidence.
CLI knobs
Section titled “CLI knobs”--bits— bit-width of candidates (higher = harder, sparser primes)--threads— worker count--total— candidates per window (controls progress granularity)--chunk— candidates per task (controls scheduling granularity)--rounds— Miller-Rabin rounds (controls confidence vs speed)
Things to try
Section titled “Things to try”- Lower
--bitsto 65 and watch primes appear frequently. - Increase
--roundsto 20 and measure the throughput impact. - Compare different
--chunkvalues — too small and dispatch overhead dominates, too large and load balance suffers. - Modify the task to search for twin primes (p and p+2 both prime) or Sophie Germain primes (p and 2p+1 both prime).