proto-opencl: OpenCL path for AMD, proven on Apple OpenCL, pocl and a wave64 CPU emulator
Exporter writes kernel.cl next to kernel.cu (same instruction list; memory-hard core emitted in a third, OpenCL C dialect with the same literals as memhard.h). Pack headers are now C99-safe so a plain C host can include them. proto-opencl/host.c: C99 + OpenCL 1.2 API, device list, runtime build, cache fill and FNV check, dataset build and self-test, 3 vector warps standalone and in batch, bench and sweep as host.cu, whole-batch fingerprint. The 32-lane exchange is sub_group_shuffle_xor only when the queried sub-group size for a 32-item work-group is exactly 32; otherwise a local-memory exchange with one barrier per exchange, so wave64 hardware cannot change the hash (WAVEFRONT.md). build.sh (macOS, Linux), build.bat (MSVC), README with the exact AMD-rig commands. Proven without AMD silicon: Apple OpenCL 1.2 on the M5 Max 96/96 on all three packs (45.0 Mhash/s at 1 GiB, Apple number, not AMD); pocl 7.2 CPU device 96/96 on both exchange paths including the real sub_group_shuffle_xor text; CPU emulator 7 configurations incl. 64-wide sub-groups, identical fingerprint f99fb375b3abeaf5 everywhere. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
c75477115d
commit
660f0eb16c
26 changed files with 2700 additions and 31 deletions
|
|
@ -159,3 +159,27 @@ Attacker speed: delay must only exceed the 2 s publish-or-lose window; margin is
|
|||
Grinding model (3,600 blocks/epoch, advantage uniform 0 to 15%, keep top quartile, one block burned per withheld candidate), gain per epoch in blocks, no delay vs with delay: s=0.1 +0.40 vs 0; s=0.2 +1.66 vs 0; s=0.3 +3.62 (+0.32%, 13.5:1 on burned blocks) vs 0; s=0.4 +6.06 vs 0. Monte Carlo over 2,000,000 epochs agrees to 0.03 blocks.
|
||||
Correctness: NUDUPL, NUCOMP and the Lehmer partial xgcd agree with Cohen 5.4.7 / plain duplication / plain-division xgcd on 15,000 random cases; block prover equals the naive O(T) prover at T = 37, 5,000 and 100,000 in both groups; 216 associativity triples; 3 tamper cases rejected per size.
|
||||
Recommend: class group 1024-bit D from the checkpoint hash, epoch T = 600 x r_ref and era T = 3,600 x r_ref with r_ref the fastest honest single-core rate measured on the devnet (98 M and 588 M on this Mac), fixed at genesis, 20 min lead time for the epoch seed and 2 h for the era draw, 256-bit Fiat-Shamir prime. Open: external review of classgroup.rs against chiavdf, reference core choice, fallback rule for a node without the seed at epoch start, carry D in the proof.
|
||||
|
||||
## 2026-10-03 igneum-pow: Rust crate bit-exact with proto-metal (consensus-engineer)
|
||||
|
||||
Machine: Apple M5 Max, one performance core, rustc 1.99.0 (rustup), release build with LTO. Crate at `igneum-pow/` (seed, generator, memhard, verify, emit; CLI `bench`, `export`, `hash`), standard library only, serde_json as a dev-dependency for the pack tests.
|
||||
Agreement with the Swift through `proto-cuda/packs/`: program.json instruction by instruction for igneum-genesis, igneum-genesis-mh and igneum-hourly (3 x 64 match); mixer rot/mul/rc match; cache head, last line and FNV-1a 64 `48c4f5bf24166b2e` match; dataset head, `[MASK]` and 64 sampled words match in all three packs; hash vectors 96/96 for igneum-genesis-mh (memory-hard) and 96/96 each for the two closed-form packs. 23 tests, all pass.
|
||||
Emitted sources: kernel.cu, program.metal, kernel.cl and program.h byte-identical for all three packs, memhard.h and memhard.metal byte-identical for igneum-genesis-mh; `igneum-pow export` then `diff -r` against the packs differs only in the provenance string of vectors.json/vectors.h.
|
||||
Found: `proto-cuda/packs/igneum-genesis-mh/program.json` is not valid JSON (main.swift line 1291 writes `jhex(cacheLineMask)` inside the `"item"` string). The Rust emitter writes the mask bare and the test normalises that line; fix pending in the Swift.
|
||||
Cache fill, 256 MiB on one core: 175 to 181 ms in Rust (5 runs) against 184.5 to 190.6 ms Swift and 161.5 ms C++ host reference.
|
||||
CPU verify per 32-lane warp, avg of 20, 1 GiB dataset: igneum-genesis 0.441 ms (Swift 0.649), /epoch1 0.411 (0.631), /epoch2 0.488 (0.701), igneum-second-seed 0.482 (0.801), igneum-second-seed/epoch1 at 144 loads and 4,608 items 0.579 (1.205). Cold single warps 0.41 to 0.87 ms (Swift 1.16 to 2.11). Closed form 0.002 ms (Swift 0.017).
|
||||
Reading: Rust is 1.4x to 2.1x faster than the Swift verifier per warp with the same algorithm (register-major lanes, 32-lane interleaved item derivation); 10 ms gate margin about 17x steady, 11x on the worst cold warp. Cache fill is within 5 percent of the Swift and 10 percent slower than clang C++.
|
||||
API for the fork: `Epoch::memory_hard(seed, day)` once per epoch (fills the cache), then `epoch.hash(nonce)`, `epoch.hash_warp(base)`, `epoch.verify_block(nonce, target)`; `emit::export_pack(&epoch, day, source)` for miner programs. `seed::seed_words_from_bytes` is the boundary for the VDF output.
|
||||
Not done: no GPU run from Rust; the 256-bit target mapping stays in the fork; the seed is still a string.
|
||||
|
||||
## 3 October 2026, proto-opencl: OpenCL path built and proven without AMD silicon (Apple OpenCL 1.2, pocl, CPU emulator)
|
||||
|
||||
Machine: the same Apple M5 Max. New: `proto-opencl/host.c` (C99, OpenCL 1.2 API), `kernel.cl` in every pack from `--export-pack` (same emitter, OpenCL C dialect; memory-hard core emitted in three dialects), `WAVEFRONT.md`, CPU emulator with a 32- or 64-wide sub-group. The AMD rig has not arrived; no AMD compiler or device has touched this code.
|
||||
Exchange rule: `sub_group_shuffle_xor` only when the device lists `cl_khr_subgroup_shuffle`, the work-group is exactly 32 and the queried sub-group size for a 32-item work-group is exactly 32; otherwise a `__local` memory exchange with one barrier per exchange (two alternating buffers). Wave64 hardware (GCN, CDNA, RDNA in wave64) therefore takes the local-memory path and the hash never depends on the wave width.
|
||||
Apple OpenCL 1.2 runtime, Apple M5 Max (40 CUs, OpenCL C 1.2, no sub-group extension, local-memory path): cache check PASS (all 2^26 words, FNV-1a 64 48c4f5bf24166b2e = Mac), dataset self-test PASS, 96/96 vectors standalone and in batch for igneum-genesis-mh; also 96/96 at `--exchange local --group-warps 2` and `--group-warps 4`; closed-form packs igneum-genesis 96/96 and igneum-hourly 96/96.
|
||||
Apple OpenCL hash rate (wall time; Apple's event timestamps are unusable), pack igneum-genesis-mh, 1 GiB, 5 x 2^24: 45.03 Mhash/s, 18.73 GB/s useful (repeat run 44.58). igneum-genesis 45.17, igneum-hourly 36.33 (128 loads). Metal on the same chip: 45.2. This is Apple's deprecated OpenCL on the M5 Max, NOT an AMD number.
|
||||
Apple OpenCL sweep (3 batches): 4 MiB 573.7 Mhash/s, 64 MiB 178.9, 256 MiB 94.3, 512 MiB 68.8, 1024 MiB 45.0 (Metal sweep shape reproduced).
|
||||
pocl 7.2 CPU device (OpenCL 3.0, LLVM 23, Khronos ICD loader, `brew install pocl`, needs SDKROOT): `--exchange auto` and `--exchange subgroup` built with `-cl-std=CL3.0 -D IGNEUM_EXCHANGE=1` and ran the real `sub_group_shuffle_xor` text: cache FNV = Mac, 96/96 PASS. pocl's `clGetKernelSubGroupInfoKHR` returns CL_INVALID_OPERATION, so the new probe kernel (`igneum_probe_subgroup`, reports get_sub_group_size() 32) decided; `--exchange local` also 96/96.
|
||||
CPU emulator (`proto-opencl/emu`, kernel.cl compiled as C++, 1 GiB dataset built on 256 host threads): 7 configurations all PASS with the identical batch fingerprint f99fb375b3abeaf5 over 2^13 outputs: exchange 0 with work-group 32/sub-group 32, 64/64, 32/64; exchange 1 (sub-group shuffles) with 32/32, 32/64, 64/64 (wave64 carrying two 32-lane units in one shuffle domain), 64/32.
|
||||
Cross-implementation fingerprint at `--batch-log2 13`, base nonce 0, igneum-genesis-mh: Apple OpenCL f99fb375b3abeaf5, pocl sub-group f99fb375b3abeaf5, pocl local f99fb375b3abeaf5, emulator f99fb375b3abeaf5 (all 7). At 2^24 Apple OpenCL prints 98af644e993239e2 (reference for the AMD run). proto-cuda emulator re-run after the header changes (program.h, vectors.h, memhard.h now C99-safe): PASS.
|
||||
Not demonstrated: any AMD compile or run, any AMD hash rate, the cost of the local-memory exchange on AMD, whether RDNA compiles igneum_hash as wave32 or wave64. Next: run the seven commands in `proto-opencl/README.md` on the AMD rig and paste the logs.
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ proto-cuda/
|
|||
CHECKLIST.md Metal/CUDA equivalence, op by op, and what was verified where
|
||||
packs/<seed>/ one program pack per seed, written by proto-metal/igneum-bench --export-pack
|
||||
kernel.cu the program as a CUDA kernel, plus fill kernel and host launch wrappers
|
||||
program.h seed, day words, dataset size, loads per hash, wrapper declarations
|
||||
kernel.cl the same program as OpenCL C for proto-opencl (AMD and any other OpenCL device), built at runtime
|
||||
program.h seed, day words, dataset size, loads per hash, wrapper declarations (C99-safe: proto-opencl/host.c includes it too)
|
||||
vectors.h expected outputs for 3 warps (96 x 64-bit) and dataset self-test values
|
||||
program.json the instruction list and all constants, for any other implementation
|
||||
vectors.json the same vectors as JSON
|
||||
|
|
|
|||
278
proto-cuda/packs/igneum-genesis-mh/kernel.cl
Normal file
278
proto-cuda/packs/igneum-genesis-mh/kernel.cl
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
// Generated by proto-metal/igneum-bench --export-pack for seed "igneum-genesis". Do not edit by hand.
|
||||
// OpenCL C twin of the Metal kernel for the same seed (see proto-opencl/README.md, WAVEFRONT.md and program.metal).
|
||||
// Built from source at runtime by proto-opencl/host.c, which passes these defines:
|
||||
// IGNEUM_GROUP work-group size of igneum_hash, a multiple of 32 (default 32: one work-group = one 32-lane unit)
|
||||
// IGNEUM_EXCHANGE 0 = local-memory exchange with a barrier (any device, any wave width; the default)
|
||||
// 1 = sub_group_shuffle_xor (cl_khr_subgroup_shuffle), only with IGNEUM_GROUP 32 and a sub-group size of exactly 32
|
||||
// 2 = intel_sub_group_shuffle_xor (cl_intel_subgroups), same condition
|
||||
// The verification unit is always 32 lanes. A 64-wide hardware wave (AMD GCN/CDNA, RDNA in wave64) runs two units;
|
||||
// the exchange masks are 1, 2, 4, 8, 16, so every partner lane lies inside the lane's own aligned run of 32.
|
||||
#ifndef IGNEUM_GROUP
|
||||
#define IGNEUM_GROUP 32
|
||||
#endif
|
||||
#ifndef IGNEUM_EXCHANGE
|
||||
#define IGNEUM_EXCHANGE 0
|
||||
#endif
|
||||
#ifdef __OPENCL_VERSION__
|
||||
#define IGNEUM_KERNEL_HASH __kernel __attribute__((reqd_work_group_size(IGNEUM_GROUP, 1, 1)))
|
||||
#define IGNEUM_LOCAL_WORDS(name, n) __local uint name[n]
|
||||
#if IGNEUM_EXCHANGE == 1
|
||||
#ifdef cl_khr_subgroups
|
||||
#pragma OPENCL EXTENSION cl_khr_subgroups : enable
|
||||
#endif
|
||||
#ifdef cl_khr_subgroup_shuffle
|
||||
#pragma OPENCL EXTENSION cl_khr_subgroup_shuffle : enable
|
||||
#endif
|
||||
#elif IGNEUM_EXCHANGE == 2
|
||||
#pragma OPENCL EXTENSION cl_intel_subgroups : enable
|
||||
#endif
|
||||
#else
|
||||
// Not an OpenCL compiler: proto-opencl/emu compiles this file as C++ and supplies the built-ins and these two macros.
|
||||
#include "emu_opencl.h"
|
||||
#endif
|
||||
|
||||
#if IGNEUM_EXCHANGE == 1
|
||||
#define IGNEUM_SHFL_XOR(dst, a, m) dst = sub_group_shuffle_xor((a), (uint)(m))
|
||||
#define IGNEUM_BCAST0(dst, a) dst = sub_group_broadcast((a), 0u)
|
||||
#elif IGNEUM_EXCHANGE == 2
|
||||
#define IGNEUM_SHFL_XOR(dst, a, m) dst = intel_sub_group_shuffle_xor((a), (uint)(m))
|
||||
#define IGNEUM_BCAST0(dst, a) dst = sub_group_broadcast((a), 0u)
|
||||
#else
|
||||
// Local-memory exchange. Two buffers of IGNEUM_GROUP words alternate (xk counts exchanges), so one barrier per
|
||||
// exchange is enough: a lane can only overwrite buffer b at exchange k+2 after passing barrier k+1, and every lane
|
||||
// reaches barrier k+1 only after its read of buffer b at exchange k. The partner lid ^ m stays inside the lane's
|
||||
// aligned run of 32 because m < 32. Control flow is uniform, so every work-item reaches every barrier.
|
||||
#define IGNEUM_SHFL_XOR(dst, a, m) { xch[(xk & 1u) * IGNEUM_GROUP + lid] = (a); barrier(CLK_LOCAL_MEM_FENCE); dst = xch[(xk & 1u) * IGNEUM_GROUP + (lid ^ (uint)(m))]; xk += 1u; }
|
||||
#define IGNEUM_BCAST0(dst, a) { xch[(xk & 1u) * IGNEUM_GROUP + lid] = (a); barrier(CLK_LOCAL_MEM_FENCE); dst = xch[(xk & 1u) * IGNEUM_GROUP + (lid & ~31u)]; xk += 1u; }
|
||||
#endif
|
||||
|
||||
static inline uint splitmix32(uint x) {
|
||||
x ^= x >> 16; x *= 0x7feb352du;
|
||||
x ^= x >> 15; x *= 0x846ca68bu;
|
||||
x ^= x >> 16;
|
||||
return x;
|
||||
}
|
||||
// n is a literal in 1..31 at every call site. OpenCL rotate() rotates left by n modulo 32.
|
||||
static inline uint rotl_imm(uint x, uint n) { return rotate(x, n); }
|
||||
// Right rotation by n modulo 32 as a left rotation by (32 - n) modulo 32; n == 0 gives x.
|
||||
static inline uint rotr_var(uint x, uint n) { return rotate(x, (0u - n) & 31u); }
|
||||
static inline uint ds_elem(uint i, uint d0, uint d1) {
|
||||
uint x = i ^ d0;
|
||||
x *= 0x9E3779B1u; x ^= x >> 15;
|
||||
x += d1;
|
||||
x *= 0x85EBCA77u; x ^= x >> 13;
|
||||
x *= 0xC2B2AE3Du; x ^= x >> 16;
|
||||
return x;
|
||||
}
|
||||
|
||||
// Memory-hard dataset core (MEMHARD.md). Cache: 2^26 words in 2^16 segments of 64 chained ChaCha12 lines.
|
||||
// Item: 8 rounds of seed-parameterised mixer + one 64-byte cache read, then a final mixer. All parameters are literals.
|
||||
#define MH_CACHE_LINE_MASK 0x003fffffu
|
||||
#define MH_SEGMENT_LINES 64u
|
||||
#define MH_QR(a, b, c, d, r1, r2, r3, r4) { a += b; d ^= a; d = mh_rotl(d, r1); c += d; b ^= c; b = mh_rotl(b, r2); a += b; d ^= a; d = mh_rotl(d, r3); c += d; b ^= c; b = mh_rotl(b, r4); }
|
||||
static inline uint mh_rotl(uint x, uint n) { return (x << n) | (x >> (32u - n)); } // n in 1..31 at every call site
|
||||
|
||||
// y = ChaCha12 core(x) + x
|
||||
static inline void mh_chacha_block(const uint* x, uint* y) {
|
||||
for (uint i = 0u; i < 16u; ++i) y[i] = x[i];
|
||||
for (uint r = 0u; r < 6u; ++r) {
|
||||
MH_QR(y[0], y[4], y[8], y[12], 16u, 12u, 8u, 7u) MH_QR(y[1], y[5], y[9], y[13], 16u, 12u, 8u, 7u)
|
||||
MH_QR(y[2], y[6], y[10], y[14], 16u, 12u, 8u, 7u) MH_QR(y[3], y[7], y[11], y[15], 16u, 12u, 8u, 7u)
|
||||
MH_QR(y[0], y[5], y[10], y[15], 16u, 12u, 8u, 7u) MH_QR(y[1], y[6], y[11], y[12], 16u, 12u, 8u, 7u)
|
||||
MH_QR(y[2], y[7], y[8], y[13], 16u, 12u, 8u, 7u) MH_QR(y[3], y[4], y[9], y[14], 16u, 12u, 8u, 7u)
|
||||
}
|
||||
for (uint i = 0u; i < 16u; ++i) y[i] += x[i];
|
||||
}
|
||||
|
||||
// One cache segment: 64 chained lines written at cache[seg * 1024]. in_j = prev ^ (sigma || K || seg || j || tag), prev_0 = 0.
|
||||
static inline void mh_cache_segment(__global uint* cache, uint seg) {
|
||||
uint prev[16]; uint x[16]; uint y[16];
|
||||
for (uint i = 0u; i < 16u; ++i) prev[i] = 0u;
|
||||
for (uint j = 0u; j < MH_SEGMENT_LINES; ++j) {
|
||||
x[0] = 0x61707865u ^ prev[0]; x[1] = 0x3320646eu ^ prev[1]; x[2] = 0x79622d32u ^ prev[2]; x[3] = 0x6b206574u ^ prev[3];
|
||||
x[4] = 0x3067619fu ^ prev[4];
|
||||
x[5] = 0x3c269176u ^ prev[5];
|
||||
x[6] = 0x84a03b03u ^ prev[6];
|
||||
x[7] = 0xf8c63294u ^ prev[7];
|
||||
x[8] = 0xff977c5bu ^ prev[8];
|
||||
x[9] = 0xe60def3eu ^ prev[9];
|
||||
x[10] = 0x63630141u ^ prev[10];
|
||||
x[11] = 0xb8fbcb58u ^ prev[11];
|
||||
x[12] = seg ^ prev[12]; x[13] = j ^ prev[13]; x[14] = 0x49676e65u ^ prev[14]; x[15] = 0x756d4d48u ^ prev[15];
|
||||
mh_chacha_block(x, y);
|
||||
__global uint* line = cache + ((seg * MH_SEGMENT_LINES + j) * 16u);
|
||||
for (uint i = 0u; i < 16u; ++i) { line[i] = y[i]; prev[i] = y[i]; }
|
||||
}
|
||||
}
|
||||
|
||||
// M_r: per word (s ^ (RC + rk)) * MUL, then a column round and a diagonal round with the seed-drawn rotations.
|
||||
static inline void mh_mixer(uint* s, uint rk) {
|
||||
s[0] = (s[0] ^ (0xbab68293u + rk)) * 0x42146205u;
|
||||
s[1] = (s[1] ^ (0xcc162340u + rk)) * 0x52cbe0fbu;
|
||||
s[2] = (s[2] ^ (0x6ce151ccu + rk)) * 0x7ecf4a03u;
|
||||
s[3] = (s[3] ^ (0xe62b8997u + rk)) * 0x6728907fu;
|
||||
s[4] = (s[4] ^ (0xc9c80297u + rk)) * 0xd81d9751u;
|
||||
s[5] = (s[5] ^ (0xf74a1654u + rk)) * 0x132952c3u;
|
||||
s[6] = (s[6] ^ (0x3d704af5u + rk)) * 0xf60de277u;
|
||||
s[7] = (s[7] ^ (0x3cf522b7u + rk)) * 0x05358035u;
|
||||
s[8] = (s[8] ^ (0x2b9cac04u + rk)) * 0xbaf6499du;
|
||||
s[9] = (s[9] ^ (0xa880ac10u + rk)) * 0xe4db9667u;
|
||||
s[10] = (s[10] ^ (0x13e5dd1du + rk)) * 0x3e98f45du;
|
||||
s[11] = (s[11] ^ (0x6fc3e233u + rk)) * 0xd0004eddu;
|
||||
s[12] = (s[12] ^ (0x2d83eeacu + rk)) * 0x2691630du;
|
||||
s[13] = (s[13] ^ (0x9006e8bfu + rk)) * 0x9beb3bcfu;
|
||||
s[14] = (s[14] ^ (0x2c4b5362u + rk)) * 0xab310379u;
|
||||
s[15] = (s[15] ^ (0x31b49ee2u + rk)) * 0x99cfb423u;
|
||||
MH_QR(s[0], s[4], s[8], s[12], 20u, 20u, 19u, 4u) MH_QR(s[1], s[5], s[9], s[13], 20u, 20u, 19u, 4u)
|
||||
MH_QR(s[2], s[6], s[10], s[14], 20u, 20u, 19u, 4u) MH_QR(s[3], s[7], s[11], s[15], 20u, 20u, 19u, 4u)
|
||||
MH_QR(s[0], s[5], s[10], s[15], 26u, 3u, 3u, 27u) MH_QR(s[1], s[6], s[11], s[12], 26u, 3u, 3u, 27u)
|
||||
MH_QR(s[2], s[7], s[8], s[13], 26u, 3u, 3u, 27u) MH_QR(s[3], s[4], s[9], s[14], 26u, 3u, 3u, 27u)
|
||||
}
|
||||
|
||||
// Item t: 16 words. s = (K, t * MUL[i] + RC[i]); 8 rounds of mixer + cache line s[0] & mask; final mixer.
|
||||
static inline void mh_item(__global const uint* cache, uint t, uint* s) {
|
||||
s[0] = 0x3067619fu;
|
||||
s[1] = 0x3c269176u;
|
||||
s[2] = 0x84a03b03u;
|
||||
s[3] = 0xf8c63294u;
|
||||
s[4] = 0xff977c5bu;
|
||||
s[5] = 0xe60def3eu;
|
||||
s[6] = 0x63630141u;
|
||||
s[7] = 0xb8fbcb58u;
|
||||
s[8] = t * 0x42146205u + 0xbab68293u;
|
||||
s[9] = t * 0x52cbe0fbu + 0xcc162340u;
|
||||
s[10] = t * 0x7ecf4a03u + 0x6ce151ccu;
|
||||
s[11] = t * 0x6728907fu + 0xe62b8997u;
|
||||
s[12] = t * 0xd81d9751u + 0xc9c80297u;
|
||||
s[13] = t * 0x132952c3u + 0xf74a1654u;
|
||||
s[14] = t * 0xf60de277u + 0x3d704af5u;
|
||||
s[15] = t * 0x05358035u + 0x3cf522b7u;
|
||||
for (uint r = 0u; r < 8u; ++r) {
|
||||
mh_mixer(s, 0x9E3779B9u * (r + 1u));
|
||||
__global const uint* line = cache + ((s[0] & MH_CACHE_LINE_MASK) * 16u);
|
||||
for (uint i = 0u; i < 16u; ++i) s[i] ^= line[i];
|
||||
}
|
||||
mh_mixer(s, 0x9E3779B9u * 9u);
|
||||
}
|
||||
// dataset[w] without the dataset: derive item w >> 4 and take word w & 15.
|
||||
static inline uint mh_word(__global const uint* cache, uint w) { uint s[16]; mh_item(cache, w >> 4u, s); return s[w & 15u]; }
|
||||
|
||||
// Memory-hard dataset (MEMHARD.md). One work-item per cache segment; one work-item per 64-byte dataset item.
|
||||
// The same constants as memhard.h in this pack (one emitter, three dialects).
|
||||
__kernel void igneum_cache_fill(__global uint* cache, uint nSegments) {
|
||||
uint seg = (uint)get_global_id(0);
|
||||
if (seg < nSegments) mh_cache_segment(cache, seg);
|
||||
}
|
||||
__kernel void igneum_build(__global uint* ds, __global const uint* cache, uint nItems) {
|
||||
uint t = (uint)get_global_id(0);
|
||||
if (t < nItems) {
|
||||
uint s[16];
|
||||
mh_item(cache, t, s);
|
||||
__global uint* d = ds + ((ulong)t * 16u);
|
||||
for (uint i = 0u; i < 16u; ++i) d[i] = s[i];
|
||||
}
|
||||
}
|
||||
|
||||
// One hash per work-item. IGNEUM_GROUP is a multiple of 32; lane = lid & 31 and every exchange stays inside the
|
||||
// lane's own aligned run of 32 work-items, exactly like simd_shuffle_xor inside a 32-wide Metal SIMD group and
|
||||
// __shfl_xor_sync inside a CUDA warp. Control flow is uniform (no branches at all).
|
||||
IGNEUM_KERNEL_HASH void igneum_hash(__global const uint* ds, __global ulong* out, uint baseNonce, uint mask) {
|
||||
uint gid = (uint)get_global_id(0);
|
||||
uint lid = (uint)get_local_id(0);
|
||||
uint nonce = baseNonce + gid;
|
||||
uint r0, r1, r2, r3, r4, r5, r6, r7;
|
||||
#if IGNEUM_EXCHANGE == 0
|
||||
IGNEUM_LOCAL_WORDS(xch, 2 * IGNEUM_GROUP);
|
||||
uint xk = 0u;
|
||||
#else
|
||||
(void)lid;
|
||||
#endif
|
||||
{ uint x = nonce ^ 0x67a9a7beu; x += 0x9e3779b9u; x = splitmix32(x); r0 = x ^ 0x1a155b25u; } // SEEDW[0], 0x9e3779b9u * 1u, SEEDW[1]
|
||||
{ uint x = nonce ^ 0x1a155b25u; x += 0x3c6ef372u; x = splitmix32(x); r1 = x ^ 0xfddfb732u; } // SEEDW[1], 0x9e3779b9u * 2u, SEEDW[2]
|
||||
{ uint x = nonce ^ 0xfddfb732u; x += 0xdaa66d2bu; x = splitmix32(x); r2 = x ^ 0x4b5af2e8u; } // SEEDW[2], 0x9e3779b9u * 3u, SEEDW[3]
|
||||
{ uint x = nonce ^ 0x4b5af2e8u; x += 0x78dde6e4u; x = splitmix32(x); r3 = x ^ 0xc55caf33u; } // SEEDW[3], 0x9e3779b9u * 4u, SEEDW[4]
|
||||
{ uint x = nonce ^ 0xc55caf33u; x += 0x1715609du; x = splitmix32(x); r4 = x ^ 0xa27c13b7u; } // SEEDW[4], 0x9e3779b9u * 5u, SEEDW[5]
|
||||
{ uint x = nonce ^ 0xa27c13b7u; x += 0xb54cda56u; x = splitmix32(x); r5 = x ^ 0x06628a48u; } // SEEDW[5], 0x9e3779b9u * 6u, SEEDW[6]
|
||||
{ uint x = nonce ^ 0x06628a48u; x += 0x5384540fu; x = splitmix32(x); r6 = x ^ 0x03852469u; } // SEEDW[6], 0x9e3779b9u * 7u, SEEDW[7]
|
||||
{ uint x = nonce ^ 0x03852469u; x += 0xf1bbcdc8u; x = splitmix32(x); r7 = x ^ 0x67a9a7beu; } // SEEDW[7], 0x9e3779b9u * 8u, SEEDW[0]
|
||||
|
||||
for (uint it = 0u; it < 8u; ++it) {
|
||||
uint sel = r0;
|
||||
r4 = rotl_imm(r4, 25u); // 0 rotl
|
||||
r0 = r0 - r5; // 1 sub
|
||||
r4 = r4 ^ ds[r3 & mask]; // 2 load
|
||||
r1 = rotl_imm(r1, 1u); // 3 rotl
|
||||
r2 = r2 + r3 + ((((sel >> 26u) & 1u) != 0u) ? 0x2735a174u : 0x61f0b51cu); // 4 add
|
||||
r5 = r5 ^ ds[r3 & mask]; // 5 load
|
||||
r5 = r5 - r7; // 6 sub
|
||||
r3 = r3 + r4 + ((((sel >> 26u) & 1u) != 0u) ? 0x5a069596u : 0x52f2dbf4u); // 7 add
|
||||
r0 = r0 ^ r4; // 8 xor
|
||||
r4 = r4 ^ r0; // 9 xor
|
||||
r2 = r2 ^ ds[r0 & mask]; // 10 load
|
||||
{ uint t_; IGNEUM_SHFL_XOR(t_, r6, 16u); r4 = r4 ^ t_; } // 11 shfl
|
||||
r1 = r1 - r5; // 12 sub
|
||||
r2 = r2 ^ r1; // 13 xor
|
||||
r4 = r4 ^ ds[r5 & mask]; // 14 load
|
||||
r2 = r2 ^ ds[r4 & mask]; // 15 load
|
||||
r3 = r3 ^ ds[r0 & mask]; // 16 load
|
||||
r4 = r4 ^ r6; // 17 xor
|
||||
r2 = r4 * r6 + r2; // 18 mad
|
||||
r6 = rotr_var(r6, r1); // 19 rotr
|
||||
r3 = r3 ^ r4; // 20 xor
|
||||
r1 = r3 * r5 + r1; // 21 mad
|
||||
r7 = mul_hi(r7, r4); // 22 mulhi
|
||||
r5 = mul_hi(r5, r2); // 23 mulhi
|
||||
r0 = r0 ^ ds[r6 & mask]; // 24 load
|
||||
r5 = r5 * r6; // 25 mul
|
||||
r7 = r7 ^ ds[r1 & mask]; // 26 load
|
||||
r3 = rotr_var(r3, r1); // 27 rotr
|
||||
{ uint t_; IGNEUM_SHFL_XOR(t_, r1, 8u); r5 = r5 ^ t_; } // 28 shfl
|
||||
r7 = r7 ^ r5; // 29 xor
|
||||
r7 = rotl_imm(r7, 23u); // 30 rotl
|
||||
r2 = r2 - r0; // 31 sub
|
||||
r7 = r7 ^ r2; // 32 xor
|
||||
r2 = r2 ^ r6; // 33 xor
|
||||
r6 = r6 ^ ds[r1 & mask]; // 34 load
|
||||
r1 = r1 ^ r4; // 35 xor
|
||||
{ uint t_; IGNEUM_SHFL_XOR(t_, r0, 1u); r3 = r3 ^ t_; } // 36 shfl
|
||||
r2 = r2 * r6; // 37 mul
|
||||
r5 = r5 + r3 + ((((sel >> 12u) & 1u) != 0u) ? 0xa29f4338u : 0x71f30417u); // 38 add
|
||||
r7 = r7 ^ r6; // 39 xor
|
||||
r7 = r7 ^ r3; // 40 xor
|
||||
r3 = rotr_var(r3, r4); // 41 rotr
|
||||
r5 = r5 ^ r3; // 42 xor
|
||||
r3 = rotr_var(r3, r6); // 43 rotr
|
||||
r1 = r3 * r5 + r1; // 44 mad
|
||||
r7 = r7 + r3 + ((((sel >> 29u) & 1u) != 0u) ? 0xa907b90bu : 0xc1ae8d3bu); // 45 add
|
||||
r7 = r7 | r2; // 46 or
|
||||
{ uint t_; IGNEUM_SHFL_XOR(t_, r3, 16u); r2 = r2 ^ t_; } // 47 shfl
|
||||
r1 = r1 ^ ds[r7 & mask]; // 48 load
|
||||
r5 = r5 - r1; // 49 sub
|
||||
r3 = r3 ^ ds[r1 & mask]; // 50 load
|
||||
r2 = r2 - r3; // 51 sub
|
||||
{ uint t_; IGNEUM_SHFL_XOR(t_, r2, 2u); r6 = r6 ^ t_; } // 52 shfl
|
||||
r2 = r2 - r0; // 53 sub
|
||||
r0 = r0 ^ ds[r3 & mask]; // 54 load
|
||||
{ uint t_; IGNEUM_SHFL_XOR(t_, r1, 16u); r2 = r2 ^ t_; } // 55 shfl
|
||||
r0 = r0 + r6 + ((((sel >> 27u) & 1u) != 0u) ? 0xf4689674u : 0x25955401u); // 56 add
|
||||
r2 = mul_hi(r2, r0); // 57 mulhi
|
||||
r4 = mul_hi(r4, r2); // 58 mulhi
|
||||
r2 = r6 * r7 + r2; // 59 mad
|
||||
r3 = r3 ^ r1; // 60 xor
|
||||
r4 = r4 * r2; // 61 mul
|
||||
r0 = r0 ^ ds[r2 & mask]; // 62 load
|
||||
r7 = mul_hi(r7, r0); // 63 mulhi
|
||||
}
|
||||
uint lo = r0 ^ rotl_imm(r1, 7u) ^ rotl_imm(r2, 14u) ^ rotl_imm(r3, 21u);
|
||||
uint hi = r4 ^ rotl_imm(r5, 9u) ^ rotl_imm(r6, 18u) ^ rotl_imm(r7, 27u);
|
||||
out[gid] = ((ulong)hi << 32) | (ulong)lo;
|
||||
}
|
||||
|
||||
#if IGNEUM_EXCHANGE != 0
|
||||
// Reports the sub-group size this device uses for a work-group of IGNEUM_GROUP items. host.c runs it only when the
|
||||
// per-kernel query (clGetKernelSubGroupInfoKHR on igneum_hash) is unavailable; that query is preferred because a
|
||||
// compiler may pick a different wave width per kernel (RDNA: wave32 or wave64). See WAVEFRONT.md.
|
||||
IGNEUM_KERNEL_HASH void igneum_probe_subgroup(__global uint* out) {
|
||||
if (get_local_id(0) == 0u) { out[0] = get_sub_group_size(); out[1] = get_num_sub_groups(); }
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,10 +1,17 @@
|
|||
// Generated by proto-metal/igneum-bench --export-pack for seed "igneum-genesis". Do not edit by hand.
|
||||
// Memory-hard dataset core, the same text that the Mac's Metal kernels and CPU verifier were checked against.
|
||||
// Included by kernel.cu (device) and host.cu (host reference). See proto-metal/MEMHARD.md for the construction.
|
||||
// Included by kernel.cu (device), host.cu (host reference) and proto-opencl/host.c (C99 host reference).
|
||||
// See proto-metal/MEMHARD.md for the construction. kernel.cl carries the same text in OpenCL C.
|
||||
#pragma once
|
||||
#ifdef __cplusplus
|
||||
#include <cstdint>
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
#if defined(__CUDACC__)
|
||||
#define IGNEUM_HD __host__ __device__ __forceinline__
|
||||
#elif defined(_MSC_VER) && !defined(__cplusplus)
|
||||
#define IGNEUM_HD static __inline
|
||||
#else
|
||||
#define IGNEUM_HD static inline
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
// Generated by proto-metal/igneum-bench --export-pack for seed "igneum-genesis". Do not edit by hand.
|
||||
// Program metadata for host.cu plus the launch wrappers defined in kernel.cu.
|
||||
// Also included by proto-opencl/host.c (C99), which defines IGNEUM_NO_CUDA first and reads only the macros.
|
||||
#pragma once
|
||||
#include <cuda_runtime.h>
|
||||
#ifdef __cplusplus
|
||||
#include <cstdint>
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
#ifndef IGNEUM_NO_CUDA
|
||||
#include <cuda_runtime.h>
|
||||
#endif
|
||||
|
||||
#define IGNEUM_SEED_STRING "igneum-genesis"
|
||||
#define IGNEUM_DAY_STRING "2026-10-03"
|
||||
|
|
@ -29,9 +36,11 @@
|
|||
#define IGNEUM_MIX_MUL_INIT { 0x42146205u, 0x52cbe0fbu, 0x7ecf4a03u, 0x6728907fu, 0xd81d9751u, 0x132952c3u, 0xf60de277u, 0x05358035u, 0xbaf6499du, 0xe4db9667u, 0x3e98f45du, 0xd0004eddu, 0x2691630du, 0x9beb3bcfu, 0xab310379u, 0x99cfb423u }
|
||||
#define IGNEUM_MIX_RC_INIT { 0xbab68293u, 0xcc162340u, 0x6ce151ccu, 0xe62b8997u, 0xc9c80297u, 0xf74a1654u, 0x3d704af5u, 0x3cf522b7u, 0x2b9cac04u, 0xa880ac10u, 0x13e5dd1du, 0x6fc3e233u, 0x2d83eeacu, 0x9006e8bfu, 0x2c4b5362u, 0x31b49ee2u }
|
||||
|
||||
#ifndef IGNEUM_NO_CUDA
|
||||
// Defined in kernel.cu. All launch on the default stream and return cudaGetLastError().
|
||||
cudaError_t igneum_launch_cache_fill(uint32_t* cache, uint32_t nSegments);
|
||||
cudaError_t igneum_launch_build(uint32_t* ds, const uint32_t* cache, uint32_t nItems);
|
||||
cudaError_t igneum_launch_hash(const uint32_t* ds, uint64_t* out, uint32_t baseNonce, uint32_t mask,
|
||||
uint32_t nonces, uint32_t blockWarps);
|
||||
cudaError_t igneum_hash_info(int* numRegs, int* blocksPerSM, uint32_t blockWarps);
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
// Generated by proto-metal/igneum-bench --export-pack for seed "igneum-genesis". Do not edit by hand.
|
||||
// Expected outputs: proto-metal CPU interpreter (cpuWarp, memory-hard dataset) on Apple M5 Max; Metal GPU cross-check PASS 3/3 warps
|
||||
#pragma once
|
||||
#ifdef __cplusplus
|
||||
#include <cstdint>
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
|
||||
#define IGNEUM_VEC_WARPS 3
|
||||
static const uint32_t IGNEUM_VEC_BASE[IGNEUM_VEC_WARPS] = { 0u, 4096u, 1000000u };
|
||||
|
|
|
|||
176
proto-cuda/packs/igneum-genesis/kernel.cl
Normal file
176
proto-cuda/packs/igneum-genesis/kernel.cl
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
// Generated by proto-metal/igneum-bench --export-pack for seed "igneum-genesis". Do not edit by hand.
|
||||
// OpenCL C twin of the Metal kernel for the same seed (see proto-opencl/README.md, WAVEFRONT.md and program.metal).
|
||||
// Built from source at runtime by proto-opencl/host.c, which passes these defines:
|
||||
// IGNEUM_GROUP work-group size of igneum_hash, a multiple of 32 (default 32: one work-group = one 32-lane unit)
|
||||
// IGNEUM_EXCHANGE 0 = local-memory exchange with a barrier (any device, any wave width; the default)
|
||||
// 1 = sub_group_shuffle_xor (cl_khr_subgroup_shuffle), only with IGNEUM_GROUP 32 and a sub-group size of exactly 32
|
||||
// 2 = intel_sub_group_shuffle_xor (cl_intel_subgroups), same condition
|
||||
// The verification unit is always 32 lanes. A 64-wide hardware wave (AMD GCN/CDNA, RDNA in wave64) runs two units;
|
||||
// the exchange masks are 1, 2, 4, 8, 16, so every partner lane lies inside the lane's own aligned run of 32.
|
||||
#ifndef IGNEUM_GROUP
|
||||
#define IGNEUM_GROUP 32
|
||||
#endif
|
||||
#ifndef IGNEUM_EXCHANGE
|
||||
#define IGNEUM_EXCHANGE 0
|
||||
#endif
|
||||
#ifdef __OPENCL_VERSION__
|
||||
#define IGNEUM_KERNEL_HASH __kernel __attribute__((reqd_work_group_size(IGNEUM_GROUP, 1, 1)))
|
||||
#define IGNEUM_LOCAL_WORDS(name, n) __local uint name[n]
|
||||
#if IGNEUM_EXCHANGE == 1
|
||||
#ifdef cl_khr_subgroups
|
||||
#pragma OPENCL EXTENSION cl_khr_subgroups : enable
|
||||
#endif
|
||||
#ifdef cl_khr_subgroup_shuffle
|
||||
#pragma OPENCL EXTENSION cl_khr_subgroup_shuffle : enable
|
||||
#endif
|
||||
#elif IGNEUM_EXCHANGE == 2
|
||||
#pragma OPENCL EXTENSION cl_intel_subgroups : enable
|
||||
#endif
|
||||
#else
|
||||
// Not an OpenCL compiler: proto-opencl/emu compiles this file as C++ and supplies the built-ins and these two macros.
|
||||
#include "emu_opencl.h"
|
||||
#endif
|
||||
|
||||
#if IGNEUM_EXCHANGE == 1
|
||||
#define IGNEUM_SHFL_XOR(dst, a, m) dst = sub_group_shuffle_xor((a), (uint)(m))
|
||||
#define IGNEUM_BCAST0(dst, a) dst = sub_group_broadcast((a), 0u)
|
||||
#elif IGNEUM_EXCHANGE == 2
|
||||
#define IGNEUM_SHFL_XOR(dst, a, m) dst = intel_sub_group_shuffle_xor((a), (uint)(m))
|
||||
#define IGNEUM_BCAST0(dst, a) dst = sub_group_broadcast((a), 0u)
|
||||
#else
|
||||
// Local-memory exchange. Two buffers of IGNEUM_GROUP words alternate (xk counts exchanges), so one barrier per
|
||||
// exchange is enough: a lane can only overwrite buffer b at exchange k+2 after passing barrier k+1, and every lane
|
||||
// reaches barrier k+1 only after its read of buffer b at exchange k. The partner lid ^ m stays inside the lane's
|
||||
// aligned run of 32 because m < 32. Control flow is uniform, so every work-item reaches every barrier.
|
||||
#define IGNEUM_SHFL_XOR(dst, a, m) { xch[(xk & 1u) * IGNEUM_GROUP + lid] = (a); barrier(CLK_LOCAL_MEM_FENCE); dst = xch[(xk & 1u) * IGNEUM_GROUP + (lid ^ (uint)(m))]; xk += 1u; }
|
||||
#define IGNEUM_BCAST0(dst, a) { xch[(xk & 1u) * IGNEUM_GROUP + lid] = (a); barrier(CLK_LOCAL_MEM_FENCE); dst = xch[(xk & 1u) * IGNEUM_GROUP + (lid & ~31u)]; xk += 1u; }
|
||||
#endif
|
||||
|
||||
static inline uint splitmix32(uint x) {
|
||||
x ^= x >> 16; x *= 0x7feb352du;
|
||||
x ^= x >> 15; x *= 0x846ca68bu;
|
||||
x ^= x >> 16;
|
||||
return x;
|
||||
}
|
||||
// n is a literal in 1..31 at every call site. OpenCL rotate() rotates left by n modulo 32.
|
||||
static inline uint rotl_imm(uint x, uint n) { return rotate(x, n); }
|
||||
// Right rotation by n modulo 32 as a left rotation by (32 - n) modulo 32; n == 0 gives x.
|
||||
static inline uint rotr_var(uint x, uint n) { return rotate(x, (0u - n) & 31u); }
|
||||
static inline uint ds_elem(uint i, uint d0, uint d1) {
|
||||
uint x = i ^ d0;
|
||||
x *= 0x9E3779B1u; x ^= x >> 15;
|
||||
x += d1;
|
||||
x *= 0x85EBCA77u; x ^= x >> 13;
|
||||
x *= 0xC2B2AE3Du; x ^= x >> 16;
|
||||
return x;
|
||||
}
|
||||
|
||||
// dataset[i] = ds_elem(i, d0, d1) for i < n. Same closed form as the Metal igneum_fill kernel.
|
||||
__kernel void igneum_fill(__global uint* ds, uint n, uint d0, uint d1) {
|
||||
uint i = (uint)get_global_id(0);
|
||||
if (i < n) ds[i] = ds_elem(i, d0, d1);
|
||||
}
|
||||
|
||||
// One hash per work-item. IGNEUM_GROUP is a multiple of 32; lane = lid & 31 and every exchange stays inside the
|
||||
// lane's own aligned run of 32 work-items, exactly like simd_shuffle_xor inside a 32-wide Metal SIMD group and
|
||||
// __shfl_xor_sync inside a CUDA warp. Control flow is uniform (no branches at all).
|
||||
IGNEUM_KERNEL_HASH void igneum_hash(__global const uint* ds, __global ulong* out, uint baseNonce, uint mask) {
|
||||
uint gid = (uint)get_global_id(0);
|
||||
uint lid = (uint)get_local_id(0);
|
||||
uint nonce = baseNonce + gid;
|
||||
uint r0, r1, r2, r3, r4, r5, r6, r7;
|
||||
#if IGNEUM_EXCHANGE == 0
|
||||
IGNEUM_LOCAL_WORDS(xch, 2 * IGNEUM_GROUP);
|
||||
uint xk = 0u;
|
||||
#else
|
||||
(void)lid;
|
||||
#endif
|
||||
{ uint x = nonce ^ 0x67a9a7beu; x += 0x9e3779b9u; x = splitmix32(x); r0 = x ^ 0x1a155b25u; } // SEEDW[0], 0x9e3779b9u * 1u, SEEDW[1]
|
||||
{ uint x = nonce ^ 0x1a155b25u; x += 0x3c6ef372u; x = splitmix32(x); r1 = x ^ 0xfddfb732u; } // SEEDW[1], 0x9e3779b9u * 2u, SEEDW[2]
|
||||
{ uint x = nonce ^ 0xfddfb732u; x += 0xdaa66d2bu; x = splitmix32(x); r2 = x ^ 0x4b5af2e8u; } // SEEDW[2], 0x9e3779b9u * 3u, SEEDW[3]
|
||||
{ uint x = nonce ^ 0x4b5af2e8u; x += 0x78dde6e4u; x = splitmix32(x); r3 = x ^ 0xc55caf33u; } // SEEDW[3], 0x9e3779b9u * 4u, SEEDW[4]
|
||||
{ uint x = nonce ^ 0xc55caf33u; x += 0x1715609du; x = splitmix32(x); r4 = x ^ 0xa27c13b7u; } // SEEDW[4], 0x9e3779b9u * 5u, SEEDW[5]
|
||||
{ uint x = nonce ^ 0xa27c13b7u; x += 0xb54cda56u; x = splitmix32(x); r5 = x ^ 0x06628a48u; } // SEEDW[5], 0x9e3779b9u * 6u, SEEDW[6]
|
||||
{ uint x = nonce ^ 0x06628a48u; x += 0x5384540fu; x = splitmix32(x); r6 = x ^ 0x03852469u; } // SEEDW[6], 0x9e3779b9u * 7u, SEEDW[7]
|
||||
{ uint x = nonce ^ 0x03852469u; x += 0xf1bbcdc8u; x = splitmix32(x); r7 = x ^ 0x67a9a7beu; } // SEEDW[7], 0x9e3779b9u * 8u, SEEDW[0]
|
||||
|
||||
for (uint it = 0u; it < 8u; ++it) {
|
||||
uint sel = r0;
|
||||
r4 = rotl_imm(r4, 25u); // 0 rotl
|
||||
r0 = r0 - r5; // 1 sub
|
||||
r4 = r4 ^ ds[r3 & mask]; // 2 load
|
||||
r1 = rotl_imm(r1, 1u); // 3 rotl
|
||||
r2 = r2 + r3 + ((((sel >> 26u) & 1u) != 0u) ? 0x2735a174u : 0x61f0b51cu); // 4 add
|
||||
r5 = r5 ^ ds[r3 & mask]; // 5 load
|
||||
r5 = r5 - r7; // 6 sub
|
||||
r3 = r3 + r4 + ((((sel >> 26u) & 1u) != 0u) ? 0x5a069596u : 0x52f2dbf4u); // 7 add
|
||||
r0 = r0 ^ r4; // 8 xor
|
||||
r4 = r4 ^ r0; // 9 xor
|
||||
r2 = r2 ^ ds[r0 & mask]; // 10 load
|
||||
{ uint t_; IGNEUM_SHFL_XOR(t_, r6, 16u); r4 = r4 ^ t_; } // 11 shfl
|
||||
r1 = r1 - r5; // 12 sub
|
||||
r2 = r2 ^ r1; // 13 xor
|
||||
r4 = r4 ^ ds[r5 & mask]; // 14 load
|
||||
r2 = r2 ^ ds[r4 & mask]; // 15 load
|
||||
r3 = r3 ^ ds[r0 & mask]; // 16 load
|
||||
r4 = r4 ^ r6; // 17 xor
|
||||
r2 = r4 * r6 + r2; // 18 mad
|
||||
r6 = rotr_var(r6, r1); // 19 rotr
|
||||
r3 = r3 ^ r4; // 20 xor
|
||||
r1 = r3 * r5 + r1; // 21 mad
|
||||
r7 = mul_hi(r7, r4); // 22 mulhi
|
||||
r5 = mul_hi(r5, r2); // 23 mulhi
|
||||
r0 = r0 ^ ds[r6 & mask]; // 24 load
|
||||
r5 = r5 * r6; // 25 mul
|
||||
r7 = r7 ^ ds[r1 & mask]; // 26 load
|
||||
r3 = rotr_var(r3, r1); // 27 rotr
|
||||
{ uint t_; IGNEUM_SHFL_XOR(t_, r1, 8u); r5 = r5 ^ t_; } // 28 shfl
|
||||
r7 = r7 ^ r5; // 29 xor
|
||||
r7 = rotl_imm(r7, 23u); // 30 rotl
|
||||
r2 = r2 - r0; // 31 sub
|
||||
r7 = r7 ^ r2; // 32 xor
|
||||
r2 = r2 ^ r6; // 33 xor
|
||||
r6 = r6 ^ ds[r1 & mask]; // 34 load
|
||||
r1 = r1 ^ r4; // 35 xor
|
||||
{ uint t_; IGNEUM_SHFL_XOR(t_, r0, 1u); r3 = r3 ^ t_; } // 36 shfl
|
||||
r2 = r2 * r6; // 37 mul
|
||||
r5 = r5 + r3 + ((((sel >> 12u) & 1u) != 0u) ? 0xa29f4338u : 0x71f30417u); // 38 add
|
||||
r7 = r7 ^ r6; // 39 xor
|
||||
r7 = r7 ^ r3; // 40 xor
|
||||
r3 = rotr_var(r3, r4); // 41 rotr
|
||||
r5 = r5 ^ r3; // 42 xor
|
||||
r3 = rotr_var(r3, r6); // 43 rotr
|
||||
r1 = r3 * r5 + r1; // 44 mad
|
||||
r7 = r7 + r3 + ((((sel >> 29u) & 1u) != 0u) ? 0xa907b90bu : 0xc1ae8d3bu); // 45 add
|
||||
r7 = r7 | r2; // 46 or
|
||||
{ uint t_; IGNEUM_SHFL_XOR(t_, r3, 16u); r2 = r2 ^ t_; } // 47 shfl
|
||||
r1 = r1 ^ ds[r7 & mask]; // 48 load
|
||||
r5 = r5 - r1; // 49 sub
|
||||
r3 = r3 ^ ds[r1 & mask]; // 50 load
|
||||
r2 = r2 - r3; // 51 sub
|
||||
{ uint t_; IGNEUM_SHFL_XOR(t_, r2, 2u); r6 = r6 ^ t_; } // 52 shfl
|
||||
r2 = r2 - r0; // 53 sub
|
||||
r0 = r0 ^ ds[r3 & mask]; // 54 load
|
||||
{ uint t_; IGNEUM_SHFL_XOR(t_, r1, 16u); r2 = r2 ^ t_; } // 55 shfl
|
||||
r0 = r0 + r6 + ((((sel >> 27u) & 1u) != 0u) ? 0xf4689674u : 0x25955401u); // 56 add
|
||||
r2 = mul_hi(r2, r0); // 57 mulhi
|
||||
r4 = mul_hi(r4, r2); // 58 mulhi
|
||||
r2 = r6 * r7 + r2; // 59 mad
|
||||
r3 = r3 ^ r1; // 60 xor
|
||||
r4 = r4 * r2; // 61 mul
|
||||
r0 = r0 ^ ds[r2 & mask]; // 62 load
|
||||
r7 = mul_hi(r7, r0); // 63 mulhi
|
||||
}
|
||||
uint lo = r0 ^ rotl_imm(r1, 7u) ^ rotl_imm(r2, 14u) ^ rotl_imm(r3, 21u);
|
||||
uint hi = r4 ^ rotl_imm(r5, 9u) ^ rotl_imm(r6, 18u) ^ rotl_imm(r7, 27u);
|
||||
out[gid] = ((ulong)hi << 32) | (ulong)lo;
|
||||
}
|
||||
|
||||
#if IGNEUM_EXCHANGE != 0
|
||||
// Reports the sub-group size this device uses for a work-group of IGNEUM_GROUP items. host.c runs it only when the
|
||||
// per-kernel query (clGetKernelSubGroupInfoKHR on igneum_hash) is unavailable; that query is preferred because a
|
||||
// compiler may pick a different wave width per kernel (RDNA: wave32 or wave64). See WAVEFRONT.md.
|
||||
IGNEUM_KERNEL_HASH void igneum_probe_subgroup(__global uint* out) {
|
||||
if (get_local_id(0) == 0u) { out[0] = get_sub_group_size(); out[1] = get_num_sub_groups(); }
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,8 +1,15 @@
|
|||
// Generated by proto-metal/igneum-bench --export-pack for seed "igneum-genesis". Do not edit by hand.
|
||||
// Program metadata for host.cu plus the launch wrappers defined in kernel.cu.
|
||||
// Also included by proto-opencl/host.c (C99), which defines IGNEUM_NO_CUDA first and reads only the macros.
|
||||
#pragma once
|
||||
#include <cuda_runtime.h>
|
||||
#ifdef __cplusplus
|
||||
#include <cstdint>
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
#ifndef IGNEUM_NO_CUDA
|
||||
#include <cuda_runtime.h>
|
||||
#endif
|
||||
|
||||
#define IGNEUM_SEED_STRING "igneum-genesis"
|
||||
#define IGNEUM_DAY_STRING "2026-10-03"
|
||||
|
|
@ -14,12 +21,16 @@
|
|||
#define IGNEUM_ITERATIONS 8
|
||||
#define IGNEUM_INSTR_COUNT 64
|
||||
#define IGNEUM_LOADS_PER_HASH 104
|
||||
#define IGNEUM_WIDE_LOADS_PER_HASH 0
|
||||
#define IGNEUM_OP_MIX "load=13 xor=13 sub=7 shfl=6 add=5 mulhi=5 mad=4 rotr=4 mul=3 rotl=3 or=1"
|
||||
// 0 = closed-form dataset (ds_elem), 1 = memory-hard cache construction (MEMHARD.md, memhard.h)
|
||||
#define IGNEUM_DATASET_MODE 0
|
||||
|
||||
#define IGNEUM_SEEDW_INIT { 0x67a9a7beu, 0x1a155b25u, 0xfddfb732u, 0x4b5af2e8u, 0xc55caf33u, 0xa27c13b7u, 0x06628a48u, 0x03852469u }
|
||||
|
||||
#ifndef IGNEUM_NO_CUDA
|
||||
// Defined in kernel.cu. Both launch on the default stream and return cudaGetLastError().
|
||||
cudaError_t igneum_launch_fill(uint32_t* ds, uint32_t nWords, uint32_t d0, uint32_t d1);
|
||||
cudaError_t igneum_launch_hash(const uint32_t* ds, uint64_t* out, uint32_t baseNonce, uint32_t mask,
|
||||
uint32_t nonces, uint32_t blockWarps);
|
||||
cudaError_t igneum_hash_info(int* numRegs, int* blocksPerSM, uint32_t blockWarps);
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"format": "igneum-program-pack-1",
|
||||
"format": "igneum-program-pack-2",
|
||||
"dataset_mode": "closed-form",
|
||||
"seed": "igneum-genesis",
|
||||
"seed_words": ["0x67a9a7be", "0x1a155b25", "0xfddfb732", "0x4b5af2e8", "0xc55caf33", "0xa27c13b7", "0x06628a48", "0x03852469"],
|
||||
"seed_derivation": "FNV-1a 64 over UTF-8 of seed, basis ^ (salt * 0x9E3779B97F4A7C15) for salt 0..3, then h ^= h>>33; h *= 0xff51afd7ed558ccd; h ^= h>>33; words[2*salt] = low 32, words[2*salt+1] = high 32",
|
||||
|
|
@ -24,7 +25,8 @@
|
|||
"rotr": "dst = rotr(dst, src & 31)",
|
||||
"mad": "dst = src * src2 + dst",
|
||||
"shfl": "dst = dst ^ (src of lane (lane ^ mask)), mask in {1,2,4,8,16}, within the 32-lane warp",
|
||||
"load": "dst = dst ^ dataset[src & dataset.mask]"
|
||||
"load": "dst = dst ^ dataset[src & dataset.mask]",
|
||||
"wload": "base = (src of lane 0 & dataset.mask) & ~31; dst = dst ^ dataset[base + lane] (warp-coalesced 128-byte load, lever b, only when --wide-frac > 0)"
|
||||
},
|
||||
"dataset": {
|
||||
"log2_words": 28,
|
||||
|
|
@ -34,6 +36,7 @@
|
|||
"day_words_from": "day/2026-10-03",
|
||||
"d0": "0x3067619f",
|
||||
"d1": "0x3c269176",
|
||||
"mode": "closed-form",
|
||||
"formula": "x = i ^ d0; x *= 0x9E3779B1; x ^= x>>15; x += d1; x *= 0x85EBCA77; x ^= x>>13; x *= 0xC2B2AE3D; x ^= x>>16 (all mod 2^32)"
|
||||
},
|
||||
"instructions": [
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
// Generated by proto-metal/igneum-bench --export-pack for seed "igneum-genesis". Do not edit by hand.
|
||||
// Expected outputs: proto-metal CPU interpreter (cpuWarp) on Apple M5 Max; Metal GPU cross-check PASS 3/3 warps
|
||||
// Expected outputs: proto-metal CPU interpreter (cpuWarp, closed-form dataset) on Apple M5 Max; Metal GPU cross-check PASS 3/3 warps
|
||||
#pragma once
|
||||
#ifdef __cplusplus
|
||||
#include <cstdint>
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
|
||||
#define IGNEUM_VEC_WARPS 3
|
||||
static const uint32_t IGNEUM_VEC_BASE[IGNEUM_VEC_WARPS] = { 0u, 4096u, 1000000u };
|
||||
|
|
@ -33,3 +37,11 @@ static const uint32_t IGNEUM_DS_HEAD[16] = {
|
|||
};
|
||||
static const uint32_t IGNEUM_DS_LAST_INDEX = 268435455u;
|
||||
static const uint32_t IGNEUM_DS_LAST = 0xf78c84a4u;
|
||||
// 64 sampled dataset words (index, value) computed on the Mac.
|
||||
#define IGNEUM_DS_SAMPLES 64
|
||||
static const uint32_t IGNEUM_DS_SAMPLE_INDEX[IGNEUM_DS_SAMPLES] = {
|
||||
59471966u, 217795994u, 208353206u, 42483309u, 172547758u, 148076330u, 183853158u, 214389424u, 267488061u, 169781097u, 184093494u, 153880993u, 84977930u, 46426879u, 3093825u, 225364072u, 44593546u, 260713159u, 168250303u, 52384140u, 223401610u, 45554030u, 95410555u, 175039924u, 79171087u, 267580473u, 24168642u, 37981670u, 171551130u, 195559979u, 204611762u, 140997658u, 138925853u, 86637313u, 20736778u, 219665210u, 160430336u, 264654675u, 8013395u, 228945585u, 213884386u, 104419827u, 44185464u, 142737231u, 99284897u, 132475900u, 61861762u, 132056166u, 262388043u, 91878046u, 117353561u, 124768597u, 71352993u, 190698941u, 46055428u, 55281366u, 165145231u, 106810753u, 171985651u, 232085256u, 159510492u, 40072060u, 209107596u, 39023794u
|
||||
};
|
||||
static const uint32_t IGNEUM_DS_SAMPLE_VALUE[IGNEUM_DS_SAMPLES] = {
|
||||
0x3ef2f18du, 0xed632a6bu, 0x2daad897u, 0x1201639fu, 0x8e79ca54u, 0x488d4f3du, 0x5206fa5cu, 0xf60c5f15u, 0x5af790f7u, 0xdb48b2cau, 0x2b55a0e4u, 0xc8c5100fu, 0xe4afb4a4u, 0x35a42586u, 0xff4cb6d1u, 0x1d6cac9fu, 0x889230c1u, 0x95316e2au, 0x61b099e9u, 0x97b89bb2u, 0x05171be5u, 0x7f0157abu, 0x13825368u, 0x562ab1bfu, 0x6e3b06b8u, 0xc9b51434u, 0xc994b628u, 0x26643aa8u, 0x56ae558cu, 0xdc8fdaaau, 0x2ed21c3fu, 0x3a29b91bu, 0xfd98ca1eu, 0x44c7dd8au, 0xaab155c8u, 0x0db02165u, 0x637c813au, 0x5648617eu, 0x3e163337u, 0x64bf0ad7u, 0x43381384u, 0x824dca58u, 0xab7bf62eu, 0x10880075u, 0x292498dau, 0x925fea4du, 0x606466a0u, 0x37923fc4u, 0x3544915bu, 0x742b205du, 0x3228e7adu, 0x1ce5ec92u, 0xd7857a5cu, 0x200a560bu, 0xc29076c7u, 0x886a1a06u, 0x1f415038u, 0x60fa6028u, 0x1fb1d0f9u, 0xee203b87u, 0xa12f7ea5u, 0x6ecd91dcu, 0x260e00b4u, 0xef4a4067u
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
{
|
||||
"seed": "igneum-genesis",
|
||||
"day": "2026-10-03",
|
||||
"dataset_mode": "closed-form",
|
||||
"dataset_log2_words": 28,
|
||||
"mask": "0x0fffffff",
|
||||
"lanes": 32,
|
||||
"source": "proto-metal CPU interpreter (cpuWarp) on Apple M5 Max; Metal GPU cross-check PASS 3/3 warps",
|
||||
"source": "proto-metal CPU interpreter (cpuWarp, closed-form dataset) on Apple M5 Max; Metal GPU cross-check PASS 3/3 warps",
|
||||
"warps": [
|
||||
{"base_nonce": 0, "expected": [
|
||||
"0x2941e93c76cb1910", "0xc0099c34df8280ae", "0xe61da8a237797181", "0xfb944794eaa8ea1d", "0x0093934e41befb07", "0xd4ba2031c0385915", "0x75a8a43902cee166", "0xb4ce2d6829aaa27c",
|
||||
|
|
@ -27,5 +28,6 @@
|
|||
],
|
||||
"dataset_head": ["0x82174c0f", "0x577bdb9c", "0x111053bf", "0x2bb85514", "0xd83e190f", "0x8ccd9427", "0x3f69d5e4", "0xf3bbe1dc", "0xb3aa7e90", "0xc33f5f73", "0xb83a2b10", "0xc4d7c8ff", "0xefa6d1a8", "0x7029b116", "0x5e48fab0", "0x0ef66e40"],
|
||||
"dataset_last_index": 268435455,
|
||||
"dataset_last": "0xf78c84a4"
|
||||
"dataset_last": "0xf78c84a4",
|
||||
"dataset_samples": [{"index": 59471966, "value": "0x3ef2f18d"}, {"index": 217795994, "value": "0xed632a6b"}, {"index": 208353206, "value": "0x2daad897"}, {"index": 42483309, "value": "0x1201639f"}, {"index": 172547758, "value": "0x8e79ca54"}, {"index": 148076330, "value": "0x488d4f3d"}, {"index": 183853158, "value": "0x5206fa5c"}, {"index": 214389424, "value": "0xf60c5f15"}, {"index": 267488061, "value": "0x5af790f7"}, {"index": 169781097, "value": "0xdb48b2ca"}, {"index": 184093494, "value": "0x2b55a0e4"}, {"index": 153880993, "value": "0xc8c5100f"}, {"index": 84977930, "value": "0xe4afb4a4"}, {"index": 46426879, "value": "0x35a42586"}, {"index": 3093825, "value": "0xff4cb6d1"}, {"index": 225364072, "value": "0x1d6cac9f"}, {"index": 44593546, "value": "0x889230c1"}, {"index": 260713159, "value": "0x95316e2a"}, {"index": 168250303, "value": "0x61b099e9"}, {"index": 52384140, "value": "0x97b89bb2"}, {"index": 223401610, "value": "0x05171be5"}, {"index": 45554030, "value": "0x7f0157ab"}, {"index": 95410555, "value": "0x13825368"}, {"index": 175039924, "value": "0x562ab1bf"}, {"index": 79171087, "value": "0x6e3b06b8"}, {"index": 267580473, "value": "0xc9b51434"}, {"index": 24168642, "value": "0xc994b628"}, {"index": 37981670, "value": "0x26643aa8"}, {"index": 171551130, "value": "0x56ae558c"}, {"index": 195559979, "value": "0xdc8fdaaa"}, {"index": 204611762, "value": "0x2ed21c3f"}, {"index": 140997658, "value": "0x3a29b91b"}, {"index": 138925853, "value": "0xfd98ca1e"}, {"index": 86637313, "value": "0x44c7dd8a"}, {"index": 20736778, "value": "0xaab155c8"}, {"index": 219665210, "value": "0x0db02165"}, {"index": 160430336, "value": "0x637c813a"}, {"index": 264654675, "value": "0x5648617e"}, {"index": 8013395, "value": "0x3e163337"}, {"index": 228945585, "value": "0x64bf0ad7"}, {"index": 213884386, "value": "0x43381384"}, {"index": 104419827, "value": "0x824dca58"}, {"index": 44185464, "value": "0xab7bf62e"}, {"index": 142737231, "value": "0x10880075"}, {"index": 99284897, "value": "0x292498da"}, {"index": 132475900, "value": "0x925fea4d"}, {"index": 61861762, "value": "0x606466a0"}, {"index": 132056166, "value": "0x37923fc4"}, {"index": 262388043, "value": "0x3544915b"}, {"index": 91878046, "value": "0x742b205d"}, {"index": 117353561, "value": "0x3228e7ad"}, {"index": 124768597, "value": "0x1ce5ec92"}, {"index": 71352993, "value": "0xd7857a5c"}, {"index": 190698941, "value": "0x200a560b"}, {"index": 46055428, "value": "0xc29076c7"}, {"index": 55281366, "value": "0x886a1a06"}, {"index": 165145231, "value": "0x1f415038"}, {"index": 106810753, "value": "0x60fa6028"}, {"index": 171985651, "value": "0x1fb1d0f9"}, {"index": 232085256, "value": "0xee203b87"}, {"index": 159510492, "value": "0xa12f7ea5"}, {"index": 40072060, "value": "0x6ecd91dc"}, {"index": 209107596, "value": "0x260e00b4"}, {"index": 39023794, "value": "0xef4a4067"}]
|
||||
}
|
||||
|
|
|
|||
176
proto-cuda/packs/igneum-hourly/kernel.cl
Normal file
176
proto-cuda/packs/igneum-hourly/kernel.cl
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
// Generated by proto-metal/igneum-bench --export-pack for seed "igneum-hourly". Do not edit by hand.
|
||||
// OpenCL C twin of the Metal kernel for the same seed (see proto-opencl/README.md, WAVEFRONT.md and program.metal).
|
||||
// Built from source at runtime by proto-opencl/host.c, which passes these defines:
|
||||
// IGNEUM_GROUP work-group size of igneum_hash, a multiple of 32 (default 32: one work-group = one 32-lane unit)
|
||||
// IGNEUM_EXCHANGE 0 = local-memory exchange with a barrier (any device, any wave width; the default)
|
||||
// 1 = sub_group_shuffle_xor (cl_khr_subgroup_shuffle), only with IGNEUM_GROUP 32 and a sub-group size of exactly 32
|
||||
// 2 = intel_sub_group_shuffle_xor (cl_intel_subgroups), same condition
|
||||
// The verification unit is always 32 lanes. A 64-wide hardware wave (AMD GCN/CDNA, RDNA in wave64) runs two units;
|
||||
// the exchange masks are 1, 2, 4, 8, 16, so every partner lane lies inside the lane's own aligned run of 32.
|
||||
#ifndef IGNEUM_GROUP
|
||||
#define IGNEUM_GROUP 32
|
||||
#endif
|
||||
#ifndef IGNEUM_EXCHANGE
|
||||
#define IGNEUM_EXCHANGE 0
|
||||
#endif
|
||||
#ifdef __OPENCL_VERSION__
|
||||
#define IGNEUM_KERNEL_HASH __kernel __attribute__((reqd_work_group_size(IGNEUM_GROUP, 1, 1)))
|
||||
#define IGNEUM_LOCAL_WORDS(name, n) __local uint name[n]
|
||||
#if IGNEUM_EXCHANGE == 1
|
||||
#ifdef cl_khr_subgroups
|
||||
#pragma OPENCL EXTENSION cl_khr_subgroups : enable
|
||||
#endif
|
||||
#ifdef cl_khr_subgroup_shuffle
|
||||
#pragma OPENCL EXTENSION cl_khr_subgroup_shuffle : enable
|
||||
#endif
|
||||
#elif IGNEUM_EXCHANGE == 2
|
||||
#pragma OPENCL EXTENSION cl_intel_subgroups : enable
|
||||
#endif
|
||||
#else
|
||||
// Not an OpenCL compiler: proto-opencl/emu compiles this file as C++ and supplies the built-ins and these two macros.
|
||||
#include "emu_opencl.h"
|
||||
#endif
|
||||
|
||||
#if IGNEUM_EXCHANGE == 1
|
||||
#define IGNEUM_SHFL_XOR(dst, a, m) dst = sub_group_shuffle_xor((a), (uint)(m))
|
||||
#define IGNEUM_BCAST0(dst, a) dst = sub_group_broadcast((a), 0u)
|
||||
#elif IGNEUM_EXCHANGE == 2
|
||||
#define IGNEUM_SHFL_XOR(dst, a, m) dst = intel_sub_group_shuffle_xor((a), (uint)(m))
|
||||
#define IGNEUM_BCAST0(dst, a) dst = sub_group_broadcast((a), 0u)
|
||||
#else
|
||||
// Local-memory exchange. Two buffers of IGNEUM_GROUP words alternate (xk counts exchanges), so one barrier per
|
||||
// exchange is enough: a lane can only overwrite buffer b at exchange k+2 after passing barrier k+1, and every lane
|
||||
// reaches barrier k+1 only after its read of buffer b at exchange k. The partner lid ^ m stays inside the lane's
|
||||
// aligned run of 32 because m < 32. Control flow is uniform, so every work-item reaches every barrier.
|
||||
#define IGNEUM_SHFL_XOR(dst, a, m) { xch[(xk & 1u) * IGNEUM_GROUP + lid] = (a); barrier(CLK_LOCAL_MEM_FENCE); dst = xch[(xk & 1u) * IGNEUM_GROUP + (lid ^ (uint)(m))]; xk += 1u; }
|
||||
#define IGNEUM_BCAST0(dst, a) { xch[(xk & 1u) * IGNEUM_GROUP + lid] = (a); barrier(CLK_LOCAL_MEM_FENCE); dst = xch[(xk & 1u) * IGNEUM_GROUP + (lid & ~31u)]; xk += 1u; }
|
||||
#endif
|
||||
|
||||
static inline uint splitmix32(uint x) {
|
||||
x ^= x >> 16; x *= 0x7feb352du;
|
||||
x ^= x >> 15; x *= 0x846ca68bu;
|
||||
x ^= x >> 16;
|
||||
return x;
|
||||
}
|
||||
// n is a literal in 1..31 at every call site. OpenCL rotate() rotates left by n modulo 32.
|
||||
static inline uint rotl_imm(uint x, uint n) { return rotate(x, n); }
|
||||
// Right rotation by n modulo 32 as a left rotation by (32 - n) modulo 32; n == 0 gives x.
|
||||
static inline uint rotr_var(uint x, uint n) { return rotate(x, (0u - n) & 31u); }
|
||||
static inline uint ds_elem(uint i, uint d0, uint d1) {
|
||||
uint x = i ^ d0;
|
||||
x *= 0x9E3779B1u; x ^= x >> 15;
|
||||
x += d1;
|
||||
x *= 0x85EBCA77u; x ^= x >> 13;
|
||||
x *= 0xC2B2AE3Du; x ^= x >> 16;
|
||||
return x;
|
||||
}
|
||||
|
||||
// dataset[i] = ds_elem(i, d0, d1) for i < n. Same closed form as the Metal igneum_fill kernel.
|
||||
__kernel void igneum_fill(__global uint* ds, uint n, uint d0, uint d1) {
|
||||
uint i = (uint)get_global_id(0);
|
||||
if (i < n) ds[i] = ds_elem(i, d0, d1);
|
||||
}
|
||||
|
||||
// One hash per work-item. IGNEUM_GROUP is a multiple of 32; lane = lid & 31 and every exchange stays inside the
|
||||
// lane's own aligned run of 32 work-items, exactly like simd_shuffle_xor inside a 32-wide Metal SIMD group and
|
||||
// __shfl_xor_sync inside a CUDA warp. Control flow is uniform (no branches at all).
|
||||
IGNEUM_KERNEL_HASH void igneum_hash(__global const uint* ds, __global ulong* out, uint baseNonce, uint mask) {
|
||||
uint gid = (uint)get_global_id(0);
|
||||
uint lid = (uint)get_local_id(0);
|
||||
uint nonce = baseNonce + gid;
|
||||
uint r0, r1, r2, r3, r4, r5, r6, r7;
|
||||
#if IGNEUM_EXCHANGE == 0
|
||||
IGNEUM_LOCAL_WORDS(xch, 2 * IGNEUM_GROUP);
|
||||
uint xk = 0u;
|
||||
#else
|
||||
(void)lid;
|
||||
#endif
|
||||
{ uint x = nonce ^ 0x6bdee811u; x += 0x9e3779b9u; x = splitmix32(x); r0 = x ^ 0x8f488bbeu; } // SEEDW[0], 0x9e3779b9u * 1u, SEEDW[1]
|
||||
{ uint x = nonce ^ 0x8f488bbeu; x += 0x3c6ef372u; x = splitmix32(x); r1 = x ^ 0xc5cdece7u; } // SEEDW[1], 0x9e3779b9u * 2u, SEEDW[2]
|
||||
{ uint x = nonce ^ 0xc5cdece7u; x += 0xdaa66d2bu; x = splitmix32(x); r2 = x ^ 0x210af22du; } // SEEDW[2], 0x9e3779b9u * 3u, SEEDW[3]
|
||||
{ uint x = nonce ^ 0x210af22du; x += 0x78dde6e4u; x = splitmix32(x); r3 = x ^ 0x2f687b65u; } // SEEDW[3], 0x9e3779b9u * 4u, SEEDW[4]
|
||||
{ uint x = nonce ^ 0x2f687b65u; x += 0x1715609du; x = splitmix32(x); r4 = x ^ 0x17471eeeu; } // SEEDW[4], 0x9e3779b9u * 5u, SEEDW[5]
|
||||
{ uint x = nonce ^ 0x17471eeeu; x += 0xb54cda56u; x = splitmix32(x); r5 = x ^ 0xee16e284u; } // SEEDW[5], 0x9e3779b9u * 6u, SEEDW[6]
|
||||
{ uint x = nonce ^ 0xee16e284u; x += 0x5384540fu; x = splitmix32(x); r6 = x ^ 0xfc9eb8f9u; } // SEEDW[6], 0x9e3779b9u * 7u, SEEDW[7]
|
||||
{ uint x = nonce ^ 0xfc9eb8f9u; x += 0xf1bbcdc8u; x = splitmix32(x); r7 = x ^ 0x6bdee811u; } // SEEDW[7], 0x9e3779b9u * 8u, SEEDW[0]
|
||||
|
||||
for (uint it = 0u; it < 8u; ++it) {
|
||||
uint sel = r0;
|
||||
r6 = r6 ^ ds[r5 & mask]; // 0 load
|
||||
r3 = r3 ^ r7; // 1 xor
|
||||
r6 = mul_hi(r6, r2); // 2 mulhi
|
||||
r1 = r1 + r0 + ((((sel >> 0u) & 1u) != 0u) ? 0x43f8f369u : 0x1eb46b1cu); // 3 add
|
||||
r3 = r3 ^ ds[r0 & mask]; // 4 load
|
||||
r5 = r5 | r7; // 5 or
|
||||
r4 = r4 ^ ds[r6 & mask]; // 6 load
|
||||
r4 = rotl_imm(r4, 21u); // 7 rotl
|
||||
r6 = r6 ^ ds[r3 & mask]; // 8 load
|
||||
r6 = r6 ^ ds[r1 & mask]; // 9 load
|
||||
{ uint t_; IGNEUM_SHFL_XOR(t_, r3, 8u); r0 = r0 ^ t_; } // 10 shfl
|
||||
r2 = r2 ^ r3; // 11 xor
|
||||
r2 = r2 + r7 + ((((sel >> 19u) & 1u) != 0u) ? 0xc26c7c2au : 0x3a1ce85eu); // 12 add
|
||||
r4 = r4 ^ ds[r3 & mask]; // 13 load
|
||||
r7 = r7 ^ ds[r1 & mask]; // 14 load
|
||||
r2 = mul_hi(r2, r3); // 15 mulhi
|
||||
r5 = r5 ^ ds[r2 & mask]; // 16 load
|
||||
r5 = r5 ^ ds[r1 & mask]; // 17 load
|
||||
r4 = r4 ^ ds[r7 & mask]; // 18 load
|
||||
r2 = rotr_var(r2, r1); // 19 rotr
|
||||
r7 = r7 ^ ds[r0 & mask]; // 20 load
|
||||
r4 = r4 ^ ds[r6 & mask]; // 21 load
|
||||
r7 = rotl_imm(r7, 25u); // 22 rotl
|
||||
r3 = r3 + r5 + ((((sel >> 15u) & 1u) != 0u) ? 0x98ae0055u : 0x942b819bu); // 23 add
|
||||
r3 = mul_hi(r3, r4); // 24 mulhi
|
||||
r6 = r6 ^ r1; // 25 xor
|
||||
r1 = rotl_imm(r1, 31u); // 26 rotl
|
||||
{ uint t_; IGNEUM_SHFL_XOR(t_, r6, 4u); r3 = r3 ^ t_; } // 27 shfl
|
||||
r6 = r6 - r5; // 28 sub
|
||||
r6 = rotr_var(r6, r3); // 29 rotr
|
||||
{ uint t_; IGNEUM_SHFL_XOR(t_, r4, 4u); r0 = r0 ^ t_; } // 30 shfl
|
||||
r4 = rotl_imm(r4, 30u); // 31 rotl
|
||||
r2 = r2 - r1; // 32 sub
|
||||
r5 = r5 | r4; // 33 or
|
||||
r7 = r6 * r3 + r7; // 34 mad
|
||||
r5 = r5 * r0; // 35 mul
|
||||
r5 = r5 - r3; // 36 sub
|
||||
r2 = r2 + r7 + ((((sel >> 5u) & 1u) != 0u) ? 0x6b5970b5u : 0x473ecfd5u); // 37 add
|
||||
r2 = r2 ^ ds[r7 & mask]; // 38 load
|
||||
r2 = rotr_var(r2, r6); // 39 rotr
|
||||
r0 = r0 ^ r5; // 40 xor
|
||||
{ uint t_; IGNEUM_SHFL_XOR(t_, r5, 8u); r4 = r4 ^ t_; } // 41 shfl
|
||||
r1 = r1 * r6; // 42 mul
|
||||
r0 = r4 * r1 + r0; // 43 mad
|
||||
r1 = r1 + r4 + ((((sel >> 20u) & 1u) != 0u) ? 0x4a502c22u : 0x04e78f3bu); // 44 add
|
||||
r6 = r2 * r7 + r6; // 45 mad
|
||||
r1 = r1 ^ r0; // 46 xor
|
||||
r5 = r5 ^ ds[r7 & mask]; // 47 load
|
||||
r0 = r0 | r4; // 48 or
|
||||
{ uint t_; IGNEUM_SHFL_XOR(t_, r4, 16u); r5 = r5 ^ t_; } // 49 shfl
|
||||
r7 = r7 + r0 + ((((sel >> 17u) & 1u) != 0u) ? 0x6fabf9ceu : 0x0a3df170u); // 50 add
|
||||
r6 = r6 ^ r3; // 51 xor
|
||||
r1 = r1 + r2 + ((((sel >> 21u) & 1u) != 0u) ? 0xad344ca0u : 0xc99bce6fu); // 52 add
|
||||
r6 = r6 * r7; // 53 mul
|
||||
r3 = mul_hi(r3, r4); // 54 mulhi
|
||||
r7 = r7 * r1; // 55 mul
|
||||
r7 = r7 ^ r1; // 56 xor
|
||||
r2 = r2 * r7; // 57 mul
|
||||
r2 = r2 ^ ds[r1 & mask]; // 58 load
|
||||
r7 = r4 * r5 + r7; // 59 mad
|
||||
r2 = r2 ^ ds[r7 & mask]; // 60 load
|
||||
r0 = r2 * r3 + r0; // 61 mad
|
||||
r1 = mul_hi(r1, r5); // 62 mulhi
|
||||
r7 = r7 + r3 + ((((sel >> 24u) & 1u) != 0u) ? 0x05e4fc1du : 0xc23e27c9u); // 63 add
|
||||
}
|
||||
uint lo = r0 ^ rotl_imm(r1, 7u) ^ rotl_imm(r2, 14u) ^ rotl_imm(r3, 21u);
|
||||
uint hi = r4 ^ rotl_imm(r5, 9u) ^ rotl_imm(r6, 18u) ^ rotl_imm(r7, 27u);
|
||||
out[gid] = ((ulong)hi << 32) | (ulong)lo;
|
||||
}
|
||||
|
||||
#if IGNEUM_EXCHANGE != 0
|
||||
// Reports the sub-group size this device uses for a work-group of IGNEUM_GROUP items. host.c runs it only when the
|
||||
// per-kernel query (clGetKernelSubGroupInfoKHR on igneum_hash) is unavailable; that query is preferred because a
|
||||
// compiler may pick a different wave width per kernel (RDNA: wave32 or wave64). See WAVEFRONT.md.
|
||||
IGNEUM_KERNEL_HASH void igneum_probe_subgroup(__global uint* out) {
|
||||
if (get_local_id(0) == 0u) { out[0] = get_sub_group_size(); out[1] = get_num_sub_groups(); }
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,8 +1,15 @@
|
|||
// Generated by proto-metal/igneum-bench --export-pack for seed "igneum-hourly". Do not edit by hand.
|
||||
// Program metadata for host.cu plus the launch wrappers defined in kernel.cu.
|
||||
// Also included by proto-opencl/host.c (C99), which defines IGNEUM_NO_CUDA first and reads only the macros.
|
||||
#pragma once
|
||||
#include <cuda_runtime.h>
|
||||
#ifdef __cplusplus
|
||||
#include <cstdint>
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
#ifndef IGNEUM_NO_CUDA
|
||||
#include <cuda_runtime.h>
|
||||
#endif
|
||||
|
||||
#define IGNEUM_SEED_STRING "igneum-hourly"
|
||||
#define IGNEUM_DAY_STRING "2026-10-03"
|
||||
|
|
@ -14,12 +21,16 @@
|
|||
#define IGNEUM_ITERATIONS 8
|
||||
#define IGNEUM_INSTR_COUNT 64
|
||||
#define IGNEUM_LOADS_PER_HASH 128
|
||||
#define IGNEUM_WIDE_LOADS_PER_HASH 0
|
||||
#define IGNEUM_OP_MIX "load=16 add=8 xor=7 mad=5 mul=5 mulhi=5 shfl=5 rotl=4 or=3 rotr=3 sub=3"
|
||||
// 0 = closed-form dataset (ds_elem), 1 = memory-hard cache construction (MEMHARD.md, memhard.h)
|
||||
#define IGNEUM_DATASET_MODE 0
|
||||
|
||||
#define IGNEUM_SEEDW_INIT { 0x6bdee811u, 0x8f488bbeu, 0xc5cdece7u, 0x210af22du, 0x2f687b65u, 0x17471eeeu, 0xee16e284u, 0xfc9eb8f9u }
|
||||
|
||||
#ifndef IGNEUM_NO_CUDA
|
||||
// Defined in kernel.cu. Both launch on the default stream and return cudaGetLastError().
|
||||
cudaError_t igneum_launch_fill(uint32_t* ds, uint32_t nWords, uint32_t d0, uint32_t d1);
|
||||
cudaError_t igneum_launch_hash(const uint32_t* ds, uint64_t* out, uint32_t baseNonce, uint32_t mask,
|
||||
uint32_t nonces, uint32_t blockWarps);
|
||||
cudaError_t igneum_hash_info(int* numRegs, int* blocksPerSM, uint32_t blockWarps);
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"format": "igneum-program-pack-1",
|
||||
"format": "igneum-program-pack-2",
|
||||
"dataset_mode": "closed-form",
|
||||
"seed": "igneum-hourly",
|
||||
"seed_words": ["0x6bdee811", "0x8f488bbe", "0xc5cdece7", "0x210af22d", "0x2f687b65", "0x17471eee", "0xee16e284", "0xfc9eb8f9"],
|
||||
"seed_derivation": "FNV-1a 64 over UTF-8 of seed, basis ^ (salt * 0x9E3779B97F4A7C15) for salt 0..3, then h ^= h>>33; h *= 0xff51afd7ed558ccd; h ^= h>>33; words[2*salt] = low 32, words[2*salt+1] = high 32",
|
||||
|
|
@ -24,7 +25,8 @@
|
|||
"rotr": "dst = rotr(dst, src & 31)",
|
||||
"mad": "dst = src * src2 + dst",
|
||||
"shfl": "dst = dst ^ (src of lane (lane ^ mask)), mask in {1,2,4,8,16}, within the 32-lane warp",
|
||||
"load": "dst = dst ^ dataset[src & dataset.mask]"
|
||||
"load": "dst = dst ^ dataset[src & dataset.mask]",
|
||||
"wload": "base = (src of lane 0 & dataset.mask) & ~31; dst = dst ^ dataset[base + lane] (warp-coalesced 128-byte load, lever b, only when --wide-frac > 0)"
|
||||
},
|
||||
"dataset": {
|
||||
"log2_words": 28,
|
||||
|
|
@ -34,6 +36,7 @@
|
|||
"day_words_from": "day/2026-10-03",
|
||||
"d0": "0x3067619f",
|
||||
"d1": "0x3c269176",
|
||||
"mode": "closed-form",
|
||||
"formula": "x = i ^ d0; x *= 0x9E3779B1; x ^= x>>15; x += d1; x *= 0x85EBCA77; x ^= x>>13; x *= 0xC2B2AE3D; x ^= x>>16 (all mod 2^32)"
|
||||
},
|
||||
"instructions": [
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
// Generated by proto-metal/igneum-bench --export-pack for seed "igneum-hourly". Do not edit by hand.
|
||||
// Expected outputs: proto-metal CPU interpreter (cpuWarp) on Apple M5 Max; Metal GPU cross-check PASS 3/3 warps
|
||||
// Expected outputs: proto-metal CPU interpreter (cpuWarp, closed-form dataset) on Apple M5 Max; Metal GPU cross-check PASS 3/3 warps
|
||||
#pragma once
|
||||
#ifdef __cplusplus
|
||||
#include <cstdint>
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
|
||||
#define IGNEUM_VEC_WARPS 3
|
||||
static const uint32_t IGNEUM_VEC_BASE[IGNEUM_VEC_WARPS] = { 0u, 4096u, 1000000u };
|
||||
|
|
@ -33,3 +37,11 @@ static const uint32_t IGNEUM_DS_HEAD[16] = {
|
|||
};
|
||||
static const uint32_t IGNEUM_DS_LAST_INDEX = 268435455u;
|
||||
static const uint32_t IGNEUM_DS_LAST = 0xf78c84a4u;
|
||||
// 64 sampled dataset words (index, value) computed on the Mac.
|
||||
#define IGNEUM_DS_SAMPLES 64
|
||||
static const uint32_t IGNEUM_DS_SAMPLE_INDEX[IGNEUM_DS_SAMPLES] = {
|
||||
59471966u, 217795994u, 208353206u, 42483309u, 172547758u, 148076330u, 183853158u, 214389424u, 267488061u, 169781097u, 184093494u, 153880993u, 84977930u, 46426879u, 3093825u, 225364072u, 44593546u, 260713159u, 168250303u, 52384140u, 223401610u, 45554030u, 95410555u, 175039924u, 79171087u, 267580473u, 24168642u, 37981670u, 171551130u, 195559979u, 204611762u, 140997658u, 138925853u, 86637313u, 20736778u, 219665210u, 160430336u, 264654675u, 8013395u, 228945585u, 213884386u, 104419827u, 44185464u, 142737231u, 99284897u, 132475900u, 61861762u, 132056166u, 262388043u, 91878046u, 117353561u, 124768597u, 71352993u, 190698941u, 46055428u, 55281366u, 165145231u, 106810753u, 171985651u, 232085256u, 159510492u, 40072060u, 209107596u, 39023794u
|
||||
};
|
||||
static const uint32_t IGNEUM_DS_SAMPLE_VALUE[IGNEUM_DS_SAMPLES] = {
|
||||
0x3ef2f18du, 0xed632a6bu, 0x2daad897u, 0x1201639fu, 0x8e79ca54u, 0x488d4f3du, 0x5206fa5cu, 0xf60c5f15u, 0x5af790f7u, 0xdb48b2cau, 0x2b55a0e4u, 0xc8c5100fu, 0xe4afb4a4u, 0x35a42586u, 0xff4cb6d1u, 0x1d6cac9fu, 0x889230c1u, 0x95316e2au, 0x61b099e9u, 0x97b89bb2u, 0x05171be5u, 0x7f0157abu, 0x13825368u, 0x562ab1bfu, 0x6e3b06b8u, 0xc9b51434u, 0xc994b628u, 0x26643aa8u, 0x56ae558cu, 0xdc8fdaaau, 0x2ed21c3fu, 0x3a29b91bu, 0xfd98ca1eu, 0x44c7dd8au, 0xaab155c8u, 0x0db02165u, 0x637c813au, 0x5648617eu, 0x3e163337u, 0x64bf0ad7u, 0x43381384u, 0x824dca58u, 0xab7bf62eu, 0x10880075u, 0x292498dau, 0x925fea4du, 0x606466a0u, 0x37923fc4u, 0x3544915bu, 0x742b205du, 0x3228e7adu, 0x1ce5ec92u, 0xd7857a5cu, 0x200a560bu, 0xc29076c7u, 0x886a1a06u, 0x1f415038u, 0x60fa6028u, 0x1fb1d0f9u, 0xee203b87u, 0xa12f7ea5u, 0x6ecd91dcu, 0x260e00b4u, 0xef4a4067u
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
{
|
||||
"seed": "igneum-hourly",
|
||||
"day": "2026-10-03",
|
||||
"dataset_mode": "closed-form",
|
||||
"dataset_log2_words": 28,
|
||||
"mask": "0x0fffffff",
|
||||
"lanes": 32,
|
||||
"source": "proto-metal CPU interpreter (cpuWarp) on Apple M5 Max; Metal GPU cross-check PASS 3/3 warps",
|
||||
"source": "proto-metal CPU interpreter (cpuWarp, closed-form dataset) on Apple M5 Max; Metal GPU cross-check PASS 3/3 warps",
|
||||
"warps": [
|
||||
{"base_nonce": 0, "expected": [
|
||||
"0x787a737506455bbe", "0xf93d248e364f7e1e", "0x8a7ce1701b5d0489", "0xd4f5b2177c414f3f", "0xe88fdf6fb22aee5c", "0xd68bc0baf30b41fc", "0x87f6a81eb0a6ac69", "0xb3cd8b6e6e57b347",
|
||||
|
|
@ -27,5 +28,6 @@
|
|||
],
|
||||
"dataset_head": ["0x82174c0f", "0x577bdb9c", "0x111053bf", "0x2bb85514", "0xd83e190f", "0x8ccd9427", "0x3f69d5e4", "0xf3bbe1dc", "0xb3aa7e90", "0xc33f5f73", "0xb83a2b10", "0xc4d7c8ff", "0xefa6d1a8", "0x7029b116", "0x5e48fab0", "0x0ef66e40"],
|
||||
"dataset_last_index": 268435455,
|
||||
"dataset_last": "0xf78c84a4"
|
||||
"dataset_last": "0xf78c84a4",
|
||||
"dataset_samples": [{"index": 59471966, "value": "0x3ef2f18d"}, {"index": 217795994, "value": "0xed632a6b"}, {"index": 208353206, "value": "0x2daad897"}, {"index": 42483309, "value": "0x1201639f"}, {"index": 172547758, "value": "0x8e79ca54"}, {"index": 148076330, "value": "0x488d4f3d"}, {"index": 183853158, "value": "0x5206fa5c"}, {"index": 214389424, "value": "0xf60c5f15"}, {"index": 267488061, "value": "0x5af790f7"}, {"index": 169781097, "value": "0xdb48b2ca"}, {"index": 184093494, "value": "0x2b55a0e4"}, {"index": 153880993, "value": "0xc8c5100f"}, {"index": 84977930, "value": "0xe4afb4a4"}, {"index": 46426879, "value": "0x35a42586"}, {"index": 3093825, "value": "0xff4cb6d1"}, {"index": 225364072, "value": "0x1d6cac9f"}, {"index": 44593546, "value": "0x889230c1"}, {"index": 260713159, "value": "0x95316e2a"}, {"index": 168250303, "value": "0x61b099e9"}, {"index": 52384140, "value": "0x97b89bb2"}, {"index": 223401610, "value": "0x05171be5"}, {"index": 45554030, "value": "0x7f0157ab"}, {"index": 95410555, "value": "0x13825368"}, {"index": 175039924, "value": "0x562ab1bf"}, {"index": 79171087, "value": "0x6e3b06b8"}, {"index": 267580473, "value": "0xc9b51434"}, {"index": 24168642, "value": "0xc994b628"}, {"index": 37981670, "value": "0x26643aa8"}, {"index": 171551130, "value": "0x56ae558c"}, {"index": 195559979, "value": "0xdc8fdaaa"}, {"index": 204611762, "value": "0x2ed21c3f"}, {"index": 140997658, "value": "0x3a29b91b"}, {"index": 138925853, "value": "0xfd98ca1e"}, {"index": 86637313, "value": "0x44c7dd8a"}, {"index": 20736778, "value": "0xaab155c8"}, {"index": 219665210, "value": "0x0db02165"}, {"index": 160430336, "value": "0x637c813a"}, {"index": 264654675, "value": "0x5648617e"}, {"index": 8013395, "value": "0x3e163337"}, {"index": 228945585, "value": "0x64bf0ad7"}, {"index": 213884386, "value": "0x43381384"}, {"index": 104419827, "value": "0x824dca58"}, {"index": 44185464, "value": "0xab7bf62e"}, {"index": 142737231, "value": "0x10880075"}, {"index": 99284897, "value": "0x292498da"}, {"index": 132475900, "value": "0x925fea4d"}, {"index": 61861762, "value": "0x606466a0"}, {"index": 132056166, "value": "0x37923fc4"}, {"index": 262388043, "value": "0x3544915b"}, {"index": 91878046, "value": "0x742b205d"}, {"index": 117353561, "value": "0x3228e7ad"}, {"index": 124768597, "value": "0x1ce5ec92"}, {"index": 71352993, "value": "0xd7857a5c"}, {"index": 190698941, "value": "0x200a560b"}, {"index": 46055428, "value": "0xc29076c7"}, {"index": 55281366, "value": "0x886a1a06"}, {"index": 165145231, "value": "0x1f415038"}, {"index": 106810753, "value": "0x60fa6028"}, {"index": 171985651, "value": "0x1fb1d0f9"}, {"index": 232085256, "value": "0xee203b87"}, {"index": 159510492, "value": "0xa12f7ea5"}, {"index": 40072060, "value": "0x6ecd91dc"}, {"index": 209107596, "value": "0x260e00b4"}, {"index": 39023794, "value": "0xef4a4067"}]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ func parseArgs() -> Options {
|
|||
[--closed-form] original closed-form dataset (default: memory-hard cache construction, see MEMHARD.md)
|
||||
[--load-weight W] generator lever (a): percent weight of the load op (default 25)
|
||||
[--wide-frac P] generator lever (b): percent of loads emitted as warp-coalesced 128-byte loads (default 0)
|
||||
[--export-pack <dir>] write the CUDA program pack for --seed, then exit
|
||||
[--export-pack <dir>] write the CUDA + OpenCL program pack for --seed, then exit
|
||||
hardening tests (run instead of the bench; several may be combined; exit 0 only if all pass):
|
||||
[--fuzz N [--fuzz-seed <string>]] N random programs, GPU vs CPU, 4 random warps each,
|
||||
dataset size drawn from 64 MiB, 256 MiB, 1 GiB
|
||||
|
|
@ -420,16 +420,21 @@ func generateProgram(seedString: String) -> Program {
|
|||
|
||||
func hex(_ v: UInt32) -> String { String(format: "0x%08xu", v) }
|
||||
|
||||
// The memory-hard core as source text, in Metal (cuda false) or CUDA C++ (cuda true). Mixer parameters are
|
||||
// literals so the GPU kernels, the CUDA pack and its host reference share one text. Names are prefixed mh_.
|
||||
// The memory-hard core as source text, in Metal, CUDA C++ or OpenCL C. Mixer parameters are
|
||||
// literals so the GPU kernels, the CUDA pack, the OpenCL pack and the host reference share one text. Names are prefixed mh_.
|
||||
// In the CUDA dialect every function is IGNEUM_HD (host and device) so host.cu can derive items too.
|
||||
func emitMemhardCore(_ mp: MixParams, cuda: Bool) -> String {
|
||||
let U = cuda ? "uint32_t" : "uint"
|
||||
let fn = cuda ? "IGNEUM_HD" : "inline"
|
||||
let cptr = cuda ? "const uint32_t*" : "device const uint*"
|
||||
let wptr = cuda ? "uint32_t*" : "device uint*"
|
||||
let lptr = cuda ? "uint32_t*" : "thread uint*"
|
||||
let lcptr = cuda ? "const uint32_t*" : "const thread uint*"
|
||||
// In the OpenCL dialect the cache pointers carry the __global address space (OpenCL C 1.2 has no generic space).
|
||||
enum CoreDialect { case metal, cuda, opencl }
|
||||
|
||||
func emitMemhardCore(_ mp: MixParams, cuda: Bool) -> String { emitMemhardCore(mp, dialect: cuda ? .cuda : .metal) }
|
||||
|
||||
func emitMemhardCore(_ mp: MixParams, dialect: CoreDialect) -> String {
|
||||
let U: String, fn: String, cptr: String, wptr: String, lptr: String, lcptr: String
|
||||
switch dialect {
|
||||
case .metal: (U, fn, cptr, wptr, lptr, lcptr) = ("uint", "inline", "device const uint*", "device uint*", "thread uint*", "const thread uint*")
|
||||
case .cuda: (U, fn, cptr, wptr, lptr, lcptr) = ("uint32_t", "IGNEUM_HD", "const uint32_t*", "uint32_t*", "uint32_t*", "const uint32_t*")
|
||||
case .opencl: (U, fn, cptr, wptr, lptr, lcptr) = ("uint", "static inline", "__global const uint*", "__global uint*", "uint*", "const uint*")
|
||||
}
|
||||
let K = mp.keyWords, R = mp.rotWords, M = mp.mulWords, C = mp.rcWords
|
||||
var s = """
|
||||
// Memory-hard dataset core (MEMHARD.md). Cache: 2^\(cacheLog2Words) words in 2^\(Int(log2(Double(cacheSegments)))) segments of \(cacheLinesPerSegment) chained ChaCha\(chachaRounds) lines.
|
||||
|
|
@ -722,7 +727,7 @@ func cpuWarpTraced(_ p: Program, baseNonce: UInt32, ds: DatasetSource,
|
|||
|
||||
// MARK: - Program pack export (CUDA twin of the Metal kernel)
|
||||
//
|
||||
// Writes, for one seed: program.json, vectors.json, kernel.cu, program.h, vectors.h, program.metal.
|
||||
// Writes, for one seed: program.json, vectors.json, kernel.cu, kernel.cl, program.h, vectors.h, program.metal.
|
||||
// The CUDA kernel is emitted from the same Instr list as the MSL above, line for line.
|
||||
// Differences by design: the dataset mask is a kernel argument (so the host can sweep dataset
|
||||
// sizes with one ahead-of-time compile), seeds are inlined as literals, and the host launch
|
||||
|
|
@ -910,15 +915,192 @@ func generateCUDA(_ p: Program, memhard: MixParams?) -> String {
|
|||
return s
|
||||
}
|
||||
|
||||
// kernel.cl: the same program as OpenCL C 1.2, built from source at runtime by proto-opencl/host.c.
|
||||
// The 32-lane exchange is selected at compile time (IGNEUM_EXCHANGE): sub-group shuffles where the device has them
|
||||
// and its sub-group size is exactly 32, or a local-memory exchange with a barrier on every other device
|
||||
// (proto-opencl/WAVEFRONT.md). The same text is compiled as C++ by proto-opencl/emu with a 32- or 64-wide sub-group.
|
||||
func generateOpenCL(_ p: Program, memhard: MixParams?) -> String {
|
||||
var s = """
|
||||
// Generated by proto-metal/igneum-bench --export-pack for seed "\(p.seedString)". Do not edit by hand.
|
||||
// OpenCL C twin of the Metal kernel for the same seed (see proto-opencl/README.md, WAVEFRONT.md and program.metal).
|
||||
// Built from source at runtime by proto-opencl/host.c, which passes these defines:
|
||||
// IGNEUM_GROUP work-group size of igneum_hash, a multiple of 32 (default 32: one work-group = one 32-lane unit)
|
||||
// IGNEUM_EXCHANGE 0 = local-memory exchange with a barrier (any device, any wave width; the default)
|
||||
// 1 = sub_group_shuffle_xor (cl_khr_subgroup_shuffle), only with IGNEUM_GROUP 32 and a sub-group size of exactly 32
|
||||
// 2 = intel_sub_group_shuffle_xor (cl_intel_subgroups), same condition
|
||||
// The verification unit is always 32 lanes. A 64-wide hardware wave (AMD GCN/CDNA, RDNA in wave64) runs two units;
|
||||
// the exchange masks are 1, 2, 4, 8, 16, so every partner lane lies inside the lane's own aligned run of 32.
|
||||
#ifndef IGNEUM_GROUP
|
||||
#define IGNEUM_GROUP 32
|
||||
#endif
|
||||
#ifndef IGNEUM_EXCHANGE
|
||||
#define IGNEUM_EXCHANGE 0
|
||||
#endif
|
||||
#ifdef __OPENCL_VERSION__
|
||||
#define IGNEUM_KERNEL_HASH __kernel __attribute__((reqd_work_group_size(IGNEUM_GROUP, 1, 1)))
|
||||
#define IGNEUM_LOCAL_WORDS(name, n) __local uint name[n]
|
||||
#if IGNEUM_EXCHANGE == 1
|
||||
#ifdef cl_khr_subgroups
|
||||
#pragma OPENCL EXTENSION cl_khr_subgroups : enable
|
||||
#endif
|
||||
#ifdef cl_khr_subgroup_shuffle
|
||||
#pragma OPENCL EXTENSION cl_khr_subgroup_shuffle : enable
|
||||
#endif
|
||||
#elif IGNEUM_EXCHANGE == 2
|
||||
#pragma OPENCL EXTENSION cl_intel_subgroups : enable
|
||||
#endif
|
||||
#else
|
||||
// Not an OpenCL compiler: proto-opencl/emu compiles this file as C++ and supplies the built-ins and these two macros.
|
||||
#include "emu_opencl.h"
|
||||
#endif
|
||||
|
||||
#if IGNEUM_EXCHANGE == 1
|
||||
#define IGNEUM_SHFL_XOR(dst, a, m) dst = sub_group_shuffle_xor((a), (uint)(m))
|
||||
#define IGNEUM_BCAST0(dst, a) dst = sub_group_broadcast((a), 0u)
|
||||
#elif IGNEUM_EXCHANGE == 2
|
||||
#define IGNEUM_SHFL_XOR(dst, a, m) dst = intel_sub_group_shuffle_xor((a), (uint)(m))
|
||||
#define IGNEUM_BCAST0(dst, a) dst = sub_group_broadcast((a), 0u)
|
||||
#else
|
||||
// Local-memory exchange. Two buffers of IGNEUM_GROUP words alternate (xk counts exchanges), so one barrier per
|
||||
// exchange is enough: a lane can only overwrite buffer b at exchange k+2 after passing barrier k+1, and every lane
|
||||
// reaches barrier k+1 only after its read of buffer b at exchange k. The partner lid ^ m stays inside the lane's
|
||||
// aligned run of 32 because m < 32. Control flow is uniform, so every work-item reaches every barrier.
|
||||
#define IGNEUM_SHFL_XOR(dst, a, m) { xch[(xk & 1u) * IGNEUM_GROUP + lid] = (a); barrier(CLK_LOCAL_MEM_FENCE); dst = xch[(xk & 1u) * IGNEUM_GROUP + (lid ^ (uint)(m))]; xk += 1u; }
|
||||
#define IGNEUM_BCAST0(dst, a) { xch[(xk & 1u) * IGNEUM_GROUP + lid] = (a); barrier(CLK_LOCAL_MEM_FENCE); dst = xch[(xk & 1u) * IGNEUM_GROUP + (lid & ~31u)]; xk += 1u; }
|
||||
#endif
|
||||
|
||||
static inline uint splitmix32(uint x) {
|
||||
x ^= x >> 16; x *= 0x7feb352du;
|
||||
x ^= x >> 15; x *= 0x846ca68bu;
|
||||
x ^= x >> 16;
|
||||
return x;
|
||||
}
|
||||
// n is a literal in 1..31 at every call site. OpenCL rotate() rotates left by n modulo 32.
|
||||
static inline uint rotl_imm(uint x, uint n) { return rotate(x, n); }
|
||||
// Right rotation by n modulo 32 as a left rotation by (32 - n) modulo 32; n == 0 gives x.
|
||||
static inline uint rotr_var(uint x, uint n) { return rotate(x, (0u - n) & 31u); }
|
||||
static inline uint ds_elem(uint i, uint d0, uint d1) {
|
||||
uint x = i ^ d0;
|
||||
x *= 0x9E3779B1u; x ^= x >> 15;
|
||||
x += d1;
|
||||
x *= 0x85EBCA77u; x ^= x >> 13;
|
||||
x *= 0xC2B2AE3Du; x ^= x >> 16;
|
||||
return x;
|
||||
}
|
||||
|
||||
|
||||
"""
|
||||
if let mp = memhard {
|
||||
s += emitMemhardCore(mp, dialect: .opencl) + "\n"
|
||||
s += """
|
||||
// Memory-hard dataset (MEMHARD.md). One work-item per cache segment; one work-item per 64-byte dataset item.
|
||||
// The same constants as memhard.h in this pack (one emitter, three dialects).
|
||||
__kernel void igneum_cache_fill(__global uint* cache, uint nSegments) {
|
||||
uint seg = (uint)get_global_id(0);
|
||||
if (seg < nSegments) mh_cache_segment(cache, seg);
|
||||
}
|
||||
__kernel void igneum_build(__global uint* ds, __global const uint* cache, uint nItems) {
|
||||
uint t = (uint)get_global_id(0);
|
||||
if (t < nItems) {
|
||||
uint s[16];
|
||||
mh_item(cache, t, s);
|
||||
__global uint* d = ds + ((ulong)t * 16u);
|
||||
for (uint i = 0u; i < 16u; ++i) d[i] = s[i];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
"""
|
||||
} else {
|
||||
s += """
|
||||
// dataset[i] = ds_elem(i, d0, d1) for i < n. Same closed form as the Metal igneum_fill kernel.
|
||||
__kernel void igneum_fill(__global uint* ds, uint n, uint d0, uint d1) {
|
||||
uint i = (uint)get_global_id(0);
|
||||
if (i < n) ds[i] = ds_elem(i, d0, d1);
|
||||
}
|
||||
|
||||
|
||||
"""
|
||||
}
|
||||
s += """
|
||||
// One hash per work-item. IGNEUM_GROUP is a multiple of 32; lane = lid & 31 and every exchange stays inside the
|
||||
// lane's own aligned run of 32 work-items, exactly like simd_shuffle_xor inside a 32-wide Metal SIMD group and
|
||||
// __shfl_xor_sync inside a CUDA warp. Control flow is uniform (no branches at all).
|
||||
IGNEUM_KERNEL_HASH void igneum_hash(__global const uint* ds, __global ulong* out, uint baseNonce, uint mask) {
|
||||
uint gid = (uint)get_global_id(0);
|
||||
uint lid = (uint)get_local_id(0);
|
||||
uint nonce = baseNonce + gid;
|
||||
uint r0, r1, r2, r3, r4, r5, r6, r7;
|
||||
#if IGNEUM_EXCHANGE == 0
|
||||
IGNEUM_LOCAL_WORDS(xch, 2 * IGNEUM_GROUP);
|
||||
uint xk = 0u;
|
||||
#else
|
||||
(void)lid;
|
||||
#endif
|
||||
|
||||
"""
|
||||
if p.hasWide { s += " uint lane = lid & 31u;\n uint wmask = mask & ~31u;\n" }
|
||||
for i in 0..<8 {
|
||||
let addc = 0x9e3779b9 &* UInt32(i + 1)
|
||||
s += " { uint x = nonce ^ \(hex(p.seed[i])); x += \(hex(addc)); x = splitmix32(x); r\(i) = x ^ \(hex(p.seed[(i + 1) & 7])); } // SEEDW[\(i)], 0x9e3779b9u * \(i + 1)u, SEEDW[\((i + 1) & 7)]\n"
|
||||
}
|
||||
s += "\n for (uint it = 0u; it < \(Program.iterations)u; ++it) {\n uint sel = r0;\n"
|
||||
for (k, ins) in p.instrs.enumerated() {
|
||||
let d = "r\(ins.dst)", a = "r\(ins.a)", b = "r\(ins.b)"
|
||||
var line: String
|
||||
switch ins.op {
|
||||
// Metal select(A, B, c) returns c ? B : A, so the true branch is imm2 here as well.
|
||||
case .add: line = "\(d) = \(d) + \(a) + ((((sel >> \(ins.bit)u) & 1u) != 0u) ? \(hex(ins.imm2)) : \(hex(ins.imm)));"
|
||||
case .sub: line = "\(d) = \(d) - \(a);"
|
||||
case .mul: line = "\(d) = \(d) * \(a);"
|
||||
case .mulhi: line = "\(d) = mul_hi(\(d), \(a));"
|
||||
case .xor: line = "\(d) = \(d) ^ \(a);"
|
||||
case .or: line = "\(d) = \(d) | \(a);"
|
||||
case .rotl: line = "\(d) = rotl_imm(\(d), \(ins.rot)u);"
|
||||
case .rotr: line = "\(d) = rotr_var(\(d), \(a));"
|
||||
case .mad: line = "\(d) = \(a) * \(b) + \(d);"
|
||||
case .shfl: line = "{ uint t_; IGNEUM_SHFL_XOR(t_, \(a), \(ins.mask)u); \(d) = \(d) ^ t_; }"
|
||||
case .load: line = "\(d) = \(d) ^ ds[\(a) & mask];"
|
||||
case .wload: line = "{ uint t_; IGNEUM_BCAST0(t_, \(a)); \(d) = \(d) ^ ds[(t_ & wmask) + lane]; }"
|
||||
}
|
||||
s += " \(line) // \(k) \(ins.op.rawValue)\n"
|
||||
}
|
||||
s += """
|
||||
}
|
||||
uint lo = r0 ^ rotl_imm(r1, 7u) ^ rotl_imm(r2, 14u) ^ rotl_imm(r3, 21u);
|
||||
uint hi = r4 ^ rotl_imm(r5, 9u) ^ rotl_imm(r6, 18u) ^ rotl_imm(r7, 27u);
|
||||
out[gid] = ((ulong)hi << 32) | (ulong)lo;
|
||||
}
|
||||
|
||||
#if IGNEUM_EXCHANGE != 0
|
||||
// Reports the sub-group size this device uses for a work-group of IGNEUM_GROUP items. host.c runs it only when the
|
||||
// per-kernel query (clGetKernelSubGroupInfoKHR on igneum_hash) is unavailable; that query is preferred because a
|
||||
// compiler may pick a different wave width per kernel (RDNA: wave32 or wave64). See WAVEFRONT.md.
|
||||
IGNEUM_KERNEL_HASH void igneum_probe_subgroup(__global uint* out) {
|
||||
if (get_local_id(0) == 0u) { out[0] = get_sub_group_size(); out[1] = get_num_sub_groups(); }
|
||||
}
|
||||
#endif
|
||||
|
||||
"""
|
||||
return s
|
||||
}
|
||||
|
||||
func generateProgramHeader(_ p: Program, dayString: String, day: (UInt32, UInt32), datasetLog2: Int, memhard: MixParams?) -> String {
|
||||
let mask = UInt32((1 << datasetLog2) - 1)
|
||||
let mix = p.histogram.map { "\($0.0)=\($0.1)" }.joined(separator: " ")
|
||||
var s = """
|
||||
// Generated by proto-metal/igneum-bench --export-pack for seed "\(p.seedString)". Do not edit by hand.
|
||||
// Program metadata for host.cu plus the launch wrappers defined in kernel.cu.
|
||||
// Also included by proto-opencl/host.c (C99), which defines IGNEUM_NO_CUDA first and reads only the macros.
|
||||
#pragma once
|
||||
#include <cuda_runtime.h>
|
||||
#ifdef __cplusplus
|
||||
#include <cstdint>
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
#ifndef IGNEUM_NO_CUDA
|
||||
#include <cuda_runtime.h>
|
||||
#endif
|
||||
|
||||
#define IGNEUM_SEED_STRING \(jstr(p.seedString))
|
||||
#define IGNEUM_DAY_STRING \(jstr(dayString))
|
||||
|
|
@ -949,6 +1131,7 @@ func generateProgramHeader(_ p: Program, dayString: String, day: (UInt32, UInt32
|
|||
#define IGNEUM_MIX_MUL_INIT { \(mp.mulWords.map(hex).joined(separator: ", ")) }
|
||||
#define IGNEUM_MIX_RC_INIT { \(mp.rcWords.map(hex).joined(separator: ", ")) }
|
||||
|
||||
#ifndef IGNEUM_NO_CUDA
|
||||
// Defined in kernel.cu. All launch on the default stream and return cudaGetLastError().
|
||||
cudaError_t igneum_launch_cache_fill(uint32_t* cache, uint32_t nSegments);
|
||||
cudaError_t igneum_launch_build(uint32_t* ds, const uint32_t* cache, uint32_t nItems);
|
||||
|
|
@ -956,6 +1139,7 @@ func generateProgramHeader(_ p: Program, dayString: String, day: (UInt32, UInt32
|
|||
"""
|
||||
} else {
|
||||
s += """
|
||||
#ifndef IGNEUM_NO_CUDA
|
||||
// Defined in kernel.cu. Both launch on the default stream and return cudaGetLastError().
|
||||
cudaError_t igneum_launch_fill(uint32_t* ds, uint32_t nWords, uint32_t d0, uint32_t d1);
|
||||
|
||||
|
|
@ -965,6 +1149,7 @@ func generateProgramHeader(_ p: Program, dayString: String, day: (UInt32, UInt32
|
|||
cudaError_t igneum_launch_hash(const uint32_t* ds, uint64_t* out, uint32_t baseNonce, uint32_t mask,
|
||||
uint32_t nonces, uint32_t blockWarps);
|
||||
cudaError_t igneum_hash_info(int* numRegs, int* blocksPerSM, uint32_t blockWarps);
|
||||
#endif
|
||||
|
||||
"""
|
||||
return s
|
||||
|
|
@ -975,15 +1160,22 @@ func generateMemhardHeader(_ p: Program, _ mp: MixParams) -> String {
|
|||
return """
|
||||
// Generated by proto-metal/igneum-bench --export-pack for seed "\(p.seedString)". Do not edit by hand.
|
||||
// Memory-hard dataset core, the same text that the Mac's Metal kernels and CPU verifier were checked against.
|
||||
// Included by kernel.cu (device) and host.cu (host reference). See proto-metal/MEMHARD.md for the construction.
|
||||
// Included by kernel.cu (device), host.cu (host reference) and proto-opencl/host.c (C99 host reference).
|
||||
// See proto-metal/MEMHARD.md for the construction. kernel.cl carries the same text in OpenCL C.
|
||||
#pragma once
|
||||
#ifdef __cplusplus
|
||||
#include <cstdint>
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
#if defined(__CUDACC__)
|
||||
#define IGNEUM_HD __host__ __device__ __forceinline__
|
||||
#elif defined(_MSC_VER) && !defined(__cplusplus)
|
||||
#define IGNEUM_HD static __inline
|
||||
#else
|
||||
#define IGNEUM_HD static inline
|
||||
#endif
|
||||
\(emitMemhardCore(mp, cuda: true))
|
||||
\(emitMemhardCore(mp, dialect: .cuda))
|
||||
"""
|
||||
}
|
||||
|
||||
|
|
@ -1002,7 +1194,11 @@ func generateVectorsHeader(_ p: Program, bases: [UInt32], outs: [[UInt64]], v: P
|
|||
// Generated by proto-metal/igneum-bench --export-pack for seed "\(p.seedString)". Do not edit by hand.
|
||||
// Expected outputs: \(source)
|
||||
#pragma once
|
||||
#ifdef __cplusplus
|
||||
#include <cstdint>
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
|
||||
#define IGNEUM_VEC_WARPS \(bases.count)
|
||||
static const uint32_t IGNEUM_VEC_BASE[IGNEUM_VEC_WARPS] = { \(bases.map { "\($0)u" }.joined(separator: ", ")) };
|
||||
|
|
@ -1204,6 +1400,7 @@ func exportPack(_ opts: Options) -> Never {
|
|||
("program.json", generateProgramJSON(program, dayString: opts.day, day: day, datasetLog2: opts.datasetLog2, memhard: ctx.mp)),
|
||||
("vectors.json", generateVectorsJSON(program, dayString: opts.day, datasetLog2: opts.datasetLog2, bases: packVectorBases, outs: outs, v: v, mask: mask, source: source, memhard: !ctx.closed)),
|
||||
("kernel.cu", generateCUDA(program, memhard: ctx.mp)),
|
||||
("kernel.cl", generateOpenCL(program, memhard: ctx.mp)),
|
||||
("program.h", generateProgramHeader(program, dayString: opts.day, day: day, datasetLog2: opts.datasetLog2, memhard: ctx.mp)),
|
||||
("vectors.h", generateVectorsHeader(program, bases: packVectorBases, outs: outs, v: v, mask: mask, source: source, memhard: !ctx.closed)),
|
||||
("program.metal", generateMSL(program, datasetLog2: opts.datasetLog2)),
|
||||
|
|
|
|||
6
proto-opencl/.gitignore
vendored
Normal file
6
proto-opencl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
igneum-bench-cl-*
|
||||
*.exe
|
||||
*.obj
|
||||
*.o
|
||||
emu/build-*/
|
||||
.DS_Store
|
||||
213
proto-opencl/README.md
Normal file
213
proto-opencl/README.md
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
# igneum-bench-cl (proto-opencl)
|
||||
|
||||
The portable third path for Igneum's random-program proof-of-work kernels, after Apple Metal (`proto-metal`) and
|
||||
NVIDIA CUDA (`proto-cuda`). It runs the same program packs through OpenCL 1.2, which is what an AMD card exposes on
|
||||
both Windows (Adrenalin driver) and Linux (ROCm, or Mesa), and checks every output bit for bit against the Mac.
|
||||
|
||||
This is a test harness, not a miner. No pool, no network, no wallet, no mining protocol. It fills a dataset, checks the
|
||||
device against known answers, and times the kernel. Nothing here earns anything.
|
||||
|
||||
Status on 3 October 2026: no AMD device has run this yet. Everything that could be proven without one has been
|
||||
(`WAVEFRONT.md`, "What was proven"): Apple's deprecated OpenCL 1.2 runtime on the M5 Max, pocl 7.2 on the Mac's CPU,
|
||||
and a CPU emulator with 32- and 64-wide sub-groups all give the Mac's 96/96 vectors and cache FNV for the memory-hard
|
||||
pack. The AMD run itself is the next step and the commands for it are below.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
proto-opencl/
|
||||
host.c C99 host: device list, runtime kernel build, cache + dataset fill, self-tests, vectors, bench, sweep
|
||||
build.sh macOS (-framework OpenCL, or the Khronos ICD loader) and Linux (-lOpenCL)
|
||||
build.bat Windows (MSVC cl.exe + OpenCL.lib)
|
||||
WAVEFRONT.md wave32 vs wave64 on AMD, and why the kernel cannot tell the difference
|
||||
emu/ CPU emulator: compiles kernel.cl as C++ and runs it with a 32- or 64-wide sub-group
|
||||
../proto-cuda/packs/<seed>/kernel.cl the OpenCL C kernel of each pack, written by proto-metal/igneum-bench --export-pack
|
||||
```
|
||||
|
||||
The packs stay in `proto-cuda/packs/` because the three implementations share one `program.h`, `vectors.h` and
|
||||
`memhard.h` per seed; `kernel.cl` sits next to `kernel.cu` and `program.metal`. Three packs are checked in:
|
||||
`igneum-genesis-mh` (memory-hard dataset, the current construction), `igneum-genesis` and `igneum-hourly`
|
||||
(closed-form dataset, kept for comparison).
|
||||
|
||||
## What the OpenCL path does differently
|
||||
|
||||
| Item | CUDA (`host.cu`) | OpenCL (`host.c`) |
|
||||
|---|---|---|
|
||||
| Kernel compile | ahead of time by nvcc | at runtime by the driver from `kernel.cl`, with `-D IGNEUM_GROUP=32W -D IGNEUM_EXCHANGE=M` |
|
||||
| 32-lane exchange | `__shfl_xor_sync` | `sub_group_shuffle_xor` only when the device's sub-group size is exactly 32 for a 32-item work-group; otherwise `__local` memory and a barrier (`WAVEFRONT.md`) |
|
||||
| Timing | cudaEvent | event profiling (`CL_PROFILING_COMMAND_START/END`); wall time on Apple, whose OpenCL timestamps are unusable |
|
||||
| Device choice | `--device D` | `--list`, then `--device D` by flat index over all platforms; default is the first GPU |
|
||||
| Language | C++17 | C99, so MSVC builds it without nvcc or any C++ toolchain |
|
||||
|
||||
Everything else (dataset fill or build, cache check against the Mac's FNV, dataset self-test, 3 vector warps standalone
|
||||
and inside the warm-up batch, 5 timed batches of 2^24, the sweep, the summary table, exit codes) mirrors `host.cu`
|
||||
line for line. Rates have the same definition: Mhash/s, and GB/s useful = loads per hash x 4 bytes x hashes/s.
|
||||
|
||||
## Build
|
||||
|
||||
Linux (AMD ROCm, AMD Adrenalin for Linux, or Mesa rusticl; any of them registers with the system ICD loader):
|
||||
|
||||
```
|
||||
sudo apt install ocl-icd-opencl-dev opencl-headers clinfo # Debian or Ubuntu; Fedora: ocl-icd-devel opencl-headers clinfo
|
||||
clinfo | head -40 # the AMD device must appear here before anything else matters
|
||||
cd proto-opencl
|
||||
./build.sh igneum-genesis-mh # -> ./igneum-bench-cl-igneum-genesis-mh
|
||||
./build.sh igneum-genesis # closed-form packs
|
||||
./build.sh igneum-hourly
|
||||
```
|
||||
|
||||
The one command behind the script, for a pack `P`:
|
||||
|
||||
```
|
||||
cc -std=c99 -O2 -Wall -Wextra -I ../proto-cuda/packs/P -DIGNEUM_KERNEL_PATH='"../proto-cuda/packs/P/kernel.cl"' -o igneum-bench-cl-P host.c -lOpenCL -ldl
|
||||
```
|
||||
|
||||
Windows (AMD Adrenalin driver installed; it provides `OpenCL.dll` and the AMD ICD, nothing else is needed to run):
|
||||
|
||||
1. Install Visual Studio 2022 or 2026 Build Tools with "Desktop development with C++". No CUDA, no nvcc.
|
||||
2. Get the OpenCL headers and the import library `OpenCL.lib`, any one of these:
|
||||
`vcpkg install opencl:x64-windows` then `set OPENCL_SDK=C:\vcpkg\installed\x64-windows`;
|
||||
or the Khronos OpenCL-SDK release zip (GitHub KhronosGroup/OpenCL-SDK) unpacked anywhere, `set OPENCL_SDK=<that dir>`;
|
||||
or an installed CUDA Toolkit, which ships both under `%CUDA_PATH%` (the script finds it on its own).
|
||||
3. From an "x64 Native Tools Command Prompt":
|
||||
|
||||
```
|
||||
cd proto-opencl
|
||||
build.bat igneum-genesis-mh
|
||||
build.bat igneum-genesis
|
||||
build.bat igneum-hourly
|
||||
```
|
||||
|
||||
The one command behind the script:
|
||||
|
||||
```
|
||||
cl /nologo /O2 /W3 /std:c11 /I "%OPENCL_SDK%\include" /I "..\proto-cuda\packs\P" /DIGNEUM_KERNEL_PATH="\"../proto-cuda/packs/P/kernel.cl\"" /Fe:igneum-bench-cl-P.exe host.c /link "%OPENCL_SDK%\lib\OpenCL.lib"
|
||||
```
|
||||
|
||||
macOS (correctness check only; Apple deprecated OpenCL in macOS 10.14 and the runtime is 1.2):
|
||||
|
||||
```
|
||||
./build.sh igneum-genesis-mh # Apple OpenCL.framework
|
||||
./build.sh igneum-genesis-mh khr # Khronos ICD loader from Homebrew, for pocl: brew install opencl-headers opencl-icd-loader pocl
|
||||
OCL_ICD_VENDORS=/opt/homebrew/etc/OpenCL/vendors SDKROOT=$(xcrun --show-sdk-path) ./igneum-bench-cl-igneum-genesis-mh-khr --list
|
||||
```
|
||||
|
||||
(`SDKROOT` is needed because pocl links its kernels with the system linker and otherwise cannot find `-lSystem`.)
|
||||
|
||||
## Run on the AMD rig
|
||||
|
||||
Run from `proto-opencl/` so the default kernel path resolves, or pass `--kernel`. Exactly this, in this order, and paste
|
||||
the whole stdout back:
|
||||
|
||||
Windows:
|
||||
|
||||
```
|
||||
cd proto-opencl
|
||||
igneum-bench-cl-igneum-genesis-mh.exe --list
|
||||
igneum-bench-cl-igneum-genesis-mh.exe > amd-mh-auto.txt
|
||||
igneum-bench-cl-igneum-genesis-mh.exe --exchange local > amd-mh-local.txt
|
||||
igneum-bench-cl-igneum-genesis-mh.exe --group-warps 2 > amd-mh-gw2.txt
|
||||
igneum-bench-cl-igneum-genesis-mh.exe --sweep > amd-mh-sweep.txt
|
||||
igneum-bench-cl-igneum-genesis.exe > amd-genesis.txt
|
||||
igneum-bench-cl-igneum-hourly.exe > amd-hourly.txt
|
||||
```
|
||||
|
||||
Linux:
|
||||
|
||||
```
|
||||
cd proto-opencl
|
||||
./igneum-bench-cl-igneum-genesis-mh --list
|
||||
./igneum-bench-cl-igneum-genesis-mh | tee amd-mh-auto.txt
|
||||
./igneum-bench-cl-igneum-genesis-mh --exchange local | tee amd-mh-local.txt
|
||||
./igneum-bench-cl-igneum-genesis-mh --group-warps 2 | tee amd-mh-gw2.txt
|
||||
./igneum-bench-cl-igneum-genesis-mh --sweep | tee amd-mh-sweep.txt
|
||||
./igneum-bench-cl-igneum-genesis | tee amd-genesis.txt
|
||||
./igneum-bench-cl-igneum-hourly | tee amd-hourly.txt
|
||||
```
|
||||
|
||||
If `--list` shows more than one device (an iGPU, a CPU runtime, an NVIDIA card in the same box), add `--device D`
|
||||
with the AMD card's index to every line. If the auto run says `exchange: sub_group_shuffle_xor`, also run
|
||||
`--exchange subgroup` once so the log has an explicit sub-group run; if it says local memory, the `--exchange local`
|
||||
run is a repeat and that is fine.
|
||||
|
||||
Flags: `--list`, `--device D`, `--dataset-mib N` (power of two, default 1024), `--sweep` (4, 64, 256, 512, 1024 MiB),
|
||||
`--batch-log2 B` (default 24), `--batches N` (default 5), `--group-warps W` (32-lane units per work-group, 1..8,
|
||||
default 1), `--exchange auto|local|subgroup`, `--kernel path`, `--build-opts "..."` (appended to clBuildProgram),
|
||||
`--time event|wall`.
|
||||
|
||||
## What PASS looks like
|
||||
|
||||
In order, the run prints:
|
||||
|
||||
1. Every platform and device, with vendor, driver, OpenCL C version, compute units, memory, local memory, the
|
||||
sub-group extension it lists, and (AMD) the wavefront width or (NVIDIA) the warp size. The chosen device is starred.
|
||||
2. `build options:` and `exchange:`. The exchange line is the one that matters on AMD; it says which path was taken and
|
||||
why, for example `local-memory exchange with barrier (the sub-group size for a 32-item work-group is not 32; queried
|
||||
sub-group size 64)` on a wave64 card, or `sub_group_shuffle_xor (cl_khr_subgroup_shuffle), sub-group size 32 for a
|
||||
32-item work-group` on a wave32 one. Both are correct by construction (`WAVEFRONT.md`).
|
||||
3. `kernel:` (work-group limit and local memory of `igneum_hash`), `program:`, `seed words:`, `day`.
|
||||
4. Memory-hard packs: cache fill time on the device (twice) and on one host thread, then `cache check: PASS (device ==
|
||||
host all 67108864 words PASS, host FNV-1a 64 48c4f5bf24166b2e vs Mac 48c4f5bf24166b2e PASS, head 16 vs Mac PASS,
|
||||
last line vs Mac PASS)`. The FNV is the same on every machine that has ever run this construction for day
|
||||
2026-10-03.
|
||||
5. `dataset build` (or `dataset fill`) times, then `dataset self-test: PASS (head 16 vs Mac PASS, element [MASK] vs Mac
|
||||
PASS, 64 random points vs host derivation PASS, 64 Mac samples PASS)`.
|
||||
6. Six `verify warp ... PASS` lines: three standalone (bases 0, 4096, 1000000) and the same three read out of the
|
||||
warm-up batch.
|
||||
7. `batch fingerprint`: FNV-1a 64 of every output of the warm-up batch. At `--batch-log2 13` every implementation so far
|
||||
prints `f99fb375b3abeaf5` for igneum-genesis-mh (Apple OpenCL, pocl on both exchange paths, the emulator in seven
|
||||
configurations); an AMD run at `--batch-log2 13` must print the same value. At the default 2^24 the value is a new
|
||||
reference to compare AMD against the next machine.
|
||||
8. `timed:` with device and wall time, then the `rate` line: Mhash/s and GB/s useful.
|
||||
9. The summary table and `OVERALL: PASS`. Exit code 0 on PASS, 1 on FAIL, 2 on an OpenCL error or a build failure (the
|
||||
build log is printed in full).
|
||||
|
||||
A FAIL with a few lanes differing points at the exchange; a FAIL in every lane at an arithmetic op or the dataset; a
|
||||
cache FAIL at the ChaCha fill. Send the whole printout either way, including the `--list` output and, on Linux,
|
||||
`clinfo` and the ROCm or Mesa version, or on Windows the Adrenalin driver version from the AMD Software panel.
|
||||
|
||||
## What to paste back
|
||||
|
||||
All seven text files above, unedited. From them the bench log gets: device name and driver string, the `exchange:`
|
||||
line, the cache check line, the 96/96 vector result per pack, the rate at 1 GiB, and the sweep table. If a run looks
|
||||
odd, run it again and keep both.
|
||||
|
||||
## Results so far (no AMD)
|
||||
|
||||
| Machine, runtime | Exchange | igneum-genesis-mh | closed-form packs | Rate at 1 GiB |
|
||||
|---|---|---|---|---|
|
||||
| Apple M5 Max, Apple OpenCL 1.2 (deprecated runtime) | local memory (runtime lists no sub-group extension) | cache FNV = Mac, 96/96; also 96/96 at `--group-warps 2` and `4` | 96/96 and 96/96 | 45.03 Mhash/s, 18.73 GB/s useful (Metal on the same chip: 45.2). Apple OpenCL on the M5 Max, not an AMD number |
|
||||
| Apple M5 Max, pocl 7.2 CPU device, OpenCL 3.0, LLVM 23 | `sub_group_shuffle_xor` (auto and `--exchange subgroup`; pocl's `clGetKernelSubGroupInfoKHR` fails with CL_INVALID_OPERATION, the probe kernel reports 32), and `--exchange local` | cache FNV = Mac, 96/96 on both paths, same batch fingerprint as Apple and the emulator at 2^13 | not run | CPU, not meaningful |
|
||||
| CPU emulator (`emu/`), 7 configurations incl. sub-group 64 | both | 96/96 in every configuration, identical batch fingerprint `f99fb375b3abeaf5` | not run | none |
|
||||
|
||||
Sweep on Apple OpenCL (3 batches, wall time): 4 MiB 573.7, 64 MiB 178.9, 256 MiB 94.3, 512 MiB 68.8, 1024 MiB 45.0 Mhash/s.
|
||||
Full tables in `docs/bench-log.md`.
|
||||
|
||||
## Checking a pack without a GPU
|
||||
|
||||
```
|
||||
emu/emu.sh igneum-genesis-mh 0 32 --sg 32 # local-memory exchange, work-group 32, 32-wide sub-group
|
||||
emu/emu.sh igneum-genesis-mh 0 64 --sg 64 # local-memory exchange on a wave64 model, two units per work-group
|
||||
emu/emu.sh igneum-genesis-mh 1 32 --sg 32 # sub_group_shuffle_xor, 32-wide sub-group
|
||||
emu/emu.sh igneum-genesis-mh 1 64 --sg 64 # sub_group_shuffle_xor over a 64-wide sub-group (two units in one wave)
|
||||
```
|
||||
|
||||
The emulator compiles `kernel.cl` unchanged as C++ (`emu/emu_opencl.h` supplies the OpenCL built-ins; the kernel's own
|
||||
prelude includes it when no OpenCL compiler is present) and runs work-groups on host threads with a barrier inside
|
||||
every exchange. It prints the cache check, dataset self-test, vectors and a fingerprint of the whole 2^13-hash batch,
|
||||
so configurations can be compared for every nonce, not only the vector warps. It says nothing about any GPU.
|
||||
|
||||
## Regenerating a pack
|
||||
|
||||
```
|
||||
cd proto-metal
|
||||
swiftc -O -o igneum-bench main.swift -framework Metal
|
||||
./igneum-bench --seed igneum-genesis --export-pack ../proto-cuda/packs/igneum-genesis-mh
|
||||
./igneum-bench --closed-form --seed igneum-genesis --export-pack ../proto-cuda/packs/igneum-genesis
|
||||
./igneum-bench --closed-form --seed igneum-hourly --export-pack ../proto-cuda/packs/igneum-hourly
|
||||
```
|
||||
|
||||
The exporter writes `kernel.cl` next to `kernel.cu` from the same instruction list, refuses to write unless the Metal
|
||||
GPU matches the CPU interpreter on all 96 vector outputs, and for memory-hard packs unless the GPU cache equals the
|
||||
CPU cache word for word. The memory-hard core is emitted once (`emitMemhardCore`) in three dialects (Metal, CUDA C++,
|
||||
OpenCL C), so the constants in `kernel.cl` are the literals of `memhard.h`.
|
||||
98
proto-opencl/WAVEFRONT.md
Normal file
98
proto-opencl/WAVEFRONT.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# Wavefront width and the 32-lane verification unit
|
||||
|
||||
Written 3 October 2026 for the OpenCL path (`proto-opencl`). It records the one place where AMD hardware differs
|
||||
from Apple and NVIDIA in a way the lottery hash can feel, and how `kernel.cl` is built so that it cannot.
|
||||
|
||||
## The unit is 32 lanes, by definition
|
||||
|
||||
The program pack defines a hash over 32 lanes. Six of its 64 instructions are exchanges (`shfl`): lane `l` reads a
|
||||
register from lane `l ^ m` with `m` in {1, 2, 4, 8, 16}. The CPU verifier (`proto-metal/main.swift`, `cpuWarp`)
|
||||
models exactly 32 lanes. Metal runs it on a 32-wide SIMD group (`simd_shuffle_xor`), CUDA on a 32-wide warp
|
||||
(`__shfl_xor_sync`). The vectors in every pack are 3 warps x 32 lanes x 64 bits. Nothing in the definition mentions
|
||||
hardware; 32 is a parameter of the hash.
|
||||
|
||||
## What AMD hardware does
|
||||
|
||||
| Architecture | Native wave width | Notes |
|
||||
|---|---|---|
|
||||
| GCN (Radeon HD 7000 to Vega, approximate) | 64 | always wave64 |
|
||||
| CDNA (Instinct MI100 to MI300, approximate) | 64 | always wave64 |
|
||||
| RDNA 1 to 4 (RX 5000 to RX 9000, approximate) | 32 or 64 | the shader compiler picks per kernel; compute kernels are often wave64 unless the compiler decides otherwise, and nothing in OpenCL lets the program demand wave32 |
|
||||
|
||||
Figures from memory, labelled approximate. NVIDIA is 32 everywhere; Apple is 32 on every chip the Mac tool has
|
||||
seen (it warns if `threadExecutionWidth` is not 32). In OpenCL terms the hardware wave is the sub-group:
|
||||
`get_sub_group_size()` and `clGetKernelSubGroupInfoKHR(CL_KERNEL_MAX_SUB_GROUP_SIZE_FOR_NDRANGE_KHR)` report it.
|
||||
|
||||
So on an AMD card a sub-group may be 64 lanes, and a sub-group shuffle (`sub_group_shuffle_xor`) spans 64 lanes.
|
||||
Two things could go wrong if the kernel simply used sub-group shuffles:
|
||||
|
||||
1. A wave64 sub-group holds two logical 32-lane units. `lane ^ m` with `m < 32` never leaves the unit's own aligned
|
||||
half, so the xor exchange itself would still be correct. The emulator below demonstrates this. But a
|
||||
`sub_group_broadcast(x, 0)` (used only by the optional wide-load lever `wload`, off by default) would broadcast
|
||||
lane 0 of the wave, which is wrong for the upper unit.
|
||||
2. The mapping from sub-group lane to work-item is not something the OpenCL specification promises to be linear
|
||||
and aligned. In practice it is (consecutive work-items in linear local id order), on AMD, NVIDIA, Intel and
|
||||
pocl alike, but "in practice" is not a guarantee a consensus rule should rest on.
|
||||
|
||||
## The rule kernel.cl follows
|
||||
|
||||
The exchange is selected at compile time by `IGNEUM_EXCHANGE`, and `proto-opencl/host.c` chooses the value:
|
||||
|
||||
| `IGNEUM_EXCHANGE` | Exchange | When host.c selects it |
|
||||
|---|---|---|
|
||||
| 0 (default) | local memory: each lane writes its register to `__local` memory, `barrier(CLK_LOCAL_MEM_FENCE)`, reads slot `lid ^ m` | always available; chosen whenever any condition below fails |
|
||||
| 1 | `sub_group_shuffle_xor` (extension `cl_khr_subgroup_shuffle`, OpenCL C 2.0 or 3.0) | the device lists the extension, `--group-warps 1` (work-group of exactly 32, so one work-group is one unit), the variant compiles, and `clGetKernelSubGroupInfoKHR` on `igneum_hash` reports a sub-group size of exactly 32 for a 32-item work-group |
|
||||
| 2 | `intel_sub_group_shuffle_xor` (`cl_intel_subgroups`) | as 1, on Intel devices without the khr shuffle extension |
|
||||
|
||||
In words: the 32-lane group is defined by the work-group of 32 (one work-group = one unit) whenever sub-group
|
||||
shuffles are used, and the local-memory exchange is used whenever the sub-group size is not exactly 32. On a
|
||||
wave64 device the queried size is 64, so AMD GCN, CDNA and RDNA-in-wave64 all take the local-memory path, and the
|
||||
hash does not depend on the wave width at all: a work-group barrier and `__local` memory mean the same thing at any
|
||||
wave width. If the sub-group size cannot be queried (`clGetKernelSubGroupInfoKHR` missing, as on pocl), host.c
|
||||
runs a probe kernel from the same build that reports `get_sub_group_size()` for a 32-item work-group; this is
|
||||
weaker than the per-kernel query because a compiler may choose the wave width per kernel, and the printout says so.
|
||||
`--exchange local` forces path 0 on any device; `--exchange subgroup` demands path 1 or 2 and fails otherwise.
|
||||
The run prints which path it took and why, on the `exchange:` line.
|
||||
|
||||
The local-memory exchange uses two buffers of `IGNEUM_GROUP` words that alternate (counter `xk`), so one barrier per
|
||||
exchange suffices: a lane can only overwrite buffer `b` at exchange `k + 2` after passing barrier `k + 1`, and every
|
||||
lane reaches barrier `k + 1` only after its read of buffer `b` at exchange `k`. Control flow is uniform (the program
|
||||
has no branches), so every work-item reaches every barrier. `--group-warps W` packs W units into one work-group of
|
||||
32 W items; `lid ^ m` stays inside the unit because `m < 32`, and the Apple runs below show W = 1, 2 and 4 bit-exact.
|
||||
|
||||
## What was proven without AMD silicon (3 October 2026)
|
||||
|
||||
| Check | Path | Result |
|
||||
|---|---|---|
|
||||
| Apple M5 Max, Apple OpenCL 1.2 runtime, pack igneum-genesis-mh | local memory (the runtime lists no sub-group extension), work-group 32 | cache FNV `48c4f5bf24166b2e` = Mac, 96/96 vectors standalone and in batch, 45.03 Mhash/s at 1 GiB |
|
||||
| Same, `--exchange local --group-warps 2` and `--group-warps 4` | local memory, work-groups of 64 and 128 | 96/96 and 96/96 |
|
||||
| Same, closed-form packs igneum-genesis and igneum-hourly | local memory | 96/96 and 96/96 |
|
||||
| pocl 7.2 CPU device (OpenCL 3.0, LLVM 23), Khronos ICD loader, `--exchange auto` and `--exchange subgroup` | `sub_group_shuffle_xor`, OpenCL C 3.0 (pocl's `clGetKernelSubGroupInfoKHR` fails with CL_INVALID_OPERATION, the probe kernel reports a sub-group of 32 for a 32-item work-group) | cache FNV = Mac, 96/96 |
|
||||
| Same, `--exchange local` | local memory | cache FNV = Mac, 96/96 |
|
||||
| CPU emulator, `IGNEUM_EXCHANGE 0`, work-group 32, sub-group 32 | local memory | PASS, batch fingerprint `f99fb375b3abeaf5` |
|
||||
| CPU emulator, `IGNEUM_EXCHANGE 0`, work-group 64, sub-group 64 (wave64, two units per wave) | local memory | PASS, same fingerprint |
|
||||
| CPU emulator, `IGNEUM_EXCHANGE 0`, work-group 32, sub-group 64 (wave64 half empty) | local memory | PASS, same fingerprint |
|
||||
| CPU emulator, `IGNEUM_EXCHANGE 1`, work-group 32, sub-group 32 | `sub_group_shuffle_xor` | PASS, same fingerprint |
|
||||
| CPU emulator, `IGNEUM_EXCHANGE 1`, work-group 32, sub-group 64 | `sub_group_shuffle_xor` over a half-empty wave64 | PASS, same fingerprint |
|
||||
| CPU emulator, `IGNEUM_EXCHANGE 1`, work-group 64, sub-group 64 (wave64, two units in one shuffle domain) | `sub_group_shuffle_xor` | PASS, same fingerprint |
|
||||
| CPU emulator, `IGNEUM_EXCHANGE 1`, work-group 64, sub-group 32 (two sub-groups per work-group) | `sub_group_shuffle_xor` | PASS, same fingerprint |
|
||||
|
||||
The fingerprint is FNV-1a 64 over the 2^13 outputs of the batch at base nonce 0; seven emulator configurations
|
||||
produced the same one, and host.c prints the same fingerprint for its warm-up batch (`--batch-log2 13`): Apple's
|
||||
OpenCL (local memory) and pocl (sub-group shuffles, and local memory) all print `f99fb375b3abeaf5`. So both exchange
|
||||
implementations, both wave widths, two real OpenCL compilers and the emulator give identical hashes for every nonce
|
||||
in the batch, not only for the three vector warps. An AMD run at `--batch-log2 13` must print the same value. The emulator rows with sub-group 64 and `IGNEUM_EXCHANGE 1` show that the xor
|
||||
exchange alone would survive a wave64 sub-group (point 1 above); host.c still refuses that configuration on a real
|
||||
device because of point 2 and because of `sub_group_broadcast`. The conservative rule costs nothing in correctness
|
||||
and, on a wave64 card, the local-memory path is what runs.
|
||||
|
||||
## What is not proven here
|
||||
|
||||
- No AMD compiler has compiled `kernel.cl`, and no AMD device has run it. The emulator is clang; pocl is LLVM on a
|
||||
CPU; Apple's OpenCL is Apple's compiler. Syntax every one of them accepts can still trip AMD's front end, which
|
||||
would be a build log (printed in full by host.c), not a silent difference.
|
||||
- No AMD hash rate exists. The cost of the local-memory exchange relative to sub-group shuffles on AMD is unknown;
|
||||
on Apple the local-memory path runs at the same 45 Mhash/s as Metal's `simd_shuffle_xor` kernel because the hash is
|
||||
bound by the 104 random dataset loads, and the same is expected elsewhere, but expected is not measured.
|
||||
- Whether RDNA compiles `igneum_hash` as wave32 (sub-group 32, path 1) or wave64 (path 0) is a driver decision. The
|
||||
run will say which on the `exchange:` line. Both are correct by construction; only the speed may differ.
|
||||
59
proto-opencl/build.bat
Normal file
59
proto-opencl/build.bat
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
@echo off
|
||||
rem Build igneum-bench-cl for one program pack (Windows, MSVC).
|
||||
rem Run from an "x64 Native Tools Command Prompt for VS 2022" (or 2026 with any toolset: this is plain C, no nvcc involved).
|
||||
rem Needs the OpenCL headers (CL\cl.h) and the ICD loader import library (OpenCL.lib). The script looks, in order, at:
|
||||
rem OPENCL_SDK a Khronos OpenCL-SDK or vcpkg tree: %OPENCL_SDK%\include\CL\cl.h and %OPENCL_SDK%\lib\OpenCL.lib
|
||||
rem CUDA_PATH the CUDA Toolkit ships both: %CUDA_PATH%\include\CL\cl.h and %CUDA_PATH%\lib\x64\OpenCL.lib
|
||||
rem OCL_ROOT the old AMD APP SDK layout: %OCL_ROOT%\include and %OCL_ROOT%\lib\x86_64
|
||||
rem The runtime itself (OpenCL.dll in System32 plus the vendor ICD) comes with the AMD Adrenalin driver; no SDK needed to run.
|
||||
rem Usage: build.bat [pack]
|
||||
rem pack directory name under ..\proto-cuda\packs\ (default igneum-genesis-mh)
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
|
||||
set PACK=%1
|
||||
if "%PACK%"=="" set PACK=igneum-genesis-mh
|
||||
set PACKDIR=..\proto-cuda\packs\%PACK%
|
||||
|
||||
where cl >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo cl.exe not found. Open an "x64 Native Tools Command Prompt" ^(Visual Studio Build Tools, Desktop development with C++^) and run this again.
|
||||
exit /b 1
|
||||
)
|
||||
if not exist "%PACKDIR%\kernel.cl" (
|
||||
echo no pack at %PACKDIR% ^(expected kernel.cl, program.h, vectors.h^)
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set INC=
|
||||
set LIB=
|
||||
if not "%OPENCL_SDK%"=="" if exist "%OPENCL_SDK%\include\CL\cl.h" (
|
||||
set INC=%OPENCL_SDK%\include
|
||||
if exist "%OPENCL_SDK%\lib\OpenCL.lib" set LIB=%OPENCL_SDK%\lib\OpenCL.lib
|
||||
if exist "%OPENCL_SDK%\lib\x64\OpenCL.lib" set LIB=%OPENCL_SDK%\lib\x64\OpenCL.lib
|
||||
)
|
||||
if "%LIB%"=="" if not "%CUDA_PATH%"=="" if exist "%CUDA_PATH%\include\CL\cl.h" (
|
||||
set INC=%CUDA_PATH%\include
|
||||
set LIB=%CUDA_PATH%\lib\x64\OpenCL.lib
|
||||
)
|
||||
if "%LIB%"=="" if not "%OCL_ROOT%"=="" if exist "%OCL_ROOT%\include\CL\cl.h" (
|
||||
set INC=%OCL_ROOT%\include
|
||||
set LIB=%OCL_ROOT%\lib\x86_64\OpenCL.lib
|
||||
)
|
||||
if "%LIB%"=="" (
|
||||
echo No OpenCL SDK found. Set OPENCL_SDK to a tree with include\CL\cl.h and lib\OpenCL.lib, for example:
|
||||
echo vcpkg install opencl:x64-windows then set OPENCL_SDK=C:\vcpkg\installed\x64-windows
|
||||
echo or the Khronos OpenCL-SDK release zip, or install the CUDA Toolkit ^(it ships CL\cl.h and OpenCL.lib^).
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo using headers %INC% and import library %LIB%
|
||||
echo cl /nologo /O2 /W3 /std:c11 /I "%INC%" /I "%PACKDIR%" /DIGNEUM_KERNEL_PATH="\"%PACKDIR:\=/%/kernel.cl\"" /Fe:igneum-bench-cl-%PACK%.exe host.c /link "%LIB%"
|
||||
cl /nologo /O2 /W3 /std:c11 /I "%INC%" /I "%PACKDIR%" /DIGNEUM_KERNEL_PATH="\"%PACKDIR:\=/%/kernel.cl\"" /Fe:igneum-bench-cl-%PACK%.exe host.c /link "%LIB%"
|
||||
if errorlevel 1 (
|
||||
echo build failed. If cl.exe rejects /std:c11, drop that flag: the code is C99 and MSVC's default C mode accepts it.
|
||||
exit /b 1
|
||||
)
|
||||
del host.obj >nul 2>nul
|
||||
echo built igneum-bench-cl-%PACK%.exe
|
||||
endlocal
|
||||
48
proto-opencl/build.sh
Executable file
48
proto-opencl/build.sh
Executable file
|
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build igneum-bench-cl for one program pack (macOS or Linux).
|
||||
# Usage: ./build.sh [pack] [loader]
|
||||
# pack directory name under ../proto-cuda/packs/ (default igneum-genesis-mh)
|
||||
# loader macOS only: "apple" (default) links Apple's deprecated OpenCL.framework; "khr" links the Khronos ICD loader
|
||||
# from Homebrew (brew install opencl-headers opencl-icd-loader pocl) so a CPU OpenCL 3.0 device can be tested.
|
||||
# On Linux the argument is ignored and -lOpenCL is used (AMD ROCm, AMD Adrenalin for Linux, Mesa rusticl/clover,
|
||||
# NVIDIA or Intel runtimes all register with the system ICD loader).
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
PACK="${1:-igneum-genesis-mh}"
|
||||
LOADER="${2:-apple}"
|
||||
PACKDIR="../proto-cuda/packs/$PACK"
|
||||
|
||||
if [ ! -f "$PACKDIR/kernel.cl" ]; then
|
||||
echo "no pack at $PACKDIR (expected kernel.cl, program.h, vectors.h); regenerate with proto-metal/igneum-bench --export-pack" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CC="${CC:-cc}"
|
||||
COMMON=(-std=c99 -O2 -Wall -Wextra -I "$PACKDIR" -DIGNEUM_KERNEL_PATH="\"$PACKDIR/kernel.cl\"")
|
||||
OUT="igneum-bench-cl-$PACK"
|
||||
|
||||
case "$(uname -s)" in
|
||||
Darwin)
|
||||
if [ "$LOADER" = "khr" ]; then
|
||||
PREFIX="$(brew --prefix 2>/dev/null || echo /opt/homebrew)"
|
||||
OUT="$OUT-khr"
|
||||
set -x
|
||||
"$CC" "${COMMON[@]}" -DIGNEUM_KHR_HEADERS -I "$PREFIX/opt/opencl-headers/include" -o "$OUT" host.c \
|
||||
-L "$PREFIX/opt/opencl-icd-loader/lib" -lOpenCL
|
||||
set +x
|
||||
echo "built ./$OUT (Khronos ICD loader; run with OCL_ICD_VENDORS=$PREFIX/etc/OpenCL/vendors)"
|
||||
else
|
||||
set -x
|
||||
"$CC" "${COMMON[@]}" -Wno-deprecated-declarations -o "$OUT" host.c -framework OpenCL
|
||||
set +x
|
||||
echo "built ./$OUT (Apple OpenCL 1.2, deprecated since macOS 10.14; correctness check only, not an AMD number)"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
set -x
|
||||
"$CC" "${COMMON[@]}" -o "$OUT" host.c -lOpenCL -ldl
|
||||
set +x
|
||||
echo "built ./$OUT"
|
||||
;;
|
||||
esac
|
||||
27
proto-opencl/emu/emu.sh
Executable file
27
proto-opencl/emu/emu.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
#!/usr/bin/env bash
|
||||
# CPU emulation of the OpenCL pack, for checking kernel.cl on a machine WITHOUT an AMD GPU (any clang++ or g++).
|
||||
# It compiles the generated kernel.cl as C++ against emu_opencl.h and runs the kernels on host threads with a chosen
|
||||
# sub-group width (32, or 64 to model a wave64 GPU running two logical 32-lane units in one wave).
|
||||
# Usage: emu/emu.sh <pack> [exchange] [group] [emulator args...]
|
||||
# pack directory name under ../proto-cuda/packs/ (default igneum-genesis-mh)
|
||||
# exchange IGNEUM_EXCHANGE: 0 = local-memory exchange, 1 = sub_group_shuffle_xor (default 0)
|
||||
# group IGNEUM_GROUP: work-group size of igneum_hash, 32 or 64 (default 32)
|
||||
# rest passed to the emulator: --sg 32|64 (default 32), --batch-log2 B (default 13), --dataset-mib N
|
||||
# Example (wave64, two units per wave, sub-group shuffles): emu/emu.sh igneum-genesis-mh 1 64 --sg 64
|
||||
# Only PASS/FAIL matters. Rates are not printed because they would be meaningless.
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
PACK="${1:-igneum-genesis-mh}"; shift || true
|
||||
EXCHANGE="${1:-0}"; shift || true
|
||||
GROUP="${1:-32}"; shift || true
|
||||
PACKDIR="$(cd "$HERE/../../proto-cuda/packs/$PACK" && pwd)"
|
||||
BUILD="$HERE/build-$PACK-e$EXCHANGE-g$GROUP"
|
||||
mkdir -p "$BUILD"
|
||||
CXX="${CXX:-c++}"
|
||||
FLAGS=(-std=c++17 -O2 -Wall -Wextra -I "$HERE" -I "$PACKDIR" -D "IGNEUM_GROUP=$GROUP" -D "IGNEUM_EXCHANGE=$EXCHANGE")
|
||||
# kernel.cl is compiled as C++ unchanged: its prelude includes emu_opencl.h when __OPENCL_VERSION__ is absent.
|
||||
"$CXX" "${FLAGS[@]}" -x c++ -c -o "$BUILD/kernel_cl.o" "$PACKDIR/kernel.cl"
|
||||
"$CXX" "${FLAGS[@]}" -c -o "$BUILD/emu_main.o" "$HERE/emu_main.cpp"
|
||||
"$CXX" -o "$BUILD/igneum-emu-cl" "$BUILD/kernel_cl.o" "$BUILD/emu_main.o" -pthread
|
||||
echo "compiled $BUILD/igneum-emu-cl (CPU emulation of kernel.cl, not a GPU build)"
|
||||
exec "$BUILD/igneum-emu-cl" "$@"
|
||||
301
proto-opencl/emu/emu_main.cpp
Normal file
301
proto-opencl/emu/emu_main.cpp
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
// CPU emulator driver for the OpenCL pack: runs the generated kernel.cl (compiled as C++ through emu_opencl.h) on
|
||||
// host threads with a configurable sub-group width, and checks cache, dataset and vectors against the pack.
|
||||
//
|
||||
// Model. A launch of global G work-items with work-group size L spawns L host threads; thread l plays work-item l
|
||||
// of every work-group in turn (group 0, 1, 2, ...), exactly like proto-cuda/emu. barrier() synchronises the L
|
||||
// threads of the current group. Sub-groups are consecutive runs of SG work-items inside the group (SG = --sg 32 or
|
||||
// 64; the last run is clipped to the group size); sub_group_shuffle_xor exchanges through a slot array with a
|
||||
// barrier among the sub-group's threads, so a 64-wide sub-group really carries two logical 32-lane units.
|
||||
// Local memory is one arena per work-group instance, never shared between groups, as on hardware.
|
||||
//
|
||||
// Compile-time (as on the device): IGNEUM_GROUP (work-group size of igneum_hash) and IGNEUM_EXCHANGE (0 local memory,
|
||||
// 1 sub-group shuffles). Runtime: --sg 32|64, --batch-log2 B (default 13), --dataset-mib N (default the pack size).
|
||||
// Only PASS/FAIL matters here. Rates are meaningless and not printed.
|
||||
#include "emu_opencl.h"
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#define IGNEUM_NO_CUDA
|
||||
#include "program.h"
|
||||
#include "vectors.h"
|
||||
#ifndef IGNEUM_DATASET_MODE
|
||||
#define IGNEUM_DATASET_MODE 0
|
||||
#endif
|
||||
#if IGNEUM_DATASET_MODE == 1
|
||||
#include "memhard.h"
|
||||
#endif
|
||||
#ifndef IGNEUM_GROUP
|
||||
#define IGNEUM_GROUP 32
|
||||
#endif
|
||||
#ifndef IGNEUM_EXCHANGE
|
||||
#define IGNEUM_EXCHANGE 0
|
||||
#endif
|
||||
|
||||
// Kernels from kernel.cl (compiled as a separate C++ translation unit with the same defines).
|
||||
void igneum_hash(const uint* ds, ulong* out, uint baseNonce, uint mask);
|
||||
#if IGNEUM_DATASET_MODE == 1
|
||||
void igneum_cache_fill(uint* cache, uint nSegments);
|
||||
void igneum_build(uint* ds, const uint* cache, uint nItems);
|
||||
#else
|
||||
void igneum_fill(uint* ds, uint n, uint d0, uint d1);
|
||||
#endif
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Runtime
|
||||
|
||||
struct Barrier {
|
||||
std::mutex m;
|
||||
std::condition_variable cv;
|
||||
unsigned size = 0, arrived = 0, generation = 0;
|
||||
void wait() {
|
||||
std::unique_lock<std::mutex> lk(m);
|
||||
unsigned gen = generation;
|
||||
if (++arrived == size) { arrived = 0; ++generation; cv.notify_all(); }
|
||||
else cv.wait(lk, [&] { return gen != generation; });
|
||||
}
|
||||
};
|
||||
|
||||
struct SubGroup {
|
||||
Barrier bar;
|
||||
uint slot[64];
|
||||
unsigned size = 0;
|
||||
};
|
||||
|
||||
struct Launch {
|
||||
unsigned local = 0, groups = 0, sg = 32;
|
||||
Barrier groupBar;
|
||||
std::vector<std::unique_ptr<SubGroup>> subs;
|
||||
std::mutex arenaMutex;
|
||||
std::unordered_map<unsigned, std::unique_ptr<uint[]>> arenas; // group index -> local memory words
|
||||
unsigned arenaWords = 0;
|
||||
};
|
||||
|
||||
static thread_local Launch* tlLaunch = nullptr;
|
||||
static thread_local unsigned tlLid = 0, tlGroup = 0;
|
||||
static unsigned gSubGroupWidth = 32;
|
||||
|
||||
size_t get_global_id(uint) { return (size_t)tlGroup * tlLaunch->local + tlLid; }
|
||||
size_t get_local_id(uint) { return tlLid; }
|
||||
size_t get_group_id(uint) { return tlGroup; }
|
||||
size_t get_local_size(uint) { return tlLaunch->local; }
|
||||
size_t get_global_size(uint) { return (size_t)tlLaunch->local * tlLaunch->groups; }
|
||||
uint get_sub_group_size(void) { return tlLaunch->subs[tlLid / tlLaunch->sg]->size; }
|
||||
uint get_sub_group_local_id(void) { return tlLid % tlLaunch->sg; }
|
||||
uint get_sub_group_id(void) { return tlLid / tlLaunch->sg; }
|
||||
uint get_num_sub_groups(void) { return (uint)tlLaunch->subs.size(); }
|
||||
|
||||
void barrier(int) { tlLaunch->groupBar.wait(); }
|
||||
|
||||
uint sub_group_shuffle_xor(uint v, uint mask) {
|
||||
SubGroup* s = tlLaunch->subs[tlLid / tlLaunch->sg].get();
|
||||
unsigned lane = tlLid % tlLaunch->sg;
|
||||
unsigned partner = lane ^ mask;
|
||||
if (partner >= s->size) {
|
||||
std::fprintf(stderr, "emu: sub_group_shuffle_xor partner lane %u outside the sub-group of %u lanes (undefined on hardware)\n", partner, s->size);
|
||||
std::exit(3);
|
||||
}
|
||||
s->slot[lane] = v;
|
||||
s->bar.wait();
|
||||
uint r = s->slot[partner];
|
||||
s->bar.wait();
|
||||
return r;
|
||||
}
|
||||
uint intel_sub_group_shuffle_xor(uint v, uint mask) { return sub_group_shuffle_xor(v, mask); }
|
||||
|
||||
uint sub_group_broadcast(uint v, uint laneSrc) {
|
||||
SubGroup* s = tlLaunch->subs[tlLid / tlLaunch->sg].get();
|
||||
unsigned lane = tlLid % tlLaunch->sg;
|
||||
s->slot[lane] = v;
|
||||
s->bar.wait();
|
||||
uint r = s->slot[laneSrc % s->size];
|
||||
s->bar.wait();
|
||||
return r;
|
||||
}
|
||||
|
||||
uint* emu_local_words(uint n) {
|
||||
Launch* L = tlLaunch;
|
||||
std::lock_guard<std::mutex> lk(L->arenaMutex);
|
||||
auto it = L->arenas.find(tlGroup);
|
||||
if (it == L->arenas.end()) {
|
||||
std::unique_ptr<uint[]> a(new uint[n]());
|
||||
it = L->arenas.emplace(tlGroup, std::move(a)).first;
|
||||
L->arenaWords = n;
|
||||
}
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
template<class F, class... A> static void emu_launch(F f, size_t global, unsigned local, A... args) {
|
||||
if (global % local != 0) { std::fprintf(stderr, "emu: global %zu not a multiple of local %u\n", global, local); std::exit(3); }
|
||||
Launch L;
|
||||
L.local = local; L.groups = (unsigned)(global / local); L.sg = gSubGroupWidth;
|
||||
L.groupBar.size = local;
|
||||
unsigned nSub = (local + L.sg - 1) / L.sg;
|
||||
for (unsigned k = 0; k < nSub; ++k) {
|
||||
L.subs.emplace_back(new SubGroup());
|
||||
L.subs.back()->size = (local - k * L.sg) < L.sg ? (local - k * L.sg) : L.sg;
|
||||
L.subs.back()->bar.size = L.subs.back()->size;
|
||||
}
|
||||
std::vector<std::thread> ts;
|
||||
for (unsigned t = 0; t < local; ++t) {
|
||||
ts.emplace_back([&, t]() {
|
||||
tlLaunch = &L; tlLid = t;
|
||||
for (unsigned g = 0; g < L.groups; ++g) { tlGroup = g; f(args...); }
|
||||
});
|
||||
}
|
||||
for (auto& th : ts) th.join();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Checks
|
||||
|
||||
static uint64_t fnv1a64(const void* p, size_t n) {
|
||||
const uint8_t* b = (const uint8_t*)p;
|
||||
uint64_t h = 0xcbf29ce484222325ull;
|
||||
for (size_t i = 0; i < n; ++i) { h ^= b[i]; h *= 0x100000001b3ull; }
|
||||
return h;
|
||||
}
|
||||
|
||||
#if IGNEUM_DATASET_MODE == 0
|
||||
static uint32_t host_ds_elem(uint32_t i, uint32_t d0, uint32_t d1) {
|
||||
uint32_t x = i ^ d0;
|
||||
x *= 0x9E3779B1u; x ^= x >> 15;
|
||||
x += d1;
|
||||
x *= 0x85EBCA77u; x ^= x >> 13;
|
||||
x *= 0xC2B2AE3Du; x ^= x >> 16;
|
||||
return x;
|
||||
}
|
||||
#endif
|
||||
|
||||
static bool compareWarp(const uint64_t* got, const uint64_t* want, uint32_t base, const char* how) {
|
||||
int bad = 0, first = -1;
|
||||
for (int l = 0; l < 32; ++l) if (got[l] != want[l]) { if (first < 0) first = l; ++bad; }
|
||||
if (bad == 0) std::printf("verify warp base %u %s: PASS\n", base, how);
|
||||
else std::printf("verify warp base %u %s: FAIL %d of 32 lanes differ, first lane %d: emu=%016llx expected=%016llx\n",
|
||||
base, how, bad, first, (unsigned long long)got[first], (unsigned long long)want[first]);
|
||||
return bad == 0;
|
||||
}
|
||||
|
||||
static double wallMs() {
|
||||
using namespace std::chrono;
|
||||
return duration<double, std::milli>(steady_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
int batchLog2 = 13;
|
||||
int datasetMib = (int)(((1ull << IGNEUM_DATASET_LOG2) * 4ull) >> 20);
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string a = argv[i];
|
||||
if (a == "--sg" && i + 1 < argc) gSubGroupWidth = (unsigned)std::atoi(argv[++i]);
|
||||
else if (a == "--batch-log2" && i + 1 < argc) batchLog2 = std::atoi(argv[++i]);
|
||||
else if (a == "--dataset-mib" && i + 1 < argc) datasetMib = std::atoi(argv[++i]);
|
||||
else { std::printf("usage: igneum-emu-cl [--sg 32|64] [--batch-log2 13] [--dataset-mib N]\n"); return 2; }
|
||||
}
|
||||
if (gSubGroupWidth != 32 && gSubGroupWidth != 64) { std::printf("--sg must be 32 or 64\n"); return 2; }
|
||||
if (IGNEUM_EXCHANGE != 0 && IGNEUM_GROUP != 32 && IGNEUM_GROUP != 64) { std::printf("sub-group exchange needs IGNEUM_GROUP 32 or 64 here\n"); return 2; }
|
||||
|
||||
const uint32_t words = (uint32_t)(((uint64_t)datasetMib << 20) / 4ull);
|
||||
const uint32_t mask = words - 1u;
|
||||
const bool atPackSize = (words == (1u << IGNEUM_DATASET_LOG2));
|
||||
std::printf("igneum-emu-cl pack \"%s\" CPU EMULATION of kernel.cl (not a GPU; PASS/FAIL only)\n", IGNEUM_SEED_STRING);
|
||||
std::printf("configuration: IGNEUM_GROUP %d, IGNEUM_EXCHANGE %d (%s), emulated sub-group width %u%s, dataset %d MiB, batch 2^%d\n",
|
||||
IGNEUM_GROUP, IGNEUM_EXCHANGE, IGNEUM_EXCHANGE == 0 ? "local-memory exchange with barrier" : "sub_group_shuffle_xor",
|
||||
gSubGroupWidth, gSubGroupWidth == 64 ? " (two logical 32-lane units per wave)" : "", datasetMib, batchLog2);
|
||||
std::printf("hardware threads: %u\n", std::thread::hardware_concurrency());
|
||||
|
||||
bool overall = true;
|
||||
std::vector<uint> ds(words);
|
||||
#if IGNEUM_DATASET_MODE == 1
|
||||
const uint32_t cacheWords = 1u << IGNEUM_CACHE_LOG2_WORDS;
|
||||
std::vector<uint> cache(cacheWords), hostCache(cacheWords);
|
||||
double t0 = wallMs();
|
||||
emu_launch(igneum_cache_fill, (size_t)IGNEUM_CACHE_SEGMENTS, 256u, cache.data(), (uint)IGNEUM_CACHE_SEGMENTS);
|
||||
double t1 = wallMs();
|
||||
for (uint32_t seg = 0; seg < IGNEUM_CACHE_SEGMENTS; ++seg) mh_cache_segment(hostCache.data(), seg);
|
||||
double t2 = wallMs();
|
||||
bool same = std::memcmp(cache.data(), hostCache.data(), (size_t)cacheWords * 4u) == 0;
|
||||
uint64_t fnv = fnv1a64(cache.data(), (size_t)cacheWords * 4u);
|
||||
bool fnvOk = fnv == IGNEUM_CACHE_FNV64;
|
||||
bool headOk = std::memcmp(cache.data(), IGNEUM_CACHE_HEAD, 64) == 0;
|
||||
bool lastOk = std::memcmp(cache.data() + cacheWords - 16u, IGNEUM_CACHE_LAST, 64) == 0;
|
||||
std::printf("cache: emulated igneum_cache_fill %.0f ms (256 threads), host memhard.h one thread %.0f ms\n", t1 - t0, t2 - t1);
|
||||
std::printf("cache check: %s (emulated kernel == host all %u words %s, FNV-1a 64 %016llx vs Mac %016llx %s, head %s, last line %s)\n",
|
||||
(same && fnvOk && headOk && lastOk) ? "PASS" : "FAIL", cacheWords, same ? "PASS" : "FAIL",
|
||||
(unsigned long long)fnv, (unsigned long long)IGNEUM_CACHE_FNV64, fnvOk ? "PASS" : "FAIL", headOk ? "PASS" : "FAIL", lastOk ? "PASS" : "FAIL");
|
||||
overall = overall && same && fnvOk && headOk && lastOk;
|
||||
t0 = wallMs();
|
||||
emu_launch(igneum_build, (size_t)(words / 16u), 256u, ds.data(), (const uint*)cache.data(), (uint)(words / 16u));
|
||||
std::printf("dataset: emulated igneum_build %.0f ms for 2^%u items\n", wallMs() - t0, (unsigned)(IGNEUM_DATASET_LOG2 - 4));
|
||||
#else
|
||||
double t0 = wallMs();
|
||||
emu_launch(igneum_fill, (size_t)words, 256u, ds.data(), words, (uint)IGNEUM_DAY0, (uint)IGNEUM_DAY1);
|
||||
std::printf("dataset: emulated igneum_fill %.0f ms\n", wallMs() - t0);
|
||||
#endif
|
||||
|
||||
// Dataset self-test, same shape as host.c.
|
||||
{
|
||||
int badHead = 0, badRnd = 0, badSample = 0, nSample = 0;
|
||||
bool lastOk = true;
|
||||
for (int i = 0; i < 16; ++i) if (ds[i] != IGNEUM_DS_HEAD[i]) ++badHead;
|
||||
if (atPackSize) lastOk = (ds[IGNEUM_DS_LAST_INDEX] == IGNEUM_DS_LAST);
|
||||
uint64_t s = 0x9E3779B97F4A7C15ull ^ (uint64_t)words;
|
||||
for (int k = 0; k < 64; ++k) {
|
||||
s += 0x9E3779B97F4A7C15ull;
|
||||
uint64_t z = s;
|
||||
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ull;
|
||||
z = (z ^ (z >> 27)) * 0x94D049BB133111EBull;
|
||||
z ^= z >> 31;
|
||||
uint32_t idx = (uint32_t)z & mask;
|
||||
#if IGNEUM_DATASET_MODE == 1
|
||||
uint32_t want = mh_word(hostCache.data(), idx);
|
||||
#else
|
||||
uint32_t want = host_ds_elem(idx, IGNEUM_DAY0, IGNEUM_DAY1);
|
||||
#endif
|
||||
if (ds[idx] != want) ++badRnd;
|
||||
}
|
||||
#ifdef IGNEUM_DS_SAMPLES
|
||||
for (int k = 0; k < IGNEUM_DS_SAMPLES; ++k) {
|
||||
if (IGNEUM_DS_SAMPLE_INDEX[k] > mask) continue;
|
||||
++nSample;
|
||||
if (ds[IGNEUM_DS_SAMPLE_INDEX[k]] != IGNEUM_DS_SAMPLE_VALUE[k]) ++badSample;
|
||||
}
|
||||
#endif
|
||||
bool dsPass = badHead == 0 && lastOk && badRnd == 0 && badSample == 0;
|
||||
std::printf("dataset self-test: %s (head 16 vs Mac %s, element [MASK] %s, 64 random points vs host %s, %d Mac samples %s)\n",
|
||||
dsPass ? "PASS" : "FAIL", badHead == 0 ? "PASS" : "FAIL", atPackSize ? (lastOk ? "PASS" : "FAIL") : "skipped",
|
||||
badRnd == 0 ? "PASS" : "FAIL", nSample, badSample == 0 ? "PASS" : "FAIL");
|
||||
overall = overall && dsPass;
|
||||
}
|
||||
|
||||
if (!atPackSize) { std::printf("vectors: skipped (not the pack size)\nOVERALL: %s\n", overall ? "PASS" : "FAIL"); return overall ? 0 : 1; }
|
||||
|
||||
// Vectors standalone: one work-group of IGNEUM_GROUP items per base nonce (the first 32 are the vector warp).
|
||||
const uint32_t nonces = 1u << batchLog2;
|
||||
std::vector<ulong> out(nonces);
|
||||
for (int w = 0; w < IGNEUM_VEC_WARPS; ++w) {
|
||||
emu_launch(igneum_hash, (size_t)IGNEUM_GROUP, (unsigned)IGNEUM_GROUP, (const uint*)ds.data(), out.data(), (uint)IGNEUM_VEC_BASE[w], mask);
|
||||
char how[96];
|
||||
std::snprintf(how, sizeof(how), "standalone, work-group %d, sub-group width %u", IGNEUM_GROUP, gSubGroupWidth);
|
||||
overall = compareWarp((const uint64_t*)out.data(), IGNEUM_VEC_OUT[w], IGNEUM_VEC_BASE[w], how) && overall;
|
||||
}
|
||||
// In batch: every vector warp that fits in 2^batchLog2 nonces.
|
||||
emu_launch(igneum_hash, (size_t)nonces, (unsigned)IGNEUM_GROUP, (const uint*)ds.data(), out.data(), 0u, mask);
|
||||
for (int w = 0; w < IGNEUM_VEC_WARPS; ++w) {
|
||||
if ((uint64_t)IGNEUM_VEC_BASE[w] + 32ull > nonces) { std::printf("verify warp base %u in batch: skipped (batch has %u nonces)\n", IGNEUM_VEC_BASE[w], nonces); continue; }
|
||||
char how[96];
|
||||
std::snprintf(how, sizeof(how), "in batch of 2^%d, work-group %d, sub-group width %u", batchLog2, IGNEUM_GROUP, gSubGroupWidth);
|
||||
overall = compareWarp((const uint64_t*)out.data() + IGNEUM_VEC_BASE[w], IGNEUM_VEC_OUT[w], IGNEUM_VEC_BASE[w], how) && overall;
|
||||
}
|
||||
// Fingerprint of the whole batch so different configurations can be compared bit for bit.
|
||||
std::printf("batch fingerprint (FNV-1a 64 of 2^%d outputs): %016llx\n", batchLog2, (unsigned long long)fnv1a64(out.data(), (size_t)nonces * 8u));
|
||||
std::printf("OVERALL: %s\n", overall ? "PASS" : "FAIL");
|
||||
return overall ? 0 : 1;
|
||||
}
|
||||
49
proto-opencl/emu/emu_opencl.h
Normal file
49
proto-opencl/emu/emu_opencl.h
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// CPU emulation shim: the slice of OpenCL C that the generated kernel.cl uses, as C++.
|
||||
// kernel.cl includes this file when __OPENCL_VERSION__ is not defined (its own prelude does that), so the exact
|
||||
// generated text compiles with clang++ or g++ and runs on host threads. emu_main.cpp supplies the runtime below.
|
||||
// The sub-group width is a runtime setting (32 or 64) so the kernel can be checked as a 64-wide hardware wave
|
||||
// (AMD GCN/CDNA, RDNA in wave64) would run it: two logical 32-lane units in one wave. Not part of any deliverable
|
||||
// that runs on a GPU, and it says nothing about AMD hardware; only PASS/FAIL matters, rates are noise.
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
|
||||
typedef unsigned int uint;
|
||||
typedef unsigned long ulong;
|
||||
static_assert(sizeof(uint) == 4, "uint must be 32 bits");
|
||||
static_assert(sizeof(ulong) == 8, "ulong must be 64 bits (LP64 host: macOS or Linux)");
|
||||
|
||||
// Address-space qualifiers and the kernel attribute mean nothing on the host.
|
||||
#define __kernel
|
||||
#define __global
|
||||
#define __constant
|
||||
#define __local
|
||||
#define __private
|
||||
#define CLK_LOCAL_MEM_FENCE 1
|
||||
#define CLK_GLOBAL_MEM_FENCE 2
|
||||
#define IGNEUM_KERNEL_HASH
|
||||
// Work-group local memory: one arena per work-group instance, shared by its work-items (emu_local_words).
|
||||
#define IGNEUM_LOCAL_WORDS(name, n) uint* name = emu_local_words((uint)(n))
|
||||
|
||||
// Work-item functions (thread-local state set by emu_launch).
|
||||
size_t get_global_id(uint dim);
|
||||
size_t get_local_id(uint dim);
|
||||
size_t get_group_id(uint dim);
|
||||
size_t get_local_size(uint dim);
|
||||
size_t get_global_size(uint dim);
|
||||
uint get_sub_group_size(void);
|
||||
uint get_sub_group_local_id(void);
|
||||
uint get_sub_group_id(void);
|
||||
uint get_num_sub_groups(void);
|
||||
|
||||
// Synchronisation and exchange.
|
||||
void barrier(int flags); // work-group barrier
|
||||
uint sub_group_shuffle_xor(uint v, uint mask); // cl_khr_subgroup_shuffle semantics over the emulated sub-group
|
||||
uint intel_sub_group_shuffle_xor(uint v, uint mask); // same semantics
|
||||
uint sub_group_broadcast(uint v, uint lane);
|
||||
uint* emu_local_words(uint n);
|
||||
|
||||
// Integer built-ins with OpenCL semantics.
|
||||
static inline uint mul_hi(uint a, uint b) { return (uint)(((uint64_t)a * (uint64_t)b) >> 32); }
|
||||
// rotate(v, i): bits shifted left by i modulo the bit width (OpenCL C spec 6.3 and 6.12.3).
|
||||
static inline uint rotate(uint x, uint n) { n &= 31u; return n == 0u ? x : ((x << n) | (x >> (32u - n))); }
|
||||
940
proto-opencl/host.c
Normal file
940
proto-opencl/host.c
Normal file
|
|
@ -0,0 +1,940 @@
|
|||
// igneum-bench-cl: OpenCL host program for Igneum's random-program proof-of-work test harness.
|
||||
// The portable third path after Apple Metal (proto-metal) and NVIDIA CUDA (proto-cuda): it runs on AMD (Windows
|
||||
// and Linux), NVIDIA, Intel and, as a correctness check only, on Apple's deprecated OpenCL 1.2 runtime.
|
||||
//
|
||||
// TEST HARNESS ONLY. No pool, no network, no wallet, no mining protocol. It fills the dataset on the device,
|
||||
// checks the device against vectors produced on the Mac (proto-metal), and times the kernel.
|
||||
//
|
||||
// C99 plus the OpenCL 1.2 API, nothing else. The kernels are compiled from packs/<seed>/kernel.cl at runtime.
|
||||
// The pack's program.h, vectors.h and (memory-hard packs) memhard.h are included at compile time; memhard.h is the
|
||||
// host reference that fills the cache on one thread and derives dataset words for the self-test.
|
||||
//
|
||||
// Build: see README.md (macOS -framework OpenCL, Linux -lOpenCL, Windows cl.exe + OpenCL.lib), or build.sh / build.bat.
|
||||
|
||||
#define _CRT_SECURE_NO_WARNINGS
|
||||
#define CL_TARGET_OPENCL_VERSION 120
|
||||
#define CL_USE_DEPRECATED_OPENCL_1_2_APIS
|
||||
#if defined(__APPLE__) && !defined(IGNEUM_KHR_HEADERS)
|
||||
#include <OpenCL/opencl.h>
|
||||
#else
|
||||
#include <CL/cl.h>
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifdef _WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <time.h>
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
#define IGNEUM_NO_CUDA
|
||||
#include "program.h"
|
||||
#include "vectors.h"
|
||||
|
||||
#ifndef IGNEUM_DATASET_MODE
|
||||
#define IGNEUM_DATASET_MODE 0
|
||||
#endif
|
||||
#if IGNEUM_DATASET_MODE == 1
|
||||
#include "memhard.h"
|
||||
#endif
|
||||
#ifndef IGNEUM_KERNEL_PATH
|
||||
#define IGNEUM_KERNEL_PATH "kernel.cl"
|
||||
#endif
|
||||
|
||||
// Sub-group query constants (cl_khr_subgroups / OpenCL 2.1). Spelled out because OpenCL 1.2 headers lack them.
|
||||
#define IG_CL_KERNEL_MAX_SUB_GROUP_SIZE_FOR_NDRANGE 0x2033
|
||||
#define IG_CL_KERNEL_SUB_GROUP_COUNT_FOR_NDRANGE 0x2034
|
||||
// Vendor device attributes (cl_amd_device_attribute_query, cl_nv_device_attribute_query).
|
||||
#define IG_CL_DEVICE_WAVEFRONT_WIDTH_AMD 0x4043
|
||||
#define IG_CL_DEVICE_WARP_SIZE_NV 0x4003
|
||||
|
||||
typedef cl_int (CL_API_CALL *ig_pfn_subgroup_info)(cl_kernel, cl_device_id, cl_uint, size_t, const void*, size_t, void*, size_t*);
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Errors and timing
|
||||
|
||||
static const char* clErrName(cl_int e) {
|
||||
switch (e) {
|
||||
case CL_SUCCESS: return "CL_SUCCESS";
|
||||
case CL_DEVICE_NOT_FOUND: return "CL_DEVICE_NOT_FOUND";
|
||||
case CL_DEVICE_NOT_AVAILABLE: return "CL_DEVICE_NOT_AVAILABLE";
|
||||
case CL_COMPILER_NOT_AVAILABLE: return "CL_COMPILER_NOT_AVAILABLE";
|
||||
case CL_MEM_OBJECT_ALLOCATION_FAILURE: return "CL_MEM_OBJECT_ALLOCATION_FAILURE";
|
||||
case CL_OUT_OF_RESOURCES: return "CL_OUT_OF_RESOURCES";
|
||||
case CL_OUT_OF_HOST_MEMORY: return "CL_OUT_OF_HOST_MEMORY";
|
||||
case CL_PROFILING_INFO_NOT_AVAILABLE: return "CL_PROFILING_INFO_NOT_AVAILABLE";
|
||||
case CL_BUILD_PROGRAM_FAILURE: return "CL_BUILD_PROGRAM_FAILURE";
|
||||
case CL_INVALID_VALUE: return "CL_INVALID_VALUE";
|
||||
case CL_INVALID_DEVICE: return "CL_INVALID_DEVICE";
|
||||
case CL_INVALID_CONTEXT: return "CL_INVALID_CONTEXT";
|
||||
case CL_INVALID_QUEUE_PROPERTIES: return "CL_INVALID_QUEUE_PROPERTIES";
|
||||
case CL_INVALID_COMMAND_QUEUE: return "CL_INVALID_COMMAND_QUEUE";
|
||||
case CL_INVALID_MEM_OBJECT: return "CL_INVALID_MEM_OBJECT";
|
||||
case CL_INVALID_BUFFER_SIZE: return "CL_INVALID_BUFFER_SIZE";
|
||||
case CL_INVALID_BUILD_OPTIONS: return "CL_INVALID_BUILD_OPTIONS";
|
||||
case CL_INVALID_PROGRAM: return "CL_INVALID_PROGRAM";
|
||||
case CL_INVALID_PROGRAM_EXECUTABLE: return "CL_INVALID_PROGRAM_EXECUTABLE";
|
||||
case CL_INVALID_KERNEL_NAME: return "CL_INVALID_KERNEL_NAME";
|
||||
case CL_INVALID_KERNEL: return "CL_INVALID_KERNEL";
|
||||
case CL_INVALID_ARG_INDEX: return "CL_INVALID_ARG_INDEX";
|
||||
case CL_INVALID_ARG_VALUE: return "CL_INVALID_ARG_VALUE";
|
||||
case CL_INVALID_ARG_SIZE: return "CL_INVALID_ARG_SIZE";
|
||||
case CL_INVALID_KERNEL_ARGS: return "CL_INVALID_KERNEL_ARGS";
|
||||
case CL_INVALID_WORK_DIMENSION: return "CL_INVALID_WORK_DIMENSION";
|
||||
case CL_INVALID_WORK_GROUP_SIZE: return "CL_INVALID_WORK_GROUP_SIZE";
|
||||
case CL_INVALID_WORK_ITEM_SIZE: return "CL_INVALID_WORK_ITEM_SIZE";
|
||||
case CL_INVALID_GLOBAL_OFFSET: return "CL_INVALID_GLOBAL_OFFSET";
|
||||
case CL_INVALID_EVENT: return "CL_INVALID_EVENT";
|
||||
case CL_INVALID_OPERATION: return "CL_INVALID_OPERATION";
|
||||
case CL_INVALID_GLOBAL_WORK_SIZE: return "CL_INVALID_GLOBAL_WORK_SIZE";
|
||||
case CL_INVALID_PLATFORM: return "CL_INVALID_PLATFORM";
|
||||
default: return "(other)";
|
||||
}
|
||||
}
|
||||
|
||||
static void clFail(cl_int e, const char* what, int line) {
|
||||
fprintf(stderr, "OpenCL error: %s (%d)\n at host.c:%d\n in %s\n", clErrName(e), (int)e, line, what);
|
||||
exit(2);
|
||||
}
|
||||
#define CL_CHECK(call) do { cl_int err_ = (call); if (err_ != CL_SUCCESS) clFail(err_, #call, __LINE__); } while (0)
|
||||
#define CL_CHECK_ERR(err_, what) do { if ((err_) != CL_SUCCESS) clFail((err_), what, __LINE__); } while (0)
|
||||
|
||||
static double wallMs(void) {
|
||||
#ifdef _WIN32
|
||||
LARGE_INTEGER f, c;
|
||||
QueryPerformanceFrequency(&f);
|
||||
QueryPerformanceCounter(&c);
|
||||
return (double)c.QuadPart * 1000.0 / (double)f.QuadPart;
|
||||
#else
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Event profiling. A runtime that cannot report timestamps (CL_PROFILING_INFO_NOT_AVAILABLE) gives -1 and the
|
||||
// harness switches the rate to wall time instead of stopping; the count of such events is reported.
|
||||
static int gProfilingFailures = 0;
|
||||
static double eventMs(cl_event e) {
|
||||
cl_ulong t0 = 0, t1 = 0;
|
||||
if (clGetEventProfilingInfo(e, CL_PROFILING_COMMAND_START, sizeof(t0), &t0, NULL) != CL_SUCCESS ||
|
||||
clGetEventProfilingInfo(e, CL_PROFILING_COMMAND_END, sizeof(t1), &t1, NULL) != CL_SUCCESS) { ++gProfilingFailures; return -1.0; }
|
||||
return (double)(t1 - t0) / 1e6;
|
||||
}
|
||||
|
||||
static double spanMs(cl_event first, cl_event last) {
|
||||
cl_ulong t0 = 0, t1 = 0;
|
||||
if (clGetEventProfilingInfo(first, CL_PROFILING_COMMAND_START, sizeof(t0), &t0, NULL) != CL_SUCCESS ||
|
||||
clGetEventProfilingInfo(last, CL_PROFILING_COMMAND_END, sizeof(t1), &t1, NULL) != CL_SUCCESS) { ++gProfilingFailures; return -1.0; }
|
||||
return (double)(t1 - t0) / 1e6;
|
||||
}
|
||||
|
||||
static void* loadSym(const char* name) {
|
||||
#ifdef _WIN32
|
||||
HMODULE m = GetModuleHandleA("OpenCL.dll");
|
||||
return m ? (void*)GetProcAddress(m, name) : NULL;
|
||||
#else
|
||||
return dlsym(RTLD_DEFAULT, name);
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Host reference
|
||||
|
||||
static const uint32_t SEEDW[8] = IGNEUM_SEEDW_INIT;
|
||||
|
||||
#if IGNEUM_DATASET_MODE == 0
|
||||
// Same closed form as ds_elem in kernel.cl and datasetElem in proto-metal/main.swift.
|
||||
static uint32_t host_ds_elem(uint32_t i, uint32_t d0, uint32_t d1) {
|
||||
uint32_t x = i ^ d0;
|
||||
x *= 0x9E3779B1u; x ^= x >> 15;
|
||||
x += d1;
|
||||
x *= 0x85EBCA77u; x ^= x >> 13;
|
||||
x *= 0xC2B2AE3Du; x ^= x >> 16;
|
||||
return x;
|
||||
}
|
||||
#else
|
||||
static uint64_t fnv1a64(const void* p, size_t n) {
|
||||
const uint8_t* b = (const uint8_t*)p;
|
||||
uint64_t h = 0xcbf29ce484222325ull;
|
||||
size_t i;
|
||||
for (i = 0; i < n; ++i) { h ^= b[i]; h *= 0x100000001b3ull; }
|
||||
return h;
|
||||
}
|
||||
static const uint32_t CACHE_WORDS_HOST = 1u << IGNEUM_CACHE_LOG2_WORDS;
|
||||
static uint32_t* hCache = NULL;
|
||||
static uint32_t host_ds_word(uint32_t w) {
|
||||
uint32_t s[16];
|
||||
mh_item(hCache, w >> 4u, s);
|
||||
return s[w & 15u];
|
||||
}
|
||||
#endif
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Options
|
||||
|
||||
typedef struct {
|
||||
int datasetMib;
|
||||
int batchLog2;
|
||||
int batches;
|
||||
int groupWarps;
|
||||
int sweep;
|
||||
int device; // flat index into the enumerated list, -1 = first GPU
|
||||
int exchange; // 0 auto, 1 force local-memory fallback, 2 force sub-group shuffles
|
||||
int list;
|
||||
int timeWall; // 1 = rate from wall time, 0 = from device event profiling, -1 = auto (wall on the Apple platform)
|
||||
const char* kernelPath;
|
||||
const char* extraOpts;
|
||||
} Options;
|
||||
|
||||
static int packMib(void) { return (int)(((1ull << IGNEUM_DATASET_LOG2) * 4ull) >> 20); }
|
||||
|
||||
static void usage(void) {
|
||||
printf(
|
||||
"igneum-bench-cl [--list] [--device D] [--dataset-mib N] [--sweep] [--batch-log2 24] [--batches 5] [--group-warps 1]\n"
|
||||
" [--exchange auto|local|subgroup] [--kernel path/to/kernel.cl] [--build-opts \"...\"]\n"
|
||||
" --list print every OpenCL platform and device, then exit\n"
|
||||
" --device D device index from the list (default: the first GPU, else device 0)\n"
|
||||
" --dataset-mib N dataset size in MiB, power of two (default 1024; vectors are only checked at %d MiB)\n"
|
||||
" --sweep run 4, 64, 256, 512 and 1024 MiB in sequence (same sweep as the Mac and the CUDA harness)\n"
|
||||
" --batch-log2 B nonces per batch = 2^B (default 24)\n"
|
||||
" --batches N timed batches after one warm-up batch (default 5)\n"
|
||||
" --group-warps W 32-lane units per work-group, 1..8 (default 1 = one work-group per unit; sub-group shuffles need 1)\n"
|
||||
" --exchange M auto (default): sub-group shuffles when the device has them and its sub-group size is 32, else local memory\n"
|
||||
" local: force the local-memory exchange; subgroup: require sub-group shuffles or fail\n"
|
||||
" --kernel P path to the pack's kernel.cl (default: the path compiled in, %s)\n"
|
||||
" --build-opts S extra options appended to clBuildProgram (for example \"-cl-std=CL2.0\")\n"
|
||||
" --time T event (default): hashes/s from device event profiling, like cudaEvent time; wall: from host wall time.\n"
|
||||
" Apple's OpenCL runtime reports unusable event timestamps, so wall is the default on the Apple platform.\n", packMib(), IGNEUM_KERNEL_PATH);
|
||||
}
|
||||
|
||||
static int isPow2(long long v) { return v > 0 && (v & (v - 1)) == 0; }
|
||||
static int log2u32(uint32_t v) { int n = 0; while (v > 1u) { v >>= 1; ++n; } return n; }
|
||||
|
||||
static Options parseArgs(int argc, char** argv) {
|
||||
Options o;
|
||||
int i;
|
||||
o.datasetMib = 1024; o.batchLog2 = 24; o.batches = 5; o.groupWarps = 1; o.sweep = 0; o.device = -1;
|
||||
o.exchange = 0; o.list = 0; o.timeWall = -1; o.kernelPath = IGNEUM_KERNEL_PATH; o.extraOpts = "";
|
||||
for (i = 1; i < argc; ++i) {
|
||||
const char* a = argv[i];
|
||||
int needs = (strcmp(a, "--dataset-mib") == 0 || strcmp(a, "--batch-log2") == 0 || strcmp(a, "--batches") == 0 ||
|
||||
strcmp(a, "--group-warps") == 0 || strcmp(a, "--device") == 0 || strcmp(a, "--exchange") == 0 ||
|
||||
strcmp(a, "--kernel") == 0 || strcmp(a, "--build-opts") == 0 || strcmp(a, "--time") == 0);
|
||||
if (needs && i + 1 >= argc) { usage(); exit(2); }
|
||||
if (strcmp(a, "--dataset-mib") == 0) o.datasetMib = atoi(argv[++i]);
|
||||
else if (strcmp(a, "--batch-log2") == 0) o.batchLog2 = atoi(argv[++i]);
|
||||
else if (strcmp(a, "--batches") == 0) o.batches = atoi(argv[++i]);
|
||||
else if (strcmp(a, "--group-warps") == 0) o.groupWarps = atoi(argv[++i]);
|
||||
else if (strcmp(a, "--device") == 0) o.device = atoi(argv[++i]);
|
||||
else if (strcmp(a, "--kernel") == 0) o.kernelPath = argv[++i];
|
||||
else if (strcmp(a, "--build-opts") == 0) o.extraOpts = argv[++i];
|
||||
else if (strcmp(a, "--time") == 0) {
|
||||
const char* m = argv[++i];
|
||||
if (strcmp(m, "event") == 0) o.timeWall = 0;
|
||||
else if (strcmp(m, "wall") == 0) o.timeWall = 1;
|
||||
else { printf("--time must be event or wall\n"); exit(2); }
|
||||
}
|
||||
else if (strcmp(a, "--exchange") == 0) {
|
||||
const char* m = argv[++i];
|
||||
if (strcmp(m, "auto") == 0) o.exchange = 0;
|
||||
else if (strcmp(m, "local") == 0) o.exchange = 1;
|
||||
else if (strcmp(m, "subgroup") == 0) o.exchange = 2;
|
||||
else { printf("--exchange must be auto, local or subgroup\n"); exit(2); }
|
||||
}
|
||||
else if (strcmp(a, "--sweep") == 0) o.sweep = 1;
|
||||
else if (strcmp(a, "--list") == 0) o.list = 1;
|
||||
else if (strcmp(a, "-h") == 0 || strcmp(a, "--help") == 0) { usage(); exit(0); }
|
||||
else { printf("unknown argument %s\n", a); usage(); exit(2); }
|
||||
}
|
||||
if (!isPow2(o.datasetMib) || o.datasetMib < 1 || o.datasetMib > 16384) { printf("--dataset-mib must be a power of two between 1 and 16384\n"); exit(2); }
|
||||
if (o.batchLog2 < 10 || o.batchLog2 > 28) { printf("--batch-log2 must be between 10 and 28\n"); exit(2); }
|
||||
if (o.batches < 1) { printf("--batches must be at least 1\n"); exit(2); }
|
||||
if (o.groupWarps < 1 || o.groupWarps > 8) { printf("--group-warps must be between 1 and 8\n"); exit(2); }
|
||||
return o;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Devices
|
||||
|
||||
typedef struct {
|
||||
cl_platform_id platform;
|
||||
cl_device_id device;
|
||||
char platformName[256], platformVersion[256];
|
||||
char name[256], vendor[256], version[256], driver[256], cVersion[256];
|
||||
char* extensions;
|
||||
cl_device_type type;
|
||||
cl_uint computeUnits, clockMHz;
|
||||
cl_ulong globalMem, maxAlloc, localMem;
|
||||
size_t maxWorkGroup;
|
||||
int cMajor, cMinor; // OpenCL C version
|
||||
int dMajor, dMinor; // device (platform profile) version
|
||||
cl_uint amdWavefront, nvWarp; // 0 if not reported
|
||||
} DeviceInfo;
|
||||
|
||||
static void devStr(cl_device_id d, cl_device_info what, char* out, size_t n) {
|
||||
out[0] = 0;
|
||||
clGetDeviceInfo(d, what, n - 1, out, NULL);
|
||||
out[n - 1] = 0;
|
||||
}
|
||||
|
||||
static const char* typeName(cl_device_type t) {
|
||||
if (t & CL_DEVICE_TYPE_GPU) return "GPU";
|
||||
if (t & CL_DEVICE_TYPE_CPU) return "CPU";
|
||||
if (t & CL_DEVICE_TYPE_ACCELERATOR) return "accelerator";
|
||||
return "other";
|
||||
}
|
||||
|
||||
static int enumerateDevices(DeviceInfo** outList) {
|
||||
cl_uint np = 0, p;
|
||||
cl_platform_id plats[16];
|
||||
DeviceInfo* list = NULL;
|
||||
int n = 0;
|
||||
cl_int e = clGetPlatformIDs(16, plats, &np);
|
||||
if (e != CL_SUCCESS || np == 0) { *outList = NULL; return 0; }
|
||||
for (p = 0; p < np; ++p) {
|
||||
cl_uint nd = 0, d;
|
||||
cl_device_id devs[32];
|
||||
char pname[256] = {0}, pver[256] = {0};
|
||||
clGetPlatformInfo(plats[p], CL_PLATFORM_NAME, sizeof(pname) - 1, pname, NULL);
|
||||
clGetPlatformInfo(plats[p], CL_PLATFORM_VERSION, sizeof(pver) - 1, pver, NULL);
|
||||
if (clGetDeviceIDs(plats[p], CL_DEVICE_TYPE_ALL, 32, devs, &nd) != CL_SUCCESS) continue;
|
||||
for (d = 0; d < nd; ++d) {
|
||||
DeviceInfo di;
|
||||
size_t extLen = 0;
|
||||
memset(&di, 0, sizeof(di));
|
||||
di.platform = plats[p]; di.device = devs[d];
|
||||
strncpy(di.platformName, pname, 255); strncpy(di.platformVersion, pver, 255);
|
||||
devStr(devs[d], CL_DEVICE_NAME, di.name, sizeof(di.name));
|
||||
devStr(devs[d], CL_DEVICE_VENDOR, di.vendor, sizeof(di.vendor));
|
||||
devStr(devs[d], CL_DEVICE_VERSION, di.version, sizeof(di.version));
|
||||
devStr(devs[d], CL_DRIVER_VERSION, di.driver, sizeof(di.driver));
|
||||
devStr(devs[d], CL_DEVICE_OPENCL_C_VERSION, di.cVersion, sizeof(di.cVersion));
|
||||
clGetDeviceInfo(devs[d], CL_DEVICE_EXTENSIONS, 0, NULL, &extLen);
|
||||
di.extensions = (char*)calloc(extLen + 1, 1);
|
||||
if (extLen) clGetDeviceInfo(devs[d], CL_DEVICE_EXTENSIONS, extLen, di.extensions, NULL);
|
||||
clGetDeviceInfo(devs[d], CL_DEVICE_TYPE, sizeof(di.type), &di.type, NULL);
|
||||
clGetDeviceInfo(devs[d], CL_DEVICE_MAX_COMPUTE_UNITS, sizeof(di.computeUnits), &di.computeUnits, NULL);
|
||||
clGetDeviceInfo(devs[d], CL_DEVICE_MAX_CLOCK_FREQUENCY, sizeof(di.clockMHz), &di.clockMHz, NULL);
|
||||
clGetDeviceInfo(devs[d], CL_DEVICE_GLOBAL_MEM_SIZE, sizeof(di.globalMem), &di.globalMem, NULL);
|
||||
clGetDeviceInfo(devs[d], CL_DEVICE_MAX_MEM_ALLOC_SIZE, sizeof(di.maxAlloc), &di.maxAlloc, NULL);
|
||||
clGetDeviceInfo(devs[d], CL_DEVICE_LOCAL_MEM_SIZE, sizeof(di.localMem), &di.localMem, NULL);
|
||||
clGetDeviceInfo(devs[d], CL_DEVICE_MAX_WORK_GROUP_SIZE, sizeof(di.maxWorkGroup), &di.maxWorkGroup, NULL);
|
||||
if (sscanf(di.cVersion, "OpenCL C %d.%d", &di.cMajor, &di.cMinor) != 2) { di.cMajor = 1; di.cMinor = 2; }
|
||||
if (sscanf(di.version, "OpenCL %d.%d", &di.dMajor, &di.dMinor) != 2) { di.dMajor = 1; di.dMinor = 2; }
|
||||
if (strstr(di.extensions, "cl_amd_device_attribute_query"))
|
||||
clGetDeviceInfo(devs[d], IG_CL_DEVICE_WAVEFRONT_WIDTH_AMD, sizeof(di.amdWavefront), &di.amdWavefront, NULL);
|
||||
if (strstr(di.extensions, "cl_nv_device_attribute_query"))
|
||||
clGetDeviceInfo(devs[d], IG_CL_DEVICE_WARP_SIZE_NV, sizeof(di.nvWarp), &di.nvWarp, NULL);
|
||||
list = (DeviceInfo*)realloc(list, sizeof(DeviceInfo) * (size_t)(n + 1));
|
||||
list[n++] = di;
|
||||
}
|
||||
}
|
||||
*outList = list;
|
||||
return n;
|
||||
}
|
||||
|
||||
static void printDevice(int idx, const DeviceInfo* d, int chosen) {
|
||||
const char* subExt = strstr(d->extensions, "cl_khr_subgroup_shuffle") ? "cl_khr_subgroup_shuffle" :
|
||||
strstr(d->extensions, "cl_intel_subgroups") ? "cl_intel_subgroups" :
|
||||
strstr(d->extensions, "cl_khr_subgroups") ? "cl_khr_subgroups (no shuffle extension)" : "none";
|
||||
printf("%s[%d] %s | %s (%s)\n", chosen ? "*" : " ", idx, d->name, d->platformName, d->platformVersion);
|
||||
printf(" %s, vendor %s, driver %s, %s, %u compute units, %u MHz\n", typeName(d->type), d->vendor, d->driver, d->cVersion, d->computeUnits, d->clockMHz);
|
||||
printf(" global %llu MiB, max alloc %llu MiB, local %llu KiB, max work-group %llu, sub-group extension: %s",
|
||||
(unsigned long long)(d->globalMem >> 20), (unsigned long long)(d->maxAlloc >> 20), (unsigned long long)(d->localMem >> 10),
|
||||
(unsigned long long)d->maxWorkGroup, subExt);
|
||||
if (d->amdWavefront) printf(", AMD wavefront width %u", d->amdWavefront);
|
||||
if (d->nvWarp) printf(", NVIDIA warp size %u", d->nvWarp);
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Program build
|
||||
|
||||
static char* readFile(const char* path, size_t* len) {
|
||||
FILE* f = fopen(path, "rb");
|
||||
char* buf;
|
||||
long n;
|
||||
if (!f) return NULL;
|
||||
fseek(f, 0, SEEK_END); n = ftell(f); fseek(f, 0, SEEK_SET);
|
||||
if (n < 0) { fclose(f); return NULL; }
|
||||
buf = (char*)malloc((size_t)n + 1);
|
||||
if (fread(buf, 1, (size_t)n, f) != (size_t)n) { fclose(f); free(buf); return NULL; }
|
||||
buf[n] = 0;
|
||||
fclose(f);
|
||||
*len = (size_t)n;
|
||||
return buf;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
cl_context ctx;
|
||||
cl_command_queue q;
|
||||
cl_program prog;
|
||||
cl_kernel kHash, kCacheFill, kBuild, kFill;
|
||||
int exchange; // 0 local memory, 1 khr sub-group shuffle, 2 intel
|
||||
size_t subGroupSize; // as queried for a 32-item work-group, 0 if not queried
|
||||
char exchangeNote[512];
|
||||
char buildOptions[512];
|
||||
} Device;
|
||||
|
||||
static const char* exchangeName(int m) { return m == 1 ? "sub_group_shuffle_xor (cl_khr_subgroup_shuffle)" : m == 2 ? "intel_sub_group_shuffle_xor (cl_intel_subgroups)" : "local-memory exchange with barrier"; }
|
||||
|
||||
// Returns 0 on success, 1 on build failure (log printed).
|
||||
static int buildProgram(Device* dv, const DeviceInfo* di, const char* src, size_t srcLen, int exchangeMode, int groupSize, const char* extra) {
|
||||
cl_int err = 0;
|
||||
const char* std;
|
||||
// The sub-group built-ins need OpenCL C 2.0 or 3.0. OpenCL 3.0 devices may report "OpenCL C 1.2" as the default
|
||||
// CL_DEVICE_OPENCL_C_VERSION while supporting 3.0 (the 3.0 API lists all versions; the 1.2 API cannot ask), so
|
||||
// the device version counts as well. The local-memory variant is always built as OpenCL C 1.2, the same text everywhere.
|
||||
int major = di->cMajor > di->dMajor ? di->cMajor : di->dMajor;
|
||||
if (exchangeMode == 0) std = "-cl-std=CL1.2";
|
||||
else if (major >= 3) std = "-cl-std=CL3.0";
|
||||
else if (major >= 2) std = "-cl-std=CL2.0";
|
||||
else std = "-cl-std=CL1.2";
|
||||
snprintf(dv->buildOptions, sizeof(dv->buildOptions), "%s -D IGNEUM_GROUP=%d -D IGNEUM_EXCHANGE=%d %s", std, groupSize, exchangeMode, extra);
|
||||
dv->prog = clCreateProgramWithSource(dv->ctx, 1, &src, &srcLen, &err);
|
||||
CL_CHECK_ERR(err, "clCreateProgramWithSource");
|
||||
err = clBuildProgram(dv->prog, 1, &di->device, dv->buildOptions, NULL, NULL);
|
||||
if (err != CL_SUCCESS) {
|
||||
size_t logLen = 0;
|
||||
char* log;
|
||||
clGetProgramBuildInfo(dv->prog, di->device, CL_PROGRAM_BUILD_LOG, 0, NULL, &logLen);
|
||||
log = (char*)calloc(logLen + 1, 1);
|
||||
if (logLen) clGetProgramBuildInfo(dv->prog, di->device, CL_PROGRAM_BUILD_LOG, logLen, log, NULL);
|
||||
printf("build FAILED (%s) with options \"%s\"\n--- build log ---\n%s\n--- end of build log ---\n", clErrName(err), dv->buildOptions, log);
|
||||
free(log);
|
||||
clReleaseProgram(dv->prog); dv->prog = NULL;
|
||||
return 1;
|
||||
}
|
||||
dv->kHash = clCreateKernel(dv->prog, "igneum_hash", &err); CL_CHECK_ERR(err, "clCreateKernel igneum_hash");
|
||||
#if IGNEUM_DATASET_MODE == 1
|
||||
dv->kCacheFill = clCreateKernel(dv->prog, "igneum_cache_fill", &err); CL_CHECK_ERR(err, "clCreateKernel igneum_cache_fill");
|
||||
dv->kBuild = clCreateKernel(dv->prog, "igneum_build", &err); CL_CHECK_ERR(err, "clCreateKernel igneum_build");
|
||||
#else
|
||||
dv->kFill = clCreateKernel(dv->prog, "igneum_fill", &err); CL_CHECK_ERR(err, "clCreateKernel igneum_fill");
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void releaseProgram(Device* dv) {
|
||||
if (dv->kHash) clReleaseKernel(dv->kHash);
|
||||
if (dv->kCacheFill) clReleaseKernel(dv->kCacheFill);
|
||||
if (dv->kBuild) clReleaseKernel(dv->kBuild);
|
||||
if (dv->kFill) clReleaseKernel(dv->kFill);
|
||||
if (dv->prog) clReleaseProgram(dv->prog);
|
||||
dv->kHash = dv->kCacheFill = dv->kBuild = dv->kFill = NULL; dv->prog = NULL;
|
||||
}
|
||||
|
||||
// Sub-group size of igneum_hash for a work-group of `local` items. 0 if the query is unavailable (reason in *why).
|
||||
static size_t querySubGroupSize(const Device* dv, const DeviceInfo* di, size_t local, char* why, size_t whyLen) {
|
||||
ig_pfn_subgroup_info fn = (ig_pfn_subgroup_info)clGetExtensionFunctionAddressForPlatform(di->platform, "clGetKernelSubGroupInfoKHR");
|
||||
const char* via = "clGetKernelSubGroupInfoKHR";
|
||||
size_t sg = 0;
|
||||
cl_int e;
|
||||
if (!fn) { fn = (ig_pfn_subgroup_info)loadSym("clGetKernelSubGroupInfo"); via = "clGetKernelSubGroupInfo (OpenCL 2.1 core, through the loader)"; }
|
||||
if (!fn) { snprintf(why, whyLen, "the sub-group size could not be queried (neither clGetKernelSubGroupInfoKHR nor clGetKernelSubGroupInfo is available)"); return 0; }
|
||||
e = fn(dv->kHash, di->device, IG_CL_KERNEL_MAX_SUB_GROUP_SIZE_FOR_NDRANGE, sizeof(local), &local, sizeof(sg), &sg, NULL);
|
||||
if (e != CL_SUCCESS) { snprintf(why, whyLen, "the sub-group size query failed: %s returned %s (%d)", via, clErrName(e), (int)e); return 0; }
|
||||
snprintf(why, whyLen, "queried through %s", via);
|
||||
return sg;
|
||||
}
|
||||
|
||||
// Decide the exchange implementation and build. See WAVEFRONT.md for the rule.
|
||||
static void setupProgram(Device* dv, const DeviceInfo* di, const Options* o, const char* src, size_t srcLen) {
|
||||
int groupSize = 32 * o->groupWarps;
|
||||
int want = 0;
|
||||
const char* why = "";
|
||||
if (strstr(di->extensions, "cl_khr_subgroup_shuffle")) want = 1;
|
||||
else if (strstr(di->extensions, "cl_intel_subgroups")) want = 2;
|
||||
else why = "device lists no sub-group shuffle extension";
|
||||
if (o->exchange == 1) { want = 0; why = "forced by --exchange local"; }
|
||||
if (want != 0 && o->groupWarps != 1) {
|
||||
if (o->exchange == 2) { printf("FAIL: --exchange subgroup needs --group-warps 1 (one work-group = one 32-lane unit)\n"); exit(2); }
|
||||
want = 0; why = "--group-warps is not 1, so a work-group is not one 32-lane unit";
|
||||
}
|
||||
if (want == 0 && o->exchange == 2) { printf("FAIL: --exchange subgroup requested but %s\n", why); exit(2); }
|
||||
|
||||
if (want != 0) {
|
||||
if (buildProgram(dv, di, src, srcLen, want, groupSize, o->extraOpts) != 0) {
|
||||
if (o->exchange == 2) { printf("FAIL: the sub-group variant did not compile\n"); exit(2); }
|
||||
want = 0; why = "the sub-group variant did not compile (log above)";
|
||||
} else {
|
||||
static char qwhy[256];
|
||||
size_t sg = querySubGroupSize(dv, di, 32, qwhy, sizeof(qwhy));
|
||||
if (sg == 0) {
|
||||
// Second choice: a probe kernel from the same build reports get_sub_group_size() for a 32-item work-group.
|
||||
// Weaker than the per-kernel query (a compiler may pick the wave width per kernel), and the note says so.
|
||||
cl_int perr = 0;
|
||||
cl_kernel kp = clCreateKernel(dv->prog, "igneum_probe_subgroup", &perr);
|
||||
if (perr == CL_SUCCESS) {
|
||||
cl_uint probe[2] = { 0u, 0u };
|
||||
cl_mem pb = clCreateBuffer(dv->ctx, CL_MEM_READ_WRITE, sizeof(probe), NULL, &perr);
|
||||
size_t g = 32, l = 32;
|
||||
if (perr == CL_SUCCESS && clSetKernelArg(kp, 0, sizeof(cl_mem), &pb) == CL_SUCCESS &&
|
||||
clEnqueueNDRangeKernel(dv->q, kp, 1, NULL, &g, &l, 0, NULL, NULL) == CL_SUCCESS &&
|
||||
clEnqueueReadBuffer(dv->q, pb, CL_TRUE, 0, sizeof(probe), probe, 0, NULL, NULL) == CL_SUCCESS && probe[0] != 0u) {
|
||||
static char pwhy[400];
|
||||
snprintf(pwhy, sizeof(pwhy), "%s; probe kernel reports get_sub_group_size() %u and %u sub-group(s) for a 32-item work-group (weaker than the per-kernel query)", qwhy, probe[0], probe[1]);
|
||||
sg = probe[0];
|
||||
strncpy(qwhy, pwhy, sizeof(qwhy) - 1); qwhy[sizeof(qwhy) - 1] = 0;
|
||||
}
|
||||
if (pb) clReleaseMemObject(pb);
|
||||
clReleaseKernel(kp);
|
||||
}
|
||||
}
|
||||
dv->subGroupSize = sg;
|
||||
if (sg == 32) {
|
||||
dv->exchange = want;
|
||||
snprintf(dv->exchangeNote, sizeof(dv->exchangeNote), "%s, sub-group size %llu for a 32-item work-group (%s)", exchangeName(want), (unsigned long long)sg, qwhy);
|
||||
return;
|
||||
}
|
||||
releaseProgram(dv);
|
||||
if (sg == 0) why = qwhy;
|
||||
else why = "the sub-group size for a 32-item work-group is not 32";
|
||||
if (o->exchange == 2) { printf("FAIL: --exchange subgroup requested but %s (sub-group size %llu)\n", why, (unsigned long long)sg); exit(2); }
|
||||
}
|
||||
}
|
||||
if (buildProgram(dv, di, src, srcLen, 0, groupSize, o->extraOpts) != 0) { printf("FAIL: kernel.cl did not compile\n"); exit(2); }
|
||||
dv->exchange = 0;
|
||||
if (dv->subGroupSize) snprintf(dv->exchangeNote, sizeof(dv->exchangeNote), "%s (%s; queried sub-group size %llu)", exchangeName(0), why, (unsigned long long)dv->subGroupSize);
|
||||
else snprintf(dv->exchangeNote, sizeof(dv->exchangeNote), "%s (%s)", exchangeName(0), why);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Launch helpers
|
||||
|
||||
static size_t kernelMaxLocal(const Device* dv, cl_kernel k, const DeviceInfo* di, size_t want) {
|
||||
size_t wg = 0;
|
||||
(void)dv;
|
||||
if (clGetKernelWorkGroupInfo(k, di->device, CL_KERNEL_WORK_GROUP_SIZE, sizeof(wg), &wg, NULL) != CL_SUCCESS || wg == 0) wg = want;
|
||||
if (wg > di->maxWorkGroup) wg = di->maxWorkGroup;
|
||||
return wg < want ? wg : want;
|
||||
}
|
||||
|
||||
static cl_event launch1D(const Device* dv, cl_kernel k, size_t global, size_t local) {
|
||||
cl_event ev = NULL;
|
||||
size_t g = ((global + local - 1) / local) * local;
|
||||
CL_CHECK(clEnqueueNDRangeKernel(dv->q, k, 1, NULL, &g, &local, 0, NULL, &ev));
|
||||
return ev;
|
||||
}
|
||||
|
||||
static cl_event launchHash(const Device* dv, cl_mem ds, cl_mem out, cl_uint baseNonce, cl_uint mask, size_t nonces, size_t groupSize) {
|
||||
CL_CHECK(clSetKernelArg(dv->kHash, 0, sizeof(cl_mem), &ds));
|
||||
CL_CHECK(clSetKernelArg(dv->kHash, 1, sizeof(cl_mem), &out));
|
||||
CL_CHECK(clSetKernelArg(dv->kHash, 2, sizeof(cl_uint), &baseNonce));
|
||||
CL_CHECK(clSetKernelArg(dv->kHash, 3, sizeof(cl_uint), &mask));
|
||||
return launch1D(dv, dv->kHash, nonces, groupSize);
|
||||
}
|
||||
|
||||
static void readWords(const Device* dv, cl_mem buf, size_t wordIndex, size_t nWords, uint32_t* dst) {
|
||||
CL_CHECK(clEnqueueReadBuffer(dv->q, buf, CL_TRUE, wordIndex * 4u, nWords * 4u, dst, 0, NULL, NULL));
|
||||
}
|
||||
|
||||
static int compareWarp(const uint64_t* got, const uint64_t* want, uint32_t base, const char* how) {
|
||||
int bad = 0, first = -1, l;
|
||||
for (l = 0; l < 32; ++l) if (got[l] != want[l]) { if (first < 0) first = l; ++bad; }
|
||||
if (bad == 0) printf("verify warp base %u (nonces %u..%u) %s: PASS\n", base, base, base + 31u, how);
|
||||
else printf("verify warp base %u (nonces %u..%u) %s: FAIL %d of 32 lanes differ, first lane %d: device=%016llx expected=%016llx\n",
|
||||
base, base, base + 31u, how, bad, first, (unsigned long long)got[first], (unsigned long long)want[first]);
|
||||
return bad == 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Cache (memory-hard packs)
|
||||
|
||||
#if IGNEUM_DATASET_MODE == 1
|
||||
static cl_mem gCache = NULL;
|
||||
static double gCacheFillFirstMs = 0, gCacheFillSecondMs = 0, gCacheHostMs = 0;
|
||||
static int gCachePass = 0;
|
||||
|
||||
static int setupCache(Device* dv, const DeviceInfo* di) {
|
||||
size_t bytes = (size_t)CACHE_WORDS_HOST * 4u;
|
||||
cl_int err = 0;
|
||||
cl_uint nSeg = IGNEUM_CACHE_SEGMENTS;
|
||||
size_t local;
|
||||
int pass;
|
||||
uint32_t* dev;
|
||||
uint32_t seg;
|
||||
double h0;
|
||||
int same, fnvOk, headOk, lastOk;
|
||||
uint64_t fnv;
|
||||
gCache = clCreateBuffer(dv->ctx, CL_MEM_READ_WRITE, bytes, NULL, &err);
|
||||
CL_CHECK_ERR(err, "clCreateBuffer cache");
|
||||
local = kernelMaxLocal(dv, dv->kCacheFill, di, 256);
|
||||
for (pass = 0; pass < 2; ++pass) {
|
||||
cl_event ev;
|
||||
CL_CHECK(clSetKernelArg(dv->kCacheFill, 0, sizeof(cl_mem), &gCache));
|
||||
CL_CHECK(clSetKernelArg(dv->kCacheFill, 1, sizeof(cl_uint), &nSeg));
|
||||
ev = launch1D(dv, dv->kCacheFill, nSeg, local);
|
||||
CL_CHECK(clFinish(dv->q));
|
||||
if (pass == 0) gCacheFillFirstMs = eventMs(ev); else gCacheFillSecondMs = eventMs(ev);
|
||||
clReleaseEvent(ev);
|
||||
}
|
||||
printf("cache fill (device): %.2f ms first, %.2f ms second (%u chains x %u ChaCha blocks, %u MiB, work-group %llu)\n",
|
||||
gCacheFillFirstMs, gCacheFillSecondMs, (unsigned)IGNEUM_CACHE_SEGMENTS, 1u << IGNEUM_CACHE_SEGMENT_LOG2_LINES,
|
||||
(unsigned)(bytes >> 20), (unsigned long long)local);
|
||||
|
||||
hCache = (uint32_t*)malloc(bytes);
|
||||
h0 = wallMs();
|
||||
for (seg = 0; seg < IGNEUM_CACHE_SEGMENTS; ++seg) mh_cache_segment(hCache, seg);
|
||||
gCacheHostMs = wallMs() - h0;
|
||||
printf("cache fill (host, one thread): %.1f ms\n", gCacheHostMs);
|
||||
|
||||
dev = (uint32_t*)malloc(bytes);
|
||||
readWords(dv, gCache, 0, CACHE_WORDS_HOST, dev);
|
||||
same = memcmp(dev, hCache, bytes) == 0;
|
||||
fnv = fnv1a64(hCache, bytes);
|
||||
fnvOk = (fnv == IGNEUM_CACHE_FNV64);
|
||||
headOk = memcmp(hCache, IGNEUM_CACHE_HEAD, 64) == 0;
|
||||
lastOk = memcmp(hCache + CACHE_WORDS_HOST - 16u, IGNEUM_CACHE_LAST, 64) == 0;
|
||||
if (!same) {
|
||||
uint32_t i;
|
||||
for (i = 0; i < CACHE_WORDS_HOST; ++i) if (dev[i] != hCache[i]) {
|
||||
printf(" cache[%u]: device 0x%08x host 0x%08x (first difference)\n", i, dev[i], hCache[i]); break;
|
||||
}
|
||||
}
|
||||
free(dev);
|
||||
gCachePass = same && fnvOk && headOk && lastOk;
|
||||
printf("cache check: %s (device == host all %u words %s, host FNV-1a 64 %016llx vs Mac %016llx %s, head 16 vs Mac %s, last line vs Mac %s)\n",
|
||||
gCachePass ? "PASS" : "FAIL", CACHE_WORDS_HOST, same ? "PASS" : "FAIL",
|
||||
(unsigned long long)fnv, (unsigned long long)IGNEUM_CACHE_FNV64, fnvOk ? "PASS" : "FAIL",
|
||||
headOk ? "PASS" : "FAIL", lastOk ? "PASS" : "FAIL");
|
||||
return gCachePass;
|
||||
}
|
||||
#endif
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// One dataset size: fill or build, self-test, vectors, bench
|
||||
|
||||
typedef struct {
|
||||
int mib;
|
||||
uint32_t words;
|
||||
double fillFirstMs, fillSecondMs;
|
||||
int dsPass;
|
||||
int vecChecked, vecPass;
|
||||
double gpuMs, wallMsTimed;
|
||||
double hashesPerSec, gbps;
|
||||
} SizeResult;
|
||||
|
||||
static SizeResult runSize(Device* dv, const DeviceInfo* di, const Options* o, int mib, cl_mem dOut, uint32_t nonces) {
|
||||
SizeResult r;
|
||||
uint64_t bytes = (uint64_t)mib << 20;
|
||||
uint32_t mask;
|
||||
int atPackSize;
|
||||
cl_int err = 0;
|
||||
cl_mem dDs;
|
||||
int pass;
|
||||
size_t groupSize = 32u * (size_t)o->groupWarps;
|
||||
uint64_t got[32];
|
||||
double w0, w1, t0, t1, total;
|
||||
cl_event* evs;
|
||||
int b;
|
||||
memset(&r, 0, sizeof(r));
|
||||
r.mib = mib;
|
||||
r.words = (uint32_t)(bytes / 4ull);
|
||||
mask = r.words - 1u;
|
||||
atPackSize = (r.words == (1u << IGNEUM_DATASET_LOG2));
|
||||
printf("\n=== dataset %d MiB (2^%d words, mask 0x%08x)%s ===\n", mib, log2u32(r.words), mask,
|
||||
atPackSize ? "" : " [not the pack size: vectors skipped, dataset head and random points still checked]");
|
||||
if ((uint64_t)di->maxAlloc < bytes) {
|
||||
printf("FAIL: CL_DEVICE_MAX_MEM_ALLOC_SIZE is %llu MiB, the dataset needs %d MiB in one buffer\n", (unsigned long long)(di->maxAlloc >> 20), mib);
|
||||
exit(2);
|
||||
}
|
||||
dDs = clCreateBuffer(dv->ctx, CL_MEM_READ_WRITE, (size_t)bytes, NULL, &err);
|
||||
CL_CHECK_ERR(err, "clCreateBuffer dataset");
|
||||
|
||||
// Fill (or build) twice: the Mac showed a first-touch cost on the first fill of a process.
|
||||
for (pass = 0; pass < 2; ++pass) {
|
||||
cl_event ev;
|
||||
#if IGNEUM_DATASET_MODE == 1
|
||||
cl_uint nItems = r.words / 16u;
|
||||
size_t local = kernelMaxLocal(dv, dv->kBuild, di, 256);
|
||||
CL_CHECK(clSetKernelArg(dv->kBuild, 0, sizeof(cl_mem), &dDs));
|
||||
CL_CHECK(clSetKernelArg(dv->kBuild, 1, sizeof(cl_mem), &gCache));
|
||||
CL_CHECK(clSetKernelArg(dv->kBuild, 2, sizeof(cl_uint), &nItems));
|
||||
ev = launch1D(dv, dv->kBuild, nItems, local);
|
||||
#else
|
||||
cl_uint n = r.words, d0 = IGNEUM_DAY0, d1 = IGNEUM_DAY1;
|
||||
size_t local = kernelMaxLocal(dv, dv->kFill, di, 256);
|
||||
CL_CHECK(clSetKernelArg(dv->kFill, 0, sizeof(cl_mem), &dDs));
|
||||
CL_CHECK(clSetKernelArg(dv->kFill, 1, sizeof(cl_uint), &n));
|
||||
CL_CHECK(clSetKernelArg(dv->kFill, 2, sizeof(cl_uint), &d0));
|
||||
CL_CHECK(clSetKernelArg(dv->kFill, 3, sizeof(cl_uint), &d1));
|
||||
ev = launch1D(dv, dv->kFill, n, local);
|
||||
#endif
|
||||
CL_CHECK(clFinish(dv->q));
|
||||
if (pass == 0) r.fillFirstMs = eventMs(ev); else r.fillSecondMs = eventMs(ev);
|
||||
clReleaseEvent(ev);
|
||||
}
|
||||
#if IGNEUM_DATASET_MODE == 1
|
||||
printf("dataset build (memory-hard, from the cache): %.2f ms first, %.2f ms second -> %.1f M items/s, %.2f G cache-line reads/s (second, device time)\n",
|
||||
r.fillFirstMs, r.fillSecondMs, (double)(r.words / 16u) / 1e6 / (r.fillSecondMs / 1000.0),
|
||||
(double)(r.words / 16u) * (double)IGNEUM_ITEM_ROUNDS / 1e9 / (r.fillSecondMs / 1000.0));
|
||||
#else
|
||||
printf("dataset fill: %.2f ms first, %.2f ms second -> %.0f GB/s write (second, device time)\n",
|
||||
r.fillFirstMs, r.fillSecondMs, (double)bytes / 1e9 / (r.fillSecondMs / 1000.0));
|
||||
#endif
|
||||
|
||||
// Dataset self-test: head 16 (any size), element [MASK] (pack size only), 64 pseudo-random points vs the host
|
||||
// formula (closed form) or the host derivation from the host cache (memory-hard), and the Mac's 64 sampled words.
|
||||
{
|
||||
uint32_t head[16];
|
||||
int badHead = 0, i, k, badRnd = 0, badSample = 0, nSample = 0, lastOk = 1;
|
||||
const char* lastText = "skipped";
|
||||
uint64_t s = 0x9E3779B97F4A7C15ull ^ (uint64_t)r.words;
|
||||
readWords(dv, dDs, 0, 16, head);
|
||||
for (i = 0; i < 16; ++i) if (head[i] != IGNEUM_DS_HEAD[i]) {
|
||||
if (badHead == 0) printf(" dataset[%d] = 0x%08x, expected 0x%08x\n", i, head[i], IGNEUM_DS_HEAD[i]);
|
||||
++badHead;
|
||||
}
|
||||
if (atPackSize) {
|
||||
uint32_t last = 0;
|
||||
readWords(dv, dDs, IGNEUM_DS_LAST_INDEX, 1, &last);
|
||||
lastOk = (last == IGNEUM_DS_LAST);
|
||||
lastText = lastOk ? "PASS" : "FAIL";
|
||||
if (!lastOk) printf(" dataset[%u] = 0x%08x, expected 0x%08x\n", IGNEUM_DS_LAST_INDEX, last, IGNEUM_DS_LAST);
|
||||
}
|
||||
for (k = 0; k < 64; ++k) {
|
||||
uint64_t z;
|
||||
uint32_t idx, v = 0, want;
|
||||
s += 0x9E3779B97F4A7C15ull;
|
||||
z = s;
|
||||
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ull;
|
||||
z = (z ^ (z >> 27)) * 0x94D049BB133111EBull;
|
||||
z ^= z >> 31;
|
||||
idx = (uint32_t)z & mask;
|
||||
readWords(dv, dDs, idx, 1, &v);
|
||||
#if IGNEUM_DATASET_MODE == 1
|
||||
want = host_ds_word(idx);
|
||||
#else
|
||||
want = host_ds_elem(idx, IGNEUM_DAY0, IGNEUM_DAY1);
|
||||
#endif
|
||||
if (v != want) {
|
||||
if (badRnd == 0) printf(" dataset[%u] = 0x%08x, host %s 0x%08x\n", idx, v, IGNEUM_DATASET_MODE == 1 ? "derivation" : "formula", want);
|
||||
++badRnd;
|
||||
}
|
||||
}
|
||||
#ifdef IGNEUM_DS_SAMPLES
|
||||
for (k = 0; k < IGNEUM_DS_SAMPLES; ++k) {
|
||||
uint32_t v = 0;
|
||||
if (IGNEUM_DS_SAMPLE_INDEX[k] > mask) continue;
|
||||
++nSample;
|
||||
readWords(dv, dDs, IGNEUM_DS_SAMPLE_INDEX[k], 1, &v);
|
||||
if (v != IGNEUM_DS_SAMPLE_VALUE[k]) {
|
||||
if (badSample == 0) printf(" dataset[%u] = 0x%08x, Mac 0x%08x\n", IGNEUM_DS_SAMPLE_INDEX[k], v, IGNEUM_DS_SAMPLE_VALUE[k]);
|
||||
++badSample;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
r.dsPass = (badHead == 0 && lastOk && badRnd == 0 && badSample == 0);
|
||||
printf("dataset self-test: %s (head 16 vs Mac %s, element [MASK] vs Mac %s, 64 random points vs host %s %s, %d Mac samples %s)\n",
|
||||
r.dsPass ? "PASS" : "FAIL", badHead == 0 ? "PASS" : "FAIL", lastText,
|
||||
IGNEUM_DATASET_MODE == 1 ? "derivation" : "formula", badRnd == 0 ? "PASS" : "FAIL",
|
||||
nSample, nSample == 0 ? "none in pack" : (badSample == 0 ? "PASS" : "FAIL"));
|
||||
}
|
||||
|
||||
// Vectors, standalone: one 32-item work-group per base nonce, exactly like the Mac cross-check.
|
||||
if (atPackSize) {
|
||||
int w;
|
||||
r.vecChecked = 1; r.vecPass = 1;
|
||||
for (w = 0; w < IGNEUM_VEC_WARPS; ++w) {
|
||||
cl_event ev;
|
||||
if (o->groupWarps != 1) {
|
||||
// reqd_work_group_size pins the hash kernel to 32 x group-warps items; run one full group and read its first 32.
|
||||
ev = launchHash(dv, dDs, dOut, IGNEUM_VEC_BASE[w], mask, groupSize, groupSize);
|
||||
} else {
|
||||
ev = launchHash(dv, dDs, dOut, IGNEUM_VEC_BASE[w], mask, 32u, 32u);
|
||||
}
|
||||
CL_CHECK(clFinish(dv->q));
|
||||
clReleaseEvent(ev);
|
||||
CL_CHECK(clEnqueueReadBuffer(dv->q, dOut, CL_TRUE, 0, sizeof(got), got, 0, NULL, NULL));
|
||||
r.vecPass = compareWarp(got, IGNEUM_VEC_OUT[w], IGNEUM_VEC_BASE[w], o->groupWarps == 1 ? "standalone, 1 unit/work-group" : "standalone, first unit of one work-group") && r.vecPass;
|
||||
}
|
||||
} else {
|
||||
printf("vectors: skipped (the pack's vectors are for %d MiB)\n", packMib());
|
||||
}
|
||||
|
||||
// Warm-up batch at base nonce 0. With the default 2^24 nonces it contains all three vector warps,
|
||||
// so the bench configuration itself (work-group = 32 x group-warps) is also checked bit for bit.
|
||||
w0 = wallMs();
|
||||
{
|
||||
cl_event ev = launchHash(dv, dDs, dOut, 0u, mask, nonces, groupSize);
|
||||
CL_CHECK(clFinish(dv->q));
|
||||
clReleaseEvent(ev);
|
||||
}
|
||||
w1 = wallMs();
|
||||
printf("warm-up batch: %u hashes in %.2f ms wall\n", nonces, w1 - w0);
|
||||
if (atPackSize) {
|
||||
// Fingerprint of every output in the batch, so two runs (two devices, two exchange paths, the CPU emulator at the
|
||||
// same --batch-log2) can be compared for all nonces, not only the vector warps.
|
||||
uint64_t* all = (uint64_t*)malloc((size_t)nonces * sizeof(uint64_t));
|
||||
uint64_t fp = 0xcbf29ce484222325ull;
|
||||
size_t i;
|
||||
CL_CHECK(clEnqueueReadBuffer(dv->q, dOut, CL_TRUE, 0, (size_t)nonces * sizeof(uint64_t), all, 0, NULL, NULL));
|
||||
for (i = 0; i < (size_t)nonces * 8u; ++i) { fp ^= ((const uint8_t*)all)[i]; fp *= 0x100000001b3ull; }
|
||||
free(all);
|
||||
printf("batch fingerprint (FNV-1a 64 of 2^%d outputs at base nonce 0): %016llx\n", o->batchLog2, (unsigned long long)fp);
|
||||
}
|
||||
if (atPackSize) {
|
||||
char how[64];
|
||||
int w;
|
||||
snprintf(how, sizeof(how), "in batch, %d unit(s)/work-group", o->groupWarps);
|
||||
for (w = 0; w < IGNEUM_VEC_WARPS; ++w) {
|
||||
if ((uint64_t)IGNEUM_VEC_BASE[w] + 32ull > (uint64_t)nonces) {
|
||||
printf("verify warp base %u in batch: skipped (batch has %u nonces)\n", IGNEUM_VEC_BASE[w], nonces);
|
||||
continue;
|
||||
}
|
||||
CL_CHECK(clEnqueueReadBuffer(dv->q, dOut, CL_TRUE, (size_t)IGNEUM_VEC_BASE[w] * 8u, sizeof(got), got, 0, NULL, NULL));
|
||||
r.vecPass = compareWarp(got, IGNEUM_VEC_OUT[w], IGNEUM_VEC_BASE[w], how) && r.vecPass;
|
||||
}
|
||||
}
|
||||
|
||||
// Timed batches. Base nonces (b * nonces) mod 2^32, as in the Metal and CUDA runs. Device time is the span from
|
||||
// the first batch's start to the last batch's end from event profiling, like cudaEvent elapsed time.
|
||||
evs = (cl_event*)calloc((size_t)o->batches, sizeof(cl_event));
|
||||
t0 = wallMs();
|
||||
for (b = 1; b <= o->batches; ++b) {
|
||||
uint32_t base = (uint32_t)((uint64_t)b * (uint64_t)nonces);
|
||||
evs[b - 1] = launchHash(dv, dDs, dOut, base, mask, nonces, groupSize);
|
||||
}
|
||||
CL_CHECK(clFinish(dv->q));
|
||||
t1 = wallMs();
|
||||
total = (double)nonces * (double)o->batches;
|
||||
r.gpuMs = spanMs(evs[0], evs[o->batches - 1]);
|
||||
for (b = 0; b < o->batches; ++b) clReleaseEvent(evs[b]);
|
||||
free(evs);
|
||||
r.wallMsTimed = t1 - t0;
|
||||
if (r.gpuMs < 0 && !o->timeWall) { printf("NOTE: event profiling unavailable on this runtime; the rate uses wall time\n"); ((Options*)o)->timeWall = 1; }
|
||||
r.hashesPerSec = total / ((o->timeWall ? r.wallMsTimed : r.gpuMs) / 1000.0);
|
||||
r.gbps = r.hashesPerSec * (double)IGNEUM_LOADS_PER_HASH * 4.0 / 1e9;
|
||||
printf("timed: %d batches x %u hashes = %.0f hashes (rate below from %s time)\n", o->batches, nonces, total, o->timeWall ? "wall" : "device event");
|
||||
printf(" device %.2f ms -> %.3f Mhash/s%s\n", r.gpuMs, total / (r.gpuMs / 1000.0) / 1e6, o->timeWall ? " (event profiling, not used for the rate on this platform)" : "");
|
||||
printf(" wall %.2f ms -> %.3f Mhash/s\n", r.wallMsTimed, total / (r.wallMsTimed / 1000.0) / 1e6);
|
||||
printf(" rate %.3f Mhash/s (%.0f hashes/s), %.2f GB/s useful (loads x 4 B)\n", r.hashesPerSec / 1e6, r.hashesPerSec, r.gbps);
|
||||
|
||||
CL_CHECK(clReleaseMemObject(dDs));
|
||||
return r;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Main
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
Options o = parseArgs(argc, argv);
|
||||
DeviceInfo* devs = NULL;
|
||||
int nDev, i, chosen = -1;
|
||||
DeviceInfo* di;
|
||||
Device dv;
|
||||
cl_int err = 0;
|
||||
size_t srcLen = 0;
|
||||
char* src;
|
||||
uint32_t nonces;
|
||||
cl_mem dOut;
|
||||
int sizes[5], nSizes = 0;
|
||||
SizeResult results[5];
|
||||
int cachePass = 1, overall, anyVec = 0;
|
||||
|
||||
printf("igneum-bench-cl pack \"%s\" (test harness: no pool, no network, no wallet)\n", IGNEUM_SEED_STRING);
|
||||
nDev = enumerateDevices(&devs);
|
||||
if (nDev == 0) { printf("FAIL: no OpenCL platform or device found (is an OpenCL driver / ICD installed?)\n"); return 2; }
|
||||
if (o.device >= 0) {
|
||||
if (o.device >= nDev) { printf("FAIL: --device %d out of range (%d devices)\n", o.device, nDev); return 2; }
|
||||
chosen = o.device;
|
||||
} else {
|
||||
for (i = 0; i < nDev; ++i) if (devs[i].type & CL_DEVICE_TYPE_GPU) { chosen = i; break; }
|
||||
if (chosen < 0) chosen = 0;
|
||||
}
|
||||
printf("OpenCL devices (%d):\n", nDev);
|
||||
for (i = 0; i < nDev; ++i) printDevice(i, &devs[i], i == chosen && !o.list);
|
||||
if (o.list) return 0;
|
||||
di = &devs[chosen];
|
||||
printf("using device [%d] %s\n", chosen, di->name);
|
||||
if (o.timeWall < 0) o.timeWall = (strcmp(di->platformName, "Apple") == 0) ? 1 : 0;
|
||||
if (o.timeWall) printf("timing: host wall time (Apple's OpenCL event timestamps are not usable; the rate is still a device rate, see README.md)\n");
|
||||
else printf("timing: device event profiling (CL_PROFILING_COMMAND_START/END), like cudaEvent elapsed time\n");
|
||||
|
||||
memset(&dv, 0, sizeof(dv));
|
||||
dv.ctx = clCreateContext(NULL, 1, &di->device, NULL, NULL, &err);
|
||||
CL_CHECK_ERR(err, "clCreateContext");
|
||||
dv.q = clCreateCommandQueue(dv.ctx, di->device, CL_QUEUE_PROFILING_ENABLE, &err);
|
||||
CL_CHECK_ERR(err, "clCreateCommandQueue");
|
||||
|
||||
src = readFile(o.kernelPath, &srcLen);
|
||||
if (!src) { printf("FAIL: cannot read kernel source %s (run from proto-opencl/ or pass --kernel)\n", o.kernelPath); return 2; }
|
||||
printf("kernel source: %s (%llu bytes)\n", o.kernelPath, (unsigned long long)srcLen);
|
||||
setupProgram(&dv, di, &o, src, srcLen);
|
||||
free(src);
|
||||
printf("build options: %s\n", dv.buildOptions);
|
||||
printf("exchange: %s\n", dv.exchangeNote);
|
||||
{
|
||||
size_t wg = 0;
|
||||
cl_ulong lmem = 0;
|
||||
clGetKernelWorkGroupInfo(dv.kHash, di->device, CL_KERNEL_WORK_GROUP_SIZE, sizeof(wg), &wg, NULL);
|
||||
clGetKernelWorkGroupInfo(dv.kHash, di->device, CL_KERNEL_LOCAL_MEM_SIZE, sizeof(lmem), &lmem, NULL);
|
||||
printf("kernel: igneum_hash max work-group %llu, local memory %llu bytes, work-group %d x 32\n",
|
||||
(unsigned long long)wg, (unsigned long long)lmem, o.groupWarps);
|
||||
}
|
||||
printf("program: %d instructions x %d iterations, loads/hash %d, op mix %s\n",
|
||||
IGNEUM_INSTR_COUNT, IGNEUM_ITERATIONS, IGNEUM_LOADS_PER_HASH, IGNEUM_OP_MIX);
|
||||
printf("seed words: %08x %08x %08x %08x %08x %08x %08x %08x\n",
|
||||
SEEDW[0], SEEDW[1], SEEDW[2], SEEDW[3], SEEDW[4], SEEDW[5], SEEDW[6], SEEDW[7]);
|
||||
printf("day \"%s\" (d0 0x%08x, d1 0x%08x), pack dataset 2^%d words = %d MiB\n",
|
||||
IGNEUM_DAY_STRING, IGNEUM_DAY0, IGNEUM_DAY1, IGNEUM_DATASET_LOG2, packMib());
|
||||
#if IGNEUM_DATASET_MODE == 1
|
||||
printf("dataset construction: memory-hard (256 MiB ChaCha cache, %d dependent cache reads per 64-byte item; proto-metal/MEMHARD.md)\n", IGNEUM_ITEM_ROUNDS);
|
||||
cachePass = setupCache(&dv, di);
|
||||
#else
|
||||
printf("dataset construction: closed-form ds_elem (the original prototype dataset, not memory-hard)\n");
|
||||
#endif
|
||||
|
||||
nonces = 1u << o.batchLog2;
|
||||
if (nonces % (32u * (uint32_t)o.groupWarps) != 0u) {
|
||||
printf("FAIL: 2^%d nonces is not a multiple of %d items per work-group\n", o.batchLog2, 32 * o.groupWarps);
|
||||
return 2;
|
||||
}
|
||||
dOut = clCreateBuffer(dv.ctx, CL_MEM_READ_WRITE, (size_t)nonces * sizeof(uint64_t), NULL, &err);
|
||||
CL_CHECK_ERR(err, "clCreateBuffer out");
|
||||
|
||||
if (o.sweep) { sizes[0] = 4; sizes[1] = 64; sizes[2] = 256; sizes[3] = 512; sizes[4] = 1024; nSizes = 5; }
|
||||
else { sizes[0] = o.datasetMib; nSizes = 1; }
|
||||
for (i = 0; i < nSizes; ++i) results[i] = runSize(&dv, di, &o, sizes[i], dOut, nonces);
|
||||
CL_CHECK(clReleaseMemObject(dOut));
|
||||
|
||||
printf("\n=== summary (%s, %s, pack %s, batch 2^%d x %d, %d unit(s)/work-group, exchange %s, %s time) ===\n",
|
||||
di->name, di->platformName, IGNEUM_SEED_STRING, o.batchLog2, o.batches, o.groupWarps, exchangeName(dv.exchange), o.timeWall ? "wall" : "device-event");
|
||||
printf("| dataset MiB | %s ms (second) | Mhash/s | GB/s useful | random loads/s (G) | loads/hash | dataset self-test | vectors |\n",
|
||||
IGNEUM_DATASET_MODE == 1 ? "build" : "fill");
|
||||
printf("|---|---|---|---|---|---|---|---|\n");
|
||||
overall = cachePass;
|
||||
for (i = 0; i < nSizes; ++i) {
|
||||
const SizeResult* r = &results[i];
|
||||
overall = overall && r->dsPass && (!r->vecChecked || r->vecPass);
|
||||
anyVec = anyVec || r->vecChecked;
|
||||
printf("| %d | %.2f | %.3f | %.2f | %.2f | %d | %s | %s |\n",
|
||||
r->mib, r->fillSecondMs, r->hashesPerSec / 1e6, r->gbps,
|
||||
r->hashesPerSec * (double)IGNEUM_LOADS_PER_HASH / 1e9, IGNEUM_LOADS_PER_HASH,
|
||||
r->dsPass ? "PASS" : "FAIL",
|
||||
r->vecChecked ? (r->vecPass ? "PASS (3 warps, standalone and in batch)" : "FAIL") : "skipped (not pack size)");
|
||||
}
|
||||
#if IGNEUM_DATASET_MODE == 1
|
||||
printf("cache: device fill %.2f ms (second), host fill %.1f ms one thread, cache check %s\n", gCacheFillSecondMs, gCacheHostMs, cachePass ? "PASS" : "FAIL");
|
||||
if (gCache) clReleaseMemObject(gCache);
|
||||
free(hCache);
|
||||
#endif
|
||||
if (gProfilingFailures) printf("NOTE: %d event profiling queries failed on this runtime (timings printed as -1.00 ms are unavailable)\n", gProfilingFailures);
|
||||
printf("exchange: %s\n", dv.exchangeNote);
|
||||
if (!anyVec) printf("NOTE: no vectors were checked. Run at %d MiB (the default) to verify against the Mac.\n", packMib());
|
||||
printf("OVERALL: %s\n", overall ? "PASS" : "FAIL");
|
||||
|
||||
releaseProgram(&dv);
|
||||
clReleaseCommandQueue(dv.q);
|
||||
clReleaseContext(dv.ctx);
|
||||
for (i = 0; i < nDev; ++i) free(devs[i].extensions);
|
||||
free(devs);
|
||||
return overall ? 0 : 1;
|
||||
}
|
||||
Loading…
Reference in a new issue