research: preserve solver experiments and reproducible benchmarks
This commit is contained in:
parent
12e4266950
commit
e93bc20b0a
85 changed files with 518443 additions and 0 deletions
239
research/BENCHMARK.md
Normal file
239
research/BENCHMARK.md
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
# Fixed-position benchmark
|
||||
|
||||
Date: 2026-10-03
|
||||
|
||||
Hardware used for this run:
|
||||
|
||||
- GPU: NVIDIA GeForce RTX 3060 Laptop GPU, compute capability 8.6
|
||||
- CPU: the current WSL host, using 16 worker threads
|
||||
- CUDA toolkit: 12.0
|
||||
|
||||
## Workload
|
||||
|
||||
Find every lower-48 seed producing a solid 5x5 slime pattern whose top-left
|
||||
chunk is `(51069, 419124)`. The known answer contains 24 seeds.
|
||||
|
||||
The new solver performs the complete workload and checks its final candidates
|
||||
with Java-compatible `nextInt(10)`. Running a complete old-style `2^48` scan is
|
||||
unnecessarily expensive, so the brute-force baselines scan large contiguous
|
||||
ranges, report throughput, and linearly project the full-space time.
|
||||
|
||||
## Results
|
||||
|
||||
| Implementation | Measured work | Measured time | Projected/complete time | Speedup |
|
||||
|---|---:|---:|---:|---:|
|
||||
| Old brute-force CPU | `2^32` seeds | 1.851 s | 121,290 s (33.7 h) | 1x |
|
||||
| New solver CPU | complete, 24/24 results | 0.61 s wall | 0.61 s | ~199,000x |
|
||||
| Old brute-force CUDA | `2^36` seeds | 1.256 s | 5,145 s (85.7 min) | 1x |
|
||||
| New solver CUDA | complete, 24/24 results | 59.6 ms kernel | 59.6 ms | ~86,300x |
|
||||
| New solver CUDA, end-to-end | complete, 24/24 results | 0.36 s wall | 0.36 s | ~14,300x |
|
||||
|
||||
The end-to-end CUDA row includes process startup, CUDA context creation, CPU
|
||||
enumeration of the 84 lower-18 roots, allocation, copies, kernel execution, and
|
||||
readback. The kernel-only comparison isolates the algorithms on the GPU.
|
||||
|
||||
The new CUDA kernel is about 10.2x faster than the new 16-thread CPU solver for
|
||||
this particular 5x5 position. The old brute-force CUDA baseline is about 23.6x
|
||||
faster than its CPU counterpart.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
make -C research
|
||||
|
||||
# New complete CPU solver
|
||||
/usr/bin/time -f 'wall_seconds=%e' \
|
||||
research/slime_pattern_cpp search \
|
||||
--x 51069 --z 419124 -n 5 -p 16 --max-results 0
|
||||
|
||||
# Old-style CPU throughput
|
||||
research/slime_bruteforce_cpp_bench \
|
||||
--x 51069 --z 419124 -n 5 --count 4294967296 -p 16
|
||||
|
||||
# New complete CUDA solver
|
||||
research/slime_pattern_cuda \
|
||||
--x 51069 --z 419124 -n 5 --threads 256 --max-results 0
|
||||
|
||||
# Old-style CUDA throughput
|
||||
research/slime_bruteforce_cuda_bench \
|
||||
--x 51069 --z 419124 -n 5 --count 68719476736
|
||||
```
|
||||
|
||||
## Cross-check against the original root CUDA executable
|
||||
|
||||
The repository's original `slime-cuda` was also run directly over one billion
|
||||
seeds with `-n 5 -d 25 -r 1`. It took 0.439 seconds. That program checks nine
|
||||
top-left positions per seed, so it is not the same fixed-position workload.
|
||||
Normalized as position/seed pairs it processed about 20.5 billion pairs/s,
|
||||
whereas the dedicated old-style benchmark processed about 54.7 billion
|
||||
seeds/s. Using the faster dedicated baseline therefore makes the reported
|
||||
new-vs-old acceleration conservative.
|
||||
|
||||
## Interpretation
|
||||
|
||||
Most of the acceleration is algorithmic, not CUDA-specific:
|
||||
|
||||
- brute force visits all `2^48 = 281,474,976,710,656` seed values;
|
||||
- the known position has only 84 viable lower-18 roots;
|
||||
- those expand to 11,010,048 lower-35-bit values;
|
||||
- each low-bit value's remaining 13 bits are solved by threshold/residue intersection,
|
||||
rather than enumerating another 8192 values.
|
||||
|
||||
Results depend strongly on pattern selectivity. Sparse patterns and patterns
|
||||
with few required-slime cells retain more roots and can produce many more upper
|
||||
solutions, reducing the speedup. These numbers should not be extrapolated to
|
||||
world-border position enumeration, which remains a separate problem.
|
||||
|
||||
## Expanded 64-position run
|
||||
|
||||
The initial result above is useful for correctness, but one known hit is not a
|
||||
performance distribution. A second, deterministic stratified run therefore
|
||||
used 64 positions:
|
||||
|
||||
- 32 positions at and around the known difficult coordinate, deliberately
|
||||
retaining a range of nonzero lower-18 root counts;
|
||||
- 32 positions covering the origin, four world-border corners, large signed
|
||||
coordinates, and pseudorandom world-border positions (seed `20261004`).
|
||||
|
||||
For every position the new CPU and CUDA solvers completed the whole lower-48
|
||||
search. The old CPU baseline actually checked `2^28` seeds per position and
|
||||
the old CUDA baseline checked `2^34` per position. Aggregate measured old-style
|
||||
work was therefore:
|
||||
|
||||
```text
|
||||
CPU: 64 * 2^28 = 17,179,869,184 seeds
|
||||
CUDA: 64 * 2^34 = 1,099,511,627,776 seeds (2^40)
|
||||
```
|
||||
|
||||
CPU and CUDA agreed on the result count for all 64 positions. Only the known
|
||||
coordinate had matches (24); every other sampled position had zero.
|
||||
|
||||
### Baseline stability
|
||||
|
||||
Across the 64 coordinates, brute-force throughput was:
|
||||
|
||||
| Baseline | Mean | Median | Range | 95% CI of mean |
|
||||
|---|---:|---:|---:|---:|
|
||||
| CPU, 16 threads | 2.187B seeds/s | 2.171B | 1.665B–2.686B | 2.139B–2.235B |
|
||||
| RTX 3060 CUDA | 53.81B seeds/s | 53.76B | 49.36B–58.13B | 53.46B–54.15B |
|
||||
|
||||
The CUDA coefficient of variation was only 2.6%, which supports linear
|
||||
projection of its complete `2^48` time to roughly 87 minutes.
|
||||
|
||||
### Root and runtime distribution
|
||||
|
||||
The full set contained 42 positions with zero lower-18 roots and 22 with
|
||||
nonzero roots. Zero-root positions are a real algorithmic result—the initial
|
||||
`2^18` parity pass proves that their entire lower-48 space is impossible—but
|
||||
they are excluded from the speedup table below to avoid inflating it.
|
||||
|
||||
For the 22 nonzero-root positions:
|
||||
|
||||
| Metric | Minimum | Median | Mean | P95 | Maximum |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| lower-18 roots | 1 | 20 | 28.6 | 84 | 152 |
|
||||
| lower-35-bit values | 131,072 | 2,621,440 | 3,753,425 | 11,010,048 | 19,922,944 |
|
||||
| New CPU search time | 6.50 ms | 166.6 ms | 225.3 ms | 606.5 ms | 1.153 s |
|
||||
| New CUDA kernel time | 1.64 ms | 14.61 ms | 20.85 ms | 60.81 ms | 107.6 ms |
|
||||
| CPU algorithm speedup | 107,104x | 774,825x | 4,444,739x | 18,288,517x | 19,220,240x |
|
||||
| CUDA algorithm speedup | 48,055x | 369,354x | 969,525x | 3,120,648x | 3,252,226x |
|
||||
|
||||
The deliberately hardest sampled position was `(51069, 419125)`: 152 roots,
|
||||
19,922,944 lower-35-bit values, 1.153 seconds on CPU, and 107.6 ms on CUDA. Even
|
||||
there, the conservative same-platform acceleration was 107,104x on CPU and
|
||||
48,055x on CUDA.
|
||||
|
||||
The expanded run is reproducible with:
|
||||
|
||||
```bash
|
||||
python3 research/benchmark_suite.py \
|
||||
--samples 64 \
|
||||
--cpu-threads 16 \
|
||||
--cpu-old-count 268435456 \
|
||||
--cuda-old-count 17179869184 \
|
||||
--cuda-threads 256 \
|
||||
--output /tmp/slime_benchmark_64.json
|
||||
```
|
||||
|
||||
Times in this section intentionally compare the measured search/kernel regions.
|
||||
CUDA process startup and context creation are not included; the earlier
|
||||
single-position end-to-end row shows their effect. The upper solver still has
|
||||
the documented eight-value Java rejection-tail completeness caveat.
|
||||
|
||||
## Coordinate-side outer-sum CUDA experiment
|
||||
|
||||
The follow-up coordinate scanner fixes world seed `61692151016558` and checks
|
||||
an `8193 x 8193` rectangle containing the known 5x5 top-left position:
|
||||
|
||||
```bash
|
||||
./research/slime_position_cuda --repeats 5
|
||||
```
|
||||
|
||||
Each invocation runs two kernels and requires identical sorted hit lists:
|
||||
|
||||
- direct: one CUDA thread per position, evaluating the full coordinate
|
||||
polynomial as cells are needed;
|
||||
- outer-sum: one 16x16 tile per CUDA block, sharing expanded `F(x+i)` and
|
||||
`G(z+j)` terms across the tile.
|
||||
|
||||
On the same RTX 3060 Laptop GPU, median kernel times were:
|
||||
|
||||
| implementation | positions | kernel time | positions/s |
|
||||
|---|---:|---:|---:|
|
||||
| direct | 67,125,249 | 1.608 ms | 41.75 billion |
|
||||
| tiled outer-sum | 67,125,249 | 1.332 ms | 50.39 billion |
|
||||
|
||||
That is a 1.21x outer-sum speedup. Both paths found exactly one hit,
|
||||
`(51069, 419124)`. Additional 4,004,001-position cross-checks at negative and
|
||||
near-world-border coordinates agreed exactly and showed 1.30--1.31x speedups.
|
||||
|
||||
The direct rate is high because a random position usually fails after one or
|
||||
two slime tests; it does not normally pay for all 25 cells. Consequently this
|
||||
experiment measures the arithmetic/reuse benefit of outer-sum, not the hoped-for
|
||||
algorithmic benefit of signature grouping. It still scans the Cartesian area
|
||||
and therefore is not a feasible full-world-border method.
|
||||
|
||||
## Full-world fixed-seed residue filter
|
||||
|
||||
The next prototype uses the fact that the necessary first-output parity is
|
||||
periodic in both coordinates modulo `2^18`. It checks all `2^36` residue pairs
|
||||
on CUDA, expands survivors over the world border, and exactly verifies only
|
||||
those expanded coordinates:
|
||||
|
||||
```bash
|
||||
./research/slime_position_residue_cuda --seed 61692151016558
|
||||
```
|
||||
|
||||
Results for four complete residue-space runs were:
|
||||
|
||||
| seed | residue kernel | survivors | expanded exact candidates | exact time | hits |
|
||||
|---:|---:|---:|---:|---:|---:|
|
||||
| 61692151016558 | 1.931 s | 180,476 | 36,937,048 | 284 ms | 1 |
|
||||
| 0 | 1.920 s | 195,672 | 40,046,949 | 352 ms | 0 |
|
||||
| 1 | 1.935 s | 175,992 | 36,010,787 | 283 ms | 0 |
|
||||
| 281474976710655 | 1.944 s | 181,276 | 37,098,020 | 313 ms | 0 |
|
||||
|
||||
The searched world rectangle contains 14,062,500,000,000 positions. For the
|
||||
known seed, the necessary filter reduces the exact workload by about 381,000x.
|
||||
Axis-table preparation took 224--289 ms, making total measured runtime roughly
|
||||
2.5 seconds.
|
||||
|
||||
For a larger direct baseline, both rectangular kernels were run five times over
|
||||
`2^34 = 17,179,869,184` positions. Median direct time was 524.1 ms (32.78
|
||||
billion positions/s), while tiled outer-sum took 315.9 ms (54.38 billion/s).
|
||||
Linear projection over the full rectangle is about 429 seconds direct or 259
|
||||
seconds tiled, versus about 2.5 seconds for the residue pipeline. Thus the new
|
||||
fixed-seed method is approximately 170x faster than direct and 100x faster than
|
||||
the tiled Cartesian scan. The residue run itself actually processes all
|
||||
`2^36` pairs; only the Cartesian full-world numbers are projections.
|
||||
|
||||
Like the seed-side prototype, the necessary parity filter omits the extremely
|
||||
rare branch where Java rejects its first `next(31)` output. Exact verification
|
||||
eliminates false positives but cannot restore a candidate already removed by
|
||||
that filter.
|
||||
|
||||
## Retired joint-search experiments
|
||||
|
||||
X/Z independent filtering and its lower35 generation experiments were removed on 2026-10-11.
|
||||
Their historical measurements and limitations are consolidated in
|
||||
[X/Z 筛轴试错报告](XZ_FILTER_TRIAL_REPORT.md). No full-world joint speedup was established.
|
||||
Loading…
Add table
Add a link
Reference in a new issue