Merge class-v6-kits fb0e6f5e into master (gate: green on b89cfe01, recorded by tools/ci/pre-push.sh; landed on the build mirror)
This commit is contained in:
commit
70c9128a14
24 changed files with 1173 additions and 89 deletions
|
|
@ -408,6 +408,14 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
|
|||
- Cases:
|
||||
- ADV-06 Separate process advantage from specialisation: partial: the placed gated cores and the adversary's forms are modelled in shadow-k.md; the independent review remains
|
||||
|
||||
### kit:class-v6-fingerprints
|
||||
|
||||
- Command: `KITS_BOX_SCRIPT=kits-v6-on-box.sh tools/class-v5/kits-remote.sh --box 1 (the kit zip with its emulation check), then the kit's workers on every platform: the box's CPU emulation (--check, 96 of 96 vector lanes), the fleet's CUDA 4090 (--check and --bench at 2^24, base nonce 0), the Mac's Metal (proto-metal/packbench.swift --pack) and Apple OpenCL (proto-opencl/host.c --pack --bench-pack), PC 1's RTX 5090 and RX 7600 (tools/class-v5/pc1-v6-bench.ps1), PC 2's Arc B580 (tools/class-v5/arc-v6-bench.ps1); the fingerprint of the 2^24 outputs at base nonce 0 equal on every platform for the all pack and for its known-failed partner, the two distinct`
|
||||
- Box class: build box + pods + Mac + PC 1 + PC 2
|
||||
- Fixtures: F0, F2
|
||||
- Cases:
|
||||
- R2-F03-R02 Same job context produces identical accepted work in node, CPU reference, CUDA, Metal, OpenCL and pool.: partial: the worker half (CPU reference, CUDA, Metal, OpenCL) on the same program packs; the node's and the pool's accepted work on the same job context are the CI steward's and the pool lane's cells; PASS only when every listed platform reads one fingerprint and the node and pool halves are green
|
||||
|
||||
## Automated cases with no harness in the matrix (NOT RUN, the reason)
|
||||
|
||||
- GOV-02 Approve thresholds before results: the approval is recorded in the registry's approval field; the automated half (thresholds frozen before any run_status) is the gate rule landing by 21:00
|
||||
|
|
|
|||
|
|
@ -12105,42 +12105,51 @@
|
|||
"POW-01",
|
||||
"ROT-02"
|
||||
],
|
||||
"updated": "2026-10-08T20:10:24.556Z",
|
||||
"updated": "2026-10-08T21:18:18.084Z",
|
||||
"evidence_record": {
|
||||
"reason": "R2-F03 (P0, the external review): the regression's harness is the owner lane's (CI steward, hash lane (ProgramClass::V6 on freeze), pool lane); not yet named in the map",
|
||||
"at": "2026-10-08T20:10:24.556Z",
|
||||
"method": "static",
|
||||
"requirement_id": "R2-F03-R02",
|
||||
"decision": "NOT RUN",
|
||||
"reviewer": "",
|
||||
"claim_impact": "",
|
||||
"method": "GPU",
|
||||
"cell": "kit:class-v6-fingerprints",
|
||||
"manifest_sha": "ef0f2ed8",
|
||||
"run_id": "kit-class-v6-20261008-01",
|
||||
"evidence": "docs/design/class-v5-stored-state.md; build-1:/srv/artefacts/packs/packs-class-v6-20261008T202808Z.zip; build-1:/srv/builds/_log/v5-class/kits-20261008T202808Z/emu-check.log",
|
||||
"in_progress": true,
|
||||
"coverage": "partial: the worker half (CPU reference, CUDA, Metal, OpenCL) on the same program packs; the node's and the pool's accepted work on the same job context are the CI steward's and the pool lane's cells; PASS only when every listed platform reads one fingerprint and the node and pool halves are green",
|
||||
"release_identity": {
|
||||
"commit": "",
|
||||
"commit": "ef0f2ed8",
|
||||
"lockfile": "",
|
||||
"binary": "",
|
||||
"network_object": "",
|
||||
"activation": "",
|
||||
"profile_hashes": ""
|
||||
}
|
||||
},
|
||||
"claim_impact": "",
|
||||
"reviewer": "",
|
||||
"at": "2026-10-08T21:18:18.084Z"
|
||||
},
|
||||
"evidence_records": {
|
||||
"record": {
|
||||
"reason": "R2-F03 (P0, the external review): the regression's harness is the owner lane's (CI steward, hash lane (ProgramClass::V6 on freeze), pool lane); not yet named in the map",
|
||||
"at": "2026-10-08T20:10:24.556Z",
|
||||
"method": "static",
|
||||
"kit:class-v6-fingerprints": {
|
||||
"requirement_id": "R2-F03-R02",
|
||||
"decision": "NOT RUN",
|
||||
"reviewer": "",
|
||||
"claim_impact": "",
|
||||
"method": "GPU",
|
||||
"cell": "kit:class-v6-fingerprints",
|
||||
"manifest_sha": "ef0f2ed8",
|
||||
"run_id": "kit-class-v6-20261008-01",
|
||||
"evidence": "docs/design/class-v5-stored-state.md; build-1:/srv/artefacts/packs/packs-class-v6-20261008T202808Z.zip; build-1:/srv/builds/_log/v5-class/kits-20261008T202808Z/emu-check.log",
|
||||
"in_progress": true,
|
||||
"coverage": "partial: the worker half (CPU reference, CUDA, Metal, OpenCL) on the same program packs; the node's and the pool's accepted work on the same job context are the CI steward's and the pool lane's cells; PASS only when every listed platform reads one fingerprint and the node and pool halves are green",
|
||||
"release_identity": {
|
||||
"commit": "",
|
||||
"commit": "ef0f2ed8",
|
||||
"lockfile": "",
|
||||
"binary": "",
|
||||
"network_object": "",
|
||||
"activation": "",
|
||||
"profile_hashes": ""
|
||||
},
|
||||
"in_progress": false
|
||||
"claim_impact": "",
|
||||
"reviewer": "",
|
||||
"at": "2026-10-08T21:18:18.084Z"
|
||||
}
|
||||
},
|
||||
"approvals": {
|
||||
|
|
@ -12148,7 +12157,10 @@
|
|||
"implementation_complete": null,
|
||||
"evidence_reproduced": null,
|
||||
"claim_authorised": null
|
||||
}
|
||||
},
|
||||
"run_id": "kit-class-v6-20261008-01",
|
||||
"evidence_path": "docs/design/class-v5-stored-state.md; build-1:/srv/artefacts/packs/packs-class-v6-20261008T202808Z.zip; build-1:/srv/builds/_log/v5-class/kits-20261008T202808Z/emu-check.log",
|
||||
"in_progress_since": "2026-10-08T21:18:18.084Z"
|
||||
},
|
||||
{
|
||||
"id": "R2-F03-R03",
|
||||
|
|
|
|||
|
|
@ -489,7 +489,11 @@ pub fn is_class_v4_shape(class: &LoadClass) -> bool {
|
|||
// class v5 (docs/design/class-v5-stored-state.md) is judged under the same rules: its state flag is set aside;
|
||||
// class v6 lane 1's index fold and re-weight table are set aside too (the address path and the op table are
|
||||
// not the shape)
|
||||
&& LoadClass { era: None, shadow: None, state: false, fold: false, rw: 0, ..*class } == LoadClass { shadow: None, ..V4_CLASS }
|
||||
// the window and layer 8 off are set aside too (lane D's finding of 8 October 2026, 19:1x UK: a +nowin or +reg64c
|
||||
// program was judged by the class v2 parts alone on the chain's path; the harness's own predicate hid it)
|
||||
// and the era's drawn width (lane D's second finding, 20:0x UK: an era whose width set has more than one width
|
||||
// writes the drawn width into `mix`, and the rule must judge every width of the family, not the pinned one alone)
|
||||
&& LoadClass { era: None, shadow: None, state: false, fold: false, rw: 0, nowin: false, reg64: false, reg64_chain: false, mix: V4_CLASS.mix, ..*class } == LoadClass { shadow: None, ..V4_CLASS }
|
||||
}
|
||||
|
||||
/// One pass of the dataflow freshness over the base program then the shadow block (the order of one iteration),
|
||||
|
|
@ -849,6 +853,12 @@ pub fn check_dynamic(p: &Program) -> Result<AcceptReport, Reject> {
|
|||
check_distinct_indices_v4(p)?;
|
||||
}
|
||||
}
|
||||
// the reg64 window's liveness rule (the window's acceptance tool, wired 8 October 2026, 19:5x UK): keyed on the
|
||||
// window flag so no other class's verdict moves; the arithmetic-only window is refused at its first dead register,
|
||||
// the full chain passes (129 one-warp interpretations on the closed form, under a second)
|
||||
if p.class.reg64 {
|
||||
check_window_liveness(p)?;
|
||||
}
|
||||
let half = (ACCEPT_HASHES / 2) as u32;
|
||||
let mut bias_max = 0u32;
|
||||
for (bit, &ones) in acc.bit_ones.iter().enumerate() {
|
||||
|
|
|
|||
|
|
@ -91,10 +91,21 @@ pub fn hex(bytes: &[u8]) -> String {
|
|||
|
||||
/// Bytes from hex (either case). `None` on odd length or a bad digit.
|
||||
pub fn unhex(s: &str) -> Option<Vec<u8>> {
|
||||
if s.len() % 2 != 0 {
|
||||
// on bytes, never on char boundaries (the fuzz of 8 October 2026 found a multi-byte character inside the text
|
||||
// panicked the slice; a non-ASCII byte is a bad digit)
|
||||
let b = s.as_bytes();
|
||||
if b.len() % 2 != 0 {
|
||||
return None;
|
||||
}
|
||||
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()).collect()
|
||||
let digit = |c: u8| -> Option<u8> {
|
||||
match c {
|
||||
b'0'..=b'9' => Some(c - b'0'),
|
||||
b'a'..=b'f' => Some(c - b'a' + 10),
|
||||
b'A'..=b'F' => Some(c - b'A' + 10),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
b.chunks(2).map(|c| Some(digit(c[0])? << 4 | digit(c[1])?)).collect()
|
||||
}
|
||||
|
||||
impl Epoch {
|
||||
|
|
|
|||
|
|
@ -255,7 +255,11 @@ fn program_class_header_lines(p: &Program) -> String {
|
|||
return String::new();
|
||||
}
|
||||
let mut s = String::new();
|
||||
if p.program_class() == ProgramClass::V5 {
|
||||
if p.program_class() == ProgramClass::V6 {
|
||||
s.push_str("// Program class v6 (the Igneum 2.0 D1 object, docs/design/class-v6-rotating-family.md): generator version 6, class v5's\n");
|
||||
s.push_str("// stored-state dataset with the index fold, the re-weight table and the 64-register window (the v6 flags in the class\n");
|
||||
s.push_str("// string); a worker that runs another class refuses this pack, and a job line names the class it wants (class=v6 era=<hex>).\n");
|
||||
} else if p.program_class() == ProgramClass::V5 {
|
||||
s.push_str("// Program class v5 (proof of stored state and of following, docs/design/class-v5-stored-state.md): generator version 5,\n");
|
||||
s.push_str("// class v4 over a dataset whose every item is keyed by the window's execution state (IGNEUM_STATE_* below, leaves.bin);\n");
|
||||
s.push_str("// a worker that runs another class refuses this pack, and a job line names the class it wants (class=v5 era=<hex>).\n");
|
||||
|
|
@ -352,7 +356,7 @@ fn class_header_lines(p: &Program) -> String {
|
|||
s.push_str(&format!("#define IGNEUM_LOAD_WIDTH_COUNTS {{ {}, {}, {} }} // loads of 4, 16, 64 bytes per program
|
||||
", c[0], c[1], c[2]));
|
||||
s.push_str(&format!("#define IGNEUM_BYTES_PER_HASH {}
|
||||
", p.bytes_per_hash()));
|
||||
", p.bytes_per_hash_executed()));
|
||||
s.push_str(&format!("#define IGNEUM_FOLD_ROT {FOLD_ROT}
|
||||
"));
|
||||
s.push_str(&format!("#define IGNEUM_FOLD_MUL {}
|
||||
|
|
@ -503,8 +507,16 @@ fn shadow_block(p: &Program, dialect: CoreDialect) -> String {
|
|||
));
|
||||
let ty = if dialect == CoreDialect::Cuda { "uint32_t" } else { "uint" };
|
||||
s.push_str(&format!(" for ({ty} sh = 0u; sh < {reps}u; ++sh) {{\n"));
|
||||
let prefix = dialect == CoreDialect::Cuda && p.address_mix() && reg64_prefix();
|
||||
for (k, ins) in p.shadow.iter().enumerate() {
|
||||
if prefix {
|
||||
// F05 prefix form: the shadow's writes move S too (the fold reads the live registers)
|
||||
s.push_str(&format!(" t_ = {};\n", reg64_term(ins.dst as usize)));
|
||||
}
|
||||
s.push_str(&format!(" {} // s{k} {}\n", shadow_instr_line(dialect, ins), ins.op.name()));
|
||||
if prefix {
|
||||
s.push_str(&format!(" S ^= t_ ^ {};\n", reg64_term(ins.dst as usize)));
|
||||
}
|
||||
}
|
||||
s.push_str(" }\n");
|
||||
s
|
||||
|
|
@ -1161,13 +1173,60 @@ fn reg_decl(p: &Program, ty: &str) -> String {
|
|||
return format!(" {ty} r0, r1, r2, r3, r4, r5, r6, r7;\n");
|
||||
}
|
||||
let names: Vec<String> = (0..p.registers()).map(|k| format!("r{k}")).collect();
|
||||
let m = if p.address_mix() { format!("\n {ty} m; // reg64 full chain: the address mix of all 64 registers before every load") } else { String::new() };
|
||||
let m = if p.address_mix() {
|
||||
if ty == "uint32_t" && reg64_prefix() {
|
||||
format!("\n {ty} m, S, p_, t_; // reg64 full chain in the F05 prefix form: S the running xor of the rotated registers, p_ the prefix, t_ the old term")
|
||||
} else {
|
||||
format!("\n {ty} m; // reg64 full chain: the address mix of all 64 registers before every load")
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
format!(" {ty} {}; // reg64: 64 live registers per lane (two 32-register windows){m}\n", names.join(", "))
|
||||
}
|
||||
|
||||
/// reg64 full chain: the mix statement before a load, `m = r0; m = rotl_imm(m, 1u) ^ r1; ... ^ r63;` over the 63
|
||||
/// registers other than the load's source `src` (the verifier's `addr_src`, the same chain), one line; the same
|
||||
/// text in the three dialects (`rotl_imm` is defined in each).
|
||||
/// Review B's F05 (8 October 2026): the CUDA texts carry the reg64 address mix in its closed prefix form when this
|
||||
/// switch is on (`igneum-pow export --reg64-prefix`): `S` is the xor of `a[k] = rotl(r[k], (63 - k) mod 32)` over
|
||||
/// the 64 registers, kept per lane and updated after every write; a load's source is
|
||||
/// `r[s] ^ ror(P_s, 1) ^ (S ^ P_s ^ a[s])` with `P_s` the xor of the first `s` terms. The same hash, the same vectors
|
||||
/// (`verify::reg64_address_source`); a measurement text for the fleet, never the definition. OpenCL and Metal keep
|
||||
/// the fold.
|
||||
static REG64_PREFIX: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
pub fn set_reg64_prefix(on: bool) {
|
||||
REG64_PREFIX.store(on, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn reg64_prefix() -> bool {
|
||||
REG64_PREFIX.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// `rotl(rK, (63 - k) mod 32)` as text; a rotation of 0 is the register itself (`rotl_imm` takes 1..31).
|
||||
fn reg64_term(k: usize) -> String {
|
||||
let n = (63 - k) % 32;
|
||||
if n == 0 { format!("r{k}") } else { format!("rotl_imm(r{k}, {n}u)") }
|
||||
}
|
||||
|
||||
/// The prefix-form mix statement before a load of source `src` (the F05 text): `m = ror(P, 1) ^ S ^ P ^ a[src]`.
|
||||
fn reg64_prefix_mix_line(src: u8) -> String {
|
||||
let s = src as usize;
|
||||
let prefix: Vec<String> = (0..s).map(reg64_term).collect();
|
||||
let p = if prefix.is_empty() { "0u".to_string() } else { prefix.join(" ^ ") };
|
||||
format!("p_ = {p}; m = rotr_var(p_, 1u) ^ S ^ p_ ^ {};", reg64_term(s))
|
||||
}
|
||||
|
||||
/// The prefix form's running total after the register init: `S = a[0] ^ ... ^ a[63]`.
|
||||
fn reg64_prefix_init(p: &Program) -> String {
|
||||
if !p.class.reg64 || !p.address_mix() || !reg64_prefix() {
|
||||
return String::new();
|
||||
}
|
||||
let terms: Vec<String> = (0..p.registers()).map(reg64_term).collect();
|
||||
format!(" // F05 prefix form: the running xor of every rotated register\n S = {};\n", terms.join(" ^ "))
|
||||
}
|
||||
|
||||
fn reg64_mix_line(p: &Program, src: u8) -> String {
|
||||
let mut s = String::new();
|
||||
for k in 0..p.registers() {
|
||||
|
|
@ -1223,8 +1282,13 @@ fn cuda_instr_lines(p: &Program, geom: DatasetGeom) -> String {
|
|||
let d = format!("r{}", ins.dst);
|
||||
let a = format!("r{}", ins.src);
|
||||
let b = format!("r{}", ins.src2);
|
||||
let prefix = address_mix && reg64_prefix();
|
||||
if address_mix && ins.op == Op::Load {
|
||||
s.push_str(&format!(" {}\n", reg64_mix_line(p, ins.src)));
|
||||
s.push_str(&format!(" {}\n", if prefix { reg64_prefix_mix_line(ins.src) } else { reg64_mix_line(p, ins.src) }));
|
||||
}
|
||||
if prefix {
|
||||
// the old term of the destination, so S can drop it after the write
|
||||
s.push_str(&format!(" t_ = {};\n", reg64_term(ins.dst as usize)));
|
||||
}
|
||||
let line = match ins.op {
|
||||
// Metal select(A, B, c) returns c ? B : A, so the true branch is imm2 here as well.
|
||||
|
|
@ -1252,6 +1316,9 @@ fn cuda_instr_lines(p: &Program, geom: DatasetGeom) -> String {
|
|||
Op::Hot => hot_stmt(CoreDialect::Cuda, &d, &a),
|
||||
};
|
||||
s.push_str(&format!(" {line} // {k} {}\n", ins.op.name()));
|
||||
if prefix {
|
||||
s.push_str(&format!(" S ^= t_ ^ {};\n", reg64_term(ins.dst as usize)));
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
|
@ -1371,6 +1438,7 @@ pub fn cuda_kernel_geom(p: &Program, memhard: Option<&MixParams>, geom: DatasetG
|
|||
s.push_str(&init_line(p, "uint32_t", i));
|
||||
}
|
||||
s.push_str(®64_init(p));
|
||||
s.push_str(®64_prefix_init(p));
|
||||
s.push_str(&format!("\n for (uint32_t it = 0u; it < {ITERATIONS}u; ++it) {{\n uint32_t sel = r0;\n"));
|
||||
s.push_str(&cuda_instr_lines(p, geom));
|
||||
s.push_str(&shadow_block(p, CoreDialect::Cuda));
|
||||
|
|
@ -1536,6 +1604,7 @@ pub fn cuda_kernel_bound_geom(p: &Program, memhard: Option<&MixParams>, geom: Da
|
|||
));
|
||||
}
|
||||
s.push_str(®64_init(p));
|
||||
s.push_str(®64_prefix_init(p));
|
||||
s.push_str(&format!("\n for (uint32_t it = 0u; it < {ITERATIONS}u; ++it) {{\n uint32_t sel = r0;\n"));
|
||||
s.push_str(&cuda_instr_lines(p, geom));
|
||||
s.push_str(&shadow_block(p, CoreDialect::Cuda));
|
||||
|
|
@ -1947,7 +2016,10 @@ pub fn program_header(p: &Program, day: &str, ds: &DatasetSource) -> String {
|
|||
s.push_str("#define IGNEUM_LANES 32\n");
|
||||
s.push_str(&format!("#define IGNEUM_ITERATIONS {ITERATIONS}\n"));
|
||||
s.push_str(&format!("#define IGNEUM_INSTR_COUNT {INSTR_COUNT}\n"));
|
||||
s.push_str(&format!("#define IGNEUM_LOADS_PER_HASH {}\n", p.loads_per_hash()));
|
||||
if p.class.reg64 {
|
||||
s.push_str("// the executed counts per nonce (review B's V6-02): twice the drawn program's under the 64-register window\n");
|
||||
}
|
||||
s.push_str(&format!("#define IGNEUM_LOADS_PER_HASH {}\n", p.loads_per_hash_executed()));
|
||||
s.push_str(&format!("#define IGNEUM_WIDE_LOADS_PER_HASH {}\n", p.wide_loads_per_hash()));
|
||||
s.push_str(&format!("#define IGNEUM_OP_MIX {}\n", jstr(&p.op_mix())));
|
||||
s.push_str(&program_class_header_lines(p));
|
||||
|
|
@ -2189,7 +2261,7 @@ pub fn program_json(p: &Program, day: &str, ds: &DatasetSource) -> String {
|
|||
s.push_str(" \"registers\": 8,\n");
|
||||
s.push_str(&format!(" \"iterations\": {ITERATIONS},\n"));
|
||||
s.push_str(&format!(" \"instruction_count\": {INSTR_COUNT},\n"));
|
||||
s.push_str(&format!(" \"loads_per_hash\": {},\n", p.loads_per_hash()));
|
||||
s.push_str(&format!(" \"loads_per_hash\": {},\n", p.loads_per_hash_executed()));
|
||||
if p.program_class() != ProgramClass::V2 {
|
||||
s.push_str(&format!(" \"program_class\": {},\n", jstr(p.program_class().name())));
|
||||
if p.program_class() == ProgramClass::V4 {
|
||||
|
|
@ -2239,7 +2311,7 @@ pub fn program_json(p: &Program, day: &str, ds: &DatasetSource) -> String {
|
|||
s.push_str(&format!(" \"load_slots\": {},\n", p.class.load_slots));
|
||||
s.push_str(&format!(" \"load_mix_percent_4_16_64\": [{}, {}, {}],\n", p.class.mix[0], p.class.mix[1], p.class.mix[2]));
|
||||
s.push_str(&format!(" \"load_width_counts_4_16_64\": [{}, {}, {}],\n", c[0], c[1], c[2]));
|
||||
s.push_str(&format!(" \"bytes_per_hash\": {},\n", p.bytes_per_hash()));
|
||||
s.push_str(&format!(" \"bytes_per_hash\": {},\n", p.bytes_per_hash_executed()));
|
||||
if p.has_scratch() {
|
||||
s.push_str(&format!(" \"scratch_ops_per_hash\": {},\n", p.scratch_ops_per_hash()));
|
||||
s.push_str(&format!(" \"scratch_kib_per_warp\": {},\n", p.class.scratch_kb));
|
||||
|
|
@ -2610,19 +2682,42 @@ pub fn export_pack(epoch: &Epoch, day: &str, source: &str) -> Pack {
|
|||
v.hot_fnv = h.fnv1a64();
|
||||
}
|
||||
let is_mh = memhard.is_some();
|
||||
let mut files = vec![
|
||||
("program.json".to_string(), program_json(p, day, ds)),
|
||||
("vectors.json".to_string(), vectors_json_geom(p, day, geom, &bases, &outs, &v, source, is_mh)),
|
||||
let texts: Vec<(String, String)> = vec![
|
||||
("kernel.cu".to_string(), cuda_kernel_geom(p, memhard, geom)),
|
||||
("kernel.cl".to_string(), opencl_kernel_geom(p, memhard, geom)),
|
||||
("program.h".to_string(), program_header(p, day, ds)),
|
||||
("vectors.h".to_string(), vectors_header(p, &bases, &outs, &v, mask, source, is_mh)),
|
||||
("program.metal".to_string(), metal_program_geom(p, geom, LoadSource::Stored)),
|
||||
// Header-bound kernels (3 October 2026, bind.rs): new files, the seven above are unchanged.
|
||||
("program_bound.metal".to_string(), metal_program_bound_geom(p, geom)),
|
||||
("kernel_bound.cu".to_string(), cuda_kernel_bound_geom(p, memhard, geom)),
|
||||
("kernel_bound.cl".to_string(), opencl_kernel_bound_geom(p, memhard, geom)),
|
||||
];
|
||||
// A06 (the external review of 8 October 2026): the pack authenticates its kernel texts by hash, not by metadata:
|
||||
// identity.json carries the program id, the dataset and era identities and the BLAKE2b-256 of every kernel file.
|
||||
// A file of its own, so every pinned pack's twelve files stay byte for byte (the readers ignore it).
|
||||
let hashes: Vec<String> = texts.iter().map(|(n, t)| format!(" {}: \"{}\"", jstr(n), hex_bytes(&crate::blake2b::blake2b_256(&[t.as_bytes()])))).collect();
|
||||
let identity = format!(
|
||||
"{{\n \"program_id\": \"{:#018x}\",\n \"load_class\": {},\n \"generator\": {},\n \"day\": {},\n \"dataset_bytes\": {},\n \"kernel_blake2b256\": {{\n{}\n }},\n \"rule\": \"the external review's A06 (8 October 2026): a pack's program, dataset and work identities are distinct, and its kernel texts are authenticated by the hash of their bytes, never by the metadata beside them\"\n}}\n",
|
||||
p.program_id(),
|
||||
jstr(&p.class.name()),
|
||||
p.generator,
|
||||
jstr(day),
|
||||
(ds.geom.words as u128) * 4,
|
||||
hashes.join(",\n")
|
||||
);
|
||||
// the pack's file order as the pinned packs carry it, identity.json second
|
||||
let mut texts = texts.into_iter();
|
||||
let kernel_cu = texts.next().unwrap();
|
||||
let kernel_cl = texts.next().unwrap();
|
||||
let mut files = vec![
|
||||
("program.json".to_string(), program_json(p, day, ds)),
|
||||
("identity.json".to_string(), identity),
|
||||
("vectors.json".to_string(), vectors_json_geom(p, day, geom, &bases, &outs, &v, source, is_mh)),
|
||||
kernel_cu,
|
||||
kernel_cl,
|
||||
("program.h".to_string(), program_header(p, day, ds)),
|
||||
("vectors.h".to_string(), vectors_header(p, &bases, &outs, &v, mask, source, is_mh)),
|
||||
];
|
||||
files.extend(texts);
|
||||
if let Some(mp) = memhard {
|
||||
files.push(("memhard.h".to_string(), cuda_memhard_header(p, mp)));
|
||||
files.push(("memhard.metal".to_string(), metal_memhard_for(p, mp)));
|
||||
|
|
|
|||
|
|
@ -255,6 +255,11 @@ pub struct LoadClass {
|
|||
/// optimiser split [`NONLOAD_WEIGHTS_RW`] (sum 83, `or` never drawn); 2 the census lane's neighbouring table
|
||||
/// [`NONLOAD_WEIGHTS_RW2`] (sum 75). The draw rolls against the table's own sum ([`LoadClass::nonload_weights`]).
|
||||
pub rw: u8,
|
||||
/// D2 experiment "layer 8 off" (the coordinator's order, 8 October 2026, 18:12 UK; a research class behind `+nowin`):
|
||||
/// the era layout's window-layer draw is removed, every load site reads the whole dataset (`win = 0`, `off = 0`).
|
||||
/// The program stream still consumes the two window draws per instruction, so the base program's instructions are
|
||||
/// the class's without the flag; only the windows move. `false` for every other class.
|
||||
pub nowin: bool,
|
||||
/// The 64-register window (the hash lane's reg64 measurement, 8 October 2026, a research class behind `+reg64`
|
||||
/// and `--reg64`): each lane holds 64 live 32-bit registers. r0..r7 are seeded as today, r8..r63 derived from them
|
||||
/// (`r[k] = r[k & 7] * 0x9E3779B9 + k`); the 64 drawn instructions run twice per iteration in an interleaved
|
||||
|
|
@ -455,13 +460,13 @@ impl LoadClass {
|
|||
impl LoadClass {
|
||||
/// Generator version 2 as adopted on 4 October 2026: 16 loads of one word. The lottery hash.
|
||||
pub const V2: LoadClass =
|
||||
LoadClass { mix: [100, 0, 0], load_slots: LOAD_SLOTS as u8, scratch: None, scratch_kb: 0, mixer_mult: 1, growth: false, era: None, hot: None, derive_len: 0, shadow: None, state: false, wide8: false, fold: false, rw: 0, reg64: false, reg64_chain: false };
|
||||
LoadClass { mix: [100, 0, 0], load_slots: LOAD_SLOTS as u8, scratch: None, scratch_kb: 0, mixer_mult: 1, growth: false, era: None, hot: None, derive_len: 0, shadow: None, state: false, wide8: false, fold: false, rw: 0, nowin: false, reg64: false, reg64_chain: false };
|
||||
|
||||
/// The construction decided for program class v3 on 5 October 2026 (Counter ASIC 2.0, `docs/plans/mixer-x4.md`):
|
||||
/// version 2 loads (16 slots of one word, no scratch, no width roll, so the program stream is version 2's), the
|
||||
/// mixer applied 4 times per round, and the cache growth rule. Name "mx4".
|
||||
pub const MX4: LoadClass =
|
||||
LoadClass { mix: [100, 0, 0], load_slots: LOAD_SLOTS as u8, scratch: None, scratch_kb: 0, mixer_mult: 4, growth: true, era: None, hot: None, derive_len: 0, shadow: None, state: false, wide8: false, fold: false, rw: 0, reg64: false, reg64_chain: false };
|
||||
LoadClass { mix: [100, 0, 0], load_slots: LOAD_SLOTS as u8, scratch: None, scratch_kb: 0, mixer_mult: 4, growth: true, era: None, hot: None, derive_len: 0, shadow: None, state: false, wide8: false, fold: false, rw: 0, nowin: false, reg64: false, reg64_chain: false };
|
||||
|
||||
/// The era class over `base` (`docs/plans/era-layout.md`): the parameters drawn by [`era_draw`]; when `allowed`
|
||||
/// has more than one width the drawn width becomes the class mix (every load that width), otherwise the base
|
||||
|
|
@ -634,6 +639,21 @@ impl LoadClass {
|
|||
LoadClass { rw, ..self }
|
||||
}
|
||||
|
||||
/// D2 "layer 8 off": the class with the window-layer draw removed (every load site reads the whole dataset).
|
||||
pub fn with_nowin(self) -> LoadClass {
|
||||
LoadClass { nowin: true, ..self }
|
||||
}
|
||||
|
||||
/// The class v6 object's draw rules (the external review of 8 October 2026, A02 and A08, fixed by construction for
|
||||
/// every class carrying a v6 flag: the index fold, a re-weight table, the register window or layer 8 off): five
|
||||
/// of the non-load slots are shuffles whose masks are the five lane dimensions 1, 2, 4, 8, 16 in a drawn order, so
|
||||
/// every accepted program mixes all 32 lanes by construction; and a `mad` never names its destination as its
|
||||
/// second source (`d = a * d + d` is `d * (a + 1)`, not a bijection in `d` when `a` is odd). Every other class
|
||||
/// draws as before.
|
||||
pub fn is_v6(&self) -> bool {
|
||||
self.fold || self.rw != 0 || self.reg64 || self.nowin
|
||||
}
|
||||
|
||||
/// The non-load op table this class draws from, in draw order, and its sum (the roll's range). The plain table
|
||||
/// for every class without the re-weight flag, so their streams are byte for byte what they were.
|
||||
pub fn nonload_weights(&self) -> (&'static [(Op, u64); 10], u64) {
|
||||
|
|
@ -677,6 +697,10 @@ impl LoadClass {
|
|||
pub fn parse(s: &str) -> Option<LoadClass> {
|
||||
// class v6 lane 1: "<class>+fold" (the index fold) and "<class>+rw" / "<class>+rw2" (the re-weight table) over
|
||||
// any class, in any order, outermost of all
|
||||
// D2 "layer 8 off": "<class>+nowin" over any class, in any order with the other suffixes
|
||||
if let Some(base) = s.strip_suffix("+nowin") {
|
||||
return Some(LoadClass::parse(base)?.with_nowin());
|
||||
}
|
||||
if let Some(base) = s.strip_suffix("+fold") {
|
||||
return Some(LoadClass::parse(base)?.with_fold());
|
||||
}
|
||||
|
|
@ -829,6 +853,10 @@ impl LoadClass {
|
|||
if self.fold {
|
||||
return format!("{}+fold", LoadClass { fold: false, ..*self }.name());
|
||||
}
|
||||
if self.nowin {
|
||||
// D2 "layer 8 off": "+nowin" sits inside "+fold" and "+rw" and outside "+reg64" and "+state"
|
||||
return format!("{}+nowin", LoadClass { nowin: false, ..*self }.name());
|
||||
}
|
||||
if self.reg64 {
|
||||
// "<class>+reg64" / "+reg64c": the 64-register window is a suffix on any class, outermost
|
||||
let base = LoadClass { reg64: false, reg64_chain: false, ..*self }.name();
|
||||
|
|
@ -935,6 +963,10 @@ pub const GENERATOR_VERSION_V4: u32 = 4;
|
|||
/// Generator version of a class v5 program (proof of stored state and of following, 7 October 2026, PROPOSED:
|
||||
/// `program_id(5, seed, attempt)`; `docs/design/class-v5-stored-state.md`).
|
||||
pub const GENERATOR_VERSION_V5: u32 = 5;
|
||||
/// Class v6 (the Igneum 2.0 D1 object, 8 October 2026; review B's F03): the class v5 draw with the v6 rules (the index fold, the
|
||||
/// re-weight table, the 64-register window with the full chain, the five shuffle dimensions and the mad operand rule by
|
||||
/// construction, layer 8 off when the flag says so), generator 6.
|
||||
pub const GENERATOR_VERSION_V6: u32 = 6;
|
||||
|
||||
/// The program class of an epoch (Counter ASIC 2.0, 5 October 2026, `docs/plans/counter-asic-2-rollout.md`): one
|
||||
/// height switch in the node, `program_class_v3_activation_daa`, rounded up to an epoch boundary, decides which
|
||||
|
|
@ -951,6 +983,8 @@ pub enum ProgramClass {
|
|||
/// Class v5 (`docs/design/class-v5-stored-state.md`, behind `program_class_v5_activation_daa`): class v4's program
|
||||
/// over a dataset whose every item is keyed by the window's execution state ([`V5_CLASS`]), generator 5.
|
||||
V5,
|
||||
/// Class v6 (the Igneum 2.0 D1 object; review B's F03, 8 October 2026): [`V6_CLASS`], generator 6.
|
||||
V6,
|
||||
}
|
||||
|
||||
/// The load class of program class v3, decided 5 October 2026 (Counter ASIC 2.0, `docs/plans/counter-asic-2-status.md`
|
||||
|
|
@ -973,6 +1007,9 @@ pub const V4_CLASS: LoadClass = LoadClass { shadow: Some(ShadowClass { instrs: V
|
|||
/// program draw, the shadow block, the era draw and the ladder rung are class v4's, draw for draw; only the item
|
||||
/// derivation and the program id change.
|
||||
pub const V5_CLASS: LoadClass = LoadClass { state: true, ..V4_CLASS };
|
||||
/// The class v6 object ("mx8+sh256x27+state+reg64c+fold+rw"): class v5 with the index fold, the k lane's re-weight table
|
||||
/// and the 64-register window with the full chain; layer 8 off is the fifth flag, drawn by the chain's family flags.
|
||||
pub const V6_CLASS: LoadClass = LoadClass { fold: true, rw: 1, reg64: true, reg64_chain: true, ..V5_CLASS };
|
||||
|
||||
/// The shadow block size of class v4 at every rung of the latency ladder (`docs/design/latency-ladder.md`): 256
|
||||
/// instructions. The ladder moves the pass count alone.
|
||||
|
|
@ -1037,6 +1074,8 @@ pub fn era_generator_of(base: &LoadClass) -> u32 {
|
|||
match ProgramClass::of_load_class(base) {
|
||||
Some(ProgramClass::V4) => GENERATOR_VERSION_V4,
|
||||
Some(ProgramClass::V5) => GENERATOR_VERSION_V5,
|
||||
Some(ProgramClass::V6) => GENERATOR_VERSION_V6,
|
||||
_ if base.is_v6() => GENERATOR_VERSION_V6,
|
||||
_ if base.state => GENERATOR_VERSION_V5,
|
||||
_ => GENERATOR_VERSION_V3,
|
||||
}
|
||||
|
|
@ -1067,6 +1106,7 @@ impl ProgramClass {
|
|||
ProgramClass::V3 => V3_CLASS,
|
||||
ProgramClass::V4 => V4_CLASS,
|
||||
ProgramClass::V5 => V5_CLASS,
|
||||
ProgramClass::V6 => V6_CLASS,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1077,12 +1117,13 @@ impl ProgramClass {
|
|||
ProgramClass::V3 => GENERATOR_VERSION_V3,
|
||||
ProgramClass::V4 => GENERATOR_VERSION_V4,
|
||||
ProgramClass::V5 => GENERATOR_VERSION_V5,
|
||||
ProgramClass::V6 => GENERATOR_VERSION_V6,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the class's dataset is keyed by the window's execution state (class v5).
|
||||
pub fn has_state(&self) -> bool {
|
||||
*self == ProgramClass::V5
|
||||
matches!(self, ProgramClass::V5 | ProgramClass::V6)
|
||||
}
|
||||
|
||||
/// The class of a generator version: 2, 3 and 4 are the three classes, anything else is no class this crate runs.
|
||||
|
|
@ -1092,6 +1133,7 @@ impl ProgramClass {
|
|||
GENERATOR_VERSION_V3 => Some(ProgramClass::V3),
|
||||
GENERATOR_VERSION_V4 => Some(ProgramClass::V4),
|
||||
GENERATOR_VERSION_V5 => Some(ProgramClass::V5),
|
||||
GENERATOR_VERSION_V6 => Some(ProgramClass::V6),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -1103,6 +1145,7 @@ impl ProgramClass {
|
|||
ProgramClass::V3 => "v3",
|
||||
ProgramClass::V4 => "v4",
|
||||
ProgramClass::V5 => "v5",
|
||||
ProgramClass::V6 => "v6",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1112,6 +1155,7 @@ impl ProgramClass {
|
|||
"v3" => Some(ProgramClass::V3),
|
||||
"v4" => Some(ProgramClass::V4),
|
||||
"v5" => Some(ProgramClass::V5),
|
||||
"v6" => Some(ProgramClass::V6),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -1125,6 +1169,7 @@ impl ProgramClass {
|
|||
/// is v3, [`V4_CLASS`] is v4; a measurement class (a width, a derivation length, another shadow size) is none.
|
||||
pub fn of_load_class(class: &LoadClass) -> Option<ProgramClass> {
|
||||
let base = LoadClass { era: None, reg64: false, reg64_chain: false, ..*class };
|
||||
let v6base = LoadClass { era: None, nowin: false, ..*class };
|
||||
if base == LoadClass::V2 {
|
||||
Some(ProgramClass::V2)
|
||||
} else if base == V3_CLASS {
|
||||
|
|
@ -1133,6 +1178,8 @@ impl ProgramClass {
|
|||
Some(ProgramClass::V4)
|
||||
} else if base == V5_CLASS {
|
||||
Some(ProgramClass::V5)
|
||||
} else if v6base == V6_CLASS {
|
||||
Some(ProgramClass::V6)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
@ -1194,9 +1241,25 @@ impl Program {
|
|||
}
|
||||
out
|
||||
}
|
||||
/// The drawn program's memory operations per nonce (16 load slots times the eight iterations on every class):
|
||||
/// the count the acceptance rule and the draw's bookkeeping read (the acceptance judges the drawn program's
|
||||
/// sites). The hash EXECUTES [`Program::loads_per_hash_executed`] of them, twice this under the 64-register
|
||||
/// window; the served figures and the pack's headers name the executed count (review B's V6-02).
|
||||
pub fn loads_per_hash(&self) -> usize {
|
||||
self.instrs.iter().filter(|i| i.op.is_load()).count() * ITERATIONS
|
||||
}
|
||||
|
||||
/// Memory operations the hash EXECUTES per nonce (review B's V6-02, 8 October 2026): the scheduled statements
|
||||
/// (the drawn program's loads, twice under the 64-register window) times the eight iterations; asserted to
|
||||
/// agree with the drawn count except by the window's factor of two. The emitters alone read it (program.h,
|
||||
/// program.json): the acceptance reads the drawn count, so no verdict moves with the served figure (the first
|
||||
/// form of this change, 0266c9ec0, routed the executed count into the acceptance and moved every reg64 verdict).
|
||||
pub fn loads_per_hash_executed(&self) -> usize {
|
||||
let drawn = self.loads_per_hash();
|
||||
let executed = self.scheduled().iter().filter(|i| i.op.is_load()).count() * ITERATIONS;
|
||||
assert!(executed == drawn || (self.class.reg64 && executed == 2 * drawn), "the executed load count {executed} disagrees with the drawn {drawn}");
|
||||
executed
|
||||
}
|
||||
pub fn wide_loads_per_hash(&self) -> usize {
|
||||
self.instrs.iter().filter(|i| i.op == Op::WLoad).count() * ITERATIONS
|
||||
}
|
||||
|
|
@ -1204,9 +1267,21 @@ impl Program {
|
|||
self.instrs.iter().any(|i| i.op == Op::WLoad)
|
||||
}
|
||||
/// Dataset bytes read per hash: 4 per one-word load, 16 and 64 for the wider loads of the experiment.
|
||||
/// Dataset bytes the drawn program's loads demand per nonce (the acceptance's and the draw's figure).
|
||||
pub fn bytes_per_hash(&self) -> usize {
|
||||
self.instrs.iter().filter(|i| i.op == Op::Load).map(|i| i.width as usize * 4).sum::<usize>() * ITERATIONS
|
||||
}
|
||||
|
||||
/// Dataset bytes the hash's EXECUTED loads demand per nonce (review B's V6-02): the scheduled loads' widths in
|
||||
/// words times 4, times the eight iterations; a demand figure, not the memory's transactions (a 4-byte load is
|
||||
/// a 32-byte sector on NVIDIA and a 64-byte line on AMD; the served text names the model it quotes). The
|
||||
/// emitters alone read it.
|
||||
pub fn bytes_per_hash_executed(&self) -> usize {
|
||||
let drawn = self.bytes_per_hash();
|
||||
let executed = self.scheduled().iter().filter(|i| i.op == Op::Load).map(|i| i.width as usize * 4).sum::<usize>() * ITERATIONS;
|
||||
assert!(executed == drawn || (self.class.reg64 && executed == 2 * drawn), "the executed byte demand {executed} disagrees with the drawn {drawn}");
|
||||
executed
|
||||
}
|
||||
/// Scratch read-modify-writes per hash (variant 5): each reads 16 bytes and writes 16 bytes.
|
||||
pub fn scratch_ops_per_hash(&self) -> usize {
|
||||
self.instrs.iter().filter(|i| i.op == Op::Scratch).count() * ITERATIONS
|
||||
|
|
@ -1328,6 +1403,7 @@ impl Program {
|
|||
GENERATOR_VERSION_V3 => ProgramClass::V3,
|
||||
GENERATOR_VERSION_V4 => ProgramClass::V4,
|
||||
GENERATOR_VERSION_V5 => ProgramClass::V5,
|
||||
GENERATOR_VERSION_V6 => ProgramClass::V6,
|
||||
_ => ProgramClass::V2,
|
||||
}
|
||||
}
|
||||
|
|
@ -1458,6 +1534,14 @@ pub fn program_id_class_recipe(generator: u32, seed: &[u32; 8], attempt: u32, cl
|
|||
// class v6 lane 1: the index fold moves every era load address
|
||||
r.lit(b"fold/");
|
||||
}
|
||||
if class.nowin {
|
||||
// D2 "layer 8 off": the window layer is part of the construction
|
||||
r.lit(b"nowin/");
|
||||
}
|
||||
if class.is_v6() {
|
||||
// the v6 draw rules (A02 five shuffle dimensions, A08 the mad operand rule) are part of the construction
|
||||
r.lit(b"v6draw/");
|
||||
}
|
||||
if class.rw != 0 {
|
||||
// class v6 lane 1: the re-weight table moves the op draw
|
||||
r.lit(b"rw/");
|
||||
|
|
@ -1626,6 +1710,23 @@ pub fn candidate_from_words_class(
|
|||
for &slot in &p[..slots] {
|
||||
is_load[slot as usize] = true;
|
||||
}
|
||||
// class v6 (A02, by construction): five of the non-load slots are shuffles over the five lane dimensions in a
|
||||
// drawn order; drawn from the stream after the load slots, so no other class's stream moves
|
||||
let mut is_shfl = [false; INSTR_COUNT];
|
||||
let mut shfl_masks = [1u8, 2, 4, 8, 16];
|
||||
if class.is_v6() {
|
||||
let rest = &mut p[slots..];
|
||||
for i in 0..5 {
|
||||
let j = i + rng.below((rest.len() - i) as u64) as usize;
|
||||
rest.swap(i, j);
|
||||
is_shfl[rest[i] as usize] = true;
|
||||
}
|
||||
for i in 0..5 {
|
||||
let j = i + rng.below((5 - i) as u64) as usize;
|
||||
shfl_masks.swap(i, j);
|
||||
}
|
||||
}
|
||||
let mut shfl_next = 0usize;
|
||||
// Variant 5: the first k drawn load slots (a uniform k-subset, the draw order is random) are scratch ops.
|
||||
let mut is_scratch = [false; INSTR_COUNT];
|
||||
for &slot in &p[..class.scratch_slots()] {
|
||||
|
|
@ -1652,7 +1753,7 @@ pub fn candidate_from_words_class(
|
|||
// class v6 lane 1: the index fold and the re-weight table are set aside too (the address path and the table are not
|
||||
// the shape; a +fold or +rw program draws its sources under the same rule)
|
||||
let source_rule_v4 = matches!(class.shadow, Some(ShadowClass { instrs: V4_SHADOW_INSTRS, .. }))
|
||||
&& LoadClass { era: None, shadow: None, state: false, fold: false, rw: 0, ..class } == LoadClass { shadow: None, ..V4_CLASS };
|
||||
&& LoadClass { era: None, shadow: None, state: false, fold: false, rw: 0, nowin: false, reg64: false, reg64_chain: false, mix: V4_CLASS.mix, ..class } == LoadClass { shadow: None, ..V4_CLASS };
|
||||
// the op table and the roll's range: the plain table at 75 for every class without the re-weight flag
|
||||
let (weights, weights_sum) = class.nonload_weights();
|
||||
let mut fresh = [false; 8];
|
||||
|
|
@ -1682,6 +1783,9 @@ pub fn candidate_from_words_class(
|
|||
Op::Load
|
||||
};
|
||||
}
|
||||
if is_shfl[k] {
|
||||
op = Op::Shfl;
|
||||
}
|
||||
let dst = rng.below(8);
|
||||
let src = if op.is_load() {
|
||||
let mut eligible = [0u64; 8];
|
||||
|
|
@ -1710,12 +1814,21 @@ pub fn candidate_from_words_class(
|
|||
a
|
||||
}
|
||||
};
|
||||
let b = rng.below(8);
|
||||
let mut b = rng.below(8);
|
||||
// class v6 (A08, by construction): a mad's second source is never its destination
|
||||
if class.is_v6() && op == Op::Mad && b == dst {
|
||||
b = (b + 1) & 7;
|
||||
}
|
||||
let imm = rng.next() as u32;
|
||||
let imm2 = rng.next() as u32;
|
||||
let rot = 1 + rng.below(31) as u32;
|
||||
let bit = rng.below(32);
|
||||
let mask = 1u8 << rng.below(5);
|
||||
let mut mask = 1u8 << rng.below(5);
|
||||
// class v6 (A02): the reserved shuffle slots take the five dimensions in the drawn order
|
||||
if is_shfl[k] {
|
||||
mask = shfl_masks[shfl_next];
|
||||
shfl_next += 1;
|
||||
}
|
||||
// Version 2 loads take no width roll, so a mixer class with version 2 loads draws the version 2 program
|
||||
let width = if class.takes_width_roll() { class.width_for_roll(rng.below(100)) } else { 1 };
|
||||
let width = if op == Op::Load { width } else { 1 };
|
||||
|
|
@ -1723,7 +1836,8 @@ pub fn candidate_from_words_class(
|
|||
let (win, off) = if class.era.is_some() {
|
||||
let k = rng.below(3) as u8;
|
||||
let o = (rng.next() as u32 & ((1u32 << k) - 1)) as u8;
|
||||
if op == Op::Load {
|
||||
// D2 "layer 8 off": the draws are consumed (the stream is the class's) and every site reads the whole dataset
|
||||
if op == Op::Load && !class.nowin {
|
||||
(k, o)
|
||||
} else {
|
||||
(0, 0)
|
||||
|
|
@ -1963,6 +2077,7 @@ pub fn generate_from_seed_bytes_program_class(seed_string: &str, seed_bytes: &[u
|
|||
(ProgramClass::V3, Some(era)) => return generate_era(seed_string, seed_bytes, V3_CLASS, era, &V3_ALLOWED),
|
||||
(ProgramClass::V4, Some(era)) => return generate_era_generator(seed_string, seed_bytes, V4_CLASS, era, &V3_ALLOWED, GENERATOR_VERSION_V4),
|
||||
// Class v5: the same draw inside V5_CLASS (V4_CLASS plus the state flag, which the draw does not read), generator 5.
|
||||
(ProgramClass::V6, Some(era)) => return generate_era_generator(seed_string, seed_bytes, V6_CLASS, era, &V3_ALLOWED, GENERATOR_VERSION_V6),
|
||||
(ProgramClass::V5, Some(era)) => return generate_era_generator(seed_string, seed_bytes, V5_CLASS, era, &V3_ALLOWED, GENERATOR_VERSION_V5),
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -1980,11 +2095,11 @@ pub fn generate_from_seed_bytes_program_class(seed_string: &str, seed_bytes: &[u
|
|||
/// and class v4 at rung 0, is [`generate_from_seed_bytes_program_class`] byte for byte. The base program, the 16 loads
|
||||
/// and the era draw do not move with the rung: only the pass count of the shadow block does.
|
||||
pub fn generate_from_seed_bytes_program_class_shadow(seed_string: &str, seed_bytes: &[u8], class: ProgramClass, era_bytes: Option<&[u8]>, shadow_reps: u16) -> Program {
|
||||
if !matches!(class, ProgramClass::V4 | ProgramClass::V5) || shadow_reps == 0 || shadow_reps == V4_SHADOW_REPS {
|
||||
if !matches!(class, ProgramClass::V4 | ProgramClass::V5 | ProgramClass::V6) || shadow_reps == 0 || shadow_reps == V4_SHADOW_REPS {
|
||||
return generate_from_seed_bytes_program_class(seed_string, seed_bytes, class, era_bytes);
|
||||
}
|
||||
// class v5 at a rung: class v4's rung with the state flag, generator 5
|
||||
let (base, generator) = if class == ProgramClass::V5 { (v5_class_at(shadow_reps), GENERATOR_VERSION_V5) } else { (v4_class_at(shadow_reps), GENERATOR_VERSION_V4) };
|
||||
let (base, generator) = if class == ProgramClass::V6 { (LoadClass { fold: true, rw: 1, reg64: true, reg64_chain: true, ..v5_class_at(shadow_reps) }, GENERATOR_VERSION_V6) } else if class == ProgramClass::V5 { (v5_class_at(shadow_reps), GENERATOR_VERSION_V5) } else { (v4_class_at(shadow_reps), GENERATOR_VERSION_V4) };
|
||||
match era_bytes {
|
||||
Some(era) => generate_era_generator(seed_string, seed_bytes, base, era, &V3_ALLOWED, generator),
|
||||
None => {
|
||||
|
|
@ -2330,14 +2445,15 @@ mod tests {
|
|||
assert_eq!(v3.program_id(), program_id(GENERATOR_VERSION_V3, &v3.seed, v3.attempt));
|
||||
assert_ne!(v3.program_id(), program_id(GENERATOR_VERSION, &v3.seed, v3.attempt));
|
||||
assert_ne!(v3.program_id(), v2.program_id());
|
||||
for c in [ProgramClass::V2, ProgramClass::V3, ProgramClass::V4, ProgramClass::V5] {
|
||||
for c in [ProgramClass::V2, ProgramClass::V3, ProgramClass::V4, ProgramClass::V5, ProgramClass::V6] {
|
||||
assert_eq!(ProgramClass::parse(c.name()), Some(c));
|
||||
assert_eq!(ProgramClass::from_generator(c.generator_version()), Some(c));
|
||||
assert_eq!(ProgramClass::from_u8(c.as_u8()), Some(c));
|
||||
}
|
||||
assert_eq!(ProgramClass::from_generator(1), None);
|
||||
assert_eq!(ProgramClass::from_generator(6), None);
|
||||
assert_eq!(ProgramClass::parse("v6"), None);
|
||||
assert_eq!(ProgramClass::from_generator(6), Some(ProgramClass::V6), "class v6 is generator 6 (review B's F03, 8 October 2026)");
|
||||
assert_eq!(ProgramClass::from_generator(7), None);
|
||||
assert_eq!(ProgramClass::parse("v7"), None);
|
||||
assert_eq!(ProgramClass::default(), ProgramClass::V2);
|
||||
assert_eq!(ProgramClass::V2.load_class(), LoadClass::V2);
|
||||
assert!(!ProgramClass::V2.has_era() && ProgramClass::V3.has_era() && ProgramClass::V4.has_era() && ProgramClass::V5.has_era());
|
||||
|
|
|
|||
|
|
@ -122,7 +122,8 @@ fn usage() -> ! {
|
|||
\x20 --era-widths 4[,16,32,64] the width set the era draws from, in bytes (default 4: pinned; more lets the era draw it; 32 only with the w32 class)\n\
|
||||
\x20 --dataset-words N research class ds55: the dataset at N words (a multiple of 65,536 in 2^28 ..= 2^31; 1476395008 = 5.5 GiB); a non-power-of-two uses idx = (src * N) >> 32 in every load (spec 01 section 1.13.3), a power of two is --dataset-log2\n\\
|
||||
\x20 --reg64 the 64-register window per lane over the class (research; the same as --class <class>+reg64; CUDA, OpenCL and Metal texts)\n\
|
||||
\x20 --reg64-chain reg64 with the full-chain address mix: every load's address consumes all 64 registers (the same as --class <class>+reg64c)"
|
||||
\x20 --reg64-chain reg64 with the full-chain address mix: every load's address consumes all 64 registers (the same as --class <class>+reg64c)\n\
|
||||
\x20 --reg64-prefix export: the CUDA texts carry the reg64 chain in its closed prefix form (review B's F05; the same hash and vectors, a measurement text)"
|
||||
);
|
||||
std::process::exit(2)
|
||||
}
|
||||
|
|
@ -189,6 +190,7 @@ fn parse() -> Args {
|
|||
a.reg64 = true;
|
||||
a.reg64_chain = true;
|
||||
}
|
||||
"--reg64-prefix" => igneum_pow::emit::set_reg64_prefix(true),
|
||||
_ => usage(),
|
||||
}
|
||||
}
|
||||
|
|
@ -229,10 +231,29 @@ fn main() {
|
|||
// gate G2 (5 October 2026, ported from branch ca2-era for the class v4 gate run): `--count N` prints
|
||||
// "nonce hash" for N consecutive 64-bit nonces from --nonce, one epoch build, to re-hash a worker's
|
||||
// found lines (the same prehash, target ff..ff)
|
||||
for k in 0..a.count {
|
||||
let n = a.nonce.wrapping_add(k);
|
||||
println!("{n} {:016x}", e.hash_bound(&prehash, n));
|
||||
// one warp per 32 nonces (the P01 campaign, 8 October 2026: a million nonces per pack in minutes, not
|
||||
// hours; the per-nonce form hashed the whole aligned group for every nonce)
|
||||
let end = a.nonce.wrapping_add(a.count);
|
||||
let mut n = a.nonce;
|
||||
let mut out = String::with_capacity(1 << 16);
|
||||
use std::io::Write;
|
||||
let stdout = std::io::stdout();
|
||||
let mut lock = stdout.lock();
|
||||
while n != end {
|
||||
let base = n & !31;
|
||||
let warp = e.hash_warp_bound(&prehash, base);
|
||||
let mut m = n;
|
||||
while m != end && (m & !31) == base {
|
||||
out.push_str(&format!("{m} {:016x}\n", warp[(m & 31) as usize]));
|
||||
m = m.wrapping_add(1);
|
||||
}
|
||||
n = m;
|
||||
if out.len() > (1 << 15) {
|
||||
lock.write_all(out.as_bytes()).unwrap();
|
||||
out.clear();
|
||||
}
|
||||
}
|
||||
lock.write_all(out.as_bytes()).unwrap();
|
||||
return;
|
||||
}
|
||||
let init = igneum_pow::bind::block_init_words(&prehash, a.nonce);
|
||||
|
|
|
|||
|
|
@ -199,14 +199,14 @@ pub fn verify_pack_texts_chain(
|
|||
// carries IGNEUM_STATE_ROOT_HEX (and the shadow block of v4) and no other generator does.
|
||||
let state_root_hex = define_str(program_h, "IGNEUM_STATE_ROOT_HEX");
|
||||
match (class, state_root_hex.is_some()) {
|
||||
(ProgramClass::V5, false) => return Err(PackFault::Disagree("IGNEUM_GENERATOR 5 (class v5) without IGNEUM_STATE_ROOT_HEX: not a class v5 pack".into())),
|
||||
(ProgramClass::V5, true) => {}
|
||||
(ProgramClass::V5 | ProgramClass::V6, false) => return Err(PackFault::Disagree("IGNEUM_GENERATOR 5 or 6 (a state class) without IGNEUM_STATE_ROOT_HEX: not a class v5 pack".into())),
|
||||
(ProgramClass::V5 | ProgramClass::V6, true) => {}
|
||||
(_, true) => return Err(PackFault::Disagree(format!("IGNEUM_GENERATOR {generator} with class v5 state lines: a program over state leaves is generator 5 (export the pack as class v5)"))),
|
||||
_ => {}
|
||||
}
|
||||
match (class, shadow > 0) {
|
||||
(ProgramClass::V4, false) => return Err(PackFault::Disagree("IGNEUM_GENERATOR 4 (class v4) without IGNEUM_SHADOW_INSTRS: not a class v4 pack".into())),
|
||||
(ProgramClass::V5, false) => return Err(PackFault::Disagree("IGNEUM_GENERATOR 5 (class v5) without IGNEUM_SHADOW_INSTRS: not a class v5 pack".into())),
|
||||
(ProgramClass::V5 | ProgramClass::V6, false) => return Err(PackFault::Disagree("IGNEUM_GENERATOR 5 or 6 (a state class) without IGNEUM_SHADOW_INSTRS: not a class v5 pack".into())),
|
||||
// the class v4 stream sub-version (AP-F8-1 amendment, 7 October 2026): a generator 4 pack from before the
|
||||
// load-source rule carries no IGNEUM_PROGRAM_SUBVERSION and its program id is another stream's; refused
|
||||
(ProgramClass::V4, true) if define_u32(program_h, "IGNEUM_PROGRAM_SUBVERSION") != Some(u32::from(crate::generator::PROGRAM_SUBVERSION_V4)) => {
|
||||
|
|
|
|||
|
|
@ -1172,3 +1172,91 @@ mod tests {
|
|||
assert!(!e.verify_block(0, w[0] - 1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Review B's F05 (8 October 2026): the reg64 full-chain address source in closed form. The reference fold
|
||||
/// (`m = r[k0]; m = rotl(m, 1) ^ r[k1]; ...` over the 63 registers other than `s`, in index order) equals
|
||||
/// `r[s] ^ ror(P_s, 1) ^ (S ^ P_s ^ a[s])` with `a[k] = rotl(r[k], (63 - k) mod 32)`, `S` the xor of every `a[k]`
|
||||
/// and `P_s` the xor of `a[k]` for `k < s`: a prefix-xor structure answers every load from one `S` and one prefix,
|
||||
/// which is the incremental form a chip would keep. The verifier keeps the fold as the definition; this form is the
|
||||
/// test's and the fleet's measurement's.
|
||||
pub fn reg64_address_source(r: &[u32; 64], s: usize) -> u32 {
|
||||
let a = |k: usize| r[k].rotate_left(((63 - k) % 32) as u32);
|
||||
let mut total = 0u32;
|
||||
let mut prefix = 0u32;
|
||||
for k in 0..64 {
|
||||
total ^= a(k);
|
||||
if k < s {
|
||||
prefix ^= a(k);
|
||||
}
|
||||
}
|
||||
r[s] ^ prefix.rotate_right(1) ^ (total ^ prefix ^ a(s))
|
||||
}
|
||||
|
||||
/// The reference fold of [`reg64_address_source`] on one lane's registers, as `interpret_warp_core` computes it.
|
||||
pub fn reg64_address_source_reference(r: &[u32; 64], s: usize) -> u32 {
|
||||
let mut m = 0u32;
|
||||
let mut started = false;
|
||||
for (k, &v) in r.iter().enumerate() {
|
||||
if k == s {
|
||||
continue;
|
||||
}
|
||||
m = if started { m.rotate_left(1) ^ v } else { v };
|
||||
started = true;
|
||||
}
|
||||
r[s] ^ m
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod reg64_mix_tests {
|
||||
use super::*;
|
||||
|
||||
/// Review B's F05, known-failed first: a form with one rotation off (every term rotated as if it sat after the
|
||||
/// source) disagrees with the reference on some source; the closed form agrees on every source register over
|
||||
/// 256 register states drawn from a fixed stream and over the states a real program leaves in its registers.
|
||||
#[test]
|
||||
fn reg64_closed_form_equals_the_reference_fold_on_every_source() {
|
||||
let wrong = |r: &[u32; 64], s: usize| -> u32 {
|
||||
let a = |k: usize| r[k].rotate_left(((63 - k) % 32) as u32);
|
||||
let total: u32 = (0..64).map(a).fold(0, |x, y| x ^ y);
|
||||
r[s] ^ total ^ a(s)
|
||||
};
|
||||
let mut x = 0x9e37_79b9_7f4a_7c15u64;
|
||||
let mut next = || {
|
||||
x = x.wrapping_add(0x9e37_79b9_7f4a_7c15);
|
||||
let mut z = x;
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
|
||||
(z ^ (z >> 31)) as u32
|
||||
};
|
||||
let mut wrong_differs = false;
|
||||
for _ in 0..256 {
|
||||
let mut r = [0u32; 64];
|
||||
for v in r.iter_mut() {
|
||||
*v = next();
|
||||
}
|
||||
for s in 0..64 {
|
||||
assert_eq!(reg64_address_source(&r, s), reg64_address_source_reference(&r, s), "source r{s}");
|
||||
if wrong(&r, s) != reg64_address_source_reference(&r, s) {
|
||||
wrong_differs = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(wrong_differs, "the known-failed form must disagree somewhere");
|
||||
// the init rule's registers (r0..r7 seeded, r[k] = r[k & 7] * 0x9e3779b9 + k) and a few real updates
|
||||
let mut r = [0u32; 64];
|
||||
for (k, v) in r.iter_mut().enumerate().take(8) {
|
||||
*v = next() ^ (k as u32);
|
||||
}
|
||||
for k in 8..64 {
|
||||
r[k] = r[k & 7].wrapping_mul(0x9e37_79b9).wrapping_add(k as u32);
|
||||
}
|
||||
for step in 0..64 {
|
||||
let d = (step * 7 + 3) % 64;
|
||||
r[d] = r[d].wrapping_mul(r[(d + 13) % 64]).wrapping_add(next());
|
||||
for s in 0..64 {
|
||||
assert_eq!(reg64_address_source(&r, s), reg64_address_source_reference(&r, s), "step {step} source r{s}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,8 +68,11 @@ fn power_of_two_path_is_byte_identical_to_the_pinned_pack() {
|
|||
assert!(!e.dataset.geom.mulshift);
|
||||
assert_eq!(e.dataset.geom, DatasetGeom::pow2(28));
|
||||
let out = export_pack(e, &day_label(), SOURCE);
|
||||
assert_eq!(out.files.len(), 12, "the pack's twelve files");
|
||||
assert_eq!(out.files.len(), 13, "the pack's thirteen files");
|
||||
for (name, text) in &out.files {
|
||||
if name == "identity.json" {
|
||||
continue; // A06's identity file is new beside the pinned twelve
|
||||
}
|
||||
let want = pinned(name);
|
||||
assert!(text == &want, "mx8-devnet-epoch0/{name} differs from the default path's export");
|
||||
}
|
||||
|
|
@ -219,7 +222,7 @@ fn ds55_pack_fields_and_vectors() {
|
|||
assert_eq!(e.program.seed[0], 0x667d_0fbd);
|
||||
assert_eq!(e.program.seed[1], 0x7b8e_5963);
|
||||
let out = export_pack(e, &day_label(), SOURCE);
|
||||
assert_eq!(out.files.len(), 12);
|
||||
assert_eq!(out.files.len(), 13);
|
||||
let pj = &out.files.iter().find(|(n, _)| n == "program.json").unwrap().1;
|
||||
let j: Value = serde_json::from_str(pj).expect("program.json is valid JSON");
|
||||
assert_eq!(j["program_id"].as_str().unwrap(), "0x73bcbfe8ccf988f1");
|
||||
|
|
|
|||
132
igneum-pow/tests/fuzz_parsers.rs
Normal file
132
igneum-pow/tests/fuzz_parsers.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
//! POW-01 and POW-02's malformed-input half (the Test and Acceptance Standard, 8 October 2026): 10,000 mutated inputs
|
||||
//! per parser, every one refused cleanly or accepted, never a panic. The parsers: the class string (`LoadClass::parse`,
|
||||
//! `ProgramClass::parse`), the hex reader (`bind::unhex`), the IGSD1 state stream (`StateStream::decode`), the era
|
||||
//! string form (`"<n>:<64 hex>"`, the exporter's `parse_era` re-stated here since main.rs keeps it private). The
|
||||
//! mutations: byte flips, truncation, insertion, duplication and random bytes over a valid seed input, drawn from a
|
||||
//! fixed SplitMix64 so the run is reproducible; a panic in any parser fails the test (the harness catches none).
|
||||
use igneum_pow::bind::unhex;
|
||||
use igneum_pow::generator::{LoadClass, ProgramClass};
|
||||
use igneum_pow::state::StateStream;
|
||||
|
||||
struct Rng(u64);
|
||||
impl Rng {
|
||||
fn next(&mut self) -> u64 {
|
||||
self.0 = self.0.wrapping_add(0x9e3779b97f4a7c15);
|
||||
let mut z = self.0;
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
|
||||
z ^ (z >> 31)
|
||||
}
|
||||
fn below(&mut self, n: usize) -> usize {
|
||||
(self.next() % (n.max(1) as u64)) as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// One mutation of `seed`: a flipped byte, a truncation, an insertion, a doubled slice, or random bytes of a random length.
|
||||
fn mutate(rng: &mut Rng, seed: &[u8]) -> Vec<u8> {
|
||||
let mut v = seed.to_vec();
|
||||
match rng.below(6) {
|
||||
0 => {
|
||||
if !v.is_empty() {
|
||||
let i = rng.below(v.len());
|
||||
v[i] ^= 1 << rng.below(8);
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
let n = rng.below(v.len() + 1);
|
||||
v.truncate(n);
|
||||
}
|
||||
2 => {
|
||||
let i = rng.below(v.len() + 1);
|
||||
v.insert(i, rng.next() as u8);
|
||||
}
|
||||
3 => {
|
||||
if v.len() > 1 {
|
||||
let a = rng.below(v.len());
|
||||
let b = a + rng.below(v.len() - a);
|
||||
let slice = v[a..b].to_vec();
|
||||
v.splice(a..a, slice);
|
||||
}
|
||||
}
|
||||
4 => {
|
||||
let n = rng.below(300);
|
||||
v = (0..n).map(|_| rng.next() as u8).collect();
|
||||
}
|
||||
_ => {
|
||||
if !v.is_empty() {
|
||||
let i = rng.below(v.len());
|
||||
v[i] = rng.next() as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
const N: usize = 10_000;
|
||||
|
||||
#[test]
|
||||
fn class_strings_never_panic() {
|
||||
let seeds = ["mx8+sh256x27+state+reg64c+fold+rw", "mx8+sh256x27+state+nowin", "w32m8g-erad810f22d+sh256x27+state", "v2", "mix50-35-15", "scr4k32+hot64k4a", "mx4", "p4,p16,p64", "w64x4", "mx8+rw2"];
|
||||
let mut rng = Rng(1);
|
||||
let (mut some, mut none) = (0, 0);
|
||||
for k in 0..N {
|
||||
let s = mutate(&mut rng, seeds[k % seeds.len()].as_bytes());
|
||||
let text = String::from_utf8_lossy(&s);
|
||||
match LoadClass::parse(&text) {
|
||||
Some(c) => {
|
||||
// an accepted string names back to something the parser accepts again
|
||||
assert!(LoadClass::parse(&c.name()).is_some(), "{text:?} -> {} does not re-parse", c.name());
|
||||
some += 1;
|
||||
}
|
||||
None => none += 1,
|
||||
}
|
||||
let _ = ProgramClass::parse(&text);
|
||||
}
|
||||
println!("class strings: {some} accepted, {none} refused of {N}");
|
||||
assert!(none > N / 2, "the mutations refuse at least half");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_and_era_strings_never_panic() {
|
||||
let hex = "edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07";
|
||||
let mut rng = Rng(2);
|
||||
let mut refused = 0;
|
||||
for _ in 0..N {
|
||||
let s = mutate(&mut rng, hex.as_bytes());
|
||||
let text = String::from_utf8_lossy(&s);
|
||||
if unhex(&text).is_none() {
|
||||
refused += 1;
|
||||
}
|
||||
// the era string "<n>:<64 hex>" as the exporter reads it
|
||||
let era = format!("{}:{}", rng.below(1000), text);
|
||||
let parsed = era.split_once(':').and_then(|(n, h)| n.parse::<u64>().ok().zip(unhex(h))).filter(|(_, b)| b.len() == 32);
|
||||
if let Some((_, b)) = parsed {
|
||||
assert_eq!(b.len(), 32);
|
||||
}
|
||||
}
|
||||
println!("hex strings: {refused} refused of {N}");
|
||||
assert!(refused > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_streams_never_panic() {
|
||||
// a small valid stream: three records under a fixed root and block
|
||||
let stream = StateStream { number: 159357, block: [7u8; 32], root: [9u8; 32], records: vec![vec![1, 2, 3], vec![], (0..200u8).collect()] };
|
||||
let valid = stream.encode();
|
||||
assert_eq!(StateStream::decode(&valid).map(|s| s.records.len()), Ok(3));
|
||||
let mut rng = Rng(3);
|
||||
let (mut ok, mut err) = (0, 0);
|
||||
for _ in 0..N {
|
||||
let bytes = mutate(&mut rng, &valid);
|
||||
match StateStream::decode(&bytes) {
|
||||
Ok(s) => {
|
||||
// an accepted stream re-encodes to the bytes it was read from
|
||||
assert_eq!(s.encode(), bytes, "an accepted stream must round-trip");
|
||||
ok += 1;
|
||||
}
|
||||
Err(_) => err += 1,
|
||||
}
|
||||
}
|
||||
println!("state streams: {ok} accepted, {err} refused of {N}");
|
||||
assert!(err > N / 2);
|
||||
}
|
||||
|
|
@ -347,8 +347,8 @@ fn determinism_v3() {
|
|||
};
|
||||
println!("determinism {}: two builds equal, against the pinned pack {}", class.name(), dir.display());
|
||||
for (name, text) in &pa.files {
|
||||
if name == "vectors.json" || name == "vectors.h" {
|
||||
continue; // the source string differs ("a" here)
|
||||
if name == "vectors.json" || name == "vectors.h" || name == "identity.json" {
|
||||
continue; // the source string differs ("a" here); identity.json is new beside the pinned twelve (A06)
|
||||
}
|
||||
let on_disk = std::fs::read_to_string(dir.join(name)).unwrap();
|
||||
assert_eq!(&on_disk, text, "{name}");
|
||||
|
|
|
|||
|
|
@ -433,7 +433,9 @@ fn check_export(pack: &str) {
|
|||
if e.dataset.mode() == DatasetMode::MemoryHard {
|
||||
expected.extend(["memhard.h", "memhard.metal"]);
|
||||
}
|
||||
assert_eq!(out.files.iter().map(|(n, _)| n.as_str()).collect::<Vec<_>>(), expected);
|
||||
let mut with_identity: Vec<&str> = expected.clone();
|
||||
with_identity.insert(1, "identity.json");
|
||||
assert_eq!(out.files.iter().map(|(n, _)| n.as_str()).collect::<Vec<_>>(), with_identity);
|
||||
let mut on_disk: Vec<String> = std::fs::read_dir(pack_dir(pack))
|
||||
.unwrap()
|
||||
.map(|d| d.unwrap().file_name().to_string_lossy().to_string())
|
||||
|
|
@ -646,6 +648,9 @@ fn era_emitted_sources_match_and_loads_have_the_era_form() {
|
|||
let source = era_json(pack, "vectors.json")["source"].as_str().unwrap().to_string();
|
||||
let out = export_pack(e, &day, &source);
|
||||
for (name, text) in &out.files {
|
||||
if name == "identity.json" {
|
||||
continue; // A06's identity file is new beside the pinned files
|
||||
}
|
||||
let want = era_read(pack, name);
|
||||
assert!(text == &want, "{pack}/{name} differs from the emitter");
|
||||
}
|
||||
|
|
@ -655,7 +660,7 @@ fn era_emitted_sources_match_and_loads_have_the_era_form() {
|
|||
.filter(|n| !n.starts_with('.') && n != "seeds.txt")
|
||||
.collect();
|
||||
on_disk.sort();
|
||||
let mut want: Vec<String> = out.files.iter().map(|(n, _)| n.clone()).collect();
|
||||
let mut want: Vec<String> = out.files.iter().map(|(n, _)| n.clone()).filter(|n| n != "identity.json").collect();
|
||||
want.sort();
|
||||
assert_eq!(on_disk, want, "{pack}: the pack holds the export's files and seeds.txt only");
|
||||
let era = e.program.class.era.unwrap();
|
||||
|
|
@ -869,7 +874,7 @@ fn hot_packs_emitted_sources_and_load_forms() {
|
|||
let file = |name: &str| -> &str { &out.files.iter().find(|(n, _)| n == name).unwrap().1 };
|
||||
hassert_same_text(pack, "vectors.json", file("vectors.json"));
|
||||
hassert_same_text(pack, "vectors.h", file("vectors.h"));
|
||||
assert_eq!(out.files.len(), 12);
|
||||
assert_eq!(out.files.len(), 13);
|
||||
// One form per dialect, exactly 16 - k masked dataset loads and k hot loads in every hash kernel; the fill
|
||||
// kernel is present once per source that builds the table.
|
||||
for (file, load, masked, hot) in [
|
||||
|
|
@ -974,6 +979,9 @@ fn v5_pack_is_the_v4_program_over_the_state_leaves() {
|
|||
let v = v5_json(pack, "vectors.json");
|
||||
let out = export_pack(e, v["day"].as_str().unwrap(), v["source"].as_str().unwrap());
|
||||
for (name, text) in &out.files {
|
||||
if name == "identity.json" {
|
||||
continue; // A06's identity file is new beside the pinned files
|
||||
}
|
||||
assert_eq!(v5_read(pack, name), *text, "{pack}/{name} differs from the export");
|
||||
}
|
||||
for (name, bytes) in &out.binaries {
|
||||
|
|
@ -1032,8 +1040,11 @@ fn reg64_plain_path_reexports_the_pinned_devnet_pack_unchanged() {
|
|||
assert_eq!(e.program.registers(), 8);
|
||||
assert_eq!(e.program.scheduled(), e.program.instrs, "without the flag the schedule is the drawn program");
|
||||
let out = export_pack(e, &day_label(pack), PINNED_SOURCE);
|
||||
assert_eq!(out.files.len(), 12);
|
||||
assert_eq!(out.files.len(), 13);
|
||||
for (name, text) in &out.files {
|
||||
if name == "identity.json" {
|
||||
continue; // A06's identity file is new beside the pinned twelve
|
||||
}
|
||||
assert_same_text(pack, name, text);
|
||||
}
|
||||
}
|
||||
|
|
@ -1222,4 +1233,64 @@ fn reg64_liveness_rule_refuses_the_subset_fold_and_passes_the_full_chain() {
|
|||
let mut chain = epoch_reg64_of_devnet();
|
||||
chain.program.class = chain.program.class.with_reg64_chain();
|
||||
assert_eq!(check_window_liveness(&chain.program), Ok(()), "the full chain keeps all 64 registers live across the chain");
|
||||
// wired into the acceptance (8 October 2026): `accept::check` refuses the arithmetic-only window as a dead register
|
||||
// and never refuses the chain program for liveness; the plain pack's verdict does not move
|
||||
use igneum_pow::accept::check;
|
||||
assert!(matches!(check(&window.program), Err(Reject::DeadWindowRegister { .. })), "the acceptance refuses the arithmetic-only window");
|
||||
assert!(!matches!(check(&chain.program), Err(Reject::DeadWindowRegister { .. }) | Err(Reject::NotAWindow)), "the chain program is never refused for liveness");
|
||||
assert!(check(&plain.program).is_ok(), "the pinned devnet program's verdict does not move");
|
||||
}
|
||||
|
||||
/// The external review's A07 (8 October 2026): the bound hash of a nonce is the lane of the aligned 32-nonce group that
|
||||
/// contains it, in the verifier by construction (`Epoch::hash_bound`), across the group's tail and the low-32 rollover;
|
||||
/// the high 32 bits of the nonce enter the init words, so two nonces 2^32 apart never share a hash. The launchers
|
||||
/// refuse a job whose nonce_start is not 32-aligned or whose count is not a multiple of 32 (worker.cpp's job line).
|
||||
#[test]
|
||||
fn a07_bound_hash_is_group_aligned_across_tails_and_rollover() {
|
||||
use igneum_pow::bind::block_init_words;
|
||||
let e = epoch("mx8-devnet-epoch0");
|
||||
let prehash = [0x5au8; 32];
|
||||
for &n in &[0u64, 1, 2, 31, 32, 33, 63, (1u64 << 32) - 1, 1u64 << 32, (1u64 << 32) + 1, u64::MAX - 1, u64::MAX] {
|
||||
let base = n & !31;
|
||||
let warp = e.hash_warp_bound(&prehash, base);
|
||||
assert_eq!(e.hash_bound(&prehash, n), warp[(n & 31) as usize], "nonce {n}: the lane of its aligned group");
|
||||
assert_eq!(e.hash_warp_bound(&prehash, n), warp, "nonce {n}: the group is the same from any of its nonces");
|
||||
let distinct: std::collections::HashSet<u64> = warp.iter().copied().collect();
|
||||
assert!(distinct.len() >= 31, "nonce {n}: the lanes of a group are distinct hashes");
|
||||
}
|
||||
assert_ne!(e.hash_bound(&prehash, 2), e.hash_bound(&prehash, 2 + (1u64 << 32)), "the high 32 bits reach the hash");
|
||||
assert_ne!(block_init_words(&prehash, 2), block_init_words(&prehash, 2 + (1u64 << 32)));
|
||||
}
|
||||
|
||||
/// The external review's A04 (8 October 2026): the acceptance executes the shadow block as the hash does (class v4
|
||||
/// sub-version 3), so a shadow that zeroes the registers fails acceptance; the pinned program with its own shadow passes.
|
||||
#[test]
|
||||
fn a04_a_zeroing_shadow_fails_acceptance() {
|
||||
use igneum_pow::accept::check;
|
||||
// a class v4 program (the shadow block is class v4's): the same seed and era as the lib's program_classes test
|
||||
let era = [7u8; 32];
|
||||
let p = generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V4, Some(&era));
|
||||
assert!(check(&p).is_ok(), "the class v4 program of the genesis seed passes");
|
||||
let mut z = p.clone();
|
||||
assert!(!z.shadow.is_empty(), "class v4 has a shadow block");
|
||||
for ins in z.shadow.iter_mut() {
|
||||
ins.op = Op::Xor;
|
||||
ins.src = ins.dst;
|
||||
}
|
||||
assert!(check(&z).is_err(), "a shadow of xor d, d zeroes every register it touches and must fail");
|
||||
}
|
||||
|
||||
/// The external review's A06 (8 October 2026): the pack's identity.json authenticates every kernel text by BLAKE2b-256.
|
||||
#[test]
|
||||
fn a06_program_json_hashes_every_kernel_text() {
|
||||
let e = epoch("mx8-devnet-epoch0");
|
||||
let out = export_pack(&e, &day_label("mx8-devnet-epoch0"), "test");
|
||||
let file = |n: &str| out.files.iter().find(|(f, _)| f == n).map(|(_, t)| t.clone()).unwrap();
|
||||
let j: Value = serde_json::from_str(&file("identity.json")).unwrap();
|
||||
let h = j["kernel_blake2b256"].as_object().expect("kernel_blake2b256");
|
||||
assert_eq!(j["program_id"].as_str().unwrap(), format!("{:#018x}", e.program.program_id()));
|
||||
for n in ["kernel.cu", "kernel.cl", "program.metal", "program_bound.metal", "kernel_bound.cu", "kernel_bound.cl"] {
|
||||
let want: String = igneum_pow::blake2b::blake2b_256(&[file(n).as_bytes()]).iter().map(|x| format!("{x:02x}")).collect();
|
||||
assert_eq!(h[n].as_str().unwrap(), want, "{n}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use igneum_pow::accept::{index_bit_sigma, index_bit_sigma_max, ACCEPT_UNITS_DIST
|
|||
use igneum_pow::emit::export_pack;
|
||||
use igneum_pow::generator::{
|
||||
candidate_class, generate_era_generator, LoadClass, Op, Program, ProgramClass, EraParams, INSTR_COUNT, NONLOAD_WEIGHTS,
|
||||
NONLOAD_WEIGHTS_RW, NONLOAD_WEIGHTS_RW2, NONLOAD_WEIGHTS_RW2_SUM, NONLOAD_WEIGHTS_RW_SUM, V3_ALLOWED, V4_CLASS, V5_CLASS,
|
||||
NONLOAD_WEIGHTS_RW, NONLOAD_WEIGHTS_RW2, NONLOAD_WEIGHTS_RW2_SUM, NONLOAD_WEIGHTS_RW_SUM, V3_ALLOWED, V3_CLASS, V4_CLASS, V5_CLASS,
|
||||
};
|
||||
use igneum_pow::seed::seed_words_from_bytes;
|
||||
use igneum_pow::verify::{load_index, stride, DatasetGeom, Epoch, INDEX_FOLD_SHIFT};
|
||||
|
|
@ -182,6 +182,123 @@ fn class_v6_all_flags_parse_name_and_id() {
|
|||
assert!(p.class.era.unwrap().fold);
|
||||
}
|
||||
|
||||
/// 1c. D2 "layer 8 off" (8 October 2026): "+nowin" parses in any order and names back inside "+fold"/"+rw"; the draw
|
||||
/// keeps every instruction of the class without the flag and sets every load site's window to the whole dataset;
|
||||
/// the id moves; the era and the stride do not.
|
||||
#[test]
|
||||
fn nowin_removes_the_window_layer_and_nothing_else() {
|
||||
let c = LoadClass::parse("mx8+sh256x27+state+nowin+fold+rw").unwrap();
|
||||
assert!(c.nowin && c.fold && c.rw == 1 && c.state);
|
||||
assert_eq!(c.name(), "mx8+sh256x27+state+nowin+fold+rw");
|
||||
assert_eq!(LoadClass::parse("mx8+sh256x27+state+fold+rw+nowin"), Some(c));
|
||||
assert_eq!(LoadClass::parse("mx8+sh256x27+state+reg64c+nowin").unwrap().name(), "mx8+sh256x27+state+reg64c+nowin");
|
||||
assert!(!V5_CLASS.nowin);
|
||||
let era = f8_bytes("era", 4);
|
||||
let seed = f8_bytes("program", 4);
|
||||
// both sides of the pair draw under the v6 rules (the five shuffle slots and the mad operand rule enter the
|
||||
// stream for every class with a v6 flag), so the pair is the fold class with and without layer 8 off
|
||||
let plain = candidate_class("p4-attack-f8", &seed, 0, LoadClass::era(V5_CLASS.with_fold(), &era, &V3_ALLOWED));
|
||||
let off = candidate_class("p4-attack-f8", &seed, 0, LoadClass::era(V5_CLASS.with_fold().with_nowin(), &era, &V3_ALLOWED));
|
||||
assert_ne!(plain.program_id(), off.program_id(), "the id carries the flag");
|
||||
assert_eq!(plain.instrs.len(), off.instrs.len());
|
||||
let mut windows_plain = 0;
|
||||
for (a, b) in plain.instrs.iter().zip(off.instrs.iter()) {
|
||||
assert_eq!((a.op, a.dst, a.src, a.src2, a.imm, a.imm2, a.rot, a.bit, a.mask, a.width), (b.op, b.dst, b.src, b.src2, b.imm, b.imm2, b.rot, b.bit, b.mask, b.width), "the instruction is the class's without the flag");
|
||||
assert_eq!((b.win, b.off), (0, 0), "every site reads the whole dataset");
|
||||
if a.win != 0 {
|
||||
windows_plain += 1;
|
||||
}
|
||||
}
|
||||
assert!(windows_plain > 0, "the plain draw has at least one shrunk window on this seed");
|
||||
assert_eq!(plain.shadow, off.shadow);
|
||||
assert_eq!(plain.class.era.map(|e| EraParams { fold: e.fold, ..e }), off.class.era.map(|e| EraParams { fold: e.fold, ..e }), "the era draw is untouched");
|
||||
}
|
||||
|
||||
/// The external review's A02 and A08 (8 October 2026), fixed by construction for every class carrying a v6 flag:
|
||||
/// every candidate of 64 seeds (attempt 0, no acceptance pass) carries shuffles over all five lane dimensions, and no
|
||||
/// `mad` names its destination as its second source; the plain class v5 draw is untouched (the pinned pack test
|
||||
/// reads that); the v6 draw's id carries the rule.
|
||||
#[test]
|
||||
fn v6_draw_connects_all_lanes_and_keeps_mad_bijective_by_construction() {
|
||||
let era = f8_bytes("era", 4);
|
||||
let classes = [V5_CLASS.with_fold(), V5_CLASS.with_rw(1), V5_CLASS.with_nowin(), LoadClass::parse("mx8+sh256x27+state+reg64c+fold+rw").unwrap()];
|
||||
for c in classes {
|
||||
assert!(c.is_v6());
|
||||
for k in 0..64u32 {
|
||||
let seed = f8_bytes("program", k);
|
||||
let p = candidate_class("p-a02", &seed, 0, LoadClass::era(c, &era, &V3_ALLOWED));
|
||||
let mut dims = [false; 5];
|
||||
for ins in &p.instrs {
|
||||
if ins.op == Op::Shfl {
|
||||
dims[ins.mask.trailing_zeros() as usize] = true;
|
||||
}
|
||||
if ins.op == Op::Mad {
|
||||
assert_ne!(ins.src2, ins.dst, "seed {k} class {}: a mad with src2 == dst", c.name());
|
||||
}
|
||||
}
|
||||
assert!(dims.iter().all(|&d| d), "seed {k} class {}: shuffle dimensions {:?}", c.name(), dims);
|
||||
assert_eq!(p.instrs.len(), 64);
|
||||
assert_eq!(p.instrs.iter().filter(|i| i.op.is_load()).count(), 16, "the load slots are the class's");
|
||||
}
|
||||
}
|
||||
assert!(!V5_CLASS.is_v6() && !V3_CLASS.is_v6());
|
||||
// a plain class v5 candidate of the same seed can lack a dimension: the rule is the v6 construction's, not a filter
|
||||
let seed = f8_bytes("program", 4);
|
||||
let a = candidate_class("p-a02", &seed, 0, LoadClass::era(V5_CLASS, &era, &V3_ALLOWED));
|
||||
let b = candidate_class("p-a02", &seed, 0, LoadClass::era(V5_CLASS.with_fold(), &era, &V3_ALLOWED));
|
||||
assert_ne!(a.program_id(), b.program_id());
|
||||
}
|
||||
|
||||
/// Lane D's finding (8 October 2026): every class v6 flag is set aside by the acceptance's class v4 shape predicate,
|
||||
/// so a +nowin, +reg64 or +reg64c program is judged by the full rule (the fresh-source rule, the saturated-source
|
||||
/// check, the index floors and the attempt cap with the last resort), not by the class v2 parts; the draw's source
|
||||
/// rule reads the same shape.
|
||||
#[test]
|
||||
fn every_v6_flag_keeps_the_class_v4_acceptance_shape() {
|
||||
use igneum_pow::accept::is_class_v4_shape;
|
||||
use igneum_pow::generator::max_attempts_for;
|
||||
let all = LoadClass::parse("mx8+sh256x27+state+reg64c+nowin+fold+rw").unwrap();
|
||||
for c in [V5_CLASS, V5_CLASS.with_nowin(), V5_CLASS.with_reg64(), V5_CLASS.with_reg64().with_reg64_chain(), V5_CLASS.with_fold(), all] {
|
||||
assert!(is_class_v4_shape(&c), "{}", c.name());
|
||||
assert!(is_class_v4_shape(&LoadClass::era(c, &f8_bytes("era", 4), &V3_ALLOWED)), "{} under an era", c.name());
|
||||
assert_eq!(max_attempts_for(&c), max_attempts_for(&V5_CLASS), "{}: the same attempt cap and last resort", c.name());
|
||||
}
|
||||
assert!(!is_class_v4_shape(&V3_CLASS) && !is_class_v4_shape(&LoadClass::V2));
|
||||
// lane D's second finding: an era drawn from a width set with more than one width writes the drawn width into
|
||||
// `mix`; the shape (and so the full rule) must hold at every width of the family
|
||||
let wide = LoadClass::era(V5_CLASS, &f8_bytes("era", 4), &[1u8, 4, 16]);
|
||||
assert!(wide.era.is_some());
|
||||
assert!(is_class_v4_shape(&wide), "{} (mix {:?})", wide.name(), wide.mix);
|
||||
assert!(is_class_v4_shape(&LoadClass::era(all, &f8_bytes("era", 9), &[1u8, 4, 16])));
|
||||
}
|
||||
|
||||
/// Review B's F03 (8 October 2026): class v6 is a program class of its own, generator 6: the object class names it,
|
||||
/// every class carrying a v6 flag draws as generator 6 under an era, the pack's program_class reads "v6", and the
|
||||
/// class v5 draw and its generator number do not move.
|
||||
#[test]
|
||||
fn program_class_v6_is_generator_6() {
|
||||
use igneum_pow::generator::{era_generator_of, GENERATOR_VERSION_V5, GENERATOR_VERSION_V6, V6_CLASS};
|
||||
assert_eq!(ProgramClass::parse("v6"), Some(ProgramClass::V6));
|
||||
assert_eq!(ProgramClass::V6.name(), "v6");
|
||||
assert_eq!(ProgramClass::V6.generator_version(), GENERATOR_VERSION_V6);
|
||||
assert_eq!(ProgramClass::from_generator(6), Some(ProgramClass::V6));
|
||||
assert!(ProgramClass::V6.has_state());
|
||||
assert_eq!(ProgramClass::V6.load_class(), V6_CLASS);
|
||||
assert_eq!(V6_CLASS.name(), "mx8+sh256x27+state+reg64c+fold+rw");
|
||||
assert_eq!(ProgramClass::of_load_class(&V6_CLASS), Some(ProgramClass::V6));
|
||||
assert_eq!(ProgramClass::of_load_class(&V6_CLASS.with_nowin()), Some(ProgramClass::V6));
|
||||
assert_eq!(ProgramClass::of_load_class(&V5_CLASS), Some(ProgramClass::V5));
|
||||
let era = f8_bytes("era", 4);
|
||||
for c in [V6_CLASS, V6_CLASS.with_nowin(), V5_CLASS.with_fold(), V5_CLASS.with_rw(1), V5_CLASS.with_nowin()] {
|
||||
assert_eq!(era_generator_of(&LoadClass::era(c, &era, &V3_ALLOWED)), GENERATOR_VERSION_V6, "{}", c.name());
|
||||
}
|
||||
assert_eq!(era_generator_of(&LoadClass::era(V5_CLASS, &era, &V3_ALLOWED)), GENERATOR_VERSION_V5);
|
||||
let seed = f8_bytes("program", 4);
|
||||
let p = candidate_class("p-f03", &seed, 0, LoadClass::era(V6_CLASS, &era, &V3_ALLOWED));
|
||||
assert!(p.class.name().starts_with("mx8-era") && p.class.name().ends_with("+sh256x27+state+reg64c+fold+rw"), "{}", p.class.name());
|
||||
// candidate_class draws without stamping the generator; the era path stamps 6 (era_generator_of above)
|
||||
}
|
||||
|
||||
/// 2. The fold's form: `y = x * M; y ^= y >> 16; y = rotl(y, R)`, and nothing else moves. The plain era's stride is
|
||||
/// `rotl(x * M, R)` byte for byte.
|
||||
#[test]
|
||||
|
|
@ -201,14 +318,18 @@ fn fold_form_and_the_plain_stride_unchanged() {
|
|||
assert!(differ > 9_000, "the fold moved {differ} of 10,000 addresses");
|
||||
// the program of a fold class is the program of the plain class: the same instructions when the same attempt
|
||||
// is accepted (the fold does not enter the draw; the indices it moves can move (c'') and (c''') verdicts)
|
||||
let p = f8_program(4, ProgramClass::V4, false);
|
||||
let f = f8_program(4, ProgramClass::V4, true);
|
||||
if p.attempt == f.attempt {
|
||||
assert_eq!(p.instrs, f.instrs);
|
||||
assert_eq!(p.shadow, f.shadow);
|
||||
}
|
||||
assert_eq!(f.class.name(), format!("{}+fold", p.class.name()));
|
||||
// since the review's draw rules (8 October 2026) a fold class draws under the v6 rules, so the pair is the v6
|
||||
// re-weight class with and without the fold: the same instructions, the fold moving the addresses alone
|
||||
let era = f8_bytes("era", 4);
|
||||
let seed = f8_bytes("program", 4);
|
||||
let p = candidate_class("p4-attack-f8", &seed, 0, LoadClass::era(V5_CLASS.with_rw(1), &era, &V3_ALLOWED));
|
||||
let f = candidate_class("p4-attack-f8", &seed, 0, LoadClass::era(V5_CLASS.with_rw(1).with_fold(), &era, &V3_ALLOWED));
|
||||
assert_eq!(p.instrs, f.instrs);
|
||||
assert_eq!(p.shadow, f.shadow);
|
||||
assert_ne!(p.program_id(), f.program_id());
|
||||
// the name orders the fold inside the re-weight suffix ("...+state+fold+rw"); both spellings parse to the class
|
||||
assert!(f.class.name().ends_with("+sh256x27+state+fold+rw"), "{}", f.class.name());
|
||||
assert_eq!(LoadClass::parse("mx8+sh256x27+state+rw+fold"), Some(V5_CLASS.with_rw(1).with_fold()));
|
||||
// and load_index reads the fold through the era
|
||||
let ins = p.instrs.iter().find(|i| i.op == Op::Load).unwrap();
|
||||
let (pe, fe) = (p.class.era.unwrap(), f.class.era.unwrap());
|
||||
|
|
@ -285,8 +406,11 @@ fn plain_class_draw_is_byte_identical_to_the_pinned_pack() {
|
|||
let e = Epoch { program, dataset };
|
||||
assert_eq!(e.program.program_id(), PINNED_ID);
|
||||
let out = export_pack(&e, &format!("bytes:{DAY_HEX}"), SOURCE);
|
||||
assert_eq!(out.files.len(), 12, "the pack's twelve files");
|
||||
assert_eq!(out.files.len(), 13, "the pack's thirteen files");
|
||||
for (name, text) in &out.files {
|
||||
if name == "identity.json" {
|
||||
continue; // A06's identity file is new beside the pinned twelve
|
||||
}
|
||||
let want = pinned(name);
|
||||
assert!(text == &want, "mx8-devnet-epoch0/{name} differs from the default path's export");
|
||||
}
|
||||
|
|
|
|||
57
proto-cuda/nvrtc/packfile-fuzz.c
Normal file
57
proto-cuda/nvrtc/packfile-fuzz.c
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/* packfile-fuzz.c: POW-01's malformed-input half for the pack reader (8 October 2026). Copies a pack directory, mutates
|
||||
* one of its files N times (a flipped byte, a truncation, an inserted byte, a doubled slice, random bytes) and calls
|
||||
* pf_load on the copy each time; the run passes when every call returns 0 (refused with a message) or 1 (accepted),
|
||||
* and the process never crashes. One line per 1,000 rounds, a RESULT line at the end.
|
||||
* cc -O2 -o packfile-fuzz packfile-fuzz.c && ./packfile-fuzz <pack dir> <rounds> <scratch dir>
|
||||
*/
|
||||
#include "packfile.h"
|
||||
#include <sys/stat.h>
|
||||
|
||||
static uint64_t rs = 0x1234567;
|
||||
static uint64_t rnext(void) { rs += 0x9e3779b97f4a7c15ull; uint64_t z = rs; z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9ull; z = (z ^ (z >> 27)) * 0x94d049bb133111ebull; return z ^ (z >> 31); }
|
||||
static size_t below(size_t n) { return n ? (size_t)(rnext() % n) : 0; }
|
||||
|
||||
static int copy_file(const char* from, const char* to) {
|
||||
size_t n = 0; char* b = pf_read_file(from, &n); if (!b) { remove(to); return 0; }
|
||||
FILE* f = fopen(to, "wb"); if (!f) { free(b); return 0; }
|
||||
fwrite(b, 1, n, f); fclose(f); free(b); return 1;
|
||||
}
|
||||
|
||||
static void write_mutated(const char* src, const char* dst) {
|
||||
size_t n = 0; char* b = pf_read_file(src, &n);
|
||||
if (!b) { remove(dst); return; } /* a pack without this file: the mutation is its absence */
|
||||
size_t cap = n + 400; char* v = (char*)malloc(cap ? cap : 1); size_t len = n; if (n) memcpy(v, b, n);
|
||||
switch (below(6)) {
|
||||
case 0: if (len) v[below(len)] ^= (char)(1 << below(8)); break;
|
||||
case 1: len = below(len + 1); break;
|
||||
case 2: { size_t i = below(len + 1); memmove(v + i + 1, v + i, len - i); v[i] = (char)rnext(); len++; } break;
|
||||
case 3: if (len > 1) { size_t a = below(len), bl = below(len - a); if (bl > 300) bl = 300; memmove(v + a + bl, v + a, len - a); memcpy(v + a, b + a, bl); len += bl; } break;
|
||||
case 4: len = below(300); for (size_t i = 0; i < len; i++) v[i] = (char)rnext(); break;
|
||||
default: if (len) v[below(len)] = (char)rnext(); break;
|
||||
}
|
||||
FILE* f = fopen(dst, "wb"); if (f) { fwrite(v, 1, len, f); fclose(f); }
|
||||
free(v); free(b);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 4) { fprintf(stderr, "usage: packfile-fuzz <pack dir> <rounds> <scratch dir>\n"); return 2; }
|
||||
const char* pack = argv[1]; long rounds = atol(argv[2]); const char* scratch = argv[3];
|
||||
const char* files[] = { "program.h", "vectors.h", "seeds.txt", "memhard.h" };
|
||||
mkdir(scratch, 0755);
|
||||
char from[1024], to[1024];
|
||||
for (int i = 0; i < 4; i++) { snprintf(from, sizeof from, "%s/%s", pack, files[i]); snprintf(to, sizeof to, "%s/%s", scratch, files[i]); copy_file(from, to); }
|
||||
PfPack pk; char err[512];
|
||||
int base = pf_load(pack, &pk, err, sizeof err);
|
||||
printf("base pack %s: %s%s\n", pack, base ? "accepted" : "refused: ", base ? "" : err);
|
||||
long accepted = 0, refused = 0;
|
||||
for (long r = 0; r < rounds; r++) {
|
||||
int which = (int)below(4);
|
||||
for (int i = 0; i < 4; i++) { snprintf(from, sizeof from, "%s/%s", pack, files[i]); snprintf(to, sizeof to, "%s/%s", scratch, files[i]); if (i == which) write_mutated(from, to); else copy_file(from, to); }
|
||||
memset(&pk, 0, sizeof pk); err[0] = 0;
|
||||
int ok = pf_load(scratch, &pk, err, sizeof err);
|
||||
if (ok) accepted++; else refused++;
|
||||
if ((r + 1) % 1000 == 0) printf("round %ld: accepted %ld refused %ld\n", r + 1, accepted, refused);
|
||||
}
|
||||
printf("RESULT packfile-fuzz pack=%s rounds=%ld accepted=%ld refused=%ld crashes=0 verdict=PASS\n", pack, rounds, accepted, refused);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -333,11 +333,13 @@ static int pf_load(const char* dir, PfPack* pk, char* err, size_t cap) {
|
|||
* kernel text, so this loader needs nothing new beyond the number and the class token), generator 5 is class v5
|
||||
* (proof of stored state, 7 October 2026: class v4 over a dataset keyed by the window's state leaves, leaves.bin,
|
||||
* which the host uploads for igneum_build; the kernel text carries the leaf read). */
|
||||
if (pk->generator != 2 && pk->generator != 3 && pk->generator != 4 && pk->generator != 5) {
|
||||
char m[200]; snprintf(m, sizeof(m), "program pack generator %u is not a generator version this worker runs (2, 3, 4 or 5)", (unsigned)pk->generator);
|
||||
if (pk->generator != 2 && pk->generator != 3 && pk->generator != 4 && pk->generator != 5 && pk->generator != 6) {
|
||||
char m[200]; snprintf(m, sizeof(m), "program pack generator %u is not a generator version this worker runs (2, 3, 4, 5 or 6)", (unsigned)pk->generator);
|
||||
free(prog); return pf_fail(err, cap, m);
|
||||
}
|
||||
strcpy(pk->programClass, pk->generator == 5 ? "v5" : pk->generator == 4 ? "v4" : pk->generator == 3 ? "v3" : "v2");
|
||||
/* generator 6 (class v6, the Igneum 2.0 D1 object, 8 October 2026): class v5's stored-state dataset with the v6 rules in the kernel text;
|
||||
* the worker runs it as it runs class v5 (the shadow block, the state leaves), the pack's texts carry the rest */
|
||||
strcpy(pk->programClass, pk->generator == 6 ? "v6" : pk->generator == 5 ? "v5" : pk->generator == 4 ? "v4" : pk->generator == 3 ? "v3" : "v2");
|
||||
{
|
||||
/* Counter ASIC 3.0 (6 October 2026): the shadow block marks class v4. A generator 3 pack with IGNEUM_SHADOW_INSTRS
|
||||
* is a v4 program stamped as v3 (the old export path; it carried the v3 control's program id) and is refused;
|
||||
|
|
@ -345,7 +347,7 @@ static int pf_load(const char* dir, PfPack* pk, char* err, size_t cap) {
|
|||
uint32_t shadow = 0;
|
||||
if (!pf_define_u32(prog, "IGNEUM_SHADOW_INSTRS", &shadow)) shadow = 0;
|
||||
if (pk->generator == 4 && shadow == 0) { free(prog); return pf_fail(err, cap, "program pack generator 4 (class v4) without IGNEUM_SHADOW_INSTRS: not a class v4 pack"); }
|
||||
if (pk->generator == 5 && shadow == 0) { free(prog); return pf_fail(err, cap, "program pack generator 5 (class v5) without IGNEUM_SHADOW_INSTRS: not a class v5 pack (class v5 is class v4 over the state leaves)"); }
|
||||
if ((pk->generator == 5 || pk->generator == 6) && shadow == 0) { free(prog); return pf_fail(err, cap, "program pack generator 5 (class v5) without IGNEUM_SHADOW_INSTRS: not a class v5 pack (class v5 is class v4 over the state leaves)"); }
|
||||
if (pk->generator == 3 && shadow != 0) { /* a generator 2 pack with a shadow is the measurement ladder (a class-bearing id) and loads */
|
||||
char m[220]; snprintf(m, sizeof(m), "program pack generator %u with a shadow block (IGNEUM_SHADOW_INSTRS %u): a class v4 program is generator 4 (export the pack as class v4)", (unsigned)pk->generator, (unsigned)shadow);
|
||||
free(prog); return pf_fail(err, cap, m);
|
||||
|
|
@ -365,8 +367,8 @@ static int pf_load(const char* dir, PfPack* pk, char* err, size_t cap) {
|
|||
pk->stateLeaves = 0; pk->stateLeavesFnv = 0; pk->stateRootHex[0] = 0; pk->stateBlockHex[0] = 0;
|
||||
strcpy(pk->stateLeavesFile, "leaves.bin");
|
||||
if (!pf_define_u32(prog, "IGNEUM_STATE_LEAVES", &pk->stateLeaves)) pk->stateLeaves = 0;
|
||||
if (pk->generator == 5 && pk->stateLeaves == 0) { free(prog); return pf_fail(err, cap, "program pack generator 5 (class v5) without IGNEUM_STATE_LEAVES: no state leaves to key the dataset (export the pack with --state)"); }
|
||||
if (pk->generator != 5 && pk->stateLeaves != 0) {
|
||||
if ((pk->generator == 5 || pk->generator == 6) && pk->stateLeaves == 0) { free(prog); return pf_fail(err, cap, "program pack generator 5 (class v5) without IGNEUM_STATE_LEAVES: no state leaves to key the dataset (export the pack with --state)"); }
|
||||
if (pk->generator != 5 && pk->generator != 6 && pk->stateLeaves != 0) {
|
||||
char m[200]; snprintf(m, sizeof(m), "program pack generator %u carries IGNEUM_STATE_LEAVES %u: state leaves belong to class v5 (generator 5)", (unsigned)pk->generator, (unsigned)pk->stateLeaves);
|
||||
free(prog); return pf_fail(err, cap, m);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2992,9 +2992,10 @@ final class ServeDataset {
|
|||
/// Counter ASIC 2.0 and 3.0: the classes this worker runs from a prepared pack only (never from the Swift version 2
|
||||
/// generator): class v3 (generator 3) and class v4 (generator 4, class v3 plus the latency-shadow block, which is in
|
||||
/// the pack's own program_bound.metal). Each carries an era seed.
|
||||
func isPackClass(_ cls: String) -> Bool { cls == "v3" || cls == "v4" || cls == "v5" } // class v5 (7 October 2026): class v4 over the state leaves, from a pack
|
||||
func isPackClass(_ cls: String) -> Bool { cls == "v3" || cls == "v4" || cls == "v5" || cls == "v6" } // class v5 (7 October 2026): class v4 over the state leaves, from a pack; class v6 (8 October 2026): class v5's stored-state dataset with the v6 rules in the kernel text (packfile.h db63e1e36: run as class v5)
|
||||
/// The class name of a pack's IGNEUM_GENERATOR (packfile.h's rule).
|
||||
func packClassOf(generator: UInt32) -> String { generator == 5 ? "v5" : generator == 4 ? "v4" : generator == 3 ? "v3" : "v2" }
|
||||
func packClassOf(generator: UInt32) -> String { generator == 6 ? "v6" : generator == 5 ? "v5" : generator == 4 ? "v4" : generator == 3 ? "v3" : "v2" }
|
||||
func isStateClass(_ cls: String) -> Bool { cls == "v5" || cls == "v6" } // the classes keyed by the state leaves
|
||||
|
||||
// The resident programs and datasets, shared by the job loop (main thread) and the prepare queue (background).
|
||||
final class ServeStore {
|
||||
|
|
@ -3085,7 +3086,7 @@ func servePackProgram(_ gpu: GPU, _ store: ServeStore, seedHex: String, seed: [U
|
|||
}
|
||||
func refuse(_ why: String) -> NSError { NSError(domain: "pack", code: 2, userInfo: [NSLocalizedDescriptionKey: "pack \(dir): \(why)"]) }
|
||||
let generator = defineU32("IGNEUM_GENERATOR") ?? 1
|
||||
guard generator == 2 || generator == 3 || generator == 4 || generator == 5 else { throw refuse("program pack generator \(generator) is not a generator version this worker runs (2, 3, 4 or 5)") }
|
||||
guard generator == 2 || generator == 3 || generator == 4 || generator == 5 || generator == 6 else { throw refuse("program pack generator \(generator) is not a generator version this worker runs (2, 3, 4, 5 or 6)") }
|
||||
let packClass = packClassOf(generator: generator)
|
||||
if let named = defineStr("IGNEUM_PROGRAM_CLASS"), named != packClass { throw refuse("program pack IGNEUM_PROGRAM_CLASS \"\(named)\" does not match IGNEUM_GENERATOR \(generator)") }
|
||||
// Counter ASIC 3.0 (6 October 2026): the shadow block marks class v4 (packfile.h's rule): a generator 3 pack with
|
||||
|
|
@ -3093,9 +3094,9 @@ func servePackProgram(_ gpu: GPU, _ store: ServeStore, seedHex: String, seed: [U
|
|||
let shadow = defineU32("IGNEUM_SHADOW_INSTRS") ?? 0
|
||||
if generator == 4 && shadow == 0 { throw refuse("program pack generator 4 (class v4) without IGNEUM_SHADOW_INSTRS: not a class v4 pack") }
|
||||
// class v5 (7 October 2026) is class v4 over the state leaves: the shadow block and IGNEUM_STATE_LEAVES both mark it
|
||||
if generator == 5 && shadow == 0 { throw refuse("program pack generator 5 (class v5) without IGNEUM_SHADOW_INSTRS: not a class v5 pack") }
|
||||
if generator == 5 && (defineU32("IGNEUM_STATE_LEAVES") ?? 0) == 0 { throw refuse("program pack generator 5 (class v5) without IGNEUM_STATE_LEAVES: no state leaves to key the dataset (export the pack with --state)") }
|
||||
if generator != 5 && (defineU32("IGNEUM_STATE_LEAVES") ?? 0) != 0 { throw refuse("program pack generator \(generator) carries IGNEUM_STATE_LEAVES: state leaves belong to class v5 (generator 5)") }
|
||||
if (generator == 5 || generator == 6) && shadow == 0 { throw refuse("program pack generator 5 (class v5) without IGNEUM_SHADOW_INSTRS: not a class v5 pack") }
|
||||
if (generator == 5 || generator == 6) && (defineU32("IGNEUM_STATE_LEAVES") ?? 0) == 0 { throw refuse("program pack generator 5 (class v5) without IGNEUM_STATE_LEAVES: no state leaves to key the dataset (export the pack with --state)") }
|
||||
if generator != 5 && generator != 6 && (defineU32("IGNEUM_STATE_LEAVES") ?? 0) != 0 { throw refuse("program pack generator \(generator) carries IGNEUM_STATE_LEAVES: state leaves belong to class v5 (generator 5)") }
|
||||
if generator == 3 && shadow != 0 { throw refuse("program pack generator \(generator) with a shadow block (IGNEUM_SHADOW_INSTRS \(shadow)): a class v4 program is generator 4 (export the pack as class v4)") }
|
||||
let eraHex = defineStr("IGNEUM_ERA_SEED_HEX") ?? ""
|
||||
if wantClass != "" && wantClass != packClass { throw refuse("program class mismatch: this pack is class \(packClass), the line names class \(wantClass) (export the pack again)") }
|
||||
|
|
@ -3162,7 +3163,9 @@ func servePackDataset(_ gpu: GPU, _ store: ServeStore, dayHex: String, dir: Stri
|
|||
guard let fillFn = lib.makeFunction(name: "igneum_cache_fill"), let buildFn = lib.makeFunction(name: "igneum_build") else { throw refuse("memhard.metal lacks igneum_cache_fill or igneum_build") }
|
||||
let fillPipe = try gpu.device.makeComputePipelineState(function: fillFn)
|
||||
let buildPipe = try gpu.device.makeComputePipelineState(function: buildFn)
|
||||
let words = 1 << datasetLog2
|
||||
// ds55 (the class v6 research packs, 8 October 2026): a non-power-of-two dataset carries IGNEUM_DATASET_WORDS (the
|
||||
// buffer's size) and IGNEUM_DATASET_ITEMS beside IGNEUM_DATASET_LOG2 (the floor); the kernel text maps (src * words) >> 32
|
||||
let words = defineU64("IGNEUM_DATASET_WORDS").map { Int($0) } ?? (1 << datasetLog2)
|
||||
// Class v5 (docs/design/class-v5-stored-state.md, 7 October 2026): the window's state leaves, leaves.bin beside program.h
|
||||
// (IGNEUM_STATE_LEAVES leaves of 16 little-endian words), checked against the pack's count and FNV-1a 64
|
||||
// (IGNEUM_STATE_LEAVES_FNV64) and bound as buffer 2 of igneum_build with the count in buffer 3 (packbench.swift's shape,
|
||||
|
|
@ -3173,7 +3176,7 @@ func servePackDataset(_ gpu: GPU, _ store: ServeStore, dayHex: String, dir: Stri
|
|||
return UInt64(programH[Range(m.range(at: 1), in: programH)!], radix: 16)
|
||||
}
|
||||
let stateLeaves = defineU32("IGNEUM_STATE_LEAVES") ?? 0
|
||||
if (packClass == "v5") != (stateLeaves > 0) { throw refuse(packClass == "v5" ? "class v5 pack without IGNEUM_STATE_LEAVES" : "a class \(packClass) pack carries IGNEUM_STATE_LEAVES \(stateLeaves): state leaves belong to class v5") }
|
||||
if isStateClass(packClass) != (stateLeaves > 0) { throw refuse(isStateClass(packClass) ? "class v5 pack without IGNEUM_STATE_LEAVES" : "a class \(packClass) pack carries IGNEUM_STATE_LEAVES \(stateLeaves): state leaves belong to class v5") }
|
||||
var leavesBuf: MTLBuffer? = nil
|
||||
var nLeaves: UInt32 = 0
|
||||
if stateLeaves > 0 {
|
||||
|
|
|
|||
|
|
@ -93,8 +93,12 @@ let cacheFnvWant = hex64(vj["cache_fnv1a64"] as! String)
|
|||
let hotFnvWant: UInt64? = (vj["hot_fnv1a64"] as? String).map(hex64)
|
||||
|
||||
guard let device = MTLCreateSystemDefaultDevice(), let queue = device.makeCommandQueue() else { fail("no Metal device") }
|
||||
let words = 1 << datasetLog2
|
||||
let mask = UInt32(words - 1)
|
||||
// ds55 (the class v6 research packs, 8 October 2026): a non-power-of-two dataset carries IGNEUM_DATASET_WORDS and
|
||||
// IGNEUM_DATASET_ITEMS beside IGNEUM_DATASET_LOG2 (the floor); the buffer is sized by the words, the items by ITEMS,
|
||||
// and the kernel text maps an address as (src * words) >> 32, so the mask is the kernel's business, not this host's
|
||||
let datasetWordsDefine = defineU64("IGNEUM_DATASET_WORDS")
|
||||
let words = datasetWordsDefine.map { Int($0) } ?? (1 << datasetLog2)
|
||||
let mask = UInt32(truncatingIfNeeded: (1 << datasetLog2) - 1)
|
||||
let cacheWords = 1 << cacheLog2
|
||||
|
||||
func compile(_ file: String) -> MTLLibrary {
|
||||
|
|
@ -138,7 +142,7 @@ func fnv1a64(_ p: UnsafeRawPointer, _ n: Int) -> UInt64 {
|
|||
for i in 0..<n { h ^= UInt64(b[i]); h = h &* 0x100000001b3 }
|
||||
return h
|
||||
}
|
||||
let items = words / 16
|
||||
let items = defineU64("IGNEUM_DATASET_ITEMS").map { Int($0) } ?? (words / 16)
|
||||
// class v5 (docs/design/class-v5-stored-state.md): a pack with IGNEUM_STATE_LEAVES carries leaves.bin (16 words per leaf), bound
|
||||
// as buffer 2 of igneum_build with the count in buffer 3; the pack's FNV of the leaves is checked first
|
||||
var leavesBuf: MTLBuffer? = nil
|
||||
|
|
|
|||
17
tools/ci/batches/kit-class-v6-20261008-01.json
Normal file
17
tools/ci/batches/kit-class-v6-20261008-01.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"run_id": "kit-class-v6-20261008-01",
|
||||
"manifest_sha": "ef0f2ed8",
|
||||
"evidence_dir": "docs/design/class-v5-stored-state.md",
|
||||
"boxes": [
|
||||
"build-1"
|
||||
],
|
||||
"cells": [
|
||||
{
|
||||
"cell": "kit:class-v6-fingerprints",
|
||||
"status": "RUNNING",
|
||||
"evidence": "docs/design/class-v5-stored-state.md; build-1:/srv/artefacts/packs/packs-class-v6-20261008T202808Z.zip; build-1:/srv/builds/_log/v5-class/kits-20261008T202808Z/emu-check.log",
|
||||
"note": "docs/design/class-v5-stored-state.md section 0, the class v6 kit row: the generator 6 packs hl-v6-all 4de7b836cc40a4ea and hl-v6-foldrw d7eba30115d26dd4 read e8f4f3289c6ee1fc and 6ce3dc344a613500 on the CPU emulation check, the CUDA 4090, Metal and Apple OpenCL (20:41 to 20:52 UK); the RTX 5090, RX 7600 and Arc rows the morning's; the kit zip 4 bb66a546 (kernel texts byte-identical to zip 3)"
|
||||
}
|
||||
],
|
||||
"method": "GPU"
|
||||
}
|
||||
|
|
@ -698,6 +698,20 @@
|
|||
"coverage": {
|
||||
"ADV-06": "partial: the placed gated cores and the adversary's forms are modelled in shadow-k.md; the independent review remains"
|
||||
}
|
||||
},
|
||||
"kit:class-v6-fingerprints": {
|
||||
"command": "KITS_BOX_SCRIPT=kits-v6-on-box.sh tools/class-v5/kits-remote.sh --box 1 (the kit zip with its emulation check), then the kit's workers on every platform: the box's CPU emulation (--check, 96 of 96 vector lanes), the fleet's CUDA 4090 (--check and --bench at 2^24, base nonce 0), the Mac's Metal (proto-metal/packbench.swift --pack) and Apple OpenCL (proto-opencl/host.c --pack --bench-pack), PC 1's RTX 5090 and RX 7600 (tools/class-v5/pc1-v6-bench.ps1), PC 2's Arc B580 (tools/class-v5/arc-v6-bench.ps1); the fingerprint of the 2^24 outputs at base nonce 0 equal on every platform for the all pack and for its known-failed partner, the two distinct",
|
||||
"box_class": "build box + pods + Mac + PC 1 + PC 2",
|
||||
"fixtures": [
|
||||
"F0",
|
||||
"F2"
|
||||
],
|
||||
"cases": [
|
||||
"R2-F03-R02"
|
||||
],
|
||||
"coverage": {
|
||||
"R2-F03-R02": "partial: the worker half (CPU reference, CUDA, Metal, OpenCL) on the same program packs; the node's and the pool's accepted work on the same job context are the CI steward's and the pool lane's cells; PASS only when every listed platform reads one fingerprint and the node and pool halves are green"
|
||||
}
|
||||
}
|
||||
},
|
||||
"not_run": {
|
||||
|
|
|
|||
103
tools/class-v5/arc-v6-bench.ps1
Normal file
103
tools/class-v5/arc-v6-bench.ps1
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# Class v6 kit (8 October 2026, the coordinator's order): the arc job of the v5 script with the class v6 packs: hl-v6-foldrw (the
|
||||
# known-failed partner; expected fingerprint 6ce3dc344a613500, the Mac's Metal and Apple OpenCL reading) and hl-v6-all (the
|
||||
# all-together pack, id 9d40978601a7df2a; with the second zip its OpenCL text is the window's real text and the expected
|
||||
# fingerprint is the CUDA reference e8f4f3289c6ee1fc; the first zip's stub refused it, that refusal line being that run's record).
|
||||
# Kit fetch job fetch-class-v6-kit-20261008-g6 = the third zip packs-class-v6-20261008T194045Z.zip (735e1411...), generator 6 packs; the
|
||||
# earlier dir fetch-class-v6-kit-20261008 holds zip 2 (generator 5) and stays as the generator 5 record's kit.
|
||||
# Class v5 kit bench on the Intel Arc B580 (7 October 2026, 23:5x UK, main's word: the B580 is the eGPU of the second PC,
|
||||
# so the Intel fingerprint runs there through the hash lane's job queue as a lock-free, non-elevated, quiet job, never beside
|
||||
# an install or a build; the page names no PC): tools/class-v5/pc1-amd-v5-bench.ps1 with the device
|
||||
# match on the Intel OpenCL platform (Arc / B580) instead of gfx1201; everything else identical (the v5-dn3-epoch0 pack,
|
||||
# the v4-genesis control, RESULT lines, SUMMARY {json}). Expected fingerprint 6ce3dc344a613500.
|
||||
# Class v5 kit bench on PC 1's RX 9070 XT (7 October 2026, docs/design/class-v5-stored-state.md section 13, "the kit:
|
||||
# OpenCL (AMD)"): the kit's igneum-worker-opencl.exe (THIS tree's proto-opencl/host.c with the class v5 leaf upload:
|
||||
# igneum_build(ds, cache, leaves, nLeaves, nItems), the leaves of leaves.bin checked against the pack's FNV-1a 64 first)
|
||||
# runs --bench-pack on the first class v5 pack, proto-cuda/packs-ca3-v5/v5-dn3-epoch0 (program id e5a4ac5978462156, 11
|
||||
# leaves under state root 7e37a9fb..., cache FNV 7334fa46e5d972eb), on the gfx1201 device. Expected: the self-test
|
||||
# PASS line (cache head, last line and FNV; dataset head, word [268435455] and 64 samples; 96 of 96 vector lanes) and the
|
||||
# fingerprint of the 2^24 outputs at base nonce 0 equal to the Metal reading, 6ce3dc344a613500 (the M5 Max, 7 October
|
||||
# 2026, 18:07:09Z). The v4-genesis control pack runs after it so the run has a known class v4 row beside the v5 row.
|
||||
# Beside the miners (a correctness and fingerprint gate; the rate row is labelled loaded when the card mines): no card is
|
||||
# switched, nothing is posted to the installed app, nothing is built on the PC. Published by the Counter ASIC coordinator
|
||||
# only (the PC 1 queue is its); the kit is fetch job $kitId. Every result line starts with RESULT; SUMMARY {json} ends it.
|
||||
# Post-review packs (8 October 2026, 20:4x UK, the third kit zip packs-class-v6-20261008T194045Z.zip, sha256 735e1411...): the packs are
|
||||
# generator 6 (class v6; ids hl-v6-all 4de7b836cc40a4ea, hl-v6-foldrw d7eba30115d26dd4) and the expected fingerprints are the
|
||||
# Mac's Metal and Apple OpenCL reading of them (all pack e8f4f3289c6ee1fc, partner 6ce3dc344a613500); the generator 5 zips' values
|
||||
# (59e6708e46f1e87c and 5a6ad122a71a888f) belong to the superseded packs and no longer match by construction.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$jobName = 'arc-v6-bench'
|
||||
$kitId = $env:IGNEUM_V5_KIT_ID; if (-not $kitId) { $kitId = 'fetch-class-v6-kit-20261008-g6' } # a fresh dir for the generator 6 zip: a fetch into the old dir left zip 2's files in place (PC 2, 20:17 UK)
|
||||
$expected = '6ce3dc344a613500'
|
||||
$started = Get-Date
|
||||
function Stamp { (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') }
|
||||
function Summary([string] $status, [hashtable] $extra) {
|
||||
$o = [ordered]@{ job = $jobName; status = $status; duration_s = [int]((Get-Date) - $started).TotalSeconds; finished_at = (Stamp) }
|
||||
foreach ($k in $extra.Keys) { $o[$k] = $extra[$k] }
|
||||
'SUMMARY ' + ($o | ConvertTo-Json -Compress -Depth 4)
|
||||
}
|
||||
"RESULT start $(Stamp) job=$jobName machine=$env:COMPUTERNAME app_version=$env:IGNEUM_APP_VERSION expected_fingerprint=$expected"
|
||||
$jobs = Split-Path $env:IGNEUM_JOB_DIR
|
||||
$kit = Join-Path $jobs $kitId
|
||||
if (-not (Test-Path $kit)) { "RESULT error kit missing at $kit (the fetch job $kitId runs first; republish it after any app update)"; Summary 'failed' @{ error = 'kit missing' }; exit 2 }
|
||||
$exe = Join-Path $kit 'bin\windows\igneum-worker-opencl.exe'
|
||||
$packs = Join-Path $kit 'packs'
|
||||
if (-not (Test-Path $exe)) { "RESULT error worker missing at $exe"; Summary 'failed' @{ error = 'worker missing' }; exit 2 }
|
||||
"RESULT worker kit $exe sha256 $((Get-FileHash -Algorithm SHA256 $exe).Hash.ToLower()) bytes $((Get-Item $exe).Length)"
|
||||
foreach ($pk in @('hl-v6-foldrw', 'hl-v6-all')) {
|
||||
$d = Join-Path $packs $pk
|
||||
if (-not (Test-Path (Join-Path $d 'kernel_bound.cl'))) { "RESULT error pack $pk missing at $d"; Summary 'failed' @{ error = "pack $pk missing" }; exit 2 }
|
||||
"RESULT pack $pk kernel_bound.cl sha256 $((Get-FileHash -Algorithm SHA256 (Join-Path $d 'kernel_bound.cl')).Hash.ToLower()) program.h sha256 $((Get-FileHash -Algorithm SHA256 (Join-Path $d 'program.h')).Hash.ToLower())"
|
||||
}
|
||||
# the class v6 kit's packs each carry their own leaves.bin (the class v5 script checked one pack by name; the 18:01 UK run on
|
||||
# PC 1 stopped on that name before the device list)
|
||||
foreach ($lp in @('hl-v6-foldrw', 'hl-v6-all')) {
|
||||
$leaves = Join-Path $packs ($lp + '\leaves.bin')
|
||||
if (-not (Test-Path $leaves)) { "RESULT error leaves.bin missing at $leaves (a class v5 pack without its leaves builds nothing)"; Summary 'failed' @{ error = 'leaves missing'; pack = $lp }; exit 2 }
|
||||
"RESULT leaves $lp $((Get-Item $leaves).Length) bytes sha256 $((Get-FileHash -Algorithm SHA256 $leaves).Hash.ToLower())"
|
||||
}
|
||||
# the OpenCL device index of the 9070 XT (the installed worker's list when present: the app's own indices)
|
||||
$inst = @("$env:LOCALAPPDATA\Programs\Igneum Miner", "$env:ProgramFiles\Igneum Miner") | Where-Object { Test-Path (Join-Path $_ 'igneum-app.exe') } | Select-Object -First 1
|
||||
$listExe = $exe
|
||||
if ($inst -and (Test-Path (Join-Path $inst 'igneum-worker-opencl.exe'))) { $listExe = Join-Path $inst 'igneum-worker-opencl.exe' }
|
||||
$list = @(& $listExe --list 2>&1 | ForEach-Object { "$_" })
|
||||
$list | ForEach-Object { "RESULT list $_" }
|
||||
$dev = $null
|
||||
foreach ($l in $list) { if ($l -match '^\s*\[(\d+)\].*(Intel|Arc|B580)' -and $l -notmatch 'dup') { $dev = [int]$Matches[1]; break } }
|
||||
if ($null -eq $dev) { "RESULT error no Intel Arc device in --list (the B580 is off the bus or has no OpenCL runtime: the Intel v5 row stays OWED)"; Summary 'failed' @{ error = 'no intel arc' }; exit 2 }
|
||||
"RESULT device $dev Intel Arc (list from $listExe)"
|
||||
function Workers { @(Get-CimInstance Win32_Process -Filter "Name = 'igneum-worker-opencl.exe' OR Name = 'igneum-worker-cuda.exe'" -ErrorAction SilentlyContinue | ForEach-Object { "$($_.Name):$($_.ProcessId):[$($_.CommandLine -replace '\s+', ' ')]" }) }
|
||||
$w = @(Workers)
|
||||
$loaded = ($w | Where-Object { $_ -match "igneum-worker-opencl.*--device\s+$dev(\s|$)" }).Count -gt 0
|
||||
"RESULT workers_before $(Stamp) $($w -join ' ')"
|
||||
$state = if ($loaded) { 'loaded' } else { 'quiet' }
|
||||
"RESULT context card_state=$state (a fingerprint gate: the load changes the rate row, never the bytes)"
|
||||
$rows = @{}
|
||||
$fpOk = $false
|
||||
foreach ($pk in @('hl-v6-foldrw', 'hl-v6-all')) {
|
||||
$d = Join-Path $packs $pk
|
||||
$t0 = Get-Date
|
||||
"RESULT bench $pk start $(Stamp) cmd=igneum-worker-opencl.exe --bench-pack --pack $d --device $dev --batch-log2 24 --batches 5"
|
||||
$out = @(& $exe --bench-pack --pack $d --device $dev --batch-log2 24 --batches 5 2>&1 | ForEach-Object { "$_" })
|
||||
$code = $LASTEXITCODE
|
||||
$secs = [int]((Get-Date) - $t0).TotalSeconds
|
||||
# the whole stdout, line by line (the 09:36 UK run kept one line per pack and lost the host's exchange, kernel,
|
||||
# rotate-fold and build-log lines, which the diagnosis needed)
|
||||
foreach ($l in $out) { "RESULT bench $pk out $l" }
|
||||
$res = $out | Where-Object { $_ -match '^RESULT ' } | Select-Object -Last 1
|
||||
$fp = ''; $mhs = ''; $check = ''
|
||||
if ($res -match 'fingerprint=([0-9a-f]{16})') { $fp = $Matches[1] }
|
||||
if ($res -match 'mhs=([0-9.]+)') { $mhs = $Matches[1] }
|
||||
if ($res -match 'check=(\w+)') { $check = $Matches[1] }
|
||||
$rows[$pk] = [ordered]@{ exit = $code; seconds = $secs; fingerprint = $fp; mhs = $mhs; check = $check; card_state = $state }
|
||||
if ($pk -eq 'hl-v6-foldrw') {
|
||||
$fpOk = ($fp -eq $expected -and $check -eq 'PASS')
|
||||
"RESULT partner fingerprint=$fp expected=$expected match=$fpOk check=$check mhs=$mhs card_state=$state exit=$code seconds=$secs $(Stamp)"
|
||||
} else {
|
||||
# the all pack: with the window's OpenCL text (the second zip) a fingerprint, expected the CUDA reference e8f4f3289c6ee1fc
|
||||
$allOk = ($fp -eq 'e8f4f3289c6ee1fc' -and $check -eq 'PASS')
|
||||
"RESULT all-pack fingerprint=$fp expected=e8f4f3289c6ee1fc match=$allOk check=$check mhs=$mhs card_state=$state exit=$code seconds=$secs $(Stamp)"
|
||||
}
|
||||
}
|
||||
"RESULT workers_after $(Stamp) $((@(Workers)) -join ' ')"
|
||||
Summary $(if ($fpOk) { 'done' } else { 'failed' }) @{ expected = $expected; partner = $rows['hl-v6-foldrw']; all_pack = $rows['hl-v6-all']; device = $dev; kit = $kitId }
|
||||
exit $(if ($fpOk) { 0 } else { 1 })
|
||||
|
|
@ -13,9 +13,9 @@
|
|||
#
|
||||
# Takes one of the box's build slots through infra/build-server/remote-run.sh (bs_remote_run), box 1 by default (the
|
||||
# build-server lane's word of 7 October 2026, 19:4x BST: the kit builds on build-1 explicitly). The work itself is
|
||||
# tools/class-v5/kits-on-box.sh, a script file run by path on the box (the inline-rm rule of 21:33 BST); when the
|
||||
# tools/class-v5/kits-on-box.sh (or KITS_BOX_SCRIPT=kits-v6-on-box.sh for the class v6 kit), a script file run by path on the box (the inline-rm rule of 21:33 BST); when the
|
||||
# build-server lane's lease tool is in place, the same file goes through `/srv/builds/_bin/lease pool <threads> -- bash
|
||||
# tools/class-v5/kits-on-box.sh ...` with the label "v5 kit".
|
||||
# tools/class-v5/${KITS_BOX_SCRIPT:-kits-on-box.sh} ...` with the label "v5 kit".
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BS_TOOL=class-v5-kits
|
||||
|
|
@ -34,9 +34,9 @@ ZIP="/srv/artefacts/packs/packs-ca3-v5-$STAMP.zip"
|
|||
bs_toolchain_check
|
||||
bs_sync_sources
|
||||
bs_log "sources at $WT (commit $BS_SHA on $BS_BRANCH); building the class v5 kits on box $BOX"
|
||||
# the body runs from a script FILE at the worktree's mirror checkout (tools/class-v5/kits-on-box.sh, carried by the commit), never
|
||||
# the body runs from a script FILE at the worktree's mirror checkout (tools/class-v5/${KITS_BOX_SCRIPT:-kits-on-box.sh}, carried by the commit), never
|
||||
# an inline string: main's rule of 7 October 2026, 21:33 BST (no rm, find -delete or truncation inside a bash -c string)
|
||||
CMD="cd '$WT' && bash tools/class-v5/kits-on-box.sh '$STAMP' '$SKIP_EMU'"
|
||||
CMD="cd '$WT' && bash tools/class-v5/${KITS_BOX_SCRIPT:-kits-on-box.sh} '$STAMP' '$SKIP_EMU'"
|
||||
set +e
|
||||
BR_KIND=build BR_COMMAND="class v5 kits (packfile-test, emu --check v5, Linux and Windows workers, kit zip)" BR_TARGET="x86_64-linux+windows" BR_ARTEFACTS="" \
|
||||
bs_remote_run "$WT" "$BS_WT class-v5 kits" "$CMD" 2>&1 | tee "/tmp/v5-kits-$STAMP.log" | grep -E '^(STEP|FAIL|KIT|BIN|LOGS|check PASS|self-test|packfile-test|emu:| v5|build-remote: RESULT)'
|
||||
|
|
|
|||
98
tools/class-v5/kits-v6-on-box.sh
Executable file
98
tools/class-v5/kits-v6-on-box.sh
Executable file
|
|
@ -0,0 +1,98 @@
|
|||
#!/usr/bin/env bash
|
||||
# The class v5 kit build as it runs ON igneum-build-1 (called by tools/class-v5/kits-remote.sh through the build-server
|
||||
# library's remote runner, from the worktree's mirror checkout, so this file travels with the commit). A script file by
|
||||
# path, never an inline string: main's rule of 7 October 2026, 21:33 BST (no rm, find -delete or truncation inside a
|
||||
# `bash -c` string; tools/ci/inline-rm-check.sh). What it does, in order: the pack loader test with the class v5 cases,
|
||||
# the Linux and Windows builds of the two one-click workers, the CPU emulation's --check on the class v5 pack, then the
|
||||
# kit zip at /srv/artefacts/packs/packs-ca3-v5-<stamp>.zip with SHA256SUMS. Logs land in /srv/builds/_log/v5-class/kits-<stamp>.
|
||||
# bash tools/class-v5/kits-on-box.sh <stamp> <skip-emu 0|1> (cwd = the worktree root on the box)
|
||||
set -u
|
||||
STAMP="${1:?stamp}"; SKIP_EMU="${2:-0}"
|
||||
ZIP="/srv/artefacts/packs/packs-class-v6-$STAMP.zip"
|
||||
LOGDIR="/srv/builds/_log/v5-class/kits-$STAMP"
|
||||
T=$(mktemp -d /tmp/v5-kits.XXXXXX); S=$T/stage; mkdir -p "$S/bin/linux" "$S/bin/windows" "$S/src" "$S/packs" "$S/tools" "$LOGDIR"
|
||||
rc=0
|
||||
step() { echo "STEP $1 $(date -u +%H:%M:%SZ)"; }
|
||||
fail() { echo "FAIL $1"; rc=1; }
|
||||
finish() { cp "$T"/*.log "$LOGDIR"/ 2>/dev/null; echo "LOGS $LOGDIR"; rm -rf "$T"; }
|
||||
# class v6 (8 October 2026, the coordinator's order): the all-together pack and its known-failed partner come from the hash
|
||||
# lane's tgz files on build-1 (V6_ALL and V6_PARTNER, default hl-v6-all and hl-v6-foldrw), extracted beside the stage;
|
||||
# the emulation check runs pack A = the all pack against pack B = the partner, and the two ids must differ
|
||||
V6_ALL="${V6_ALL:-hl-v6-all}"; V6_PARTNER="${V6_PARTNER:-hl-v6-foldrw}"
|
||||
PK=$T/v6packs; mkdir -p "$PK"
|
||||
for p in "$V6_ALL" "$V6_PARTNER"; do tar -xzf "/srv/artefacts/packs/$p.tgz" -C "$PK" 2>/dev/null || { echo "FAIL no pack tgz /srv/artefacts/packs/$p.tgz"; exit 1; }; done
|
||||
rm -rf "$PK"/._* "$PK"/*/._* 2>/dev/null
|
||||
V5="$PK/$V6_ALL"; V6P="$PK/$V6_PARTNER"
|
||||
echo "PACKS $V6_ALL id $(grep -oE 'IGNEUM_PROGRAM_ID 0x[0-9a-f]+' "$V5/program.h" | awk '{print $2}') | $V6_PARTNER id $(grep -oE 'IGNEUM_PROGRAM_ID 0x[0-9a-f]+' "$V6P/program.h" | awk '{print $2}')"
|
||||
[ "$(grep -oE 'IGNEUM_PROGRAM_ID 0x[0-9a-f]+' "$V5/program.h")" != "$(grep -oE 'IGNEUM_PROGRAM_ID 0x[0-9a-f]+' "$V6P/program.h")" ] || { echo "FAIL the all pack and its partner carry one id"; exit 1; }
|
||||
# the test's third argument is its class v5 known-good (the checked-in Devnet 3 epoch 0 pack, generator 5, 11 leaves); the kit's all
|
||||
# pack is generator 6 since the post-review export (class v6, 93 leaves) and fails that check by construction (the third kit build,
|
||||
# 20:38 UK), so the checked-in pack is named and the v6 packs are read by the emulation check below
|
||||
step packfile-test
|
||||
cc -std=c99 -Wall -Wextra -Wno-unused-function -O1 -o "$T/packfile-test" proto-cuda/nvrtc/emu/packfile-test.c && "$T/packfile-test" proto-cuda/packs/igneum-devnet-v4-epoch0 proto-cuda/packs-ca3-v5/v5-dn3-epoch0 > "$T/packfile-test.log" 2>&1 || fail "packfile-test ($(tail -1 "$T/packfile-test.log"))"
|
||||
grep -E '^(FAIL| v5 pack)' "$T/packfile-test.log"; grep -c '^ok' "$T/packfile-test.log" | sed 's/^/packfile-test ok lines /'
|
||||
step linux-opencl-worker
|
||||
gcc -std=c99 -O2 -Wall -Wextra -Wno-stringop-truncation -Wno-format-truncation -DIGNEUM_CL_DYNAMIC -DCL_TARGET_OPENCL_VERSION=120 -I proto-cuda/packs/igneum-devnet-v4-epoch0 -DIGNEUM_KERNEL_PATH='"kernel_bound.cl"' -o "$S/bin/linux/igneum-worker-opencl" proto-opencl/host.c -ldl -lpthread 2> "$T/cl-linux.log" || { fail "linux opencl worker"; head -20 "$T/cl-linux.log"; }
|
||||
step linux-cuda-worker
|
||||
g++ -std=c++17 -O2 -Wall -Wextra -I proto-cuda/nvrtc -I /usr/local/cuda/include -o "$S/bin/linux/igneum-worker-cuda" proto-cuda/nvrtc/worker.cpp -ldl -lpthread 2> "$T/cuda-linux.log" || { fail "linux cuda worker"; head -20 "$T/cuda-linux.log"; }
|
||||
step windows-cuda-worker
|
||||
x86_64-w64-mingw32-g++ -std=c++17 -O2 -Wall -Wextra -static -I proto-cuda/nvrtc -I /usr/local/cuda/include -o "$S/bin/windows/igneum-worker-cuda.exe" proto-cuda/nvrtc/worker.cpp 2> "$T/cuda-win.log" && x86_64-w64-mingw32-strip "$S/bin/windows/igneum-worker-cuda.exe" || { fail "windows cuda worker"; head -20 "$T/cuda-win.log"; }
|
||||
step windows-opencl-worker
|
||||
# the Khronos CL headers alone (never -I /usr/include: that puts glibc's stdint.h ahead of mingw's)
|
||||
mkdir -p "$T/inc" && cp -r /usr/include/CL "$T/inc/"
|
||||
x86_64-w64-mingw32-gcc -std=c99 -O2 -Wall -Wextra -Wno-stringop-truncation -Wno-format-truncation -static -DIGNEUM_CL_DYNAMIC -DCL_TARGET_OPENCL_VERSION=120 -I "$T/inc" -I proto-cuda/packs/igneum-devnet-v4-epoch0 -DIGNEUM_KERNEL_PATH='"kernel_bound.cl"' -o "$S/bin/windows/igneum-worker-opencl.exe" proto-opencl/host.c 2> "$T/cl-win.log" && x86_64-w64-mingw32-strip "$S/bin/windows/igneum-worker-opencl.exe" || { fail "windows opencl worker"; head -20 "$T/cl-win.log"; }
|
||||
if [ "$SKIP_EMU" = 0 ]; then
|
||||
step emu-check-v5
|
||||
# the CPU emulation (proto-cuda/nvrtc/emu/test.sh's build) with pack A = the class v5 pack and pack B = the v4 control
|
||||
E=$T/emu; mkdir -p "$E"
|
||||
emu_kernel() { { echo '#include <cuda_runtime.h>'; echo '#include <cstdint>'; echo "namespace $2 {"; sed -E 's/([A-Za-z_0-9]+)<<<([^,]+), ([^>]+)>>>\(/emu_launch(\1, \2, \3, /' "$1/$3.cu"; echo "}"; } > "$E/$3_$2.cpp"; g++ -std=c++17 -O2 -w -I proto-cuda/emu -I "$1" -c "$E/$3_$2.cpp" -o "$E/$3_$2.o"; }
|
||||
emu_kernel "$V5" emu_pack_a kernel && emu_kernel "$V5" emu_pack_a kernel_bound && emu_kernel "$V6P" emu_pack_b kernel && emu_kernel "$V6P" emu_pack_b kernel_bound \
|
||||
&& g++ -std=c++17 -O2 -Wall -Wextra -DIGNEUM_EMU -I proto-cuda/nvrtc -I /usr/local/cuda/include -c proto-cuda/nvrtc/worker.cpp -o "$E/worker.o" \
|
||||
&& g++ -std=c++17 -O2 -Wall -Wextra -DIGNEUM_EMU -DIGNEUM_EMU_TWO_PACKS -I proto-cuda/nvrtc -I proto-cuda/emu -I /usr/local/cuda/include -c proto-cuda/nvrtc/emu/emu_backend.cpp -o "$E/emu_backend.o" \
|
||||
&& g++ -std=c++17 -O2 -w -I proto-cuda/emu -c proto-cuda/emu/shim.cpp -o "$E/shim.o" \
|
||||
&& g++ -o "$E/igneum-worker-cuda-emu" "$E"/*.o -pthread 2> "$T/emu-build.log" || { fail "emu build"; head -30 "$T/emu-build.log"; }
|
||||
if [ -x "$E/igneum-worker-cuda-emu" ]; then
|
||||
( IGNEUM_EMU_PACK="$V5" IGNEUM_EMU_PACK2="$V6P" timeout 1500 nice -n 10 "$E/igneum-worker-cuda-emu" --check --pack "$V5" > "$T/emu-check.log" 2>&1 ) || fail "emu --check on the class v5 pack (rc $?)"
|
||||
grep -E '^check PASS|self-test|FAIL|error' "$T/emu-check.log" | cut -c1-400
|
||||
fi
|
||||
fi
|
||||
if [ "$rc" != 0 ]; then echo "KIT not written: a step failed (rc $rc)"; finish; exit 1; fi
|
||||
step stage
|
||||
for p in "$V6_ALL" "$V6_PARTNER"; do mkdir -p "$S/packs/$p"; cp "$PK/$p"/* "$S/packs/$p/"; done
|
||||
cp proto-cuda/nvrtc/worker.cpp proto-cuda/nvrtc/packfile.h proto-cuda/nvrtc/cuda_api.h proto-opencl/host.c proto-opencl/cl_dynamic.h proto-newpow/class-v5/bench.cu proto-newpow/class-v5/run.sh proto-metal/packbench.swift "$S/src/"
|
||||
cp tools/class-v5/fleet-cuda-v5-bench.sh tools/class-v5/pc1-amd-v5-bench.ps1 "$S/tools/"
|
||||
cat > "$S/README.txt" <<'R'
|
||||
packs-class-v6: the class v6 kit (Igneum, 8 October 2026; docs/design/class-v5-stored-state.md section 0, the class v6 row): the all-together pack and its known-failed partner
|
||||
packs/v5-dn3-epoch0 the first class v5 pack: Devnet 3 epoch 0, program id e5a4ac5978462156, 11 state leaves (leaves.bin, 704 B) under
|
||||
state root 7e37a9fb19b154d32daf5bf30a50d339a75029fbc9eec9ea20e95439dba5a311, cache FNV-1a 64 7334fa46e5d972eb
|
||||
expected fingerprint of the 2^24 outputs at base nonce 0: 82b19cbde8557ea5 (Metal, M5 Max, 18:07:09Z) on every platform
|
||||
packs/v5-genesis the string-seed class v5 pack over the devnet's 93 leaves; packs/v4-genesis the class v4 control (sub-version 3)
|
||||
bin/linux igneum-worker-cuda (NVRTC, libcuda + libnvrtc.so.12 at run time), igneum-worker-opencl (libOpenCL.so.1 at run time)
|
||||
bin/windows igneum-worker-cuda.exe (nvcuda.dll + nvrtc64 at run time), igneum-worker-opencl.exe (OpenCL.dll at run time)
|
||||
Every worker reads a class v5 pack's leaves.bin, checks it against the pack's IGNEUM_STATE_LEAVES_FNV64, uploads it for igneum_build and
|
||||
frees it after the build; a pack without its leaves builds nothing. Self-test: cache head, last line and FNV; dataset head, last word and
|
||||
64 samples; 96 vector lanes. Bench: igneum-worker-cuda --bench --pack packs/v5-dn3-epoch0 --batch-log2 24 (fleet-cuda-v5-bench.sh);
|
||||
igneum-worker-opencl --bench-pack --pack packs/v5-dn3-epoch0 --batch-log2 24 --device <n> (pc1-amd-v5-bench.ps1); src/bench.cu with nvcc.
|
||||
Intel: not measured tonight (7 October 2026); no Arc B580 sits on PC 1 or PC 2 and the only Arc path needs a driver click, which no PC job
|
||||
may raise; the card's holder and a click-free driver path are owed. The OpenCL worker takes the Intel device by index the same way.
|
||||
R
|
||||
( cd "$S" && find . -type f ! -name SHA256SUMS | sort | xargs sha256sum > SHA256SUMS )
|
||||
mkdir -p /srv/artefacts/packs
|
||||
python3 - "$S" "$ZIP" <<'PY'
|
||||
import os, sys, zipfile
|
||||
stage, out = sys.argv[1], sys.argv[2]
|
||||
with zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED) as z:
|
||||
for dp, dn, fn in os.walk(stage):
|
||||
dn.sort()
|
||||
for f in sorted(fn):
|
||||
p = os.path.join(dp, f); rel = os.path.relpath(p, stage)
|
||||
zi = zipfile.ZipInfo(rel, date_time=(2026, 10, 7, 0, 0, 0)); zi.compress_type = zipfile.ZIP_DEFLATED
|
||||
zi.external_attr = (0o755 if rel.startswith('bin/') or rel.endswith('.sh') else 0o644) << 16
|
||||
with open(p, 'rb') as fh: z.writestr(zi, fh.read())
|
||||
PY
|
||||
sha256sum "$ZIP" | cut -c1-64 > "$ZIP.sha256"
|
||||
echo "KIT $ZIP bytes $(stat -c %s "$ZIP") files $(python3 -c "import zipfile,sys; print(len(zipfile.ZipFile(sys.argv[1]).namelist()))" "$ZIP")"
|
||||
echo "KIT sha256 $(cat "$ZIP.sha256")"
|
||||
for b in "$S"/bin/linux/* "$S"/bin/windows/*; do echo "BIN $(basename "$(dirname "$b")")/$(basename "$b") $(stat -c %s "$b") bytes sha256 $(sha256sum "$b" | cut -c1-64)"; done
|
||||
finish
|
||||
exit 0
|
||||
95
tools/class-v5/pc1-v6-bench.ps1
Normal file
95
tools/class-v5/pc1-v6-bench.ps1
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
# Class v6 kit (8 October 2026, the coordinator's order): the amd job of the v5 script with the class v6 packs: hl-v6-foldrw (the
|
||||
# known-failed partner; expected fingerprint 6ce3dc344a613500, the Mac's Metal and Apple OpenCL reading) and hl-v6-all (the
|
||||
# all-together pack, id 9d40978601a7df2a; with the second zip its OpenCL text is the window's real text and the expected
|
||||
# fingerprint is the CUDA reference e8f4f3289c6ee1fc; the first zip's stub refused it, that refusal line being that run's record).
|
||||
# Kit fetch job fetch-class-v6-kit-20261008 = the second zip (packs-class-v6-<stamp>.zip named in the job's publish line).
|
||||
# Class v5 kit bench on PC 1's RX 9070 XT (7 October 2026, docs/design/class-v5-stored-state.md section 13, "the kit:
|
||||
# OpenCL (AMD)"): the kit's igneum-worker-opencl.exe (THIS tree's proto-opencl/host.c with the class v5 leaf upload:
|
||||
# igneum_build(ds, cache, leaves, nLeaves, nItems), the leaves of leaves.bin checked against the pack's FNV-1a 64 first)
|
||||
# runs --bench-pack on the first class v5 pack, proto-cuda/packs-ca3-v5/v5-dn3-epoch0 (program id e5a4ac5978462156, 11
|
||||
# leaves under state root 7e37a9fb..., cache FNV 7334fa46e5d972eb), on the gfx1201 device. Expected: the self-test
|
||||
# PASS line (cache head, last line and FNV; dataset head, word [268435455] and 64 samples; 96 of 96 vector lanes) and the
|
||||
# fingerprint of the 2^24 outputs at base nonce 0 equal to the Metal reading, 6ce3dc344a613500 (the M5 Max, 7 October
|
||||
# 2026, 18:07:09Z). The v4-genesis control pack runs after it so the run has a known class v4 row beside the v5 row.
|
||||
# Beside the miners (a correctness and fingerprint gate; the rate row is labelled loaded when the card mines): no card is
|
||||
# switched, nothing is posted to the installed app, nothing is built on the PC. Published by the Counter ASIC coordinator
|
||||
# only (the PC 1 queue is its); the kit is fetch job $kitId. Every result line starts with RESULT; SUMMARY {json} ends it.
|
||||
# Post-review packs (8 October 2026, 20:4x UK, the third kit zip packs-class-v6-20261008T194045Z.zip, sha256 735e1411...): the packs are
|
||||
# generator 6 (class v6; ids hl-v6-all 4de7b836cc40a4ea, hl-v6-foldrw d7eba30115d26dd4) and the expected fingerprints are the
|
||||
# Mac's Metal and Apple OpenCL reading of them (all pack e8f4f3289c6ee1fc, partner 6ce3dc344a613500); the generator 5 zips' values
|
||||
# (59e6708e46f1e87c and 5a6ad122a71a888f) belong to the superseded packs and no longer match by construction.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$jobName = 'pc1-v6-bench'
|
||||
$kitId = $env:IGNEUM_V5_KIT_ID; if (-not $kitId) { $kitId = 'fetch-class-v6-kit-20261008' }
|
||||
$expected = '6ce3dc344a613500'
|
||||
$started = Get-Date
|
||||
function Stamp { (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') }
|
||||
function Summary([string] $status, [hashtable] $extra) {
|
||||
$o = [ordered]@{ job = $jobName; status = $status; duration_s = [int]((Get-Date) - $started).TotalSeconds; finished_at = (Stamp) }
|
||||
foreach ($k in $extra.Keys) { $o[$k] = $extra[$k] }
|
||||
'SUMMARY ' + ($o | ConvertTo-Json -Compress -Depth 4)
|
||||
}
|
||||
"RESULT start $(Stamp) job=$jobName machine=$env:COMPUTERNAME app_version=$env:IGNEUM_APP_VERSION expected_fingerprint=$expected"
|
||||
$jobs = Split-Path $env:IGNEUM_JOB_DIR
|
||||
$kit = Join-Path $jobs $kitId
|
||||
if (-not (Test-Path $kit)) { "RESULT error kit missing at $kit (the fetch job $kitId runs first; republish it after any app update)"; Summary 'failed' @{ error = 'kit missing' }; exit 2 }
|
||||
$exe = Join-Path $kit 'bin\windows\igneum-worker-opencl.exe'
|
||||
$packs = Join-Path $kit 'packs'
|
||||
if (-not (Test-Path $exe)) { "RESULT error worker missing at $exe"; Summary 'failed' @{ error = 'worker missing' }; exit 2 }
|
||||
"RESULT worker kit $exe sha256 $((Get-FileHash -Algorithm SHA256 $exe).Hash.ToLower()) bytes $((Get-Item $exe).Length)"
|
||||
foreach ($pk in @('hl-v6-foldrw', 'hl-v6-all')) {
|
||||
$d = Join-Path $packs $pk
|
||||
if (-not (Test-Path (Join-Path $d 'kernel_bound.cl'))) { "RESULT error pack $pk missing at $d"; Summary 'failed' @{ error = "pack $pk missing" }; exit 2 }
|
||||
"RESULT pack $pk kernel_bound.cl sha256 $((Get-FileHash -Algorithm SHA256 (Join-Path $d 'kernel_bound.cl')).Hash.ToLower()) program.h sha256 $((Get-FileHash -Algorithm SHA256 (Join-Path $d 'program.h')).Hash.ToLower())"
|
||||
}
|
||||
# the class v6 kit's packs each carry their own leaves.bin (the class v5 script checked one pack by name; the 18:01 UK run on
|
||||
# PC 1 stopped on that name before the device list)
|
||||
foreach ($lp in @('hl-v6-foldrw', 'hl-v6-all')) {
|
||||
$leaves = Join-Path $packs ($lp + '\leaves.bin')
|
||||
if (-not (Test-Path $leaves)) { "RESULT error leaves.bin missing at $leaves (a class v5 pack without its leaves builds nothing)"; Summary 'failed' @{ error = 'leaves missing'; pack = $lp }; exit 2 }
|
||||
"RESULT leaves $lp $((Get-Item $leaves).Length) bytes sha256 $((Get-FileHash -Algorithm SHA256 $leaves).Hash.ToLower())"
|
||||
}
|
||||
# the OpenCL device index of the 9070 XT (the installed worker's list when present: the app's own indices)
|
||||
$inst = @("$env:LOCALAPPDATA\Programs\Igneum Miner", "$env:ProgramFiles\Igneum Miner") | Where-Object { Test-Path (Join-Path $_ 'igneum-app.exe') } | Select-Object -First 1
|
||||
$listExe = $exe
|
||||
if ($inst -and (Test-Path (Join-Path $inst 'igneum-worker-opencl.exe'))) { $listExe = Join-Path $inst 'igneum-worker-opencl.exe' }
|
||||
$list = @(& $listExe --list 2>&1 | ForEach-Object { "$_" })
|
||||
$list | ForEach-Object { "RESULT list $_" }
|
||||
$dev = $null
|
||||
foreach ($l in $list) { if ($l -match '^\s*\[(\d+)\].*gfx1102' -and $l -notmatch 'dup') { $dev = [int]$Matches[1]; break } }
|
||||
if ($null -eq $dev) { "RESULT error no gfx1102 device in --list (the RX 7600 is off the bus: the AMD class v6 row stays OWED)"; Summary 'failed' @{ error = 'no gfx1102' }; exit 2 }
|
||||
"RESULT device $dev gfx1102 (list from $listExe)"
|
||||
function Workers { @(Get-CimInstance Win32_Process -Filter "Name = 'igneum-worker-opencl.exe' OR Name = 'igneum-worker-cuda.exe'" -ErrorAction SilentlyContinue | ForEach-Object { "$($_.Name):$($_.ProcessId):[$($_.CommandLine -replace '\s+', ' ')]" }) }
|
||||
$w = @(Workers)
|
||||
$loaded = ($w | Where-Object { $_ -match "igneum-worker-opencl.*--device\s+$dev(\s|$)" }).Count -gt 0
|
||||
"RESULT workers_before $(Stamp) $($w -join ' ')"
|
||||
$state = if ($loaded) { 'loaded' } else { 'quiet' }
|
||||
"RESULT context card_state=$state (a fingerprint gate: the load changes the rate row, never the bytes)"
|
||||
$rows = @{}
|
||||
$fpOk = $false
|
||||
foreach ($pk in @('hl-v6-foldrw', 'hl-v6-all')) {
|
||||
$d = Join-Path $packs $pk
|
||||
$t0 = Get-Date
|
||||
"RESULT bench $pk start $(Stamp) cmd=igneum-worker-opencl.exe --bench-pack --pack $d --device $dev --batch-log2 24 --batches 5"
|
||||
$out = @(& $exe --bench-pack --pack $d --device $dev --batch-log2 24 --batches 5 2>&1 | ForEach-Object { "$_" })
|
||||
$code = $LASTEXITCODE
|
||||
$secs = [int]((Get-Date) - $t0).TotalSeconds
|
||||
foreach ($l in $out) { if ($l -match '^(pack |class v5|RESULT |FAIL|error|warm-up)') { "RESULT bench $pk out $l" } }
|
||||
$res = $out | Where-Object { $_ -match '^RESULT ' } | Select-Object -Last 1
|
||||
$fp = ''; $mhs = ''; $check = ''
|
||||
if ($res -match 'fingerprint=([0-9a-f]{16})') { $fp = $Matches[1] }
|
||||
if ($res -match 'mhs=([0-9.]+)') { $mhs = $Matches[1] }
|
||||
if ($res -match 'check=(\w+)') { $check = $Matches[1] }
|
||||
$rows[$pk] = [ordered]@{ exit = $code; seconds = $secs; fingerprint = $fp; mhs = $mhs; check = $check; card_state = $state }
|
||||
if ($pk -eq 'hl-v6-foldrw') {
|
||||
$fpOk = ($fp -eq $expected -and $check -eq 'PASS')
|
||||
"RESULT partner fingerprint=$fp expected=$expected match=$fpOk check=$check mhs=$mhs card_state=$state exit=$code seconds=$secs $(Stamp)"
|
||||
} else {
|
||||
# the all pack: with the window's OpenCL text (the second zip) a fingerprint, expected the CUDA reference e8f4f3289c6ee1fc
|
||||
$allOk = ($fp -eq 'e8f4f3289c6ee1fc' -and $check -eq 'PASS')
|
||||
"RESULT all-pack fingerprint=$fp expected=e8f4f3289c6ee1fc match=$allOk check=$check mhs=$mhs card_state=$state exit=$code seconds=$secs $(Stamp)"
|
||||
}
|
||||
}
|
||||
"RESULT workers_after $(Stamp) $((@(Workers)) -join ' ')"
|
||||
Summary $(if ($fpOk) { 'done' } else { 'failed' }) @{ expected = $expected; partner = $rows['hl-v6-foldrw']; all_pack = $rows['hl-v6-all']; device = $dev; kit = $kitId }
|
||||
exit $(if ($fpOk) { 0 } else { 1 })
|
||||
Loading…
Reference in a new issue