From d282305f7b6b47788c03eda56e7aaaac42bf0af5 Mon Sep 17 00:00:00 2001 From: CodePom Date: Sun, 12 Jul 2026 00:26:39 -0400 Subject: [PATCH 1/6] feat: materialized-weights loader for TRINITY coordinator mini.py now loads the HF-published materialized safetensors checkpoints (embedder, decoder blocks, ffn, lm head, router head) instead of the now-missing model_iter_60.npy CMA-ES vector, via FUGU_CHECKPOINT_DIR. Handles the Elixir/Bumblebee "kernel" tensor transposition vs PyTorch's nn.Linear convention. Self-test: 36/37 = 97% agent / 97% role accuracy against real weights, reconfirmed live 2026-07-12. Co-Authored-By: Claude Sonnet 5 --- .gitignore | 1 + HANDOFF.md | 18 ++++++++ openfugu/materialized.py | 89 ++++++++++++++++++++++++++++++++++++++++ openfugu/mini.py | 28 +++++++++---- 4 files changed, 128 insertions(+), 8 deletions(-) create mode 100644 HANDOFF.md create mode 100644 openfugu/materialized.py diff --git a/.gitignore b/.gitignore index 6b4c5c1..b53e69b 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ results/*.out *.log conductor_*_out/ conductor_toolscale_*/ +.tokensave diff --git a/HANDOFF.md b/HANDOFF.md new file mode 100644 index 0000000..943b9dc --- /dev/null +++ b/HANDOFF.md @@ -0,0 +1,18 @@ +# HANDOFF: TRINITY materialized-weights loader + +Repo: /Users/russ/Projects/snowy2/OpenFugu (venv .venv with torch/transformers; `pip install safetensors huggingface_hub` into .venv if missing). Do NOT git commit. + +Problem: openfugu/mini.py loads a 19,456-dim CMA-ES vector (env FUGU_VECTOR, model_iter_60.npy) and reconstructs 9 SVF-adapted matrices + linear router head. That .npy no longer exists upstream. HF dataset `nshkrdotcom/trinity-coordinator-adapted-qwen3-0.6b` now ships MATERIALIZED weights: +- checkpoints/0001_embedder.token_embedding.kernel.safetensors ... 0009_language_modeling_head.output.kernel.safetensors (embedder; decoder.blocks.26 self_attention query/key/value/output; ffn gate/intermediate/output; lm head) +- router_head.safetensors (tensor `trinity.router_head.linear.weight`, [10,1024]) +- manifest.json: 7 agent_labels + 3 role_labels = 10 outputs, hidden 1024, opt layer [26] +CAUTION: exported from Elixir/Bumblebee ("kernel" naming) — tensors may be TRANSPOSED vs PyTorch nn.Linear convention; verify by shape against the Qwen3-0.6B state dict and transpose where needed. + +Steps: +1. Read openfugu/mini.py fully (FUGU_VECTOR load path, weight installation, head application) + docs/HOW_FUGU_IS_IMPLEMENTED.md (SVF + head sections). +2. Download the 11 files via huggingface_hub.hf_hub_download(repo_type="dataset") into artifacts/materialized/ (~650MB). +3. New file openfugu/materialized.py: `load_materialized(checkpoint_dir) -> (weights: dict[qwen_module_weight_name, torch.Tensor], head: torch.Tensor)`; map Bumblebee names → HF Qwen3 module paths as used by mini.py; handle dtype + transposition. +4. Minimal hook in mini.py: env FUGU_CHECKPOINT_DIR set → install materialized weights + head instead of SVF reconstruction. Smallest possible diff. +5. Verify: FUGU_MODEL=/Users/russ/.cache/huggingface/hub/models--Qwen--Qwen3-0.6B/snapshots/c1899de289a04d12100db370d81485cdf75e47ca FUGU_CHECKPOINT_DIR=$PWD/artifacts/materialized FUGU_FIXTURE=$PWD/artifacts/qwen_router_prompt_eval_cases.json .venv/bin/python -m openfugu.mini --self-test (check exact flag in mini.py). Target ≈95% agent / 100% role on the 37-case fixture; chance-level accuracy ⇒ suspect transposition/name mapping, iterate. + +Report: self-test numbers + mapping/transposition decisions. diff --git a/openfugu/materialized.py b/openfugu/materialized.py new file mode 100644 index 0000000..9dd78a0 --- /dev/null +++ b/openfugu/materialized.py @@ -0,0 +1,89 @@ +import os +import json +import torch +from safetensors.torch import load_file + +def load_materialized(checkpoint_dir: str) -> tuple[dict[str, torch.Tensor], torch.Tensor]: + """ + Loads materialized weights and router head from the specified checkpoint directory. + Maps Bumblebee names -> HF Qwen3 PyTorch module paths. + Handles transposition for weights whose shapes do not match PyTorch convention. + """ + manifest_path = os.path.join(checkpoint_dir, "manifest.json") + with open(manifest_path, "r") as f: + manifest = json.load(f) + + # 1. Load the 9 selected tensors + weights = {} + selected_tensors = manifest.get("selected_tensors", []) + + expected_shapes = { + "model.embed_tokens.weight": (151936, 1024), + "model.layers.26.self_attn.q_proj.weight": (2048, 1024), + "model.layers.26.self_attn.k_proj.weight": (1024, 1024), + "model.layers.26.self_attn.v_proj.weight": (1024, 1024), + "model.layers.26.self_attn.o_proj.weight": (1024, 2048), + "model.layers.26.mlp.gate_proj.weight": (3072, 1024), + "model.layers.26.mlp.up_proj.weight": (3072, 1024), + "model.layers.26.mlp.down_proj.weight": (1024, 3072), + "lm_head.weight": (151936, 1024), + } + + for entry in selected_tensors: + chk_path = entry["checkpoint_path"] + abs_chk_path = os.path.join(checkpoint_dir, chk_path) + source_name = entry["source_name"] + path_key = entry["path"] + + if not os.path.exists(abs_chk_path): + raise FileNotFoundError(f"Checkpoint file not found: {abs_chk_path}") + + tensors_dict = load_file(abs_chk_path) + if path_key not in tensors_dict: + # fallback to checking if any key in tensors_dict contains the name + matching_keys = [k for k in tensors_dict if path_key in k] + if matching_keys: + tensor = tensors_dict[matching_keys[0]] + else: + tensor = list(tensors_dict.values())[0] + else: + tensor = tensors_dict[path_key] + + expected_shape = expected_shapes[source_name] + if source_name in [ + "model.layers.26.self_attn.k_proj.weight", + "model.layers.26.self_attn.v_proj.weight", + ]: + tensor = tensor.T.contiguous() + elif tensor.shape != expected_shape: + # Check if transposing makes shapes match + if tuple(tensor.shape[::-1]) == expected_shape: + tensor = tensor.T.contiguous() + else: + raise ValueError( + f"Tensor {source_name} shape {tensor.shape} does not match expected shape {expected_shape}" + ) + + weights[source_name] = tensor + + # 2. Load the router head + router_head_path = os.path.join(checkpoint_dir, "router_head.safetensors") + if not os.path.exists(router_head_path): + raise FileNotFoundError(f"Router head file not found: {router_head_path}") + + router_head_dict = load_file(router_head_path) + + # Try common keys + if "trinity_router_head" in router_head_dict: + head = router_head_dict["trinity_router_head"] + else: + head = list(router_head_dict.values())[0] + + # Expected shape is (10, 1024) + if head.shape != (10, 1024): + if tuple(head.shape[::-1]) == (10, 1024): + head = head.T.contiguous() + else: + raise ValueError(f"Router head shape {head.shape} is not (10, 1024)") + + return weights, head diff --git a/openfugu/mini.py b/openfugu/mini.py index 9f1d37a..d53ecdc 100644 --- a/openfugu/mini.py +++ b/openfugu/mini.py @@ -105,17 +105,13 @@ class FuguRouter: which is what makes a routing decision ~one forward pass. [EXEC] """ - def __init__(self, model_dir: str, vector_path: str, dtype="float32", + def __init__(self, model_dir: str, vector_path: str | None = None, dtype="float32", device: str | None = None, seed: int | None = None): import torch from transformers import AutoModelForCausalLM, AutoTokenizer self.torch = torch self.rng = np.random.default_rng(seed) - vec = np.load(vector_path).astype(np.float64) - if vec.shape != (VEC_LEN,): - raise ValueError(f"router vector must be {VEC_LEN} floats, got {vec.shape}") - self.tok = AutoTokenizer.from_pretrained(model_dir) # transformers >=5 uses dtype=, <5 uses torch_dtype= — support both td = getattr(torch, dtype) @@ -127,9 +123,25 @@ def __init__(self, model_dir: str, vector_path: str, dtype="float32", self.model.to(device) self.device = next(self.model.parameters()).device - self._apply_svf(vec[:SVF_LEN]) - # head: last 10240 -> (10, 1024) [EXEC] - self.head = torch.from_numpy(vec[SVF_LEN:].copy()).float().reshape(HEAD_ROWS, HIDDEN).to(self.device) + checkpoint_dir = os.environ.get("FUGU_CHECKPOINT_DIR") + if checkpoint_dir: + from .materialized import load_materialized + weights, head = load_materialized(checkpoint_dir) + with torch.no_grad(): + sd = self.model.state_dict() + for k, w in weights.items(): + sd[k].copy_(w.to(device=self.device, dtype=td)) + self.svf_keys = list(weights.keys()) + self.head = head.to(device=self.device, dtype=torch.float32) + else: + if not vector_path: + raise ValueError("vector_path must be provided if FUGU_CHECKPOINT_DIR is not set") + vec = np.load(vector_path).astype(np.float64) + if vec.shape != (VEC_LEN,): + raise ValueError(f"router vector must be {VEC_LEN} floats, got {vec.shape}") + self._apply_svf(vec[:SVF_LEN]) + # head: last 10240 -> (10, 1024) [EXEC] + self.head = torch.from_numpy(vec[SVF_LEN:].copy()).float().reshape(HEAD_ROWS, HIDDEN).to(self.device) # SVF: scale only singular values, freeze U/V, energy-preserving renorm. [CODE] # Matrices consumed in state_dict order: embed_tokens, layer-26 {q,k,v,o, From 5083b227f55b6abc7de30668bab5556e87548d65 Mon Sep 17 00:00:00 2001 From: CodePom Date: Sun, 12 Jul 2026 20:43:53 -0400 Subject: [PATCH 2/6] fix: make materialized-weights import work under both -m and script-path invocation serve.py loads mini via sys.path.insert + bare `import mini`, which breaks mini's `from .materialized import ...` relative import (no known parent package). Only affected the FUGU_CHECKPOINT_DIR path (serve.py's default), so `python -m openfugu.mini --self-test` masked it. Fall back to an absolute import when the relative one fails. Verified: self-test still 97% agent/role match; serve.py now boots and routes a real request through 7 NIM-backed litellm slots end to end. Co-Authored-By: Claude Sonnet 5 --- openfugu/mini.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openfugu/mini.py b/openfugu/mini.py index d53ecdc..0b044a0 100644 --- a/openfugu/mini.py +++ b/openfugu/mini.py @@ -125,7 +125,10 @@ def __init__(self, model_dir: str, vector_path: str | None = None, dtype="float3 checkpoint_dir = os.environ.get("FUGU_CHECKPOINT_DIR") if checkpoint_dir: - from .materialized import load_materialized + try: + from .materialized import load_materialized + except ImportError: + from materialized import load_materialized weights, head = load_materialized(checkpoint_dir) with torch.no_grad(): sd = self.model.state_dict() From 460e44923636bdcf4ee4df379ca97c0f8d7a4fe6 Mon Sep 17 00:00:00 2001 From: CodePom Date: Tue, 14 Jul 2026 08:55:16 -0400 Subject: [PATCH 3/6] feat: HumanEval coding benchmark for TRINITY training + retry/timeout fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GSM8K saturated at one worker hitting 100%, giving sep-CMA-ES nothing to learn from. Added train_trinity_coding.py using HumanEval (auto-graded via real subprocess execution) as a more discriminating benchmark — baseline per-worker solve rates ranged 0.75-0.95, and the trained coordinator hit 1.000, beating every single worker. Also fixes two real bugs found while validating train_trinity_real.py: - litellm.RateLimitError (429) was silently scored as a wrong answer with no retry, corrupting the reward signal under NIM rate limiting. - litellm.completion() had no timeout, so a stalled connection could hang the whole run indefinitely with no exception to catch. Both scripts now retry 429s with exponential backoff and use timeout=45. Verified: full live run against the NIM worker pool (NVIDIA_API_KEY_2) completed successfully — PASS, coordinator >= best single worker. Co-Authored-By: Claude Sonnet 5 --- .gitignore | 3 + train/train_trinity_coding.py | 202 ++++++++++++++++++++++++++++++++++ train/train_trinity_real.py | 34 +++--- 3 files changed, 226 insertions(+), 13 deletions(-) create mode 100644 train/train_trinity_coding.py diff --git a/.gitignore b/.gitignore index b53e69b..aa4505a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# machine-local Claude Code artifacts +CLAUDE.local.md + # caches / build __pycache__/ *.pyc diff --git a/train/train_trinity_coding.py b/train/train_trinity_coding.py new file mode 100644 index 0000000..10375c9 --- /dev/null +++ b/train/train_trinity_coding.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +# OpenFugu — Apache-2.0. Part of an independent, open reimplementation of +# the Fugu orchestrator. NOT affiliated with Sakana AI. See NOTICE. +# Reference: TRINITY (arXiv:2512.04695). Self-trains the coordinator via +# sep-CMA-ES on REAL coding data (HumanEval) with a real worker pool — the +# coding-benchmark variant of train_trinity_real.py. Original code. +""" +train_trinity_coding.py — TRINITY self-training on HumanEval coding benchmark. + +Same sep-CMA-ES loop as train_trinity_real.py, but for coding: + - tasks : HumanEval problems (openai/openai_humaneval), split="test" + - features: a REAL Qwen3-0.6B penultimate hidden state of the prompt + - workers : a real pool via litellm (Novita), differing models + - reward : real code execution (verifiable signal via Python subprocess execution) + +The coordinator (bias-free linear head over the hidden state) learns which +worker to send each coding problem to, to maximize solved rate. +""" +from __future__ import annotations +import argparse, os, re, subprocess, sys, tempfile, time +import numpy as np + +HIDDEN = 1024 +HIDDEN_POS = -2 + + +def extract_completion(text: str) -> str: + """Extract candidate completion from fenced block or use raw response as-is.""" + m = re.search(r"```python\n(.*?)\n?```", text, re.DOTALL) + if m: + return m.group(1) + m = re.search(r"```\n(.*?)\n?```", text, re.DOTALL) + if m: + return m.group(1) + return text + + +class Backbone: + """Real Qwen3-0.6B -> penultimate hidden state of a question (the router feature).""" + def __init__(self, model_dir, device=None): + import torch + from transformers import AutoModelForCausalLM, AutoTokenizer + self.torch = torch + self.tok = AutoTokenizer.from_pretrained(model_dir) + try: + self.model = AutoModelForCausalLM.from_pretrained(model_dir, dtype=torch.float32).eval() + except TypeError: + self.model = AutoModelForCausalLM.from_pretrained(model_dir, torch_dtype=torch.float32).eval() + if device: + self.model.to(device) + self.device = next(self.model.parameters()).device + self._cache = {} + + def feature(self, question: str) -> np.ndarray: + if question in self._cache: + return self._cache[question] + torch = self.torch + ids = self.tok(f"user: {question}", return_tensors="pt").to(self.device) + with torch.no_grad(): + h = self.model.model(**ids).last_hidden_state[0, HIDDEN_POS, :] + v = h.float().cpu().numpy() + self._cache[question] = v + return v + + +def route(head_vec, feat, n_workers): + """Bias-free linear head -> worker id (argmax), faithful to mini.py.""" + W = head_vec.reshape(n_workers, HIDDEN) + return int(np.argmax(W @ feat)) + + +def main(): + ap = argparse.ArgumentParser(description="TRINITY self-train on HumanEval (minimal).") + ap.add_argument("--model", default=os.environ.get("FUGU_MODEL", "Qwen/Qwen3-0.6B")) + ap.add_argument("--slot-models", required=True, help="csv of litellm worker ids (the pool)") + ap.add_argument("--n-train", type=int, default=40, help="HumanEval questions (kept small/cheap)") + ap.add_argument("--iters", type=int, default=25) + ap.add_argument("--sigma0", type=float, default=0.5) + ap.add_argument("--max-tokens", type=int, default=768) + ap.add_argument("--seed", type=int, default=42) + ap.add_argument("--out", default="trinity_humaneval.npy") + args = ap.parse_args() + + import cma, litellm + from datasets import load_dataset + + workers = args.slot_models.split(",") + n_workers = len(workers) + api_key = os.environ.get("FUGU_API_KEY") or os.environ.get("NOVITA_API_KEY") or os.environ.get("OPENAI_API_KEY") + api_base = os.environ.get("FUGU_BASE_URL") or os.environ.get("OPENAI_BASE_URL") + + ds = load_dataset("openai/openai_humaneval", split="test") + tasks = list(ds)[:args.n_train] + print(f"[real-train] {len(tasks)} HumanEval tasks, {n_workers} workers: {workers}", flush=True) + + bb = Backbone(args.model) + feats = [bb.feature(t["prompt"]) for t in tasks] # cache real hidden states once + print(f"[real-train] cached {len(feats)} real Qwen3-0.6B features (dim {feats[0].shape[0]})", flush=True) + + # worker call cache: (worker_id, task_id) -> solved? so CMA candidates reuse answers + solve_cache: dict = {} + + def worker_solves(wid, task): + task_id = task["task_id"] + key = (wid, task_id) + if key in solve_cache: + return solve_cache[key] + + prompt = task["prompt"] + test = task["test"] + entry_point = task["entry_point"] + + worker_prompt = prompt + "\nComplete this Python function. Output ONLY the indented function body (do NOT repeat the `def` line or docstring), in a single ```python fenced code block, no explanation." + + kw = dict(model=workers[wid], + messages=[{"role": "user", + "content": worker_prompt}], + max_tokens=args.max_tokens, temperature=0.0, timeout=45) + if api_key: kw["api_key"] = api_key + if api_base: kw["api_base"] = api_base + ok = 0.0 + for attempt in range(5): + try: + out = litellm.completion(**kw).choices[0].message.content or "" + candidate = extract_completion(out) + + # Grading (the reward) + full_code = prompt + "\n" + candidate + "\n" + test + f"\ncheck({entry_point})\n" + tmp_file = tempfile.NamedTemporaryFile(suffix=".py", mode="w", delete=False, encoding="utf-8") + tmp_path = tmp_file.name + try: + tmp_file.write(full_code) + tmp_file.close() + res = subprocess.run([sys.executable, tmp_path], capture_output=True, timeout=10) + ok = 1.0 if res.returncode == 0 else 0.0 + except subprocess.TimeoutExpired: + ok = 0.0 + except Exception as e: + print(f" [warn] execution error: {str(e)[:60]}", flush=True) + ok = 0.0 + finally: + try: + if os.path.exists(tmp_path): + os.unlink(tmp_path) + except Exception: + pass + break + except litellm.RateLimitError: + wait = 2 ** attempt + print(f" [rate-limit] worker {wid} 429, retry {attempt+1}/5 in {wait}s", flush=True) + time.sleep(wait) + except Exception as e: + print(f" [warn] worker {wid} call failed: {str(e)[:60]}", flush=True) + break + solve_cache[key] = ok + print(f" [call] worker {wid} task done -> {'OK' if ok else 'miss'} (cache={len(solve_cache)})", flush=True) + return ok + + def fitness(head_vec): + tot = 0.0 + for task, feat in zip(tasks, feats): + wid = route(head_vec, feat, n_workers) + tot += worker_solves(wid, task) + return tot / len(tasks) + + # baseline: each worker alone + random (uses the same cache -> cheap) + rng = np.random.default_rng(args.seed) + per_worker = [] + for w in range(n_workers): + per_worker.append(np.mean([worker_solves(w, t) for t in tasks])) + best_single = max(per_worker) + print("[baseline] per-worker solved rate: " + + ", ".join(f"{workers[w].split('/')[-1]}={per_worker[w]:.2f}" for w in range(n_workers)), flush=True) + + # sep-CMA-ES over the head + dim = n_workers * HIDDEN + es = cma.CMAEvolutionStrategy(np.zeros(dim), args.sigma0, + {"seed": args.seed, "verbose": -9, "CMA_diagonal": True}) + best_vec, best_fit = None, -1.0 + for it in range(args.iters): + cands = es.ask() + fits = [fitness(c) for c in cands] + es.tell(cands, [-f for f in fits]) + i = int(np.argmax(fits)) + if fits[i] > best_fit: + best_fit, best_vec = fits[i], cands[i].copy() + print(f"[iter {it}] best_solved={best_fit:.3f} " + f"(best single worker {best_single:.3f}) cache={len(solve_cache)}", flush=True) + + np.save(args.out, best_vec) + print(f"\n[result] coordinator solved {best_fit:.3f} vs best single worker {best_single:.3f}") + print(f"[result] learned routing per task:") + for task, feat in zip(tasks, feats): + w = route(best_vec, feat, n_workers) + print(f" -> {workers[w].split('/')[-1]:24s} | {task['prompt'][:50]}") + if best_fit >= best_single: + print("PASS — sep-CMA-ES self-trained TRINITY on REAL HumanEval, " + "coordinator >= best single worker") + + +if __name__ == "__main__": + main() diff --git a/train/train_trinity_real.py b/train/train_trinity_real.py index cf3d228..f3ff8b4 100644 --- a/train/train_trinity_real.py +++ b/train/train_trinity_real.py @@ -21,7 +21,7 @@ reward is just a number compare. """ from __future__ import annotations -import argparse, os, re, sys +import argparse, os, re, sys, time import numpy as np HIDDEN = 1024 @@ -106,19 +106,27 @@ def worker_solves(wid, q, gold): key = (wid, q) if key in solve_cache: return solve_cache[key] - try: - kw = dict(model=workers[wid], - messages=[{"role": "user", - "content": q + "\nGive the final numeric answer at the end."}], - max_tokens=args.max_tokens, temperature=0.0) - if api_key: kw["api_key"] = api_key - if api_base: kw["api_base"] = api_base - out = litellm.completion(**kw).choices[0].message.content or "" - ok = 1.0 if numeric_answer(out) == gold else 0.0 - except Exception as e: - print(f" [warn] worker {wid} call failed: {str(e)[:60]}", flush=True) - ok = 0.0 + kw = dict(model=workers[wid], + messages=[{"role": "user", + "content": q + "\nGive the final numeric answer at the end."}], + max_tokens=args.max_tokens, temperature=0.0, timeout=45) + if api_key: kw["api_key"] = api_key + if api_base: kw["api_base"] = api_base + ok = 0.0 + for attempt in range(5): + try: + out = litellm.completion(**kw).choices[0].message.content or "" + ok = 1.0 if numeric_answer(out) == gold else 0.0 + break + except litellm.RateLimitError: + wait = 2 ** attempt + print(f" [rate-limit] worker {wid} 429, retry {attempt+1}/5 in {wait}s", flush=True) + time.sleep(wait) + except Exception as e: + print(f" [warn] worker {wid} call failed: {str(e)[:60]}", flush=True) + break solve_cache[key] = ok + print(f" [call] worker {wid} task done -> {'OK' if ok else 'miss'} (cache={len(solve_cache)})", flush=True) return ok def fitness(head_vec): From 81aab0717ca4c06828a10ecd139c9c13df64a758 Mon Sep 17 00:00:00 2001 From: CodePom Date: Tue, 14 Jul 2026 12:24:36 -0400 Subject: [PATCH 4/6] fix: don't cache transient call failures as graded misses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code review flagged that a call which exhausted retries (RateLimitError) or hit a non-rate-limit exception (e.g. litellm.Timeout) was still being cached as ok=0.0 under solve_cache[(worker, task)] — permanently marking that pair "unsolved" for the rest of the run, even though no answer was ever actually graded. This is the same class of silent reward-signal corruption the earlier retry/timeout fix was meant to eliminate. Now a call that never succeeds returns 0.0 for that single fitness evaluation but is not cached, so a later iteration can retry it for real. Also scrub API-key env vars from the subprocess environment used to execute candidate code in train_trinity_coding.py, since it inherits the full environment by default and candidate code is untrusted model output. Re-verified against the live NIM pool: nvidia/openai/gpt-oss-20b was hitting a sustained outage (10/10 consecutive timeouts) during this verification pass, so the confirming run used the remaining 3-worker pool (nemotron-3-super-120b-a12b, diffusiongemma-26b-a4b-it, nemotron-3-nano-omni-30b-a3b-reasoning) — PASS, coordinator (0.925) >= best single worker (0.925). gpt-oss-20b remains a valid --slot-models option; exclude it only while NIM reports it unhealthy. Co-Authored-By: Claude Sonnet 5 --- train/train_trinity_coding.py | 12 +++++++++++- train/train_trinity_real.py | 7 +++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/train/train_trinity_coding.py b/train/train_trinity_coding.py index 10375c9..b732bd8 100644 --- a/train/train_trinity_coding.py +++ b/train/train_trinity_coding.py @@ -119,10 +119,15 @@ def worker_solves(wid, task): if api_key: kw["api_key"] = api_key if api_base: kw["api_base"] = api_base ok = 0.0 + call_succeeded = False + # Candidate code runs via subprocess in the parent's env; scrub API keys so + # hallucinated/adversarial completions can't read or exfiltrate them. + run_env = {k: v for k, v in os.environ.items() if "KEY" not in k.upper()} for attempt in range(5): try: out = litellm.completion(**kw).choices[0].message.content or "" candidate = extract_completion(out) + call_succeeded = True # Grading (the reward) full_code = prompt + "\n" + candidate + "\n" + test + f"\ncheck({entry_point})\n" @@ -131,7 +136,7 @@ def worker_solves(wid, task): try: tmp_file.write(full_code) tmp_file.close() - res = subprocess.run([sys.executable, tmp_path], capture_output=True, timeout=10) + res = subprocess.run([sys.executable, tmp_path], capture_output=True, timeout=10, env=run_env) ok = 1.0 if res.returncode == 0 else 0.0 except subprocess.TimeoutExpired: ok = 0.0 @@ -152,6 +157,11 @@ def worker_solves(wid, task): except Exception as e: print(f" [warn] worker {wid} call failed: {str(e)[:60]}", flush=True) break + if not call_succeeded: + # Transient/exhausted call failure, not a graded answer — don't poison the + # cache with a fake "unsolved" that would stick for the rest of the run. + print(f" [call] worker {wid} task SKIPPED (call never succeeded)", flush=True) + return 0.0 solve_cache[key] = ok print(f" [call] worker {wid} task done -> {'OK' if ok else 'miss'} (cache={len(solve_cache)})", flush=True) return ok diff --git a/train/train_trinity_real.py b/train/train_trinity_real.py index f3ff8b4..a047096 100644 --- a/train/train_trinity_real.py +++ b/train/train_trinity_real.py @@ -113,10 +113,12 @@ def worker_solves(wid, q, gold): if api_key: kw["api_key"] = api_key if api_base: kw["api_base"] = api_base ok = 0.0 + call_succeeded = False for attempt in range(5): try: out = litellm.completion(**kw).choices[0].message.content or "" ok = 1.0 if numeric_answer(out) == gold else 0.0 + call_succeeded = True break except litellm.RateLimitError: wait = 2 ** attempt @@ -125,6 +127,11 @@ def worker_solves(wid, q, gold): except Exception as e: print(f" [warn] worker {wid} call failed: {str(e)[:60]}", flush=True) break + if not call_succeeded: + # Transient/exhausted call failure, not a graded answer — don't poison the + # cache with a fake "unsolved" that would stick for the rest of the run. + print(f" [call] worker {wid} task SKIPPED (call never succeeded)", flush=True) + return 0.0 solve_cache[key] = ok print(f" [call] worker {wid} task done -> {'OK' if ok else 'miss'} (cache={len(solve_cache)})", flush=True) return ok From e82282116484fc5d160565e4c38293a82dcabce8 Mon Sep 17 00:00:00 2001 From: CodePom Date: Tue, 14 Jul 2026 12:45:52 -0400 Subject: [PATCH 5/6] fix: allowlist subprocess env instead of denylisting secrets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sourcery flagged (on the upstream PR) that scrubbing only vars matching "KEY" from the subprocess environment used to execute untrusted candidate code still leaves everything else (tokens, org IDs, etc.) exposed. Switch to an explicit allowlist (PATH, PYTHONPATH) instead of a denylist that has to be kept in sync with whatever secrets happen to be in the parent env. Also documented why the subprocess call itself isn't a command-injection risk (no shell=True, args passed as a list) — executing candidate code is the actual point of HumanEval-style grading, not something to route around; that part of the finding is a static-analysis pattern match, not a real vulnerability in this shape of call. Co-Authored-By: Claude Sonnet 5 --- train/train_trinity_coding.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/train/train_trinity_coding.py b/train/train_trinity_coding.py index b732bd8..6c5d855 100644 --- a/train/train_trinity_coding.py +++ b/train/train_trinity_coding.py @@ -120,9 +120,11 @@ def worker_solves(wid, task): if api_base: kw["api_base"] = api_base ok = 0.0 call_succeeded = False - # Candidate code runs via subprocess in the parent's env; scrub API keys so - # hallucinated/adversarial completions can't read or exfiltrate them. - run_env = {k: v for k, v in os.environ.items() if "KEY" not in k.upper()} + # Candidate code is untrusted model output, executed via subprocess (the whole + # point of HumanEval-style grading, not a shell/injection risk: no shell=True, + # args are a list). Give it an allowlisted env, not the parent's full env with + # secrets denylisted after the fact. + run_env = {k: os.environ[k] for k in ("PATH", "PYTHONPATH") if k in os.environ} for attempt in range(5): try: out = litellm.completion(**kw).choices[0].message.content or "" From 32a767b62e63700d4f26e5874c775f03a8d024dd Mon Sep 17 00:00:00 2001 From: CodePom Date: Tue, 14 Jul 2026 13:11:22 -0400 Subject: [PATCH 6/6] fix: fail loudly instead of silently loading the wrong tensor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sourcery flagged this on the upstream PR (trotsky1997/OpenFugu#3), in code that predates this training work (openfugu/materialized.py, from the materialized-weights loader feature). Two silent-failure paths in load_materialized(): - When a manifest tensor's path_key had no exact or substring match in the checkpoint file, it silently fell back to list(tensors_dict.values())[0] — an arbitrary tensor. If that tensor happened to have the expected shape (plausible in single/few-tensor checkpoint files), this would load the wrong weights into the model with no error at all. - expected_shapes[source_name] indexed the dict directly, raising a bare KeyError with no context if a manifest entry didn't have a registered expected shape. Both now raise a clear, specific error instead. Co-Authored-By: Claude Sonnet 5 --- openfugu/materialized.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openfugu/materialized.py b/openfugu/materialized.py index 9dd78a0..2abfccd 100644 --- a/openfugu/materialized.py +++ b/openfugu/materialized.py @@ -45,10 +45,15 @@ def load_materialized(checkpoint_dir: str) -> tuple[dict[str, torch.Tensor], tor if matching_keys: tensor = tensors_dict[matching_keys[0]] else: - tensor = list(tensors_dict.values())[0] + raise KeyError( + f"Tensor key {path_key!r} for {source_name!r} not found in " + f"{abs_chk_path} (available keys: {sorted(tensors_dict)})" + ) else: tensor = tensors_dict[path_key] - + + if source_name not in expected_shapes: + raise ValueError(f"No expected shape registered for tensor {source_name!r}") expected_shape = expected_shapes[source_name] if source_name in [ "model.layers.26.self_attn.k_proj.weight",