Experiment: 53bce29
rad experiment show 53bce2962c861acd43fe014810bd8a4900070e28
by hdhhdh@did:key:z6MkfuXgBSe5G8U6d5NuVbvrbuXRwzYjKJWPPddXgbVjqT9n · Jun 9 22:48 2026
first --log-radicle run
Measurements
MetricBaselineThe benchmark measurement of the unmodified codeCandidateThe code with the proposed optimization appliedCandidate/Parent ΔThe performance change from baseline to candidate
val_acc (%) primary0.000 %0.973 % (n=1)0.00%
val_loss secondary0.0000.096 (n=1)0.00%
train_loss secondary0.0000.378 (n=1)0.00%
Annotations
batch_size32
epochs1
lr0.001
sourcegrape-leaves/train.py --log-radicle
Base CommitThe starting commit before the optimization was appliedb092a35b426df89b5a3259836dc7129d568dd46b
Candidate CommitThe code with the proposed optimization applied760c5a3a28335991e68b10af21a2201f6eb673d5
bench_cmduv run python train.py --epochs 1 --lr 0.001 --batch-size 32
build_cmduv pip install -e .
Schemav5
Diff
~ pyproject.toml
@@ -8,7 +8,11 @@ dependencies = [
8 "kagglehub>=1.0.0", 9 "matplotlib>=3.10.8", 10 "pillow>=12.2.0", 11+ "radicle", 12 "scikit-learn>=1.8.0", 13 "torch>=2.11.0", 14 "torchvision>=0.26.0", 15 ] 16+ 17+[tool.uv.sources] 18+radicle = { path = "../python-radicle", editable = true }
+ radicle_log.py
@@ -0,0 +1,222 @@
1+"""Publish grape-leaves train.py runs as Radicle cc.experiment COBs. 2+ 3+This module wraps the python-radicle SDK so train.py can call a single 4+`publish_run(...)` after training completes. It does not run any inference 5+or training itself — it only captures git/env state, looks up the parent 6+commit's prior experiment COB for baseline values, and publishes a new 7+COB through `radicle.experiment.Experiments`. 8+""" 9+ 10+from __future__ import annotations 11+ 12+import os 13+import platform 14+import subprocess 15+from pathlib import Path 16+from typing import Any 17+ 18+import radicle 19+from radicle import experiment 20+ 21+_METRIC_DEFS: tuple[tuple[str, str, str], ...] = ( 22+ ("val_acc", "%", experiment.Criteria.HIGHER_IS_BETTER), 23+ ("val_loss", "", experiment.Criteria.LOWER_IS_BETTER), 24+ ("train_loss", "", experiment.Criteria.LOWER_IS_BETTER), 25+) 26+ 27+ 28+def _run_git(repo_dir: Path, *args: str) -> str: 29+ result = subprocess.run( 30+ ["git", "-C", str(repo_dir), *args], 31+ check=True, 32+ capture_output=True, 33+ text=True, 34+ ) 35+ return result.stdout.strip() 36+ 37+ 38+def _capture_env() -> experiment.Env: 39+ try: 40+ memory_bytes = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") 41+ except (OSError, ValueError): 42+ memory_bytes = 0 43+ return experiment.Env( 44+ arch=platform.machine() or "", 45+ os=platform.platform() or "", 46+ cpu=platform.processor() or "", 47+ memory_bytes=memory_bytes, 48+ ) 49+ 50+ 51+def _capture_commits(repo_dir: Path) -> tuple[str, str | None]: 52+ candidate = _run_git(repo_dir, "rev-parse", "HEAD") 53+ try: 54+ parent = _run_git(repo_dir, "rev-parse", "HEAD~1") 55+ except subprocess.CalledProcessError: 56+ parent = None 57+ return candidate, parent 58+ 59+ 60+def _check_clean_worktree(repo_dir: Path, allow_dirty: bool) -> None: 61+ if allow_dirty: 62+ return 63+ # `git diff --quiet` exits 1 when the worktree differs, 0 when clean. 64+ # We intentionally don't go through `_run_git` here because we need the 65+ # return code, not an exception. 66+ unstaged = subprocess.run( 67+ ["git", "-C", str(repo_dir), "diff", "--quiet"], 68+ ) 69+ staged = subprocess.run( 70+ ["git", "-C", str(repo_dir), "diff", "--cached", "--quiet"], 71+ ) 72+ if unstaged.returncode != 0 or staged.returncode != 0: 73+ raise RuntimeError( 74+ "worktree is dirty — pass --allow-dirty to override" 75+ ) 76+ 77+ 78+def _normalize_rid(value: str) -> str: 79+ # Normalize: 80+ # rad://<rid> -> rad:<rid> 81+ # rad://<rid>/<peer-id> -> rad:<rid> (drop peer suffix) 82+ # rad:<rid> -> rad:<rid> (already canonical) 83+ if value.startswith("rad://"): 84+ body = value[len("rad://"):] 85+ rid_part = body.split("/", 1)[0] 86+ return f"rad:{rid_part}" 87+ return value 88+ 89+ 90+def _resolve_rid(repo_dir: Path, override: str | None) -> str: 91+ raw = override if override is not None else _run_git(repo_dir, "remote", "get-url", "rad") 92+ return radicle.parse_repo_id(_normalize_rid(raw)) 93+ 94+ 95+def _lookup_baseline( 96+ store: experiment.Experiments, 97+ parent_oid: str | None, 98+ metric_name: str, 99+) -> experiment.Measurement: 100+ if parent_oid is None: 101+ return experiment.Measurement() 102+ best: experiment.Measurement | None = None 103+ best_t = -1 104+ for _id, exp in store.list(): 105+ if exp.oid != parent_oid or exp.created_at_millis <= best_t: 106+ continue 107+ for m in exp.metrics: 108+ if m.name == metric_name: 109+ best = m.candidate 110+ best_t = exp.created_at_millis 111+ break 112+ return best if best is not None else experiment.Measurement() 113+ 114+ 115+def _build_metric( 116+ name: str, 117+ value: float, 118+ unit: str, 119+ criteria: str, 120+ baseline: experiment.Measurement, 121+) -> experiment.Metric: 122+ # val_acc is preserved as a fraction in [0, 1] for compatibility with the 123+ # autoresearch regex pipeline; do not multiply by 100 even though unit='%'. 124+ scaled = round(value * 1000) 125+ candidate = experiment.Measurement( 126+ n=1, 127+ median_x1000=scaled, 128+ std_x1000=0, 129+ samples_x1000=[scaled], 130+ ) 131+ return experiment.Metric( 132+ name=name, 133+ baseline=baseline, 134+ candidate=candidate, 135+ unit=unit, 136+ criteria=criteria, 137+ ) 138+ 139+ 140+def _build_config(epochs: int, lr: float, batch_size: int) -> experiment.ExperimentConfig: 141+ # `--log-radicle` is intentionally omitted from bench_cmd so that a 142+ # reproduction of this config does not recursively publish a new COB. 143+ return experiment.ExperimentConfig( 144+ bench_cmd=f"uv run python train.py --epochs {epochs} --lr {lr} --batch-size {batch_size}", 145+ build_cmd="uv pip install -e .", 146+ ) 147+ 148+ 149+def _build_asi(epochs: int, lr: float, batch_size: int) -> dict[str, Any]: 150+ return { 151+ "epochs": epochs, 152+ # Radicle COBs use canonical JSON, which rejects floating-point 153+ # numbers. Store the learning rate as a string while preserving the 154+ # exact command in ExperimentConfig.bench_cmd. 155+ "lr": str(lr), 156+ "batch_size": batch_size, 157+ "source": "grape-leaves/train.py --log-radicle", 158+ } 159+ 160+ 161+def publish_run( 162+ *, 163+ history: dict[str, list[float]], 164+ epochs: int, 165+ lr: float, 166+ batch_size: int, 167+ rid: str | None = None, 168+ description: str | None = None, 169+ allow_dirty: bool = False, 170+ agent_system: str = "grape-leaves/train.py", 171+ agent_model: str = "deterministic", 172+) -> str: 173+ """Publish this run's metrics as a new cc.experiment COB. 174+ 175+ Returns the new experiment object id. Raises ``RuntimeError`` if the 176+ worktree is dirty (unless ``allow_dirty=True``), if HEAD is a root 177+ commit, or if ``history`` is empty for any of the three tracked metrics. 178+ Raises ``radicle.Error`` subclasses on profile / repository / signer / 179+ store failures, and ``subprocess.CalledProcessError`` if git fails. 180+ """ 181+ for name, _unit, _criteria in _METRIC_DEFS: 182+ if not history.get(name): 183+ raise RuntimeError("no epoch results to publish") 184+ 185+ repo_dir = Path(__file__).parent 186+ 187+ resolved_rid = _resolve_rid(repo_dir, rid) 188+ _check_clean_worktree(repo_dir, allow_dirty) 189+ candidate_oid, parent_oid = _capture_commits(repo_dir) 190+ if parent_oid is None: 191+ raise RuntimeError( 192+ "cannot publish on a root commit (no HEAD~1); rad-experiment requires a base OID" 193+ ) 194+ 195+ profile = radicle.Profile.load() 196+ repo = profile.repository(resolved_rid) 197+ signer = profile.signer() 198+ store = experiment.Experiments.open(repo) 199+ 200+ metrics: list[experiment.Metric] = [] 201+ for name, unit, criteria in _METRIC_DEFS: 202+ baseline = _lookup_baseline(store, parent_oid, name) 203+ value = history[name][-1] 204+ metrics.append(_build_metric(name, value, unit, criteria, baseline)) 205+ 206+ action = experiment.Action.publish( 207+ schema_version=experiment.SCHEMA_VERSION, 208+ description=description, 209+ base=parent_oid, 210+ oid=candidate_oid, 211+ metrics=metrics, 212+ env=_capture_env(), 213+ agent_system=agent_system, 214+ agent_model=agent_model, 215+ asi=_build_asi(epochs, lr, batch_size), 216+ bench_hash=None, 217+ config=_build_config(epochs, lr, batch_size), 218+ ) 219+ 220+ obj_id, _exp = store.publish(action, signer) 221+ print(f"Published experiment COB: {obj_id}") 222+ return obj_id
~ train.py
@@ -77,7 +77,14 @@ val_transform = transforms.Compose([
77 # Training loop 78 # --------------------------------------------------------------------------- 79 80-def train(epochs: int = 10, lr: float = 1e-3, batch_size: int = 32): 80+def train( 81+ epochs: int = 10, 82+ lr: float = 1e-3, 83+ batch_size: int = 32, 84+ log_radicle: bool = False, 85+ description: str | None = None, 86+ allow_dirty: bool = False, 87+): 88 device = ( 89 torch.device("mps") if torch.backends.mps.is_available() 90 else torch.device("cuda") if torch.cuda.is_available()
@@ -161,10 +168,43 @@ def train(epochs: int = 10, lr: float = 1e-3, batch_size: int = 32):
168 fig.savefig(out_dir / "training_curves.png", dpi=150) 169 print(f"Training curves saved to {out_dir / 'training_curves.png'}") 170 171+ if log_radicle: 172+ try: 173+ import radicle_log 174+ radicle_log.publish_run( 175+ history=history, 176+ epochs=epochs, 177+ lr=lr, 178+ batch_size=batch_size, 179+ description=description, 180+ allow_dirty=allow_dirty, 181+ ) 182+ except Exception as e: 183+ print(f"WARNING: radicle logging failed: {e!r}") 184+ 185 186 if __name__ == "__main__": 187 parser = argparse.ArgumentParser() 188 parser.add_argument("--epochs", type=int, default=10) 189 parser.add_argument("--lr", type=float, default=1e-3) 190 parser.add_argument("--batch-size", type=int, default=32) 191+ parser.add_argument( 192+ "--log-radicle", 193+ action="store_true", 194+ help="Publish this run as a cc.experiment COB AFTER training completes; " 195+ "the worktree cleanliness check runs post-train, so a dirty worktree " 196+ "will not refund the training time", 197+ ) 198+ parser.add_argument( 199+ "--description", 200+ default=None, 201+ help="Optional human description recorded on the experiment COB", 202+ ) 203+ parser.add_argument( 204+ "--allow-dirty", 205+ action="store_true", 206+ help="Allow publishing when the worktree has uncommitted changes " 207+ "(by default radicle logging refuses, since the COB's commit oid " 208+ "would not match the code that actually ran)", 209+ ) 210 train(**vars(parser.parse_args()))
~ uv.lock
@@ -304,6 +304,7 @@ dependencies = [
304 { name = "kagglehub" }, 305 { name = "matplotlib" }, 306 { name = "pillow" }, 307+ { name = "radicle" }, 308 { name = "scikit-learn" }, 309 { name = "torch" }, 310 { name = "torchvision" },
@@ -314,6 +315,7 @@ requires-dist = [
315 { name = "kagglehub", specifier = ">=1.0.0" }, 316 { name = "matplotlib", specifier = ">=3.10.8" }, 317 { name = "pillow", specifier = ">=12.2.0" }, 318+ { name = "radicle", editable = "../python-radicle" }, 319 { name = "scikit-learn", specifier = ">=1.8.0" }, 320 { name = "torch", specifier = ">=2.11.0" }, 321 { name = "torchvision", specifier = ">=0.26.0" },
@@ -970,6 +972,19 @@ wheels = [
972 ] 973 974 [[package]] 975+name = "radicle" 976+version = "0.0.0" 977+source = { editable = "../python-radicle" } 978+ 979+[package.metadata] 980+ 981+[package.metadata.requires-dev] 982+dev = [ 983+ { name = "maturin", specifier = ">=1.7,<2.0" }, 984+ { name = "pytest", specifier = ">=8" }, 985+] 986+ 987+[[package]] 988 name = "requests" 989 version = "2.33.1" 990 source = { registry = "https://pypi.org/simple" }
Environment
Archarm64
OSmacOS-26.3.1-arm64-arm-64bit
CPUarm
Agentgrape-leaves/train.py / deterministic
Filespyproject.toml, radicle_log.py, train.py, uv.lock