From 5359409f3a7837379d2095746778a100bff6c656 Mon Sep 17 00:00:00 2001 From: Vasilev Dmitrii Date: Fri, 7 Aug 2026 22:06:47 +0700 Subject: [PATCH] feat: cross-target bit-exact for IGLA RACE ternary MAC + gen-backend findings Applies the GF-T trainer's multi-target discipline to IGLA RACE. verify_igla_race.py emits ternary_mul/ternary_mac (the multiplier-free R-SI-1 MAC primitives) from specs/igla/race/ternary_mac.t27 to C and Rust via t27c and cross-checks against an independent reference over 800 vectors incl. edges (a=-128 sign-flip wrap, invalid codes, i32 edges): C == Rust == reference bit-exact. Verified the guard catches a corrupted sign-flip (161 mismatches). Finding: the FULL spec does NOT emit compilable C/Rust -- gen-c chokes on slice .len() (ternary_dot) and emits test functions twice; gen-rust emits serde derives (unavailable) + a non-Copy struct moved twice. The tool reports this and verifies the core by extracting decode/mul/mac. Finding: seal math_math-igla-primitives.json exists but specs/math/igla_primitives.t27 is absent from the tree. Wired into emit-bitexact-gate (SKIPs if cc/rustc/t27c absent). Refs #1764 Co-Authored-By: Claude Opus 4.8 --- .github/workflows/emit-bitexact-gate.yml | 5 + docs/NOW.md | 9 +- tools/verify_igla_race.py | 206 +++++++++++++++++++++++ 3 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 tools/verify_igla_race.py diff --git a/.github/workflows/emit-bitexact-gate.yml b/.github/workflows/emit-bitexact-gate.yml index 3777c35965..b0f4efcc50 100644 --- a/.github/workflows/emit-bitexact-gate.yml +++ b/.github/workflows/emit-bitexact-gate.yml @@ -19,6 +19,8 @@ on: - "tools/verify_multitarget.py" - "tools/verify_trainer_c.py" - "tools/fuzz_trainer.py" + - "tools/verify_igla_race.py" + - "specs/igla/race/ternary_mac.t27" - "specs/ternary/gft_smul.t27" - "specs/ternary/gft_sadd.t27" - ".github/workflows/emit-bitexact-gate.yml" @@ -56,3 +58,6 @@ jobs: - name: Differential-fuzz the trainer (random topologies, edge inputs) run: python3 tools/fuzz_trainer.py 40 + + - name: Cross-check IGLA RACE ternary MAC (C + Rust == model) + run: python3 tools/verify_igla_race.py diff --git a/docs/NOW.md b/docs/NOW.md index fa0ca8fa01..6731218b63 100644 --- a/docs/NOW.md +++ b/docs/NOW.md @@ -1,7 +1,14 @@ -# NOW — feat: differential fuzzer for the trainer (random topologies + edge inputs) (2026-08-07) +# NOW — feat: cross-target bit-exact for IGLA RACE ternary MAC + gen-backend findings (2026-08-07) Last updated: 2026-08-07 +## feat: apply the multi-target harness to IGLA RACE ternary_mac -- bridge + findings (Refs #1764) + +- Applied the GF-T trainer's multi-target discipline to IGLA RACE. `tools/verify_igla_race.py` emits `ternary_mul`/`ternary_mac` (the multiplier-free R-SI-1 MAC primitives, {-1,0,+1} weights) from `specs/igla/race/ternary_mac.t27` to C and Rust via t27c and cross-checks against an independent reference over 800 vectors incl. edges (a=-128 sign-flip wrap, invalid codes>2 decode to 0, i32-accumulator edges): **C == Rust == reference BIT-EXACT** -- the RACE ternary MAC core is now multi-target-verified, closing the "RACE specs sealed/simulated but not cross-checked" gap. Verified the guard catches a corrupted sign-flip (161 mismatches) +- **FINDING (gen-backend gaps on this IGLA spec):** the FULL spec does NOT emit compilable C or Rust -- gen-c chokes on slice `.len()` (in `ternary_dot`) and emits test functions twice (redefinition); gen-rust emits `serde::Serialize/Deserialize` derives (serde not available) + a non-Copy struct moved twice. The tool reports this as a NOTE and verifies the core arithmetic by extracting decode/mul/mac (which don't use slices). So RACE's "spec -> any target" is only true for the arithmetic core, not the whole spec +- **FINDING (provenance):** `.trinity/seals/math_math-igla-primitives.json` exists but its source `specs/math/igla_primitives.t27` is ABSENT from the tree (imported by `coder/arch.t27`, `coder/training.t27`) -- a live seal over a missing spec +- Wired into emit-bitexact-gate (SKIPs if cc/rustc/t27c absent). Realizes the IGLA<->GF-T-trainer bridge from the prior research cycle. Tool+CI only; Refs #1764 + ## feat: fuzz C-trainer == GF-T model over random topologies with edge values (Refs #1764) - The whole-trainer cross-target proof (verify_trainer_c) checked a few CHOSEN nets on a fixed 80-step sequence. `tools/fuzz_trainer.py` widens it to a RANDOMIZED space: random topology (1-3 inputs, 1-3 hidden layers of width 1-5, 1-3 outputs) x random training inputs with EDGE VALUES injected (offset-saturation-large 1e5, tiny 1e-9, exact 0/+-1/+-2/+-0.5), cross-checking the C trainer against the Python GF-T model per step diff --git a/tools/verify_igla_race.py b/tools/verify_igla_race.py new file mode 100644 index 0000000000..b122906ec3 --- /dev/null +++ b/tools/verify_igla_race.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""Cross-target bit-exactness for IGLA RACE's ternary MAC core. + +The RACE ternary accelerator specs (specs/igla/race/ternary_mac.t27) are sealed and +simulated but were never cross-checked across t27's backends. This applies the same +multi-target discipline the GF-T trainer uses: it emits `ternary_mul` / `ternary_mac` +(the multiplier-free R-SI-1 MAC primitives, {-1,0,+1} weights) to C and Rust via +t27c, compiles both, and cross-checks against an independent Python reference over +random + edge inputs -- a=-128 sign-flip wrap, invalid codes (>2 decode to 0), and +i32-accumulator edges. Closes the "RACE specs not multi-target-verified" gap. + +Self-contained + CI-friendly: SKIPs (exit 0) if t27c / cc / rustc is missing; a real +divergence exits 1. Run: python3 tools/verify_igla_race.py +""" +import os, re, sys, shutil, subprocess, tempfile, random + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SPEC = "specs/igla/race/ternary_mac.t27" +N = 800 + + +def skip(msg): + print(f"SKIP verify_igla_race: {msg}"); sys.exit(0) + + +def find_t27c(): + for p in ("target/debug/t27c", "target/release/t27c"): + c = os.path.join(ROOT, p) + if os.path.exists(c): + return c + return shutil.which("t27c") + + +def i8(x): return ((x + 128) & 0xFF) - 128 +def i32(x): return ((x + (1 << 31)) & 0xFFFFFFFF) - (1 << 31) + + +def ref_mul(a, code): + a = i8(a); d = 1 if code == 1 else (-1 if code == 2 else 0) + if d == 0: return 0 + return i8(a if d == 1 else -a) + + +def ref_mac(acc, a, code): return i32(i32(acc) + ref_mul(a, code)) + + +def gen_vectors(): + r = random.Random(7777) + A = [-128, -1, 0, 1, 127]; C = [0, 1, 2, 3, 255]; ACC = [0, (1 << 31) - 1, -(1 << 31), 1000000] + v = [] + for a in A: + for c in C: + for ac in ACC: + v.append((a, c, ac)) + while len(v) < N: + v.append((r.randint(-128, 127), r.choice([0, 1, 2, 3, r.randint(0, 255)]), + r.randint(-(1 << 31), (1 << 31) - 1))) + return v[:N] + + +def _brace_block(src, start): + """Return src[start .. matching close brace], brace-matched from the first {.""" + b = src.find("{", start) + if b < 0: + return None + depth, j = 0, b + while j < len(src): + if src[j] == "{": depth += 1 + elif src[j] == "}": + depth -= 1 + if depth == 0: + return src[start:j + 1] + j += 1 + return None + + +def _extract_def(src, sig): + """Extract the DEFINITION (sig followed by a `{...}` body), skipping any + prototype (`sig;`). Returns the block or None.""" + for m in re.finditer(re.escape(sig), src): + rest = src[m.end():] + nxt = rest.lstrip() + if nxt[:1] == "{": + return _brace_block(src, m.start()) + return None + + +def full_spec_compiles(t27c, wd): + """Diagnostic: does the WHOLE spec emit compilable C and Rust? Returns (c_ok, + rust_ok, note). This surfaces gen-backend gaps in the IGLA spec (slice .len(), + duplicate test emission, serde deps, non-Copy struct).""" + c = subprocess.run([t27c, "gen-c", SPEC], capture_output=True, text=True, cwd=ROOT).stdout + open(os.path.join(wd, "full.c"), "w").write('#define assert_eq(x,y) ((void)0)\n' + c + "\nint main(){return 0;}\n") + c_ok = subprocess.run(["cc", "-c", "-o", os.path.join(wd, "f.o"), os.path.join(wd, "full.c")], + cwd=wd, capture_output=True, text=True).returncode == 0 + r = subprocess.run([t27c, "gen-rust", SPEC], capture_output=True, text=True, cwd=ROOT).stdout + open(os.path.join(wd, "full.rs"), "w").write(r + "\nfn main(){}\n") + r_ok = subprocess.run(["rustc", "-A", "warnings", "--emit=metadata", "-o", os.path.join(wd, "f.rmeta"), + os.path.join(wd, "full.rs")], cwd=wd, capture_output=True, text=True).returncode == 0 + return c_ok, r_ok + + +def _core_c(t27c): + src = subprocess.run([t27c, "gen-c", SPEC], capture_output=True, text=True, cwd=ROOT).stdout + st = re.search(r"typedef struct\s*\{[^}]*\}\s*TernaryWeight\s*;", src) + defs = [_extract_def(src, s) for s in ( + "int8_t ternary_decode(TernaryWeight w)", + "int8_t ternary_mul(int8_t a, TernaryWeight w)", + "int32_t ternary_mac(int32_t acc, int8_t a, TernaryWeight w)")] + if not st or any(d is None for d in defs): + return None + return "#include \n#include \n" + st.group(0) + "\n" + "\n".join(defs) + + +def _core_rust(t27c): + src = subprocess.run([t27c, "gen-rust", SPEC], capture_output=True, text=True, cwd=ROOT).stdout + ms = re.search(r"pub struct TernaryWeight\b", src) + if not ms: + return None + st = _brace_block(src, ms.start()) # "pub struct TernaryWeight { pub code: u8, }" + blocks = [] + for name in ("ternary_decode", "ternary_mul", "ternary_mac"): + m = re.search(r"pub fn " + name + r"\b", src) + if not m: + return None + blk = _brace_block(src, m.start()) # Rust has definitions only, no prototypes + if blk is None: + return None + blocks.append(blk) + if st is None: + return None + return "#[derive(Clone, Copy)]\n" + st + "\n" + "\n".join(blocks) + + +def run_c(t27c, vecs, wd): + core = _core_c(t27c) + if core is None: + return None + A = ",".join(str(a) for a, _, _ in vecs); C = ",".join(str(c) for _, c, _ in vecs) + AC = ",".join(str(ac) for _, _, ac in vecs) + src = (core + f'\nint main(){{int8_t A[]={{{A}}}; uint8_t C[]={{{C}}}; int32_t AC[]={{{AC}}}; int n={len(vecs)};' + f'for(int i=0;i