Merge class-v6-family-gate 43adcf75 into master (gate: green on 43adcf75, recorded by tools/ci/pre-push.sh; landed on the build mirror)

This commit is contained in:
igneum-labs 2026-10-08 18:01:23 +00:00
commit acbbe44d99
10 changed files with 13684 additions and 1 deletions

View file

@ -331,6 +331,19 @@ The research lane's close names, from the k lane's mix optimiser over the band,
VERDICT (18:00 BST, to the coordinator, the hash lane, the census lane and the research lane; the exact-table rows in at 18:20): PASS. The table halves the draw's cost against the band (the lossy share 6 of 75 cuts (a') from 76 to 58 percent of rejections), costs nothing at the floors beyond the width's own cost, adds no hot set and no seed over 1.2x on the live item map at 2^20, and leaves the bit-R mechanism of section 6.8 where every table of the day left it. The census lane's neighbouring table (add 13, xor 11, mul 6, mad 10, shfl 8, rotl 8, sub 7, mulhi 2, rotr 6, or 4) stays the fallback, unused.
### 6.10 The hash lane's D2 experiment "layer 8 off" through the acceptance (build-4, 18:3x to 19:0x BST)
The ask (the hash lane, 18:12 BST): the window-layer draw removed (`+nowin`: the draw consumes the two window draws per instruction as before and writes win = 0, off = 0 on every load site, so the base program, the shadow and the era draw are byte-equal to the class's without the flag; class-v6 542cd7028, `LoadClass.nowin`), through the same acceptance shape as the day's strata. The harness is the family harness ported onto class-v6 542cd7028 (`logs/harness-v6-542cd7028.diff`: the family predicate sets nowin aside as it does fold and rw; `IGNEUM_FG_NOWIN` applies `with_nowin()` on every era class built through `LoadClass::era` and on the program classes' load classes, so the census and attack-f8 draw the flagged class; attack-f8's "window layer" line reads every site at 2^24 under the flag and the halves and quarters without it). Everything else D1's: the best-mix table (14, 13, 4, 11, 3, 10, 9, 2, 9, 0), shape 256 x 27, `m` = 8, the class v5 rule with (c''') at 0.995, the same crate for the flagged and the unflagged rows.
| Row | Layer 8 OFF (`+nowin`) | Layer 8 ON (the same crate and table, the control) | Reading |
|---|---|---|---|
| The attempts census, width 4 (the pinned genesis width) | 3,000 eras: r = 0.181 per candidate, mean attempt 0.22, max 4, 0 exhausted; of the 3,067 candidates reaching the 2^20 pass (c'') refuses 0.2 percent and (c''') 2.0 (1.6 percent of all candidates); accepted min ratio p1 0.9959; `logs/nowin-w4-*` | 1,500 eras: r = 0.268, mean attempt 0.37, max 5, 0 exhausted; (c'') 1.4 and (c''') 13.0 percent of 1,751 reaching (11.1 percent of all); p1 0.9960; `logs/win-w4-control-*` | the draw is cheaper by a third and the (c''') cost at width 4 falls from 13 to 2 percent of reaching candidates: the width-4 floor cost of section 6.5 was the WINDOW's (a quarter window at width 4 is 2^20 aligned slots against 2^20 evaluations, so the birthday collisions the ratio reads are the window's own), not the width's |
| The attempts census, width 1 | 1,500 eras: r = 0.179, mean 0.22, max 4, 0 exhausted; (c'') 0 and (c''') 0 of 1,500 reaching; p1 0.9981; `logs/nowin-w1-*` | the D1 exact-table rows of 6.9 at width 1 on 13885e16f: r = 0.155, (c'') 0.3, (c''') 0.8 percent | at width 1 the floors refuse nothing in 1,500 eras under the flag |
| The per-site index-bit read (the fold's lever) | over 6 sigma at one site in 44.3 percent of eras at width 4 (47.5 at width 1), over 100 sigma 29.5 (33.1), over 300 sigma 19.9 (21.8); 78 percent of the biased sites at address bit R exactly; bucket over +8 sigma 13.8 percent, p99 +32 | 34.3 / 18.8 / 8.3 percent at width 4; 71 percent at bit R; bucket over +8 7.5, p99 +27.5 | the bit-R mechanism stays and reads LARGER with the windows off: a quarter window replaces the two top address bits by the offset, which cut the product's bit 0 whenever R landed at 26 or 27 and spread the rest over a smaller index space; with every site on the whole dataset every R from 1 to 27 lands inside the 28-bit index. The fold in `load_index` is the lever, as the hash lane expected; nothing in this row is the window's to fix |
| attack-f8, 64 F8 seeds at 2^20 on the Devnet 3 state, the window-model control (flat under the flag), the by-site attribution | hot sets 0 of 64; beyond 1.2x of the control 0 of 64 (worst p3 1.053x, then 1.015x, 1.012x); windowed buckets beyond 6 sigma in 10 of 64; `logs/f8-nowin.log` | hot sets 0; beyond 1.2x 0 (worst p3 1.103x, 1.098x, 1.091x); buckets beyond 6 sigma 12 of 64; `logs/f8-win.log` | the item map is the flat control's to 1.05x without the windows (the 1.10x tail at the same seeds was the window model's own spread); the bucket class (the bit-R mechanism on the live map) is unchanged at 10 to 12 of 64 |
Verdict for the hash lane (19:0x BST): the layer-8-off draw PASSES the acceptance with a cheaper draw and a cheaper floor than the windowed class on the same crate (r 0.18 against 0.27 at width 4; (c''') 2 percent against 13), no hot set and no seed over 1.2x on the F8 set at 2^20, and the fold still the lever for bit R (its share up, not down, with the windows gone). What the windows were buying was never on the acceptance's side of the ledger (their worth is the SRAM-mirror argument of Counter ASIC 2.0 layer 8, the GPU rows the hash lane holds); on the acceptance's side they cost a third of the draw and most of the width-4 floor cost. Per tier: nothing moves for a card on these rows; a node's epoch draw costs 0.22 attempts instead of 0.37.
## 7. The 16:30 BST report (the founder's clock, pulled from 09:00; a partial carries its count)
1. DONE 13:2x BST: the lossy-share curve per shape (section 6.4) and the last-resort scan's verdicts on every exhausted era.
@ -340,7 +353,8 @@ VERDICT (18:00 BST, to the coordinator, the hash lane, the census lane and the r
5. DONE 14:4x BST: the width-4 plus shape-64 crossing (build-4, 3,000 eras: r = 0.654, 0 exhausted, (c''') 5.3 percent of candidates, the width's floor cost again) and section 6.8, the F8 label space's p2 to p65 and p212 to p225 through the sigma form at the shipped parameters (build-2, queued 14:0x BST), so the two F8-256 attributions the hash lane added to layer 4's bucket-bound row (p212, site 9 at 1.75x its window expectation; p225, a value-level concentration at a mad-written site with the bucket at expectation) and the morning's four tail seeds are read as known-failed cases of the bucket-sigma and bit-bias tests on one scale; and the bucket bound in sigma (from the bit-clean spread: p99 +6.8, p99.9 +16.5 to +20.1, max +35 over 9,409 eras; a band at +8 refuses 0.3 to 0.4 percent of bit-clean programs and 11 to 18 percent of all, which is the biased population, so the bucket bound and the bit read are one rule).
6. The gate-record form: `logs/gate-records-lossy-base.jsonl` (3,000 records of the band stratum in the section 4.3 shape, one per line, 3.6 MB) and `logs/fg-records.py`, which derives every other stratum's records from its TSV rows in this directory.
7. DONE 18:0x BST, section 6.9 (the Igneum 2.0 pause of 17:10 BST keeps this row as D1's op-mix acceptance and pauses the rest of the family gate; this report stands as the no-rescue test's control document): the research lane's best-mix genesis table (add 16, xor 14, mad 12, rotl 11, sub 10, rotr 10, shfl 4, mul 4, mulhi 2, or 0, sum 83; renormalised to 75 by largest remainder as 14, 13, 4, 11, 3, 10, 9, 2, 9, 0 in the generator's order) through the acceptance at the shipped parameters: the attempts census with the refused-ratio column at widths 4 and 1, the index-bit read, and attack-f8 at 2^20 on 64 seeds: PASS, the exact-table rows in at 18:20 BST. The harness is also ported onto class-v6 (13885e16f; the diff `logs/harness-v6-13885e16f.diff`, reproducing the class-v5 rows to the last digit) for the hash lane's layer-8-off acceptance due 12:00 BST on 9 October.
8. Not in this report: the mixer ladder re-run at m = 4 and 16 (adv-mixer-3's harness rows; the per-family rows of section 4), the F8 census at D = 29 (the stand-in gap row), the 10^5-program verifier census at the top corner (F6 per family): each a named per-family row with its hours in section 4.1.
8. DONE 19:0x BST, section 6.10: the hash lane's layer-8-off draw (`+nowin`, class-v6 542cd7028) through the acceptance against the same-crate windowed control: PASS, the draw a third cheaper and the width-4 floor cost down from 13 to 2 percent of reaching candidates, 0 hot sets at 2^20, the bit-R mechanism unchanged (the fold the lever).
9. Not in this report: the mixer ladder re-run at m = 4 and 16 (adv-mixer-3's harness rows; the per-family rows of section 4), the F8 census at D = 29 (the stand-in gap row), the 10^5-program verifier census at the top corner (F6 per family): each a named per-family row with its hours in section 4.1.
## 8. Sources

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,497 @@
diff --git a/igneum-pow/src/accept.rs b/igneum-pow/src/accept.rs
index e02c92efe..73161076f 100644
--- a/igneum-pow/src/accept.rs
+++ b/igneum-pow/src/accept.rs
@@ -365,7 +365,88 @@ pub struct SiteIndexStats {
/// The window of a load site in words at the rule's dataset: `2^28 >> min(win, 2)`.
pub fn site_window_words(ins: &Instr) -> u64 {
- (1u64 << ACCEPT_DATASET_LOG2) >> (ins.win as u64).min(2)
+ // a load of `width` words reads an aligned address, so its index space is the window over `width` (1 on every
+ // shipped class; the class v6 family-gate harness draws 4 and the expectation must follow, 8 October 2026)
+ ((1u64 << ACCEPT_DATASET_LOG2) >> (ins.win as u64).min(2)) / (ins.width.max(1) as u64)
+}
+
+/// Class v6 family-gate harness: what one load site's indices look like beyond [`SiteIndexStats`]: the largest
+/// 256-item bucket (4,096 words, the bucket of the F8 tail attribution of 8 October 2026) as a ratio to the window's
+/// expectation and in sigma of it, and the largest index-bit one-count excess in sigma over the window's free bits
+/// (adv-cache-2's value-level read: a product's low bits placed at address bit R and up).
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct FamilySiteStats {
+ pub base: SiteIndexStats,
+ pub bucket_ratio: f64,
+ pub bucket_z: f64,
+ pub bucket_id: u32,
+ /// the site's window draw `k_off` (0 the dataset, 1 a half, 2 a quarter)
+ pub win: u8,
+ pub bit_z: f64,
+ pub bit: u8,
+}
+
+/// The same run as [`site_index_stats`] with the two extra statistics per site.
+pub fn family_site_stats(p: &Program, units: usize) -> Result<Vec<FamilySiteStats>, Reject> {
+ let mut indices = site_indices(p, units)?;
+ let load_instrs: Vec<&Instr> = p.instrs.iter().filter(|i| i.op.is_load()).collect();
+ let mut out = Vec::with_capacity(indices.len());
+ for (site, ix) in indices.iter_mut().enumerate() {
+ let ins = load_instrs[site];
+ let n = ix.len() as f64;
+ let k = (ins.win as u32).min(2);
+ let free_bits = ACCEPT_DATASET_LOG2 - k;
+ let mut ones = [0u64; 32];
+ let mut buckets = vec![0u32; 1usize << (ACCEPT_DATASET_LOG2 - 12)];
+ for &x in ix.iter() {
+ buckets[(x >> 12) as usize] += 1;
+ let mut v = x;
+ let mut b = 0;
+ while v != 0 {
+ ones[b] += (v & 1) as u64;
+ v >>= 1;
+ b += 1;
+ }
+ }
+ let width = ins.width.max(1) as u32;
+ let align_bits = width.trailing_zeros();
+ let (mut bit_z, mut bit) = (0.0f64, 0u8);
+ for b in align_bits..free_bits {
+ let z = (ones[b as usize] as f64 - n / 2.0) / (n / 4.0).sqrt();
+ if z.abs() > bit_z.abs() {
+ bit_z = z;
+ bit = b as u8;
+ }
+ }
+ let window_buckets = (site_window_words(ins) * width as u64) >> 12;
+ let expect = n / window_buckets as f64;
+ let (mut bmax, mut bid) = (0u32, 0u32);
+ for (i, &c) in buckets.iter().enumerate() {
+ if c > bmax {
+ bmax = c;
+ bid = i as u32;
+ }
+ }
+ ix.sort_unstable();
+ let mut st = SiteIndexStats { distinct: 0, pairs: 0, top_index: 0, top_count: 0 };
+ let mut i = 0;
+ while i < ix.len() {
+ let mut j = i + 1;
+ while j < ix.len() && ix[j] == ix[i] {
+ j += 1;
+ }
+ let run = (j - i) as u32;
+ st.distinct += 1;
+ st.pairs += (run as u64) * (run as u64 - 1) / 2;
+ if run > st.top_count {
+ st.top_count = run;
+ st.top_index = ix[i];
+ }
+ i = j;
+ }
+ out.push(FamilySiteStats { base: st, bucket_ratio: bmax as f64 / expect, bucket_z: (bmax as f64 - expect) / expect.sqrt(), bucket_id: bid, win: k as u8, bit_z, bit });
+ }
+ Ok(out)
}
/// One interpreter run over `units` units of the seed's acceptance stream with every load site's word indices kept,
@@ -485,6 +566,9 @@ pub fn check_indices_v5(p: &Program) -> Result<(), Reject> {
/// Whether `class` is the class v4 shape (the 256-instruction shadow block over the class v3 base, the pass count and
/// the era set aside): the shape the sub-version 2 rules (a') and (c') apply to, on every draw path.
pub fn is_class_v4_shape(class: &LoadClass) -> bool {
+ if family_gate_on() {
+ return is_family_shape(class);
+ }
matches!(class.shadow, Some(ShadowClass { instrs: V4_SHADOW_INSTRS, .. }))
// 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
@@ -492,6 +576,31 @@ pub fn is_class_v4_shape(class: &LoadClass) -> bool {
&& LoadClass { era: None, shadow: None, state: false, fold: false, rw: 0, ..*class } == LoadClass { shadow: None, ..V4_CLASS }
}
+/// Class v6 family-gate harness (8 October 2026, `docs/analysis/class-v6/family-gate.md`): the acceptance keyed on the
+/// FAMILY's shapes instead of the one class v4 shape, on when `IGNEUM_FAMILY_GATE` is set in the environment of a
+/// harness run and never on a chain path. The family: the shadow block in {64, 128, 256} instructions at the same
+/// 6,912 instructions per iteration, the mixer multiplier in {4, 8, 16}, the read width the era's draw over {1, 4}
+/// words (the mix one-hot on the drawn width), the rest the class v4 base with the state flag, the class v6 fold and
+/// the re-weight table set aside (as the class's own predicate sets them aside).
+pub fn is_family_shape(class: &LoadClass) -> bool {
+ let sh = match class.shadow {
+ Some(s) => s,
+ None => return false,
+ };
+ if !matches!(sh.instrs, 64 | 128 | 256) || sh.instrs as usize * sh.reps as usize != V4_SHADOW_INSTRS as usize * crate::generator::V4_SHADOW_REPS as usize {
+ return false;
+ }
+ if !matches!(class.mixer_mult, 4 | 8 | 16) {
+ return false;
+ }
+ LoadClass { era: None, shadow: None, state: false, fold: false, rw: 0, nowin: false, mixer_mult: 8, mix: V4_CLASS.mix, ..*class } == LoadClass { shadow: None, ..V4_CLASS }
+}
+
+fn family_gate_on() -> bool {
+ static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
+ *ON.get_or_init(|| std::env::var_os("IGNEUM_FAMILY_GATE").is_some())
+}
+
/// One pass of the dataflow freshness over the base program then the shadow block (the order of one iteration),
/// from `fresh`; `check` reports the first load that reads a register that is not fresh. The rule (AP-F8-1,
/// `docs/analysis/ca3-v4-uniform.md`): a load leaves its destination fresh only if its source was (a saturated
@@ -1398,4 +1507,238 @@ mod tests {
assert!(check(&generate(s)).is_ok());
}
}
+ /// Class v6 family-gate harness (8 October 2026, `docs/analysis/class-v6/family-gate.md`): one drawn era per seed of a
+ /// label space, every parameter of the family drawn from the era's own stream (the shadow block shape in {64, 128,
+ /// 256}, the mixer multiplier in {4, 8, 16}, the read width over {1, 4} words through the era draw, the ten non-load
+ /// weights perturbed within B points with shuffle and mulhi never raised), the chain draw of that era's epoch
+ /// through the real rule with the first failing part of every candidate, then on the accepted program the per-site
+ /// statistics at the rule's own 2^20 sample: the (c'') ratio, the largest 256-item bucket, the index-bit bias.
+ /// One TSV row per era to `IGNEUM_FG_OUT`. Pins for the strata: `IGNEUM_FG_SHAPE`, `IGNEUM_FG_MIXER`,
+ /// `IGNEUM_FG_WIDTH`, `IGNEUM_FG_B` (default 4), `IGNEUM_FG_LOSSY_CAP` (or, mul and mulhi at +B: the lossy corner),
+ /// `IGNEUM_FG_LABEL` (default `igneum-family-gate`). Requires `IGNEUM_FAMILY_GATE=1` in the environment.
+ #[test]
+ #[ignore]
+ fn family_gate_era_census() {
+ use crate::generator::{set_family_weights, LoadClass, ShadowClass, NONLOAD_WEIGHTS, V5_CLASS, MAX_ATTEMPTS_V4};
+ use crate::seed::seed_words_from_bytes;
+ use std::io::Write;
+ use std::sync::atomic::{AtomicU32, Ordering};
+ use std::sync::Mutex;
+ assert!(family_gate_on(), "IGNEUM_FAMILY_GATE=1 is required");
+ let env_u = |k: &str, d: u32| std::env::var(k).ok().and_then(|v| v.parse().ok()).unwrap_or(d);
+ let seeds = env_u("IGNEUM_FG_SEEDS", 1000);
+ let from = env_u("IGNEUM_FG_FROM", 0);
+ let threads = env_u("IGNEUM_FG_THREADS", 32) as usize;
+ let b_pts = env_u("IGNEUM_FG_B", 4) as i64;
+ let label = std::env::var("IGNEUM_FG_LABEL").unwrap_or_else(|_| "igneum-family-gate".to_string());
+ let pin_shape = std::env::var("IGNEUM_FG_SHAPE").ok().and_then(|v| v.parse::<u16>().ok());
+ let pin_mixer = std::env::var("IGNEUM_FG_MIXER").ok().and_then(|v| v.parse::<u8>().ok());
+ let pin_width = std::env::var("IGNEUM_FG_WIDTH").ok().and_then(|v| v.parse::<u8>().ok());
+ let lossy_cap = std::env::var_os("IGNEUM_FG_LOSSY_CAP").is_some();
+ // the proposed band after the 12:00 BST reading of 8 October: B on the injecting families only, the three lossy
+ // families (or, mul, mulhi) never raised above their base
+ let lossy_base = std::env::var_os("IGNEUM_FG_LOSSY_BASE").is_some();
+ // the lossy-share curve (the full report's item 3): or, mul and mulhi each at exactly +n points
+ let lossy_plus: Option<i64> = std::env::var("IGNEUM_FG_LOSSY_PLUS").ok().and_then(|v| v.parse().ok());
+ let out_path = std::env::var("IGNEUM_FG_OUT").unwrap_or_else(|_| "family-gate.tsv".to_string());
+ let out = Mutex::new(std::fs::File::create(&out_path).expect("IGNEUM_FG_OUT"));
+ let wnames: Vec<String> = NONLOAD_WEIGHTS.iter().map(|(o, _)| format!("w_{o:?}").to_lowercase()).collect();
+ writeln!(out.lock().unwrap(), "k\tshape\treps\tmixer\twidth\tR\tM\tpos\t{}\tattempt\tcandidates\ta_prime\ta\tb\tc_const\tc_lane\tc_sat\tc_bias\tc_distinct\tc1_sat_source\tc2_low_entropy\tc3_hot_item\tc_other\tmin_ratio\tmin_site\ttop_count_max\tbucket_ratio_max\tbucket_z_max\tbucket_site\tbucket_win\tbit_z_max\tbit_site\tbit_win\tbit\tlast_resort_k\tc3_ratios\tsecs", wnames.join("\t")).unwrap();
+ let part_ix = |r: &Reject| -> usize {
+ match r {
+ Reject::UnfreshLoadSource { .. } => 0,
+ Reject::StaleLoadSource { .. } => 1,
+ Reject::NoInjectingWrite { .. } => 2,
+ Reject::ConstantBit { .. } => 3,
+ Reject::LaneConstantSite { .. } => 4,
+ Reject::Saturated { .. } => 5,
+ Reject::OutputBias { .. } => 6,
+ Reject::DistinctAddresses { .. } => 7,
+ Reject::SaturatedSource { .. } => 8,
+ Reject::LowEntropySite { .. } | Reject::RepeatedSource { .. } => 9,
+ Reject::HotItemSite { .. } => 10,
+ _ => 11,
+ }
+ };
+ let next = AtomicU32::new(from);
+ let t0 = std::time::Instant::now();
+ let done = AtomicU32::new(0);
+ std::thread::scope(|sc| {
+ for _ in 0..threads {
+ sc.spawn(|| loop {
+ let k = next.fetch_add(1, Ordering::Relaxed);
+ if k >= from + seeds {
+ break;
+ }
+ let ts = std::time::Instant::now();
+ let w = |s: String| -> Vec<u8> { seed_words_from_bytes(s.as_bytes()).iter().flat_map(|x| x.to_le_bytes()).collect() };
+ let epoch = w(format!("{label}/program/{k}"));
+ let era = w(format!("{label}/era/{k}"));
+ // the family's draws from the era's own stream, in a fixed order, each consumed whether pinned or not
+ let dw = seed_words_from_bytes(&[b"igneum-family-gate/draw/".as_slice(), era.as_slice()].concat());
+ let mut rng = SplitMix64::new(dw[0] as u64 | ((dw[1] as u64) << 32));
+ let shapes = [64u16, 128, 256];
+ let shape = pin_shape.unwrap_or(shapes[rng.below(3) as usize]);
+ let mixers = [4u8, 8, 16];
+ let mixer = pin_mixer.unwrap_or(mixers[rng.below(3) as usize]);
+ // the base table: NONLOAD_WEIGHTS, or IGNEUM_FG_WEIGHTS when set (a fixed genesis table under test
+ // with B = 0, the research lane's close of 15:3x BST on 8 October)
+ let base_weights = crate::generator::family_weights_env_table();
+ let mut weights = base_weights;
+ let mut raw = [0i64; 10];
+ for (i, (op, wgt)) in base_weights.iter().enumerate() {
+ let mut d = rng.below((2 * b_pts + 1) as u64) as i64 - b_pts;
+ if matches!(op, Op::Shfl | Op::MulHi) && d > 0 {
+ d = 0;
+ }
+ if lossy_cap && matches!(op, Op::Or | Op::Mul | Op::MulHi) {
+ d = b_pts;
+ }
+ if lossy_base && matches!(op, Op::Or | Op::Mul | Op::MulHi) && d > 0 {
+ d = 0;
+ }
+ if let Some(n) = lossy_plus {
+ if matches!(op, Op::Or | Op::Mul | Op::MulHi) {
+ d = n;
+ }
+ }
+ // a drawn table keeps every family at one point or more; a fixed table under test (B = 0 with
+ // IGNEUM_FG_WEIGHTS) is taken as given, a zero weight included (the first run of the best-mix
+ // table floored `or` at 1 of 75 by this line; the record names both)
+ raw[i] = (*wgt as i64 + d).max(if b_pts == 0 { 0 } else { 1 });
+ }
+ // renormalise to 75 by largest remainder
+ let total: i64 = raw.iter().sum();
+ let mut floors = [0u64; 10];
+ let mut rems: Vec<(i64, usize)> = Vec::new();
+ let mut sum = 0u64;
+ for i in 0..10 {
+ floors[i] = ((raw[i] * 75) / total) as u64;
+ rems.push((((raw[i] * 75) % total), i));
+ sum += floors[i];
+ }
+ rems.sort_by(|x, y| y.0.cmp(&x.0).then(x.1.cmp(&y.1)));
+ let mut short = 75 - sum;
+ for &(_, i) in &rems {
+ if short == 0 {
+ break;
+ }
+ floors[i] += 1;
+ short -= 1;
+ }
+ for i in 0..10 {
+ weights[i].1 = floors[i];
+ }
+ set_family_weights(Some(weights));
+ let reps = (V4_SHADOW_INSTRS as u32 * crate::generator::V4_SHADOW_REPS as u32 / shape as u32) as u16;
+ let mut base = LoadClass { shadow: Some(ShadowClass { instrs: shape, reps }), mixer_mult: mixer, ..V5_CLASS };
+ if std::env::var_os("IGNEUM_FG_NOWIN").is_some() {
+ base = base.with_nowin();
+ }
+ // a one-entry set is the pinned-width path of LoadClass::era (the mix stays the base's and the draw is
+ // redrawn to the base's widest, 1 word) and the set must be strictly ascending, so a pinned width is
+ // built by hand: the era drawn over the one-entry set and the mix one-hot on that width, as the
+ // drawn-set path does
+ let class = match pin_width {
+ Some(w) if w == 1 || w == 4 => {
+ let mut c = base;
+ let i = crate::generator::WIDTH_WORDS.iter().position(|&x| x == w).unwrap();
+ c.mix = [0, 0, 0];
+ c.mix[i] = 100;
+ c.era = Some(crate::generator::era_draw(&era, &[w]));
+ c
+ }
+ _ => LoadClass::era(base, &era, &[1, 4]),
+ };
+ let e = class.era.unwrap();
+ let lbl = f8_label(&epoch);
+ let mut parts = [0u32; 12];
+ let mut accepted: Option<(u32, Program)> = None;
+ // the ratios of the candidates the two floors refused (the pre-floor spread per width: the full
+ // report's per-width calibration), as "c2:<milli>" or "c3:<milli>" per refusal
+ let mut c3_ratios: Vec<String> = Vec::new();
+ for attempt in 0..MAX_ATTEMPTS_V4 {
+ let c = candidate_class(&lbl, &epoch, attempt, class);
+ match check(&c) {
+ Ok(_) => {
+ accepted = Some((attempt, c));
+ break;
+ }
+ Err(r) => {
+ match &r {
+ Reject::HotItemSite { ratio_milli, .. } => c3_ratios.push(format!("c3:{ratio_milli}")),
+ Reject::LowEntropySite { ratio_milli, .. } => c3_ratios.push(format!("c2:{ratio_milli}")),
+ _ => {}
+ }
+ parts[part_ix(&r)] += 1
+ }
+ }
+ }
+ // an exhausted era takes class v5's last resort: the scan of 256 more candidates past the cap, each
+ // rewritten (or, mul, mulhi to xor) and its stale loads re-sourced, the first that passes the whole
+ // rule; the column records that k (cap + i), or "fallback" when none of the 256 passed
+ let last_resort_k: String = if accepted.is_none() {
+ use crate::generator::{last_resort_v4, repair_stale_loads, LAST_RESORT_SCAN};
+ let mut found = String::from("fallback");
+ for kk in MAX_ATTEMPTS_V4..MAX_ATTEMPTS_V4 + LAST_RESORT_SCAN {
+ let q = repair_stale_loads(last_resort_v4(candidate_class(&lbl, &epoch, kk, class)));
+ if check(&q).is_ok() {
+ found = kk.to_string();
+ break;
+ }
+ }
+ found
+ } else {
+ String::new()
+ };
+ set_family_weights(None);
+ let candidates: u32 = parts.iter().sum::<u32>() + accepted.is_some() as u32;
+ let (attempt, stats_row) = match &accepted {
+ Some((att, p)) => {
+ let st = family_site_stats(p, ACCEPT_UNITS_DISTINCT_V4).expect("the accepted program runs");
+ let n = (ACCEPT_UNITS_DISTINCT_V4 * LANES * ITERATIONS) as f64;
+ let loads: Vec<&Instr> = p.instrs.iter().filter(|i| i.op.is_load()).collect();
+ let (mut min_r, mut min_s) = (f64::MAX, 0usize);
+ let (mut bmax, mut bz, mut bsite, mut bwin) = (0.0f64, 0.0f64, 0usize, 0u8);
+ let (mut zmax, mut zsite, mut zbit, mut zwin) = (0.0f64, 0usize, 0u8, 0u8);
+ let mut top = 0u32;
+ for (s, fs) in st.iter().enumerate() {
+ let r = site_ratio(fs.base.distinct, n, site_window_words(loads[s]));
+ if r < min_r {
+ min_r = r;
+ min_s = s;
+ }
+ if fs.bucket_z > bz {
+ bmax = fs.bucket_ratio;
+ bz = fs.bucket_z;
+ bsite = s;
+ bwin = fs.win;
+ }
+ if fs.bit_z.abs() > zmax.abs() {
+ zmax = fs.bit_z;
+ zsite = s;
+ zbit = fs.bit;
+ zwin = fs.win;
+ }
+ top = top.max(fs.base.top_count);
+ }
+ (*att as i64, format!("{min_r:.5}\t{min_s}\t{top}\t{bmax:.3}\t{bz:.2}\t{bsite}\t{bwin}\t{zmax:.2}\t{zsite}\t{zwin}\t{zbit}"))
+ }
+ None => (-1, "\t\t\t\t\t\t\t\t\t\t".to_string()),
+ };
+ let wcols: Vec<String> = weights.iter().map(|(_, x)| x.to_string()).collect();
+ let pcols: Vec<String> = parts.iter().map(|x| x.to_string()).collect();
+ let row = format!("{k}\t{shape}\t{reps}\t{mixer}\t{}\t{}\t{:08x}\t{:?}\t{}\t{attempt}\t{candidates}\t{}\t{stats_row}\t{last_resort_k}\t{}\t{:.1}", e.width_words, e.stride_rot, e.stride_mul, e.pos, wcols.join("\t"), pcols.join("\t"), c3_ratios.join(","), ts.elapsed().as_secs_f64());
+ writeln!(out.lock().unwrap(), "{row}").unwrap();
+ let d = done.fetch_add(1, Ordering::Relaxed) + 1;
+ if d % 100 == 0 {
+ println!("family_gate_era_census: {d} eras in {:.0} s", t0.elapsed().as_secs_f64());
+ out.lock().unwrap().flush().unwrap();
+ }
+ });
+ }
+ });
+ out.lock().unwrap().flush().unwrap();
+ println!("family_gate_era_census: {} eras {from}..{} in {:.0} s on {threads} threads -> {out_path}", done.load(Ordering::Relaxed), from + seeds, t0.elapsed().as_secs_f64());
+ }
}
diff --git a/igneum-pow/src/generator.rs b/igneum-pow/src/generator.rs
index 39cde7c3a..269f59577 100644
--- a/igneum-pow/src/generator.rs
+++ b/igneum-pow/src/generator.rs
@@ -474,7 +474,8 @@ impl LoadClass {
/// width that mix can draw.
pub fn era(base: LoadClass, era_bytes: &[u8], allowed: &[u8]) -> LoadClass {
let mut e = era_draw(era_bytes, allowed);
- let mut c = base;
+ // class v6 family-gate harness: the shape and nowin hooks on every era class (under IGNEUM_FAMILY_GATE only)
+ let mut c = family_shape_of(base);
if allowed.len() > 1 {
let i = WIDTH_WORDS.iter().position(|&w| w == e.width_words).unwrap();
c.mix = [0, 0, 0];
@@ -647,6 +648,9 @@ impl LoadClass {
/// 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) {
+ if let Some(o) = family_weights_override() {
+ return o;
+ }
match self.rw {
1 => (&NONLOAD_WEIGHTS_RW, NONLOAD_WEIGHTS_RW_SUM),
2 => (&NONLOAD_WEIGHTS_RW2, NONLOAD_WEIGHTS_RW2_SUM),
@@ -1077,14 +1081,32 @@ pub fn generate_era_generator(seed_string: &str, seed_bytes: &[u8], base: LoadCl
p
}
+/// Class v6 family-gate harness: `base` with the shadow block shape from `IGNEUM_FG_SHAPE` (64, 128 or 256 at 6,912 per
+/// iteration) under `IGNEUM_FAMILY_GATE`, so an unmodified harness runs the family's shape; `base` otherwise.
+fn family_shape_of(base: LoadClass) -> LoadClass {
+ let mut c = base;
+ if std::env::var_os("IGNEUM_FAMILY_GATE").is_some() {
+ if let Some(n) = std::env::var("IGNEUM_FG_SHAPE").ok().and_then(|v| v.parse::<u16>().ok()) {
+ assert!(matches!(n, 64 | 128 | 256), "IGNEUM_FG_SHAPE is 64, 128 or 256");
+ let reps = (V4_SHADOW_INSTRS as u32 * V4_SHADOW_REPS as u32 / n as u32) as u16;
+ c = LoadClass { shadow: Some(ShadowClass { instrs: n, reps }), ..c };
+ }
+ // the hash lane's layer-8-off experiment (8 October 2026, 18:1x BST): the "+nowin" flag on the harness's draws
+ if std::env::var_os("IGNEUM_FG_NOWIN").is_some() {
+ c = c.with_nowin();
+ }
+ }
+ c
+}
+
impl ProgramClass {
/// The load class this program class draws from.
pub fn load_class(&self) -> LoadClass {
match self {
ProgramClass::V2 => LoadClass::V2,
ProgramClass::V3 => V3_CLASS,
- ProgramClass::V4 => V4_CLASS,
- ProgramClass::V5 => V5_CLASS,
+ ProgramClass::V4 => family_shape_of(V4_CLASS),
+ ProgramClass::V5 => family_shape_of(V5_CLASS),
}
}
@@ -1521,6 +1543,50 @@ pub const NONLOAD_WEIGHTS: [(Op, u64); 10] = [
/// Class v6 lane 1, the re-weight table behind the index fold (`+rw`): the k lane's optimiser split in draw order,
/// sum [`NONLOAD_WEIGHTS_RW_SUM`] (83). Shuffle stays at 4 (a shuffle-heavy draw is the worst thing the class can do
/// on the chip side, k 0.021 routed) and `or` is never drawn. The roll ranges over the table's own sum.
+thread_local! {
+ static FAMILY_WEIGHTS: std::cell::Cell<Option<&'static [(Op, u64); 10]>> = const { std::cell::Cell::new(None) };
+}
+/// Class v6 family-gate harness (8 October 2026): the ten non-load weights this thread's draws use when set (a leaked
+/// table per era, so [`LoadClass::nonload_weights`] keeps its static signature), else the process-wide table. Set only
+/// by the harness census (`accept::tests::family_gate_era_census`), never on a chain path.
+pub fn set_family_weights(w: Option<[(Op, u64); 10]>) {
+ FAMILY_WEIGHTS.with(|c| c.set(w.map(|t| &*Box::leak(Box::new(t)))));
+}
+pub fn family_weights() -> [(Op, u64); 10] {
+ FAMILY_WEIGHTS.with(|c| c.get()).map(|t| *t).unwrap_or_else(family_weights_env_table)
+}
+/// The family override for a draw, when the harness set one: the table and its sum.
+fn family_weights_override() -> Option<(&'static [(Op, u64); 10], u64)> {
+ if std::env::var_os("IGNEUM_FAMILY_GATE").is_none() {
+ return None;
+ }
+ let t: &'static [(Op, u64); 10] = match FAMILY_WEIGHTS.with(|c| c.get()) {
+ Some(t) => t,
+ None => FAMILY_WEIGHTS_ENV.get_or_init(family_weights_env),
+ };
+ Some((t, t.iter().map(|w| w.1).sum()))
+}
+static FAMILY_WEIGHTS_ENV: std::sync::OnceLock<[(Op, u64); 10]> = std::sync::OnceLock::new();
+/// The process-wide table (`IGNEUM_FG_WEIGHTS` under `IGNEUM_FAMILY_GATE`, else [`NONLOAD_WEIGHTS`]).
+pub fn family_weights_env_table() -> [(Op, u64); 10] {
+ *FAMILY_WEIGHTS_ENV.get_or_init(family_weights_env)
+}
+/// The harness's process-wide weight table: `IGNEUM_FG_WEIGHTS=w0,..,w9` (sum 75, the order of [`NONLOAD_WEIGHTS`]),
+/// read only with `IGNEUM_FAMILY_GATE` set (an unmodified harness such as attack-f8 then draws the family's weights).
+fn family_weights_env() -> [(Op, u64); 10] {
+ let mut w = NONLOAD_WEIGHTS;
+ if std::env::var_os("IGNEUM_FAMILY_GATE").is_some() {
+ if let Ok(s) = std::env::var("IGNEUM_FG_WEIGHTS") {
+ let v: Vec<u64> = s.split(',').filter_map(|x| x.trim().parse().ok()).collect();
+ assert!(v.len() == 10 && v.iter().sum::<u64>() == 75, "IGNEUM_FG_WEIGHTS: ten weights summing to 75");
+ for (i, x) in v.into_iter().enumerate() {
+ w[i].1 = x;
+ }
+ }
+ }
+ w
+}
+
pub const NONLOAD_WEIGHTS_RW: [(Op, u64); 10] = [
(Op::Add, 16),
(Op::Xor, 14),
@@ -1673,8 +1739,9 @@ pub fn candidate_from_words_class(
// class v5 (docs/design/class-v5-stored-state.md) draws under the same rule: its state flag is set aside here too
// 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, nowin: false, ..class } == LoadClass { shadow: None, ..V4_CLASS };
+ // class v6 family-gate harness (8 October 2026): the same predicate as the acceptance's, generalised to the
+ // family's shapes only when IGNEUM_FAMILY_GATE is set (never on a chain path; see accept::is_family_shape)
+ let source_rule_v4 = crate::accept::is_class_v4_shape(&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];

View file

@ -0,0 +1,26 @@
binary 61a5a737ac02c843 commit v6n test family_gate_era_census from 0 seeds 1500 threads 8 start 2026-10-08T17:30:49Z
lease: holding 8 pool cores (40,41,42,43,44,45,46,47, waited 0 s, class measure, mem 8 GB): family gate: family_gate_era_census nowin-w1 seeds 0+1500
running 1 test
test accept::tests::family_gate_era_census ... family_gate_era_census: 100 eras in 61 s
family_gate_era_census: 200 eras in 118 s
family_gate_era_census: 300 eras in 172 s
family_gate_era_census: 400 eras in 233 s
family_gate_era_census: 500 eras in 293 s
family_gate_era_census: 600 eras in 353 s
family_gate_era_census: 700 eras in 412 s
family_gate_era_census: 800 eras in 471 s
family_gate_era_census: 900 eras in 530 s
family_gate_era_census: 1000 eras in 588 s
family_gate_era_census: 1100 eras in 647 s
family_gate_era_census: 1200 eras in 704 s
family_gate_era_census: 1300 eras in 766 s
family_gate_era_census: 1400 eras in 822 s
family_gate_era_census: 1500 eras in 881 s
family_gate_era_census: 1500 eras 0..1500 in 881 s on 8 threads -> /srv/builds/_adv-family-gate/logs/nowin-w1-family_gate_era_census-0-1500.tsv
ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 80 filtered out; finished in 881.11s
lease: released 8 pool cores after 881 s, exit 0
end 2026-10-08T17:45:30Z rc 0

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,41 @@
binary 61a5a737ac02c843 commit v6n test family_gate_era_census from 0 seeds 3000 threads 16 start 2026-10-08T17:30:49Z
lease: holding 16 pool cores (56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71, waited 0 s, class measure, mem 8 GB): family gate: family_gate_era_census nowin-w4 seeds 0+3000
running 1 test
test accept::tests::family_gate_era_census ... family_gate_era_census: 100 eras in 41 s
family_gate_era_census: 200 eras in 78 s
family_gate_era_census: 300 eras in 111 s
family_gate_era_census: 400 eras in 144 s
family_gate_era_census: 500 eras in 174 s
family_gate_era_census: 600 eras in 218 s
family_gate_era_census: 700 eras in 260 s
family_gate_era_census: 800 eras in 304 s
family_gate_era_census: 900 eras in 346 s
family_gate_era_census: 1000 eras in 387 s
family_gate_era_census: 1100 eras in 423 s
family_gate_era_census: 1200 eras in 463 s
family_gate_era_census: 1300 eras in 501 s
family_gate_era_census: 1400 eras in 541 s
family_gate_era_census: 1500 eras in 583 s
family_gate_era_census: 1600 eras in 626 s
family_gate_era_census: 1700 eras in 670 s
family_gate_era_census: 1800 eras in 712 s
family_gate_era_census: 1900 eras in 751 s
family_gate_era_census: 2000 eras in 792 s
family_gate_era_census: 2100 eras in 829 s
family_gate_era_census: 2200 eras in 867 s
family_gate_era_census: 2300 eras in 902 s
family_gate_era_census: 2400 eras in 938 s
family_gate_era_census: 2500 eras in 973 s
family_gate_era_census: 2600 eras in 1010 s
family_gate_era_census: 2700 eras in 1049 s
family_gate_era_census: 2800 eras in 1089 s
family_gate_era_census: 2900 eras in 1128 s
family_gate_era_census: 3000 eras in 1171 s
family_gate_era_census: 3000 eras 0..3000 in 1171 s on 16 threads -> /srv/builds/_adv-family-gate/logs/nowin-w4-family_gate_era_census-0-3000.tsv
ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 80 filtered out; finished in 1171.35s
lease: released 16 pool cores after 1171 s, exit 0
end 2026-10-08T17:50:20Z rc 0

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
binary 61a5a737ac02c843 commit v6n test family_gate_era_census from 0 seeds 1500 threads 8 start 2026-10-08T17:30:49Z
lease: holding 8 pool cores (48,49,50,51,52,53,54,55, waited 0 s, class measure, mem 8 GB): family gate: family_gate_era_census win-w4-control seeds 0+1500
running 1 test
test accept::tests::family_gate_era_census ... family_gate_era_census: 100 eras in 64 s
family_gate_era_census: 200 eras in 127 s
family_gate_era_census: 300 eras in 188 s
family_gate_era_census: 400 eras in 255 s
family_gate_era_census: 500 eras in 320 s
family_gate_era_census: 600 eras in 386 s
family_gate_era_census: 700 eras in 450 s
family_gate_era_census: 800 eras in 516 s
family_gate_era_census: 900 eras in 581 s
family_gate_era_census: 1000 eras in 648 s
family_gate_era_census: 1100 eras in 717 s
family_gate_era_census: 1200 eras in 781 s
family_gate_era_census: 1300 eras in 845 s
family_gate_era_census: 1400 eras in 909 s
family_gate_era_census: 1500 eras in 970 s
family_gate_era_census: 1500 eras 0..1500 in 970 s on 8 threads -> /srv/builds/_adv-family-gate/logs/win-w4-control-family_gate_era_census-0-1500.tsv
ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 80 filtered out; finished in 969.71s
lease: released 8 pool cores after 970 s, exit 0
end 2026-10-08T17:46:59Z rc 0

File diff suppressed because it is too large Load diff