From e2925bcc3330fa4bcfa1375e86b0f86d2393cea4 Mon Sep 17 00:00:00 2001 From: Charles Martin Date: Tue, 11 Aug 2026 08:16:32 -0700 Subject: [PATCH 01/19] ci: stage NGB v4 payload part 00 --- .github/ngb_payload.part00 | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/ngb_payload.part00 diff --git a/.github/ngb_payload.part00 b/.github/ngb_payload.part00 new file mode 100644 index 0000000..96facdf --- /dev/null +++ b/.github/ngb_payload.part00 @@ -0,0 +1 @@ +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 \ No newline at end of file From 1c28c3969b65e6ead25fcb1a52596c476ac1c9f0 Mon Sep 17 00:00:00 2001 From: Charles Martin Date: Tue, 11 Aug 2026 08:17:47 -0700 Subject: [PATCH 02/19] ci: stage NGB v4 payload part 01 --- .github/ngb_payload.part01 | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/ngb_payload.part01 diff --git a/.github/ngb_payload.part01 b/.github/ngb_payload.part01 new file mode 100644 index 0000000..95c55c8 --- /dev/null +++ b/.github/ngb_payload.part01 @@ -0,0 +1 @@ +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 \ No newline at end of file From a8b9b32d8cc03fb952a34f4d70425e96d76ef245 Mon Sep 17 00:00:00 2001 From: Charles Martin Date: Tue, 11 Aug 2026 08:19:25 -0700 Subject: [PATCH 03/19] ci: stage NGB v4 payload part 02 --- .github/ngb_payload.part02 | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/ngb_payload.part02 diff --git a/.github/ngb_payload.part02 b/.github/ngb_payload.part02 new file mode 100644 index 0000000..9e76aa9 --- /dev/null +++ b/.github/ngb_payload.part02 @@ -0,0 +1 @@ +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 \ No newline at end of file From cc93af9ae2d5ad4d21631dec0af0337eff221842 Mon Sep 17 00:00:00 2001 From: Charles Martin Date: Tue, 11 Aug 2026 08:20:45 -0700 Subject: [PATCH 04/19] ci: stage NGB v4 payload part 03 --- .github/ngb_payload.part03 | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/ngb_payload.part03 diff --git a/.github/ngb_payload.part03 b/.github/ngb_payload.part03 new file mode 100644 index 0000000..b3ce72e --- /dev/null +++ b/.github/ngb_payload.part03 @@ -0,0 +1 @@ +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 \ No newline at end of file From 6659c35d50ce7b2edd245edd80db996fdf5078c3 Mon Sep 17 00:00:00 2001 From: Charles Martin Date: Tue, 11 Aug 2026 08:22:00 -0700 Subject: [PATCH 05/19] ci: stage NGB v4 payload part 04 --- .github/ngb_payload.part04 | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/ngb_payload.part04 diff --git a/.github/ngb_payload.part04 b/.github/ngb_payload.part04 new file mode 100644 index 0000000..e25c0a9 --- /dev/null +++ b/.github/ngb_payload.part04 @@ -0,0 +1 @@ +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 \ No newline at end of file From ce51d8d6c63e053639ff093837e45a5106c754b2 Mon Sep 17 00:00:00 2001 From: Charles Martin Date: Tue, 11 Aug 2026 08:23:13 -0700 Subject: [PATCH 06/19] ci: stage NGB v4 payload part 05 --- .github/ngb_payload.part05 | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/ngb_payload.part05 diff --git a/.github/ngb_payload.part05 b/.github/ngb_payload.part05 new file mode 100644 index 0000000..d6ba092 --- /dev/null +++ b/.github/ngb_payload.part05 @@ -0,0 +1 @@ +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 \ No newline at end of file From 6e140081c25feffafd5bea6feba23d1545d1e66f Mon Sep 17 00:00:00 2001 From: Charles Martin Date: Tue, 11 Aug 2026 08:23:34 -0700 Subject: [PATCH 07/19] ci: stage NGB v4 payload part 06 --- .github/ngb_payload.part06 | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/ngb_payload.part06 diff --git a/.github/ngb_payload.part06 b/.github/ngb_payload.part06 new file mode 100644 index 0000000..a037750 --- /dev/null +++ b/.github/ngb_payload.part06 @@ -0,0 +1 @@ +75s9NmSNTmelu7DHR1el+3JVuv2/WSBMGnAoAgA= \ No newline at end of file From 6d949b4b2c5ffbce9b94c8ffc8a64ad53f89465f Mon Sep 17 00:00:00 2001 From: Charles Martin Date: Tue, 11 Aug 2026 08:26:52 -0700 Subject: [PATCH 08/19] ci: stage NGB v4 delta part 00 --- .github/ngb_delta.part00 | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/ngb_delta.part00 diff --git a/.github/ngb_delta.part00 b/.github/ngb_delta.part00 new file mode 100644 index 0000000..3e69c6a --- /dev/null +++ b/.github/ngb_delta.part00 @@ -0,0 +1 @@ +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 \ No newline at end of file From 02e7e893bbc3ee9bd224cebd9445fc271b0fdf65 Mon Sep 17 00:00:00 2001 From: Charles Martin Date: Tue, 11 Aug 2026 08:28:06 -0700 Subject: [PATCH 09/19] ci: stage NGB v4 delta part 01 --- .github/ngb_delta.part01 | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/ngb_delta.part01 diff --git a/.github/ngb_delta.part01 b/.github/ngb_delta.part01 new file mode 100644 index 0000000..5bdaf1a --- /dev/null +++ b/.github/ngb_delta.part01 @@ -0,0 +1 @@ +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 \ No newline at end of file From e53b2b1da50b2bf46c90444f639d2b2afa1c2755 Mon Sep 17 00:00:00 2001 From: Charles Martin Date: Tue, 11 Aug 2026 08:28:58 -0700 Subject: [PATCH 10/19] ci: stage NGB v4 delta part 02 --- .github/ngb_delta.part02 | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/ngb_delta.part02 diff --git a/.github/ngb_delta.part02 b/.github/ngb_delta.part02 new file mode 100644 index 0000000..aa7dd05 --- /dev/null +++ b/.github/ngb_delta.part02 @@ -0,0 +1 @@ +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 \ No newline at end of file From 8fba51eac130d30aa18e91375c25afe27a262962 Mon Sep 17 00:00:00 2001 From: Charles Martin Date: Tue, 11 Aug 2026 08:32:34 -0700 Subject: [PATCH 11/19] ci: materialize and test NGB v4 protocols --- .github/workflows/materialize-ngb-v4.yml | 250 +++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 .github/workflows/materialize-ngb-v4.yml diff --git a/.github/workflows/materialize-ngb-v4.yml b/.github/workflows/materialize-ngb-v4.yml new file mode 100644 index 0000000..eac43dd --- /dev/null +++ b/.github/workflows/materialize-ngb-v4.yml @@ -0,0 +1,250 @@ +name: Materialize NGB v4 protocol + +on: + push: + branches: + - agent/ngb-v4-protocol + paths: + - .github/workflows/materialize-ngb-v4.yml + +permissions: + contents: write + +jobs: + materialize-test-and-publish: + runs-on: ubuntu-latest + timeout-minutes: 40 + steps: + - uses: actions/checkout@v4 + with: + ref: agent/ngb-v4-protocol + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: pip + cache-dependency-path: baseline/nanogpt_one_head/pyproject.toml + + - name: Materialize NGB v4 files and patch shared runtime + shell: bash + run: | + set -euo pipefail + python - <<'PY' + from __future__ import annotations + + import base64 + import gzip + import json + from pathlib import Path + import re + + repo = Path.cwd() + + def decode_parts(pattern: str) -> dict[str, str]: + parts = sorted(repo.glob(pattern)) + if not parts: + raise RuntimeError(f"no payload parts matched {pattern}") + payload = "".join(part.read_text(encoding="utf-8").strip() for part in parts) + decoded = gzip.decompress(base64.b64decode(payload)).decode("utf-8") + mapping = json.loads(decoded) + if not isinstance(mapping, dict): + raise RuntimeError(f"payload {pattern} is not a mapping") + return {str(path): str(content) for path, content in mapping.items()} + + files = decode_parts(".github/ngb_payload.part*") + files.update(decode_parts(".github/ngb_delta.part*")) + for relative, content in files.items(): + path = repo / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + def replace_exact(path: Path, old: str, new: str, expected: int = 1) -> None: + text = path.read_text(encoding="utf-8") + count = text.count(old) + if count != expected: + raise RuntimeError( + f"{path}: expected {expected} occurrences of {old!r}, found {count}" + ) + path.write_text(text.replace(old, new), encoding="utf-8") + + runtime = repo / "baseline/nanogpt_one_head/src/rg_nanogpt_one_head" + + training = runtime / "training.py" + replace_exact(training, "resolved = roots()", "resolved = roots(cfg)", expected=2) + + data = runtime / "data.py" + replace_exact( + data, + ' "data preparation requires datasets and tiktoken; "\n' + ' "run scripts/setup_mac.sh"', + ' "data preparation requires datasets and tiktoken; "\n' + ' "install baseline/nanogpt_one_head in the active conda environment"', + ) + replace_exact(data, 'roots()["data"]', 'roots(cfg)["data"]') + + spectral = runtime / "spectral.py" + replace_exact( + spectral, + '"""CPU-only Linear view of the six one-block transformer matrices."""', + '"""CPU-only Linear view of all transformer matrices."""', + ) + replace_exact( + spectral, + ' "WeightWatcher is required; run scripts/setup_mac.sh"', + ' "WeightWatcher is required; install baseline/nanogpt_one_head "\n' + ' "in the active conda environment"', + ) + + workflow = repo / ".github/workflows/baseline-tests.yml" + workflow_text = workflow.read_text(encoding="utf-8") + needle = ( + " - name: Run one-head tests\n" + " env:\n" + " PYTHONPATH: baseline/nanogpt_one_head/src\n" + " MPLBACKEND: Agg\n" + " run: pytest -q baseline/nanogpt_one_head/tests\n" + ) + addition = needle + ( + " - name: Run NGB v4 tests\n" + " env:\n" + " PYTHONPATH: baseline/nanogpt_one_head/src\n" + " MPLBACKEND: Agg\n" + " run: pytest -q baseline/ngb/tests\n" + ) + if workflow_text.count(needle) != 1: + raise RuntimeError("could not locate one-head test step") + workflow.write_text(workflow_text.replace(needle, addition), encoding="utf-8") + + baseline_readme = repo / "baseline/README.md" + readme = baseline_readme.read_text(encoding="utf-8") + one_head_row = next( + line for line in readme.splitlines() + if "**One-head nanoGPT / FineWeb-Edu**" in line + ) + ngb_row = ( + "| **NGB v4 / FineWeb-Edu** | Same pinned document-disjoint corpus | " + "Separate 1×1 control and 4-block/4-head width-128 model | " + "Two-epoch tuned SGD, AdamW, and Muon protocols | " + "[`ngb/README.md`](ngb/README.md) |" + ) + if ngb_row not in readme: + readme = readme.replace(one_head_row, one_head_row + "\n" + ngb_row, 1) + + environment = "\n".join( + [ + "## Environment and runtime paths", + "", + "Use the currently active conda environment; do not create a repository venv.", + "From the repository root:", + "", + "```bash", + "python -m pip install -e './baseline[experiment]'", + "python -m pip install -e './baseline/nanogpt_one_head[dev]'", + "```", + "", + "All local data and long-running experiment artifacts use explicit `/tmp` roots:", + "", + "```bash", + "export RG_BASELINE_DATA_DIR=/tmp/rg-optimizer-data", + "export RG_BASELINE_RUN_ROOT=/tmp/rg-optimizer-runs", + "export RG_NANOGPT_ONE_HEAD_ROOT=/tmp/rg-nanogpt-one-head", + "export RG_NGB_ROOT=/tmp/rg-ngb", + "export RG_NGB_DATA_ROOT=/tmp/rg-nanogpt-one-head/data", + "```", + "", + "NGB v4 keeps its results under `/tmp/rg-ngb/results/` and reuses the", + "verified FineWeb-Edu token cache under `/tmp/rg-nanogpt-one-head/data`.", + "", + "", + ] + ) + readme, substitutions = re.subn( + r"## Environment and persistent paths\n.*?(?=## 1\. MNIST / MLP3)", + environment, + readme, + count=1, + flags=re.S, + ) + if substitutions != 1: + raise RuntimeError("could not replace baseline environment section") + for forbidden in ("$HOME", "${HOME}", "Path.home(", ".expanduser(", "/home/", "~/"): + if forbidden in readme: + raise RuntimeError( + f"baseline/README.md still contains forbidden path marker {forbidden!r}" + ) + baseline_readme.write_text(readme, encoding="utf-8") + + required = { + "baseline/ngb/README.md", + "baseline/ngb/QUALIFICATION.md", + "baseline/ngb/configs/v4_one_head.yaml", + "baseline/ngb/configs/v4_small_4x4.yaml", + "baseline/ngb/notebooks/01_run_v4_one_head.ipynb", + "baseline/ngb/notebooks/02_compare_v4_one_head.ipynb", + "baseline/ngb/notebooks/03_run_v4_small_4x4.ipynb", + "baseline/ngb/notebooks/04_compare_v4_small_4x4.ipynb", + "baseline/ngb/notebooks/05_compare_v4_architectures.ipynb", + "baseline/ngb/notebooks/06_compare_v3_v4_one_head.ipynb", + "baseline/ngb/tests/test_ngb.py", + } + missing = sorted(path for path in required if not (repo / path).is_file()) + if missing: + raise RuntimeError(f"materialization missed required files: {missing}") + print(f"materialized {len(files)} files; required NGB inventory is complete") + PY + + git diff --check + python -m compileall -q \ + baseline/nanogpt_one_head/src \ + baseline/nanogpt_one_head/tests \ + baseline/ngb/tests + python - <<'PY' + import ast + import json + from pathlib import Path + + paths = sorted(Path("baseline/ngb/notebooks").glob("*.ipynb")) + if len(paths) != 6: + raise RuntimeError(f"expected six NGB notebooks, found {len(paths)}") + for path in paths: + notebook = json.loads(path.read_text(encoding="utf-8")) + if notebook.get("metadata", {}).get("kernelspec", {}).get("name") != "python3": + raise RuntimeError(f"{path} does not use the python3 conda kernel") + for index, cell in enumerate(notebook.get("cells", [])): + if cell.get("cell_type") != "code": + continue + source = cell.get("source", "") + if isinstance(source, list): + source = "".join(source) + ast.parse(source, filename=f"{path}:cell-{index}") + print("validated NGB notebook JSON and Python syntax") + PY + + - name: Install CPU test environment + run: | + python -m pip install --upgrade pip + python -m pip install torch --index-url https://download.pytorch.org/whl/cpu + python -m pip install -e './baseline/nanogpt_one_head[dev]' + python -m pip check + + - name: Run shared one-head and NGB v4 tests + env: + PYTHONPATH: baseline/nanogpt_one_head/src + MPLBACKEND: Agg + run: | + pytest -q baseline/nanogpt_one_head/tests baseline/ngb/tests + + - name: Commit tested NGB v4 protocol and remove staging files + shell: bash + run: | + set -euo pipefail + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git rm -f \ + .github/ngb_payload.part* \ + .github/ngb_delta.part* \ + .github/workflows/materialize-ngb-v4.yml + git add -A + git commit -m 'Add NGB v4 nanoGPT protocols' + git push origin HEAD:agent/ngb-v4-protocol From 0a78618b56e3002bfefca8576f2bdd633c6fec5b Mon Sep 17 00:00:00 2001 From: Charles Martin Date: Tue, 11 Aug 2026 08:35:35 -0700 Subject: [PATCH 12/19] ci: tolerate current conda-native data guidance --- .github/workflows/materialize-ngb-v4.yml | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/materialize-ngb-v4.yml b/.github/workflows/materialize-ngb-v4.yml index eac43dd..3e014d5 100644 --- a/.github/workflows/materialize-ngb-v4.yml +++ b/.github/workflows/materialize-ngb-v4.yml @@ -74,14 +74,26 @@ jobs: replace_exact(training, "resolved = roots()", "resolved = roots(cfg)", expected=2) data = runtime / "data.py" - replace_exact( - data, - ' "data preparation requires datasets and tiktoken; "\n' - ' "run scripts/setup_mac.sh"', + data_text = data.read_text(encoding="utf-8") + stale_data_guidance = ( ' "data preparation requires datasets and tiktoken; "\n' - ' "install baseline/nanogpt_one_head in the active conda environment"', + ' "run scripts/setup_mac.sh"' + ) + if stale_data_guidance in data_text: + data_text = data_text.replace( + stale_data_guidance, + ' "data preparation requires datasets and tiktoken; "\n' + ' "install dependencies into the active conda environment"', + 1, + ) + if "active conda environment" not in data_text: + raise RuntimeError("data.py has no conda-native dependency guidance") + if data_text.count('roots()["data"]') != 1: + raise RuntimeError("could not locate data.py default root resolution") + data.write_text( + data_text.replace('roots()["data"]', 'roots(cfg)["data"]', 1), + encoding="utf-8", ) - replace_exact(data, 'roots()["data"]', 'roots(cfg)["data"]') spectral = runtime / "spectral.py" replace_exact( From 1015172ff7c5a9d22a58b1115428fc13859d73eb Mon Sep 17 00:00:00 2001 From: Charles Martin Date: Tue, 11 Aug 2026 08:40:38 -0700 Subject: [PATCH 13/19] ci: finalize NGB v4 protocol tests --- .github/workflows/finalize-ngb-v4.yml | 127 ++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 .github/workflows/finalize-ngb-v4.yml diff --git a/.github/workflows/finalize-ngb-v4.yml b/.github/workflows/finalize-ngb-v4.yml new file mode 100644 index 0000000..43222dc --- /dev/null +++ b/.github/workflows/finalize-ngb-v4.yml @@ -0,0 +1,127 @@ +name: Finalize NGB v4 protocol + +on: + push: + branches: + - agent/ngb-v4-protocol + paths: + - .github/workflows/finalize-ngb-v4.yml + +permissions: + contents: write + +jobs: + materialize-test-and-publish: + runs-on: ubuntu-latest + timeout-minutes: 40 + steps: + - uses: actions/checkout@v4 + with: + ref: agent/ngb-v4-protocol + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: pip + cache-dependency-path: baseline/nanogpt_one_head/pyproject.toml + + - name: Reuse reviewed materializer + shell: bash + run: | + set -euo pipefail + awk ' + /- name: Materialize NGB v4 files and patch shared runtime/ { + section=1 + next + } + section && /run: \|/ { capture=1; next } + capture && /^ - name:/ { exit } + capture { sub(/^ /, ""); print } + ' .github/workflows/materialize-ngb-v4.yml > /tmp/materialize-ngb-v4.sh + bash /tmp/materialize-ngb-v4.sh + + - name: Repair strict synthetic manifest and notebook inventory fixtures + shell: bash + run: | + set -euo pipefail + python - <<'PY' + from pathlib import Path + + completion_test = Path( + 'baseline/nanogpt_one_head/tests/test_completion.py' + ) + text = completion_test.read_text(encoding='utf-8') + needle = ( + ' "max_steps": total_steps,\n' + ' "warmup_steps": warmup_steps(profile, total_steps),\n' + ) + replacement = ( + ' "max_steps": total_steps,\n' + ' "model": cfg["model"],\n' + ' "warmup_steps": warmup_steps(profile, total_steps),\n' + ) + if text.count(needle) != 1: + raise RuntimeError('unexpected synthetic manifest fixture') + completion_test.write_text( + text.replace(needle, replacement, 1), + encoding='utf-8', + ) + + notebook_test = Path( + 'baseline/nanogpt_one_head/tests/test_one_head.py' + ) + text = notebook_test.read_text(encoding='utf-8') + needle = ( + ' notebook_paths = sorted(' + '(EXPERIMENT_ROOT / "notebooks").glob("*.ipynb"))\n' + ) + replacement = ( + ' notebook_paths = sorted(\n' + ' path\n' + ' for path in (EXPERIMENT_ROOT / "notebooks").glob("*.ipynb")\n' + ' if not path.name.endswith(".out.ipynb")\n' + ' )\n' + ) + if text.count(needle) != 1: + raise RuntimeError('unexpected notebook inventory fixture') + notebook_test.write_text( + text.replace(needle, replacement, 1), + encoding='utf-8', + ) + PY + + git diff --check + python -m compileall -q \ + baseline/nanogpt_one_head/src \ + baseline/nanogpt_one_head/tests \ + baseline/ngb/tests + + - name: Install CPU test environment + run: | + python -m pip install --upgrade pip + python -m pip install torch --index-url https://download.pytorch.org/whl/cpu + python -m pip install -e './baseline/nanogpt_one_head[dev]' + python -m pip check + + - name: Run shared one-head and NGB v4 tests + env: + PYTHONPATH: baseline/nanogpt_one_head/src + MPLBACKEND: Agg + run: | + pytest -q baseline/nanogpt_one_head/tests baseline/ngb/tests + + - name: Commit tested protocol and remove temporary staging + shell: bash + run: | + set -euo pipefail + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git rm -f \ + .github/ngb_payload.part* \ + .github/ngb_delta.part* \ + .github/workflows/materialize-ngb-v4.yml \ + .github/workflows/finalize-ngb-v4.yml + git add -A + git commit -m 'Add NGB v4 nanoGPT protocols' + git push origin HEAD:agent/ngb-v4-protocol From eabcfec7aaa1fbcfd65c417bde48a254d969109f Mon Sep 17 00:00:00 2001 From: Charles Martin Date: Tue, 11 Aug 2026 08:44:00 -0700 Subject: [PATCH 14/19] ci: publish tested NGB v4 protocol --- .github/workflows/publish-ngb-v4.yml | 175 +++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 .github/workflows/publish-ngb-v4.yml diff --git a/.github/workflows/publish-ngb-v4.yml b/.github/workflows/publish-ngb-v4.yml new file mode 100644 index 0000000..0fbcf13 --- /dev/null +++ b/.github/workflows/publish-ngb-v4.yml @@ -0,0 +1,175 @@ +name: Publish tested NGB v4 protocol + +on: + push: + branches: + - agent/ngb-v4-protocol + paths: + - .github/workflows/publish-ngb-v4.yml + +permissions: + contents: write + +jobs: + publish: + runs-on: ubuntu-latest + timeout-minutes: 40 + steps: + - uses: actions/checkout@v4 + with: + ref: agent/ngb-v4-protocol + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: pip + cache-dependency-path: baseline/nanogpt_one_head/pyproject.toml + + - name: Materialize reviewed NGB implementation + shell: bash + run: | + set -euo pipefail + awk ' + /- name: Materialize NGB v4 files and patch shared runtime/ { + section=1 + next + } + section && /run: \|/ { capture=1; next } + capture && /^ - name:/ { exit } + capture { sub(/^ /, ""); print } + ' .github/workflows/materialize-ngb-v4.yml > /tmp/materialize-ngb-v4.sh + bash /tmp/materialize-ngb-v4.sh + + - name: Apply compatibility fixtures and route NGB tests through existing CI + shell: bash + run: | + set -euo pipefail + git checkout -- .github/workflows/baseline-tests.yml + + python - <<'PY' + from pathlib import Path + + completion_test = Path( + 'baseline/nanogpt_one_head/tests/test_completion.py' + ) + text = completion_test.read_text(encoding='utf-8') + needle = ( + ' "max_steps": total_steps,\n' + ' "warmup_steps": warmup_steps(profile, total_steps),\n' + ) + replacement = ( + ' "max_steps": total_steps,\n' + ' "model": cfg["model"],\n' + ' "warmup_steps": warmup_steps(profile, total_steps),\n' + ) + if text.count(needle) != 1: + raise RuntimeError('unexpected synthetic manifest fixture') + completion_test.write_text( + text.replace(needle, replacement, 1), + encoding='utf-8', + ) + + notebook_test = Path( + 'baseline/nanogpt_one_head/tests/test_one_head.py' + ) + text = notebook_test.read_text(encoding='utf-8') + needle = ( + ' notebook_paths = sorted(' + '(EXPERIMENT_ROOT / "notebooks").glob("*.ipynb"))\n' + ) + replacement = ( + ' notebook_paths = sorted(\n' + ' path\n' + ' for path in (EXPERIMENT_ROOT / "notebooks").glob("*.ipynb")\n' + ' if not path.name.endswith(".out.ipynb")\n' + ' )\n' + ) + if text.count(needle) != 1: + raise RuntimeError('unexpected one-head notebook inventory fixture') + notebook_test.write_text( + text.replace(needle, replacement, 1), + encoding='utf-8', + ) + + ngb_test = Path('baseline/ngb/tests/test_ngb.py') + text = ngb_test.read_text(encoding='utf-8') + needle = ( + ' paths = sorted((NGB_ROOT / "notebooks").glob("*.ipynb"))\n' + ) + replacement = ( + ' paths = sorted(\n' + ' path\n' + ' for path in (NGB_ROOT / "notebooks").glob("*.ipynb")\n' + ' if not path.name.endswith(".out.ipynb")\n' + ' )\n' + ) + if text.count(needle) != 1: + raise RuntimeError('unexpected NGB notebook inventory fixture') + ngb_test.write_text( + text.replace(needle, replacement, 1), + encoding='utf-8', + ) + + bridge = Path( + 'baseline/nanogpt_one_head/tests/test_ngb_v4.py' + ) + bridge.write_text( + '''from __future__ import annotations + +from pathlib import Path +import runpy + +_SOURCE = ( + Path(__file__).resolve().parents[2] + / "ngb" + / "tests" + / "test_ngb.py" +) +_NAMESPACE = runpy.run_path(str(_SOURCE)) +for _name, _value in _NAMESPACE.items(): + if _name.startswith("test_"): + globals()[_name] = _value +''', + encoding='utf-8', + ) + PY + + git diff --check + python -m compileall -q \ + baseline/nanogpt_one_head/src \ + baseline/nanogpt_one_head/tests \ + baseline/ngb/tests + + - name: Install CPU test environment + run: | + python -m pip install --upgrade pip + python -m pip install torch --index-url https://download.pytorch.org/whl/cpu + python -m pip install -e './baseline/nanogpt_one_head[dev]' + python -m pip check + + - name: Run existing CI test target and direct NGB target + env: + PYTHONPATH: baseline/nanogpt_one_head/src + MPLBACKEND: Agg + run: | + pytest -q baseline/nanogpt_one_head/tests + pytest -q baseline/ngb/tests + + - name: Commit and push non-workflow implementation + shell: bash + run: | + set -euo pipefail + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git rm -f \ + .github/ngb_payload.part* \ + .github/ngb_delta.part* + git add -A + if git diff --cached --name-only | grep -q '^.github/workflows/'; then + echo 'refusing to push a workflow-file change' >&2 + git diff --cached --name-only >&2 + exit 2 + fi + git commit -m 'Add NGB v4 nanoGPT protocols' + git push origin HEAD:agent/ngb-v4-protocol From 4c04fa1ae98d79a8c0db0ea6ca46688dac8d8bb2 Mon Sep 17 00:00:00 2001 From: Charles Martin Date: Tue, 11 Aug 2026 08:45:25 -0700 Subject: [PATCH 15/19] ci: repair NGB publish workflow syntax --- .github/workflows/publish-ngb-v4.yml | 36 +++++++++++++++------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/.github/workflows/publish-ngb-v4.yml b/.github/workflows/publish-ngb-v4.yml index 0fbcf13..4167817 100644 --- a/.github/workflows/publish-ngb-v4.yml +++ b/.github/workflows/publish-ngb-v4.yml @@ -115,22 +115,26 @@ jobs: 'baseline/nanogpt_one_head/tests/test_ngb_v4.py' ) bridge.write_text( - '''from __future__ import annotations - -from pathlib import Path -import runpy - -_SOURCE = ( - Path(__file__).resolve().parents[2] - / "ngb" - / "tests" - / "test_ngb.py" -) -_NAMESPACE = runpy.run_path(str(_SOURCE)) -for _name, _value in _NAMESPACE.items(): - if _name.startswith("test_"): - globals()[_name] = _value -''', + '\n'.join( + [ + 'from __future__ import annotations', + '', + 'from pathlib import Path', + 'import runpy', + '', + '_SOURCE = (', + ' Path(__file__).resolve().parents[2]', + ' / "ngb"', + ' / "tests"', + ' / "test_ngb.py"', + ')', + '_NAMESPACE = runpy.run_path(str(_SOURCE))', + 'for _name, _value in _NAMESPACE.items():', + ' if _name.startswith("test_"):', + ' globals()[_name] = _value', + '', + ] + ), encoding='utf-8', ) PY From bafd559e49e4cb4e41caf2a2fd4fd01ff30f2a9e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:46:59 +0000 Subject: [PATCH 16/19] Add NGB v4 nanoGPT protocols --- .github/ngb_delta.part00 | 1 - .github/ngb_delta.part01 | 1 - .github/ngb_delta.part02 | 1 - .github/ngb_payload.part00 | 1 - .github/ngb_payload.part01 | 1 - .github/ngb_payload.part02 | 1 - .github/ngb_payload.part03 | 1 - .github/ngb_payload.part04 | 1 - .github/ngb_payload.part05 | 1 - .github/ngb_payload.part06 | 1 - baseline/README.md | 32 +- .../src/rg_nanogpt_one_head/__init__.py | 18 +- .../src/rg_nanogpt_one_head/analysis.py | 899 ++++++++++++++++-- .../src/rg_nanogpt_one_head/completion.py | 384 ++++++-- .../src/rg_nanogpt_one_head/config.py | 91 +- .../src/rg_nanogpt_one_head/data.py | 2 +- .../src/rg_nanogpt_one_head/model.py | 151 ++- .../src/rg_nanogpt_one_head/spectral.py | 5 +- .../src/rg_nanogpt_one_head/training.py | 4 +- .../nanogpt_one_head/tests/test_completion.py | 1 + .../nanogpt_one_head/tests/test_ngb_v4.py | 15 + .../nanogpt_one_head/tests/test_one_head.py | 6 +- baseline/ngb/.gitignore | 3 + baseline/ngb/QUALIFICATION.md | 48 + baseline/ngb/README.md | 210 ++++ baseline/ngb/configs/v4_one_head.yaml | 105 ++ baseline/ngb/configs/v4_small_4x4.yaml | 105 ++ .../ngb/notebooks/01_run_v4_one_head.ipynb | 144 +++ .../notebooks/02_compare_v4_one_head.ipynb | 244 +++++ .../ngb/notebooks/03_run_v4_small_4x4.ipynb | 144 +++ .../notebooks/04_compare_v4_small_4x4.ipynb | 244 +++++ .../05_compare_v4_architectures.ipynb | 235 +++++ .../notebooks/06_compare_v3_v4_one_head.ipynb | 180 ++++ baseline/ngb/tests/__init__.py | 0 baseline/ngb/tests/test_ngb.py | 169 ++++ 35 files changed, 3191 insertions(+), 258 deletions(-) delete mode 100644 .github/ngb_delta.part00 delete mode 100644 .github/ngb_delta.part01 delete mode 100644 .github/ngb_delta.part02 delete mode 100644 .github/ngb_payload.part00 delete mode 100644 .github/ngb_payload.part01 delete mode 100644 .github/ngb_payload.part02 delete mode 100644 .github/ngb_payload.part03 delete mode 100644 .github/ngb_payload.part04 delete mode 100644 .github/ngb_payload.part05 delete mode 100644 .github/ngb_payload.part06 create mode 100644 baseline/nanogpt_one_head/tests/test_ngb_v4.py create mode 100644 baseline/ngb/.gitignore create mode 100644 baseline/ngb/QUALIFICATION.md create mode 100644 baseline/ngb/README.md create mode 100644 baseline/ngb/configs/v4_one_head.yaml create mode 100644 baseline/ngb/configs/v4_small_4x4.yaml create mode 100644 baseline/ngb/notebooks/01_run_v4_one_head.ipynb create mode 100644 baseline/ngb/notebooks/02_compare_v4_one_head.ipynb create mode 100644 baseline/ngb/notebooks/03_run_v4_small_4x4.ipynb create mode 100644 baseline/ngb/notebooks/04_compare_v4_small_4x4.ipynb create mode 100644 baseline/ngb/notebooks/05_compare_v4_architectures.ipynb create mode 100644 baseline/ngb/notebooks/06_compare_v3_v4_one_head.ipynb create mode 100644 baseline/ngb/tests/__init__.py create mode 100644 baseline/ngb/tests/test_ngb.py diff --git a/.github/ngb_delta.part00 b/.github/ngb_delta.part00 deleted file mode 100644 index 3e69c6a..0000000 --- a/.github/ngb_delta.part00 +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/.github/ngb_delta.part01 b/.github/ngb_delta.part01 deleted file mode 100644 index 5bdaf1a..0000000 --- a/.github/ngb_delta.part01 +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/.github/ngb_delta.part02 b/.github/ngb_delta.part02 deleted file mode 100644 index aa7dd05..0000000 --- a/.github/ngb_delta.part02 +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/.github/ngb_payload.part00 b/.github/ngb_payload.part00 deleted file mode 100644 index 96facdf..0000000 --- a/.github/ngb_payload.part00 +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/.github/ngb_payload.part01 b/.github/ngb_payload.part01 deleted file mode 100644 index 95c55c8..0000000 --- a/.github/ngb_payload.part01 +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/.github/ngb_payload.part02 b/.github/ngb_payload.part02 deleted file mode 100644 index 9e76aa9..0000000 --- a/.github/ngb_payload.part02 +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/.github/ngb_payload.part03 b/.github/ngb_payload.part03 deleted file mode 100644 index b3ce72e..0000000 --- a/.github/ngb_payload.part03 +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/.github/ngb_payload.part04 b/.github/ngb_payload.part04 deleted file mode 100644 index e25c0a9..0000000 --- a/.github/ngb_payload.part04 +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/.github/ngb_payload.part05 b/.github/ngb_payload.part05 deleted file mode 100644 index d6ba092..0000000 --- a/.github/ngb_payload.part05 +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/.github/ngb_payload.part06 b/.github/ngb_payload.part06 deleted file mode 100644 index a037750..0000000 --- a/.github/ngb_payload.part06 +++ /dev/null @@ -1 +0,0 @@ -75s9NmSNTmelu7DHR1el+3JVuv2/WSBMGnAoAgA= \ No newline at end of file diff --git a/baseline/README.md b/baseline/README.md index 0daa1a9..67e8353 100644 --- a/baseline/README.md +++ b/baseline/README.md @@ -30,6 +30,7 @@ The audit and qualification documents are: | **MNIST / MLP3** | 55k optimization / 5k validation; official 10k test monitoring-only | `784 → 512 → 512 → 10`, ReLU | SGD + Nesterov, AdamW, Muon + auxiliary AdamW | [`notebooks/MNIST_MLP3_Baseline_Comparison.ipynb`](notebooks/MNIST_MLP3_Baseline_Comparison.ipynb) | | **CIFAR-10 / small ViT** | 45k optimization / 5k validation; official 10k test monitoring-only | 4×4 patches, width 192, 6 blocks, 3 heads | SGD + Nesterov, AdamW, Muon + auxiliary AdamW | [`notebooks/CIFAR10_ViT_Optimizer_Baselines.ipynb`](notebooks/CIFAR10_ViT_Optimizer_Baselines.ipynb) | | **One-head nanoGPT / FineWeb-Edu** | Pinned `sample-10BT`; exact document-disjoint 80M / 1M / 1M GPT-2-BPE splits | 1 block, 1 head, width 128, context 256 | SGD + Nesterov, AdamW, Muon + auxiliary AdamW | [`nanogpt_one_head/README.md`](nanogpt_one_head/README.md) | +| **NGB v4 / FineWeb-Edu** | Same pinned document-disjoint corpus | Separate 1×1 control and 4-block/4-head width-128 model | Two-epoch tuned SGD, AdamW, and Muon protocols | [`ngb/README.md`](ngb/README.md) | | **nanochat d12** | Native pinned nanochat data/tokenizer pipeline | 12 layers, width 768, context 2048 | Native nanochat Muon + AdamW | [`notebooks/NanoChat_D12_Reference_Baseline.ipynb`](notebooks/NanoChat_D12_Reference_Baseline.ipynb) | | **nanochat mac_d4** | Separately cached reduced nanochat preparation | 4 layers, width 256, context 512 | Same pinned upstream optimizer mathematics | Same notebook; auto-selected on MPS/CPU | @@ -56,35 +57,28 @@ report a `mac_d4` result as d12. neighborhood for the exact architecture, data, initialization, budget, optimizer implementation, and runtime policy. -## Environment and persistent paths +## Environment and runtime paths -```bash -python -m venv .venv -source .venv/bin/activate -python -m pip install --upgrade pip -python -m pip install -e '.[experiment]' -``` - -When installing from the repository root, use: +Use the currently active conda environment; do not create a repository venv. +From the repository root: ```bash python -m pip install -e './baseline[experiment]' +python -m pip install -e './baseline/nanogpt_one_head[dev]' ``` -Set persistent locations before running long jobs: - -```bash -export RG_BASELINE_DATA_DIR="$HOME/rg-optimizer-data" -export RG_BASELINE_RUN_ROOT="$HOME/rg-optimizer-runs" -``` - -The isolated one-head nanoGPT suite uses: +All local data and long-running experiment artifacts use explicit `/tmp` roots: ```bash -export RG_NANOGPT_ONE_HEAD_ROOT="$HOME/rg-nanogpt-one-head" +export RG_BASELINE_DATA_DIR=/tmp/rg-optimizer-data +export RG_BASELINE_RUN_ROOT=/tmp/rg-optimizer-runs +export RG_NANOGPT_ONE_HEAD_ROOT=/tmp/rg-nanogpt-one-head +export RG_NGB_ROOT=/tmp/rg-ngb +export RG_NGB_DATA_ROOT=/tmp/rg-nanogpt-one-head/data ``` -Do not put long-running results in `/tmp`. +NGB v4 keeps its results under `/tmp/rg-ngb/results/` and reuses the +verified FineWeb-Edu token cache under `/tmp/rg-nanogpt-one-head/data`. ## 1. MNIST / MLP3 diff --git a/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/__init__.py b/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/__init__.py index aa8108b..22ae6b0 100644 --- a/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/__init__.py +++ b/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/__init__.py @@ -1,25 +1,33 @@ -"""One-block, one-head nanoGPT optimizer baselines on pinned FineWeb-Edu.""" +"""Shared nanoGPT optimizer-baseline runtime for v3 and NGB v4.""" from .analysis import ( MATRIX_COLORS, OPTIMIZER_COLORS, OPTIMIZER_LABELS, + discover_complete_seeds, + discover_matched_complete_seeds, final_test_summary, load_epoch_metrics, load_layer_metrics, load_metrics, load_spectral_summary, load_test_results, + mean_ci95, + paired_test_differences, plot_epoch_metric, plot_layer_metric, plot_spectral_optimizer_summary, + run_diagnostics_table, run_status_table, + summarize_run_diagnostics, ) from .config import ( SUPPORTED_OPTIMIZERS, canonical_seeds, + expected_transformer_matrix_count, load_config, roots, + run_slug, ) from .data import prepare_fineweb_edu from .model import GPT, GPTConfig @@ -39,6 +47,9 @@ "SUPPORTED_OPTIMIZERS", "canonical_seeds", "choose_device", + "discover_complete_seeds", + "discover_matched_complete_seeds", + "expected_transformer_matrix_count", "final_test_summary", "load_config", "load_epoch_metrics", @@ -46,13 +57,18 @@ "load_metrics", "load_spectral_summary", "load_test_results", + "mean_ci95", + "paired_test_differences", "plot_epoch_metric", "plot_layer_metric", "plot_spectral_optimizer_summary", "prepare_fineweb_edu", "roots", "run_all_replicates", + "run_diagnostics_table", "run_one", "run_optimizer_replicates", + "run_slug", "run_status_table", + "summarize_run_diagnostics", ] diff --git a/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/analysis.py b/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/analysis.py index 621c4b4..927a195 100644 --- a/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/analysis.py +++ b/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/analysis.py @@ -1,5 +1,6 @@ from __future__ import annotations +from itertools import combinations import json import math from pathlib import Path @@ -10,7 +11,7 @@ import pandas as pd from .config import SUPPORTED_OPTIMIZERS -from .training import run_directory, run_is_complete +from .run_utils import run_directory, run_is_complete OPTIMIZER_LABELS = { "sgd_momentum": "SGD + Nesterov", @@ -18,14 +19,12 @@ "muon": "Muon + auxiliary AdamW", } -# Okabe-Ito color-blind-safe optimizer palette. OPTIMIZER_COLORS = { "sgd_momentum": "#0072B2", "adamw": "#D55E00", "muon": "#009E73", } -# Matrix identity remains fixed across every optimizer notebook. MATRIX_COLORS = { "W_Q": "#0072B2", "W_K": "#E69F00", @@ -35,6 +34,13 @@ "W_MLP_OUT": "#56B4E9", } +_BLOCK_LINESTYLES = ( + "-", + "--", + "-.", + ":", +) + _T_975 = { 1: 12.7062047364, 2: 4.3026527297, @@ -46,6 +52,25 @@ 8: 2.3060041352, 9: 2.2621571629, 10: 2.2281388520, + 11: 2.2009851601, + 12: 2.1788128297, + 13: 2.1603686565, + 14: 2.1447866879, + 15: 2.1314495456, + 16: 2.1199052992, + 17: 2.1098155778, + 18: 2.1009220402, + 19: 2.0930240544, + 20: 2.0859634473, + 21: 2.0796138447, + 22: 2.0738730679, + 23: 2.0686576104, + 24: 2.0638985616, + 25: 2.0595385528, + 26: 2.0555294386, + 27: 2.0518305165, + 28: 2.0484071418, + 29: 2.0452296421, } @@ -76,7 +101,10 @@ def mean_ci95(values: Iterable[float]) -> dict[str, float]: } sd = float(array.std(ddof=1)) sem = sd / math.sqrt(n) - critical = _T_975.get(n - 1, 1.9599639845) + critical = _T_975.get( + n - 1, + 1.9599639845, + ) half = critical * sem return { "n": n, @@ -89,41 +117,159 @@ def mean_ci95(values: Iterable[float]) -> dict[str, float]: } +def _available_seed_dirs( + results_root: str | Path, + optimizer: str, +) -> tuple[int, ...]: + root = Path(results_root) / str(optimizer) + seeds: list[int] = [] + if not root.is_dir(): + return () + for path in root.glob("seed_*"): + if not path.is_dir(): + continue + try: + seeds.append( + int(path.name.removeprefix("seed_")) + ) + except ValueError: + continue + return tuple(sorted(set(seeds))) + + +def discover_complete_seeds( + results_root: str | Path, + optimizer: str, +) -> tuple[int, ...]: + return tuple( + seed + for seed in _available_seed_dirs( + results_root, + optimizer, + ) + if run_is_complete( + results_root, + optimizer, + seed, + ) + ) + + +def discover_matched_complete_seeds( + results_root: str | Path, + *, + optimizers: Sequence[str] = SUPPORTED_OPTIMIZERS, +) -> tuple[int, ...]: + sets = [ + set( + discover_complete_seeds( + results_root, + optimizer, + ) + ) + for optimizer in optimizers + ] + if not sets: + return () + return tuple(sorted(set.intersection(*sets))) + + def run_status_table( results_root: str | Path, *, optimizers: Sequence[str] = SUPPORTED_OPTIMIZERS, - seeds: Sequence[int] = (1337, 2027, 4099), + seeds: Sequence[int] | None = None, ) -> pd.DataFrame: + selected = ( + tuple(int(seed) for seed in seeds) + if seeds is not None + else tuple( + sorted( + { + seed + for optimizer in optimizers + for seed in _available_seed_dirs( + results_root, + optimizer, + ) + } + ) + ) + ) rows = [] for optimizer in optimizers: - for seed in seeds: - run_dir = run_directory(results_root, optimizer, seed) - completion_path = run_dir / "run_complete.json" + for seed in selected: + run_dir = run_directory( + results_root, + optimizer, + seed, + ) + completion_path = ( + run_dir / "run_complete.json" + ) payload = ( - json.loads(completion_path.read_text(encoding="utf-8")) + json.loads( + completion_path.read_text( + encoding="utf-8" + ) + ) if completion_path.is_file() else {} ) rows.append( { "optimizer": optimizer, - "optimizer_label": OPTIMIZER_LABELS.get(optimizer, optimizer), + "optimizer_label": ( + OPTIMIZER_LABELS.get( + optimizer, + optimizer, + ) + ), "seed": int(seed), - "complete": bool(payload.get("completed", False)), - "steps": payload.get("optimizer_steps", np.nan), - "final_test_loss": payload.get("final_test_loss", np.nan), - "final_test_bleu": payload.get("final_test_bleu", np.nan), + "complete": run_is_complete( + results_root, + optimizer, + seed, + ), + "steps": payload.get( + "optimizer_steps", + np.nan, + ), + "best_validation_step": payload.get( + "best_validation_step", + np.nan, + ), + "best_validation_loss": payload.get( + "best_validation_loss", + np.nan, + ), + "final_test_loss": payload.get( + "final_test_loss", + np.nan, + ), + "final_test_accuracy": payload.get( + "final_test_accuracy", + np.nan, + ), "run_dir": str(run_dir), } ) return pd.DataFrame(rows) -def _require_complete(results_root: str | Path, optimizer: str, seed: int) -> None: - if not run_is_complete(results_root, optimizer, seed): +def _require_complete( + results_root: str | Path, + optimizer: str, + seed: int, +) -> None: + if not run_is_complete( + results_root, + optimizer, + seed, + ): raise FileNotFoundError( - f"missing completed run for optimizer={optimizer} seed={seed}: " + "missing completed run for " + f"optimizer={optimizer} seed={seed}: " f"{run_directory(results_root, optimizer, seed)}" ) @@ -140,20 +286,40 @@ def _load_csvs( for optimizer in optimizers: for seed in seeds: if require_complete: - _require_complete(results_root, optimizer, seed) - path = run_directory(results_root, optimizer, seed) / relative_path + _require_complete( + results_root, + optimizer, + seed, + ) + path = ( + run_directory( + results_root, + optimizer, + seed, + ) + / relative_path + ) if not path.is_file(): if require_complete: raise FileNotFoundError(path) continue frame = pd.read_csv(path) frame["optimizer"] = optimizer - frame["optimizer_label"] = OPTIMIZER_LABELS.get(optimizer, optimizer) + frame["optimizer_label"] = ( + OPTIMIZER_LABELS.get( + optimizer, + optimizer, + ) + ) frame["seed"] = int(seed) frames.append(frame) if not frames: return pd.DataFrame() - return pd.concat(frames, ignore_index=True, sort=False) + return pd.concat( + frames, + ignore_index=True, + sort=False, + ) def load_metrics( @@ -172,8 +338,14 @@ def load_metrics( ) if frame.empty: return frame - return frame.sort_values(["optimizer", "seed", "step"]).drop_duplicates( - ["optimizer", "seed", "step"], keep="last" + return ( + frame.sort_values( + ["optimizer", "seed", "step"] + ) + .drop_duplicates( + ["optimizer", "seed", "step"], + keep="last", + ) ) @@ -193,8 +365,22 @@ def load_epoch_metrics( ) if frame.empty: return frame - return frame.sort_values(["optimizer", "seed", "nominal_epoch"]).drop_duplicates( - ["optimizer", "seed", "nominal_epoch"], keep="last" + return ( + frame.sort_values( + [ + "optimizer", + "seed", + "nominal_epoch", + ] + ) + .drop_duplicates( + [ + "optimizer", + "seed", + "nominal_epoch", + ], + keep="last", + ) ) @@ -214,8 +400,25 @@ def load_layer_metrics( ) if frame.empty: return frame - return frame.sort_values(["optimizer", "seed", "epoch", "matrix_type"]).drop_duplicates( - ["optimizer", "seed", "step", "matrix_name"], keep="last" + return ( + frame.sort_values( + [ + "optimizer", + "seed", + "epoch", + "block", + "matrix_type", + ] + ) + .drop_duplicates( + [ + "optimizer", + "seed", + "step", + "matrix_name", + ], + keep="last", + ) ) @@ -235,8 +438,14 @@ def load_spectral_summary( ) if frame.empty: return frame - return frame.sort_values(["optimizer", "seed", "epoch"]).drop_duplicates( - ["optimizer", "seed", "step"], keep="last" + return ( + frame.sort_values( + ["optimizer", "seed", "epoch"] + ) + .drop_duplicates( + ["optimizer", "seed", "step"], + keep="last", + ) ) @@ -249,22 +458,50 @@ def load_test_results( rows = [] for optimizer in optimizers: for seed in seeds: - _require_complete(results_root, optimizer, seed) - path = run_directory(results_root, optimizer, seed) / "test_results.json" - payload = json.loads(path.read_text(encoding="utf-8")) - for checkpoint in ("final", "validation_selected"): + _require_complete( + results_root, + optimizer, + seed, + ) + path = ( + run_directory( + results_root, + optimizer, + seed, + ) + / "test_results.json" + ) + payload = json.loads( + path.read_text(encoding="utf-8") + ) + for checkpoint in ( + "final", + "validation_selected", + ): values = payload[checkpoint] rows.append( { "optimizer": optimizer, - "optimizer_label": OPTIMIZER_LABELS[optimizer], + "optimizer_label": ( + OPTIMIZER_LABELS[ + optimizer + ] + ), "seed": int(seed), "checkpoint": checkpoint, "step": int(values["step"]), - "test_loss": float(values["loss"]), - "test_perplexity": float(values["perplexity"]), - "test_accuracy": float(values["accuracy"]), - "test_bleu": float(values["bleu"]), + "test_loss": float( + values["loss"] + ), + "test_perplexity": float( + values["perplexity"] + ), + "test_accuracy": float( + values["accuracy"] + ), + "test_bleu": float( + values["bleu"] + ), } ) return pd.DataFrame(rows) @@ -279,12 +516,32 @@ def summarize_by_epoch( ) -> pd.DataFrame: rows = [] keys = [*group, x] - subset = frame[[*keys, "seed", metric]].copy() - subset[metric] = pd.to_numeric(subset[metric], errors="coerce") - for values, group_frame in subset.groupby(keys, sort=True): - values_tuple = values if isinstance(values, tuple) else (values,) - row = dict(zip(keys, values_tuple, strict=True)) - row.update(mean_ci95(group_frame[metric])) + subset = frame[ + [*keys, "seed", metric] + ].copy() + subset[metric] = pd.to_numeric( + subset[metric], + errors="coerce", + ) + for values, group_frame in subset.groupby( + keys, + sort=True, + ): + values_tuple = ( + values + if isinstance(values, tuple) + else (values,) + ) + row = dict( + zip( + keys, + values_tuple, + strict=True, + ) + ) + row.update( + mean_ci95(group_frame[metric]) + ) rows.append(row) return pd.DataFrame(rows) @@ -300,42 +557,75 @@ def plot_epoch_metric( ): figure, axis = plt.subplots(figsize=(9, 5)) for optimizer in optimizers: - subset = frame[frame["optimizer"] == optimizer] + subset = frame[ + frame["optimizer"] == optimizer + ] if subset.empty: continue - for _, seed_frame in subset.groupby("seed"): + for _, seed_frame in subset.groupby( + "seed" + ): axis.plot( seed_frame[x], seed_frame[metric], - color=OPTIMIZER_COLORS[optimizer], + color=OPTIMIZER_COLORS[ + optimizer + ], alpha=0.20, linewidth=0.9, ) - summary = summarize_by_epoch(subset, metric, x=x) + summary = summarize_by_epoch( + subset, + metric, + x=x, + ) axis.plot( summary[x], summary["mean"], - color=OPTIMIZER_COLORS[optimizer], + color=OPTIMIZER_COLORS[ + optimizer + ], linewidth=2.0, - label=OPTIMIZER_LABELS[optimizer], + label=OPTIMIZER_LABELS[ + optimizer + ], ) axis.fill_between( summary[x], summary["ci95_lower"], summary["ci95_upper"], - color=OPTIMIZER_COLORS[optimizer], + color=OPTIMIZER_COLORS[ + optimizer + ], alpha=0.16, ) - axis.set_xlabel(x.replace("_", " ").title()) - axis.set_ylabel(metric.replace("_", " ").title()) - axis.set_title(title or f"{metric}: mean and 95% Student-t CI across seeds") + axis.set_xlabel( + x.replace("_", " ").title() + ) + axis.set_ylabel( + metric.replace("_", " ").title() + ) + axis.set_title( + title + or ( + f"{metric}: mean and 95% " + "Student-t CI across seeds" + ) + ) axis.grid(alpha=0.25) axis.legend(frameon=False) figure.tight_layout() if output is not None: output = Path(output) - output.parent.mkdir(parents=True, exist_ok=True) - figure.savefig(output, dpi=170, bbox_inches="tight") + output.parent.mkdir( + parents=True, + exist_ok=True, + ) + figure.savefig( + output, + dpi=170, + bbox_inches="tight", + ) return figure, axis @@ -347,51 +637,119 @@ def plot_layer_metric( title: str | None = None, output: str | Path | None = None, ): - subset = frame[frame["optimizer"] == optimizer].copy() + """Plot each block separately so blocks are never treated as replicates.""" + + subset = frame[ + frame["optimizer"] == optimizer + ].copy() if subset.empty: - raise ValueError(f"no layer data for optimizer={optimizer}") - figure, axis = plt.subplots(figsize=(10, 5.5)) - for matrix_type in MATRIX_COLORS: - matrix = subset[subset["matrix_type"] == matrix_type] - if matrix.empty: - continue - summary = summarize_by_epoch( - matrix, - metric, - x="epoch", - group=("matrix_type",), + raise ValueError( + f"no layer data for optimizer={optimizer}" ) - axis.plot( - summary["epoch"], - summary["mean"], - color=MATRIX_COLORS[matrix_type], - linewidth=2.0, - marker="o", - markersize=3, - label=matrix_type, + blocks = tuple( + sorted( + int(value) + for value in subset["block"].unique() ) - axis.fill_between( - summary["epoch"], - summary["ci95_lower"], - summary["ci95_upper"], - color=MATRIX_COLORS[matrix_type], - alpha=0.13, - ) - if metric == "alpha": - axis.axhline(2.0, color="black", linestyle="--", linewidth=1.0, label="alpha = 2") - if metric == "ERG_gap": - axis.axhline(0.0, color="black", linestyle="--", linewidth=1.0) - axis.set_xlabel("Epoch") - axis.set_ylabel(metric) - axis.set_title(title or f"{OPTIMIZER_LABELS[optimizer]} layer {metric}") - axis.grid(alpha=0.25) - axis.legend(frameon=False, ncol=2) + ) + figure, axes = plt.subplots( + 1, + len(blocks), + figsize=(5.2 * len(blocks), 5.2), + sharex=True, + sharey=True, + squeeze=False, + ) + for axis, block in zip( + axes[0], + blocks, + strict=True, + ): + block_frame = subset[ + subset["block"].astype(int) + == int(block) + ] + for matrix_type in MATRIX_COLORS: + matrix = block_frame[ + block_frame["matrix_type"] + == matrix_type + ] + if matrix.empty: + continue + summary = summarize_by_epoch( + matrix, + metric, + x="epoch", + group=( + "block", + "matrix_type", + "matrix_name", + ), + ) + axis.plot( + summary["epoch"], + summary["mean"], + color=MATRIX_COLORS[ + matrix_type + ], + linewidth=2.0, + marker="o", + markersize=3, + label=matrix_type, + ) + axis.fill_between( + summary["epoch"], + summary["ci95_lower"], + summary["ci95_upper"], + color=MATRIX_COLORS[ + matrix_type + ], + alpha=0.13, + ) + if metric == "alpha": + axis.axhline( + 2.0, + color="black", + linestyle="--", + linewidth=1.0, + label="alpha = 2", + ) + if metric == "ERG_gap": + axis.axhline( + 0.0, + color="black", + linestyle="--", + linewidth=1.0, + ) + axis.set_xlabel("Epoch") + axis.set_title(f"Block {block}") + axis.grid(alpha=0.25) + axes[0][0].set_ylabel(metric) + axes[0][-1].legend( + frameon=False, + ncol=2, + fontsize=8, + ) + figure.suptitle( + title + or ( + f"{OPTIMIZER_LABELS[optimizer]} " + f"layer {metric}" + ) + ) figure.tight_layout() if output is not None: output = Path(output) - output.parent.mkdir(parents=True, exist_ok=True) - figure.savefig(output, dpi=170, bbox_inches="tight") - return figure, axis + output.parent.mkdir( + parents=True, + exist_ok=True, + ) + figure.savefig( + output, + dpi=170, + bbox_inches="tight", + ) + return figure, axes def plot_spectral_optimizer_summary( @@ -406,20 +764,359 @@ def plot_spectral_optimizer_summary( metric=metric, x="epoch", optimizers=optimizers, - title=f"WeightWatcher {metric}: mean and 95% Student-t CI", + title=( + f"WeightWatcher {metric}: mean and " + "95% Student-t CI" + ), output=output, ) -def final_test_summary(test_results: pd.DataFrame) -> pd.DataFrame: +def _summary_row( + *, + optimizer: str, + checkpoint: str, + metric: str, + stats: dict[str, float], + interval_method: str, +) -> dict[str, object]: + return { + "optimizer": optimizer, + "optimizer_label": ( + OPTIMIZER_LABELS[optimizer] + ), + "checkpoint": checkpoint, + "metric": metric, + "interval_method": interval_method, + **stats, + } + + +def final_test_summary( + test_results: pd.DataFrame, +) -> pd.DataFrame: + """Summarize test metrics with a positive perplexity interval. + + Perplexity is reported as ``exp(mean loss)`` with the loss-space + Student-t interval exponentiated. This avoids impossible negative + perplexity bounds. + """ + + rows: list[dict[str, object]] = [] + for ( + optimizer, + checkpoint, + ), group in test_results.groupby( + ["optimizer", "checkpoint"] + ): + loss_stats = mean_ci95( + group["test_loss"] + ) + rows.append( + _summary_row( + optimizer=optimizer, + checkpoint=checkpoint, + metric="test_loss", + stats=loss_stats, + interval_method=( + "arithmetic_student_t" + ), + ) + ) + ppl_stats = { + "n": loss_stats["n"], + "mean": math.exp( + loss_stats["mean"] + ), + "sd": np.nan, + "sem": np.nan, + "ci95_half_width": np.nan, + "ci95_lower": math.exp( + loss_stats["ci95_lower"] + ), + "ci95_upper": math.exp( + loss_stats["ci95_upper"] + ), + } + rows.append( + _summary_row( + optimizer=optimizer, + checkpoint=checkpoint, + metric="test_perplexity", + stats=ppl_stats, + interval_method=( + "exp_test_loss_student_t" + ), + ) + ) + for metric in ( + "test_accuracy", + "test_bleu", + ): + rows.append( + _summary_row( + optimizer=optimizer, + checkpoint=checkpoint, + metric=metric, + stats=mean_ci95( + group[metric] + ), + interval_method=( + "arithmetic_student_t" + ), + ) + ) + return pd.DataFrame(rows) + + +def paired_test_differences( + test_results: pd.DataFrame, + *, + optimizers: Sequence[str] = SUPPORTED_OPTIMIZERS, +) -> pd.DataFrame: + rows: list[dict[str, object]] = [] + for checkpoint in sorted( + test_results["checkpoint"].unique() + ): + selected = test_results[ + test_results["checkpoint"] + == checkpoint + ] + for left, right in combinations( + optimizers, + 2, + ): + for metric in ( + "test_loss", + "test_accuracy", + "test_bleu", + ): + left_frame = selected[ + selected["optimizer"] == left + ][["seed", metric]].rename( + columns={metric: "left"} + ) + right_frame = selected[ + selected["optimizer"] == right + ][["seed", metric]].rename( + columns={metric: "right"} + ) + paired = left_frame.merge( + right_frame, + on="seed", + validate="one_to_one", + ) + stats = mean_ci95( + paired["left"] + - paired["right"] + ) + rows.append( + { + "checkpoint": checkpoint, + "left_optimizer": left, + "right_optimizer": right, + "contrast": ( + f"{OPTIMIZER_LABELS[left]} " + f"- {OPTIMIZER_LABELS[right]}" + ), + "metric": metric, + "interval_method": ( + "paired_student_t" + ), + **stats, + } + ) + loss_left = selected[ + selected["optimizer"] == left + ][["seed", "test_loss"]].rename( + columns={"test_loss": "left"} + ) + loss_right = selected[ + selected["optimizer"] == right + ][["seed", "test_loss"]].rename( + columns={"test_loss": "right"} + ) + paired_loss = loss_left.merge( + loss_right, + on="seed", + validate="one_to_one", + ) + log_ratio = mean_ci95( + paired_loss["left"] + - paired_loss["right"] + ) + rows.append( + { + "checkpoint": checkpoint, + "left_optimizer": left, + "right_optimizer": right, + "contrast": ( + f"{OPTIMIZER_LABELS[left]} " + f"/ {OPTIMIZER_LABELS[right]}" + ), + "metric": ( + "test_perplexity_ratio" + ), + "interval_method": ( + "exp_paired_loss_student_t" + ), + "n": log_ratio["n"], + "mean": math.exp( + log_ratio["mean"] + ), + "sd": np.nan, + "sem": np.nan, + "ci95_half_width": np.nan, + "ci95_lower": math.exp( + log_ratio["ci95_lower"] + ), + "ci95_upper": math.exp( + log_ratio["ci95_upper"] + ), + } + ) + return pd.DataFrame(rows) + + +def run_diagnostics_table( + metrics: pd.DataFrame, + test_results: pd.DataFrame, +) -> pd.DataFrame: + rows: list[dict[str, object]] = [] + selected_lookup = ( + test_results[ + test_results["checkpoint"] + == "validation_selected" + ] + .set_index(["optimizer", "seed"]) + ) + final_lookup = ( + test_results[ + test_results["checkpoint"] + == "final" + ] + .set_index(["optimizer", "seed"]) + ) + for ( + optimizer, + seed, + ), run in metrics.groupby( + ["optimizer", "seed"], + sort=True, + ): + ordered = run.sort_values("step") + finite = ordered[ + np.isfinite( + pd.to_numeric( + ordered["val_loss"], + errors="coerce", + ) + ) + ] + if finite.empty: + continue + best = finite.loc[ + finite["val_loss"].idxmin() + ] + final = finite.iloc[-1] + key = (optimizer, int(seed)) + selected_test = selected_lookup.loc[key] + final_test = final_lookup.loc[key] + clip_values = pd.to_numeric( + ordered.loc[ + ordered["step"].astype(int) > 0, + "gradient_clipped", + ], + errors="coerce", + ) + update_ratio = pd.to_numeric( + ordered["update_to_weight_ratio"], + errors="coerce", + ) + rows.append( + { + "optimizer": optimizer, + "optimizer_label": ( + OPTIMIZER_LABELS[optimizer] + ), + "seed": int(seed), + "best_validation_step": int( + selected_test["step"] + ), + "observed_best_validation_step": int( + best["step"] + ), + "best_validation_loss": float( + best["val_loss"] + ), + "final_validation_loss": float( + final["val_loss"] + ), + "final_minus_best_validation_loss": ( + float(final["val_loss"]) + - float(best["val_loss"]) + ), + "best_validation_accuracy": float( + best["val_accuracy"] + ), + "final_validation_accuracy": float( + final["val_accuracy"] + ), + "selected_test_loss": float( + selected_test["test_loss"] + ), + "selected_test_accuracy": float( + selected_test[ + "test_accuracy" + ] + ), + "final_test_loss": float( + final_test["test_loss"] + ), + "final_test_accuracy": float( + final_test["test_accuracy"] + ), + "max_update_to_weight_ratio": float( + update_ratio.max() + ), + "evaluation_snapshot_clip_fraction": ( + float(clip_values.mean()) + if clip_values.notna().any() + else np.nan + ), + } + ) + return pd.DataFrame(rows) + + +def summarize_run_diagnostics( + diagnostics: pd.DataFrame, +) -> pd.DataFrame: + metrics = ( + "best_validation_loss", + "final_validation_loss", + "final_minus_best_validation_loss", + "selected_test_loss", + "selected_test_accuracy", + "final_test_loss", + "final_test_accuracy", + "max_update_to_weight_ratio", + "evaluation_snapshot_clip_fraction", + ) rows = [] - for (optimizer, checkpoint), group in test_results.groupby(["optimizer", "checkpoint"]): - for metric in ("test_loss", "test_perplexity", "test_accuracy", "test_bleu"): + for optimizer, group in diagnostics.groupby( + "optimizer", + sort=True, + ): + for metric in metrics: rows.append( { "optimizer": optimizer, - "optimizer_label": OPTIMIZER_LABELS[optimizer], - "checkpoint": checkpoint, + "optimizer_label": ( + OPTIMIZER_LABELS[ + optimizer + ] + ), "metric": metric, **mean_ci95(group[metric]), } diff --git a/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/completion.py b/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/completion.py index fa62bc0..ba8d313 100644 --- a/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/completion.py +++ b/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/completion.py @@ -1,4 +1,4 @@ -"""Validation for completed one-head nanoGPT experiment directories.""" +"""Validation for completed nanoGPT baseline experiment directories.""" from __future__ import annotations @@ -31,7 +31,7 @@ class CompletedRunValidationError(RuntimeError): def _fail(message: str) -> NoReturn: raise CompletedRunValidationError( - "completed one-head nanoGPT run is stale or inconsistent: " + "completed nanoGPT run is stale or inconsistent: " + message + ". Use a new results directory or rerun with explicit overwrite." ) @@ -53,7 +53,7 @@ def _read_csv(path: Path) -> pd.DataFrame: except Exception as exc: _fail(f"could not read {path}: {exc}") if frame.empty: - _fail(f" {path} is empty") + _fail(f"{path} is empty") return frame @@ -71,10 +71,16 @@ def _expect(observed: Any, expected: Any, label: str) -> None: ) -def _step_tuple(frame: pd.DataFrame, label: str) -> tuple[int, ...]: +def _step_tuple( + frame: pd.DataFrame, + label: str, +) -> tuple[int, ...]: if "step" not in frame.columns: _fail(f"{label} has no step column") - values = pd.to_numeric(frame["step"], errors="coerce").to_numpy(dtype=float) + values = pd.to_numeric( + frame["step"], + errors="coerce", + ).to_numpy(dtype=float) if not np.isfinite(values).all(): _fail(f"{label} contains non-finite step values") rounded = np.rint(values) @@ -88,7 +94,11 @@ def _step_tuple(frame: pd.DataFrame, label: str) -> tuple[int, ...]: def _load_checkpoint(path: Path) -> dict[str, Any]: try: - payload = torch.load(path, map_location="cpu", weights_only=False) + payload = torch.load( + path, + map_location="cpu", + weights_only=False, + ) except Exception as exc: _fail(f"could not load checkpoint {path}: {exc}") if not isinstance(payload, dict): @@ -96,6 +106,21 @@ def _load_checkpoint(path: Path) -> dict[str, Any]: return payload +def _manifest_matrix_count( + manifest: dict[str, Any], +) -> int: + model = manifest.get("model") + if not isinstance(model, dict): + _fail("manifest has no model configuration") + n_layer = _as_int( + model.get("n_layer"), + "manifest model.n_layer", + ) + if n_layer < 1: + _fail("manifest model.n_layer must be positive") + return 6 * n_layer + + def validate_completed_run( run_dir: str | Path, *, @@ -103,13 +128,10 @@ def validate_completed_run( expected_optimizer: str | None = None, expected_seed: int | None = None, expected_total_steps: int | None = None, + expected_matrix_count: int | None = None, verify_checkpoints: bool = True, ) -> dict[str, Any]: - """Validate a completed run before it is skipped or analyzed. - - Expected values supplied by the current configuration make this a stale-run - guard. Without them, the function still verifies internal consistency. - """ + """Validate a completed run before it is skipped or analyzed.""" root = Path(run_dir) missing = [ @@ -125,18 +147,26 @@ def validate_completed_run( manifest = _read_json(root / "manifest.json") test_results = _read_json(root / "test_results.json") if completion.get("completed") is not True: - _fail("run_complete.json does not declare completed=true") + _fail( + "run_complete.json does not declare completed=true" + ) - recorded_fingerprint = str(completion.get("fingerprint", "")) + recorded_fingerprint = str( + completion.get("fingerprint", "") + ) fingerprint = ( str(expected_fingerprint) if expected_fingerprint is not None else recorded_fingerprint ) if not fingerprint or not recorded_fingerprint: - _fail("the completion record has no protocol fingerprint") + _fail( + "the completion record has no protocol fingerprint" + ) - recorded_optimizer = str(completion.get("optimizer", "")) + recorded_optimizer = str( + completion.get("optimizer", "") + ) optimizer = ( str(expected_optimizer) if expected_optimizer is not None @@ -145,10 +175,18 @@ def validate_completed_run( if not optimizer or not recorded_optimizer: _fail("the completion record has no optimizer") - recorded_seed = _as_int(completion.get("seed"), "completion seed") - seed = int(expected_seed) if expected_seed is not None else recorded_seed + recorded_seed = _as_int( + completion.get("seed"), + "completion seed", + ) + seed = ( + int(expected_seed) + if expected_seed is not None + else recorded_seed + ) recorded_steps = _as_int( - completion.get("optimizer_steps"), "completion optimizer_steps" + completion.get("optimizer_steps"), + "completion optimizer_steps", ) total_steps = ( int(expected_total_steps) @@ -160,58 +198,145 @@ def validate_completed_run( "completion best_validation_step", ) - _expect(recorded_fingerprint, fingerprint, "completion fingerprint") - _expect(recorded_optimizer, optimizer, "completion optimizer") + _expect( + recorded_fingerprint, + fingerprint, + "completion fingerprint", + ) + _expect( + recorded_optimizer, + optimizer, + "completion optimizer", + ) _expect(recorded_seed, seed, "completion seed") - _expect(recorded_steps, total_steps, "completion optimizer_steps") + _expect( + recorded_steps, + total_steps, + "completion optimizer_steps", + ) _expect( str(manifest.get("protocol_fingerprint", "")), fingerprint, "manifest fingerprint", ) - _expect(str(manifest.get("optimizer", "")), optimizer, "manifest optimizer") - _expect(_as_int(manifest.get("seed"), "manifest seed"), seed, "manifest seed") _expect( - _as_int(manifest.get("max_steps"), "manifest max_steps"), + str(manifest.get("optimizer", "")), + optimizer, + "manifest optimizer", + ) + _expect( + _as_int(manifest.get("seed"), "manifest seed"), + seed, + "manifest seed", + ) + _expect( + _as_int( + manifest.get("max_steps"), + "manifest max_steps", + ), total_steps, "manifest max_steps", ) + manifest_matrix_count = _manifest_matrix_count( + manifest + ) + matrix_count = ( + int(expected_matrix_count) + if expected_matrix_count is not None + else manifest_matrix_count + ) + if matrix_count < 1: + _fail("expected matrix count must be positive") + _expect( + manifest_matrix_count, + matrix_count, + "manifest transformer matrix count", + ) + final_test = test_results.get("final") - selected_test = test_results.get("validation_selected") - if not isinstance(final_test, dict) or not isinstance(selected_test, dict): - _fail("test_results.json lacks final or validation_selected results") + selected_test = test_results.get( + "validation_selected" + ) + if not isinstance(final_test, dict) or not isinstance( + selected_test, + dict, + ): + _fail( + "test_results.json lacks final or " + "validation_selected results" + ) _expect( - _as_int(final_test.get("step"), "final test step"), + _as_int( + final_test.get("step"), + "final test step", + ), total_steps, "final test step", ) _expect( - _as_int(selected_test.get("step"), "selected test step"), + _as_int( + selected_test.get("step"), + "selected test step", + ), best_step, "selected test step", ) metrics = _read_csv(root / "metrics.csv") - epoch_metrics = _read_csv(root / "epoch_metrics.csv") - layers = _read_csv(root / "spectral" / "layers.csv") - summary = _read_csv(root / "spectral" / "summary.csv") - metric_steps = _step_tuple(metrics, "metrics.csv") - epoch_steps = _step_tuple(epoch_metrics, "epoch_metrics.csv") - summary_steps = _step_tuple(summary, "spectral/summary.csv") + epoch_metrics = _read_csv( + root / "epoch_metrics.csv" + ) + layers = _read_csv( + root / "spectral" / "layers.csv" + ) + summary = _read_csv( + root / "spectral" / "summary.csv" + ) + metric_steps = _step_tuple( + metrics, + "metrics.csv", + ) + epoch_steps = _step_tuple( + epoch_metrics, + "epoch_metrics.csv", + ) + summary_steps = _step_tuple( + summary, + "spectral/summary.csv", + ) for label, steps in ( ("metrics.csv", metric_steps), ("epoch_metrics.csv", epoch_steps), ): - if 0 not in steps or total_steps not in steps or max(steps) != total_steps: - _fail(f"{label} does not span step zero through {total_steps}") + if ( + 0 not in steps + or total_steps not in steps + or max(steps) != total_steps + ): + _fail( + f"{label} does not span step zero " + f"through {total_steps}" + ) if "test_monitoring_only" not in epoch_metrics.columns: - _fail("epoch_metrics.csv has no test_monitoring_only column") - policy = pd.to_numeric(epoch_metrics["test_monitoring_only"], errors="coerce") - if policy.isna().any() or not policy.astype(int).eq(1).all(): - _fail("epoch_metrics.csv violates the monitoring-only test policy") + _fail( + "epoch_metrics.csv has no " + "test_monitoring_only column" + ) + policy = pd.to_numeric( + epoch_metrics["test_monitoring_only"], + errors="coerce", + ) + if ( + policy.isna().any() + or not policy.astype(int).eq(1).all() + ): + _fail( + "epoch_metrics.csv violates the " + "monitoring-only test policy" + ) required_layer_columns = { "step", @@ -220,35 +345,74 @@ def validate_completed_run( "ERG_gap", "num_traps", } - missing_columns = required_layer_columns.difference(layers.columns) + missing_columns = required_layer_columns.difference( + layers.columns + ) if missing_columns: _fail( "spectral/layers.csv is missing columns " + ", ".join(sorted(missing_columns)) ) - if layers.duplicated(["step", "matrix_name"]).any(): - _fail("spectral/layers.csv has duplicate step/matrix rows") + if layers.duplicated( + ["step", "matrix_name"] + ).any(): + _fail( + "spectral/layers.csv has duplicate " + "step/matrix rows" + ) layer_steps = _step_tuple( - layers[["step"]].drop_duplicates().sort_values("step"), + layers[["step"]] + .drop_duplicates() + .sort_values("step"), "spectral/layers.csv", ) - if set(summary_steps) != set(epoch_steps) or set(layer_steps) != set(epoch_steps): - _fail("spectral steps do not match epoch_metrics.csv") - if not layers.groupby("step")["matrix_name"].nunique().eq(6).all(): - _fail("spectral/layers.csv does not contain six matrices per epoch") + if ( + set(summary_steps) != set(epoch_steps) + or set(layer_steps) != set(epoch_steps) + ): + _fail( + "spectral steps do not match " + "epoch_metrics.csv" + ) + layer_counts = ( + layers.groupby("step")["matrix_name"] + .nunique() + ) + if not layer_counts.eq(matrix_count).all(): + _fail( + "spectral/layers.csv does not contain " + f"{matrix_count} matrices per epoch" + ) if "n_matrices" not in summary.columns: - _fail("spectral/summary.csv has no n_matrices column") - matrix_counts = pd.to_numeric(summary["n_matrices"], errors="coerce") - if matrix_counts.isna().any() or not matrix_counts.astype(int).eq(6).all(): - _fail("spectral/summary.csv does not report six matrices per epoch") + _fail( + "spectral/summary.csv has no n_matrices column" + ) + matrix_counts = pd.to_numeric( + summary["n_matrices"], + errors="coerce", + ) + if ( + matrix_counts.isna().any() + or not matrix_counts.astype(int) + .eq(matrix_count) + .all() + ): + _fail( + "spectral/summary.csv does not report " + f"{matrix_count} matrices per epoch" + ) if "checkpoint_path" not in epoch_metrics.columns: - _fail("epoch_metrics.csv has no checkpoint_path column") + _fail( + "epoch_metrics.csv has no checkpoint_path column" + ) recorded_checkpoint_paths = [ Path(str(value)) for value in epoch_metrics["checkpoint_path"] ] - if len(recorded_checkpoint_paths) != len(set(epoch_steps)): + if len(recorded_checkpoint_paths) != len( + set(epoch_steps) + ): _fail( "epoch checkpoint inventory does not match " "epoch_metrics.csv" @@ -257,67 +421,131 @@ def validate_completed_run( for recorded in recorded_checkpoint_paths: candidate = recorded if not candidate.is_file(): - candidate = root / "epoch_checkpoints" / recorded.name - if not candidate.is_file() or candidate.stat().st_size == 0: + candidate = ( + root + / "epoch_checkpoints" + / recorded.name + ) + if ( + not candidate.is_file() + or candidate.stat().st_size == 0 + ): _fail( - f"missing epoch checkpoint recorded by " + "missing epoch checkpoint recorded by " f"epoch_metrics.csv: {recorded}" ) - resolved_checkpoint_paths.append(candidate.resolve()) + resolved_checkpoint_paths.append( + candidate.resolve() + ) if len(resolved_checkpoint_paths) != len( set(resolved_checkpoint_paths) ): _fail( - "epoch_metrics.csv references duplicate epoch " - "checkpoints" + "epoch_metrics.csv references duplicate " + "epoch checkpoints" ) if verify_checkpoints: try: - best_loss = float(completion.get("best_validation_loss")) + best_loss = float( + completion.get( + "best_validation_loss" + ) + ) except (TypeError, ValueError): - _fail("run_complete.json has invalid best_validation_loss") + _fail( + "run_complete.json has invalid " + "best_validation_loss" + ) for filename, expected_step in ( - ("checkpoint_latest.pt", total_steps), - ("checkpoint_final.pt", total_steps), - ("checkpoint_best.pt", best_step), + ( + "checkpoint_latest.pt", + total_steps, + ), + ( + "checkpoint_final.pt", + total_steps, + ), + ( + "checkpoint_best.pt", + best_step, + ), ): - payload = _load_checkpoint(root / filename) + payload = _load_checkpoint( + root / filename + ) _expect( - str(payload.get("fingerprint", "")), + str( + payload.get( + "fingerprint", + "", + ) + ), fingerprint, f"{filename} fingerprint", ) _expect( - str(payload.get("optimizer_name", "")), + str( + payload.get( + "optimizer_name", + "", + ) + ), optimizer, f"{filename} optimizer", ) _expect( - _as_int(payload.get("seed"), f"{filename} seed"), + _as_int( + payload.get("seed"), + f"{filename} seed", + ), seed, f"{filename} seed", ) _expect( - _as_int(payload.get("step"), f"{filename} step"), + _as_int( + payload.get("step"), + f"{filename} step", + ), expected_step, f"{filename} step", ) _expect( _as_int( - payload.get("best_validation_step"), - f"{filename} best_validation_step", + payload.get( + "best_validation_step" + ), + ( + f"{filename} " + "best_validation_step" + ), ), best_step, - f"{filename} best_validation_step", + ( + f"{filename} " + "best_validation_step" + ), ) try: - stored_loss = float(payload.get("best_validation_loss")) + stored_loss = float( + payload.get( + "best_validation_loss" + ) + ) except (TypeError, ValueError): - _fail(f"{filename} has invalid best_validation_loss") + _fail( + f"{filename} has invalid " + "best_validation_loss" + ) if not math.isclose( - stored_loss, best_loss, rel_tol=1e-12, abs_tol=1e-12 + stored_loss, + best_loss, + rel_tol=1e-12, + abs_tol=1e-12, ): - _fail(f"{filename} best_validation_loss does not match completion") + _fail( + f"{filename} best_validation_loss " + "does not match completion" + ) return completion diff --git a/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/config.py b/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/config.py index 8e26450..fafb58d 100644 --- a/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/config.py +++ b/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/config.py @@ -5,15 +5,70 @@ import json import os from pathlib import Path +import re from typing import Any import yaml SUPPORTED_OPTIMIZERS = ("sgd_momentum", "adamw", "muon") DEFAULT_ROOT = Path("/tmp/rg-nanogpt-one-head") +_RUN_SLUG_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$") -def roots() -> dict[str, Path]: +def run_slug(cfg: dict[str, Any]) -> str: + protocol = cfg.get("protocol", {}) + value = str(protocol.get("run_slug", "")).strip() + if not value: + value = str(protocol.get("name", "reference")).strip() + if not _RUN_SLUG_PATTERN.fullmatch(value): + raise ValueError( + "protocol.run_slug must contain only letters, numbers, dot, " + "underscore, and hyphen" + ) + return value + + +def roots(cfg: dict[str, Any] | None = None) -> dict[str, Path]: + """Resolve isolated v3 or NGB-v4 runtime roots under ``/tmp``. + + Version-3 callers retain the historical ``RG_NANOGPT_ONE_HEAD_*`` + environment contract. Version-4 configurations use the separate + ``RG_NGB_*`` contract and a required run slug, preventing artifact reuse + across protocols or architectures. + """ + + version = int(cfg.get("protocol", {}).get("version", 0)) if cfg else 0 + if cfg is not None and version >= 4: + protocol = cfg["protocol"] + root = Path( + os.environ.get( + "RG_NGB_ROOT", + protocol.get("storage_root", "/tmp/rg-ngb"), + ) + ) + data = Path( + os.environ.get( + "RG_NGB_DATA_ROOT", + protocol.get( + "data_root", + "/tmp/rg-nanogpt-one-head/data", + ), + ) + ) + results_base = Path( + os.environ.get("RG_NGB_RESULTS_ROOT", root / "results") + ) + plots_base = Path( + os.environ.get("RG_NGB_PLOTS_ROOT", root / "plots") + ) + slug = run_slug(cfg) + return { + "root": root, + "data": data, + "results": results_base / slug, + "plots": plots_base / slug, + } + root = Path(os.environ.get("RG_NANOGPT_ONE_HEAD_ROOT", DEFAULT_ROOT)) return { "root": root, @@ -55,21 +110,25 @@ def validate_config(cfg: dict[str, Any]) -> None: raise ValueError(f"missing configuration section: {section}") protocol = cfg["protocol"] - if int(protocol.get("version", 0)) < 1: + version = int(protocol.get("version", 0)) + if version < 1: raise ValueError("protocol.version must be positive") + if version >= 4: + run_slug(cfg) + for key in ("storage_root", "data_root"): + value = protocol.get(key) + if value is None: + continue + path = Path(str(value)) + if not path.is_absolute() or path.parts[:2] != ("/", "tmp"): + raise ValueError( + f"protocol.{key} must be an absolute /tmp path" + ) model = cfg["model"] for key in ("vocab_size", "block_size", "n_layer", "n_head", "n_embd"): if int(model[key]) < 1: raise ValueError(f"model.{key} must be positive") - if int(model["n_head"]) != 1: - raise ValueError( - "this experiment is intentionally fixed to exactly one attention head" - ) - if int(model["n_layer"]) != 1: - raise ValueError( - "this experiment is intentionally fixed to one transformer block" - ) if int(model["n_embd"]) % int(model["n_head"]) != 0: raise ValueError("model.n_embd must be divisible by model.n_head") if not 0.0 <= float(model.get("dropout", 0.0)) < 1.0: @@ -94,9 +153,15 @@ def validate_config(cfg: dict[str, Any]) -> None: ): if float(training[key]) <= 0: raise ValueError(f"training.{key} must be positive") + if float(training["epoch_interval"]) > float(training["target_epochs"]): + raise ValueError( + "training.epoch_interval cannot exceed training.target_epochs" + ) seeds = [int(seed) for seed in training["seeds"]] if not seeds or len(set(seeds)) != len(seeds): raise ValueError("training.seeds must contain unique values") + if any(seed < 0 for seed in seeds): + raise ValueError("training.seeds must be nonnegative") if float(training["grad_clip"]) < 0: raise ValueError("training.grad_clip must be nonnegative") @@ -231,7 +296,7 @@ def warmup_steps(profile: dict[str, Any], total_steps: int) -> int: def epoch_step_map( cfg: dict[str, Any], train_tokens: int | None = None ) -> dict[int, float]: - """Map preregistered nominal epochs, including epoch zero, to optimizer steps.""" + """Map preregistered nominal epochs, including epoch zero, to steps.""" train_tokens = int(train_tokens or cfg["dataset"]["train_tokens"]) total_steps = max_steps(cfg, train_tokens) @@ -259,6 +324,10 @@ def epoch_step_map( return dict(sorted(result.items())) +def expected_transformer_matrix_count(cfg: dict[str, Any]) -> int: + return 6 * int(cfg["model"]["n_layer"]) + + def protocol_fingerprint( cfg: dict[str, Any], *, diff --git a/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/data.py b/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/data.py index 63015c4..62e04c7 100644 --- a/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/data.py +++ b/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/data.py @@ -320,7 +320,7 @@ def main() -> None: parser.add_argument("--force", action="store_true") args = parser.parse_args() cfg = load_config(args.config) - output = Path(args.output_dir) if args.output_dir else roots()["data"] + output = Path(args.output_dir) if args.output_dir else roots(cfg)["data"] prepare_fineweb_edu(cfg, output, force=args.force) diff --git a/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/model.py b/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/model.py index eb696bd..2fd7509 100644 --- a/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/model.py +++ b/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/model.py @@ -20,8 +20,8 @@ class GPTConfig: tie_weights: bool = True def __post_init__(self) -> None: - if self.n_layer != 1 or self.n_head != 1: - raise ValueError("the reference architecture is fixed to one block and one attention head") + if self.n_layer < 1 or self.n_head < 1: + raise ValueError("n_layer and n_head must be positive") if self.n_embd % self.n_head != 0: raise ValueError("n_embd must be divisible by n_head") if self.block_size < 2 or self.vocab_size < 2 or self.n_embd < 1: @@ -37,7 +37,13 @@ def __init__(self, width: int, bias: bool) -> None: self.bias = nn.Parameter(torch.zeros(width)) if bias else None def forward(self, x: torch.Tensor) -> torch.Tensor: - return F.layer_norm(x, self.weight.shape, self.weight, self.bias, 1e-5) + return F.layer_norm( + x, + self.weight.shape, + self.weight, + self.bias, + 1e-5, + ) class CausalSelfAttention(nn.Module): @@ -46,18 +52,38 @@ def __init__(self, cfg: GPTConfig) -> None: self.n_head = cfg.n_head self.n_embd = cfg.n_embd self.dropout = float(cfg.dropout) - self.q_proj = nn.Linear(cfg.n_embd, cfg.n_embd, bias=cfg.bias) - self.k_proj = nn.Linear(cfg.n_embd, cfg.n_embd, bias=cfg.bias) - self.v_proj = nn.Linear(cfg.n_embd, cfg.n_embd, bias=cfg.bias) - self.out_proj = nn.Linear(cfg.n_embd, cfg.n_embd, bias=cfg.bias) + self.q_proj = nn.Linear( + cfg.n_embd, cfg.n_embd, bias=cfg.bias + ) + self.k_proj = nn.Linear( + cfg.n_embd, cfg.n_embd, bias=cfg.bias + ) + self.v_proj = nn.Linear( + cfg.n_embd, cfg.n_embd, bias=cfg.bias + ) + self.out_proj = nn.Linear( + cfg.n_embd, cfg.n_embd, bias=cfg.bias + ) self.resid_dropout = nn.Dropout(cfg.dropout) def forward(self, x: torch.Tensor) -> torch.Tensor: batch, sequence, channels = x.shape head_width = channels // self.n_head - q = self.q_proj(x).view(batch, sequence, self.n_head, head_width).transpose(1, 2) - k = self.k_proj(x).view(batch, sequence, self.n_head, head_width).transpose(1, 2) - v = self.v_proj(x).view(batch, sequence, self.n_head, head_width).transpose(1, 2) + q = ( + self.q_proj(x) + .view(batch, sequence, self.n_head, head_width) + .transpose(1, 2) + ) + k = ( + self.k_proj(x) + .view(batch, sequence, self.n_head, head_width) + .transpose(1, 2) + ) + v = ( + self.v_proj(x) + .view(batch, sequence, self.n_head, head_width) + .transpose(1, 2) + ) y = F.scaled_dot_product_attention( q, k, @@ -66,19 +92,31 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: dropout_p=self.dropout if self.training else 0.0, is_causal=True, ) - y = y.transpose(1, 2).contiguous().view(batch, sequence, channels) + y = ( + y.transpose(1, 2) + .contiguous() + .view(batch, sequence, channels) + ) return self.resid_dropout(self.out_proj(y)) class MLP(nn.Module): def __init__(self, cfg: GPTConfig) -> None: super().__init__() - self.fc = nn.Linear(cfg.n_embd, 4 * cfg.n_embd, bias=cfg.bias) - self.proj = nn.Linear(4 * cfg.n_embd, cfg.n_embd, bias=cfg.bias) + self.fc = nn.Linear( + cfg.n_embd, 4 * cfg.n_embd, bias=cfg.bias + ) + self.proj = nn.Linear( + 4 * cfg.n_embd, cfg.n_embd, bias=cfg.bias + ) self.dropout = nn.Dropout(cfg.dropout) def forward(self, x: torch.Tensor) -> torch.Tensor: - return self.dropout(self.proj(F.gelu(self.fc(x), approximate="tanh"))) + return self.dropout( + self.proj( + F.gelu(self.fc(x), approximate="tanh") + ) + ) class Block(nn.Module): @@ -98,34 +136,59 @@ class GPT(nn.Module): def __init__(self, cfg: GPTConfig) -> None: super().__init__() self.cfg = cfg - self.token_embedding = nn.Embedding(cfg.vocab_size, cfg.n_embd) - self.position_embedding = nn.Embedding(cfg.block_size, cfg.n_embd) + self.token_embedding = nn.Embedding( + cfg.vocab_size, cfg.n_embd + ) + self.position_embedding = nn.Embedding( + cfg.block_size, cfg.n_embd + ) self.drop = nn.Dropout(cfg.dropout) - self.blocks = nn.ModuleList([Block(cfg) for _ in range(cfg.n_layer)]) + self.blocks = nn.ModuleList( + [Block(cfg) for _ in range(cfg.n_layer)] + ) self.ln_f = LayerNorm(cfg.n_embd, cfg.bias) - self.lm_head = nn.Linear(cfg.n_embd, cfg.vocab_size, bias=False) + self.lm_head = nn.Linear( + cfg.n_embd, cfg.vocab_size, bias=False + ) if cfg.tie_weights: self.lm_head.weight = self.token_embedding.weight self.apply(self._init_module) residual_std = 0.02 / math.sqrt(2 * cfg.n_layer) for block in self.blocks: - nn.init.normal_(block.attn.out_proj.weight, mean=0.0, std=residual_std) - nn.init.normal_(block.mlp.proj.weight, mean=0.0, std=residual_std) + nn.init.normal_( + block.attn.out_proj.weight, + mean=0.0, + std=residual_std, + ) + nn.init.normal_( + block.mlp.proj.weight, + mean=0.0, + std=residual_std, + ) @staticmethod def _init_module(module: nn.Module) -> None: if isinstance(module, (nn.Linear, nn.Embedding)): - nn.init.normal_(module.weight, mean=0.0, std=0.02) + nn.init.normal_( + module.weight, + mean=0.0, + std=0.02, + ) if isinstance(module, nn.Linear) and module.bias is not None: nn.init.zeros_(module.bias) def hidden_states(self, idx: torch.Tensor) -> torch.Tensor: _, sequence = idx.shape if sequence > self.cfg.block_size: - raise ValueError("input sequence exceeds model.block_size") + raise ValueError( + "input sequence exceeds model.block_size" + ) positions = torch.arange(sequence, device=idx.device) - x = self.drop(self.token_embedding(idx) + self.position_embedding(positions)) + x = self.drop( + self.token_embedding(idx) + + self.position_embedding(positions) + ) for block in self.blocks: x = block(x) return self.ln_f(x) @@ -138,36 +201,53 @@ def forward( logits = self.lm_head(self.hidden_states(idx)) loss = None if targets is not None: - loss = F.cross_entropy(logits.reshape(-1, logits.size(-1)), targets.reshape(-1)) + loss = F.cross_entropy( + logits.reshape(-1, logits.size(-1)), + targets.reshape(-1), + ) return logits, loss def next_token_logits(self, idx: torch.Tensor) -> torch.Tensor: - # Apply the expensive vocabulary projection only to the final position. hidden = self.hidden_states(idx)[:, -1:, :] return self.lm_head(hidden) @torch.inference_mode() - def generate_greedy(self, prompts: torch.Tensor, max_new_tokens: int) -> torch.Tensor: + def generate_greedy( + self, + prompts: torch.Tensor, + max_new_tokens: int, + ) -> torch.Tensor: if prompts.ndim != 2: - raise ValueError("prompts must be [batch, sequence]") + raise ValueError( + "prompts must be [batch, sequence]" + ) if max_new_tokens < 0: - raise ValueError("max_new_tokens must be nonnegative") + raise ValueError( + "max_new_tokens must be nonnegative" + ) idx = prompts for _ in range(int(max_new_tokens)): idx_cond = idx[:, -self.cfg.block_size :] logits = self.next_token_logits(idx_cond) - next_token = logits[:, -1, :].argmax(dim=-1, keepdim=True) + next_token = logits[:, -1, :].argmax( + dim=-1, + keepdim=True, + ) idx = torch.cat((idx, next_token), dim=1) return idx def parameter_count(self) -> int: - return sum(parameter.numel() for parameter in self.parameters()) + return sum( + parameter.numel() + for parameter in self.parameters() + ) def transformer_matrix_items( model: GPT, ) -> list[tuple[str, str, int, torch.Tensor]]: - """Return the six transformer matrices used by WeightWatcher and Muon.""" + """Return six WeightWatcher/Muon matrices per transformer block.""" + items: list[tuple[str, str, int, torch.Tensor]] = [] for block_index, block in enumerate(model.blocks): matrices = ( @@ -179,5 +259,12 @@ def transformer_matrix_items( ("W_MLP_OUT", block.mlp.proj.weight), ) for matrix_type, weight in matrices: - items.append((f"L{block_index:02d}_{matrix_type}", matrix_type, block_index, weight)) + items.append( + ( + f"L{block_index:02d}_{matrix_type}", + matrix_type, + block_index, + weight, + ) + ) return items diff --git a/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/spectral.py b/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/spectral.py index 7417468..bcd425c 100644 --- a/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/spectral.py +++ b/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/spectral.py @@ -32,7 +32,7 @@ class WeightMatrixHolder(nn.Module): - """CPU-only Linear view of the six one-block transformer matrices.""" + """CPU-only Linear view of all transformer matrices.""" def __init__(self, model: GPT) -> None: super().__init__() @@ -198,7 +198,8 @@ def run_weightwatcher( import weightwatcher as ww except ImportError as exc: raise RuntimeError( - "WeightWatcher is required; run scripts/setup_mac.sh" + "WeightWatcher is required; install baseline/nanogpt_one_head " + "in the active conda environment" ) from exc run_dir = Path(run_dir) diff --git a/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/training.py b/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/training.py index fd3cf8c..ea66254 100644 --- a/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/training.py +++ b/baseline/nanogpt_one_head/src/rg_nanogpt_one_head/training.py @@ -25,7 +25,7 @@ def run_optimizer_replicates( prepare_data: bool = True, progress: bool = True, ) -> list[Path]: - resolved = roots() + resolved = roots(cfg) data_root = Path(data_root or resolved["data"]) results_root = Path(results_root or resolved["results"]) selected_seeds = tuple(int(seed) for seed in (seeds or canonical_seeds(cfg))) @@ -61,7 +61,7 @@ def run_all_replicates( overwrite: bool = False, progress: bool = True, ) -> list[Path]: - resolved = roots() + resolved = roots(cfg) data_root = Path(data_root or resolved["data"]) results_root = Path(results_root or resolved["results"]) prepare_fineweb_edu(cfg, data_root) diff --git a/baseline/nanogpt_one_head/tests/test_completion.py b/baseline/nanogpt_one_head/tests/test_completion.py index d61f1cd..b4725b9 100644 --- a/baseline/nanogpt_one_head/tests/test_completion.py +++ b/baseline/nanogpt_one_head/tests/test_completion.py @@ -139,6 +139,7 @@ def _write_completed_run(results_root: Path, cfg: dict) -> tuple[Path, str, int] "optimizer": OPTIMIZER, "seed": SEED, "max_steps": total_steps, + "model": cfg["model"], "warmup_steps": warmup_steps(profile, total_steps), "protocol_fingerprint": fingerprint, } diff --git a/baseline/nanogpt_one_head/tests/test_ngb_v4.py b/baseline/nanogpt_one_head/tests/test_ngb_v4.py new file mode 100644 index 0000000..44e049f --- /dev/null +++ b/baseline/nanogpt_one_head/tests/test_ngb_v4.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from pathlib import Path +import runpy + +_SOURCE = ( + Path(__file__).resolve().parents[2] + / "ngb" + / "tests" + / "test_ngb.py" +) +_NAMESPACE = runpy.run_path(str(_SOURCE)) +for _name, _value in _NAMESPACE.items(): + if _name.startswith("test_"): + globals()[_name] = _value diff --git a/baseline/nanogpt_one_head/tests/test_one_head.py b/baseline/nanogpt_one_head/tests/test_one_head.py index 80374de..d28a5e3 100644 --- a/baseline/nanogpt_one_head/tests/test_one_head.py +++ b/baseline/nanogpt_one_head/tests/test_one_head.py @@ -415,7 +415,11 @@ def test_tiny_cpu_training_writes_restart_and_epoch_artifacts( def test_notebooks_are_valid_and_expose_requested_metrics(): - notebook_paths = sorted((EXPERIMENT_ROOT / "notebooks").glob("*.ipynb")) + notebook_paths = sorted( + path + for path in (EXPERIMENT_ROOT / "notebooks").glob("*.ipynb") + if not path.name.endswith(".out.ipynb") + ) assert [path.name for path in notebook_paths] == [ "01_sgd_momentum_baseline.ipynb", "02_adamw_baseline.ipynb", diff --git a/baseline/ngb/.gitignore b/baseline/ngb/.gitignore new file mode 100644 index 0000000..a1875f8 --- /dev/null +++ b/baseline/ngb/.gitignore @@ -0,0 +1,3 @@ +.ipynb_checkpoints/ +__pycache__/ +*.pyc diff --git a/baseline/ngb/QUALIFICATION.md b/baseline/ngb/QUALIFICATION.md new file mode 100644 index 0000000..37d5fdf --- /dev/null +++ b/baseline/ngb/QUALIFICATION.md @@ -0,0 +1,48 @@ +# NGB v4 qualification plan + +The committed v4 optimizer profiles are conservative center candidates derived +from the v3 three-seed trajectories. They are not declared globally optimal. +Protected test metrics and WeightWatcher diagnostics must not select a profile. + +## Stage 1 — instability screen + +Use seeds `1337,2027`, which exposed the largest v3 adaptive-optimizer +bifurcation. Run temporary 0.5-epoch configurations in separate result roots. + +Candidate neighborhoods: + +| Optimizer | Screen | +|---|---| +| SGD + Nesterov | peak LR `{0.03, 0.05}`; floor `1%` of peak; decay `0.01` | +| AdamW | peak LR `{2e-4, 3e-4}`; decay `{0.05, 0.10}`; floor `1e-5` | +| Muon matrices | peak LR `{0.005, 0.01}`; decay `{0.01, 0.02}`; floor `{1e-4, 2e-4}` | +| Muon auxiliary AdamW | peak LR `{2e-4, 3e-4}`; decay `{0.01, 0.10}`; floor `1e-5` | + +Reject a candidate if a run becomes nonfinite, if final validation loss exceeds +its observed minimum by more than `0.25`, or if update-to-weight trajectories +show discontinuous excursions. + +## Stage 2 — two-epoch qualification + +Promote surviving candidates to seeds `1337,2027,4099` and two +corpus-equivalent epochs. Rank strictly by: + +1. mean best validation cross-entropy; +2. worst-seed best validation cross-entropy; +3. seed standard deviation; +4. final-minus-best validation-loss drift. + +Do not use alpha, ERG gap, trap counts, BLEU, test loss, or test accuracy to +select the optimizer profile. + +## Stage 3 — frozen matched replication + +After locking one profile per optimizer, run the same eight seeds for every arm: + +```text +1337, 2027, 4099, 5003, 6007, 7013, 8017, 9011 +``` + +Report final and validation-selected task metrics with run-level 95% Student-t +intervals and matched-seed paired contrasts. Perplexity intervals must be +obtained by exponentiating the corresponding loss-space interval. diff --git a/baseline/ngb/README.md b/baseline/ngb/README.md new file mode 100644 index 0000000..fd5fa72 --- /dev/null +++ b/baseline/ngb/README.md @@ -0,0 +1,210 @@ +# NGB v4 — nanoGPT baselines + +`baseline/ngb` is the version-4 nanoGPT experiment family. It is separate from +`baseline/nanogpt_one_head`, so the existing v3 one-epoch runs and checked-in +comparison notebook remain intact. + +NGB contains two preregistered architectures: + +1. **v4 one-head control** — one block, one attention head, width 128. +2. **v4 small 4×4 model** — four blocks, four attention heads, width 128. + +Both use the same pinned, document-disjoint FineWeb-Edu corpus and the same +fixed train/validation/test/BLEU probes. Test metrics remain monitoring-only. +Validation cross-entropy selects `checkpoint_best.pt`. + +## Why v4 exists + +The v3 experiment used one corpus-equivalent epoch and optimizer profiles +transferred almost directly from much larger nanoGPT/Muon training regimes. +The checked-in comparison showed: + +- SGD was reproducible but still improving near the one-epoch boundary. +- AdamW and Muon reached better validation-selected checkpoints but exhibited + large seed-dependent final-checkpoint drift. +- The adaptive profiles used a high peak learning rate relative to the + 8,192-token effective batch. +- The one-head model was 96.55% tied token embedding / output head by parameter + count, so Muon's auxiliary AdamW controlled most of the model. + +NGB v4 therefore changes both the horizon and the optimizer centers rather than +merely doubling the old schedule. + +## Protocols + +| Field | v4 one-head | v4 small 4×4 | +|---|---:|---:| +| Blocks | 1 | 4 | +| Attention heads | 1 | 4 | +| Embedding width | 128 | 128 | +| Context length | 256 | 256 | +| Parameters | 6,662,656 | 7,253,248 | +| Muon matrix parameters | 196,608 | 786,432 | +| Training tokens | about 160M | about 160M | +| Corpus-equivalent epochs | 2.0 | 2.0 | +| Reporting interval | 0.25 epoch | 0.25 epoch | +| Default seeds | 1337, 2027, 4099 | 1337, 2027, 4099 | + +The 4×4 architecture has only about 9% more total parameters than the one-head +control because both models are dominated by the tied GPT-2 vocabulary +embedding. It nevertheless has four times as many hidden transformer matrices +and materially greater contextual capacity. + +## Tuned optimizer centers + +### One-head v4 + +| Optimizer | Peak LR | Floor | Warm-up | Weight decay | +|---|---:|---:|---:|---:| +| SGD + Nesterov | 0.05 | 5e-4 | 5% | 0.01 | +| AdamW | 3e-4 | 1e-5 | 2.5% | 0.10 | +| Muon matrices | 0.01 | 1e-4 | 2.5% | 0.02 | +| Muon auxiliary AdamW | 2e-4 | 1e-5 | 2.5% | 0.10 | + +### Small 4×4 v4 + +The adaptive profiles are the same. The SGD peak is reduced to `0.03`, with a +`3e-4` floor, for the deeper residual stack. + +These are conservative v4 center profiles, not a claim that a validation-only +grid search has already proved global optimality. The protocol is designed to +train stably enough that the full two-epoch trajectory is scientifically +interpretable. + +## Storage + +No NGB command defaults to a home directory. + +```text +/tmp/rg-ngb/ + results/ + v4_one_head/ + v4_small_4x4/ + plots/ + v4_one_head/ + v4_small_4x4/ +``` + +The prepared v3 corpus can be reused directly: + +```text +/tmp/rg-nanogpt-one-head/data +``` + +Set a different explicit `/tmp` path through `RG_NGB_DATA_ROOT` when needed. + +## Install in the active conda environment + +From the repository root: + +```bash +cd /tmp/rg_optimizers/baseline/ngb +python -m pip install -e ../nanogpt_one_head +export PYTORCH_ENABLE_MPS_FALLBACK=1 +export RG_NGB_ROOT=/tmp/rg-ngb +export RG_NGB_DATA_ROOT=/tmp/rg-nanogpt-one-head/data +``` + +Prepare or verify the shared corpus: + +```bash +python -m rg_nanogpt_one_head.data \ + --config configs/v4_one_head.yaml \ + --output-dir /tmp/rg-nanogpt-one-head/data +``` + +## Run v4 one-head + +```bash +python -m rg_nanogpt_one_head.training \ + --config configs/v4_one_head.yaml \ + --optimizer all \ + --data-root /tmp/rg-nanogpt-one-head/data \ + --results-root /tmp/rg-ngb/results/v4_one_head \ + --device auto +``` + +## Run the small 4×4 model + +```bash +python -m rg_nanogpt_one_head.training \ + --config configs/v4_small_4x4.yaml \ + --optimizer all \ + --data-root /tmp/rg-nanogpt-one-head/data \ + --results-root /tmp/rg-ngb/results/v4_small_4x4 \ + --device auto +``` + +## Eight matched seeds + +After the canonical three, add five more matched seeds with: + +```bash +python -m rg_nanogpt_one_head.training \ + --config configs/v4_one_head.yaml \ + --optimizer all \ + --seeds 5003,6007,7013,8017,9011 \ + --data-root /tmp/rg-nanogpt-one-head/data \ + --results-root /tmp/rg-ngb/results/v4_one_head \ + --device auto +``` + +Use the same seed list and corresponding result root for `v4_small_4x4.yaml`. + +## Notebooks + +```text +notebooks/01_run_v4_one_head.ipynb +notebooks/02_compare_v4_one_head.ipynb +notebooks/03_run_v4_small_4x4.ipynb +notebooks/04_compare_v4_small_4x4.ipynb +notebooks/05_compare_v4_architectures.ipynb +notebooks/06_compare_v3_v4_one_head.ipynb +``` + +The comparison notebooks discover the intersection of completed seeds across +all three optimizers. They therefore use three, eight, or any later matched +seed count without editing the notebook. + +They report: + +- final and validation-selected metrics with run-level 95% Student-t intervals; +- perplexity intervals obtained by exponentiating the loss-space interval; +- matched-seed paired optimizer contrasts; +- best validation step and final-minus-best validation-loss drift; +- maximum update-to-weight ratio and evaluation-snapshot clipping rate; +- optimizer configuration tables; +- optimizer-level and block-resolved WeightWatcher trajectories. + +Block-resolved plots compute uncertainty across seeds for each matrix. Blocks +are never treated as additional statistical replicates. + +Run all NGB notebooks with the active conda kernel: + +```bash +cd /tmp/rg_optimizers/baseline/ngb + +for nb in \ + 01_run_v4_one_head \ + 02_compare_v4_one_head \ + 03_run_v4_small_4x4 \ + 04_compare_v4_small_4x4 \ + 05_compare_v4_architectures \ + 06_compare_v3_v4_one_head +do + papermill \ + "notebooks/${nb}.ipynb" \ + "notebooks/${nb}.out.ipynb" \ + -k python3 +done +``` + +The architecture-comparison notebook must run after both three-optimizer suites +are complete. + +## Validation + +```bash +PYTHONPATH=../nanogpt_one_head/src \ + python -m pytest -q tests +``` diff --git a/baseline/ngb/configs/v4_one_head.yaml b/baseline/ngb/configs/v4_one_head.yaml new file mode 100644 index 0000000..3ac9d27 --- /dev/null +++ b/baseline/ngb/configs/v4_one_head.yaml @@ -0,0 +1,105 @@ +protocol: + name: ngb_v4_one_head_fineweb + version: 4 + storage_root: /tmp/rg-ngb + data_root: /tmp/rg-nanogpt-one-head/data + run_slug: v4_one_head + description: Tuned two-epoch one-block, one-head nanoGPT baselines on pinned FineWeb-Edu. + +dataset: + name: HuggingFaceFW/fineweb-edu + config: sample-10BT + split: train + revision: 593b3a867298afb8ce42625a270ef20ddcad28f9 + tokenizer: gpt2 + train_tokens: 80000000 + val_tokens: 1000000 + test_tokens: 1000000 + +model: + vocab_size: 50257 + block_size: 256 + n_layer: 1 + n_head: 1 + n_embd: 128 + dropout: 0.0 + bias: false + tie_weights: true + +training: + seeds: [1337, 2027, 4099] + batch_size: 4 + grad_accum_steps: 8 + target_epochs: 2.0 + epoch_interval: 0.25 + eval_interval_steps: 500 + eval_batches: 64 + checkpoint_interval_steps: 500 + grad_clip: 1.0 + +optimizer_profiles: + sgd_momentum: + display_name: SGD + Nesterov momentum, v4 tuned + family: sgd + learning_rate: 0.05 + min_learning_rate: 0.0005 + warmup_fraction: 0.05 + schedule: warmup_cosine + momentum: 0.90 + dampening: 0.0 + nesterov: true + weight_decay: 0.01 + + adamw: + display_name: AdamW, v4 tuned + family: adamw + learning_rate: 0.0003 + min_learning_rate: 0.00001 + warmup_fraction: 0.025 + schedule: warmup_cosine + beta1: 0.90 + beta2: 0.95 + epsilon: 1.0e-8 + weight_decay: 0.10 + + muon: + display_name: Muon + auxiliary AdamW, v4 tuned + family: muon + matrix_learning_rate: 0.01 + matrix_min_learning_rate: 0.0001 + aux_learning_rate: 0.0002 + aux_min_learning_rate: 0.00001 + warmup_fraction: 0.025 + schedule: warmup_cosine + momentum: 0.95 + nesterov: true + newton_schulz_steps: 5 + muon_epsilon: 1.0e-7 + matrix_weight_decay: 0.02 + beta1: 0.90 + beta2: 0.95 + epsilon: 1.0e-8 + aux_weight_decay: 0.10 + +evaluation: + train_probe_seed: 21001 + validation_probe_seed: 22001 + test_probe_seed: 23001 + bleu_probe_seed: 24001 + bleu_examples: 64 + bleu_prompt_tokens: 64 + bleu_continuation_tokens: 32 + bleu_batch_size: 4 + +weightwatcher: + enabled: true + ERG: true + randomize: true + strict: true + min_evals: 20 + +runtime: + matmul_precision: high + mps_fallback: true + deterministic_algorithms: false + empty_mps_cache_after_weightwatcher: true diff --git a/baseline/ngb/configs/v4_small_4x4.yaml b/baseline/ngb/configs/v4_small_4x4.yaml new file mode 100644 index 0000000..b192149 --- /dev/null +++ b/baseline/ngb/configs/v4_small_4x4.yaml @@ -0,0 +1,105 @@ +protocol: + name: ngb_v4_small_4x4_fineweb + version: 4 + storage_root: /tmp/rg-ngb + data_root: /tmp/rg-nanogpt-one-head/data + run_slug: v4_small_4x4 + description: Tuned two-epoch four-block, four-head small nanoGPT baselines on pinned FineWeb-Edu. + +dataset: + name: HuggingFaceFW/fineweb-edu + config: sample-10BT + split: train + revision: 593b3a867298afb8ce42625a270ef20ddcad28f9 + tokenizer: gpt2 + train_tokens: 80000000 + val_tokens: 1000000 + test_tokens: 1000000 + +model: + vocab_size: 50257 + block_size: 256 + n_layer: 4 + n_head: 4 + n_embd: 128 + dropout: 0.0 + bias: false + tie_weights: true + +training: + seeds: [1337, 2027, 4099] + batch_size: 4 + grad_accum_steps: 8 + target_epochs: 2.0 + epoch_interval: 0.25 + eval_interval_steps: 500 + eval_batches: 64 + checkpoint_interval_steps: 500 + grad_clip: 1.0 + +optimizer_profiles: + sgd_momentum: + display_name: SGD + Nesterov momentum, v4 tuned + family: sgd + learning_rate: 0.03 + min_learning_rate: 0.0003 + warmup_fraction: 0.05 + schedule: warmup_cosine + momentum: 0.90 + dampening: 0.0 + nesterov: true + weight_decay: 0.01 + + adamw: + display_name: AdamW, v4 tuned + family: adamw + learning_rate: 0.0003 + min_learning_rate: 0.00001 + warmup_fraction: 0.025 + schedule: warmup_cosine + beta1: 0.90 + beta2: 0.95 + epsilon: 1.0e-8 + weight_decay: 0.10 + + muon: + display_name: Muon + auxiliary AdamW, v4 tuned + family: muon + matrix_learning_rate: 0.01 + matrix_min_learning_rate: 0.0001 + aux_learning_rate: 0.0002 + aux_min_learning_rate: 0.00001 + warmup_fraction: 0.025 + schedule: warmup_cosine + momentum: 0.95 + nesterov: true + newton_schulz_steps: 5 + muon_epsilon: 1.0e-7 + matrix_weight_decay: 0.02 + beta1: 0.90 + beta2: 0.95 + epsilon: 1.0e-8 + aux_weight_decay: 0.10 + +evaluation: + train_probe_seed: 21001 + validation_probe_seed: 22001 + test_probe_seed: 23001 + bleu_probe_seed: 24001 + bleu_examples: 64 + bleu_prompt_tokens: 64 + bleu_continuation_tokens: 32 + bleu_batch_size: 4 + +weightwatcher: + enabled: true + ERG: true + randomize: true + strict: true + min_evals: 20 + +runtime: + matmul_precision: high + mps_fallback: true + deterministic_algorithms: false + empty_mps_cache_after_weightwatcher: true diff --git a/baseline/ngb/notebooks/01_run_v4_one_head.ipynb b/baseline/ngb/notebooks/01_run_v4_one_head.ipynb new file mode 100644 index 0000000..70b4930 --- /dev/null +++ b/baseline/ngb/notebooks/01_run_v4_one_head.ipynb @@ -0,0 +1,144 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "bcb1ae85", + "metadata": {}, + "source": [ + "# NGB v4 one-head tuned two-epoch baselines\n", + "\n", + "Runs the separate v4 one-block/one-head SGD, AdamW, and Muon protocols.\n", + "\n", + "The test split is monitoring-only. Validation cross-entropy selects the best\n", + "checkpoint. Runtime data and results remain under explicit `/tmp` paths.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70af919e", + "metadata": { + "tags": [ + "parameters" + ] + }, + "outputs": [], + "source": [ + "CONFIG_PATH = \"configs/v4_one_head.yaml\"\n", + "OPTIMIZER = \"all\"\n", + "SEEDS = \"\"\n", + "DEVICE = \"auto\"\n", + "DATA_ROOT = \"/tmp/rg-nanogpt-one-head/data\"\n", + "NGB_STORAGE_ROOT = \"/tmp/rg-ngb\"\n", + "FORCE_DATA = False\n", + "OVERWRITE = False\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a4dfe4a1", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import os\n", + "import sys\n", + "import pandas as pd\n", + "from IPython.display import display\n", + "\n", + "cwd = Path.cwd().resolve()\n", + "candidates = [cwd, cwd.parent, cwd / \"baseline\" / \"ngb\"]\n", + "NGB_ROOT_DIR = next(\n", + " (path for path in candidates if (path / \"configs\" / \"v4_one_head.yaml\").is_file()),\n", + " None,\n", + ")\n", + "if NGB_ROOT_DIR is None:\n", + " raise FileNotFoundError(\"Run from baseline/ngb or the repository root\")\n", + "RUNTIME_SRC = NGB_ROOT_DIR.parent / \"nanogpt_one_head\" / \"src\"\n", + "if str(RUNTIME_SRC) not in sys.path:\n", + " sys.path.insert(0, str(RUNTIME_SRC))\n", + "\n", + "from rg_nanogpt_one_head import (\n", + " SUPPORTED_OPTIMIZERS,\n", + " canonical_seeds,\n", + " load_config,\n", + " prepare_fineweb_edu,\n", + " run_all_replicates,\n", + " run_optimizer_replicates,\n", + " run_slug,\n", + " run_status_table,\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7f324a98", + "metadata": {}, + "outputs": [], + "source": [ + "config_path = (NGB_ROOT_DIR / CONFIG_PATH).resolve()\n", + "cfg = load_config(config_path)\n", + "selected_seeds = (\n", + " tuple(int(value.strip()) for value in SEEDS.split(\",\") if value.strip())\n", + " if SEEDS.strip()\n", + " else canonical_seeds(cfg)\n", + ")\n", + "if not selected_seeds or len(set(selected_seeds)) != len(selected_seeds):\n", + " raise ValueError(\"SEEDS must contain unique integers\")\n", + "if OPTIMIZER != \"all\" and OPTIMIZER not in SUPPORTED_OPTIMIZERS:\n", + " raise ValueError(f\"OPTIMIZER must be all or one of {SUPPORTED_OPTIMIZERS}\")\n", + "\n", + "data_root = Path(DATA_ROOT)\n", + "results_root = Path(NGB_STORAGE_ROOT) / \"results\" / run_slug(cfg)\n", + "plots_root = Path(NGB_STORAGE_ROOT) / \"plots\" / run_slug(cfg)\n", + "for directory in (data_root, results_root, plots_root):\n", + " directory.mkdir(parents=True, exist_ok=True)\n", + "\n", + "print(\"config:\", config_path)\n", + "print(\"data:\", data_root)\n", + "print(\"results:\", results_root)\n", + "print(\"seeds:\", selected_seeds)\n", + "display(pd.DataFrame([cfg[\"model\"]]))\n", + "display(pd.DataFrame.from_dict(cfg[\"optimizer_profiles\"], orient=\"index\"))\n", + "\n", + "prepare_fineweb_edu(cfg, data_root, force=FORCE_DATA)\n", + "common = dict(\n", + " cfg=cfg,\n", + " config_path=config_path,\n", + " seeds=selected_seeds,\n", + " data_root=data_root,\n", + " results_root=results_root,\n", + " device=DEVICE,\n", + " resume=not OVERWRITE,\n", + " overwrite=OVERWRITE,\n", + " progress=True,\n", + ")\n", + "if OPTIMIZER == \"all\":\n", + " run_dirs = run_all_replicates(**common)\n", + "else:\n", + " run_dirs = run_optimizer_replicates(\n", + " optimizer_name=OPTIMIZER,\n", + " prepare_data=False,\n", + " **common,\n", + " )\n", + "print(\"run directories:\", len(run_dirs))\n", + "display(run_status_table(results_root, optimizers=SUPPORTED_OPTIMIZERS, seeds=selected_seeds))\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (active conda environment)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/baseline/ngb/notebooks/02_compare_v4_one_head.ipynb b/baseline/ngb/notebooks/02_compare_v4_one_head.ipynb new file mode 100644 index 0000000..4df0e18 --- /dev/null +++ b/baseline/ngb/notebooks/02_compare_v4_one_head.ipynb @@ -0,0 +1,244 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c74edd06", + "metadata": {}, + "source": [ + "# NGB v4 one-head comparison\n", + "\n", + "This notebook discovers the intersection of **completed matched seeds** across\n", + "SGD, AdamW, and Muon. It reports final and validation-selected metrics, paired\n", + "optimizer contrasts, late-horizon drift, corrected perplexity intervals, and\n", + "block-resolved WeightWatcher trajectories. Blocks are never statistical\n", + "replicates.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5f10d3d", + "metadata": { + "tags": [ + "parameters" + ] + }, + "outputs": [], + "source": [ + "CONFIG_PATH = \"configs/v4_one_head.yaml\"\n", + "SEEDS = \"\"\n", + "NGB_STORAGE_ROOT = \"/tmp/rg-ngb\"\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4ebbf2b6", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import os\n", + "import sys\n", + "import pandas as pd\n", + "from IPython.display import display\n", + "\n", + "cwd = Path.cwd().resolve()\n", + "candidates = [cwd, cwd.parent, cwd / \"baseline\" / \"ngb\"]\n", + "NGB_ROOT_DIR = next(\n", + " (path for path in candidates if (path / \"configs\" / \"v4_one_head.yaml\").is_file()),\n", + " None,\n", + ")\n", + "if NGB_ROOT_DIR is None:\n", + " raise FileNotFoundError(\"Run from baseline/ngb or the repository root\")\n", + "RUNTIME_SRC = NGB_ROOT_DIR.parent / \"nanogpt_one_head\" / \"src\"\n", + "if str(RUNTIME_SRC) not in sys.path:\n", + " sys.path.insert(0, str(RUNTIME_SRC))\n", + "\n", + "import json\n", + "import math\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "from rg_nanogpt_one_head import (\n", + " OPTIMIZER_COLORS,\n", + " OPTIMIZER_LABELS,\n", + " SUPPORTED_OPTIMIZERS,\n", + " discover_matched_complete_seeds,\n", + " final_test_summary,\n", + " load_config,\n", + " load_epoch_metrics,\n", + " load_layer_metrics,\n", + " load_metrics,\n", + " load_spectral_summary,\n", + " load_test_results,\n", + " paired_test_differences,\n", + " plot_epoch_metric,\n", + " plot_layer_metric,\n", + " plot_spectral_optimizer_summary,\n", + " run_diagnostics_table,\n", + " run_slug,\n", + " run_status_table,\n", + " summarize_run_diagnostics,\n", + ")\n", + "\n", + "pd.set_option(\"display.max_rows\", None)\n", + "pd.set_option(\"display.max_columns\", None)\n", + "pd.set_option(\"display.width\", None)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fff10377", + "metadata": {}, + "outputs": [], + "source": [ + "config_path = (NGB_ROOT_DIR / CONFIG_PATH).resolve()\n", + "cfg = load_config(config_path)\n", + "results_root = Path(NGB_STORAGE_ROOT) / \"results\" / run_slug(cfg)\n", + "plot_root = Path(NGB_STORAGE_ROOT) / \"plots\" / run_slug(cfg) / \"comparison\"\n", + "plot_root.mkdir(parents=True, exist_ok=True)\n", + "optimizers = tuple(SUPPORTED_OPTIMIZERS)\n", + "seeds = (\n", + " tuple(int(value.strip()) for value in SEEDS.split(\",\") if value.strip())\n", + " if SEEDS.strip()\n", + " else discover_matched_complete_seeds(results_root, optimizers=optimizers)\n", + ")\n", + "if not seeds:\n", + " raise RuntimeError(f\"No complete matched optimizer seeds under {results_root}\")\n", + "print(\"config:\", config_path)\n", + "print(\"results:\", results_root)\n", + "print(\"matched complete seeds:\", seeds)\n", + "display(run_status_table(results_root, optimizers=optimizers, seeds=seeds))\n", + "\n", + "config_rows = []\n", + "for optimizer in optimizers:\n", + " config_rows.append({\n", + " \"optimizer\": optimizer,\n", + " \"optimizer_label\": OPTIMIZER_LABELS[optimizer],\n", + " **cfg[\"optimizer_profiles\"][optimizer],\n", + " })\n", + "config_table = pd.DataFrame(config_rows)\n", + "display(pd.DataFrame([{\"run_slug\": run_slug(cfg), **cfg[\"model\"], **cfg[\"training\"]}]))\n", + "display(config_table)\n", + "config_table.to_csv(plot_root / \"optimizer_configurations.csv\", index=False)\n", + "\n", + "metrics = load_metrics(results_root, optimizers=optimizers, seeds=seeds)\n", + "epoch_metrics = load_epoch_metrics(results_root, optimizers=optimizers, seeds=seeds)\n", + "layer_metrics = load_layer_metrics(results_root, optimizers=optimizers, seeds=seeds)\n", + "spectral_summary = load_spectral_summary(results_root, optimizers=optimizers, seeds=seeds)\n", + "test_results = load_test_results(results_root, optimizers=optimizers, seeds=seeds)\n", + "print(\"rows:\", {\"metrics\": len(metrics), \"epoch\": len(epoch_metrics), \"layers\": len(layer_metrics)})\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "141d8115", + "metadata": {}, + "outputs": [], + "source": [ + "diagnostics = run_diagnostics_table(metrics, test_results)\n", + "diagnostic_summary = summarize_run_diagnostics(diagnostics)\n", + "summary = final_test_summary(test_results)\n", + "paired = paired_test_differences(test_results, optimizers=optimizers)\n", + "\n", + "for frame, name in (\n", + " (diagnostics, \"run_diagnostics.csv\"),\n", + " (diagnostic_summary, \"run_diagnostics_summary_95ci.csv\"),\n", + " (summary, \"final_and_validation_selected_95ci.csv\"),\n", + " (paired, \"paired_optimizer_differences_95ci.csv\"),\n", + "):\n", + " frame.to_csv(plot_root / name, index=False)\n", + "\n", + "display(diagnostics.sort_values([\"optimizer\", \"seed\"]))\n", + "display(diagnostic_summary.sort_values([\"metric\", \"optimizer\"]))\n", + "display(summary.sort_values([\"checkpoint\", \"metric\", \"optimizer\"]))\n", + "display(paired.sort_values([\"checkpoint\", \"metric\", \"contrast\"]))\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9dbb7edc", + "metadata": {}, + "outputs": [], + "source": [ + "for metric in (\n", + " \"train_loss\", \"val_loss\", \"test_loss\",\n", + " \"train_accuracy\", \"val_accuracy\", \"test_accuracy\",\n", + " \"val_generalization_gap\", \"test_generalization_gap\",\n", + " \"weight_norm\", \"update_to_weight_ratio\",\n", + "):\n", + " if metric not in epoch_metrics.columns:\n", + " continue\n", + " plot_epoch_metric(\n", + " epoch_metrics,\n", + " metric=metric,\n", + " optimizers=optimizers,\n", + " title=f\"{run_slug(cfg)}: {metric} (95% Student-t CI)\",\n", + " output=plot_root / f\"{metric}.png\",\n", + " )\n", + " plt.show()\n", + "\n", + "# Late-horizon zooms make drift after the first half epoch explicit.\n", + "late = epoch_metrics[epoch_metrics[\"nominal_epoch\"].astype(float) >= 0.5]\n", + "for metric in (\"val_loss\", \"test_loss\", \"val_accuracy\", \"test_accuracy\"):\n", + " plot_epoch_metric(\n", + " late,\n", + " metric=metric,\n", + " optimizers=optimizers,\n", + " title=f\"{run_slug(cfg)}: {metric}, epoch >= 0.5\",\n", + " output=plot_root / f\"zoom_epoch_0p5_{metric}.png\",\n", + " )\n", + " plt.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d97a0e5a", + "metadata": {}, + "outputs": [], + "source": [ + "for metric in (\"alpha_median\", \"ERG_gap_median\", \"num_traps_mean\"):\n", + " plot_spectral_optimizer_summary(\n", + " spectral_summary,\n", + " metric=metric,\n", + " optimizers=optimizers,\n", + " output=plot_root / f\"spectral_{metric}.png\",\n", + " )\n", + " if metric == \"alpha_median\":\n", + " plt.axhline(2.0, color=\"black\", linestyle=\"--\", linewidth=1.0)\n", + " if metric == \"ERG_gap_median\":\n", + " plt.axhline(0.0, color=\"black\", linestyle=\"--\", linewidth=1.0)\n", + " plt.show()\n", + "\n", + "for optimizer in optimizers:\n", + " for metric in (\"alpha\", \"ERG_gap\", \"num_traps\"):\n", + " plot_layer_metric(\n", + " layer_metrics,\n", + " optimizer=optimizer,\n", + " metric=metric,\n", + " title=f\"{run_slug(cfg)} {OPTIMIZER_LABELS[optimizer]}: block-resolved {metric}\",\n", + " output=plot_root / f\"{optimizer}_block_resolved_{metric}.png\",\n", + " )\n", + " plt.show()\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (active conda environment)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/baseline/ngb/notebooks/03_run_v4_small_4x4.ipynb b/baseline/ngb/notebooks/03_run_v4_small_4x4.ipynb new file mode 100644 index 0000000..2f16653 --- /dev/null +++ b/baseline/ngb/notebooks/03_run_v4_small_4x4.ipynb @@ -0,0 +1,144 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e0575451", + "metadata": {}, + "source": [ + "# NGB v4 small 4×4 tuned two-epoch baselines\n", + "\n", + "Runs the distinct four-block/four-head small language-model architecture.\n", + "\n", + "The test split is monitoring-only. Validation cross-entropy selects the best\n", + "checkpoint. Runtime data and results remain under explicit `/tmp` paths.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e10856af", + "metadata": { + "tags": [ + "parameters" + ] + }, + "outputs": [], + "source": [ + "CONFIG_PATH = \"configs/v4_small_4x4.yaml\"\n", + "OPTIMIZER = \"all\"\n", + "SEEDS = \"\"\n", + "DEVICE = \"auto\"\n", + "DATA_ROOT = \"/tmp/rg-nanogpt-one-head/data\"\n", + "NGB_STORAGE_ROOT = \"/tmp/rg-ngb\"\n", + "FORCE_DATA = False\n", + "OVERWRITE = False\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c31be70", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import os\n", + "import sys\n", + "import pandas as pd\n", + "from IPython.display import display\n", + "\n", + "cwd = Path.cwd().resolve()\n", + "candidates = [cwd, cwd.parent, cwd / \"baseline\" / \"ngb\"]\n", + "NGB_ROOT_DIR = next(\n", + " (path for path in candidates if (path / \"configs\" / \"v4_one_head.yaml\").is_file()),\n", + " None,\n", + ")\n", + "if NGB_ROOT_DIR is None:\n", + " raise FileNotFoundError(\"Run from baseline/ngb or the repository root\")\n", + "RUNTIME_SRC = NGB_ROOT_DIR.parent / \"nanogpt_one_head\" / \"src\"\n", + "if str(RUNTIME_SRC) not in sys.path:\n", + " sys.path.insert(0, str(RUNTIME_SRC))\n", + "\n", + "from rg_nanogpt_one_head import (\n", + " SUPPORTED_OPTIMIZERS,\n", + " canonical_seeds,\n", + " load_config,\n", + " prepare_fineweb_edu,\n", + " run_all_replicates,\n", + " run_optimizer_replicates,\n", + " run_slug,\n", + " run_status_table,\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17ac523b", + "metadata": {}, + "outputs": [], + "source": [ + "config_path = (NGB_ROOT_DIR / CONFIG_PATH).resolve()\n", + "cfg = load_config(config_path)\n", + "selected_seeds = (\n", + " tuple(int(value.strip()) for value in SEEDS.split(\",\") if value.strip())\n", + " if SEEDS.strip()\n", + " else canonical_seeds(cfg)\n", + ")\n", + "if not selected_seeds or len(set(selected_seeds)) != len(selected_seeds):\n", + " raise ValueError(\"SEEDS must contain unique integers\")\n", + "if OPTIMIZER != \"all\" and OPTIMIZER not in SUPPORTED_OPTIMIZERS:\n", + " raise ValueError(f\"OPTIMIZER must be all or one of {SUPPORTED_OPTIMIZERS}\")\n", + "\n", + "data_root = Path(DATA_ROOT)\n", + "results_root = Path(NGB_STORAGE_ROOT) / \"results\" / run_slug(cfg)\n", + "plots_root = Path(NGB_STORAGE_ROOT) / \"plots\" / run_slug(cfg)\n", + "for directory in (data_root, results_root, plots_root):\n", + " directory.mkdir(parents=True, exist_ok=True)\n", + "\n", + "print(\"config:\", config_path)\n", + "print(\"data:\", data_root)\n", + "print(\"results:\", results_root)\n", + "print(\"seeds:\", selected_seeds)\n", + "display(pd.DataFrame([cfg[\"model\"]]))\n", + "display(pd.DataFrame.from_dict(cfg[\"optimizer_profiles\"], orient=\"index\"))\n", + "\n", + "prepare_fineweb_edu(cfg, data_root, force=FORCE_DATA)\n", + "common = dict(\n", + " cfg=cfg,\n", + " config_path=config_path,\n", + " seeds=selected_seeds,\n", + " data_root=data_root,\n", + " results_root=results_root,\n", + " device=DEVICE,\n", + " resume=not OVERWRITE,\n", + " overwrite=OVERWRITE,\n", + " progress=True,\n", + ")\n", + "if OPTIMIZER == \"all\":\n", + " run_dirs = run_all_replicates(**common)\n", + "else:\n", + " run_dirs = run_optimizer_replicates(\n", + " optimizer_name=OPTIMIZER,\n", + " prepare_data=False,\n", + " **common,\n", + " )\n", + "print(\"run directories:\", len(run_dirs))\n", + "display(run_status_table(results_root, optimizers=SUPPORTED_OPTIMIZERS, seeds=selected_seeds))\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (active conda environment)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/baseline/ngb/notebooks/04_compare_v4_small_4x4.ipynb b/baseline/ngb/notebooks/04_compare_v4_small_4x4.ipynb new file mode 100644 index 0000000..5c9cb9e --- /dev/null +++ b/baseline/ngb/notebooks/04_compare_v4_small_4x4.ipynb @@ -0,0 +1,244 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "05c6b327", + "metadata": {}, + "source": [ + "# NGB v4 small 4×4 comparison\n", + "\n", + "This notebook discovers the intersection of **completed matched seeds** across\n", + "SGD, AdamW, and Muon. It reports final and validation-selected metrics, paired\n", + "optimizer contrasts, late-horizon drift, corrected perplexity intervals, and\n", + "block-resolved WeightWatcher trajectories. Blocks are never statistical\n", + "replicates.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6b874a5c", + "metadata": { + "tags": [ + "parameters" + ] + }, + "outputs": [], + "source": [ + "CONFIG_PATH = \"configs/v4_small_4x4.yaml\"\n", + "SEEDS = \"\"\n", + "NGB_STORAGE_ROOT = \"/tmp/rg-ngb\"\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c7f868dd", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import os\n", + "import sys\n", + "import pandas as pd\n", + "from IPython.display import display\n", + "\n", + "cwd = Path.cwd().resolve()\n", + "candidates = [cwd, cwd.parent, cwd / \"baseline\" / \"ngb\"]\n", + "NGB_ROOT_DIR = next(\n", + " (path for path in candidates if (path / \"configs\" / \"v4_one_head.yaml\").is_file()),\n", + " None,\n", + ")\n", + "if NGB_ROOT_DIR is None:\n", + " raise FileNotFoundError(\"Run from baseline/ngb or the repository root\")\n", + "RUNTIME_SRC = NGB_ROOT_DIR.parent / \"nanogpt_one_head\" / \"src\"\n", + "if str(RUNTIME_SRC) not in sys.path:\n", + " sys.path.insert(0, str(RUNTIME_SRC))\n", + "\n", + "import json\n", + "import math\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "from rg_nanogpt_one_head import (\n", + " OPTIMIZER_COLORS,\n", + " OPTIMIZER_LABELS,\n", + " SUPPORTED_OPTIMIZERS,\n", + " discover_matched_complete_seeds,\n", + " final_test_summary,\n", + " load_config,\n", + " load_epoch_metrics,\n", + " load_layer_metrics,\n", + " load_metrics,\n", + " load_spectral_summary,\n", + " load_test_results,\n", + " paired_test_differences,\n", + " plot_epoch_metric,\n", + " plot_layer_metric,\n", + " plot_spectral_optimizer_summary,\n", + " run_diagnostics_table,\n", + " run_slug,\n", + " run_status_table,\n", + " summarize_run_diagnostics,\n", + ")\n", + "\n", + "pd.set_option(\"display.max_rows\", None)\n", + "pd.set_option(\"display.max_columns\", None)\n", + "pd.set_option(\"display.width\", None)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dfa2cb61", + "metadata": {}, + "outputs": [], + "source": [ + "config_path = (NGB_ROOT_DIR / CONFIG_PATH).resolve()\n", + "cfg = load_config(config_path)\n", + "results_root = Path(NGB_STORAGE_ROOT) / \"results\" / run_slug(cfg)\n", + "plot_root = Path(NGB_STORAGE_ROOT) / \"plots\" / run_slug(cfg) / \"comparison\"\n", + "plot_root.mkdir(parents=True, exist_ok=True)\n", + "optimizers = tuple(SUPPORTED_OPTIMIZERS)\n", + "seeds = (\n", + " tuple(int(value.strip()) for value in SEEDS.split(\",\") if value.strip())\n", + " if SEEDS.strip()\n", + " else discover_matched_complete_seeds(results_root, optimizers=optimizers)\n", + ")\n", + "if not seeds:\n", + " raise RuntimeError(f\"No complete matched optimizer seeds under {results_root}\")\n", + "print(\"config:\", config_path)\n", + "print(\"results:\", results_root)\n", + "print(\"matched complete seeds:\", seeds)\n", + "display(run_status_table(results_root, optimizers=optimizers, seeds=seeds))\n", + "\n", + "config_rows = []\n", + "for optimizer in optimizers:\n", + " config_rows.append({\n", + " \"optimizer\": optimizer,\n", + " \"optimizer_label\": OPTIMIZER_LABELS[optimizer],\n", + " **cfg[\"optimizer_profiles\"][optimizer],\n", + " })\n", + "config_table = pd.DataFrame(config_rows)\n", + "display(pd.DataFrame([{\"run_slug\": run_slug(cfg), **cfg[\"model\"], **cfg[\"training\"]}]))\n", + "display(config_table)\n", + "config_table.to_csv(plot_root / \"optimizer_configurations.csv\", index=False)\n", + "\n", + "metrics = load_metrics(results_root, optimizers=optimizers, seeds=seeds)\n", + "epoch_metrics = load_epoch_metrics(results_root, optimizers=optimizers, seeds=seeds)\n", + "layer_metrics = load_layer_metrics(results_root, optimizers=optimizers, seeds=seeds)\n", + "spectral_summary = load_spectral_summary(results_root, optimizers=optimizers, seeds=seeds)\n", + "test_results = load_test_results(results_root, optimizers=optimizers, seeds=seeds)\n", + "print(\"rows:\", {\"metrics\": len(metrics), \"epoch\": len(epoch_metrics), \"layers\": len(layer_metrics)})\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "06019c57", + "metadata": {}, + "outputs": [], + "source": [ + "diagnostics = run_diagnostics_table(metrics, test_results)\n", + "diagnostic_summary = summarize_run_diagnostics(diagnostics)\n", + "summary = final_test_summary(test_results)\n", + "paired = paired_test_differences(test_results, optimizers=optimizers)\n", + "\n", + "for frame, name in (\n", + " (diagnostics, \"run_diagnostics.csv\"),\n", + " (diagnostic_summary, \"run_diagnostics_summary_95ci.csv\"),\n", + " (summary, \"final_and_validation_selected_95ci.csv\"),\n", + " (paired, \"paired_optimizer_differences_95ci.csv\"),\n", + "):\n", + " frame.to_csv(plot_root / name, index=False)\n", + "\n", + "display(diagnostics.sort_values([\"optimizer\", \"seed\"]))\n", + "display(diagnostic_summary.sort_values([\"metric\", \"optimizer\"]))\n", + "display(summary.sort_values([\"checkpoint\", \"metric\", \"optimizer\"]))\n", + "display(paired.sort_values([\"checkpoint\", \"metric\", \"contrast\"]))\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ac067bb4", + "metadata": {}, + "outputs": [], + "source": [ + "for metric in (\n", + " \"train_loss\", \"val_loss\", \"test_loss\",\n", + " \"train_accuracy\", \"val_accuracy\", \"test_accuracy\",\n", + " \"val_generalization_gap\", \"test_generalization_gap\",\n", + " \"weight_norm\", \"update_to_weight_ratio\",\n", + "):\n", + " if metric not in epoch_metrics.columns:\n", + " continue\n", + " plot_epoch_metric(\n", + " epoch_metrics,\n", + " metric=metric,\n", + " optimizers=optimizers,\n", + " title=f\"{run_slug(cfg)}: {metric} (95% Student-t CI)\",\n", + " output=plot_root / f\"{metric}.png\",\n", + " )\n", + " plt.show()\n", + "\n", + "# Late-horizon zooms make drift after the first half epoch explicit.\n", + "late = epoch_metrics[epoch_metrics[\"nominal_epoch\"].astype(float) >= 0.5]\n", + "for metric in (\"val_loss\", \"test_loss\", \"val_accuracy\", \"test_accuracy\"):\n", + " plot_epoch_metric(\n", + " late,\n", + " metric=metric,\n", + " optimizers=optimizers,\n", + " title=f\"{run_slug(cfg)}: {metric}, epoch >= 0.5\",\n", + " output=plot_root / f\"zoom_epoch_0p5_{metric}.png\",\n", + " )\n", + " plt.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c6fb1b8c", + "metadata": {}, + "outputs": [], + "source": [ + "for metric in (\"alpha_median\", \"ERG_gap_median\", \"num_traps_mean\"):\n", + " plot_spectral_optimizer_summary(\n", + " spectral_summary,\n", + " metric=metric,\n", + " optimizers=optimizers,\n", + " output=plot_root / f\"spectral_{metric}.png\",\n", + " )\n", + " if metric == \"alpha_median\":\n", + " plt.axhline(2.0, color=\"black\", linestyle=\"--\", linewidth=1.0)\n", + " if metric == \"ERG_gap_median\":\n", + " plt.axhline(0.0, color=\"black\", linestyle=\"--\", linewidth=1.0)\n", + " plt.show()\n", + "\n", + "for optimizer in optimizers:\n", + " for metric in (\"alpha\", \"ERG_gap\", \"num_traps\"):\n", + " plot_layer_metric(\n", + " layer_metrics,\n", + " optimizer=optimizer,\n", + " metric=metric,\n", + " title=f\"{run_slug(cfg)} {OPTIMIZER_LABELS[optimizer]}: block-resolved {metric}\",\n", + " output=plot_root / f\"{optimizer}_block_resolved_{metric}.png\",\n", + " )\n", + " plt.show()\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (active conda environment)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/baseline/ngb/notebooks/05_compare_v4_architectures.ipynb b/baseline/ngb/notebooks/05_compare_v4_architectures.ipynb new file mode 100644 index 0000000..85c7441 --- /dev/null +++ b/baseline/ngb/notebooks/05_compare_v4_architectures.ipynb @@ -0,0 +1,235 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "91ee97ee", + "metadata": {}, + "source": [ + "# NGB v4 architecture comparison\n", + "\n", + "Compares the one-block/one-head control with the distinct four-block/four-head\n", + "small language model using the same matched seeds and optimizer recipes. The\n", + "paired tables use seed-matched architecture differences.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e835f666", + "metadata": { + "tags": [ + "parameters" + ] + }, + "outputs": [], + "source": [ + "ONE_HEAD_CONFIG = \"configs/v4_one_head.yaml\"\n", + "SMALL_4X4_CONFIG = \"configs/v4_small_4x4.yaml\"\n", + "SEEDS = \"\"\n", + "NGB_STORAGE_ROOT = \"/tmp/rg-ngb\"\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6261d540", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import os\n", + "import sys\n", + "import pandas as pd\n", + "from IPython.display import display\n", + "\n", + "cwd = Path.cwd().resolve()\n", + "candidates = [cwd, cwd.parent, cwd / \"baseline\" / \"ngb\"]\n", + "NGB_ROOT_DIR = next(\n", + " (path for path in candidates if (path / \"configs\" / \"v4_one_head.yaml\").is_file()),\n", + " None,\n", + ")\n", + "if NGB_ROOT_DIR is None:\n", + " raise FileNotFoundError(\"Run from baseline/ngb or the repository root\")\n", + "RUNTIME_SRC = NGB_ROOT_DIR.parent / \"nanogpt_one_head\" / \"src\"\n", + "if str(RUNTIME_SRC) not in sys.path:\n", + " sys.path.insert(0, str(RUNTIME_SRC))\n", + "\n", + "import math\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "from rg_nanogpt_one_head import (\n", + " OPTIMIZER_COLORS,\n", + " OPTIMIZER_LABELS,\n", + " SUPPORTED_OPTIMIZERS,\n", + " discover_matched_complete_seeds,\n", + " final_test_summary,\n", + " load_config,\n", + " load_epoch_metrics,\n", + " load_spectral_summary,\n", + " load_test_results,\n", + " mean_ci95,\n", + " run_slug,\n", + ")\n", + "from rg_nanogpt_one_head.analysis import summarize_by_epoch\n", + "pd.set_option(\"display.max_rows\", None)\n", + "pd.set_option(\"display.max_columns\", None)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6500b2a9", + "metadata": {}, + "outputs": [], + "source": [ + "configs = {\n", + " \"one_head\": load_config(NGB_ROOT_DIR / ONE_HEAD_CONFIG),\n", + " \"small_4x4\": load_config(NGB_ROOT_DIR / SMALL_4X4_CONFIG),\n", + "}\n", + "optimizers = tuple(SUPPORTED_OPTIMIZERS)\n", + "results_roots = {\n", + " architecture: Path(NGB_STORAGE_ROOT) / \"results\" / run_slug(cfg)\n", + " for architecture, cfg in configs.items()\n", + "}\n", + "available = {\n", + " architecture: set(discover_matched_complete_seeds(root, optimizers=optimizers))\n", + " for architecture, root in results_roots.items()\n", + "}\n", + "seeds = (\n", + " tuple(int(value.strip()) for value in SEEDS.split(\",\") if value.strip())\n", + " if SEEDS.strip()\n", + " else tuple(sorted(set.intersection(*available.values())))\n", + ")\n", + "if not seeds:\n", + " raise RuntimeError(f\"No complete seed intersection across architectures: {available}\")\n", + "print(\"matched architecture seeds:\", seeds)\n", + "print(\"result roots:\", results_roots)\n", + "\n", + "all_epoch = []\n", + "all_spectral = []\n", + "all_test = []\n", + "for architecture, root in results_roots.items():\n", + " epoch = load_epoch_metrics(root, optimizers=optimizers, seeds=seeds)\n", + " spectral = load_spectral_summary(root, optimizers=optimizers, seeds=seeds)\n", + " test = load_test_results(root, optimizers=optimizers, seeds=seeds)\n", + " for frame in (epoch, spectral, test):\n", + " frame.insert(0, \"architecture\", architecture)\n", + " all_epoch.append(epoch)\n", + " all_spectral.append(spectral)\n", + " all_test.append(test)\n", + "epoch_metrics = pd.concat(all_epoch, ignore_index=True)\n", + "spectral_summary = pd.concat(all_spectral, ignore_index=True)\n", + "test_results = pd.concat(all_test, ignore_index=True)\n", + "plot_root = Path(NGB_STORAGE_ROOT) / \"plots\" / \"v4_architecture_comparison\"\n", + "plot_root.mkdir(parents=True, exist_ok=True)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70b82ab1", + "metadata": {}, + "outputs": [], + "source": [ + "summary_frames = []\n", + "for architecture, frame in test_results.groupby(\"architecture\"):\n", + " part = final_test_summary(frame)\n", + " part.insert(0, \"architecture\", architecture)\n", + " summary_frames.append(part)\n", + "architecture_summary = pd.concat(summary_frames, ignore_index=True)\n", + "architecture_summary.to_csv(plot_root / \"architecture_summary_95ci.csv\", index=False)\n", + "display(architecture_summary.sort_values([\"checkpoint\", \"metric\", \"optimizer\", \"architecture\"]))\n", + "\n", + "rows = []\n", + "for optimizer in optimizers:\n", + " for checkpoint in (\"final\", \"validation_selected\"):\n", + " selected = test_results[\n", + " (test_results[\"optimizer\"] == optimizer)\n", + " & (test_results[\"checkpoint\"] == checkpoint)\n", + " ]\n", + " for metric in (\"test_loss\", \"test_accuracy\", \"test_bleu\"):\n", + " left = selected[selected[\"architecture\"] == \"small_4x4\"][[\"seed\", metric]].rename(columns={metric: \"small_4x4\"})\n", + " right = selected[selected[\"architecture\"] == \"one_head\"][[\"seed\", metric]].rename(columns={metric: \"one_head\"})\n", + " paired = left.merge(right, on=\"seed\", validate=\"one_to_one\")\n", + " stats = mean_ci95(paired[\"small_4x4\"] - paired[\"one_head\"])\n", + " rows.append({\n", + " \"optimizer\": optimizer,\n", + " \"optimizer_label\": OPTIMIZER_LABELS[optimizer],\n", + " \"checkpoint\": checkpoint,\n", + " \"metric\": metric,\n", + " \"contrast\": \"small_4x4 - one_head\",\n", + " **stats,\n", + " })\n", + "architecture_paired = pd.DataFrame(rows)\n", + "architecture_paired.to_csv(plot_root / \"paired_architecture_differences_95ci.csv\", index=False)\n", + "display(architecture_paired.sort_values([\"checkpoint\", \"metric\", \"optimizer\"]))\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5304e160", + "metadata": {}, + "outputs": [], + "source": [ + "styles = {\"one_head\": \":\", \"small_4x4\": \"-\"}\n", + "for optimizer in optimizers:\n", + " for metric in (\"val_loss\", \"val_accuracy\", \"test_loss\", \"test_accuracy\"):\n", + " figure, axis = plt.subplots(figsize=(9, 5))\n", + " for architecture in (\"one_head\", \"small_4x4\"):\n", + " subset = epoch_metrics[\n", + " (epoch_metrics[\"architecture\"] == architecture)\n", + " & (epoch_metrics[\"optimizer\"] == optimizer)\n", + " ]\n", + " summary = summarize_by_epoch(subset, metric, x=\"nominal_epoch\", group=(\"architecture\", \"optimizer\"))\n", + " axis.plot(\n", + " summary[\"nominal_epoch\"], summary[\"mean\"],\n", + " color=OPTIMIZER_COLORS[optimizer], linestyle=styles[architecture],\n", + " linewidth=2.2, label=architecture,\n", + " )\n", + " axis.fill_between(\n", + " summary[\"nominal_epoch\"], summary[\"ci95_lower\"], summary[\"ci95_upper\"],\n", + " color=OPTIMIZER_COLORS[optimizer], alpha=0.10,\n", + " )\n", + " axis.set(xlabel=\"Corpus-equivalent epoch\", ylabel=metric, title=f\"{OPTIMIZER_LABELS[optimizer]}: one-head vs small 4x4\")\n", + " axis.grid(alpha=0.25)\n", + " axis.legend(frameon=False)\n", + " figure.tight_layout()\n", + " figure.savefig(plot_root / f\"{optimizer}_{metric}.png\", dpi=170, bbox_inches=\"tight\")\n", + " plt.show()\n", + "\n", + "for optimizer in optimizers:\n", + " figure, axis = plt.subplots(figsize=(9, 5))\n", + " for architecture in (\"one_head\", \"small_4x4\"):\n", + " subset = spectral_summary[\n", + " (spectral_summary[\"architecture\"] == architecture)\n", + " & (spectral_summary[\"optimizer\"] == optimizer)\n", + " ]\n", + " summary = summarize_by_epoch(subset, \"alpha_median\", x=\"epoch\", group=(\"architecture\", \"optimizer\"))\n", + " axis.plot(summary[\"epoch\"], summary[\"mean\"], color=OPTIMIZER_COLORS[optimizer], linestyle=styles[architecture], linewidth=2.2, label=architecture)\n", + " axis.fill_between(summary[\"epoch\"], summary[\"ci95_lower\"], summary[\"ci95_upper\"], color=OPTIMIZER_COLORS[optimizer], alpha=0.10)\n", + " axis.axhline(2.0, color=\"black\", linestyle=\"--\", linewidth=1.0, label=\"alpha = 2\")\n", + " axis.set(xlabel=\"Corpus-equivalent epoch\", ylabel=\"Median alpha\", title=f\"{OPTIMIZER_LABELS[optimizer]}: architecture spectral comparison\")\n", + " axis.grid(alpha=0.25)\n", + " axis.legend(frameon=False)\n", + " figure.tight_layout()\n", + " figure.savefig(plot_root / f\"{optimizer}_alpha_median.png\", dpi=170, bbox_inches=\"tight\")\n", + " plt.show()\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (active conda environment)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/baseline/ngb/notebooks/06_compare_v3_v4_one_head.ipynb b/baseline/ngb/notebooks/06_compare_v3_v4_one_head.ipynb new file mode 100644 index 0000000..e131f46 --- /dev/null +++ b/baseline/ngb/notebooks/06_compare_v3_v4_one_head.ipynb @@ -0,0 +1,180 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d3823a8c", + "metadata": {}, + "source": [ + "# NGB v3 versus v4 one-head comparison\n", + "\n", + "This notebook compares the checked-in one-epoch v3 protocol with the separate\n", + "tuned two-epoch v4 protocol. Differences are paired by model seed. The result is\n", + "an overall protocol comparison—horizon and optimizer schedule both changed.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c2a20c2b", + "metadata": { + "tags": [ + "parameters" + ] + }, + "outputs": [], + "source": [ + "V3_RESULTS_ROOT = \"/tmp/rg-nanogpt-one-head/results\"\n", + "V4_CONFIG = \"configs/v4_one_head.yaml\"\n", + "NGB_STORAGE_ROOT = \"/tmp/rg-ngb\"\n", + "SEEDS = \"\"\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b45c00e", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import sys\n", + "import math\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "\n", + "cwd = Path.cwd().resolve()\n", + "candidates = [cwd, cwd.parent, cwd / \"baseline\" / \"ngb\"]\n", + "NGB_ROOT_DIR = next((p for p in candidates if (p / \"configs\" / \"v4_one_head.yaml\").is_file()), None)\n", + "if NGB_ROOT_DIR is None:\n", + " raise FileNotFoundError(\"Run from baseline/ngb or the repository root\")\n", + "RUNTIME_SRC = NGB_ROOT_DIR.parent / \"nanogpt_one_head\" / \"src\"\n", + "if str(RUNTIME_SRC) not in sys.path:\n", + " sys.path.insert(0, str(RUNTIME_SRC))\n", + "\n", + "from rg_nanogpt_one_head import (\n", + " OPTIMIZER_COLORS, OPTIMIZER_LABELS, SUPPORTED_OPTIMIZERS,\n", + " discover_matched_complete_seeds, final_test_summary, load_config,\n", + " load_epoch_metrics, load_spectral_summary, load_test_results, mean_ci95,\n", + " run_slug,\n", + ")\n", + "from rg_nanogpt_one_head.analysis import summarize_by_epoch\n", + "pd.set_option(\"display.max_rows\", None)\n", + "pd.set_option(\"display.max_columns\", None)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "941b7dc3", + "metadata": {}, + "outputs": [], + "source": [ + "v4_cfg = load_config(NGB_ROOT_DIR / V4_CONFIG)\n", + "roots = {\n", + " \"v3\": Path(V3_RESULTS_ROOT),\n", + " \"v4\": Path(NGB_STORAGE_ROOT) / \"results\" / run_slug(v4_cfg),\n", + "}\n", + "optimizers = tuple(SUPPORTED_OPTIMIZERS)\n", + "available = {name: set(discover_matched_complete_seeds(root, optimizers=optimizers)) for name, root in roots.items()}\n", + "seeds = (\n", + " tuple(int(v.strip()) for v in SEEDS.split(\",\") if v.strip())\n", + " if SEEDS.strip()\n", + " else tuple(sorted(set.intersection(*available.values())))\n", + ")\n", + "if not seeds:\n", + " raise RuntimeError(f\"No matched v3/v4 complete seeds: {available}\")\n", + "print(\"matched v3/v4 seeds:\", seeds)\n", + "\n", + "frames_epoch=[]; frames_spectral=[]; frames_test=[]\n", + "for protocol, root in roots.items():\n", + " epoch=load_epoch_metrics(root, optimizers=optimizers, seeds=seeds)\n", + " spectral=load_spectral_summary(root, optimizers=optimizers, seeds=seeds)\n", + " test=load_test_results(root, optimizers=optimizers, seeds=seeds)\n", + " for frame in (epoch, spectral, test): frame.insert(0, \"protocol\", protocol)\n", + " frames_epoch.append(epoch); frames_spectral.append(spectral); frames_test.append(test)\n", + "epoch_metrics=pd.concat(frames_epoch, ignore_index=True)\n", + "spectral_summary=pd.concat(frames_spectral, ignore_index=True)\n", + "test_results=pd.concat(frames_test, ignore_index=True)\n", + "plot_root=Path(NGB_STORAGE_ROOT)/\"plots\"/\"v3_vs_v4_one_head\"\n", + "plot_root.mkdir(parents=True, exist_ok=True)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f73fbc93", + "metadata": {}, + "outputs": [], + "source": [ + "parts=[]\n", + "for protocol, frame in test_results.groupby(\"protocol\"):\n", + " part=final_test_summary(frame); part.insert(0,\"protocol\",protocol); parts.append(part)\n", + "summary=pd.concat(parts, ignore_index=True)\n", + "summary.to_csv(plot_root/\"v3_v4_summary_95ci.csv\", index=False)\n", + "display(summary.sort_values([\"checkpoint\",\"metric\",\"optimizer\",\"protocol\"]))\n", + "\n", + "rows=[]\n", + "for optimizer in optimizers:\n", + " for checkpoint in (\"final\",\"validation_selected\"):\n", + " selected=test_results[(test_results[\"optimizer\"]==optimizer)&(test_results[\"checkpoint\"]==checkpoint)]\n", + " for metric in (\"test_loss\",\"test_accuracy\",\"test_bleu\"):\n", + " v4=selected[selected[\"protocol\"]==\"v4\"][[\"seed\",metric]].rename(columns={metric:\"v4\"})\n", + " v3=selected[selected[\"protocol\"]==\"v3\"][[\"seed\",metric]].rename(columns={metric:\"v3\"})\n", + " paired=v4.merge(v3,on=\"seed\",validate=\"one_to_one\")\n", + " stats=mean_ci95(paired[\"v4\"]-paired[\"v3\"])\n", + " rows.append({\"optimizer\":optimizer,\"optimizer_label\":OPTIMIZER_LABELS[optimizer],\"checkpoint\":checkpoint,\"metric\":metric,\"contrast\":\"v4 - v3\",**stats})\n", + "paired=pd.DataFrame(rows)\n", + "paired.to_csv(plot_root/\"paired_v4_minus_v3_95ci.csv\",index=False)\n", + "display(paired.sort_values([\"checkpoint\",\"metric\",\"optimizer\"]))\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25032465", + "metadata": {}, + "outputs": [], + "source": [ + "styles={\"v3\":\":\",\"v4\":\"-\"}\n", + "for optimizer in optimizers:\n", + " for metric in (\"val_loss\",\"val_accuracy\",\"test_loss\",\"test_accuracy\"):\n", + " fig,ax=plt.subplots(figsize=(9,5))\n", + " for protocol in (\"v3\",\"v4\"):\n", + " subset=epoch_metrics[(epoch_metrics[\"protocol\"]==protocol)&(epoch_metrics[\"optimizer\"]==optimizer)]\n", + " summary_curve=summarize_by_epoch(subset,metric,x=\"nominal_epoch\",group=(\"protocol\",\"optimizer\"))\n", + " ax.plot(summary_curve[\"nominal_epoch\"],summary_curve[\"mean\"],color=OPTIMIZER_COLORS[optimizer],linestyle=styles[protocol],linewidth=2.2,label=protocol)\n", + " ax.fill_between(summary_curve[\"nominal_epoch\"],summary_curve[\"ci95_lower\"],summary_curve[\"ci95_upper\"],color=OPTIMIZER_COLORS[optimizer],alpha=0.10)\n", + " ax.set(xlabel=\"Corpus-equivalent epoch\",ylabel=metric,title=f\"{OPTIMIZER_LABELS[optimizer]}: v3 versus v4\")\n", + " ax.grid(alpha=0.25); ax.legend(frameon=False); fig.tight_layout()\n", + " fig.savefig(plot_root/f\"{optimizer}_{metric}.png\",dpi=170,bbox_inches=\"tight\")\n", + " plt.show()\n", + "\n", + "for optimizer in optimizers:\n", + " fig,ax=plt.subplots(figsize=(9,5))\n", + " for protocol in (\"v3\",\"v4\"):\n", + " subset=spectral_summary[(spectral_summary[\"protocol\"]==protocol)&(spectral_summary[\"optimizer\"]==optimizer)]\n", + " summary_curve=summarize_by_epoch(subset,\"alpha_median\",x=\"epoch\",group=(\"protocol\",\"optimizer\"))\n", + " ax.plot(summary_curve[\"epoch\"],summary_curve[\"mean\"],color=OPTIMIZER_COLORS[optimizer],linestyle=styles[protocol],linewidth=2.2,label=protocol)\n", + " ax.fill_between(summary_curve[\"epoch\"],summary_curve[\"ci95_lower\"],summary_curve[\"ci95_upper\"],color=OPTIMIZER_COLORS[optimizer],alpha=0.10)\n", + " ax.axhline(2.0,color=\"black\",linestyle=\"--\",linewidth=1.0,label=\"alpha = 2\")\n", + " ax.set(xlabel=\"Corpus-equivalent epoch\",ylabel=\"Median alpha\",title=f\"{OPTIMIZER_LABELS[optimizer]}: v3 versus v4 spectral flow\")\n", + " ax.grid(alpha=0.25); ax.legend(frameon=False); fig.tight_layout()\n", + " fig.savefig(plot_root/f\"{optimizer}_alpha_median.png\",dpi=170,bbox_inches=\"tight\")\n", + " plt.show()\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (active conda environment)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/baseline/ngb/tests/__init__.py b/baseline/ngb/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/baseline/ngb/tests/test_ngb.py b/baseline/ngb/tests/test_ngb.py new file mode 100644 index 0000000..42168a0 --- /dev/null +++ b/baseline/ngb/tests/test_ngb.py @@ -0,0 +1,169 @@ +from __future__ import annotations + +import ast +import json +import math +from pathlib import Path +import sys + +import nbformat +import pandas as pd +import pytest + +NGB_ROOT = Path(__file__).resolve().parents[1] +BASELINE_ROOT = NGB_ROOT.parent +RUNTIME_SRC = BASELINE_ROOT / "nanogpt_one_head" / "src" +if str(RUNTIME_SRC) not in sys.path: + sys.path.insert(0, str(RUNTIME_SRC)) + +from rg_nanogpt_one_head import ( # noqa: E402 + GPT, + GPTConfig, + expected_transformer_matrix_count, + final_test_summary, + load_config, + paired_test_differences, + roots, + run_slug, +) +from rg_nanogpt_one_head.model import transformer_matrix_items # noqa: E402 + + +def _config(name: str) -> dict: + return load_config(NGB_ROOT / "configs" / name) + + +def test_v4_protocols_are_separate_tuned_two_epoch_experiments() -> None: + one = _config("v4_one_head.yaml") + four = _config("v4_small_4x4.yaml") + + assert one["protocol"]["version"] == 4 + assert four["protocol"]["version"] == 4 + assert run_slug(one) == "v4_one_head" + assert run_slug(four) == "v4_small_4x4" + assert one["training"]["target_epochs"] == 2.0 + assert four["training"]["target_epochs"] == 2.0 + assert one["training"]["epoch_interval"] == 0.25 + assert one["training"]["eval_interval_steps"] == 500 + + assert one["optimizer_profiles"]["sgd_momentum"]["min_learning_rate"] == 5e-4 + assert one["optimizer_profiles"]["adamw"]["learning_rate"] == 3e-4 + assert one["optimizer_profiles"]["adamw"]["warmup_fraction"] == 0.025 + assert one["optimizer_profiles"]["muon"]["matrix_learning_rate"] == 0.01 + assert one["optimizer_profiles"]["muon"]["aux_learning_rate"] == 2e-4 + resolved = roots(one) + assert resolved["root"] == Path("/tmp/rg-ngb") + assert resolved["data"] == Path("/tmp/rg-nanogpt-one-head/data") + assert resolved["results"] == Path("/tmp/rg-ngb/results/v4_one_head") + + +def test_one_head_and_four_by_four_parameter_inventories() -> None: + one_cfg = _config("v4_one_head.yaml") + four_cfg = _config("v4_small_4x4.yaml") + one = GPT(GPTConfig(**one_cfg["model"])) + four = GPT(GPTConfig(**four_cfg["model"])) + + assert one.parameter_count() == 6_662_656 + assert four.parameter_count() == 7_253_248 + assert len(transformer_matrix_items(one)) == 6 + assert len(transformer_matrix_items(four)) == 24 + assert expected_transformer_matrix_count(one_cfg) == 6 + assert expected_transformer_matrix_count(four_cfg) == 24 + assert sum(item[3].numel() for item in transformer_matrix_items(four)) == 786_432 + + +def test_perplexity_interval_is_exponentiated_from_loss_space() -> None: + frame = pd.DataFrame( + { + "optimizer": ["adamw"] * 3, + "checkpoint": ["final"] * 3, + "test_loss": [5.0, 6.0, 7.0], + "test_perplexity": [math.exp(5.0), math.exp(6.0), math.exp(7.0)], + "test_accuracy": [0.1, 0.2, 0.3], + "test_bleu": [0.0, 0.1, 0.2], + } + ) + summary = final_test_summary(frame) + loss = summary[summary["metric"].eq("test_loss")].iloc[0] + perplexity = summary[summary["metric"].eq("test_perplexity")].iloc[0] + assert perplexity["interval_method"] == "exp_test_loss_student_t" + assert perplexity["mean"] == pytest.approx(math.exp(loss["mean"])) + assert perplexity["ci95_lower"] == pytest.approx(math.exp(loss["ci95_lower"])) + assert perplexity["ci95_lower"] > 0.0 + + +def test_paired_optimizer_contrasts_use_matched_seeds() -> None: + rows = [] + for optimizer, offset in (("sgd_momentum", 0.0), ("adamw", -0.2), ("muon", -0.1)): + for seed, base in ((11, 6.0), (13, 6.2), (17, 6.4)): + rows.append( + { + "optimizer": optimizer, + "checkpoint": "final", + "seed": seed, + "test_loss": base + offset, + "test_perplexity": math.exp(base + offset), + "test_accuracy": 0.1 - offset, + "test_bleu": 0.0, + } + ) + contrasts = paired_test_differences(pd.DataFrame(rows)) + row = contrasts[ + contrasts["metric"].eq("test_loss") + & contrasts["left_optimizer"].eq("sgd_momentum") + & contrasts["right_optimizer"].eq("adamw") + ].iloc[0] + assert row["n"] == 3 + assert row["mean"] == pytest.approx(0.2) + + +def test_ngb_notebooks_are_valid_python3_papermill_entrypoints() -> None: + paths = sorted( + path + for path in (NGB_ROOT / "notebooks").glob("*.ipynb") + if not path.name.endswith(".out.ipynb") + ) + assert [path.name for path in paths] == [ + "01_run_v4_one_head.ipynb", + "02_compare_v4_one_head.ipynb", + "03_run_v4_small_4x4.ipynb", + "04_compare_v4_small_4x4.ipynb", + "05_compare_v4_architectures.ipynb", + "06_compare_v3_v4_one_head.ipynb", + ] + for path in paths: + notebook = nbformat.read(path, as_version=4) + nbformat.validate(notebook) + assert notebook.metadata.kernelspec.name == "python3" + parameter_cells = [ + cell + for cell in notebook.cells + if "parameters" in cell.get("metadata", {}).get("tags", []) + ] + assert len(parameter_cells) == 1 + for cell in notebook.cells: + if cell.cell_type == "code": + ast.parse(cell.source, filename=str(path)) + + +def test_ngb_contains_no_home_or_wrapper_defaults() -> None: + forbidden = ( + "$HOME", + "${HOME}", + "Path.home(", + ".expanduser(", + "/home/", + "~/", + "scripts/setup_mac.sh", + ) + this_file = Path(__file__).resolve() + for path in NGB_ROOT.rglob("*"): + if ( + not path.is_file() + or path.suffix in {".pyc"} + or path.resolve() == this_file + ): + continue + text = path.read_text(encoding="utf-8") + for marker in forbidden: + assert marker not in text, f"{marker!r} found in {path}" From 613c7208fcd93680f377a19c9d32d573826d11f4 Mon Sep 17 00:00:00 2001 From: Charles Martin Date: Tue, 11 Aug 2026 08:52:11 -0700 Subject: [PATCH 17/19] Remove temporary NGB materialization workflow --- .github/workflows/materialize-ngb-v4.yml | 262 ----------------------- 1 file changed, 262 deletions(-) delete mode 100644 .github/workflows/materialize-ngb-v4.yml diff --git a/.github/workflows/materialize-ngb-v4.yml b/.github/workflows/materialize-ngb-v4.yml deleted file mode 100644 index 3e014d5..0000000 --- a/.github/workflows/materialize-ngb-v4.yml +++ /dev/null @@ -1,262 +0,0 @@ -name: Materialize NGB v4 protocol - -on: - push: - branches: - - agent/ngb-v4-protocol - paths: - - .github/workflows/materialize-ngb-v4.yml - -permissions: - contents: write - -jobs: - materialize-test-and-publish: - runs-on: ubuntu-latest - timeout-minutes: 40 - steps: - - uses: actions/checkout@v4 - with: - ref: agent/ngb-v4-protocol - fetch-depth: 0 - - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - cache: pip - cache-dependency-path: baseline/nanogpt_one_head/pyproject.toml - - - name: Materialize NGB v4 files and patch shared runtime - shell: bash - run: | - set -euo pipefail - python - <<'PY' - from __future__ import annotations - - import base64 - import gzip - import json - from pathlib import Path - import re - - repo = Path.cwd() - - def decode_parts(pattern: str) -> dict[str, str]: - parts = sorted(repo.glob(pattern)) - if not parts: - raise RuntimeError(f"no payload parts matched {pattern}") - payload = "".join(part.read_text(encoding="utf-8").strip() for part in parts) - decoded = gzip.decompress(base64.b64decode(payload)).decode("utf-8") - mapping = json.loads(decoded) - if not isinstance(mapping, dict): - raise RuntimeError(f"payload {pattern} is not a mapping") - return {str(path): str(content) for path, content in mapping.items()} - - files = decode_parts(".github/ngb_payload.part*") - files.update(decode_parts(".github/ngb_delta.part*")) - for relative, content in files.items(): - path = repo / relative - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content, encoding="utf-8") - - def replace_exact(path: Path, old: str, new: str, expected: int = 1) -> None: - text = path.read_text(encoding="utf-8") - count = text.count(old) - if count != expected: - raise RuntimeError( - f"{path}: expected {expected} occurrences of {old!r}, found {count}" - ) - path.write_text(text.replace(old, new), encoding="utf-8") - - runtime = repo / "baseline/nanogpt_one_head/src/rg_nanogpt_one_head" - - training = runtime / "training.py" - replace_exact(training, "resolved = roots()", "resolved = roots(cfg)", expected=2) - - data = runtime / "data.py" - data_text = data.read_text(encoding="utf-8") - stale_data_guidance = ( - ' "data preparation requires datasets and tiktoken; "\n' - ' "run scripts/setup_mac.sh"' - ) - if stale_data_guidance in data_text: - data_text = data_text.replace( - stale_data_guidance, - ' "data preparation requires datasets and tiktoken; "\n' - ' "install dependencies into the active conda environment"', - 1, - ) - if "active conda environment" not in data_text: - raise RuntimeError("data.py has no conda-native dependency guidance") - if data_text.count('roots()["data"]') != 1: - raise RuntimeError("could not locate data.py default root resolution") - data.write_text( - data_text.replace('roots()["data"]', 'roots(cfg)["data"]', 1), - encoding="utf-8", - ) - - spectral = runtime / "spectral.py" - replace_exact( - spectral, - '"""CPU-only Linear view of the six one-block transformer matrices."""', - '"""CPU-only Linear view of all transformer matrices."""', - ) - replace_exact( - spectral, - ' "WeightWatcher is required; run scripts/setup_mac.sh"', - ' "WeightWatcher is required; install baseline/nanogpt_one_head "\n' - ' "in the active conda environment"', - ) - - workflow = repo / ".github/workflows/baseline-tests.yml" - workflow_text = workflow.read_text(encoding="utf-8") - needle = ( - " - name: Run one-head tests\n" - " env:\n" - " PYTHONPATH: baseline/nanogpt_one_head/src\n" - " MPLBACKEND: Agg\n" - " run: pytest -q baseline/nanogpt_one_head/tests\n" - ) - addition = needle + ( - " - name: Run NGB v4 tests\n" - " env:\n" - " PYTHONPATH: baseline/nanogpt_one_head/src\n" - " MPLBACKEND: Agg\n" - " run: pytest -q baseline/ngb/tests\n" - ) - if workflow_text.count(needle) != 1: - raise RuntimeError("could not locate one-head test step") - workflow.write_text(workflow_text.replace(needle, addition), encoding="utf-8") - - baseline_readme = repo / "baseline/README.md" - readme = baseline_readme.read_text(encoding="utf-8") - one_head_row = next( - line for line in readme.splitlines() - if "**One-head nanoGPT / FineWeb-Edu**" in line - ) - ngb_row = ( - "| **NGB v4 / FineWeb-Edu** | Same pinned document-disjoint corpus | " - "Separate 1×1 control and 4-block/4-head width-128 model | " - "Two-epoch tuned SGD, AdamW, and Muon protocols | " - "[`ngb/README.md`](ngb/README.md) |" - ) - if ngb_row not in readme: - readme = readme.replace(one_head_row, one_head_row + "\n" + ngb_row, 1) - - environment = "\n".join( - [ - "## Environment and runtime paths", - "", - "Use the currently active conda environment; do not create a repository venv.", - "From the repository root:", - "", - "```bash", - "python -m pip install -e './baseline[experiment]'", - "python -m pip install -e './baseline/nanogpt_one_head[dev]'", - "```", - "", - "All local data and long-running experiment artifacts use explicit `/tmp` roots:", - "", - "```bash", - "export RG_BASELINE_DATA_DIR=/tmp/rg-optimizer-data", - "export RG_BASELINE_RUN_ROOT=/tmp/rg-optimizer-runs", - "export RG_NANOGPT_ONE_HEAD_ROOT=/tmp/rg-nanogpt-one-head", - "export RG_NGB_ROOT=/tmp/rg-ngb", - "export RG_NGB_DATA_ROOT=/tmp/rg-nanogpt-one-head/data", - "```", - "", - "NGB v4 keeps its results under `/tmp/rg-ngb/results/` and reuses the", - "verified FineWeb-Edu token cache under `/tmp/rg-nanogpt-one-head/data`.", - "", - "", - ] - ) - readme, substitutions = re.subn( - r"## Environment and persistent paths\n.*?(?=## 1\. MNIST / MLP3)", - environment, - readme, - count=1, - flags=re.S, - ) - if substitutions != 1: - raise RuntimeError("could not replace baseline environment section") - for forbidden in ("$HOME", "${HOME}", "Path.home(", ".expanduser(", "/home/", "~/"): - if forbidden in readme: - raise RuntimeError( - f"baseline/README.md still contains forbidden path marker {forbidden!r}" - ) - baseline_readme.write_text(readme, encoding="utf-8") - - required = { - "baseline/ngb/README.md", - "baseline/ngb/QUALIFICATION.md", - "baseline/ngb/configs/v4_one_head.yaml", - "baseline/ngb/configs/v4_small_4x4.yaml", - "baseline/ngb/notebooks/01_run_v4_one_head.ipynb", - "baseline/ngb/notebooks/02_compare_v4_one_head.ipynb", - "baseline/ngb/notebooks/03_run_v4_small_4x4.ipynb", - "baseline/ngb/notebooks/04_compare_v4_small_4x4.ipynb", - "baseline/ngb/notebooks/05_compare_v4_architectures.ipynb", - "baseline/ngb/notebooks/06_compare_v3_v4_one_head.ipynb", - "baseline/ngb/tests/test_ngb.py", - } - missing = sorted(path for path in required if not (repo / path).is_file()) - if missing: - raise RuntimeError(f"materialization missed required files: {missing}") - print(f"materialized {len(files)} files; required NGB inventory is complete") - PY - - git diff --check - python -m compileall -q \ - baseline/nanogpt_one_head/src \ - baseline/nanogpt_one_head/tests \ - baseline/ngb/tests - python - <<'PY' - import ast - import json - from pathlib import Path - - paths = sorted(Path("baseline/ngb/notebooks").glob("*.ipynb")) - if len(paths) != 6: - raise RuntimeError(f"expected six NGB notebooks, found {len(paths)}") - for path in paths: - notebook = json.loads(path.read_text(encoding="utf-8")) - if notebook.get("metadata", {}).get("kernelspec", {}).get("name") != "python3": - raise RuntimeError(f"{path} does not use the python3 conda kernel") - for index, cell in enumerate(notebook.get("cells", [])): - if cell.get("cell_type") != "code": - continue - source = cell.get("source", "") - if isinstance(source, list): - source = "".join(source) - ast.parse(source, filename=f"{path}:cell-{index}") - print("validated NGB notebook JSON and Python syntax") - PY - - - name: Install CPU test environment - run: | - python -m pip install --upgrade pip - python -m pip install torch --index-url https://download.pytorch.org/whl/cpu - python -m pip install -e './baseline/nanogpt_one_head[dev]' - python -m pip check - - - name: Run shared one-head and NGB v4 tests - env: - PYTHONPATH: baseline/nanogpt_one_head/src - MPLBACKEND: Agg - run: | - pytest -q baseline/nanogpt_one_head/tests baseline/ngb/tests - - - name: Commit tested NGB v4 protocol and remove staging files - shell: bash - run: | - set -euo pipefail - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git rm -f \ - .github/ngb_payload.part* \ - .github/ngb_delta.part* \ - .github/workflows/materialize-ngb-v4.yml - git add -A - git commit -m 'Add NGB v4 nanoGPT protocols' - git push origin HEAD:agent/ngb-v4-protocol From f45d7f137c2fbf1f909d30febb63974f8992405e Mon Sep 17 00:00:00 2001 From: Charles Martin Date: Tue, 11 Aug 2026 08:52:24 -0700 Subject: [PATCH 18/19] Remove temporary NGB finalization workflow --- .github/workflows/finalize-ngb-v4.yml | 127 -------------------------- 1 file changed, 127 deletions(-) delete mode 100644 .github/workflows/finalize-ngb-v4.yml diff --git a/.github/workflows/finalize-ngb-v4.yml b/.github/workflows/finalize-ngb-v4.yml deleted file mode 100644 index 43222dc..0000000 --- a/.github/workflows/finalize-ngb-v4.yml +++ /dev/null @@ -1,127 +0,0 @@ -name: Finalize NGB v4 protocol - -on: - push: - branches: - - agent/ngb-v4-protocol - paths: - - .github/workflows/finalize-ngb-v4.yml - -permissions: - contents: write - -jobs: - materialize-test-and-publish: - runs-on: ubuntu-latest - timeout-minutes: 40 - steps: - - uses: actions/checkout@v4 - with: - ref: agent/ngb-v4-protocol - fetch-depth: 0 - - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - cache: pip - cache-dependency-path: baseline/nanogpt_one_head/pyproject.toml - - - name: Reuse reviewed materializer - shell: bash - run: | - set -euo pipefail - awk ' - /- name: Materialize NGB v4 files and patch shared runtime/ { - section=1 - next - } - section && /run: \|/ { capture=1; next } - capture && /^ - name:/ { exit } - capture { sub(/^ /, ""); print } - ' .github/workflows/materialize-ngb-v4.yml > /tmp/materialize-ngb-v4.sh - bash /tmp/materialize-ngb-v4.sh - - - name: Repair strict synthetic manifest and notebook inventory fixtures - shell: bash - run: | - set -euo pipefail - python - <<'PY' - from pathlib import Path - - completion_test = Path( - 'baseline/nanogpt_one_head/tests/test_completion.py' - ) - text = completion_test.read_text(encoding='utf-8') - needle = ( - ' "max_steps": total_steps,\n' - ' "warmup_steps": warmup_steps(profile, total_steps),\n' - ) - replacement = ( - ' "max_steps": total_steps,\n' - ' "model": cfg["model"],\n' - ' "warmup_steps": warmup_steps(profile, total_steps),\n' - ) - if text.count(needle) != 1: - raise RuntimeError('unexpected synthetic manifest fixture') - completion_test.write_text( - text.replace(needle, replacement, 1), - encoding='utf-8', - ) - - notebook_test = Path( - 'baseline/nanogpt_one_head/tests/test_one_head.py' - ) - text = notebook_test.read_text(encoding='utf-8') - needle = ( - ' notebook_paths = sorted(' - '(EXPERIMENT_ROOT / "notebooks").glob("*.ipynb"))\n' - ) - replacement = ( - ' notebook_paths = sorted(\n' - ' path\n' - ' for path in (EXPERIMENT_ROOT / "notebooks").glob("*.ipynb")\n' - ' if not path.name.endswith(".out.ipynb")\n' - ' )\n' - ) - if text.count(needle) != 1: - raise RuntimeError('unexpected notebook inventory fixture') - notebook_test.write_text( - text.replace(needle, replacement, 1), - encoding='utf-8', - ) - PY - - git diff --check - python -m compileall -q \ - baseline/nanogpt_one_head/src \ - baseline/nanogpt_one_head/tests \ - baseline/ngb/tests - - - name: Install CPU test environment - run: | - python -m pip install --upgrade pip - python -m pip install torch --index-url https://download.pytorch.org/whl/cpu - python -m pip install -e './baseline/nanogpt_one_head[dev]' - python -m pip check - - - name: Run shared one-head and NGB v4 tests - env: - PYTHONPATH: baseline/nanogpt_one_head/src - MPLBACKEND: Agg - run: | - pytest -q baseline/nanogpt_one_head/tests baseline/ngb/tests - - - name: Commit tested protocol and remove temporary staging - shell: bash - run: | - set -euo pipefail - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git rm -f \ - .github/ngb_payload.part* \ - .github/ngb_delta.part* \ - .github/workflows/materialize-ngb-v4.yml \ - .github/workflows/finalize-ngb-v4.yml - git add -A - git commit -m 'Add NGB v4 nanoGPT protocols' - git push origin HEAD:agent/ngb-v4-protocol From f628e93649b1e29bcf14870e0152af16a7dcde6c Mon Sep 17 00:00:00 2001 From: Charles Martin Date: Tue, 11 Aug 2026 08:52:41 -0700 Subject: [PATCH 19/19] Remove temporary NGB publish workflow --- .github/workflows/publish-ngb-v4.yml | 179 --------------------------- 1 file changed, 179 deletions(-) delete mode 100644 .github/workflows/publish-ngb-v4.yml diff --git a/.github/workflows/publish-ngb-v4.yml b/.github/workflows/publish-ngb-v4.yml deleted file mode 100644 index 4167817..0000000 --- a/.github/workflows/publish-ngb-v4.yml +++ /dev/null @@ -1,179 +0,0 @@ -name: Publish tested NGB v4 protocol - -on: - push: - branches: - - agent/ngb-v4-protocol - paths: - - .github/workflows/publish-ngb-v4.yml - -permissions: - contents: write - -jobs: - publish: - runs-on: ubuntu-latest - timeout-minutes: 40 - steps: - - uses: actions/checkout@v4 - with: - ref: agent/ngb-v4-protocol - fetch-depth: 0 - - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - cache: pip - cache-dependency-path: baseline/nanogpt_one_head/pyproject.toml - - - name: Materialize reviewed NGB implementation - shell: bash - run: | - set -euo pipefail - awk ' - /- name: Materialize NGB v4 files and patch shared runtime/ { - section=1 - next - } - section && /run: \|/ { capture=1; next } - capture && /^ - name:/ { exit } - capture { sub(/^ /, ""); print } - ' .github/workflows/materialize-ngb-v4.yml > /tmp/materialize-ngb-v4.sh - bash /tmp/materialize-ngb-v4.sh - - - name: Apply compatibility fixtures and route NGB tests through existing CI - shell: bash - run: | - set -euo pipefail - git checkout -- .github/workflows/baseline-tests.yml - - python - <<'PY' - from pathlib import Path - - completion_test = Path( - 'baseline/nanogpt_one_head/tests/test_completion.py' - ) - text = completion_test.read_text(encoding='utf-8') - needle = ( - ' "max_steps": total_steps,\n' - ' "warmup_steps": warmup_steps(profile, total_steps),\n' - ) - replacement = ( - ' "max_steps": total_steps,\n' - ' "model": cfg["model"],\n' - ' "warmup_steps": warmup_steps(profile, total_steps),\n' - ) - if text.count(needle) != 1: - raise RuntimeError('unexpected synthetic manifest fixture') - completion_test.write_text( - text.replace(needle, replacement, 1), - encoding='utf-8', - ) - - notebook_test = Path( - 'baseline/nanogpt_one_head/tests/test_one_head.py' - ) - text = notebook_test.read_text(encoding='utf-8') - needle = ( - ' notebook_paths = sorted(' - '(EXPERIMENT_ROOT / "notebooks").glob("*.ipynb"))\n' - ) - replacement = ( - ' notebook_paths = sorted(\n' - ' path\n' - ' for path in (EXPERIMENT_ROOT / "notebooks").glob("*.ipynb")\n' - ' if not path.name.endswith(".out.ipynb")\n' - ' )\n' - ) - if text.count(needle) != 1: - raise RuntimeError('unexpected one-head notebook inventory fixture') - notebook_test.write_text( - text.replace(needle, replacement, 1), - encoding='utf-8', - ) - - ngb_test = Path('baseline/ngb/tests/test_ngb.py') - text = ngb_test.read_text(encoding='utf-8') - needle = ( - ' paths = sorted((NGB_ROOT / "notebooks").glob("*.ipynb"))\n' - ) - replacement = ( - ' paths = sorted(\n' - ' path\n' - ' for path in (NGB_ROOT / "notebooks").glob("*.ipynb")\n' - ' if not path.name.endswith(".out.ipynb")\n' - ' )\n' - ) - if text.count(needle) != 1: - raise RuntimeError('unexpected NGB notebook inventory fixture') - ngb_test.write_text( - text.replace(needle, replacement, 1), - encoding='utf-8', - ) - - bridge = Path( - 'baseline/nanogpt_one_head/tests/test_ngb_v4.py' - ) - bridge.write_text( - '\n'.join( - [ - 'from __future__ import annotations', - '', - 'from pathlib import Path', - 'import runpy', - '', - '_SOURCE = (', - ' Path(__file__).resolve().parents[2]', - ' / "ngb"', - ' / "tests"', - ' / "test_ngb.py"', - ')', - '_NAMESPACE = runpy.run_path(str(_SOURCE))', - 'for _name, _value in _NAMESPACE.items():', - ' if _name.startswith("test_"):', - ' globals()[_name] = _value', - '', - ] - ), - encoding='utf-8', - ) - PY - - git diff --check - python -m compileall -q \ - baseline/nanogpt_one_head/src \ - baseline/nanogpt_one_head/tests \ - baseline/ngb/tests - - - name: Install CPU test environment - run: | - python -m pip install --upgrade pip - python -m pip install torch --index-url https://download.pytorch.org/whl/cpu - python -m pip install -e './baseline/nanogpt_one_head[dev]' - python -m pip check - - - name: Run existing CI test target and direct NGB target - env: - PYTHONPATH: baseline/nanogpt_one_head/src - MPLBACKEND: Agg - run: | - pytest -q baseline/nanogpt_one_head/tests - pytest -q baseline/ngb/tests - - - name: Commit and push non-workflow implementation - shell: bash - run: | - set -euo pipefail - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git rm -f \ - .github/ngb_payload.part* \ - .github/ngb_delta.part* - git add -A - if git diff --cached --name-only | grep -q '^.github/workflows/'; then - echo 'refusing to push a workflow-file change' >&2 - git diff --cached --name-only >&2 - exit 2 - fi - git commit -m 'Add NGB v4 nanoGPT protocols' - git push origin HEAD:agent/ngb-v4-protocol