Repository navigation
Conversation
The GPU manager slept 10 ms whenever no launch had finished. A keypair launch targets 0.4 s, so the average 5 ms between a launch finishing and the next one starting left the GPU idle about 2% of the time. Measured with OpenCL event profiling on an Apple M3 Max, the idle share between launches falls from 1.95% to 0.38% at 1 ms. The shutdown drain loop keeps its 10 ms sleep.
…logging Tuning the keypair search on a new GPU meant editing constants and rebuilding. These optional variables override them at run time: - VANITY_KP_LOCAL, VANITY_KP_WAVES: work-group size and groups per compute unit for the keypair kernel - VANITY_TARGET_SEC: the launch length the iteration count adapts toward - VANITY_CL_OPTS: extra OpenCL build options, such as -D overrides VANITY_CL_INFO=1 prints the device, its vendor and the launch geometry, then, after five warm-up launches, each launch's kernel-only rate from OpenCL event profiling next to the wall-clock rate since warm-up; the gap between the two is time the GPU sat idle between launches. Apple reports profiling timestamps in Mach ticks (41.67 ns on Apple Silicon) rather than nanoseconds, so the host converts them with mach_timebase_info.
A keypair kernel that computed wrong public keys would grind forever without saying so: the GPU callback re-derives each reported seed on the CPU and silently drops it when the key matches no pattern. There was no way to check the kernel's arithmetic short of waiting for matches. `vanity gpu-self-test` derives the public keys of random seeds on GPU 0 with the search kernel's comb table and compares each with ed25519-dalek. Given --pattern (and --case-insensitive), it also runs the search kernel's matcher on random 32-byte keys and compares every verdict with the CPU check grind-keypair applies to GPU results. It exits non-zero on any difference. The kernels behind it live in keypair_selftest.cl, compiled into the keypair program; keypair.cl is unchanged. The GPU callback now prints a warning when a reported key matches no pattern, since the kernel reports only keys it matched. On an Apple M3 Max, main's kernels pass: 0 mismatches in 500,000 seeds, and the matcher agrees with the CPU for single, multi-pattern and case-insensitive sets.
On an Apple M3 Max the OpenCL keypair search ran at about 14M keys/s. Almost all of that time is field multiplication, and Apple GPUs run a 32x32->64 widening multiply at an eighth of the rate of a float FMA (0.82 T/s against 6.4 T/s on the 40-core M3 Max), so the path is rebuilt around doing fewer of them: - A 13-bit signed comb over affine multiples (ge32.cl): 20 windows at 7 multiplies each (ref10's mixed addition) instead of 52 radix-32 windows at 8. Window 0 stores (x, y, xy) so it loads with no multiplies, and the last window stops before T, which encoding never reads: about 132 multiplies per key instead of 416. The 7.9 MB table is built by two kernels, the window bases on one work-item and then one work-item per entry, instead of one work-item for the whole table. - Field arithmetic on eight 32-bit limbs (fe32.cl): 64 widening multiplies per multiply instead of ref10's 100. Values stay below 2^256 with 2^256 = 38 folded back; every carry near 2^256 is exact, and subtraction adds 4p so nothing borrows. The multiply and square are straight-line and column-ordered, and they stay out of line, as does one comb window step: inlined at every call site they overflow the instruction cache and the kernel ran at a third of the speed. - SHA-512 of the 32-byte seed as one fixed block with unrolled rounds (sha512_32), instead of the streaming code with byte buffers. - Batches of 16 keys per inversion, keeping only the batch's first seed; a match re-walks the chain to its own seed. The host picks the path per device: the Apple path when the OpenCL vendor is Apple, keypair.cl everywhere else, since the new path is tuned and measured only on an Apple GPU. VANITY_OPENCL_PROFILE=apple|portable overrides the choice and VANITY_COMB_W sets the window width (2-16). keypair.cl, ge.cl, doppler.cl and the CUDA kernels are unchanged; the new code sits in fe32.cl, ge32.cl and keypair32.cl, with sha512_32 and the new types added to sha512.cl and preamble.cl. gpu-self-test covers the path through pubkeys_from_seeds32 and match_keys32. On the M3 Max: 0 mismatches against ed25519-dalek in 2,000,000 seeds; the matcher agrees with the CPU for single, multi-pattern, suffix-only, case-insensitive and '1' prefix sets; and 56 keys from single and multi-pattern grinds all verify with solana-keygen, on this path and on the portable one forced with VANITY_OPENCL_PROFILE=portable.
Every key paid for a full base58 conversion before its prefix was checked: 64 wide multiplies and 16 emulated 64-bit divisions, plus x = X/Z for the sign bit. Almost every key fails the prefix, and whether a 32-byte key's base58 form starts with a given prefix is a range test on the key read as a big-endian number. The host now turns each pattern's prefix, in every spelling the match LUT folds together, into exact ranges over a key's top 64 bits, for both lengths a key without leading zero bytes encodes to (43 and 44 characters), and sorts and merges them. The kernel binary-searches the ranges with the canonical y alone and computes x and the base58 digits only for keys inside one. A key inside a range still takes the existing exact check, so the ranges only have to over-cover: they use at most the first 10 characters and each pattern's share of 256 spellings, and they stay correct as patterns leave the active mask. With a suffix-only pattern or a prefix that may start with '1' (a leading zero byte) there is no filter and every key takes the existing path. VANITY_NO_PREFILTER=1 turns it off. On an Apple M3 Max: +6.4% and +6.3% in alternating runs. The GPU matcher agrees with the CPU check on 38M random keys across 19 pattern sets: prefixes that only start 44- or only 43-character addresses, the 43/44 boundary (5Q, 5R), case-insensitive spellings including L, '1' and '11' prefixes, prefix plus suffix, suffix only, and multi-pattern sets, including one mixing a normal prefix with a '1' prefix.
With the comb down to about 132 multiplies per key, each work-item's own batch inversion (254 squarings and 11 multiplies per 16 keys) was about a tenth of the search: stubbing it out made the kernel 10-14% faster. Each work-item now multiplies its batch's Z values together, and fe32_group_invert inverts every item's product at once: a product tree over the work-group in local memory, work-item 0 inverting the root, and the tree walked back down, each node handing its children the inverse times the sibling. That is one inversion per 2,048 keys instead of per 16, for about three extra multiplies per work-item. The barriers need every work-item to run the same number of batches, so work-item 0 now decides once per batch whether the group stops, and a work-item that finds a match stays in step until then; the host rounds the group size down to a power of two the kernel can run, and passes 2 * group size * 32 bytes of local memory. On an Apple M3 Max this measured +3.6% and +5.9% while developing it on the pre-multi-pattern code, and +1.2% on average over five alternating pairs here, with the machine thermally throttled. gpu-self-test and 56 keys from single and multi-pattern grinds all verify.
One Karatsuba level over 128-bit halves cuts fe32_mul from 64 limb products to 48: L = f0 g0, H = f1 g1, M = (f0 + f1)(g0 + g1), and f g = L + (M - L - H) 2^128 + H 2^256, with the half sums' carry bits picked up at limbs 4 and 8 of M. It is exact: it matches ref10 on 1M random and 65,860 edge and near-2^256 inputs, and gpu-self-test passes with it. It is not the default because on an Apple M3 Max it was 1-2.6% slower in the search kernel than the schoolbook multiply (1.7% faster in a multiply-only microbenchmark): its extra signed additions and larger body cost more there than the 16 multiplies it saves. It stays available as VANITY_CL_OPTS=-DFE32_KARATSUBA for GPUs where the balance differs.
The performance table's OpenCL row said ~15 M keypairs/s for Apple Silicon; the Apple path measures ~44 M on an M3 Max with the laptop cool and roughly 30 M once sustained load throttles it. The install section now says which OpenCL devices get the Apple path, how to override it, what gpu-self-test checks, and why --num-cpus 1 is usually enough on Apple Silicon.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On Apple GPUs,
grind-keypairover OpenCL goes from ~14.2M to ~44M keypairs/s on an M3 Max (3.03–3.11× in alternating runs, ~2.8× once the laptop throttles). No CLI, output or key-file changes.VANITY_OPENCL_PROFILE=apple); other devices keepmain's search kernel.vanity gpu-self-test: checks GPU public keys againsted25519-dalekand, with--pattern, the GPU matcher against the CPU check.Results
MacBook Pro 16" M3 Max (40-core GPU, 48 GB), macOS 14.4, AC power, Apple OpenCL 1.2, Rust 1.97.1.
grind-keypair --pattern zzzzzzzzzz --num-gpus 1 --num-cpus 1, builds alternated so throttling hits both:main@ 4e0f88dUnder sustained load the PR build settles around 30 M/s;
mainstays near 14 M/s.What changed
Field multiplication dominates the search, and on this GPU a 32×32→64 multiply runs at ⅛ the rate of an fp32 FMA (0.82 vs 6.4 ×10¹² ops/s), so most of the work cuts multiplies. Gains are from alternating runs; some were measured while developing on the code before #30.
f261b4cf2aedda479ab695895d84868d273VANITY_CL_OPTS=-DFE32_KARATSUBA: exact, but 1–2.6% slower hereAlso:
25f556aadds optional tuning variables (VANITY_KP_LOCAL,VANITY_KP_WAVES,VANITY_TARGET_SEC,VANITY_CL_OPTS,VANITY_COMB_W,VANITY_NO_PREFILTER) andVANITY_CL_INFO=1per-launch logging;5e768f6addsgpu-self-testand the rejected-key warning;92f9989updates the README.The new code is in
fe32.cl,ge32.clandkeypair32.cl;keypair.cl,ge.cl,doppler.cland the CUDA kernels are unchanged. Tried and dropped: compiling as Metal (same speed, since Apple's OpenCL compiles through Metal), 14–16-bit windows, longer launches, larger batches and float-based arithmetic.Correctness
gpu-self-testfinds 0 mismatches againsted25519-dalekin 2M seeds, and the matcher agrees with the CPU on 38M random keys across 19 pattern sets (43/44-character boundaries, case-insensitive,1prefixes, suffixes, multi-pattern).solana-keygen, on the Apple path and onmain's path (VANITY_OPENCL_PROFILE=portable).cargo test,cargo fmt --checkand clippy (default, deploy, opencl, opencl+deploy) are clean. On aarch64 macOS,clippy --all-targetsalready fails onmainwith two dead-code errors insrc/fast; this PR adds none.Limits
gpu-self-testwill say) but may want different defaults.sha512_32); I've only compiled it with Apple's OpenCL. The path is chosen from theCL_DEVICE_VENDORstring.grind-dopplerstays onmain's kernels. Separately, it passes the secret seed toprint_doppler_result, so its printed.equ EXPECTED_KEY_*constants are the private key's seed; I'll send a fix in its own PR.Reproducing