Class v6 family gate, 6.9 closed: the best-mix table's exact or = 0 attempts rows at both widths on build-4 (r 0.29 and 0.16, 0 exhausted, the floors at the width's cost), the owed line closed; the harness ported onto class-v6 a7c2c082f for the layer-8-off acceptance
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
8ce0956b48
commit
08010e76a4
6 changed files with 3537 additions and 3 deletions
|
|
@ -327,9 +327,9 @@ The research lane's close names, from the k lane's mix optimiser over the band,
|
|||
| The attempts census, width 4 (the or-floored copy, fg8) | 2,497 eras read (1,500 plus 997 of a second 1,500 on build-3 before it stopped answering ssh at 16:4x BST); `logs/ktable-w4-*`, `logs/ktable-w4b-*` | r = 0.329 per candidate (the band 0.62, the shipped draw 0.68); mean attempt 0.49, max 10, 0 past attempt 31, 0 exhausted; of the 3,011 candidates reaching the 2^20 pass (c'') refuses 1.5 percent and (c''') 15.6 (12.6 percent of all candidates); the accepted draw's minimum ratio p1 0.9960, median 0.9999 | the draw is half the band's cost (the lossy share 6 of 75 against 18 cuts (a') from 76 to 58 percent of rejections); the (c''') cost at width 4 is the width's (15.6 against the band's 14.6 percent of reaching candidates), not the table's |
|
||||
| The attempts census, width 1 (fg8) | 1,500 eras; `logs/ktable-w1-*` | r = 0.191; mean attempt 0.24, max 4, 0 exhausted; (c'') 1.2 and (c''') 1.0 percent of reaching candidates (0.9 percent of all); accepted min ratio p1 0.9960 | the band at width 1: r = 0.565, (c''') 1.8 percent of reaching candidates |
|
||||
| The index-bit read (the fg8 rows) | the same eras | over 6 sigma at one site in 40.5 percent of eras at width 4 (51.1 at width 1), over 100 sigma 22.6 (36.3), over 300 sigma 9.3 (22.6); the bucket excess over +8 sigma 9.8 percent (18.7), p99 +28 (+50) | the band: 44.9 / 26.5 / 9.4 at width 4, 47.7 / 31.1 / 15.7 at width 1. The table's products (mad 11, mul 4, mulhi 2 against the base's 8, 8, 6) keep the bit-R mechanism where it was: this table does not change the standing fact of section 6.8 and does not worsen it |
|
||||
| The attempts census at the EXACT table (or = 0, fg9), widths 4 and 1 | OWED: 466 (width 4) and 401 (width 1) of 1,500 eras were done on build-3 when it stopped answering ssh at 16:4x BST (unreachable at 17:52); the fallback on build-2 (`fg-chain13.sh`, the fg9 build) waited 70 minutes on a full pool and had not started by the clock | lands as an amendment line when either box gives cores | one point of `or` (1.3 percent of instructions) separates these from the fg8 rows above, so the verdict does not turn on them |
|
||||
| The attempts census at the EXACT table (or = 0), widths 4 and 1 | DONE 18:20 BST on build-4, on the harness ported to class-v6 13885e16f (fold off, rw 0: the class v5 acceptance, the port reproducing section 6.8's p4, p8 and p10 rows to the last digit); 1,500 eras per width; `logs/ktable0-w4-*`, `logs/ktable0-w1-*` | width 4: r = 0.286, mean attempt 0.40, max 8, 0 exhausted; (c'') 1.3 and (c''') 13.9 percent of the 1,768 candidates reaching the 2^20 pass (11.7 percent of all); accepted min ratio p1 0.9960; the bit read over 6 sigma 38.7 percent of eras, over 300 sigma 9.1; bucket over +8 sigma 8.7. Width 1: r = 0.155, mean 0.18, max 5, 0 exhausted; (c'') 0.3 and (c''') 0.8 percent; bit over 6 sigma 47.5, over 300 19.5 | the exact table draws cheaper still than the or-floored copy (r 0.29 against 0.33 at width 4; `or` gone takes the last 1.3 percent of lossy instructions out) and reads the same at the floors and on the bit level: the verdict stands on the exact table |
|
||||
|
||||
VERDICT (18:00 BST, to the coordinator, the hash lane, the census lane and the research lane): 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.
|
||||
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.
|
||||
|
||||
## 7. The 16:30 BST report (the founder's clock, pulled from 09:00; a partial carries its count)
|
||||
|
||||
|
|
@ -339,7 +339,7 @@ VERDICT (18:00 BST, to the coordinator, the hash lane, the census lane and the r
|
|||
4. DONE 15:1x BST: the x4, x8 and x16 verifier rows (section 6.7, build-3 one core: 3.73 / 4.12 / 7.13 ms per warp; x16 outside the band on the 10 ms gate by scaling to the proxies).
|
||||
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 (1,500 eras each, build-3, the fg8 harness), the index-bit read, and attack-f8 at 2^20 on 64 seeds; running from 15:5x BST, the verdict to the coordinator, the hash, census and research lanes with its minute.
|
||||
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. Sources
|
||||
|
|
|
|||
480
docs/analysis/class-v6/logs/harness-v6-13885e16f.diff
Normal file
480
docs/analysis/class-v6/logs/harness-v6-13885e16f.diff
Normal file
|
|
@ -0,0 +1,480 @@
|
|||
diff --git a/igneum-pow/src/accept.rs b/igneum-pow/src/accept.rs
|
||||
index e02c92efe..b592235f3 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, 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,236 @@ 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,
|
||||
+ // class v6's own parts (the fold's dead-window register and whatever the branch adds next): one column
|
||||
+ _ => 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 base = LoadClass { shadow: Some(ShadowClass { instrs: shape, reps }), mixer_mult: mixer, ..V5_CLASS };
|
||||
+ // 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 61fe4874b..a0d7109ba 100644
|
||||
--- a/igneum-pow/src/generator.rs
|
||||
+++ b/igneum-pow/src/generator.rs
|
||||
@@ -637,6 +637,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),
|
||||
@@ -1059,14 +1062,27 @@ 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 {
|
||||
+ 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;
|
||||
+ return LoadClass { shadow: Some(ShadowClass { instrs: n, reps }), ..base };
|
||||
+ }
|
||||
+ }
|
||||
+ base
|
||||
+}
|
||||
+
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1499,6 +1515,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),
|
||||
@@ -1651,8 +1711,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, ..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];
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
binary 77835525ede65a55 commit v6 test family_gate_era_census from 0 seeds 1500 threads 16 start 2026-10-08T17:09:32Z
|
||||
lease: holding 16 pool cores (40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55, waited 0 s, class measure, mem 8 GB): family gate: family_gate_era_census ktable0-w1 seeds 0+1500
|
||||
|
||||
running 1 test
|
||||
test accept::tests::family_gate_era_census ... family_gate_era_census: 100 eras in 33 s
|
||||
family_gate_era_census: 200 eras in 59 s
|
||||
family_gate_era_census: 300 eras in 86 s
|
||||
family_gate_era_census: 400 eras in 113 s
|
||||
family_gate_era_census: 500 eras in 139 s
|
||||
family_gate_era_census: 600 eras in 165 s
|
||||
family_gate_era_census: 700 eras in 192 s
|
||||
family_gate_era_census: 800 eras in 219 s
|
||||
family_gate_era_census: 900 eras in 247 s
|
||||
family_gate_era_census: 1000 eras in 276 s
|
||||
family_gate_era_census: 1100 eras in 303 s
|
||||
family_gate_era_census: 1200 eras in 331 s
|
||||
family_gate_era_census: 1300 eras in 360 s
|
||||
family_gate_era_census: 1400 eras in 388 s
|
||||
family_gate_era_census: 1500 eras in 418 s
|
||||
family_gate_era_census: 1500 eras 0..1500 in 418 s on 16 threads -> /srv/builds/_adv-family-gate/logs/ktable0-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 418.43s
|
||||
|
||||
lease: released 16 pool cores after 418 s, exit 0
|
||||
end 2026-10-08T17:16:30Z rc 0
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,26 @@
|
|||
binary 77835525ede65a55 commit v6 test family_gate_era_census from 0 seeds 1500 threads 16 start 2026-10-08T17:09:32Z
|
||||
lease: holding 16 pool cores (8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23, waited 0 s, class measure, mem 8 GB): family gate: family_gate_era_census ktable0-w4 seeds 0+1500
|
||||
|
||||
running 1 test
|
||||
test accept::tests::family_gate_era_census ... family_gate_era_census: 100 eras in 50 s
|
||||
family_gate_era_census: 200 eras in 93 s
|
||||
family_gate_era_census: 300 eras in 135 s
|
||||
family_gate_era_census: 400 eras in 176 s
|
||||
family_gate_era_census: 500 eras in 213 s
|
||||
family_gate_era_census: 600 eras in 249 s
|
||||
family_gate_era_census: 700 eras in 284 s
|
||||
family_gate_era_census: 800 eras in 316 s
|
||||
family_gate_era_census: 900 eras in 347 s
|
||||
family_gate_era_census: 1000 eras in 382 s
|
||||
family_gate_era_census: 1100 eras in 418 s
|
||||
family_gate_era_census: 1200 eras in 463 s
|
||||
family_gate_era_census: 1300 eras in 505 s
|
||||
family_gate_era_census: 1400 eras in 549 s
|
||||
family_gate_era_census: 1500 eras in 592 s
|
||||
family_gate_era_census: 1500 eras 0..1500 in 592 s on 16 threads -> /srv/builds/_adv-family-gate/logs/ktable0-w4-family_gate_era_census-0-1500.tsv
|
||||
ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 80 filtered out; finished in 592.40s
|
||||
|
||||
lease: released 16 pool cores after 592 s, exit 0
|
||||
end 2026-10-08T17:19:24Z rc 0
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue